ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 6장 - 열거타입과 애너테이션
    📕 book/이펙티브 자바 2022. 11. 4. 19:21

    자바의 특수한 목적의 참조타입 두가지

    • 클래스의 일종인 열거 타입(Enum)
    • 인터페이스의 일종인 어노테이션(annotation)

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


    열거 타입은 일정 개수의 상수 값을 정의한 후, 그 외의 값은 허용하지 않는 타입이다.

    자바에서 열거 타입을 지원하기 전에는 정수 상수를 한 묶음 선언해서 사용하곤 했다.

    public static final int APPPLE_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) 기법에는 단점이 많다.

    • 타입 안전을 보장할 방법이 없다.
    • 표현력도 좋지 않다.

    평범한 상수를 나열한 것이라 컴파일하면 다른 타입을 사용하더라도 컴파일 에러가 발생하지 않는다.

    그리고 값이 바뀌면 다시 컴파일을 해야하며, 단지 숫자로만 보여 문자열로 출력하기도 어렵다.

    정수 대신 문자열 상수를 사용할 수 있지만, 문자열 상수의 이름 대신 문자열 값을 그대로 하드코딩하기 때문에 더 나쁘다.

    열거 타입(Enum Type)

    public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
    public enum Orange { NAVEL, TEMPLE, BLOOD }

    자바의 열거 타입은 완전한 형태의 클래스라서 다른 언어의 열거 타입보다 훨씬 강력하다.

     

    열거 타입 자체는 클래스이며, 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static 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
      }
    }

    열거 타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.

    • 필드를 public으로 선언해도 되지만, private으로 두고 별도의 public 접근자 메서드를 두는 게 낫다.

     

    상수별 메서드

    열거 타입은 상수별로 다르게 동작하는 코드를 구현하는 수단을 제공한다.

    열거 타입에 추상 메서드를 선언하고 각 상수별 클래스 몸체, 즉 각 상수에서 자신에 맞게 재정의하는 방법

    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;}};
    	  
    	public abstract double apply(double x, double y);
    }
    • apply 가 추상메서드이기 때문에 재정의를 하지 않을 경우 컴파일 에러가 발생하여 알 수 있다.

     

    단점

    • 열거 타입 상수끼리 코드를 공유하기 어렵다

    → switch 문으로 구현하면 가능하지만 상수마다 case 문을 추가해주는 문제가 발생

    → 좋은 방법 : 중첩 열거 타입을 두어 생성자에서 선택하고, 중첩 열거타입에 따라 분기하는 ‘전략’ 방식 사용

    enum PayrollDay {
      MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY), THURDAY(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);
        }
      }
    }
    • PayrollDay 열거 타입은 잔업수당 계산을 전략 열거 타입(PayType)에 위임
    • 따라서, switch문이나 상수별 메서드 구현이 필요 없게 된다.

     

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


    대부분의 열거 타입 상수는 자연스럽게 하나의 정숫값에 대응된다.

    ordinal()

    • 모든 열거 타입은 해당 상수가 그 열거 타입에서 몇 번째 위치인지를 반환하는 메서드 제공

    ordinal을 사용하는 경우 상수 선언 순서를 바꾸는 순간 오동작한다.

    값을 중간에 비워둘 수도 없어 중간에 사용하지 않는 더미 상수를 추가해야 한다.

     

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

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


    열거한 값들이 주로 집합으로 사용될 경우, 예전에는 각 상수에 서로 다른 2의 거듭제곱 값을 할당한 정수 열거 패턴을 사용했다.

    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) { ... } 
    }
    • 이 처럼 비트별 OR를 사용해 여러 상수를 하나의 집합으로 모을 수 있다.
    • 이렇게 만들어진 집합을 비트 필드(bit field)라 한다.

    비트 필드를 사용하면 비트별 연산을 사용해 합집합과 교집합 같은 집합 연산을 효율적으로 수행할 수 있다.

    하지만, 비트 필드는 정수 열거 상수의 단점을 그대로 지니며, 추가 문제가 있다.

    • 비트 필드 값이 그대로 출력되면 단순한 정수 열거 상수를 출력할 때보다 해석하기가 훨씬 어렵다.
    • 비트 필드 하나에 녹아 있는 모든 원소를 순회하기가 까다롭다.
    • 최대 몇 비트가 필요한지를 미리 예측하여 적절한 타입(int, long)을 선택해야 한다.

     

    ⇒ 더 나은 대안 : java.util 패키지의 EnumSet 클래스

    • 열거 타입 상수의 값으로 구성된 집합을 효과적으로 표현해준다.
    • Set 인터페이스를 완벽히 구현하며, 타입 안전하고, 다른 어떤 Set 구현체와도 함께 사용할 수 있다.
    public class Text {
      public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
      
      // 어떤 Set을 넘겨도 되나, EnumSet이 가장 좋다.
      public void applyStyles(Set<Style> styles) { ... }
    }

    EnumSet은 집합 생성 등 다양한 기능의 정적 팩터리를 제공한다.

    // of 메서드
    text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
    

     

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


    ordinal은 Enum의 선언 순서에 대한 인덱스를 가져올 수 있다.

    그러나 이 ordinal 메서드 값을 배열의 인덱스로 활용하면 여러 문제가 발생한다.

    • 배열은 제네릭과 호환되지 않아 비검사 형변환을 수행해야 한다.
    • 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다.
    • 정확한 정숫값을 사용한다는 타입안정성이 없다.
      • 직접 보증해야 한다.

    ⇒ 열거 타입을 키로 사용하도록 설계한 아주 빠른 Map 구현체, EnumMap 사용

    • 내부에서 배열을 사용
    • 내부 구현 방식을 안으로 숨겨서 Map의 타입 안전성과 배열의 성능을 모두 얻어낸 것.
    // 스트림을 사용한 코드1 - EnumMap 사용 x
    System.out.println(Arrays.stream(garden)
            .collect(groupingBy(p -> p.lifeCycle)));
    
    // 스트림을 사용한 코드2 - EnumMap을 이용해 데이터와 열거 타입 매핑
    System.out.println(Arrays.stream(garden)
            .collect(groupingBy(p -> p.lifeCycle,
                () -> new EnumMap<>(LifeCycle.class), toSet())));

    EnumMap 버전은 언제나 생애주기당 하나씩의 중첩 맵을 만들지만, 스트림 버전에서는 해당 생애주기에 속하는 것이 있을 때만 만든다.

     

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


    열거타입은 확장을 할 수 없다.

    • 대부분 상황에서 열거 타입을 확장하는건 좋지 않다.

    대신 인터페이스를 정의하고 열거 타입이 그 인터페이스를 구현하여 비슷한 효과를 낼 수 있다.

    열거 타입은 확장할 수 없지만, 인터페이스는 확장할 수 있어 인터페이스를 연산의 타입으로 이용

    • 인터페이스를 구현한 또 다른 열거타입을 정의해 기존에 정의한 열거타입을 대체할 수 있다.

    인터페이스를 이용해 확장 가능한 열거 타입을 흉내내는 방식의 문제

    • 열거 타입끼리 구현을 상속할 수 없다.

    그러나, 아무 상태에도 의존하지 않는다면 디폴트 구현을 통해 인터페이스에 추가하거나

    중복되는 코드가 있다면 별도의 도우미 클래스나 정적 도우미 메서드로 분리하여 사용한다.

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


    도구나 프레임워크가 특별히 다뤄야 할 프로그램 요소에는 딱 구분되는 명명 패턴을 적용해 왔다.

    하지만, 단점이 존재

    1. 오타가 나면 안된다.
    2. 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다.
    3. 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.

    애너테이션은 이 모든 문제를 해결해준다.

    • ex) JUnit4 에서 도입된 @Test
    import java.lang.annotation.*;
    
    /**
     * 테스트 메서드임을 선언하는 어노테이션이다.
     * 매개변수 없는 정적 메서드 전용이다.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Test { }
    • @Test 어노테이션 타입 선언에도 @Retention, @Target와 같은 메타어노테이션이 달려있다.

    메타어노테이션

    • 런타임시 유지, 메서드에만 사용…등
    • 어노테이션의 여러 조건을 명시하는데 사용할 수 있다.
    • 따라서 클래스 선언, 필드 선언 등 다른 프로그램 요소에는 달 수 없다.

    @Test 어노테이션이 클래스의 의미에 직접적인 영향을 주지 않지만 그저 애너테이션에 관심 있는 도구에서 특별한 처리를 할 기회를 준다

    • 도구 예 : RunTests

    @ExceptionTest 는 특정 예외를 던져야 성공하는 테스트를 지원하는 어노테이션

    import java.lang.annotation.*;
    
    /**
     * 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface ExceptionTest {
      Class<? extends Throwable> value();
    }
    • 이 어노테이션의 매개변수 타입인 Class<? extends Throwable>은 Throwable을 확장한 Class 객체라는 뜻으로 모든 예외 타입을 수용한다.

    예외를 여러개 명시하고 하나가 성공하도록 할 수도 있다.

    @ExceptionTest의 매개변수 타입을 Class 객체의 배열로 수정

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface ExceptionTest {
      Class<? extends Throwable>[] value();
    }
    @ExceptionTest({ IndexOutOfBoundsException.class, NullPointerException.class })
    public static void doublyBad() {
      List<String> list = new ArrayList<>();
      
      // 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나 NullPointerException을 던질 수 있다.
      list.addAll(5, null);
    }
    • @ExceptionTest 어노테이션에 배열을 매개변수로 전달하기 위해선
    • 원소들을 중괄호로 감싸고 쉼표로 구분한다.

    자바 8에서는 배열 매개변수 대신에 @Repeatable 메타어노테이션을 이용해 여러 개의 값을 받는 어노테이션을 만들 수 있다.

    @Repeatable를 단 어노테이션은 하나의 요소에 여러번 달 수 있지만 주의할 점이 있다.

    • @Repeatable을 단 어노테이션을 반환하는 ‘컨테이너 어노테이션’을 하나 더 정의하고, @Repeatable에 이 컨테이너 어노테이션의 class 객체를 매개변수로 전달해야 한다.
    • 컨테이너 어노테이션은 내부 어노테이션 타입의 배열을 반환하는 value 메서드를 정의해야 한다.
    • 컨테이너 어노테이션 타입에는 @Retention 과 @Target을 명시해야 한다.
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @Repeatable(ExceptionTestContainer.class)
    public @interface ExceptionTest {
      Class<? extends Throwable> value();
    }
    
    // 컨테이너 어노테이션
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface ExceptionTestContainer {
      ExceptionTest[] value();
    }

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

    자바 프로그래머라면 예외 없이 자바가 제공하는 어노테이션 타입들을 사용해보자

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


    @Override

    • 자바가 기본으로 제공하는 어노테이션
    • 메서드 선언에만 달 수 있다.
    • 상위 타입의 메서드를 재정의했음을 뜻한다.
    • 일관되게 사용하면 여러 악명 높은 버그들을 예방해준다.
      • 재정의(overriding)이 아닌 다중정의(overloading) 하는 것 같은 버그

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

    예외는 한가지이다.

    • 구체 클래스에서 상위 클래스의 추상 클래스 메서드를 재정의할 때는 굳이 @Override를 달지 않아도 된다.

    @Override는 클래스뿐 아니라 인터페이스의 메서드를 재정의할 때도 사용할 수 있다.

     

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


    마커 인터페이스

    아무 메서드를 담고 있지 않고 자신을 구현하는 클래스가 특정 속성을 가지고 있음을 표시해주는 인터페이스

    • ex) Serializable 인터페이스 → 자신을 구현한 클래스는 직렬화할 수 있다고 알려준다.

    마커 인터페이스가 마커 어노테이션보다 나은 두 가지

    • 마커 인터페이스는 이를 구현한 클래스의 인스턴스를 구분하는 타입으로 쓸 수 있으나, 마커 어노테이션은 그렇지 않다.
      • 마커 인터페이스는 타입이기 때문에, 마커 어노테이션을 사용하면 런타임에 발견될 오류를 컴파일타임에 잡을 수 있다.
    • 마커 인터페이스는 적용 대상을 더 정밀하게 지정할 수 있다.
      • 어노테이션은 모든 타입(클래스, 인터페이스, 열거 타입, 어노테이션)에 달 수 있다.
        • 더 세밀하게 제한하지 못한다.
      • 그러나, 마커 인터페이스는 마킹하고 싶은 클래스에만 그 인터페이스를 구현(확장) 하면 된다.

    마커 어노테이션이 마커 인터페이스보다 나은 점

    • 거대한 어노테이션 시스템의 지원을 받는다.
    • 어노테이션을 적극적으로 활용하는 프레임워크에서는 마커 어노테이션을 쓰는게 일관성을 지키는데 좋다.

    언제 마커 인터페이스? 마커 어노테이션?

    • 확실한 것은 클래스와 인터페이스 외의 프로그램 요소에 마킹 ⇒ 마커 어노테이션
    • 마커를 클래스나 인터페이스에 적용해야 하는 경우
      • ‘이 마킹이 된 객체를 매개변수로 받는 메서드를 작성할 일이 있을까’ 를 고민
        • 그렇다라면 마커 인터페이스를 사용
      • 마커 인터페이스를 사용해 컴파일타임에 오류를 잡아낼 수 있다.

     

    참고 자료

    http://www.yes24.com/Product/Goods/65551284

    '📕 book > 이펙티브 자바' 카테고리의 다른 글

    7장 - 람다와 스트림  (0) 2022.11.07
    5장 - 제네릭  (0) 2022.11.01
    4장 - 클래스와 인터페이스  (0) 2022.11.01
    3장 - 모든 객체의 공통 메서드  (0) 2022.10.26
    2장 - 객체 생성과 파괴  (0) 2022.10.25

    댓글

Designed by Tistory.