개발자 피터
Peter's Dev Blog
개발자 피터
전체 방문자
오늘
어제
  • 분류 전체보기 (77)
    • 🧑🏻‍💻 Develop (13)
      • Devops (3)
      • Elasticsearch (3)
      • Design Pattern (1)
      • SQL (4)
      • Architecture (1)
      • APM (1)
    • 💻 Service (7)
      • E-ROOM (3)
      • Briefing (4)
    • 💡 Problem Solving (43)
      • Baekjoon (40)
      • Programmers (2)
    • 📚 Reading (12)
      • Tech (9)
      • Self-Help (3)
    • 💬 Etc (1)
    • 📈 Retrospective (1)

블로그 메뉴

  • 🌟 깃허브
  • 🏷️ 태그 클라우드
  • 📝 방명록

공지사항

인기 글

태그

  • elasticsearch
  • 브루트포스
  • 문자열
  • 그리디
  • 백준
  • java
  • E-ROOM
  • briefing
  • SQL
  • boj
  • 독서
  • 백트래킹
  • MySQL
  • 구현
  • Programmers

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
개발자 피터

Peter's Dev Blog

📚 Reading/Tech

이펙티브 자바 - 6장. 열거 타입과 애너테이션

2023. 1. 24. 15:18
728x90
반응형

6장 - 열거 타입과 애너테이션


💡 int 상수 대신 열거 타입을 사용하라

“열거 타입은 정수 상수보다 더 읽기 쉽고 안전하고 강력하다”

1. 정수 열거 패턴

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 pattern)의 문제점
    1. 타입 안전을 보장할 방법이 없음
    2. 표현력도 좋지 않음
    • ex) 오렌지를 건네야 할 메서드에 사과를 보내고 동등 연산자(==)로 비교하더라도 컴파일러는 아무런 경고 메시지를 출력하지 않음.
  • 자바가 정수 열거 패턴을 위한 별도의 이름공간(namespace)를 지원하지 않기 때문에 어쩔 수 없이 APPLE, ORANGE 등의 접두어를 사용해서 이름 충돌을 방지해야 함.
  • 정수 대신 문자열 상수를 사용하는 문자열 열거 패턴(string enum pattern)은 더 나쁨.
    • 문자열 상수의 이름 대신 문자열 값을 그대로 하드코딩하게 만들기 때문
    • 하드코딩한 문자열에 오타가 있어도 컴파일러는 확인할 길이 없으므로 자연스럽게 런타임 버그가 생김

 

2. 열거 타입

public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NEVEL, TEMPLE, BLOOD }
  • 열거 타입 설명
    • 완전한 형태의 클래스이며 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개함.
    • 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final임.
    • 싱글턴을 일반화한 형태, 싱글턴은 원소가 하나인 열거타입.
  • 열거 타입의 장점
    • 열거 타입은 컴파일 타임 안전성을 제공함.
    • 각자의 이름공간이 있어서 이름이 같은 상수도 평화롭게 공존함.
    • 열거 타입에는 임의의 메서드나 필드를 추가할 수 있고, 임의의 인터페이스를 구현하게 할 수도 있음
  • 데이터와 메서드를 갖는 열거 타입
    • 열거 타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 됨.
    • 열거 타입은 근본적으로 불변이므로 모든 필드는 final이어야 함.
// 데이터와 메서드를 갖는 열거 타입
public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS  (4.869e+24, 6.052e6),
    EARTH  (5.975e+24, 6.378e6),
    MARS   (6.419e+23, 3.393e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN (5.685e+26, 6.027e7),
    URANUS (8.683e+25, 2.556e7),
    NEPTUNE(1.024e+26, 2.477e7);

    private final double mass;           // 질량(단위: 킬로그램)
    private final double radius;         // 반지름(단위: 미터)
    private final double surfaceGravity; // 표면중력(단위: m / s^2)

    // 중력상수(단위: m^3 / kg s^2)
    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;  // F = ma
    }
}
  • 상수별 메서드 구현을 활용한 열거 타입
    • 상수별로 다르게 동작하는 코드를 구현하고자 할 때 상수별 메서드 구현(constant-specific method implementation)을 사용하면 됨.
    • 아래 예시에서는 apply라는 추상 메서드를 선언하고 각 상수별 클래스 몸체, 즉 각 상수에서 자신에 맞게 재정의하였음.
// 상수별 클래스 몸체(class body)와 데이터를 사용한 열거 타입
public enum 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; }
    },
    DIVIDE("/") {
        public double apply(double x, double y) { return x / y; }
    };

    private final String symbol;

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

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

    public abstract double apply(double x, double y);

    // 코드 34-7 열거 타입용 fromString 메서드 구현하기 (216쪽)
    private static final Map<String, Operation> stringToEnum =
            Stream.of(values()).collect(
                    toMap(Object::toString, e -> e));

    // 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
    public static Optional<Operation> fromString(String symbol) {
        return Optional.ofNullable(stringToEnum.get(symbol));
    }

    public static void main(String[] args) {
        double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        for (Operation op : Operation.values())
            System.out.printf("%f %s %f = %f%n",
                    x, op, y, op.apply(x, y));
    }
}
  • 전략 열거 타입 패턴
    • PayrollDay 열거 타입의 생성자에서 적절한 잔업수당 전략(PayType)을 선택
    • PayrollDay 열거 타입은 잔업수당 계산을 그 전략 열거 패턴에 위임하여, switch문이나 상수별 메서드 구현없이 안전하고 유연하게 설계.
// 전략 열거 타입 패턴
enum PayrollDay {
    MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
    THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
    SATURDAY(WEEKEND), SUNDAY(WEEKEND);

    private final PayType payType;

    PayrollDay(PayType payType) { this.payType = payType; }

    int pay(int minutesWorked, int payRate) {
        return payType.pay(minutesWorked, payRate);
    }

    // 전략 열거 타입
    enum PayType {
        WEEKDAY {
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked <= MINS_PER_SHIFT ? 0 :
                        (minsWorked - MINS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked * payRate / 2;
            }
        };

        abstract int overtimePay(int mins, int payRate);
        private static final int MINS_PER_SHIFT = 8 * 60;

        int pay(int minsWorked, int payRate) {
            int basePay = minsWorked * payRate;
            return basePay + overtimePay(minsWorked, payRate);
        }
    }

    public static void main(String[] args) {
        for (PayrollDay day : values())
            System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1));
    }
}
  • 유용한 열거 타입 메서드
    • values() : 정의된 상수들의 값을 배열에 담아 반환.
    • valueOf(String) : 상수 이름을 문자열로 입력받아 그 이름에 해당하는 상수를 반환
  • 정리
    • 필요한 원소를 컴파일타임에 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자.
    • 열거 타입의 어떤 메서드가 상수별로 다르게 동작해야할 경우
      • switch문 대신 상수별 메서드 구현 사용하기
    • 열거 타입의 상수 일부가 같은 동작을 공유한다면?
      • 전략 열거 타입 패턴 사용하기

 


💡 ordinal 메서드 대신 인스턴스 필드를 사용하라

“열거 타입 상수에 연결된 값은 ordinal메서드로 얻지 말고, 인스턴스 필드에 저장하자”

1. ordinal 메서드

  • ordinal() : 해당 상수가 그 열거 타입에서 몇 번째 위치인지를 반환
  • ordinal을 잘못 사용한 예시
    • 유지보수하기 끔찍한 코드다..
    • 상수 선언을 바꾸는 순간 numberOfMusicians가 오동작하며, 이미 사용 중인 정수와 값이 같은 상수는 추가할 방법이 없음.
      • ex) 8중주가 이미 있으니 똑같이 8명이 연주하는 복4중주는 추가할 수 없음
public enum Ensemble {
        SOLO, DUET, TRIO, QUARTET, QUINTET,
        SEXTET, SEPTET, OCTET, NONET, DECTET;

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

 

2. 인스턴스 필드

→ 인스턴스 필드에 저장하면 위와 같은 문제들이 말끔히 해결됨

  • 인스턴스 필드를 사용한 예
// 인스턴스 필드에 정수 데이터를 저장하는 열거 타입
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; }
}

 


💡 비트 필드 대신 EnumSet을 사용하라

“EnumSet 클래스 : 비트 필드 수준의 명료함과 성능 + 열거 타입의 장점”

1. 비트 필드

  • 비트 필드 열거 상수 예시
    • 비트 필드(bit field) : 비트별 OR을 사용해 여러 상수를 하나의 집합으로 모은 것.
    • text.applyStyles(STYLE_BOLD | STYLE_ITALIC); 처럼 사용.
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        

        // 매개변수 styles는 0개 이상의 STYLE_ 상수를 비트별 OR한 값이다.
        public void applyStyles(int styles) { ... }
}
  • 비트 필드의 단점
    1. 정수 열거 상수의 단점을 그대로 지님
    2. 비트 필드 값이 그대로 출력되면 단순한 정수 열거 상수를 출력할 때보다 해석하기가 훨씬 어렵다.
      • 어떤 조합으로 비트 필드 값이 나왔는지를 알아내야하기 때문.
    3. 최대 몇 비트가 필요한지를 API작성 시 미리 예측하여 적절한 타입(int or long)을 선택해야 함.

 

2. EnumSet

→ 열거 타입 상수의 값으로 구성된 집합을 효과적으로 표현해주는 EnumSet을 사용하자.
장

  • EnumSet 사용 예시
// EnumSet - 비트 필드를 대체하는 현대적 기법
public class Text {
    public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}

    // 어떤 Set을 넘겨도 되나, EnumSet이 가장 좋다.
    public void applyStyles(Set<Style> styles) {
        System.out.printf("Applying styles %s to text%n",
                Objects.requireNonNull(styles));
    }

    // 사용 예
    public static void main(String[] args) {
        Text text = new Text();
        text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
    }
}
  • EnumSet
    1. Set 인터페이스를 완벽히 구현하며, 타입 안전
    2. 내부는 비트 벡터로 구현되어 있어, 대부분의 경우 비트 필드에 비견되는 성능을 보여줌
    3. removeAll과 retainAll과 같은 대량 작업은 비트를 효율적으로 처리할 수 있는 산술 연산을 써서 구현하였음

 


💡 ordinal 인덱싱 대신 EnumMap을 사용하라

“배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니, 대신 EnumMap을 사용하라”

  • Plant 클래스
// 식물을 아주 단순하게 표현한 클래스 (226쪽)
class Plant {
    enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }

    final String name;
    final LifeCycle lifeCycle;

    Plant(String name, LifeCycle lifeCycle) {
        this.name = name;
        this.lifeCycle = lifeCycle;
    }

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

1. ordinal 인덱싱

  • ordinal()을 배열 인덱스로 사용하는 예시
// ordinal()을 배열 인덱스로 사용 - 따라 하지 말 것!
Set<Plant>[] plantsByLifeCycleArr =
        (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycleArr.length; i++)
    plantsByLifeCycleArr[i] = new HashSet<>();
for (Plant p : garden)
    plantsByLifeCycleArr[p.lifeCycle.ordinal()].add(p);
// 결과 출력
for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
    System.out.printf("%s: %s%n",
            Plant.LifeCycle.values()[i], plantsByLifeCycleArr[i]);
}
  • 문제점
    1. 배열은 제네릭과 호환되지 않으니 비검사 형변환을 수행해야 하고 깔끔히 컴파일되지 않을 것임.
    2. 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 함.
    3. 정확한 정숫값을 사용한다는 것을 직접 보증해야 함. (정수는 열거 타입과 달리 타입 안전하지 않기 때문)
      • 잘못된 값을 사용하면 잘못된 동작을 묵묵히 수행하거나 (운이 좋다면)ArrayIndexOutOfBoundsException을 던질 것임.

 

2. EnumMap

  • EnumMap을 사용하는 예시
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =
        new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values())
    plantsByLifeCycle.put(lc, new HashSet<>());
for (Plant p : garden)
    plantsByLifeCycle.get(p.lifeCycle).add(p);
System.out.println(plantsByLifeCycle);
  • 장점
    1. 더 짧고 명료하며 안전하고 성능도 ordinal()인덱싱 버전과 동일함.
      • EnumMap 내부에서 배열을 사용하기 때문에 성능이 동일함.
    2. 안전하지 않은 형변환은 쓰지 않고, 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공하니 출력 결과에 직접 레이블을 달 일도 없음.
    3. 배열 인덱스를 계산하는 과정에서 오류가 날 가능성도 원천봉쇄됨.
  • 다차원 관계 : EnumMap<..., EnumMap<..>>으로 표현해서 사용.
    // 중첩 EnumMap으로 데이터와 열거 타입 쌍을 연결했다
    public enum Phase {
        SOLID, LIQUID, GAS;
        public enum Transition {
            MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
            BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
            SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);

            private final Phase from;
            private final Phase to;
            Transition(Phase from, Phase to) {
                this.from = from;
                this.to = to;
            }

            // 상전이 맵을 초기화한다.
            private static final Map<Phase, Map<Phase, Transition>>
                    m = Stream.of(values()).collect(groupingBy(t -> t.from,
                    () -> new EnumMap<>(Phase.class),
                    toMap(t -> t.to, t -> t,
                            (x, y) -> y, () -> new EnumMap<>(Phase.class))));

            public static Transition from(Phase from, Phase to) {
                return m.get(from).get(to);
            }
        }

        // 간단한 데모 프로그램 - 깔끔하지 못한 표를 출력한다.
        public static void main(String[] args) {
            for (Phase src : Phase.values()) {
                for (Phase dst : Phase.values()) {
                    Transition transition = Transition.from(src, dst);
                    if (transition != null)
                        System.out.printf("%s에서 %s로 : %s %n", src, dst, transition);
                }
            }
        }
    }

 


💡 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라

“열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 사용해 같은 효과를 낼 수 있음”

1. 열거 타입 확장

  • 열거 타입은 확장할 수 없음.
  • 사실 대부분 상황에서 열거 타입을 확장하는 건 좋지 않은 생각임
    • 확장한 타입 원소는 기반 타입의 원소로 취급하지만 그 반대는 성립하지 않는다면 이상하기 때문
    • 기반 타입과 확장된 타입들의 원소 모두를 순회할 방법도 마땅치 않음.

 

2. 인터페이스 구현 (확장 흉내 내기)

  • 기본 아이디어 : 열거 타입이 임의의 인터페이스를 구현할 수 있다는 사실 이용
  • 연산 코드용 인터페이스를 정의하고 열거 타입이 인터페이스를 구현한 예
public interface Operation {
    double apply(double x, double y);
}

public enum ExtendedOperation implements Operation {
    EXP("^") {
        public double apply(double x, double y) {
            return Math.pow(x, y);
        }
    },
    REMAINDER("%") {
        public double apply(double x, double y) {
            return x % y;
        }
    };
    private final String symbol;
    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
    @Override public String toString() {
        return symbol;
    }

    // 컬렉션 인스턴스를 이용해 확장된 열거 타입의 모든 원소를 사용하는 예 (235쪽)
    public static void main(String[] args) {
        double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        test(Arrays.asList(ExtendedOperation.values()), x, y);
    }
    private static void test(Collection<? extends Operation> opSet,
                             double x, double y) {
        for (Operation op : opSet)
            System.out.printf("%f %s %f = %f%n",
                    x, op, y, op.apply(x, y));
    }
}
  • 장점 : API가 기본 열거 타입을 직접 명시하지 않고 인터페이스 기반으로 작성되었다면 기본 열거 타입 인스턴스가 쓰이는 모든 곳을 새로 확장한 열거 타입의 인스턴스로 대체해 사용할 수 있음
    • ex) 위 코드에서 for (Operation op : opSet)처럼.
  • 한계 : 열거 타입끼리 구현 상속 불가.

 


💡 명명 패턴보다 애너테이션을 사용하라

“어노테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유가 없다”

1. 명명 패턴

  • 명명 패턴 : 변수나 함수의 이름을 일관된 방식으로 작성하는 패턴
  • ex) JUnit은 버전 3까지 테스트 메서드 이름을 test로 시작하게끔 했음.
  • 명명 패턴의 문제점
    1. 오타가 나면 안 됨
      • ex) 실수로 메서드 이름을 tsetSafetyOverride로 지으면 JUnit3은 이 메서드를 무시하고 지나침
    2. 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없음
      • ex) 클래스 이름을 TestSafetyMechanisms로 지어 JUnit에 던져줬다고 해보자. 개발자는 이 클래스에 정의된 테스트 메서드들을 수행해주길 기대하겠지만 JUnit은 클래스 이름에는 관심이 없음
    3. 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없음.
  • 어노테이션은 명명 패턴의 모든 문제점을 해결해주는 개념으로, JUnit도 버전 4부터 전면 도입하였음.

 

2. 마커 어노테이션

  • 마커 어노테이션 : 아무 매개변수 없이 단순히 대상에 마킹(marking)하는 어노테이션
  • 메타 어노테이션 : 어노테이션 선언에 다는 어노테이션
    • @Retention(RetentionPolicy.RUNTIME) : 어노테이션이 런타임에도 유지되어야 한다는 표시
    • @Target(ElementType.METHOD) : 어노테이션이 메서드 선언에서만 사용되어야 한다는 표시
  • 마커 어노테이션 선언 예시
/**
 * 테스트 메서드임을 선언하는 애너테이션이다.
 * 매개변수 없는 정적 메서드 전용이다.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
  • 마커 어노테이션 사용 예시
    • Test 어노테이션에 주석으로 “매개변수 없는 정적 메서드 전용이다.”라고 쓰여있지만, 해당 주석을 컴파일러가 해석해서 강제할 수는 없음
    • 그래서 아래 예시를 원하는 대로 동작시키기 위해서는 적절한 어노테이션 처리기가 필요함
      • 관련 방법은 javax.annotation.processing API 문서 참고.
// 마커 애너테이션을 사용한 프로그램 예
public class Sample {
    @Test
    public static void m1() { }        // 성공해야 한다.
    public static void m2() { }
    @Test public static void m3() {    // 실패해야 한다.
        throw new RuntimeException("실패");
    }
    public static void m4() { }  // 테스트가 아니다.
    @Test public void m5() { }   // 잘못 사용한 예: 정적 메서드가 아니다.
    public static void m6() { }
    @Test public static void m7() {    // 실패해야 한다.
        throw new RuntimeException("실패");
    }
    public static void m8() { }
}

 

3. 어노테이션 with 매개변수

  • 어노테이션에 매개변수를 사용하는 법을 알아보자
  • 매개변수 하나를 받는 어노테이션 선언 예시
    • Class<? extends Throwable> : Throwable을 확장한 클래스의 Class 객체.
/**
 * 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}
  • 매개변수 하나를 받는 어노테이션 사용 예시
// 매개변수 하나짜리 애너테이션을 사용한 프로그램
public class Sample2 {
    @ExceptionTest(ArithmeticException.class)
    public static void m1() {  // 성공해야 한다.
        int i = 0;
        i = i / i;
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m2() {  // 실패해야 한다. (다른 예외 발생)
        int[] a = new int[0];
        int i = a[1];
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m3() { }  // 실패해야 한다. (예외가 발생하지 않음)
}
  • 배열 매개변수를 받는 어노테이션 선언 예시
// 배열 매개변수를 받는 애너테이션 타입
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception>[] value();
}
  • 배열 매개변수를 받는 어노테이션 사용 예시
// 배열 매개변수를 받는 애너테이션을 사용하는 프로그램
public class Sample3 {
    // 이 변형은 원소 하나짜리 매개변수를 받는 애너테이션도 처리할 수 있다
    @ExceptionTest(ArithmeticException.class)
    public static void m1() {  // 성공해야 한다.
        int i = 0;
        i = i / i;
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m2() {  // 실패해야 한다. (다른 예외 발생)
        int[] a = new int[0];
        int i = a[1];
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m3() { }  // 실패해야 한다. (예외가 발생하지 않음)

    // 배열 매개변수를 받는 애너테이션을 사용하는 코드
    @ExceptionTest({ IndexOutOfBoundsException.class,
                     NullPointerException.class })
    public static void doublyBad() {   // 성공해야 한다.
        List<String> list = new ArrayList<>();

        // 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나
        // NullPointerException을 던질 수 있다.
        list.addAll(5, null);
    }
}

 

4. 반복가능 어노테이션

  • 하나의 프로그램 요소에 같은 어노테이션을 여러 번 달도록 만들고 싶은 상황에 사용
  • 반복 가능한 어노테이션 선언 예시
    • 반복 가능 어노테이션 선언 방법 : @Repeatable 메타 어노테이션을 달면 됨
      • 단, @Repeatable 어노테이션의 매개변수로 ‘컨테이너 어노테이션’을 전달해야 함.
      • 컨테이너 어노테이션 : 내부 어노테이션 타입의 배열을 반환하는 value 메서드를 정의 해야함
// 반복 가능한 애너테이션의 컨테이너 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
    ExceptionTest[] value();
}

// 반복 가능한 애너테이션 타입
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}
  • 반복 가능한 어노테이션 사용 예시
// 반복 가능한 애너테이션을 사용한 프로그램
public class Sample4 {
    // 반복 가능 애너테이션을 두 번 단 코드
    @ExceptionTest(IndexOutOfBoundsException.class)
    @ExceptionTest(NullPointerException.class)
    public static void doublyBad() {
        List<String> list = new ArrayList<>();

        // 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나
        // NullPointerException을 던질 수 있다.
        list.addAll(5, null);
    }
}

 


💡 @Override 어노테이션을 일관되게 사용하라

“상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 어노테이션을 달자”

  • 재정의한 모든 메서드에 @Override 어노테이션을 의식적으로 달면 실수했을 때, 컴파일러가 잘 알려줄 것임
  • 예외 경우가 한 가지 있음.
    • 구체 클래스에서 상위 클래스의 추상 메서드를 재정의한 경우엔 이 어노테이션을 달지 않아도 됨. 그런데 단다고 해서 해로울 것도 없음. → 걍 달자.

 


💡 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라

“마커 인터페이스와 마터 어노테이션은 각자의 쓰임이 있다.”

1. 마커 인터페이스

  • 마커 인터페이스 : 아무 메서드도 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스
  • ex) Serializable 인터페이스
  • 마커 인터페이스가 마커 어노테이션보다 나은 점
    1. 마커 인터페이스는 이를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있으나, 마커 어노테이션은 그렇지 않음.
      • 마커 인터페이스는 어엿한 타입이므로 마커 어노테이션을 사용했다면 런타임에야 발견될 오류를 컴파일 타임에 잡을 수 있음
    2. 적용 대상을 더 정밀하게 지정할 수 있음
      • 적용 대상(@Target)을 ElementType.TYPE으로 선언한 어노테이션은 모든 타입(클래스, 인터페이스, 열거 타입, 어노테이션)에 달 수 있음. 부착할 수 있는 타입을 세밀히 제한할 수 없음.
      • 특정 인터페이스를 구현한 클래스에만 적용하고 싶은 마커가 있다면 그냥 마킹하고 싶은 크래스에서만 그 인터페이스를 구현하면 됨.
  • 반대로, 마커 어노테이션이 마커 인터페이스보다 나은 점
    • 거대한 어노테이션 시스템의 지원을 받음

 

2. 마커 어노테이션

  • 그렇다면 마커 어노테이션은 언제 사용해야 하는가?
    • 클래스와 인터페이스 외의 프로그램 요소(모듈, 패키지, 필드, 지역변수 등)에 마킹해야 할 때, 어노테이션을 사용
    • 어노테이션을 활발히 활용하는 프레임워크에서 사용하려는 마커일 때.
  • “이 마킹이 된 객체를 매개변수로 받는 메서드를 작성할 일이 있을까?”
    • “그렇다”일 경우 → 마커 인터페이스를 사용해야 함. 컴파일타임 타입 안정성을 위해..
  • 만약 적용 대상이 Element.TYPE인 마커 어노테이션을 작성하고 있다면, 마커 인터페이스가 낫지는 않을지 고민해보자.
728x90
반응형
저작자표시 (새창열림)

'📚 Reading > Tech' 카테고리의 다른 글

이펙티브 자바 - 8장. 메서드  (0) 2023.01.24
이펙티브 자바 - 7장. 람다와 스트림  (0) 2023.01.24
이펙티브 자바 - 5장. 제네릭  (0) 2023.01.24
이펙티브 자바 - 4장. 클래스와 인터페이스  (0) 2023.01.24
이펙티브 자바 - 3장. 모든 객체의 공통 메서드  (0) 2023.01.24
    '📚 Reading/Tech' 카테고리의 다른 글
    • 이펙티브 자바 - 8장. 메서드
    • 이펙티브 자바 - 7장. 람다와 스트림
    • 이펙티브 자바 - 5장. 제네릭
    • 이펙티브 자바 - 4장. 클래스와 인터페이스
    개발자 피터
    개발자 피터
    Backend Engineer 🔥

    티스토리툴바