JAVA/Effective Java

[아이템 38] 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라

꾸준함. 2024. 2. 27. 17:00

아이템 34에서 언급한 대로 열거 타입은 거의 모든 상황에서 타입 안전 열거 패턴보다 우수합니다.

단, 한가지 예외 사항이 있습니다.

  • Enum 클래스는 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 상속이 안되기 때문에 확장 불가
  • 반면 타입 안전 열거 패턴은 확장이 가능

 

Enum 타입이 확장 불가한 이유

대부분 상황에서 열거 타입을 확장하는 것은 좋지 않을 가능성이 높으며 이유는 다음과 같습니다.

  • Enum 타입은 각각 고유한 상수 값의 집합을 나타내며, 서로 다른 enum 타입 간에는 상수 값들이 일치하지 않아 서로 호환되지 않기 때문에 상속과 확장이 어려움
    • 기반(enum) 타입에서 파생된(enum을 확장한) 타입의 원소는 기반 타입의 원소로 취급되지만, 그 반대는 성립하지 않음
    • 즉, 확장한 타입의 원소는 기반 타입의 원소와 호환되며, 기반 타입의 변수에 할당할 수 있음
    • 하지만 반대로 기반 타입의 원소는 확장한 타입의 변수에 할당할 수 없음

 

  • 확장성을 높이려면 고려할 요소가 늘어나 설계와 구현이 늘어남

 

 

 

인터페이스를 통해 열거 타입을 확장하는 것처럼 보이는 방법

열거 타입이 임의의 인터페이스를 구현할 수 있다는 사실을 이용하면 열거 타입으로도 확장하는 것과 유사한 효과를 얻을 수 있습니다.

연산 코드용 인터페이스를 정의하고 열거 타입이 이 인터페이스를 구현하게 하면 됩니다.

이때 Enum 타입이 그 인터페이스의 표준 구현체 역할을 합니다.

연산 코드 Operation을 예시로 들자면 다음과 같습니다.

 

 

 

 

코드 부연 설명

  • 열거 타입인 BasicOperation은 확장할 수 없지만 인터페이스인 Operation은 확장할 수 있고 해당 인터페이스를 연산의 타입으로 사용하면 됨
  • 연산 타입을 확장해 지수 연산(EXP)과 나머지 연산(REMAINDER)을 추가하고 싶을 때 Operation 인터페이스를 구현한 열거 타입 ExtendedOperation을 작성
  • 두 열거 타입 모두 Operation 인터페이스를 사용하도록 작성되었기 때문에 새로 작성한 연산도 기존 연산을 쓰던 곳이면 어디든 사용 가능
    • apply 메서드가 인터페이스에 선언되어 있으므로 Enum 타입에 apply 메서드를 별도 추상 메서드로 선언하지 않아도 됨

 

위 코드는 인터페이스를 통해 확장 효과를 모방했기 때문에, 다형성을 적용할 수 있습니다.

 

1. 클래스 리터럴을 넘겨 새로 작성한 연산 확인하는 방법

 

 

2. 한정적 와일드카드 타입인 Collection<? extends Operation>을 넘겨 확인하는 방법

 

 

2번째 방법은 덜 복잡하면서 여러 구현 타입의 연산을 조합해 호출할 수 있어 유연합니다.

그러나 특정 연산에서는 EnumSet과 EnumMap을 사용할 수 없다는 단점이 있습니다.

  • test 메서드는 Operation 인터페이스를 구현한 열거 타입의 컬렉션을 받아와서 각각의 연산을 수행하는 기능을 가지고  있음
  • 해당 메서드의 인자 opSet은 Operation을 구현한 어떤 타입의 컬렉션이라도 받을 수 있도록 선언되어 있음
  • 그러나 EnumSet과 EnumMap은 특정한 열거 타입에 대한 비트 벡터와 맵을 생성하는 것이 목적이기 때문에, Operation을 구현한 모든 타입을 다루기에는 적합하지 않음

 

Enum 타입을 인터페이스를 이용해 확장할 때 발생하는 사소한 문제점

Enum 타입을 인터페이스를 이용해 확장할 때 열거 타입끼리 구현을 상속할 수 없는 사소한 문제점이 존재합니다.

  • 아무 상태에도 의존하지 않는 경우에는 default 메서드를 인터페이스에 추가하는 방법도 있지만
  • Operation의 경우 연산 기호를 저장하고 찾는 로직이 BasicOperation과 ExtendedOperation 둘 다 들어가야 함

 

 

현재는 Operation 구현체가 적어 중복된 부분이 적어서 문제가 되지 않습니다.

그러나 공유하는 기능이 많을 경우 해당 부분을 별도의 도우미 클래스나 정적 도우미 메서드로 분리하는 방식으로 코드 중복을 제거하는 것이 권장됩니다.

 

 

자바 라이브러리 예시

자바에서 제공하는 nio 라이브러리에서 이번 아이템에서 소개한 패턴을 사용합니다.

 

 

 

LinkOption Enum 클래스가 CopyOption가 OpenOption 인터페이스를 구현할 것을 확인할 수 있습니다.

  • OpenOption은 파일을 열 때 옵션을 지정하는 인터페이스
      • LinkOption은 링크를 따를지 여부를 지정하는 옵션으로 파일을 열 때 이 옵션을 사용할 수 있음
      • ex) Files.newInputStream, Files.newOutputStream 등의 메서드에서 OpenOption을 받는 매개변수로 LinkOption.NOFOLLOW_LINKS를 전달하여 심볼릭 링크를 따르지 않도록 지정할 수 있음

 

 

  • CopyOption은 파일 복사 시의 옵션을 지정하는 인터페이스
    • LinkOption은 파일 복사 시에도 사용될 수 있음
    • ex) Files.copy 메서드에서 CopyOption을 받는 매개변수로 LinkOption.NOFOLLOW_LINKS를 전달하여 심볼릭 링크 대신에 심볼릭 링크가 참조하는 파일을 복사할 수 있음

 

  • 즉, 이러한 인터페이스들을 구현함으로써 LinkOption을 특정 파일 작업에 적용할 수 있게 하고, API가 확장 가능하도록 설계
    • 개발자는 파일을 열거나 복사할 때 특정 옵션을 지정하여 파일 작업을 미세하게 제어할 수 있음

 

번외로 StandardCopyOption Enum 클래스 또한 CopyOption 인터페이스를 구현한 것을 확인할 수 있습니다.

 

 

참고

이펙티브 자바

 

 

반응형