자료구조와 함꼐 배우는 알고리즘 입문(자바편) 책을 보다가 우연히 전에 사놓고 읽지 않는 이것이 자바다라는 책을 보게 되었습니다. 사실 점점 더 어려워지는 알고리즘 공부가 싫어서 만든 핑계는?? 아닙니다 하하… 이 책은 총 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

클린 코더스 강의 1.OOP

백명석 선생님의 클린코드라는 강의가 나온지 6년이나 되었지만, 이제 막 프로그래밍 세계에 입문한 저한테는 정말 배울게 많은 강의였습니다. 가장 기억에 남는 말씀은 개발자들이 많이하는 실수는 객체지향 프로그래밍을 할때 기능을 중심으로 생각하는게 아니라 해당 클래스가 가지고 있는 데이터 중심으로 사고를 한다는거 였습니다.

에를들어 글이나 기사를 작성하는 서비스에 대한 네이밍을 WriteArticleService가 아니고 ArticleService라고 작명한는 것도 기능이 아니라 데이터 중심으로 생각을 해서 발생하는 문제입니다.

1강의 핵심은 돌아가는 코드도 중요하지만 사람이 읽을 수 있는 코드에 초점을 맞춘 강의였습니다. 리펙토링을 하는 습관을 항상 가져야되고… 아무리 바쁘더라도 리펙토링을 미루면 안됩니다. 개발을 빨리 한다고 해도 코드가 지저분하고 돌아가는데만 신경을 쓴다면 나중에 유지보수하는데 상당한 골치를 겪을 확률이 높습니다.

이번 시간에는 공통된 데이터나 프로세스를 제공하는 객체들을 하나의 타입(인터페이스)로 추상화 하는 코드에 대해서 배웠습니다.

스크린샷 2019-10-05 오전 12 29 16

위의 클래스 다이어그램에서 해당 객체들의 공통점은 바로 로그를 수집하고 iterator() 메소드를 이용해서 수집한 로그를 반복처리하는 역할을 하는 객체입니다.

만약 이 로직을 변경하지 않고 로그 수집 방법을 변경하기 위해서는 인터페이스를 재사용 하는 것입니다. 인터페이스를 재사용하면 Client는 구현 변경에 대해서 영향을 받지 않습니다. 한마디로 추상화를 통해 유연함을 얻기 위한 규칙입니다.

concrete class를 직접 사용하는 경우

public class FlowController{

    private final Parser parser = new Parser();

    public void process(){
        FileLogCollector collector = new FileCollector();

        FileLogSet logset = collector.collect();

        Iterator<String> it = logSet.iterate();

        FileLogWriter writer = new FileLogWriter();

        for(String line = it.next; line != null;){
            String parsedLine = parser.parse(line);
            writer.write(parsedLine);
        }
    }
}

인터페이스 추상화

public class FlowController{

    private final Parser parser = new Parser();

    public void process(){
        LogCollector collector = new FileCollector();

        LogSet logset = collector.collect();

        Iterator<String> it = logSet.iterate();

        LogWriter writer = new FileLogWriter();

        for(String line = it.next; line != null;){
            String parsedLine = parser.parse(line);
            writer.write(parsedLine);
        }
    }
}

위의 소스에서는 LogCollector, LogSet을 interface로 두어서 타입을 추상화 하여 클라이언트에게 구체적인 모습은 드러내지 않고 변경을 유연하다는 걸 보여줍니다.

아래에 백명석 강사님이 설명하신 변경에 유여한 코드를 따라서 만들어본 예제입니다.
mills 단위로 경과시간을 구해주는 ProceuduralStopWatch가 클래스를 설계하였습니다.

// 스탑워치의 역할을 해야하는 클래스
public class ProceduralStopWatch {

    private long startTime;
    private long stopTime;

    public long getElapsedTime(){
        return stopTime - startTime;
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProceduralStopWatchTest {

    private long expectedElapsedTime = 100l;

    @Test
    public void should_return_elapsed_mills() {

        ProceduralStopWatch stopWatch = new ProceduralStopWatch();
        //mills 단위로 인스턴스 변수 초기화
        stopWatch.startTime = System.currentTimeMillis();

        doSomething();

        stopWatch.stopTime = System.currentTimeMillis();

         assertThat(elapsedTime, is(greaterThanOrEqualTo(expectedElapsedTime);

    }
}

위의 코드를 보면 doSomething() 메소드가 얼마나 걸리는지 밀리타임으로 경과된 시간을 구하는 프로그램입니다. 만약 여기서 nano 단위로 경과된 시간을 구하라는 요구사항이 추가 된다면 데이터 구조 변경이 유발됩니다.

public class ProceduralStopWatch {

    private long startTime;
    private long stopTime;
    private long startNanoTime;
    private long stopNanoTime;

    public long getElapsedTime(){
        return stopTime - startTime;
    }

    public long getElapsedNanoTime(){
        return stopNanoTime - startNanoTime;
    }   
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProceduralStopWatchTest {

    private long expectedElapsedTime = 100l;

    @Test
    public void should_return_elapsed_mills() {

        ProceduralStopWatch stopWatch = new ProceduralStopWatch();
        //mills 단위로 인스턴스 변수 초기화
        stopWatch.startTime = System.nanoTime();

        doSomething();

        stopWatch.stopTime = System.nanoTime();

        long elapsedTime = stopWatch.getElapsedNanoTime();
        assertThat(elapsedTime, is(greaterThanOrEqualTo(expectedElapsedTime * (long)pow(10, 6))));
    }
}

nano 단위의 경과 시간을 구하는 기능이 추가되었을 뿐인데 해당 데이터를 사용하는 모든 코드를 수정해야 합니다. 프로젝트 규모가 커질수록 이렇게 수정하는데 많은 시간이 할애됩니다.

객체지향으로 해당 클래스를 설계한다면 다음과 같이 클라이언트에 영향을 안마치는 코드를 짤수가 있습니다.

public class ProceduralStopWatch {

    private long startTime;
    private long stopTime;


    public void start(){
        this.startTime = System.nanoTime();
    }

    public void stop(){
        this.stopTime = System.nanoTime();
    }

    public Time getElapsedTime(){
        return new Time(stopTime - startTime);
    }
}
public class Time(){

    private long nano;

    public Time(long nano){
        this.nano = nano;
    }

    public void getMilliTime(){
        return (long)(nano / pow(10, 6));
    }

    public void geNanoTime(){
        return nano;
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProceduralStopWatchTest {

    private long expectedElapsedTime = 100l;

    @Test
    public void should_return_elapsed_mills() {

        ProceduralStopWatch stopWatch = new ProceduralStopWatch();

        // startTime 필드에 값을 할당하지 않고 기능 실행
        stopWatch.start();

        doSomething();

        // stopTime 필드에 값을 할당하지 않고 기능 실행
        stopWatch.stop();

        Time time = stopWatch.getElapsedTime();    

         assertThat(time.getNanoTime, is(greaterThanOrEqualTo(expectedElapsedTime * (long)pow(10, 6))));

    }
}

이렇게 내부적으로 구현 내용을 감추게 되면 클라이언트에 영향을 안 미치게 됩니다.
Tell, Don’t Ask라는 뜻으로 데이터를 요청해서 변경하고 저장하라고 하지말고 무슨 기능을 실행하라는 뜻입니다. 데이터를 잘 알고 있는 객체에게 기능을 수행하라고 요청하면 클라이언트 입장에서는 그 객체가 어떤 방법을 수행해도 원하는 값만 잘 던져주면 되는거죠…

마지막으로 객체는 각각 역할을 가지고 있습니다. 역할이란 객체가 가지는 책임들의 집합을 의미합니다. 항상 객체지향적으로 사고하는 습 관을 기르기 위해서 데이터보다 기능중심으로 먼저 생각해야 된다는걸 숙지해야 합니다.

참조: 백명석의 클린코드

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

프록시 패턴  (0) 2019.11.20
Object- 1장 객체, 설계  (0) 2019.11.04
Stream(스트림)  (0) 2019.10.05
@Annotation 이란?  (0) 2019.10.05
빌더 패턴(Builder Pattern)  (0) 2019.10.05

빌더 패턴(Builder Pattern)

빌더 패턴은 객체를 생성할 때 흔하게 사용하는 패턴입니다.
다음과 같은 코드 스타일로 객체를 생성하는 코드가 있다면, 빌더 패턴을 사용했다고 할 수 있습니다.

Member.Builder builder = new Member.Builder("sa1341", "junyoung");
       builder.age(28)
              .hobby("soccer");

        Member member = builder.build();

언제 Builder 패턴을 사용하는지는 Effective Java에는 다음과 같이 설명합니다.

규칙 2. 생성자 인자가 많을 때는 Builder 패턴 적용을 고라하라.

이펙티브 자바에서 말하는 빌더 패턴은 객체 생성을 깔끔하고 유연하게 하기 위한 기법입니다.

객체를 생성하는 방법을 3가지 정도 소개하고 있습니다.

  • 점층적 생성자 패턴(telescoping constructor pattern)
  • 자바빈 패턴
  • 빌더 패턴

점층적 패턴

먼저 점층적 생성자 패턴을 만드는 방법은 다음과 같습니다.

  1. 필수 인자를 받는 필수 생성자를 하나 만듭니다.
  2. 1개의 선택적 인자를 받는 생성자를 추가합니다.
  3. 2개의 선택적 인자를 받는 생성자를 추가합니다.
  4. 반복 수행
  5. 모든 선택적 인자를 다 받는 생성자를 추가합니다.
//점층적 생성자 패턴 코드의 예: 회원가입 관련 코드
public class Member{

    private final String id;
    private final String name;
    private final String hobby;

    //필수 생성자
    public Member(String id){
        this(id, "이름 비공개", "취미 비공개");
    }

    // 1개의 선택적 인자를 받는 생성자
    public Member(String id, String name){
        this(id, name, "취미 비공개");
    }

    //모든 선택적 인자를 다 받는 생성자
    public Member(String id, String name, String hobby){
        this.id = id;
        this.name = name;
        this.hobby = hobby;
    }
}

장점

new Member(“sa1341”, “이름 비공개”, “취미 비공개”) 같은 호출이 빈번하게 일어난다면, new Member(“sa1341”)로 대체할 수 있습니다.

단점

  • 다른 생성자를 호출하는 생성자가 많으므로, 인자가 추가되는 일이 발생하면 코드를 수정하기 어렵습니다. 또한 코드 가독성도 떨어집니 다.

  • 특히 인자수가 많을 때 호출 코드만 봐서는 의미를 알기 어렵습니다.

//호출 코드만으로는 각 인자의 의미를 알기 어렵습니다.
Car car = new Car(1,30.2,5,7,8,6,4);
Car car = new Car(3,35,210,24);
Car car = new Car(230,71);

자바빈 패턴

점층적 패턴의 대안으로 등장한 자바빈 패턴을 소개합니다.
이 패턴은 setter 메서드를 이용해 생성 코드를 읽기 좋게 만드는 것입니다.

Car car = new Car();
car.setName("Hyundai");
car.setNumber(240);

장점

  • 이제 각 인자의 의미를 파악하기 쉬워졌습니다.
  • 복잡하게 여러 개의 생성자를 만들지 않아도 됩니다.

단점

  • 객체의 일관성이 깨집니다.
    1회의 호출로 객체 생성이 끝나지 않습니다.
    즉 한번에 생성하지 않고 생성한 객체에 값을 계속 셋팅해주고 있습니다.

  • setter 메서드가 있으므로 변경 불가능(immutable)클래스를 만들 수가 없습니다.
    스레드 안전성을 확보하려면 점층적 생성자 패턴보다 많은 일을 해야 합니다.

빌더 패턴(Effective Java 스타일)

public class Member {

    private final String id;
    private final String name;
    private final int age;
    private final String hobbyy;

    public static class Builder {

        private final String id;
        private final String name;
        private int age;
        private String hobby;


        public Builder(String id, String name) {
            this.id = id;
            this.name = name;
        }

        public Builder age(int age){
            this.age = age;
            return this;
        }

        public Builder hobby(String hobby){
            this.hobby = hobby;
            return this;
        }

        public Member build() {
            return new Member(this);
        }

    }

    public Member(Builder builder) {
        this.id = builder.id;
        this.name = builder.name;
        this.age = builder.age;
        this.hobbyy = builder.hobby;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getHobbyy() {
        return hobbyy;
    }
}

위와 같이 하면 다음과 같이 객체를 생성할 수 있습니다.

Member.Builder builder = new Member.Builder("sa1341", "junyoung");
        builder.age(28)
               .hobby("soccer");

         Member member = builder.build();

아래와 같이 사용할 수도 있습니다.

//각 줄마다 builder를 타이핑 하지 않아도 되어 편리합니다.
Member member = new Member.Builder("sa1341","junyoung") // 필수값 입력
    .age(28)
    .hobby("농구")
    .build(); // build() 메소드가 객체를 생성해 돌려줍니다.

장점

  • 각 인자가 어떤 의미인지 알기 쉽습니다.
  • setter 메소드가 없으므로 변경 불가능한 객체를 만들 수 있습니다.
  • 한번에 객체를 생성하므로 객체 일관성이 깨지지 않습니다.
  • build() 함수가 잘못된 값이 입력되었는지 검증하게 할 수도 있습니다.
참조: https://johngrib.github.io/wiki/builder-pattern/ (기계인간 종립)

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

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

+ Recent posts