금요일, 3월 29
Shadow

#011 열거형(Enum)과 주석(Annotation) 두번째

작명 패턴보다는 주석(annotation)을 사용하자

What is the Annotation?
어노테이션은 일종의 메타 정보를 컴파일러에게 제공할 수 있는 문법이다.
// 나 /**/ 등으로 적는 문법은 컴파일러는 알아먹을 수 없고 사람, 정확히는 프로그래머에게 이 코드는 이러저러한 코드다…라고 정보를 주는 문법이지만
어노테이션은 컴파일러에게 메타 정보를 주는 문법이다.
물론 잘 쓴 어노테이션은 프로그래머에게도 정보를 줄 수 있다.


참고 ## 메타 정보

메타데이터 (metadata)란 데이터(data)를 위한 데이터다. 어떤 데이터 즉 구조화된 정보를 분석, 분류하고 부가적 정보를 추가하기 위해 그 데이터 뒤에 함께 따라가는 정보를 말한다.
이를테면, 디지털 카메라에서는 사진을 찍어 기록할 때마다 카메라 자체의 정보와 촬영 당시의 시간, 노출, 플래시 사용 여부, 해상도, 사진 크기 등의 사진 정보를 화상 데이터와 같이 저장하게 되어 있다. 이러한 데이터를 분석하여 이용하면 그 뒤에 사진을 적절하게 정리하거나 다시 가공할 때에 아주 유용하게 쓸 수 있는 정보가 된다.

어노테이션 문법
아마 일반적으로 제일 많이 볼 수 있는 어노테이션은 @Override 일것이다.

@Override
public void xxx() { ... }

어노테이션의 선언 방법부터 살펴보자.
어노테이션은 @interface 타입으로 선언하며 뒤에 이름이 온다. 만일 TODO 라는 작업이라는 메타 정보를 제공하는 어노테이션을 선언한다면, 이러한 문법이 된다.

public @interface TODO {
  String value();
}

만일 선언한 어노테이션을 쓴다면 이러한 형식이 될 것이다.

@TODO("유대리 빨리 구현해줘요")
public class Something { ... }

위의 String value() 는 어노테이션 안에 문자열을 인자로 줄 수 있다는 것을 의미한다. 어노테이션도 객체 타입이기에 상태, 즉 값을 가질 수 있다.
어노테이션에는 이 어노테이션이 선언될 수 있는 요소(클래스, 메서드, 필드, 생성자) 를 지정하거나 주석의 생존 스코프(소스, 클래스, 런타임)를 지정해줄 수 있다.
만일 위 주석을 런타임 단계에도 적용하고, 메소드 레벨에만 적용하고 싶다면 어노테이션 선언시 다음과 같은 메타 어노테이션을 선언하면 된다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TODO {
  String value();
}

@Retention 은 어노테이션이 얼마나 오랫동안 유지되는지에 대해 표시해줄 수 있고, RetentionPolicy enum으로 총 3가지를 가질 수 있으며 SOURCE, CLASS, RUNTIME 이 있다.
위 어노테이션은 RUNTIME 선언이므로, 런타임 시에도 생존하는 스코프를 지닌다. 이런 생존범위라면 각종 런타임 레벨에서 어노테이션의 값을 조회하고 프로그래밍 적으로 이용할 수 있다.
@Target으로는 이 어노테이션이 적용될 요소를 지정해줄 수 있다. 이 어노테이션은 메서드 레벨에만 적용된다.


뭐가 좋지?
만일 특정 DB의 값을 쿼리하여 매핑하는 자바 DTO JavaBeans 가 있다고 해보자.

public class User {

    String name;
    String password;
    int todayAccess:

    public String getName() { return this.name; }
    public String setName(String name) { this.name = name; }

    public String getPassword() { return this.password; }
    public String setPassword(String password) { this.password = password ; }

    public String incAccess() { this.todayAccess++; }
    public String getAccessCount() { return this.todayAccess; }

}

이러한 빈즈는 자바빈즈 규약을 따른 작명 규칙에 의해 만들어져 있다.
필드 하나만 get/get이 없는데 이 필드는 DB에 있는 데이터가 아닌 프로그램 실행 중 동적으로 변화하는 값이라고 가정한다.
여기까진 문제될 게 없다. 하지만 저런 DB 매핑 DTO 클래스가 많아지면 자연스럽게 클래스 수는 많아지고, DB 매핑 클래스를 프로그램에서 체크하여 한번에 관리하고 싶을 경우, 일일히 그 패키지를 기억하여 관리하는 방법은 너무 번거롭고 실수하기 쉽다.
어떻게 하면 좋을까…위의 클래스에 어노테이션을 적용하여 의미를 분명하게 지정해 본다면 어떨까?
아래와 같이 지정해보자. 먼저 DB 매핑 클래스라는것을 명시하는 어노테이션을 선언한다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Entity{
  String value();
}

그 다음에는 저 어노테이션을 지정한다.

@Entity("user_table")
public class User {

    String name;
    String password;
    int todayAccess:

    public String getName() { return this.name; }
    public String setName(String name) { this.name = name; }

    public String getPassword() { return this.password; }
    public String setPassword(String password) { this.password = password ; }

    public String incAccess() { this.todayAccess++; }
    public String getAccessCount() { return this.todayAccess; }

}

다음부터는 프로그램에서 어떠한 객체가 전달되었을 때, 이 객체가 매핑 객체임을 알려면 isAnnotationPresent 메서드로 Entity 어노테이션이 선언되어 있는지 체크하면 간단하다.
그리고 프로그래머는 이 객체가 DB의 user_table과 관련이 있음을 알 수도 있다.
이제 더 생각을 해보자.
DB 매핑 작업중 이 객체의 필드(name, password, todayAccess)가 실제 테이블과 매핑이 되어 있는지 체크하는 로직이 필요해졌다고 해보자.
프로그래머는 당연히 todayAccess가 DB와는 관련없는 필드인 것을 알지만 프로그램은 알지 못하기에 일일히 DB 매핑 클래스마다 따로 체크하는 로직을 넣어야 한다.
하지만 그렇기엔 체크 로직이 매핑 클래스수만큼 늘어나게 되고 유연성은 떨어진다.
어노테이션을 사용하여 한번 고쳐보자.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Column {
  String value();
}

DB 테이블의 실제 컬럼을 나타내는 필드 레벨의 어노테이션을 선언했다
그렇다면 클래스의 구성은 다음과 같이 바뀔 것이다.

@Entity("user_table")
public class User {

    @Column("username")
    String name;

    @Column("passwd")
    String password;

    int todayAccess:

    public String getName() { return this.name; }
    public String setName(String name) { this.name = name; }

    public String getPassword() { return this.password; }
    public String setPassword(String password) { this.password = password ; }

    public String incAccess() { this.todayAccess++; }
    public String getAccessCount() { return this.todayAccess; }

}

클래스가 한결 더 보기 쉬워졌다.
프로그래머가 봐도, JVM이 봐도, 이 클래스는 user 테이블의 쿼리 결과를 담는 객체이며, 해당 테이블의 컬럼값 두개를 name과 password에 담는 동작을 하는 것으로 알 수 있다.
여기서 좀 더 발전시킨다면 특정 액션 동작을 취할 시, 자동으로 이 클래스의 인스턴스를 생성하고 해당 DB에 쿼리한 뒤 결과를 담아 리턴해주는 자동 매핑 로직을 구현해볼 수도 있을 것이다.
실제로 Hibernate 가 비슷한 방식으로 DB와 객체의 매핑이 구현되어 있다.


Override 주석을 일관성 있게 사용하자

자바 1.5 배포판에 주석(annotation) 중에 일반 프로그래머에게 가장 중요한 것이 Override이다.

 
// 어디에 버그가 있을까?
import java.util.*;

public class Bigram {
    private final char first;
    private final char second;
    public Bigram(char first, char second) {
        this.first  = first;
        this.second = second;
    }

    public boolean equals(Bigram b) {
        return b.first == first && b.second == second;
    }

    public int hashCode() {
        return 31 * first + second;
    }

    public static void main(String[] args) {
        Set s = new HashSet();
        for (int i = 0; i < 10; i++)
            for (char ch = 'a'; ch <= 'z'; ch++)
                s.add(new Bigram(ch, ch));
        System.out.println(s.size());
    }
}

---------- execute ----------
260

Bigram 클래스 개발자는 equals 메소드를 (항목 8) 오버라이딩 하려고 했을 것이다. 그리고 hashCode를 (항목 9) 오버라이딩 하는 것도 잊지 않았다.
그러나 equals 메소드의 매개 변수를 Object 타입으로 정의하지 않았으므로, 오버라이딩 하는 대신 오버로딩 하게 되었다. (항목 41)
@Override 주석을 사용하면 컴파일 도중에 에러를 찾아낼 수 있다. 그 메세지는 아래와 같다.

The method equals(Bigram) of type Bigram must override or implement a supertype method Bigram.java
 @Override
    public boolean equals(Object o) {
        if(!(o instanceof Bigram))
            return false;

        Bigram b = (Bigram) o;
        return b.first == first && b.second == second;
    }

---------- execute ----------
26

수퍼 클래스의 메소드를 오버라이드 한다고 생각되는 모든 메소드에 Override 주석을 사용해야 한다.
단, 이 규칙에 한가지 예외가 있다. 실체 클래스(abstract가 붙지않은)를 작성하면서 추상 메소드를 오버라이트 한다면, 그 메소드에 Override 주석을 달아줄 필요가 없다.
제대로 오버라이드 하지 않으면, 컴파일러가 알아서 에러메세지를 발생시키기 때문이다.

요약 : 수퍼 타입의 메소드를 오버라이드 하는 모든 메소드에 Override 주석을 달아주면, 컴파일러가 굉장히 많은 에러를 막아줄 수 있다. 한 가지 예외가 있는데, 실체 클래스에서는 수퍼 클래스의 추상 메소드를 오버라이드하는 메소드에 주석을 달 필요없다.

creative by javacafe

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.