본문 바로가기
언어/JAVA

[JAVA] Effective Java. 6장 열거 타입과 애너테이션

by sebinChu 2024. 1. 12.

개요

회사 업무로 진행하는 이펙티브 자바 세미나 자료 내용이다. 책과 블로그를 참고하였으며, 자세한 코드는 여기서 확인할 수 있다. 

 

 

GitHub - cobinding/effective-java-semina: [JAVA] 인턴십 업무로 진행한 Effective-JAVA 세미나 코드

[JAVA] 인턴십 업무로 진행한 Effective-JAVA 세미나 코드. Contribute to cobinding/effective-java-semina development by creating an account on GitHub.

github.com

 

 

 


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

 

같은 그룹으로 묶이는 int 타입 상수들은 나열하지 말고, enum 클래스를 활용하자. 열거 타입은 그 자체가 클래스이며, 상수들이 개별로 인스턴스를 만들어 public static final로 공개한다.

  • 외부에서 접근 가능한 생성자를 제공하지 않으므로 직접 인스턴스 생성불가. 사실상 모든 필드는 final이다.
  • 임의의 메소드나 필드를 추가할 수 있고 인터페이스를 구현하게 할 수도 있다.
  • 정수 상수보다 가독성이 좋다.
  • 컴파일 타입에 안정성을 제공한다.

 

public class item34 {
   
    public enum Season {
		// 각 상수는 자체적으로 생성자를 가질 수 있음
        SPRING("Warm"), 
        SUMMER("Hot"),
        AUTUMN("Cool"),
        WINTER("Cold");

	// 각 상수는 개별적으로 필드를 가질 수 있음
        private final String description;

        Season(String description) {
            this.description = description;
        }

        public String getDescription() {
            return description;
        }
    }

    public static void main(String[] args) {
        Season currentSeason = Season.SUMMER;

        System.out.println("Current season: " + currentSeason);
        System.out.println("Description: " + currentSeason.getDescription());

        // 외부에서 열거 타입의 인스턴스를 직접 생성할 수 없음
        // Season invalidSeason = new Season("Invalid"); // 컴파일 에러

        // 열거 타입은 클래스이므로 다양한 메소드나 필드를 추가할 수 있음
        System.out.println("All seasons:");
        for (Season season : Season.values()) {
            System.out.println(season + ": " + season.getDescription());
        }
    }
}

 

*상수 값에 따라 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);

		// 8시간을 분 단위로 환산한 것, 표준 근무 시간
    private static final int MINS_PER_SHIFT = 8 * 60;
    
		// 근무 시간과 시급에 따른 임금
    int pay(int minsWorked, int payRate) {
      int basePay = minsWorked * payRate;
      return basePay + overtimePay(minsWorked, payRate);
    }
  }

}

 

열거 타입 사용 기준

  • 필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자.
  • 열거 타입에 정의된 상수가 영원히 불변일 필요는 없다.

 


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

대부분의 열거 타입 상수는 자연스럽게 하나의 정숫값에 대응된다. 그리고 모든 열거 타입은 해당 상수가 그 열거 타입에서 몇 번째 위치인지를 반환하는 ordinal이라는 메서드를 제공한다.

 

 

이러한 ordinal은

  • 상수 선언 순서가 바뀌는 순간 오동작할 가능성이 있다.
  • 값을 중간에 비워둘 수 없기 때문에 더미 상수를 추가해야 한다.

따라서, 열거 타입에 연결된 값은 ordinal 대신 인스턴스 필드에 저장하여 사용한다.

 

 

인스턴스 필드에 저장하여 사용한 예시

public enum DayEnum {
    MONDAY(1),
    TUESDAY(2),
    WEDNESDAY(3);

    private final int dayNumber;

    DayEnum(int dayNumber) {
        this.dayNumber = dayNumber;
    }

    public static void main(String[] args) {
        DayEnum day = DayEnum.MONDAY;
        System.out.println("day = " + day);
    }
}

 

 

ordinal로 정의한 예시

public enum DayOrdinal {
    MONDAY,
    TUESDAY,
    WEDNESDAY;

    // ordinal 메서드를 사용하여 열거 상수 위치 반환
    public int getDayOrdinal() {
        return ordinal() + 1;
    }

    public static void main(String[] args) {
        DayOrdinal dayOrdinal = DayOrdinal.WEDNESDAY;
        System.out.println("dayOrdinal = " + dayOrdinal.getDayOrdinal());
    }
}

 


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

 

비트 필드란?

 

메모리를 절약하기 위해 사용한 도구로 참-거짓값을 결헙해서 하나의 바이트로 만들어, 메모리를 절약한다. 예를 들어, c나 c++같은 경우 정수형을 저장할 때 1~2비트만 필요하지만 메모리는 16비트를 저장해야하는 케이스가 있다. 이러한 낮은 값을 갖는 정수 변수가 많을 때 메모리 소비를 줄이기 위해 사용한다.

 

public class TextAttribute {
    public static final int BOLD = 1 << 0; // 1
    public static final int ITALIC = 1 << 1; // 2
 //    public static final int UNDERLINE = 1 << 2; // 4
 //    public static final int STRIKE = 1 << 3; // 8

    private int style;

    public TextAttribute() {
        // 초기값이 0으로 모든 스타일을 비활성화 해둠
        this.style = 0;
    }

    // 비트 연산을 통해 값 확인
    public boolean isBold() {
        return (style & BOLD) != 0;
    }

    public boolean isItalic() {
        return (style & ITALIC) != 0;
    }

    public void setBold(boolean bold) {
        style = bold ? style | BOLD : style & ~BOLD;
    }

    public void setItalic(boolean italic) {
        style = italic ? style | ITALIC : style & ~ITALIC;
    }

    public static void main(String[] args) {
        TextAttribute textAttribute = new TextAttribute();

        textAttribute.setBold(true);
        textAttribute.setItalic(false);

        System.out.println("textAttribute is Bold = " + textAttribute.isBold());
        System.out.println("textAttribute is Italic = " + textAttribute.isItalic());
    }
}

 

 

  • 비트 필드는 비트별 연산을 통해서 집합 연산을 효율적으로 수행하지만, item34의 정수 열거 상수의 단점을 그대로 갖는다.
  • 비트 필드값이 그대로 노출되면 코드 해석이 어렵다.
  • 비트 필드 하나에 녹아있는 모든 원소를 순회하기가 까다롭다.
  • 최대 몇 비트가 필요한지를 미리 예측하여 적절한 타입(int or long)을 선택해야 한다.

 

⇒ 따라서 bit 필드 대신 java.util.EnumSet을 사용하자.

  • Set 인퍼페이스를 구현하여 어떤 Set 구현체와도 사용이 가능하다.
  • 내부가 비트 벡터로 구현되어있다. EnumSet 전체를 long 변수 하나로 표현하여 비트 필드에 비견되는 성능을 보여준다.
  • EnumSet은 불변 객체 못 만드는데, 만들고 싶으면 guava 사용하기Collections.unmodifiableSet() 으로 감싸서 사용하면 불변 가능

 

public class TextAttributeEnumSet {
    public enum Style {
        BOLD,
        ITALIC,
        UNDERLINE,
        STRIKE;
    }

    private EnumSet<Style> styles;
    
    public TextAttributeEnumSet() {
        this.styles = EnumSet.noneOf(Style.class);
    }
    
    public void setStyle(Style style, boolean active) {
        if (active) {
            styles.add(style);
        } else {
            styles.remove(style);
        }
    }
    
    public boolean isStyleActive(Style style) {
        return styles.contains(style);
    }

    public static void main(String[] args) {
        TextAttributeEnumSet textAttributeEnumSet = new TextAttributeEnumSet();
        
        textAttributeEnumSet.setStyle(Style.BOLD, true);
        textAttributeEnumSet.setStyle(Style.ITALIC, false);

        System.out.println("textAttributeEnumSet.isStyleActive(Style.BOLD) = " + textAttributeEnumSet.isStyleActive(Style.BOLD));
        System.out.println("textAttributeEnumSet.isStyleActive(Style.ITALIC) = " + textAttributeEnumSet.isStyleActive(Style.ITALIC));
    }
    
}

 


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

 

item34에서 밝혔듯, 열거 타입은 인터페이스로 구현이 가능하다.

열거 타입은 웹만하면 확장을 지양해야 한다. 확장한 타입의 원소를 기반 타입의 원소로 취급한다면 그 반대도 성립해야 하는데, 열거 타입은 그렇지 않다. 따라서 기반 타입과 확장된 타입들의 원소 모두를 순회할 방법도 마땅치 않다.

 

예외! 확장할 수 있는 열거 타입이 어울리는 쓰임 ⇒ 연산 코드

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;
            }
        };
        private final String symbol;

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

        @Override
        public String toString() {
            return symbol;
        }
    }
}
  • 아래와 같은 방법으로 enum을 인터페이스로 확장 가능

 

연산을 구현할 때 좋은 방식

public enum ExtendedOperation implements Operation {
    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;

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

 


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

명명 패턴이란?

변수나 함수의 이름을 일관된 방식으로 작성하는 채턴

예를 들면 JUnit3의 테스트 메서드 이름은 항상 test로 시작한다.

 

 

단점

  • test를 tset로 오타를 내게된다면, 테스트 메서드로 인식하지 못하고 테스트를 수행하지 않는다.
  • 명명 패턴을 의도한 곳에서, 올바르게 사용된다는 보증이 없다.
    • 예를 들어 클래스 이름을 TestSafety로 지어서 JUni에 줘도, 테스트는 수행되지 않으며 경고 메시지조차 없다.
  • 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.

⇒ 에너테이션이 이러한 명명 패턴의 문제들을 해결해준다. (애너테이션은 JUnit4에서 도입되었다.)

 

 

마커 애너테이션

아무 매개변수 없이 단순히 대상에 마킹하는 용도

/**
* 테스트 메서드임을 선언하는 JUnit애너테이션이다.
* 매개변수 없는 정적 메서드 전용
*/

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {}

 

⇒ 프로그래머가 Test 이름에 오타를 내거나 메서드 선언 외의 프로그램 요소에 달면 컴파일 오류를 내준다.

@Retention: Test가 런타임에도 유지되어야 한다는 표시

@Target : Test가 반드시 메서드에 선언되어야 한다는 표시

 

 

마커 애너테이션을 사용한 프로그램 예시

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() { }
}

 

 

매개변수를 받는 애너테이션 타입

/**
 * 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
 */

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Throwable> value();  // 매개변수
}

 

이 애너테이션의 매개변수 타입은 Class<? extends Throwable>이다. 이는 Throwable을 확장한 클래스의 Class 객체라는 뜻으로, 모든 예외와 오류 타입을 수용한다.

 

 

 


item40. @Override 애너테이션을 일관되게 사용하라.

  • 상위 클래스의 메서드를 재정의 하려는 모든 메서드에 @Override 애너테이션을 달아야 한다. 재정의 의도를 명확하게 나타낼 수 있고, 컴파일러가 잘못된 부분을 명확히 알려준다.
  • 구체 클래스에서 상위 클래스의 추상 메서드를 재정의할 때는 @Override를 달지 않아도 된다. 구체 클래스인데 아직 구현하지 않은 추상 메서드가 있다면 컴파일러가 이를 알려주기 때문이다.
    • 하지만 @Override를 일괄적으로 다는 게 좋아 보인다면 @Override를 달아줘도 상관없다. 그렇게 하는 것이 좋다!

댓글