상세 컨텐츠

본문 제목

Effective Java 2/E 스터디 3일차

SW/미분류

by 푸로그 2019. 6. 25. 08:31

본문

6월 27일 9장 예외

7월 2일 2장 객체의 생성과 소멸
7월 5일 3장 모든 객체에 공통적인 메소드

7월 9일 4장 클래스와 인터페이스
7월 12일 5장 제네릭
7월 16일 10장 동시성
7월 19일 11장 직렬화

6장 열거형(enum)과 어노테이션 

규칙 30 int 상수 대신 enum을 사용하라 
규칙 31 ordinal 대신 객체 필드를 사용하라 
규칙 32 비트 필드(bit field) 대신 EnumSet을 사용하라 
규칙 33 ordinal을 배열 첨자로 사용하는 대신 EnumMap을 이용하라 
규칙 34 확장 가능한 enum을 만들어야 한다면 인터페이스를 이용하라 
규칙 35 작명 패턴 대신 어노테이션을 사용하라 
규칙 36 Override 어노테이션은 일관되게 사용하라 
규칙 37 자료형을 정의할 때 표식 인터페이스를 사용하라 

이번장의 고민

자바 1.5 에서 새로운 자료형(reference type)이 추가 됨.
열거 자료형(enum type)의 새로운 클래스, 어노테이션 자료형의 새로운 인터페이스
새로운 자료형의 활용하는 법에 대해

규칙 30 int 상수 대신 enum을 사용하라

enum 이전의 상수 예제
int형 상수들을 정의해서 열거 자료형을 흉내 냈다.

// int를 사용한 enum 패턴 - 지극히 불만족스럽다.
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;

이 기법은 int enum 패턴으로 알려져 있는데, 단점이 많다. 형 안전성 관점에서도 그렇고, 편의성 관점에서 봐도 그렇다. 오렌지를 기대하는 메서드에 사과를 인자로 넘겨도 컴파일러는 불평하지 않는다. == 연산자를 사용해 사과를 오렌지와 비교해도 마찬가지다. 더 심각한 사례를 보자.

// 귤 맛 나는 사과 주스!
int I = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN;

int enum 패턴을 사용하는 프로그램은 깨지기 쉽다. int enum 상수는 컴파일 시점 상수이기 때문에 상수를 사용하는 클라이언트도 다시 컴파일해야 한다. 컴파일하지 않더라도 실행은 되겠지만 어떤 결과를 야기할지는 알 수 없다.

int enum 상수는 인쇄 가능한 문자열로 변환하기도 쉽지 않다. 여러 상수를 출력하거나 디버거로 확인해보면 보이는 것은 숫자뿐이라서 크게 도움이 되지 않는다. 그룹 내의 int enum 상수를 순차적으로 이용하거나, int enum 그룹의 크기를 알아낼 좋은 방법도 없다.

이 패턴의 변종 가운데 int 대신 String 상수를 사용하는 것이 있다. 이 변종은 String enum 패턴이라고 불리는데, 더 나쁜 패턴이다.

  1. 상수 이름을 화면에 출력할 수 있다는 장점은 있지만 상수 비교를 할 때 문자열 비교를 해야 하므로 성능이 떨어질 수 있다.

  2. 더 큰 문제는 아무 생각 없는 사용자가 필드 이름 대신 하드코딩된 문자열 상수를 클라이언트 코드 안에 박아버릴 수 있다는 점이다.

  3. 하드코딩된 문자열 상수에 오타가 있는 경우, 컴파일할 때는 오류를 발견할 수 없기 때문에 실행 도중에 문제가 생기게 될 것이다.

다행인 것은 1.5부터는 int와 String enum 패턴의 문제점을 해소하는 대안이 도입되었다는 것이다. 이 대안에는 장점이 많다.

//enum 자료형
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH };
public enum ORANGE { NAVEL, TEMPLE, BLOOD };

보기에는 C, C++, C# 같은 언어에서 제공하는 enum 자료형과 비슷하지만 정말 그렇게 믿으면 곤란하다. 자바의 enum 자료형은 완전한 기능을 갖춘 클래스이므로, 다른 언어의 enum보다 강력하다. 다른 언어들의 enum은 결국 int 값이다.

열거 상수별로 하나의 객체를 public static final 필드 형태로 제공하는 것이다. enum 자료형은 실질적으로는 final로 선언된 것이나 마찬가지인데, 클라이언트가 접근할 수 있는 생성자가 없기 때문이다. 클라이언트가 enum 자료형으로 새로운 객체를 생성하거나 계승을 통해 확장할 수 없기 때문에, 이미 선언된 enum 상수 이외의 객체는 사용할 수 없다. 다시 말해서, enum 자료형의 개체 수는 엄격히 통제된다. enum 자료형은 싱글턴 패턴을 일반화한 것으로(규칙 3), 싱글턴 패턴은 본질적으로 보면 열거 상수가 하나뿐인 enum과 같다.

enum 자료형은 컴파일 시점 형 안전성을 제공한다. Apple 형의 인자를 받는다고 선언한 메서드는 반드시 Apple 값 세 개 가운데 하나만 인자를 받는다. 엉뚱한 자료형의 값을 인자로 전달하려 하면, 자료형 불일치 때문에 컴파일할 때 오류가 발생한다. == 연산자를 사용해 서로 다른 자료형의 enum 상수들을 비교하려 해도 마찬가지다.

enum 자료형은 같은 이름의 상수가 평화롭게 공존할 수 있도록 한다. 이름 공간이 분리되기 때문이다. 상수를 추가하거나 순서를 변경해도 클라이언트는 다시 컴파일할 필요가 없다.
enum 자료형은 toString 메서드를 호출하면 인쇄 가능 문자열로 쉽게 변환할 수 있다.

// 데이터의 연산을 구비한 enum 자료형
public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass;
    private final double radius;
    private final double surfaceGravity;

    private static final double G = 6.67300E-11;

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }

    public double mass() { return mass; }
    public double radius() { return radius; }
    public double surfaceGravity() { return surfaceGravity; }

    public double surfaceWeight(double mass) {
        return mass * surfaceGravity;
    }
}
  1. enum은 원래 변경 불가능이므로 모든 필드는 final로 선언되어야 한다(규칙 15)
  2. 필드는 public으로 선언할 수도 있지만, private로 선언하고 public 접근자를 두는 편이 낫다(규칙 14)
//args > 175
public class WeightTable {
    public static void main(String[] args) {
        double earthWeight = Double.parseDouble(args[0]);
        double mess = earthWeight / Planet.EARTH.surfaceGravity();
        for (Planet p : Planet.values())
            System.out.printf("Weight on %s is %f%n", 
                    p, p.surfaceWeight(mess));
    }
}

//결과
Weight on MERCURY is 0.755515
Weight on VENUS is 1.809998
Weight on EARTH is 2.000000
Weight on MARS is 0.757474
Weight on JUPITER is 5.061115
Weight on SATURN is 2.132031
Weight on URANUS is 1.810254
Weight on NEPTUNE is 2.276656

규칙 31 ordinal 대신 객체 필드를 사용하라

모든 enum에는 ordinal이라는 메서드가 있는데, enum 자료형 안에서 enum 상수의 위치를 나타내는 정수값을 반환한다.

// ordinal을 남용한 사례 - 따라하면 곤란
public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUINTET,
    SEXTET, SEPTET, OCTET, NONET, DECTET;

    public int numberOfMusicians() { return ordinal() + 1; }
}

동작은 하지만 유지보수 관점에서 보면 끔찍한 코드다. 상수 순서를 변경하는 순간 numberOfMusicians 메서드는 깨지고 만다.

다행히도 이 문제는 간단히 해결할 수 있다. 원칙은, enum 상수에 연계되는 값을 ordinal을 사용해 표현하지 말라는 것이다. 그런 값이 필요하다면 그 대신 객체 필드에 저장해야 한다.

public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
    NONET(9), DECTET(10), TRIPLE_QUARTET(12);

    private final int numberOfMusicians;
    Ensemble(int size) { this.numberOfMusicians = size; }
    public int numberOfMusicians() { return numberOfMusicians; }
}

자바의 Enum 관련 명세를 보면, ordinal에 대해 이렇게 설명되어 있는 것을 확인할 수 있다. "대부분의 프로그래머는 이 메서드를 사용할 일이 없을 것이다. EnumSet이나 EnumMap처럼 일반적인 용도의 enum 기반 자료 구조에서 사용할 목적으로 설계한 메서드다."
그런 자료 구조를 만들 생각이 없다면, ordinal 메서드는 사용하지 않는 것이 최선이다.

규칙 32 비트 필드(bit field) 대신 EnumSet을 사용하라

// 비트 필드 열거형 상수 - 이제는 피해야 할 구현법
public class Text {
    public static final int STYLE_BOLD = 1 << 0;  // 1
    public static final int STYLE_ITALIC = 1 << 1;  // 2
    public static final int STYLE_UNDERLINE = 1 << 2;  // 4
    public static final int STYLE_STRIKETHROUGH = 1 << 3;  // 8

    // 이 메서드의 인자는 STYLE_ 상수를 비트별로 OR한 값이거나 0.
    public void applyStyles(int styles) { ... }
}

하지만 비트 필드는 int enum 패턴과 똑같은 단점들을 갖고 있다. 비트 필드를 출력한 결과는 int enum 상수를 출력한 결과보다도 이해하기 어렵다. 비트 필드에 포함된 모든 요소를 순차적으로 살펴보기도 어렵다.
어떤 프로그래머는 int 상수 대신 enum을 사용하면서도, 상수 집합을 여기저기 전달할 떄는 비트 필드를 쓴다. 그러나 더 좋은 방법이 있으므로, 그럴 이유는 없다. java.util 패키지에는 EnumSet이라는 클래스가 있는데, 이 클래스를 사용하면 특정한 enum 자료형의 값으로 구성된 집합을 효율적으로 표현할 수 있다.

이 클래스는 Set 인터페이스를 구현하며, Set이 제공하는 풍부한 기능들을 그대로 제공할 뿐 아니라, 형 안전성, 그리고 다른 Set 구현들과 같은 수준의 상호운용성도 제공한다. 하지만 내부적으로는 비트 벡터를 사용한다. enum 값 개수가 64개 이하인 경우(대부분 그런다) EnumSet은 long 값 하나만 사용한다. 따라서 비트 필드에 필적하는 성능이 나온다.
removeAll이나 retainAll 같은 일괄 연산도 비트 단위 산술 연산을 통해 구현된다. 비트 필드를 수작업으로 조작할 때와 같은 절차를 밟는다는 것이다. 그러면서도 비트들을 직접 조작할 때 생길 수 있는 오류나, 어수선한 코드는 피할 수 있다. 어려운 일은 EnumSet이 다 처리해 주기 때문이다.
비트 필드 대신 enum을 사용하도록 고친 예제를 아래에 보였다. 더 짧고, 간결하고, 안전하다.

// EnumSet - 비트 필드를 대신할 현대적 기술
public Class Text {
    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH };

    // 어떤 Set 객체도 인자로 전달할 수 있으나, EnumSet이 분명 최선
    public void applyStyles(Set<Style> styles) { ... }
}

applyStyles 메서드에 EnumSet 객체를 전달하는 클라이언트 코드는 아래와 같다. EnumSet에는 정적 팩터리 메서드가 다양하게 준비되어 있어서 편하게 객체를 만들 수 있는데, 그 가운데 하나를 사용했다.
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

규칙 33 ordinal을 배열 첨자로 사용하는 대신 EnumMap을 이용하라

때로, ordinal 메서드가(규칙 31) 반환하는 값을 배열 첨자로 이용하는 코드를 만날 떄가 있다.

class Herb {
    enum Type { ANNUAL, PERENNIAL, BIENNIAL }

    final String name;
    final Type type;

    Herb(String name, Type type) {
        this.name = name;
        this.type = type;
    }

    @Override public String toString() {
        return name;
    }
}

이제, 화단에 심은 허브들을 나타내는 배열이 하나 있다고 해 보자. 이 허브들은 품종별로 (일년생인지, 다년생인지, 격년생인지에 따라서) 나열해야 한다고 해 보자. 그러려면 품종별 집합을 세 개 만든 다음에, 허브 각각을 그 품종에 맞는 집합에 넣어야 한다.

// ordinal() 값을 배열 첨자로 사용 - 이러시면 곤란합니다
Herb[] garden = ...;

Set<Herb>[] herbsByType = // Indexed by Herb.Type.ordinal()
    (Set<Herb>[]) new Set[Herb.Type.values().length];
for (int i = 0; i < herbsByType.length; i++)
        herbsByType[i] = new HashSet<Herb>();

// 결과 출력
for (int i = 0; i < herbsByType.length; i++) {
    System.out.println("%s: %s%n", Herb.Type.values()[i], herbsByType[i]);
}

동작은 하지만 문제투성이다. 배열은 제네릭과 호환되지 않으므로(규칙 25) 배열을 쓰려면 무점검 형변환이 필요하며 깔끔하게 컴파일되지 않는다. 거기다 배열은 첨자가 무엇을 나타내는지 모르므로, 출력 결과에 붙일 레이블을 수동적으로 만들어 줘야 한다. 가장 심각한 문제는 enum의 ordinal 값으로 배열 원소를 참조할 때, 정확한 int 값이 사용되도록 해야 한다는 것이다. int는 enum과 같은 수준의 형 안전성을 보장하지 않는다. 틀린 값을 쓰게 되면 프로그램은 이상한 짓을 하거나, 운이 좋다면 ArrayIndexOutOfBoundException 예외를 낼 것이다.

하지만 더 좋은 방법이 있다. 위의 배열은 enum 상수를 어떤 값에 대응시킬 목적으로 사용되고 있는데, 그런 용도라면 Map을 쓰면 된다. 사실 enum 상수를 키로 사용할 목적으로 설계된, 성능이 아주 우수한 Map이 하나 있다. 바로 java.util.EnumMap이다. 위의 코드를 enumMap으로 다시 만들어보면 아래와 같다.

// EnumMap을 사용해 enum 상수별 데이터를 저장하는 프로그램
Map<Herb.Type, Set<Herb>> herbsByType = 
    new EnumMap<Herb.Type, Set<Herb>>(Herb.Type.class);
    for(Herb.Type t : Herb.Type.values())
        herbsByType.put(t, new HashSet<Herb>());
    for (Herb h : garden)
        herbsByType.get(h.type).add(h);
    System.out.println(herbsByType);

이 프로그램은 더 짧을 뿐 아니라 깔끔하고 안전하다. ordinal을 이용해 구현한 프로그램과 성능 면에서 월등하다. 무점검 형변환도 없고, 레이블을 손수 만들 필요도 없다. Map에 보관된 키가 enum이나, 출력 가능한 문자열로 알아서 변환되기 때문이다. 배열 첨자를 계산하다 오류가 발생할 가능성도 없다.

EnumMap의 성능이 ordinal 값을 배열 첨자로 쓰는 기법과 속도 면에서 별 차이가 없는 것은 EnumMap이 이미 그런 배열을 내부적으로 사용하고 있기 때문이다. EnumMap 생성자는 키의 자료형을 나타내는 Class 객체를 인자로 받는다는 것에 주의하자. 이런 Class 객체를 한정적 자료형 토큰이란 부르는데, 실행시점 제네릭 자료형 정보를 제공한다(규칙 29).

요약하자면, ordinal 값을 배열 첨자로 사용하는 것은 적절치 않다는 것이다. 대신 EnumMap을 써라. 표현해야 하는 관계가 다차원적이라면, EnumMap<..., EnumMap<...>>과 같이 표현하면 된다. 이것은 응용프로그램 작성자는 가급적 Enum.ordinal 사용을 피해야 한다는 원칙(규칙 31)의 특별한 경우다.

규칙 34 확장 가능한 enum을 만들어야 한다면 인터페이스를 이용하라

enum 자료형은 형 안전 enum 패턴보다 거의 모든 면에서 월등하다. 그러나 형 안전 enum 패턴은 계승을 통한 확장이 가능했던 반면, enum 자료형은 그렇지 않다. 다시 말해서, 형 안전 enum 패턴을 쓸 경우에는 다른 열거 자료형을 계승해서 새로운 열거 자료형을 만드는 것이 가능하지만 enum 자료형은 그럴 수 없다는 이야기다.

때로는 API 사용자가 API가 기본 제공하는 연산 집합을 확장하여 자기만의 연산을 추가할 수 있도록 해야 할 때가 있다.

다행스럽게도, enum 자료형으로도 이런 효과를 낼 수 있는 좋은 방법이 하나 있다. 기본 아이디어는, enum 자료형이 임의의 인터페이스를 구현할 수 있다는 사실을 이용하는 것이다.

// 인터페이스를 이용해 확장 가능하게 만든 enum 자료형
public interface Operation {
    double apply(double x, double y);
}

public enum BasicOperation implements Operation {
    PLUS("+") {
        public double apply(double x, double y) { return x + y; }
    },
    MINUS("-") {
        public double apply(double x, double y) { return x - y; }
    },
    TIMES("*") {
        public double apply(double x, double y) { return x * y; }
    },
    DEVIDE("/") {
        public double apply(double x, double y) { return x / y; }
    };

    private final String symbol;

    BasicOperation(String symbol) {
        this.symbol = symbol;
    }

    @Override public String toString() {
        return symbol;
    }
}

요약하자면, 계승 가능 enum 자료형은 만들 수 없지만, 인터페이스를 만들고 그 인터페이스를 구현하는 기본 enum 자료형을 만들면 계승 가능 enum 자료형을 흉내낼 수 있다. 그렇게 하면 클라이언트가 해당 인터페이스를 구현하는 enum 자료형을 손수 만들 수 있다.

규칙 35 작명 패턴 대신 어노테이션을 사용하라

자바 1.5 이전에는 도구나 프레임워크가 특별히 취급해야 하는 프로그램 요소를 구별하기 위해 작명 패턴을 썼다. 일례로, JUnit이라는 테스트 프레임워크를 사용하려면 테스트 메서드 이름을 test로 시작해야 했다. 하지만 이 방법에는 몇 가지 큰 문제가 있다. 우선, 철자를 틀리면 알아채기 힘든 문제가 생긴다. 예를 들어, 테스트 메서드의 이름을 testSafetyOverride가 아니라 tsetSafetyOverride라고 했다 치자. 그 결과, 테스트되지 않은 기능이 프로그램에 포함되고 만다.

작명 패턴의 두 번째 단점은, 특정한 프로그램 요소에만 적용되도록 만들 수 없다는 것이다. 예를 들어 testSafetyMechanisms 라는 이름의 클래스를 만들었다고 해 보자. 클래스 이름을 이렇게 지으면 JUnit이 그 메서드 전부를, 이름에 상관없이 자동으로 실행하지 않을까 하는 바람에서다. 하지만 JUnit은 역시 이번에도 아무런 불평 없이, 모든 테스트를 깡끄리 무시한다.(클래스 이름까지는 확인하지 않기 때문이다.)

작명 패턴의 세 번째 단점은, 프로그램 요소에 인자를 전달할 마땅한 방법이 없다는 것이다. 예를 들어, 특정 예외가 발생해야 성공으로 판정하는 테스트를 지원하고 싶다고 해 보자. 해당 테스트에는 예외 자료형이 반드시 인자로 전달되어야 할 것이다. 물론 예외 자료형 이름을 테스트 메서드 이름에 집어넣는 방법이 있겠으나 보기 흉할 뿐 아니라 그런 코드는 깨지기도 쉽다(규칙 50). 아울러 컴파일러는 메서드 이름에 포함된 문자열이 예외 이름인지 알 도리가 없다. 테스트를 실행하기 전까지는 같은 이름의 클래스가 있는지, 있더라도 예외인지 절대로 알 수 없을 것이다.

어노테이션은 이 모든 문제를 멋지게 해결한다. 예를 들어, 어노테이션 자료형을 이용해서 테스트 시에 자동으로 실행될 테스트 메서드를 지정할 뿐 아니라 해당 메서드 안에서 예외가 발생하면 테스트가 실패한 것으로 보겠다는 사실을 명시하고 싶다고 하자. 아래에 그런 어노테이션 자료형 Test를 어떻게 정의하는지를 보였다.

/**
 * 어노테이션이 붙은 메서드가 테스트 메서드임을 표시.
 * 무인자 정적 메서드에만 사용 가능.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

어노테이션 자료형 Test의 선언부에도 Retention과 Target이라는 어노테이션이 붙어 있다. 어노테이션 자료형 선언부에 붙는 어노테이션은 메타-어노테이션이라 부른다. @Retention(RetentionPolicy.RUNTIME)은 Test가 실행시간에도 유지되어야 하는 어노테이션이라는 뜻이다. 그렇지 않으면 Test는 테스트 도구에게는 보이지 않는다. @Target(ElementType.METHOD)는 Test가 메서드 선언부에만 적용할 수 있는 어노테이션이라는 뜻이다. 클래스나 필드 등 다른 프로그램 요소들에는 적용할 수 없다.

Test 어노테이션의 실제 사용 예를 아래에 보였다. 이런 어노테이션을 표식 어노테이션이라 부른다. 아무 인자도 받지 않으며, 어노테이션이 적용된 프로그램 요소에 특정한 "표식"을 다는 구실만 하기 때문이다. Test 철자를 틀리거나, Test 메서드 선언부가 아닌 다른 곳에 붙이면 이 프로그램은 컴파일 되지 않을 것이다.

// 표식 어노테이션을 사용하는 프로그램
public class Sample {
    @Test public static void m1() { } // 이 테스트는 성공해야 함
    public static void m2() { }
    @test public static void m3() {   // 이 테스트는 실패해야 함
        throw new RuntimeException("Boom");
    }
    public static void m4() { }
    @test public void m5() { }        // 잘못된 사용: static 메서드가 아님
    public static void m6() { }
    @Test public static void m7() {   // 이 테스트는 실패해야 함
        throw new RuntimeException("Crash");
    }
    public static void m8() { }
}

프로그래머가 소스 파일에 정보를 추가할 수 있도록 하는 도구를 만들고 싶다면, 어떤 어노테이션 자료형이 필요한지 찾아서 만들라. 어노테이션이 있으므로 더 이상은 작명 패턴에 기대면 안 된다.

// 배열을 인자로 받는 어노테이션 사용 예
@ExceptionTest({ IndexOutOfBoundsException.class,
                 NullPointerException.class })
public static void doublyBad() {
    List<String> list = new ArrayList<String>();

    // 자바 명세에는 아래와 같이 addAll을 호출하면
    // IndexOutOfBoundsException이나 NullPointerException이 발생한다고 
    // 명시되어 있다.
    list.addAll(5, null);
}

돌려 말하면 대부분의 프로그래머는, 도구 개발에 관심있는 개발자가 아니라면, 어노테이션 자료형을 정의할 필요가 없다는 뜻이기도 하다. 그렇더라도 모든 프로그래머는 자바 플랫폼이 제공하는 어노테이션 자료형들을 사용하도록 해야 한다(규칙 36, 규칙 24). 또한, IDE나 정적 분석 도구가 제공하는 어노테이션들을 사용할 필요가 있는지 살펴봐야 한다. 그럼 어노테이션을 사용하면 분석 도구가 제공하는 정보의 품질을 향상시킬 수 있다. 하지만 이런 어노테이션들은 표준화가 된 것이 아니므로, 도구를 바꾸거나 새 표준이 재정될 경우에는 수정해야 할 것이다.

규칙 36 Override 어노테이션은 일관되게 사용하라

자바 1.5에 어노테이션이 도입되었을 떄, 자바 라이브러리에 몇 가지 어노테이션 자료형이 추가되었다. 그 가운데, 대부분의 프로그래머에게 가장 중요하게 쓰이는 것은 Override일 것이다. 이 어노테이션은 메서드 선언부에만 사용할 수 있고, 상위 자료형에 선언된 메서드를 재정의한다는 사실을 표현한다. 이 어노테이션을 일관된게 사용하면 끔찍한 버그들을 방지할 수 있다.

재정의할 때는 반드시 선언부에 Override 어노테이션을 붙여야 한다.
상위 자료형에 선언된 메서드를 재정의하는 모든 메서드에 Override 어노테이션을 붙이도록 하면 굉장히 많은 오류를 막을 수 있다는 것이다. 예외적으로, 비-abstract 클래스에서 상위 클래스의 abstract 메서드를 재정의할 때는 Override 어노테이션을 붙이지 않아도 된다(붙여도 나쁠 것은 없지만 말이다).

규칙 37 자료형을 정의할 때 표식 인터페이스(Marker Interface)를 사용하라

표식 인터페이스는 아무 메서드도 선언하지 않는 인터페이스다. 클래스를 만들 때 표식 인터페이스를 구현하는 것은, 해당 클래스가 어떤 속성을 만족한다는 사실을 표시하는 것과 같다. Serializable 인터페이스가 그 예다.

표식 어노테이션과 비교했을 때, 표식 인터페이스에는 두 가지 장점이 있다. 가장 중요한 첫 번째 장점은, 표식 인터페이스는 결국 표식 붙은 클래스가 만드는 객체들이 구현하는 자료형이라는 것이다. 표식 어노테이션은 자료형이 아니다. 표식 인터페이스는 자료형이므로, 표식 어노테이션을 쓴다면 프로그램 실행 중에나 발견하게 될 오류를 컴파일 시점에 발견할 수 있도록 한다.

그렇다면 표식 어노테이션과 표식 인터페이스는 각각 어떤 상황에 잘맞나? 클래스나 인터페이스 이외의 프로그램 요소에 적용되어야 하는 표식은 어노테이션으로 만들어야 한다. 클래스나 인터페이스에 대한 표식은 인터페이스로도 만들 수 있지만, 다른 것들은 그렇지 않다. 클래스나 인터페이스에만 적용할 표식이라면, 스스로 질문해봐야 한다. 이 표식이 붙은 객체만 인자로 받을 수 있는 메서드를 만들 작정인가? 그렇다면 어노테이션 대신 표식 인터페이스를 써야 한다. 그러면 해당 메서드의 인자 자료형으로 해당 인터페이스를 사용할 수 있어서, 컴파일 시간에 형 검사를 진행할 수 있게 된다.
하지만 그런 메서드를 만들 필요가 없다면, 한 가지 질문을 더 던져보자. 이 표식을 앞으로도 영원히 특정 인터페이스 객체에만 적용할 예정인가? 그렇다면 그 표식은 그 인터페이스의 하위 인터페이스로 정의하는 것이 좋다. 하지만 아니라면, 표식 어노테이션을 이용하는 것이 바람직할 것이다.

요약하자면, 표식 인터페이스와 표식 어노테이션은 쓰임새가 다르다. 새로운 메서드가 없는 자료형을 정의하고자 한다면 표식 인터페이스를 이용해야 한다. 클래스나 인터페이스 이외의 프로그램 요소에 표식을 달아야 하고, 앞으로 표식에 더 많은 정보를 추가할 가능성이 있다면, 표식 어노테이션을 사용해야 한다.

 

관련글 더보기

댓글 영역

페이징