순간을 성실히, 화려함보단 꾸준함을

[리뷰] 11주차 : Enum 본문

백기선님과 함께 하는 자바 스터디

[리뷰] 11주차 : Enum

폭발토끼 2021. 7. 10. 18:43

Enum은 왜 만들어지게 되었을까?

출처:

https://wisdom-and-record.tistory.com/52
https://yadon079.github.io/2021/java%20study%20halle/week-11-feedback

결론부터 언급하자면 상수를 클래스를 선언하여 사용할때 발생하는 이득들을 모두 취하고, 좀 더 간단한 방법으로 선언하고 사용하기 위해 탄생하게 되었다.

예제는 입력을 받으면 과일의 가격이 출려되는 프로그램이다.

public class EnumEx {
    public static final int APPLE = 1;
    public static final int BANANA = 2;
    public static final int COCONUT = 3;

    public static void main(String[] args) {
        int type = APPLE;
        switch (type) {
            case APPLE:
                System.out.println("360원");
                break;
            case BANANA:
                System.out.println("6000원");
                break;
            case COCONUT:
                System.out.println("2000원");
                break;
        }
    }
}

여기서 각 과일들을 구분시켜주기 위해 1,2,3 가 같은 리터럴을 넣어주었다. 그러나 이런 리터럴들이 각 과일의 명칭과 관련이 있을까?? 당연히 하나도 관련이 없다. 우리가 리터럴을 써준 이유는 단순히 각 과일들을 구분 시켜주기 위한 방법일 뿐이다.

또 다른 문제점이 발생할 수 있다.

public class EnumEx {
    public static final int APPLE = 1;
    public static final int BANANA = 2;
    public static final int COCONUT = 3;

      ...

    public static final int APPLE = 1;
    public static final int GOOGLE = 2;
    public static final int FACEBOOK = 3;

      ...
}

프로그램의 규모가 커지게 되었고 기업들의 정보도 추가되었다고 가정하자. 그러나 과일의 APPLE기업이름의 APPLE 이 겹치는 걸 확인할 수 있다.
이는 컴파일에러를 발생시킬 수 있는 문제점을 가지고 있다.

이를 해결하기 위해 인터페이스를 사용할 수 있다.

interface Fruit {
    int APPLE = 1, BANANA = 2, COCONUT = 3;
}

interface Company {
    int APPLE = 1, GOOGLE = 2, FACEBOOK = 3;
}

그러나 지난 인터페이스에 관련된 글에도 언급했듯이 상수를 인터페이스에서 관리하는건 인터페이스의 목적성과 어긋나는 Anti-Pattern이다.

그럼 다시 위로 돌아가서 문제점을 파악해보자.

if(Fruit.APPLE == Company.APPLE) {
        ...
    }

위의 코드는 문법상 아무런 문제가 발생하지 않는다. 이유는 둘다 int형의 자료형이기 때문이다.
그러나 우린 서로 다른 APPLE임을 알고 있고 같지 않게 만들어야 한다.

이를 해결하기 위해 서로 다른 객체를 생성하여 정의해 주자

class Fruit {
    public static final Fruit APPLE = new Fruit();
    public static final Fruit BANANA = new Fruit();
    public static final Fruit COCONUT = new Fruit();
}

class Company {
    public static final Company APPLE = new Company();
    public static final Company GOOGLE = new Company();
    public static final Company FACEBOOK = new Company();
}

public class EnumEx {
    public static void main(String[] args) {
        if (Fruit.APPLE == Company.APPLE) {}   // 컴파일 에러 발생
    }
}

어떤가??해결되어 보이는가?

위에서 언급한 문제점 3개

  1. 리터럴의 관계성
  2. 서로 다른 개념의 이름 충돌
  3. 서로 다른 개념이지만 비교시 걸러내지 못함

이 모두 해결되었다. 그러나 이러한 방식은 또 다른 문제점을 야기한다.

public class EnumEx {
    public static void main(String[] args) {
        Fruit type = Fruit.APPLE;
        switch (type) {   // 컴파일 에러
            case Fruit.APPLE:
                System.out.println("360원");
                break;
            case Fruit.BANANA:
                System.out.println("6000원");
                break;
            case Fruit.COCONUT:
                System.out.println("2000원");
                break;
        }

    }
}

안타깝게도 사용자 정의 함수는 switch 문의 조건으로 들어가지를 못한다.
(switch문의 조건으로 들어갈 수 있는 데이터 타입은 byte, short, char, int, enum, String, Byte, Short, Character, Integer이다.)

이 때문에 Enum 이 탄생하게 된 것이고 우린 상수들을 좀 더 간편하게 사용할 수 있는 것 이다.

values() 와 valueOf()는 어디에서 오는거지?

public class EnumExample {

    enum Fruit {
        Apple, Banana
    }

    public static void main(String[] args) {
        System.out.println(Fruit.Apple.ordinal());
        System.out.println(Fruit.Banana.ordinal());
    }
}

이 소스를 한번 컴파일하여 바이트코드로 까보자

 // class version 55.0 (55)
// access flags 0x4030
// signature Ljava/lang/Enum<Lcom/example/Main$Fruit;>;
// declaration: com/example/Main$Fruit extends java.lang.Enum<com.example.Main$Fruit>
final enum com/example/Main$Fruit extends java/lang/Enum {

  // compiled from: Main.java
  NESTHOST com/example/Main
  // access flags 0x4018
  final static enum INNERCLASS com/example/Main$Fruit com/example/Main Fruit

  // access flags 0x4019
  public final static enum Lcom/example/Main$Fruit; Apple

  // access flags 0x4019
  public final static enum Lcom/example/Main$Fruit; Banana

  // access flags 0x101A
  private final static synthetic [Lcom/example/Main$Fruit; $VALUES

  // access flags 0x9
  public static values()[Lcom/example/Main$Fruit;
   L0
    LINENUMBER 6 L0
    GETSTATIC com/example/Main$Fruit.$VALUES : [Lcom/example/Main$Fruit;
    INVOKEVIRTUAL [Lcom/example/Main$Fruit;.clone ()Ljava/lang/Object;
    CHECKCAST [Lcom/example/Main$Fruit;
    ARETURN
    MAXSTACK = 1
    MAXLOCALS = 0

  // access flags 0x9
  public static valueOf(Ljava/lang/String;)Lcom/example/Main$Fruit;
    // parameter mandated  name
   L0
    LINENUMBER 6 L0
    LDC Lcom/example/Main$Fruit;.class
    ALOAD 0
    INVOKESTATIC java/lang/Enum.valueOf (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
    CHECKCAST com/example/Main$Fruit
    ARETURN
   L1
    LOCALVARIABLE name Ljava/lang/String; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x2
  // signature ()V
  // declaration: void <init>()
  private <init>(Ljava/lang/String;I)V
    // parameter synthetic  $enum$name
    // parameter synthetic  $enum$ordinal
   L0
    LINENUMBER 6 L0
    ALOAD 0
    ALOAD 1
    ILOAD 2
    INVOKESPECIAL java/lang/Enum.<init> (Ljava/lang/String;I)V
    RETURN
   L1
    LOCALVARIABLE this Lcom/example/Main$Fruit; L0 L1 0
    MAXSTACK = 3
    MAXLOCALS = 3

  // access flags 0x8
  static <clinit>()V
   L0
    LINENUMBER 7 L0
    NEW com/example/Main$Fruit
    DUP
    LDC "Apple"
    ICONST_0
    INVOKESPECIAL com/example/Main$Fruit.<init> (Ljava/lang/String;I)V
    PUTSTATIC com/example/Main$Fruit.Apple : Lcom/example/Main$Fruit;
    NEW com/example/Main$Fruit
    DUP
    LDC "Banana"
    ICONST_1
    INVOKESPECIAL com/example/Main$Fruit.<init> (Ljava/lang/String;I)V
    PUTSTATIC com/example/Main$Fruit.Banana : Lcom/example/Main$Fruit;
   L1
    LINENUMBER 6 L1
    ICONST_2
    ANEWARRAY com/example/Main$Fruit
    DUP
    ICONST_0
    GETSTATIC com/example/Main$Fruit.Apple : Lcom/example/Main$Fruit;
    AASTORE
    DUP
    ICONST_1
    GETSTATIC com/example/Main$Fruit.Banana : Lcom/example/Main$Fruit;
    AASTORE
    PUTSTATIC com/example/Main$Fruit.$VALUES : [Lcom/example/Main$Fruit;
    RETURN
    MAXSTACK = 4
    MAXLOCALS = 0
}

바이트코드를 확인해보면

public static values()[Lcom/example/Main$Fruit;
   L0
    LINENUMBER 6 L0
    GETSTATIC com/example/Main$Fruit.$VALUES : [Lcom/example/Main$Fruit;
    INVOKEVIRTUAL [Lcom/example/Main$Fruit;.clone ()Ljava/lang/Object;
    CHECKCAST [Lcom/example/Main$Fruit;
    ARETURN
    MAXSTACK = 1
    MAXLOCALS = 0

public static valueOf(Ljava/lang/String;)Lcom/example/Main$Fruit;
    // parameter mandated  name
   L0
    LINENUMBER 6 L0
    LDC Lcom/example/Main$Fruit;.class
    ALOAD 0
    INVOKESTATIC java/lang/Enum.valueOf (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
    CHECKCAST com/example/Main$Fruit
    ARETURN
   L1
    LOCALVARIABLE name Ljava/lang/String; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 1

별 다른 호출을 하지 않았는데도 values()valueOf() 메소드를 자동으로 상속받는걸 확인할 수 있다.

Type-Safety

type-safety란?

- 어떠한 오퍼레이션(또는 연산)도 정의되지 않은 결과를 내놓지 않는것, 즉, 예측불가능한 결과를 내지 않는것을 뜻한다.

문자열은 타입 세이프티가 보장되지 않는다. 따라서 문자열로 sql을 작성하는 것보다 QueryDSL과 같이 클래스에서 추출한 정보를 이용해 작성하면 훨씬 수월하고, 컴파일 타임에 오타가 날 일도 없고 특정한 타입 기반으로 컴파일을 하기 때문에 다 처리된다. 런타임에 문자열 오타로 발생하는 sql에러를 미연에 방지할 수 있다.

public class TypeSafetyEx {
    public static void main(String[] args) {
        System.out.println("hello");
    }
}

위의 소스는 hello를 입력하려다가 잘못하여 hell 를 입력하던가 tello를 입력할 수 있다. 즉, type-safety 하지 않는 코드라는 뜻이다.

이를 방지하기 위해 Enum으로 정의를 해두는 것이다.

public class TypeSafetyEx {

    enum Greet {
        Hello("hello");

        Greet(String message) {
            this.message = message;
        }

        String message;

        public String getMessage() {
            return message;
        }
    }

    public static void main(String[] args) {
        System.out.println(Greet.Hello.getMessage());
    }
}

코드는 길어졌지만 출력할 때 편하고 오타가 나더라도 컴파일러가 알려주기 때문에 오타 방지가 된다.