완전탐색 알고리즘

카펫

스크린샷 2019-10-24 오후 11 18 36

해당 문제는 완전탐색 알고리즘의 문제로.. 사실 완전 탐색에 대해서 공부를 한적이 없어서 처음에 어떻게 접근할까 고민하다가 이틀정도 고민 끝에 해답을 찾을 수 있었습니다. 난이도는 그렇게 어려운 편이 아니라고 하지만.. 역시 수포자한테는 힘들었던 문제였습니다.... 풀이를 봐도 전혀 이해를 못하다가 결국 그림을 그리면서 패턴을 찾아서 풀었습니다.

위의 문제에서 접근법을 찾을 수 있는 힌트는 가로의 길이가 세로의 길이보다 크거나 같다는 전제 조건을 캐치하는것과 갈색 벽돌이 빨간색 벽돌을 둘러쌓야하는 것을 이해하였다면 접근방법을 이해하는데 시간이 단축 될 것 입니다.

먼저, 갈색 벽돌의 개수는 카펫의 위, 아래 가로 길이가 같고, 세로 길이 역시 좌, 우가 같기 때문에 (2 * y) + (2 * x) - 4라는 방정식으로 갈색 벽돌의 개수를 구할 수 있습니다. 여기서 - 4는 가로, 세로 길이가 아무리 변해도 카펫의 꼭지점의 개수는 4개로 변하지 않는 불변의 값을 가집니다.

그리고 빨간색 벽돌의 세로 길이는 갈색 벽돌의 세로 길이보다 -2만큼 이고, 가로 길이 역시 갈색 벽돌 -2 정도이기 때문에 (y - 2) * (x - 2) 식을 도출할 수 가 있습니다.

갈색 벽돌과 빨간색 벽돌의 개수를 이용하여 카펫의 가로, 세로의 길이를 구할 수 있었습니다.

JAVA 코드로 다음과 같이 작성 할 수 있습니다.

import java.util.Arrays;

public class Carpet {

    public int[] solution(int brown, int red){

        if(brown < 8 ||brown >5000 && red < 1 || red >2000000) {
            throw new IllegalStateException();
        }

        int[] answer = new int[2];
        // 빨간색 벽돌은 갈색벽돌에 둘러 쌓여 있기 때문에 세로의 길이는 무조건 3이상부터 시작한다.
        // 가로 길이도 마찬가지로 갈색 벽돌이 빨간색 벽돌을 둘러쌓기 위해서는 3이상이여 됩니다.
        A:for (int y = 3; y < 5000; y++) {
            for (int x = y; x < 5000; x++) {
                if(isSameAsBrown(y, x, brown)){
                    if(isSameAsRed(y, x, red)){
                        answer[0] = x;
                        answer[1] = y;

                        break A;
                    }
                }

            }
        }
        return answer;
    }

    // 세로의 길이 : y, 가로의 길이 : x, 갈색 벽돌 개수 : brown 을 argument로 받아서 가로 세로의 길이를 구하기 위한 로직
    public static boolean isSameAsBrown(int y, int x, int brown){

        //위 아래로 가로 길이는 같고, 좌 우 가로 길이는 같기 때문에 갈색 벽돌의 개수는 아래의 식으로 구할 수 있습니다.
        //가로 세로의 길이에 따라서 변하지 않는 점은 결국 꼭지점은 항상 4개이기 때문에 -4를 해주어야 합니다.
       if((((y * 2) + (x * 2)) - 4) == brown){

            return true;
        }

        return false;
    }

    // 마찬가지로 빨산색 벽돌의 수를 가지고 가로 세로의 길이를 구합니다.
    public static boolean isSameAsRed(int y, int x, int red){
        //가로 -2 * 세로 - 2를 하면 빨간색 벽돌의 넓이가 나옵니다.
        if(((y - 2) * (x - 2)) == red){
            return true;
        }
        return false;
    }

    public static void main(String[] args) {

        Carpet carpet = new Carpet();
        System.out.println(Arrays.toString(carpet.solution(10,2)));
        System.out.println(Arrays.toString(carpet.solution(8,1)));
        System.out.println(Arrays.toString(carpet.solution(24,24)));
    }
}
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.Arrays;

import static org.junit.Assert.*;

@RunWith(JUnit4.class)
public class CarpetTest {

    @Test
    public void 완전탐색_카펫_테스트() {
        //given
        Carpet carpet = new Carpet();

        //when
        int[] tesCase1 = carpet.solution(8, 1);
        int[] tesCase2 = carpet.solution(10, 2);
        int[] tesCase3 = carpet.solution(24, 24);

        //then
        assertEquals("[3, 3]", Arrays.toString(tesCase1));
        assertEquals("[4, 3]", Arrays.toString(tesCase2));
        assertEquals("[8, 6]", Arrays.toString(tesCase3));
    }
}

위의 소스코드에서 이중 포문에서 첫번째 포문을 A로 alias를 설정하여 break문으로 해당 포문을 한번에 빠져나갈 수 있는 문법입니다.

A:for (int y = 3; y < 5000; y++) {
    // 비즈니스로직 구현부
} 

처음 써보는건데 유용해서 한번 써봤습니다.

어제 알고리즘 스터디를 진행하면서 카카오 2020 신입 공채로 나온 문제를 풀기로 하였습니다.
카카오 난이도 중에서 가장 정답률이 높았던 문제여서 가벼운 마음으로 도전하였지만... 역시 저에겐 알고리즘이란 거대한 벽은 넘기가 힘들었습니다. 차라리 Hackerrank에서 영어로 된 난이도 중정도 되는 문제가 나은거 같기도하고.... 어쨌든 멍청한 저의 머리로 하루내내 생각하면서 짜보았습니다.

문제 설명

데이터 처리 전문가가 되고 싶은 어피치는 문자열을 압축하는 방법에 대해 공부를 하고 있습니다. 최근에 대량의 데이터 처리를 위한 간단한 비손실 압축 방법에 대해 공부를 하고 있는데, 문자열에서 같은 값이 연속해서 나타나는 것을 그 문자의 개수와 반복되는 값으로 표현하여 더 짧은 문자열로 줄여서 표현하는 알고리즘을 공부하고 있습니다.
간단한 예로 aabbaccc의 경우 2a2ba3c(문자가 반복되지 않아 한번만 나타난 경우 1은 생략함)와 같이 표현할 수 있는데, 이러한 방식은 반복되는 문자가 적은 경우 압축률이 낮다는 단점이 있습니다. 예를 들면, abcabcdede와 같은 문자열은 전혀 압축되지 않습니다. 어피치는 이러한 단점을 해결하기 위해 문자열을 1개 이상의 단위로 잘라서 압축하여 더 짧은 문자열로 표현할 수 있는지 방법을 찾아보려고 합니다.

예를 들어, ababcdcdababcdcd의 경우 문자를 1개 단위로 자르면 전혀 압축되지 않지만, 2개 단위로 잘라서 압축한다면 2ab2cd2ab2cd로 표현할 수 있습니다. 다른 방법으로 8개 단위로 잘라서 압축한다면 2ababcdcd로 표현할 수 있으며, 이때가 가장 짧게 압축하여 표현할 수 있는 방법입니다.

다른 예로, abcabcdede와 같은 경우, 문자를 2개 단위로 잘라서 압축하면 abcabc2de가 되지만, 3개 단위로 자른다면 2abcdede가 되어 3개 단위가 가장 짧은 압축 방법이 됩니다. 이때 3개 단위로 자르고 마지막에 남는 문자열은 그대로 붙여주면 됩니다.

압축할 문자열 s가 매개변수로 주어질 때, 위에 설명한 방법으로 1개 이상 단위로 문자열을 잘라 압축하여 표현한 문자열 중 가장 짧은 것의 길이를 return 하도록 solution 함수를 완성해주세요.

 

 

 

스크린샷 2019-10-09 오전 2 44 42

 

스크린샷 2019-10-09 오전 2 44 54

 

 


## @DataJpaTest

@DataJpaTest 어노테이션은 JPA 관련 테스트 설정만 로드합니다. DataSource의 설정이 정상적인지, JPA를 사용하여 데이터를 제대로 생성, 수정, 삭제하는지 등의 테스트가 가능합니다. 그리고 가장 좋은점은.. 무려 내장형 데이터베이스를 사용하여 실제 데이터베이스를 사용하지 않고 테스트 데이터베이스로 테스트할 수 있는.. 개꿀같은 장점이 있습니다.

@DataJpaTest는 기본적으로 @Entity 어노테이션이 적용된 클래스를 스캔하여 스프링 데이터 JPA 저장소를 구성합니다. 만약 최적하한 별도의 데이터소스를 사용하여 테스트하고 싶다면 기본 설정된 데이터소스를 사용하지 않도록 아래와 같이 설정해도 됩니다.


```java
package kakao;

public class ConvertRightBracket {

    // 올바른 괄호인지 판단을 하는 메소드
    public boolean isRightBracket(String p) {

        int temp = 0;

        for (int i = 0; i < p.length(); i++) {

            if (p.charAt(i) == '(') temp++;
            if (p.charAt(i) == ')') temp--;
            if (temp < 0) return false;

            //String a = "(()())()";
        }
        return true;
    }


    public String convertBracket(String p) {

        //문자열 길이가 0 이거나 ""(빈문자열)이면 "" 리턴
        if (p.equals("") || p.length() == 0)
            return "";

        String u = balancedBracket(p);
        String v = p.substring(u.length());


        StringBuilder sb = new StringBuilder();

        if (isRightBracket(u)) {
            sb.append(u);
            sb.append(convertBracket(v));
            return sb.toString();
        } else {
            sb.append("(");
            sb.append(convertBracket(v));
            sb.append(")");
            sb.append(removeAndReverse(u));
            return sb.toString();
        }
    }


    public String balancedBracket(String w) {

        int temp = 0;

        for (int i = 0; i < w.length(); i++) {

            if (w.charAt(i) == '(') temp++;
            if (w.charAt(i) == ')') temp--;

            if (i >= 1 && temp == 0) return w.substring(0, i + 1);
        }

        return "";
    }

    // 괄호 문자열 맨 앞 뒤 제거 후 남은 문자열 역순으로 나열하기
    public String removeAndReverse(String u) {
        return reverse(u.substring(1, u.length() - 1));
    }


    public String reverse(String u){

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < u.length(); i++) {

            if(u.charAt(i) == '(')
                sb.append(")");

            if(u.charAt(i) == ')')
                sb.append("(");

        }

        return sb.toString();
    }


    public String soluton(String p) {

        if (isRightBracket(p)) return p;
        else return convertBracket(p);

    }

    public static void main(String[] args) {

        ConvertRightBracket c = new ConvertRightBracket();
        String p = "()))((()";
        System.out.println(c.soluton("()))((()"));
        System.out.println(c.soluton(")("));
        System.out.println(c.soluton("()))((()"));
    }
}

 

이 문제는 푸는것보다 문제를 이해하는데 더 많은 어려움을 겪었습니다.  정말 문제설명에서 나온것처럼 시키는대로 잘하면 풀수 있는 문제였는데.. 왜 이렇게 오래걸렸는지 OTL.... 이 문제를 다룬 다른 블로그를 보면 정말 훌륭하게 객체지향적으로 코드를 작성하신 분들이 많습니다. 그걸 보고 나중에 저도 코드를 작성할때 참조 하겠습니다.

 

이상 카카오 2020 문자열 압축.. 리뷰였습니다.

'알고리즘' 카테고리의 다른 글

해시 - 완주하지 못한 마라톤 선수  (0) 2019.10.31
2018년 카카오 블라인드 공채 문제1 - 비밀지도  (0) 2019.10.29
Hackerrank- EqualizeTheArray  (0) 2019.10.28
완전탐색 - 카펫  (0) 2019.10.25
스택과 큐  (0) 2019.10.05

@DataJpaTest

@DataJpaTest 어노테이션은 JPA 관련 테스트 설정만 로드합니다. DataSource의 설정이 정상적인지, JPA를 사용하여 데이터를 제대로 생성, 수정, 삭제하는지 등의 테스트가 가능합니다. 그리고 가장 좋은점은.. 무려 내장형 데이터베이스를 사용하여 실제 데이터베이스를 사용하지 않고 테스트 데이터베이스로 테스트할 수 있는.. 개꿀같은 장점이 있습니다.

@DataJpaTest는 기본적으로 @Entity 어노테이션이 적용된 클래스를 스캔하여 스프링 데이터 JPA 저장소를 구성합니다. 만약 최적화한 별도의 데이터소스를 사용하여 테스트하고 싶다면 기본 설정된 데이터소스를 사용하지 않도록 아래와 같이 설정해도 됩니다.

RunWith(SpringRunner.class)
@DataJpaTest
@ActiveProfiles("...")
@AutoConfigureTestDatabase(replace = 
@AutoConfigureTestDatabase.Replace.NONE)
public class JpaTest {

}

@AutoConfigureTestDatabase 어노테이션의 기본 설정값인 Replace.Any를 사용하면 기본적으로 내장된 임베디드 데이터베이스를 사용합니다. 위의 코드에서 Replace.NONE로 설정하면 @ActiveProfiles에 설정한 프로파일 환경값에 따라 데이터 소스가 적용됩니다. yml 파일에서 프로퍼티 설정을 spring.test.database.replace: NONE으로 변경하면 됩니다.

@DataJpaTest는 기본적으로 @Transactional 어노테이션을 포함하고 있습니다. 그래서 테스트가 완료되면 자동으로 롤백하기 때문에 직접 선언적 트렌젝션 어노테이션을 달아줄 필요가 없습니다.

만약에 @Transactional 기능이 필요하지 않다면 아래와 같이 줄 수 있습니다.

@RunWith(SpringRunner.class)
@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class JpaTest {
    ...
}

그리고 어떤 테스트 데이터베이스를 사용할 것인지도 선택 할 수 있습니다. spring.test.database.connection: H2와 같이 프로퍼티를 설정하는 방법과 @AutoConfigureTestDatabase(connection = H2) 어노테이션으로 설정하는 방법이 있습니다.

@DataJpaTest에서 EntityManager의 대체제로 만들어진 테스트용 TestEntityManager를 사용하면 persist, flush, find 등과 같은 기본적인 JPA테스트가 가능합니다. 아래 간단하게 도메인 객체에 대한 JPA 테스트를 수행할 수 있게 Book 클래스에 JPA 관련 어노테이션을 추가하고 BookRepository 인터페이스를 생성하였습니다.

Entity

package com.jun.jpacommunity.domain;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Book {

    @Id
    @GeneratedValue
    private Integer idx;

    private String title;

    private LocalDateTime publishedAt;

    @Builder
    public Book(String title, LocalDateTime publishedAt) {
        this.title = title;
        this.publishedAt = publishedAt;
    }
}

Repository

package com.jun.jpacommunity.repository;

import com.jun.jpacommunity.domain.Book;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Integer> {

}

Test 코드

package com.jun.jpacommunity.repository;


import com.jun.jpacommunity.domain.Board;
import com.jun.jpacommunity.domain.Book;
import com.jun.jpacommunity.domain.Member;
import org.hamcrest.collection.IsEmptyCollection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.junit4.SpringRunner;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
public class JpaTest {

    private final static String BOOT_TEST_TITLE = "Spring Boot Test Book";

    @Autowired
    private TestEntityManager testEntityManager;

    @Autowired
    private BookRepository bookRepository;

    @Test
    public void Book_저장하기_테스트() throws Exception {
        //given
        Book book1 = Book.builder()
                .title("타짜")
                .publishedAt(LocalDateTime.now())
                .build();

        //when
        testEntityManager.persist(book1);

        //then
        assertEquals(book1, testEntityManager.find(Book.class, book1.getIdx()));

     }

     @Test
     public void BookList_저장하고_검색_테스트() throws Exception {

         //given
         Book book1 = Book.builder()
                 .title(BOOT_TEST_TITLE +"1")
                 .publishedAt(LocalDateTime.now())
                 .build();

         testEntityManager.persist(book1);

         Book book2 = Book.builder()
                 .title(BOOT_TEST_TITLE +"2")
                 .publishedAt(LocalDateTime.now())
                 .build();

         testEntityManager.persist(book2);

         Book book3 = Book.builder()
                 .title(BOOT_TEST_TITLE +"3")
                 .publishedAt(LocalDateTime.now())
                 .build();

         testEntityManager.persist(book3);


         //when
         List<Book> bookList = bookRepository.findAll();

         //then
         assertEquals(bookList.size(), 3);
         assertEquals(book1, bookList.get(0));
      }


    @Test
    public void BookList_저장하고_삭제_테스트() throws Exception {

        //given
        Book book1 = Book.builder().title(BOOT_TEST_TITLE + "1").
                publishedAt(LocalDateTime.now()).build();

        testEntityManager.persist(book1);

        Book book2 = Book.builder().title(BOOT_TEST_TITLE + "2").
                publishedAt(LocalDateTime.now()).build();

        testEntityManager.persist(book2);

        //when
        bookRepository.deleteAll();
        //then
        assertThat(bookRepository.findAll(), IsEmptyCollection.empty());
    }
}

요즘에 TDD 방식으로 테스트 하는 습관을 기르기 위해서 인텔리제이에서 제공해주는 라이브 템플릿으로 간단하게 give, when, then으로 테스트 영역을 구분하여 테스트를 수행하였습니다.

  • Book_저장하기_테스트(): testEntityManager로 persist() 기능이 정상 동작되는지 테스트를 수행하였습니다.

  • BookList_저장하고_검색_테스트(): Book 3개를 저장한 후 저장된 Book의 개수가 3개가 맞는지, bookList에 0번째 인덱스에 저장된 book1 객체가 포함 되어있는지 테스트를 수행하였습니다.

  • BookList_저장하고_삭제_테스트(): 저장된 Book 중에서 2개가 제대로 삭제되었는지 테스트를 하였습니다.

참조: https://hyper-cube.io/2017/08/10/spring-boot-test-2/, 처음배우는 스프링 부트2

'SpringFramework > JPA' 카테고리의 다른 글

복합 키와 식별관계 매핑  (0) 2019.10.29
고급매핑 - 상속관계 매핑  (0) 2019.10.28
JPA 다양한 연관관계 매핑  (0) 2019.10.04
JPA 연관관계 매핑기초  (0) 2019.10.04
JPA가 지원하는 엔티티 매핑  (0) 2019.10.04

최근에 자료구조와 함께 배우는 알고리즘 입문(자바편) 책을보고 예전에 대학교 시절에 대충 배웠던 자료구조에 대해서 자세하게 공부할 기회가 생겨서 리뷰를 하게 되었습니다.

확실히 책은 한번 읽는것보다 여러번 읽어보니 그때는 왜 이런걸 못봤을까…하는 생각이 듭니다.
스택과 큐에 대해서 간단하게 코드 리뷰와 이러한 자료구조를 어디에서 사용되는지도 다시 한번 알게된 계기가 되었습니다.

스택(Stack)

스택은 데이터를 일시적으로 저장하기 위해 사용하는 자료구조로, 선입선출(가장 나중에 넣은 데이터를 가장 먼저 꺼내는) 방식입니다. 음… 이걸 읽고 생각나는게… 아침에 출근을 할때 지하철을 타게되면 분명 가장먼저 탔는데 내릴때는 가장 늦게내리는 제 모습이 생각나는군요…묘하게 비슷합니다.

스택의 구조

스크린샷 2019-10-05 오전 1 17 27

맨 처음에 스택이라는 배열에는 Data 1, Data 2, Data 3이 들어왔고, 그 다음으로 Data 4가 들어왔습니다. 이때 스택안으로 자료를 넣는 작업을 Push라고 합니다. 그리고 해당 데이터를 꺼내는 작업을 Pop이라고 합니다. 이때 알아두어야 할 점은 내가 원하는 데이터를 꺼낼수 있는게 아니라 스택에서 가장 후입(늦게 들어온 데이터)된 데이터를 꺼낸다는 점입니다.

스택은 사용자가 구현한 Java 프로그램에서 메서드를 호출하고 실행할 때 프로그램 내부에서는 스택을 사용합니다. 다음 아래와 같이 main메서드를 포함한 총 4개의 메서드로 이루어진 코드로 스택을 살펴보겠습니다.

스택 사용의 예제

void x(){
    System.out.println("x를 호출하였습니다.")
}
void y(){
    System.out.println("y를 호출하였습니다.")
}

void z(){
    x();
    y();
    System.out.println("z를 호출하였습니다.")
}

int main(){
    z();
}

먼저 사용자가 작성한 자바소스 코드는 javac 컴파일러라는 녀석에 의해서 기계어로 번역되고 만약 문법적인 오류가 없다면 해당 소스코드를 class 파일로 만든 후 클래스 로더라는 녀석이 JVM이 관리하고 있는 각각의 메모리 영역에 로딩을 하게 됩니다. 그리고 JVM은 프로그램 흐름이 시작하는 main 메서드를 찾게됩니다.

위의 코드에서 main 메서드를 가장 먼저 찾아서 스택에 Push를 수행합니다. main 메서드는 스코프 안에 정의되어 있는 z 메서드를 호출합니다. 마찬가지로 z메서드에서 x 메서드를 호출하게 됩니다.
그러면 스택에서 main -> z -> x 순으로 Push가 수행되고 x 메서드가 실행종료 되면 다시 z 메서드로 돌아오는데 이때 스택에서는 x 메서드를 Pop하는 작업을 수행하게 됩니다.

z 메서드는 y 메서드를 호출하고, 스택에서는 Push를 수행하여 main -> z -> y 순으로 쌓여 있습니다. y 메서드가 실행종료되면, 다시 z 메서드로 돌아오고 더 이상 실행할 문장이 없이 종료가 되면 스택 영역에서 Pop 작업이 수행됩니다. 그러면 스택 안에서는 더 이상 main 메서드 밖에 남지 않게 됩니다. main 메서드도 실행할 문장이 존재하지 않다면 스택에서 Pop이 이루어지고 프로그램이 종료가 됩니다.

이처럼 스택은 가장 나중에 저장된 데이터가 가장 먼저 인출되는 방식으로 동작합니다.

아래 소스코드는 스택에서 사용할 수 있는 메소드를 책을보고 구현해본 코드입니다.

public class IntStack {

    int max; //스택 용량 총 배열의 크기를 말함
    int ptr; //스택에 쌓여있는 데이터 수를 나타낸다.
    int[] stk; //스택 본체를 의미한다.

    //실행 시 예외: 스택이 비어 있을때 
    public class EmptyIntStackException extends RuntimeException {
        public EmptyIntStackException() {
        }
    }

    //실행 시 예외: 스택이 가득 차 있을때 
    public class OverflowIntStackException extends RuntimeException {
        public OverflowIntStackException() {
        }
    }

    //생성자를 통해서 IntStack 클래스의 맴버변수들을 초기화 수행
    public IntStack(int capacity) {

        this.ptr = 0;
        this.max = capacity;
        try {
            this.stk = new int[max];
        } catch (OutOfMemoryError e) {
            max = 0;
        }
    }
    //스택영역에 데이터를 Push를 수행하는 메서드
    public int push(int x) throws OverflowIntStackException {
        if (ptr >= max) //데이터의 개수가 스택 용량과 크거나 같으면 오버플로우 예외 발생
            throw new OverflowIntStackException();

        return stk[ptr++] = x;
    }

    //스택영역에 데이터를 Pop을 수행하는 메서드 
    public int pop() throws EmptyIntStackException {
        if (ptr <= 0) //데이터의 개수가 0보다 작거나 같으면 EmptyIntStack 예외 발생
            throw new EmptyIntStackException();
        return stk[--ptr];
    }
    //peek은 엿보다라는 뜻으로 스택영역에서 가장 늦게 들어온 데이터를 읽어서 출력하는 메서드
    public int peek() throws EmptyIntStackException {
        if (ptr <= 0) { //마찬가지로 스택이 비어있으면 EmptyIntStack 예외 발생
            throw new EmptyIntStackException();
        }
        return stk[ptr - 1];
    }

    //특정 데이터의 index를 가져오는 메서드
    public int indexOf(int x) {
        for (int i = ptr - 1; i >= 0; i--)
            if (stk[i] == x)
                return i;

        return -1; //해당 데이터가 존재하지 않으면 -1을 리턴
    }

    //스택에 있는 데이터를 비우는 메서드
    public void clear() {
        ptr = 0;
    }
    //스택의 용량을 확인하는 메서드
    public int capacity() {
        return max;
    }
    //스택의 데이터 개수를 확인하는 메서드
    public int size() {
        return ptr;
    }
    //dump 메서드는 가장 먼저 들어온 데이터부터 숱차적으로 읽어서 출력하는 메서드
    public void dump() {
        if (ptr <= 0)
            System.out.println("스택이 비어 있습니다.");
        else {
            for (int i = 0; i < ptr; i++)
                System.out.print(stk[i] + " ");
            System.out.println();
        }
    }
}

큐(Queue)

마지막으로 살펴볼 큐는 스택과 마찬가지로 데이터를 일시적으로 쌓아 놓은 자료구조입니다. 하지만 가장 구별되는 특징은 가장 먼저 넣은 데이터를 가장 먼저 꺼내는 선입선출인 점이 스택과 다릅니다.
예를들어서 대형마트 같은곳에서 계산을 할때에는 대기열 맨앞에 줄을서는 고객이 먼져 계산하고 나가는게 대표적이네요. 어떻게보면 공정하다고 할 수 있는 자료구조라고 생각됩니다…

큐의 구조

스크린샷 2019-10-05 오전 1 17 41

큐에 데이터를 넣는 작업을 인큐(enqueue)라 하고, 데이터를 꺼내는 작업을 디큐(dequeue)라고 합니다.
또 데이터를 꺼내는 쪽을 프런트(front)라 하고, 데이터를 넣는 쪽을 리어(rear)라고 합니다.

위의 그림에서 보듯이 큐는 스택과 마찬가지로 배열을 사용하여 구현할 수 있습니다.
현재 1번 그림은 배열의 프런트부터 5개(a,b,c,d,e)의 데이터가 들어가 있는 모습입니다. 배열의 이름을 que라고 할 경우 que[0]부터 que[4]까지의 데이터가 저장됩니다.

2번 그림은 a를 디큐합니다. 디큐를 하면 프런트의 값이 +1 증가하하게 됩니다.
3번 그림도 b를 디큐하고, 프런트 값이 1 증가하였습니다. 인큐는 시간복잡도가 O(1)이지만 디큐를 하게되면 모든 요소를 하나씩 앞쪽으로 옮겨야 합니다. 이러한 문제때문에 처리의 복잡도는 O(n)이 되며 데이터를 꺼낼 때마다 이런 처리를 하게되면 효율이 떨어지는 단점이 있습니다.

이러한 문제때문에 배열 요소를 앞쪽으로 옮기지 않는 큐를 구현해 보았습니다.,
이러한 큐를 링 버퍼(ring buffer)라고 합니다. 링 버퍼는 그림처럼 배열의 처음과 끝이 연결되었다고 보는 자료구조입니다.

스크린샷 2019-10-05 오전 1 17 49

프런트: 맨 처음 요소의 인덱스
리어: 맨 끝 요소의 인덱스

3개의 데이터(A,B,C)가 차례대로 que[0],que[1],que[2]에 저장 됩니다. 프런트의 값은 0이고 리어 값은 3입니다. 그리고 D라는 데이터를 인큐하면 리어 값이 1만큼 증가합니다.

큐가 완전히 비어있는 상황일 경우 프런트, 리어는 0값을 가지고 있습니다. C라는 데이터를 디큐하게 되면 프런트의 값이 1증가하는것을 볼 수가 있습니다. 이렇게 큐를 구현하면 프런트와 리어 값을 업데이트하며 인큐와 디큐를 수행하기 때문에 앞에서 발생한 요소 이동 문제를 해결할 수 있습니다. 물론 처리복잡도 또한 O(1)입니다.

큐도 마찬가지로 소스코드로 구현해보았습니다.

링 버퍼 구현

public class IntQueue {

    private int max;    //큐의 용량
    private int front;  //첫벉째 요소 커서
    private int rear;   //마지막 요소 커서
    private int num;    //현재 데이터 수
    private int[] que;  //큐 본체

    //스택과 마찬가지로 실행시 예외:큐가 비어있을때 
    public class EmptyIntQueueException extends RuntimeException {
        public EmptyIntQueueException() {
        }
    }
    //싱행시 예외: 큐가 가득 차있을때
    public class OverflowIntQueueException extends RuntimeException {
        public OverflowIntQueueException() {
        }
    }
    //생성자
    public IntQueue(int capacity) {
        num = front = rear = 0;
        this.max = capacity;
        try {
            que = new int[max]; //인자 값으로 큐 본체용 배열을 생성함
        } catch (OutOfMemoryError e) {  //생성할 수 없음
            max = 0;
        }
    }
    //인큐 발생시에 rear 값을 1만큼 증가시킨다. 
    public int enque(int x) throws OverflowIntQueueException {
        if (num >= max)
            throw new OverflowIntQueueException();

        que[rear++] = x;
        num++;
        if (rear == max) //배열의 마지막 인덱스에 데이터가 존재하면 rear 값을 0으로 변경
            rear = 0;
        return x;
    }

    //디큐가 발생하면 프런트 값이 1만큼 증가한다.
    public int deque() throws EmptyIntQueueException {
        if (num <= 0) //큐가 비어있을때는 예외발생
            throw new EmptyIntQueueException();
        int x = que[front++];
        num--;
        if (front == max) //계속해서 디큐가 발생하여 프런트 값이 큐의 끝에 달하면 0으로 변경
            front = 0;
        return x;
    }

    //마찬가지로 큐의 프런트 부분을 읽고 출력하는 메서드
    public int peek() throws EmptyIntQueueException {
        if (num <= 0)
            throw new EmptyIntQueueException();
        return que[front];
    }

    //해당 데이터가 큐의 몇번째 인덱스에 들어있는지 출력하는 메서드
    public int indextOf(int x) {
        for (int i = 0; i < num; i++) {
            int idx = (i + front) % max;
            if (que[idx] == x)
                return idx;
        }
        return -1;
    }

    public void clear() { //큐를 비우는 메서드
        num = front = rear = 0;
    }


    public int capacity() { //큐의 전체크기를 반환
        return max;
    }

    public int size() { //큐에 들어있는 데이터 개수를 반환
        return num;
    }

    public boolean isEmpty() { //큐가 비어있는지 확인하는 메서드
        return num <= 0;
    }

    public boolean isFull() { //큐가 가득 차있는지 확인하는 메서드
        return num >= max;
    }

    //큐의 모든 데이터를 프런트 -> 리어 순으로 출력
    public void dump() {
        if (num <= 0) {
            System.out.println("큐가 비어있습니다.");
        }else {
            for (int i = 0; i < num; i++) {
                int idx = (i + front) % max;
                System.out.print(que[idx] + " ");
            }
            System.out.println();
        }
    }
}

링버퍼는 프런트와 리어부분만 업데이트를 해주기 때문에 인큐가 발생해도 모든 요소들을 옮기지 않아도 되고,만약 n개인 배열에 계속 데이터가 입력되면 가장 최근에 들어온 데이터 n개만 저장하고 오래된 데이터를 버리는 방식입니다. 이러한 링 버퍼를 어디서 사용할까 생각을 해보면서… 스마트 뱅킹을 사용할때 고객들이 최근 카드 결제내역 10건등을 조회하는데 링 버퍼를 이용하면 유용하지 않을까 생각을 해보았습니다.

오늘은 이렇게 자료구조를 대표하는 스택과 큐에 대해서 공부를 하면서 대학교때 대강대강 넘어가면서 보지 못했던 부분을 다시 보게되어서 보람이 있었습니다.

다음번에도 자료구조와 관련해서 다양한 알고리즘에 대해서 리뷰를 진행하겠습니다.

스트림(Stream)

스트림은 자바 8부터 추가된 컬렉션(배열포함)의 저장 요소를 하나씩 참조해서 람다식(험수적 -스타일)으로 처리할 수 있도록 해주는 반복자이다. 스트림에 대해서 저는 전혀 몰랐었고(자랑이다…), 프로젝트를 진행하면서 구글링할때 다른 개발자분이 스트림으로 작성한 코드와 스프링 부트 책의 예제에서 잠깐 보고 말았던 것인데 따로 공부를 하면서 정말 편하게 컬렉션의 요소들을 처리 할 수 있다는게 너무 좋아서 공부를 해봤습니다.

자바7 이전까지는 List 컬렉션에서 요소를 순차적으로 처리하기 위해 Iterator 반복자를 사용하여 처리하곤 했습니다.

ex)

List<String> list = Arrays.asList("홍길동", "임준영", "자바킴");
Iterator<String> iterator = list.iterator();//반복처리를 위해 Iterator 객체를 얻어옴
while(iterator.hasNext()){ // 해당 컬렉션에 요소가 있는지 확인 있으면 true 리턴
    String name = iterator.next(); //요소를 name 변수에 저장
    System.out.println(name); 
}

위의 코드처럼 iterator.hasNext()로 컬렉션 타입에 요소가 있는지 확인하고 꺼내서 처리를 하지만 스트림을 이용하면 다음처럼 구현할 수가 있습니다.

List<String> list = Arrays.asList("홍길동", "임준영", "자바킴");
Stream<String> stream = list.stream(); //컬렉션의 stream()메소드로 스트림 객체를 얻음 
stream.forEach(name -> System.out.println(name)); //해당 메소드로 컬렉션의 요소 출력

스트림 객체의 forEach 메소드의 구현부를 보면 Consumer 함수적 인터페이스 타입의 매개값을 가집니다.
따라서, 컬렉션의 요소를 소비할 코드를 람다식으로 기술할 수 있습니다.

void forEach(Consumer<? super T> action);

스트림을 사용하기전에 함수적 인터페이스와 람다식에 대해서 간단하게 설명하자면 자바 코드를 간결하고 컬렉션의 요소를 필터링하거나 매핑해서 원하는 결과를 쉽게 집계하기 위해 만들어졌고, 이벤트 지향 프로그래밍과 병렬처리를 수행하기 위해 스트림과 마찬가지로 자바 8부터 지원하는 함수적 프로그래밍이라고 생각하면 됩니다.

  • 객체지향 언어 + 함수지향 언어 == 람다식
(s) -> System.out.println(s);  // 매개변수와 실행문으로 구성되어 있습니다.

람다식의 형태는 위의 코드처럼 매개 변수를 가진 코드 블록이지만, 런타임 시에는 구현 객체를 생성합니다.

람다식이 하나의 메소드를 정의하기 때문에 두개 이상의 추상 메소드가 선언된 인터페이스는 익명으로 구현이 불가능합니다. 그렇기 때문에 하나의 추상 메소드가 선언된 인터페이스만이 람다식의 타겟타입이 될 수 있는데, 이러한 인터페이스를 함수적 인터페이스라고 부릅니다.

자바에서 제공되는 표준 API에서 한 개의 추상 메소드를 가지는 인터페이스들은 모두 람다식을 이용해서 익명 구현 객체로 표현이 가능합니다. 자바 8부터는 빈번하게 사용되는 함수적 인터페이스는 java.util.function 표준 API 패키지로 제공합니다. 이 패키지에서 제공하는 함수적 인터페이스의 목적은 메소드 또는 생성자의 매개 타입으로 사용되어 람다식을 대입할 수 있도록 하기 위해서입니다.

스트림의 forEach 메소드의 매개값에 대해서 이해하기 위해서 뭔가를 쓰긴했는데…서론이 쓰잘대 없이 길었네요ㅎㅎ… Consumer는 함수적 인터페이스로 특징은 리턴값이 없는 accept() 메소드를 가지고 있습니다. accept()메소드는 단지 매개값을 소비하는 역할만 하는데. 소비하다는 말은 사용만 할뿐 리턴값이 없는 딱히 의미있는 건 아닙니다.

Consumer<String> consumer = t -> { t를 소비하는 실행문; }; //람다식으로 익명구현 객체 생성

대충 이런식으로 매개값을 소비합니다.

이제 본격적으로 스트림에 대해서 알아보겠습니다. 위의 Iterator를 사용한 코드와 Stream을 사용한 코드를 비교해보면 Stream을 사용하는 것이 훨씬 간결해 보입니다.

스트림의 특징

Stream은 Iterator와 비슷한 역할을 하는 반복자이지만, 람다식으로 요소 처리 코드를 제공하는 점과 내부 반복자를 사용하므로 병렬 처리가 쉽다는 점 그리고 중간 처리와 최종 처리 작업을 수행하는 점에서 많은 차이를 가지고 있습니다.

Stream이 제공하는 대부분의 요소 처리 메소드는 함수적 인터페이스 매개 타입을 가지기 때문에 람다식 또는 메소드 참조를 이용해서 요소 처리 내용을 매개값으로 전달할 수 있습니다.

아래 이해하기 쉬운 좋은 예제가 있습니다.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class LamdaExpressionsExample {

    public static void main(String[] args) {

        List<Student> list = Arrays.asList(
                new Student("임준영",100),
                new Student("노성운",92)
        );

        Stream<Student> stream = list.stream(); //스트림 얻기
        stream.forEach(s -> {//List컬렉션에서 Student를 가져와 람다식의 매개값으로 제공.
            String name = s.getName();
            int score = s.getScore();
            System.out.println("학생이름: " + name + " " + "점수: " + score);
        });
    }
}
public class Student {

    private String name;
    private int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }
} 

스크린샷 2019-10-05 오전 1 11 58

스트림의 강력한 장점 중 하나는 내부 반복자 패턴이다. Iterator는 컬렉션의 요소를 가져오는 것에서부터 처리하는 것까지 모두 개발자가 작성해야 하지만, 스트림은 람다식으로 요소 처리 내용만 전달할 뿐, 반복은 컬렉션 내부에서 일어납니다. 스트림을 이용하면 코드도 간결해지지만, 무엇보다도 요소의 병렬 처리가 컬렉션 내부에서 처리되므로 일석이조의 효과를 누릴수 있습니다 ㅎㅎ.

또 다른 장점은 제가 스트림에 대해서 가장 많은 매력을 느낀점인데…
중간 처리와 최종처리가 가능하다는 점입니다!!!
이게 무슨 소리냐면 스트림은 컬렉션의 요소에 대해 중간 처리와 최종 처리를 수행할 수 있는데, 중간 처리에서는 매핑, 필터링, 정렬을 수행하고 최종처리에는 반복, 카운팅, 평균, 총합 등의 집계처리를 수행합니다.

예를 들어 학생 객체를 요소로 가지는 컬렉션이 있다고 가정해봅시다. 중간 처리에서는 학생의 점수를 뽑아내고, 최정처리에서는 점수의 평균 값을 산출합니다.

스크린샷 2019-10-05 오전 1 12 09

중간처리: 학생의 개별 점수를 뽑아낸다. (Student 객체를 점수로 매핑)

최종처리: 점수의 평균값을 산출한다.(집계)

여윽시…코드로 봐봅시다.

mport java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class MapAndReduceExample {
    public static void main(String[] args) {

        List<Student> studentList = Arrays.asList(
          new Student("임준영", 10),
          new Student("임광빈", 20),
          new Student("박찬욱", 30)
        );

        double avg = studentList.stream()
        .mapToInt(Student::getScore)
        .average()
        .getAsDouble();  //한줄로 평균을 구하는... 스트림의 위엄....

        System.out.println("평균 점수: " + avg);
        Stream<Student> stream = studentList.stream();
        stream.forEach(s -> System.out.println(s.getName()));
    }
}

스트림과 람다식을 사용하면 정말 간결하게 해당 객체의 점수를 뽑아내서 평균을 구할 수 있습니다.

List 컬렉션의 studentList변수에는 Student 타입의 객체 요소들을 가지고 있습니다.
스트림 객체를 얻어온 다음에 체인방식으로 mapToInt 메소드를 이용하여 중간처리에서 해당객체를 score 필드값으로 매핑을 합니다.

IntStream mapToInt(ToIntFunction<? super T> mapper);

마찬가지로 해당 메소드의 구현부를 보면 함수적 인터페이스를 매개값으로 받습니다.
ToIntFunction 함수적 인터페이스는 객체 T를 매개로 받아서 int 타입으로 매핑합니다.
이런식으로 컬렉션 내부적으로 스트림이 반복처리를 하고 중간처리에서 매핑한 score 필드값들로 score의 평균값을 산출하는 과정입니다. 정말 unbelievable 합니다.

스트림에서 제공하는 메소드에 대해서 아래 URL을 참조해주세요.
스트림 처리 메소드 - https://webcoding-start.tistory.com/48

'SpringFramework > JAVA' 카테고리의 다른 글

프록시 패턴  (0) 2019.11.20
Object- 1장 객체, 설계  (0) 2019.11.04
@Annotation 이란?  (0) 2019.10.05
클린코드 OOP의 장점  (0) 2019.10.05
빌더 패턴(Builder Pattern)  (0) 2019.10.05

자료구조와 함꼐 배우는 알고리즘 입문(자바편) 책을 보다가 우연히 전에 사놓고 읽지 않는 이것이 자바다라는 책을 보게 되었습니다. 사실 점점 더 어려워지는 알고리즘 공부가 싫어서 만든 핑계는?? 아닙니다 하하… 이 책은 총 2권으로 구성 되어있고, 대학교때 시중에서 자바와 관련된 책 중에서 나는 Java를 공부한적이 없어요라는 책을 읽으면서 어느정도 자바의 문법과 개념에 대해서 공부했었는데… 내가 자바에 대해서 얼마나 기초지식을 얼마나 기억하고있는지 심심해서… 한번 정독을 해보았습니다.
그 중에서도 어노테이션이에 대한 내용이 나와있길래 자세히 읽어보고 이해하게 되었습니다.

@Annotation

어노테이션에 대해서 떠오르는건 스프링 MVC로 프로젝트를 진행할때 어노테이션은 단순히 lombok을 사용하여 @Getter, @Setter를 특정 클래스 위에 기술하여 클래스 파일 생성시 getter, setter를 알아서 자동으로 만들어주는 아주 편리한녀석??으로만 생각했었습니다.
또 한 부모클래스나 인터페이스 같은 상위타입의 메소드를 재정의할 때 해당메소드 위에 @Override를 표시하는 정도만 알고먼 있었습니다. 부끄럽지만 Java 1.5부터 제공하는 어노테이션에 대해서 예제를 통해서 정확하게 무엇을 하고 어떻게 Customizing 해서 사용하는지 리뷰를 해보겠습니다.

어노테이션 개념

어노테이션(Annotation)은 메타데이터(metadata)라고 볼 수 있습니다. 메타데이터는 Application이 처리하는 데이터가 아니고, 컴파일 과정과 런타임 과정에서 코드를 어떻게 컴파일하고 처리할것인지를 알려주는 정보입니다. 어노테이션은 다음과 같은 형태로 작성됩니다.

@AnnotationName

어노테이션은 다음 세가지 용도로 사용됩니다.

  • 컴파일러에게 코드 문법 에러를 체크하도록 정보를 제공
  • 소프트웨어 개발 툴이 빌드나 배치 시 코드를 자동으로 생성할 수 있도록 정보를 제공
  • 실행 시(런타임 시) 특정 기능을 실행하도록 정보를 제공

첫번째로 컴파일러에게 코드 문법 에러를 체크하도록 정보를 제공하는 대표적인 예는 위에어 언급한 @Override 어노테이션 입니다. 예를 들어서 부모클래스의 메소드를 재정의하여 사용할때 컴파일 시 상위타입(부모클래스, 인터페이스)에 해당 메소드가 존재하는지 확인하고 만약 존재하지 않는다면 컴파일 에러를 발생시킵니다. 어노테이션은 빌드 시 자동으로 XML 설정 파일을 생성하거나, 배포를 위해 JAR 압축 파일을 생성하는데에도 사용됩니다. 그리고 실행 시 클래스의 역할을 정의하기도 합니다.

어노테이션 정의

어노테이션 타입을 정의하는 방법은 인터페이스를 정의하는 것과 유사합니다.

public @interface AnnotationName{

}

이렇게 정의한 어노테이션은 코드에서 다음과 같이 사용합니다.

@AnnotationName

어노테이션은 클래스의 필드처럼 엘리먼트라는 녀석을 맴버로 가질 수 있습니다. 각 엘리먼트는 타입과 이름으로 구성되며, 디폴트 값을 가질 수 있습니다. 엘리먼트 타입으로는 int나 double과 같은 원시 타입이나 String, Class 타입, 그리고 이들의 배열 타입을 사용할 수 있습니다.

public @interface AnnotationName{
    String elementName1();        // 엘리먼트 선언
    int elementName2() default 5;
}

이렇게 정의한 어노테이션을 코드에서 적용할 때에는 다음과 같이 기술합니다.

@Annotation(elementName1="값", elementName2=3);
또는
@AnnotationName(elementName1 = "값");

elementName1은 디폴트 값이 없기 때문에 반드시 값을 기술해야하고, elementName2는 디폴트 값이 있기 때문에 생략 가능합니다. 어노테이션은 기본 엘리먼트인 value를 가질 수 있습니다.

Value 엘리먼트를 가진 어노테이션을 코드에서 적용할 때에는 다음과 같이 값만 기술할 수 있습니다.
이 값은 기본 엘리먼트인 value 값으로 자동 설정됩니다.

@Annotation("값");

만약 value 엘리먼트와 다른 엘리먼트의 값을 동시에 주고 싶다면 다음과 같이 정상적인 방법으로 지정하면 됩니다.

@AnnotationName(value="값",  elementName=3);

@Annotation 적용 대상

스크린샷 2019-10-05 오전 1 02 58

@Target

어노테이션이 적용될 대상을 지정할 때에는 @Target 어노테이션을 사용합니다. @Target의 기본 엘리먼트인 value는 ElementType 배열을 값으로 가진다. 이것은 어노테이션이 적용될 대상을 복수개로 지정하기 위해서 입니다.

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
public @interface AnnotatiionName{

}

어노테이션 유지 정책

어노테이션 정의 시 한 가지 더 추가해야 할 내용은 사용 용도에 따라 @AnnotationName을 어느 범위까지 유지할 것인지 지정해야 합니다. 쉽게 설명하면 소스상에만 유지할 건지, 컴파일된 클래스까지 유지할 건지, 런타임 시에도 유지할 건지를 지정해야 합니다. 어노테이션 유지 정책은 java.lang.annotation.RetentionPolicy 열거 상수로 다음과 같이 정의되어 있습니다.

스크린샷 2019-10-05 오전 1 03 05

리플렉션(Reflection)이란 런타임 시에 클래스의 메타 정보를 얻는 기능을 말합니다. 예를 들어 클래스가 가지고 있는 필드가 무엇인지, 어떤 생성자를 갖고 있는지, 어떤 메소드를 가지고 있는지, 적용된 어노테이션이 무엇인지 알아내는 것이 리플렉션 입니다. 리플렉션을 이용해서 런타임 시에 어노테이션 정보를 얻으려면 어노테이션 유지 정채을 RUNTIME으로 설정해야 합니다. 어노테이션 유지 정책을 지정할 때에는 @Retention 어노테이션을 사용합니다.

@Annotation 예제 코드

어노테이션과 리플렉션을 이용해서 간단한 예제를 만들어 보도록 합시다. 다음은 각 메소드의 실행 내용을 구분선으로 분리해서 콘솔에 출력하도록 하는 PrintAnnotation입니다.

Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintAnnotation {
    String value() default "-";
    int number() default 15;
}

위의 코드에서 @Target은 메소드에만 적용하였고, @Retention은 런타임 시까지 어노테이션 정보를 유지하도록 하였습니다. 기본 엘리먼트 value는 구분선에 사용될 문자이고, number는 반복 출력 횟수입니다. 각각 디폴트 값으로 "-"와 15를 주었습니다. 다음은 PrintAnnotation을 적용한 Service 클래스 입니다.

public class Service {

    @PrintAnnotation // 해당 어노테이션은 엘리먼트의 기본값으로 설정
    public void method1(){
        System.out.println("실행 내용1");
    }

    @PrintAnnotation("*") // 엘리먼트 value 값을 "*"로 설정
    public void method2(){
        System.out.println("실행 내용2");
    }

    @PrintAnnotation(value="#", number=20) // value 값을 "#", number 20 설정
    public void method3(){
        System.out.println("실행 내용3");
    }
}

다음 PrintAnnotationExample 클래스는 리플렉션을 이용해서 Service 클래스에 적용된 어노테이션 정보를 읽고 엘리먼트 값에 따라 출력할 문자와 출력 횟수를 콘솔에 출력한 후, 해당 메소드를 호출합니다.

public class PrintAnnotationExample {
    public static void main(String[] args) {
        //Service 클래스의 메소드 정보를 Method 배열로 리턴합니다.
        Method[] declaredMethods = Service.class.getDeclaredMethods();
        //메소드 객체를 하나씩 처리
        for (Method method : declaredMethods) {
            if (method.isAnnotationPresent(PrintAnnotation.class)) {
                //객체 얻기
                PrintAnnotation printAnnotation = method.getAnnotation(PrintAnnotation.class);

                //메소드 이름 출력
                System.out.println("[" + method.getName() + "] ");

                //구분선 출력
                for (int i = 0; i < printAnnotation.number(); i++) {
                    System.out.print(printAnnotation.value());
                }
                System.out.println();


                try {
                    method.invoke(new Service());
                } catch (Exception e) {}
                    System.out.println();
            }
        }
    }
}

위의 isAnnotationPresent 메소드는 지정한 어노테이션이 적용되었는지 여부, Class에서 호출했을때 상위 클래스에 적용된 경우도 true를 리턴합니다.
method.invoke(new Service())는 Service 객체를 생성하고 생성된 Service 객체의 메소드를 호출하는 코드입니다. 메모리에 해당 객체가 있어야만 메소드를 호출이 가능하기 때문이죠.

이러한 어노테이션의 기본적인 개념에 대해서 이해를 하고 @Getter 어노테이션을 살펴보니 전에는 몰랐던 부분이 조금이나마 이해가 되었습니다. 실습을 통해서 리플렉션과 어노테이션에 대해서 깊게는 아니지만 보이지 않았던 부분이 조금이나마 보이게되서 정말 보람찬 하루였습니다.ㅎㅎ

'SpringFramework > JAVA' 카테고리의 다른 글

프록시 패턴  (0) 2019.11.20
Object- 1장 객체, 설계  (0) 2019.11.04
Stream(스트림)  (0) 2019.10.05
클린코드 OOP의 장점  (0) 2019.10.05
빌더 패턴(Builder Pattern)  (0) 2019.10.05

+ Recent posts