Java Enum

JooMal·2021년 12월 13일
1

Java와 Spring

목록 보기
2/2

Java Enum은 "열거형" 정도로만 막연하게 알고 있었는데, 실무에서는 많이 그리고 유용하게 사용하는 듯 해서 이번 주차에는 Enum의 동작을 공부해보게 되었다.

목차

  1. Enum의 이상한 부분들 살펴보기
  2. Enum을 열어보자 : 추상 클래스, 자바 제네릭스
  3. Enum의 동작 - 컴파일되는 과정
  4. 만들어보면서 이해하기
  5. 왜 사용할까?

Enum의 이상한 부분들 살펴보기

코드들을 살펴보니, 보통은 다음과 같은 형태로 Enum을 사용하고 있었다.

public enum NetworkType1 {
    HTTP("0001"),
    TCP("0002"),
    ;

    private String code;
    NetworkType1(String code) {
        this.code = code;
    }
    
    ... // 그 외의 동작들
}

그 외의 동작들이란,

  • Enum의 추가속성 값(ex. "0001", "0002")에 해당되는 Enum(ex. HTTP, TCP)을 반환해주기 위한 Map 생성 & 추가속성 값이 들어오면 해당되는 Enum값을 반환하는 메소드
  • Entity 속성 <--> 데이터베이스 컬럼값 으로 변환될 수 있게 해주는 메소드

등을 찾아볼 수 있었다.

현재 코드에서는 찾아보지 못했지만, 인터넷에서 다른 코드들도 살펴보니 Enum 클래스에서 추상 메소드를 선언 -> Enum 타입에 맞게 각기 다른 일을 수행하도록 override 하는 경우도 자주 찾아볼 수 있었다.

예시

  • 현재 가격에서 VIP면 30%, GOLD면 10%를 할인해줄 때에, VIP와 GOLD를 Enum 으로 명시하고 -> VIP면 (가격)*(1-0.3)을, GOLD면 (가격)*(1-0.1)을 반환하는 클래스

이렇게 Enum 별로 메소드를 override하는 코드로 확장시켜 생각해보면, 다음과 같은 코드를 알아두면 좋을 것 같았다.

public enum NetworkType1 {
    HTTP("0001") {
        @Override
        public void print() {
            System.out.println("Hello HTTP");
        }
    },
    TCP("0002") {
        @Override
        public void print() {
            System.out.println("Hello TCP");
        }
    },
    ;

    private String code;
    NetworkType1(String code) {
        this.code = code;
    }

    public abstract void print();
}

위 코드에서 이해하기 어려웠던 부분은 크게 두 가지였다.

  1. HTTP("0001")만으로 어떻게 NetworkType1 이라는 enum의 인스턴스(?)가 만들어질 수 있는 걸까?
  2. 생성자는 왜 만들어주고 있는 걸까?

이 두 부분을 이해해주기 위해 jdk에서 Enum을 열어보게 되었다.

Enum을 열어보자 : 추상 클래스, 자바 제네릭스

현재 External Libraries로 들어가있는 jdk 1.8에서 rt.jar > java > lang에 가보면 Enum 이라는 클래스가 있다.

public abstract class Enum< E extends Enum<E>> implements Comparable< E >, Serializable {
  private final String name;
  public  final String name() { ... }
  private final int ordinal;
  public  final int ordinal() { ... }

  protected Enum(String name, int ordinal) { ... }

  public String           toString() { ... }
  public final boolean    equals(Object other) { ... }
  public final int        hashCode() { ... }
  protected final Object  clone() throws CloneNotSupportedException { ... }
  public final int        compareTo( E o) { ... }

  public final Class< E > getDeclaringClass() { ... }
  public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { ... }
}

우선 가장 이해하기 어려웠던 부분은 Enum<E Extends Enum<E> 이었다. Enum<E Extends Enum>이라고 쓰면 안됐던 걸까?
다행히 같은 의문을 가진 사람이 쓴 질문글이 있어서, Enum의 내부 동작을 잘 설명해주는 좋은 글을 찾을 수 있었다.

enum Color {RED, BLUE, GREEN}

라는 코드를 작성하게 되면, 컴파일러는 대략 다음과 같은 코드로 변환을 한다.

public final class Color extends Enum<Color> {
  public static final Color[] values() { return (Color[])$VALUES.clone(); }
  public static Color valueOf(String name) { ... }
  private Color(String s, int i) { super(s, i); }

  public static final Color RED;
  public static final Color BLUE;
  public static final Color GREEN;

  private static final Color $VALUES[];

  static {
    RED = new Color("RED", 0);
    BLUE = new Color("BLUE", 1);
    GREEN = new Color("GREEN", 2);
    $VALUES = (new Color[] { RED, BLUE, GREEN });
  }
}

결국 단순한 열거형으로 작성한 코드여도, 내부적으로는

  1. Color 라는 클래스 타입을 typeparameter로 받는 Enum<Color>를 extends 해오게 된다.
  2. 열거형으로 작성한 값들이 static final 인 하나의 객체들로 생성이 된다.

(1) extends Enum<Color>

  • extends를 해올 때, Color는 Enum<Color>에 구현된 메소드를 상속해오게 된다.
  • 그렇기 때문에, Enum의 메소드인 compareTo()에 정확한 타입 파라미터를 넘겨주기 위해서 Enum<E Extends Enum<E>> 를 사용해주고 있음을 알 수 있었다.
    public final int compareTo(E o) {
        Enum<?> other = (Enum<?>)o;
        Enum<E> self = this;
        if (self.getClass() != other.getClass() &&
            self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }
  • Enum의 compareTo에서는 제네릭으로 E를 받아와 사용해주고 있다. 결국 Enum<E Extends Enum<E>>Color Extends Enum<Color>로 컴파일되고 -> Enum<Color>에서 extends된 Color는 정확히 필요한 대로 type parameter인 E를 받아와 compareTo가 동작하고 있음을 알 수 있다.
  • 결국 : Enum의 subtype인 클래스 타입(ex. Color extends Enum<Color>)만을 정확히! 타입 파라미터로 받기 위해 위와 같은 방식(=Enum<E Extends Enum<E>>)을 사용하고 있다.

(2) public static final Color RED

나아가, enum에서 열겨형으로 만들어졌던 RED, BLUE, GREEN의 값은 해당 enum 타입의 객체를 매번 생성하지 않더라도 Color.RED 와 같은 식으로 참조할 수 있어야 하고(=static), 한 번 할당된 변수의 값이 바뀌는 것을 허용하지 않기 때문에(=final) static final 형태로 객체를 만들어주고 있는 것이었다.

왜 static final 일까?

  • final 키워드 : 상수, 메소드, 클래스를 정의한 후 변경하지 못하게 할 때 사용한다.
    1. 상수 : 값 재할당을 금지한다.
    1. 메소드 : 오버라이딩을 못하게 한다.
    2. 클래스 : 상속을 못하게 한다.
  • static 키워드 : Static 영역(=클래스 영역)에 할당되게 한다. 주로 클래스들이 할당되며, 모든 객체들이 메모리를 공유할 수 있다. (=> 가비지 컬렉터의 관리 영역 밖이어서 프로그램 종료까지 메모리가 할당된 채로 존재한다.)
    - 메소드에 사용하게 되면, 클래스가 메모리에 올라갈 때 정적 메소드가 자동으로 생성되므로, 인스턴스를 굳이 생성하지 않아도 호출할 수 있다. (유틸리티 함수를 만들 때에 유용! 다만 메모리에 계속 올라가 있으므로 많이 사용하면 좋지 않다.)
  • static final
    - static 영역에 할당되어서 모든 객체들이 참조할 수 있게 할건데, 한 번 정의한 후에는 바꾸면 컴파일 에러를 내게 해줄 것이다.

=> enum에서 열거형으로 만들어진 RED, BLUE, GREEN의 값은 (1) 해당 enum 타입의 객체를 매번 생성하지 않아도 Color.RED 이런 식으로 참조할 수 있어야 하고(=static), (2) 한 번 할당된 변수의 값이 바뀌는 것을 허용하지 않기 때문에(=final) static final 형태로 컴파일되는 듯 하다.

Enum의 동작 - 컴파일되는 과정

결국 정리하자면 Enum은 다음과 같은 동작을 하는 듯 하다.

Week thisWeek = Week.SUNDAY;
Week nextWeek = Week.SUNDAY;
thisWeek == nextWeek // true

위와 같은 코드가 있다고 생각할 때,

  1. (JVM 클래스로더가 로드할 때) enum은 static(클래스 변수)으로, Week.SUNDAY, Week.MONDAY, ...에 대한 메모리 공간이 확보되어 JVM 메소드 영역에 올라가게 되고,

  1. (1) enum을 실제로 호출하면(ex. Week.SUNDAY) Heap 영역에 Week 객체가 만들어지고, (2) 메소드 영역에 할당되어있든 메모리에 힙 영역 객체의 참조가 들어간다.

  1. (1) thisWeek, nextWeek은 지역 변수이기 때문에 Stack 영역에 객체가 만들어지는데, (2) 그 Stack 영역 각각은 메소드 영역에 할당되어 있던 메모리에서 값(=Heap 영역의 참조)을 받아오게 된다.

그렇기 때문에 위에서 본 코드에서 thisWeek == nextWeek이 가능한 것이었다. (같은 힙 영역의 객체를 참조하고 있음)

  • 다만, Enum 클래스에서 선언한 상수들은 하나의 인스턴스로 생성되어, 하나의 인스턴스를 계속 재활용하는 생글톤 형태로 어플리케이션 전체에서 사용되므로,
  • Enum 인스턴스에서 변수를 추가해서 사용하는 것은 멀티 쓰레드 환경에서 위험할 수 있다고 한다..!
    public enum Rank {
    	THREE(3, 4_000),
    	FOUR(4, 10_000),
    	FIVE(5, 30_000);
    	
    	private final int match;
    	private final int money;
    	private int count; // 가령 각 인스턴스의 count는 공유되고 있으므로, 멀티 스레드 환경에서 개발자가 예상하지 못한 에러를 발생시킬 수 있다!
    	
    	Rank(int match, int money) {
    		this.match = match;
    		this.money = money;
    	}
    
    	public void plusCount() {
    		this.count++; 
    	}
    }

나아가서, enum 클래스의 멤버 변수들은 컴파일될 때에 static final로 객체가 각각 만들어지기 때문에, 가령 Enum 끼리 공유하는 변수가 있다면, 상속받은 클래스 간의 접근을 허가하는 protected를 사용해야 에러가 발생하지 않는 것이었다.

관련 예시

public enum NetworkType1 {
    HTTP("0001") {
        @Override
        public void print() {
            System.out.println("Hello HTTP");
        }

        @Override
        public int getTypeCount() {
            return typeCount;
        }
    },
    TCP("0002") {
        @Override
        public void print() {
            System.out.println("Hello TCP");
        }

        @Override
        public int getTypeCount() {
            return typeCount;
        }
    },
    ;

    private String code;
//    private int typeCount; <- 이렇게 private으로 해주면 컴파일 에러가 발생함
    protected int typeCount;
    NetworkType1(String code) {
        this.code = code;
    }

    public abstract void print();
    public abstract int getTypeCount();
}

만들어보면서 이해하기

공부한 것들을 바탕으로 java.lang.Enum을 참고해서 Enum을 만들어보긴 했는데, 그다지 이해에 큰 도움을 주는 것 같진 않았다...

MyEnum

public abstract class MyEnum<E extends MyEnum<E>> implements Comparable<E> {

    static int index = 0;
    String name;
    String name() { return name; }

    int ordinal;
    int ordinal() { return ordinal; }

    MyEnum(String name) {
        this.name = name;
        this.ordinal = index++;
    }

    public int compareTo(E other) {
        return ordinal - other.ordinal;
    }

}

MyEnum을 extends하는 NetworkType2

public abstract class NetworkType2 extends MyEnum<NetworkType2>{

    private String code;

    // enum 사용시 => HTTP("0001"), TCP("0002");
    public static final NetworkType2 HTTP = new NetworkType2("HTTP", "0001") {
        @Override
        public void print() {
            System.out.println("Hello HTTP 2!");
        }
    };

    public static final NetworkType2 TCP = new NetworkType2("TCP", "0002") {
        @Override
        public void print() {
            System.out.println("Hello TCP 2!");
        }
    };

    NetworkType2(String name, String code) {
        super(name);
        this.code = code;
    }

    public abstract void print();
}

다만 만들면서 추가적인 의문이 들었던 부분은,
추상 클래스면 인스턴스를 생성할 수 없는 클래스로 알고 있었는데... 돌이켜보니 new로 객체를 생성해주고 있다. (???)

왜 사용할까?

데이터의 그룹화

상수를 한 곳에 모아두고 사용할 수 있다.

  • 코드가 단순해지고, 가독성이 올라간다.
  • 관리가 용이해진다.
  • 데이터와 연관있는 상태와 행위도 한 곳에서 관리할 수 있다.
  • 이로 인해서 리팩토링을 할 때에 변경해야 하는 범위가 최소화된다.

활용

profile
🏄‍♂️ 𝐒𝐭𝐮𝐝𝐲𝐢𝐧𝐠

0개의 댓글