공부내용공유

Enum 활용하기 (feat : EnumSet, EnumMap) 본문

Server/Java

Enum 활용하기 (feat : EnumSet, EnumMap)

forfun 2024. 3. 8. 15:34

서론


프로젝트를 진행하다 보면 Enum은 한번쯤은 꼭 사용하게 되는 타입이다.

 

맨 처음 프로젝트에서 Enum을 사용할 때는 단순히 열거를 위해서만 사용하고 타입 안정성을 위해서 사용하는구나 하고 넘어갔었는데
다른 프로젝트를 진행하면서 받은 리뷰를 통해 Enum의 다양한 활용법, Enum을 위한 특별한 (java의) 자료구조등을 알게되었다.

 

이번 글에서는 Enum에 대한 설명보다는 Enum을 어떤식으로 활용할 수 있는지, Enum을 사용할 때 알고있으면 좋은 자료구조에 대해 주로 다룰 예정이다.

 

 

본론


Enum 기본 활용

일단 가장 기본적인 Enum 사용법을 보자

public enum Message {
    NORMAL,
    ENCRYPTED,
    ANONYMOUS,
    RESERVATION,
    KAKAO
}

 

이렇게 사용했을 때 Enum의 장점은 어떤 것들이 있을까?

  • 값들을 제한함으로 type safety를 제공해준다.
  • switch문과 함께 사용할 수 있고 시너지가 좋다.
  • IDE가 오류를 잡아준다.

 

Enum에 값 응집시키기

 

아래 예시와 같이 메세지라는 도메인에서 NORMAL, ENCRYPTED, ANONYMOUS, RESERVATION, KAKAO와 같은 타입이 있고 여러 값들이 각각의 타입에 맵핑되어야 한다고 가정해보자.

NORMAL = ["normal", "NORMAL_MESSAGE", "DEFAULT"]
KAKAO = ["KAKAO", "MOBILE", "THIRD_PARTY"]

 

여러 타입이 있고 각 타입마다 여러 값들이 있다고 하면 어떤식으로 다루고 맵핑해 줄 수 있을까?

 

DB에 따로 테이블을 만들어서 저장

+----+-------------+-------------------+
| id | type        | content           |
+----+-------------+-------------------+
| 1  | NORMAL      | normal            |
| 2  | NORMAL      | NORMAL_MESSAGE    |
| 3  | NORMAL      | DEFAULT           |
| 4  | ENCRYPTED   | ENCRYPTED         |
| 5  | ENCRYPTED   | encrypted         |
| 6  | ENCRYPTED   | SECRET            |
| 7  | ANONYMOUS   | ANONYMOUS         |
| 8  | ANONYMOUS   | NO_NAME           |
| 9  | ANONYMOUS   | A_MESSAGE         |
| 10 | RESERVATION | RESERVATION       |
| 11 | RESERVATION | R_MESSAGE         |
| 12 | RESERVATION | RESERVED          |
| 13 | KAKAO       | KAKAO             |
| 14 | KAKAO       | MOBILE            |
| 15 | KAKAO       | THIRD_PARTY       |
+----+-------------+-------------------+
made by gpt...bb

 

이렇게도 해결할 수는 있지만 데이터가 많지 않고 변경이 잘 일어나지 않는다는 가정하에 DB에 저장하여 조회를 위해 쿼리를 날리는 것은 분명히 부담이 가는 방식이다.

 

if 분기문으로 모든 경우의 수 커버

if("normal" || "NORMAL_MESSAGE" || DEFAULT) return "NORAML"
else if("ENCRYPTED" || "encrypted" || "SECRET")  return "ENCRYPTED"
else if ("ANONYMOUS" || "NO_NAME" || "A_MESSAGE") return "ANONYMOUS"
...

 

위 코드처럼 연관관계가 있는 모든 값들에 대해 분기 처리를 해줄 수도 있지만

  • 해당 분기처리가 다른 곳에서도 사용된다면 해당 코드를 추가해줘야 한다. (DRY 하지 못하다.)
  • 연관관계가 늘어나 코드를 변경하면 책임이 없는 메서드들에게도 변경이 일어난다. (OCP 위반)

등과 같은 단점들이 있다.

 

Enum 활용

   public enum Message {
    NORMAL(100, Arrays.asList("normal", "NORMAL_MESSAGE", "DEFAULT")),
    ENCRYPTED(300, Arrays.asList("ENCRYPTED", "encrypted", "SECRET")),
    ANONYMOUS(500, Arrays.asList("ANONYMOUS", "NO_NAME", "A_MESSAGE")),
    RESERVATION(300, Arrays.asList("RESERVATION", "R_MESSAGE", "RESERVED")),
    KAKAO(30, Arrays.asList("KAKAO", "MOBILE", "THIRD_PARTY"));

    private int cost;

    private List<String> messageCodeList;

    Message(int cost, List<String> messageCodeList) {
        this.cost = cost;
        this.messageCodeList = messageCodeList;
    }

 

위 코드에서 확인할 수 있듯이 java에서 Enum은 하나의 클래스이기 때문에 추가적인 값들을 넣고 사용할 수 있다.

이렇게 Enum을 활용함으로

  • 특정 타입, 값이 추가되어도 Enum 내부에서만 변경이 일어나기 때문에 다른 코드에 영향을 최소화 시킬 수 있다.
  • Enum 클래스 내부에 값 조회 로직을 관리함으로 재사용성, 테스트 등에 용이한 코드를 작성할 수 있다.

 

 

상태와 행위를 같이 관리

 

사실 이 사항은 내가 직접 경험해본 적은 없고 이동욱님의 Enum 활용기에서 본 내용이다. (사실 내 글의 내용도 이동욱님이 이미 잘 정리하신 내용이다.)

 

간단히 설명하자면 추상 메소드 혹은 함수형 인터페이스를 사용하여 Enum 값에 따라 각기 다른 동작을 할 수 있게 만들어주는 것인데

public enum Message {
    NORMAL(){
        @Override
        public void sendMessage(String message) {
            //일반 메세지 발송 프로세스
        }
    },
    ENCRYPTED(){
        @Override
        public void sendMessage(String message) {
            //보안 메일 발송 프로세스
        }
    },
    KAKAO(){
        @Override
        public void sendMessage(String message) {
            //카카오톡 발송 프로세스
        }
    };


    public abstract void sendMessage(String message);

 

이런식으로 활용이 가능하다. 물론 각 로직이 너무 무거워진다면 한 클래스 안에서 관리를 다 하는 것이 맞을까에 대한 고민을 해야한다고 생각한다.

 

 

EnumSet, EnumMap

 

위의 예시들처럼 적절한 상황에 Enum을 사용함으로 좋은 코드를 작성할 수 있다.

 

이렇게 사용하다 보면 Enum을 자료구조에 넣어서 사용해야 할 수 있는데 Factory 패턴을 위한 Enum 활용 사례

특히 Set, Map과 같은 자료구조에 자주 쓰이는데 java에서는 Enum에 최적화된 EnumSet, EnumMap을 제공해준다.

 

EnumSet의 특징 및 장점

// 생성할 때 크기에 따라 larger, regular EnumSet을 생성함을 볼 수 있다.

if (universe.length <= 64)
    return new RegularEnumSet<>(elementType, universe);
else
    return new JumboEnumSet<>(elementType, universe);



 // Set add 구현 코드이다. 비트 연산을 통해 조작함을 볼 수 있다.

 public boolean add(E e) {
    typeCheck(e);

    long oldElements = elements;
    elements |= (1L << ((Enum<?>)e).ordinal());
    return elements != oldElements;
    }
  • 내부에서 bit vectors를 사용함으로 시간, 공간 효율성을 극대화 시킬 수 있다.
  • Null을 허용하지 않는다. (Null 값이 들어올 경우 NullPointerException을 던진다.)
  • 다른 자료구조와 같이 synchronize 되어있지 않다, 만약 멀티쓰레드 환경이고 변경의 경우가 있다면 synchronizedSet 혹은 적절한 조치가 필요하다.

 

이러한 특성을 가지고 있기 때문에 set의 값을 변경하는 것이 아닌 조회를 할 경우 EnumSet으로 어플리케이션을 좀 더
최적화 할 수 있다.

 

EnumMap의 특징 및 장점

 //객체를 생성할 때 Enum의 값들만큼의 길이를 가진 배열을 생성한다.
public EnumMap(Class<K> keyType) {
  this.keyType = keyType;
  keyUniverse = getKeyUniverse(keyType);
  vals = new Object[keyUniverse.length];
 }


 //객체를 생성할 때 Enum의 값들만큼의 길이를 가진 배열을 생성한다.
public V put(K key, V value) {
    typeCheck(key);
    int index = key.ordinal();
    Object oldValue = vals[index];
    vals[index] = maskNull(value);
    if (oldValue == null)
        size++;
    return unmaskNull(oldValue);
 }
  • 이렇게 fit한 배열을 통해 구현하기 때문에 시간, 공간적인 성능이 좋다.

EnumMap 또한 동시성은 보장되지 않기 때문에 멀티스레드 환경에서 변경이 일어날거 같으면 synchronizedMap을 사용해야 한다.

 

 

결론


이렇게 Enum의 다양한 상황에서 활용, 활용을 할 때 사용할 수 있는 최적화된 자료구조 클래스들을 알아봤다.

 

다른 언어와 다르게 단순 숫자가 아닌 클래스임을 잘 활용하여 더 객체지향적인 코드를 만들어보자!