Item34 : int constant대신 enum을 사용하라

 

JAVA에서는 두 가지 특별한 reference type을 제공하고 있다.

  • class의 일종인 enum type
  • interface의 일종인 annotation type

기존에는 어떠했나? 정수 열거패턴,

int enum pattern 이라고 알려진 기법을 사용했다.

  • named [ int constant ]의 그룹을 선언하여 사용했다. ( JAVA에서는 static final로 선언 )
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;

public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;

💥 정수열거 패턴의  많은 단점들

  • type safety를 제공하지 않는다. 즉, orange를 기대하는 method에다가 apple을 전달 하더라도, 둘 다 같은 int형이기 때문에 compiler에서는 오류를 잡아내지도 못한
public void orange_process(int ORANGE_VALUE){...}
  • 각 constant의 이름에 접두사(ORANGE_,APPLE_)를 붙여줘야 한다.
    • JAVA에서는 " int enum group 에게 namespace를 제공하지 않기 " 때문이다. 접두사를 통해서, 두 int enum group(APPLE 그룹, ORAGNE그룹 ) 에 동일한 이름을 가진 constant 가 있는경우 이름의 충돌을 방지  해야 한다.
  • int enum constant를 "문자열로 출력 할 방법의 부재" .. print를 하면 그저 정수값이 출력된다 .
  • 예를들어 , int enum constant의 name인 "APPLE_FUJI" 라는 문자열로 출력하고 싶어도 그럴 방법이 없다.
  • 상수 그룹 내의 상수들을 iterate할 방법  이 없음
  • 상수 그룹의 size를 구할 방법이 딱히 없다

기존에는 어떠했나?  문자열 열거패턴

정수 상수 대신, 문자열 상수를 사용하는 방법

  • constant를 문자열로 출력하는 것은 가능하다. 
  • 하지만 이를 위해서 필드네임이 아닌, hard-coded 문자열 상수를 사용해야한다 -> 오타로 인한 버그 가능성 -> 이를 컴파일 타임에 잡지도 못함 -> 런타임 버그 발생 
  • 성능적으로 BAD ( 문자열 비교(equality) 를 사용하기 때문 )

JAVA에 enum의 도입(Java 5 에 도입)

JAVA에서의 열거타입이 다른언어들에 존재하는 열거타입보다 강력한 이유는 무엇일까?

  • java의 Enumtype 은 완전한 Class << --- >> 다른 언어들에서 열거타입은 단순히 정수값
  • 열거타입 정의는 컴파일러에 의해 class로 생성된다

Enum의 특징

  • - 외부에서 접근할 수 있는 생성자는 제공 하지 않는다
  • 상수(const) 하나당 자신의 “instance” 를 하나씩 생성해 둔다public static final 필드로 공개
    • 그러면서, 외부에서 호출가능한 "생성자"는 없다 ==> "싱글톤 인스턴스". => 매번 같은 인스턴스를 사용하게 되는 것. 
public enum Apple { FUJI, PIPPIN}; 
//는 사실상 대략적으로 아래와 같은 것 
public enum Apple{
	public static final Apple FUJI;
	public static final Apple PIPPIN; 
}

- final =⇒ Client가 인스턴스를 직접 생성 X, 확장 X

- 열거타입 선언으로 만들어진 인스턴스들은 “ 딱 하나씩만 ” 존재.

  • 인스턴스 통제가 되는 것 
  • 열거타입은 "싱글톤"이라고도 말 할 수 있고, 싱글톤을 "원소가 하나뿐인 열거타입"이라고 할 수도 있다.

- 컴파일 타임 “타입 안정성”제공 

- 각자의 네임스페이스 가짐 → “이름이 같은 상수” 공존 가능

public Enum Applie{ FUJI, PIPPIN};
public Enum Orange{ FUJI, PIPPIN};
  • 메소드, 필드를 가질 수 있음
    • 🚀Class 이므로 메소드,필드를 가질 수 있음.
    • 고차원의 추상개념을 표현해낼 수 있는 것.

 

 


문자열 타입 ( 레거시) —> 열거타입파라미터

보기만 해도 끔찍하다

public String checkPrameterBehavior(String object){
	if(object.equals("이렇게")){
		return "이렇게한다";
	}else if(object.equals("문자열을 통해")){
		return "문자열통해한다";
	}else return "타입쓰고싶다";
}
  • 저 String 이 Enum type이 된다면..
    • “타입체크도 되고 “,
    • “문자열을 작성하는 과정에서 일어나는 오타로 인한 버그도 없어지겠지

이 부분에 대한 예시는 아래에 이 링크로 가자. 

Java Enum 활용기 | 우아한형제들 기술블로그

 

 


메소드? 필드? → 데이터들의 연관관계 표현이 가능하다

즉, class의 특징( 연관된 데이터들을 모아두고 사용가능 )을 가질 수 있다는 것 이라 생각한다 → 객체지향적 프로그래밍

  • 데이터들 간의 연관관계를 표현할 수 있다. → 상수 별로 데이터에 대한 중복이 일어나던 것을 해결
  • 상태와 행위를 한 곳에서 처리 ( class의 특징 )
  • 각 상수와 연관된 데이터를 상수자체에 두고 싶다 → 인스턴스 필드를 선언

이 때, 

  • Enum type의 모든 필드 → final 이어야 한다. ( 선언과 동시 or 생성자를 통해 런타임에 초기화) 
    • 열거타입은 근본적으로 “Immutable” 하기 때문.
  • 필드는 public 보다는 private + getter 같은 접근자를 두는 것이 낫다.

(내 생각 ) 아래 코드에서, 표면중력에 대한 것은 사실 인스턴스 필드(surfaceGravity)를 두지 않고, 표면중력을 계산하는 로직 메소드를 따로 두고 그 때 마다 계산할 수도 있으나, 이를 구성하는 값들이 인스턴스필드로 선언되어있어 ( 열거타입에서 인스턴스필드는 final ) → 생성시에만 초기화가 되기에. “최적화”를 위하여 생성시에 미리 표면중력값을 계산해 두고 이것이 필요할 때면 그저 값을 넘기는 식으로 최적화를 할 수 있는 것 같다. ( 👏불필요하게 계산 로직메소드를 따로 두지 말자 )

// Enum type with data and behavior  (159-160)
public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS  (4.869e+24, 6.052e6),
    EARTH  (5.975e+24, 6.378e6),
    MARS   (6.419e+23, 3.393e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN (5.685e+26, 6.027e7),
    URANUS (8.683e+25, 2.556e7),
    NEPTUNE(1.024e+26, 2.477e7);

    private final double mass;           // In kilograms
    private final double radius;         // In meters
    private final double surfaceGravity; // In m / s^2

    // Universal gravitational constant in m^3 / kg s^2
    private static final double G = 6.67300E-11;

    // Constructor
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
				// 표면중력을 미리 계산해 저장하고 있다. 
        surfaceGravity = G * mass / (radius * radius);
    }

enum type “static” method : values(),toString()

열거 타입을 정의하면, 자바 컴파일러가 자동으로 특별한 메소드를 추가며 class를 생성한다. 이 때 추가되는 메소드에 아래의 values() 가 포함. ( toString()은 Enum class가 Object class 를 상속하기 때문에 )
  • values() :해당 enum type에 정의된 상수들의 배열을 반환
  • toString() : 상수”이름”을 “문자열”로 리턴. ( 당연히 override가능 )
for(Planet planet : Planet.values()){
	// Enum타입의 planet 을 print 하면 -> 상수이름이 출력
	System.out.printf("%s의 질량은 %f"", **planet**, planet.mass());
}

toString()을 재정의했다면 fromString()이라는 메소드도 고려해보자

  • toString() : “열거타입” → String

그렇다면 String → “열거타입” 의 변환을 해 줄 수 있는 메소드도 있으면 좋지 않을까??

  • EX) “String” 과 “열거타입 인스턴스”를 매핑해 둔 자료구조(Map<String,MyEnumtype> )를 사용하면 되지 않을까?

> 일단 아래 코드는 에러💥가 난다

public enum GumballState{ 
	private static final Map<String, GumballState> map = new HashMap<>();

    GumballState() {
        for (GumballState value : values()) {
            map.put(value); // 컴파일 에러
        }
    }

에러를 살펴보자 ->  It is illegal to access static member 'map' from enum constructor or instance initializer

왜 이런 에러가 난 걸까?? -> 이는 "초기화 시점" 과 관련되어있다

  • enum 인스턴스의 초기화 시점은 언제이길래 그럴까 ?? -> static initializer처럼 class load time 에 초기화 된다. 
🚀 따라서 열거타입 “생성자가 실행되는 시점 “에는, “ static 필드들 “ 이 아직 초기화되기 전 이기 때문에, 이들을 참조를 하면 위험하다.
따라서, 이런 제약이 있음
It is a compile-time error to reference a  static  field of an enum type from constructors, instance initializers, or instance variable initializer expressions of the enum type, unless the field is a constant variable
 위와 같은 코드가 컴파일 되지 못하도록 해 놓은 것  

출처 : https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.9.2

그렇다면 위의 map 을 초기화 시키려면 어떻게 해야할까? 

static block내에서 Map을 초기화하는 것이 가능하다  
    
    public enum GumballState implements EnumString{
	    static{
        	for(GumballState state:values()){
            	map.put(state.toString(),state);
       		 }
   		 }
    	....
    	@Override
   	 	public Enum fromString(String name) {
        	return map.get(name);
    	}
    }

이제 이 fromString(String name) 을 통해 리턴되는 열거타입이 실제로 해당 열거타입 인스턴스인지 확인해보자 

	@Test
	public void testEnum(){
		// given
		GumballState state = GumballState.SOLD;
		// when
		Enum test = state.fromString("NO_QUARTER");
		//then
		Assertions.assertEquals(test,GumballState.NO_QUARTER);
	}

새로운 상수를 추가 or 상수의 순서를 뒤바꾸더라도 recompile하지 않아도 됨

공개 되는건 오직 "필드 이름"
- 정수 열거 패턴과 달리, "상수 값" 이 클라이언트로 컴파일 되어가는게 아니 기 때문

 

아래는 이 말의 이해를 도와주는 예시로, 정수형 열거패턴을 사용할때와의 차이점을 보여준다.

Enum requires no recompilation of its clients - How?

먼저 경우를 두 가지로 나눠보자.

  • int 상수 사용
public class Constants {
    public static final int FOO = 1;
}
public class Client {
    public static void main(String[] args) {
        System.out.println(Constants.FOO);
    }
}

이 상태에서 변경

   public static final int FOO = 27;

Client는 recompile하지 않고, Constants만 recompile 하고 → Client를 실행.

출력되는 FOO는 여전히 1이다.

  • Client class가 compiled될 때, 컴파일러에 의해, Client class로 이 값 자체가 복사되어왔기 때문이다.
  • 반면에 열거타입을 사용한다면? 🚀 상수의 “제거가 아닌 수정” 을 한 경우에는, 열거타입의 경우에는, client측에서는 client의 recompiling 없이도 항상 옳바른 값을 얻을 수가 있다. 

일부 상수를 제거한다면?

상수 추가 및 변경시에는 기존의 Client를 recompile 시킬 필요가 없다고 했었다. ( 클라이언트 코드에 "실제 값"이 들어가는 것이 아니기 때문에 ) 

그렇다면, 상수를 제거해도 그럴까?? ->> 아닐 거라는 직감이 올 것 이다. 

먼저  제거한 상수 ”를 참조하지 않는 client에는 아무런 영향이 없다

  • 하지만 제거한 상수를 참조하는 client라면?
    클라이언트 프로그램 재컴파일 → 해당 상수 참조라인에 컴파일 에러
    ▪ Recompile을 하지 않고 실행하면 → 런타임에 같은 에러 발생
이 부분에 대해서는 클래스로더.. 링킹 과정의 이해가 필요할 듯 하다. 컴파일과 인터프리팅 과정 자체를 이해 못 하고 있는 듯;; 🤔🤔💥❓

열거타입에 숨기자 private method ( Item 15)

  • 다른 클래스에서와 마찬가지로, client에 노출해야할 이유가 없다면
  • 열거타입을 선언한 클래스 or 그 패키지에서만 유용한 기능 “private”이나 “package-private 메소드”로 구현
    • 해당 범위에서만 사용가능한 기능

 

톱레벨, 멤버 클래스 열거타입(Item 24)

  • 다른 클래스와 마찬가지로, [ 널리 쓰이는 열거 타입 → 톱레벨 클래스로 ]
  • 특정 클래스에서만 쓰인다면 , [ 해당 클래스의 member 클래스로 ]

 

상수마다 동작이 달라야하는 경우 ?

1️⃣ 가장 심플 : switch, if else 구문으로, 상수에 따라 다른 로직을 실행하도록 작성

→ 가독성 매우 떨어짐.

→ 관리하기 위험 💥: 상수가 추가 될 때 마다, 해당 상수에 대한 로직을 추가해야 한다.

  • 로직 추가하는 것을 잊고, 해당 상수에 대해 메소드 호출 → 💥 런타임 에러 ( 컴파일 에러를 안 줌.. )

2️⃣ 🚀 열거타입에 추상메소드 선언 → 각 상수에서 constant-specific class body를 선언하자🚀

  • 즉, 각 상수에서, override 하는 것임.
  • 비어있는 추상메소드로 선언했다면 → 상수 추가&& but, 추상메소드 재정의를 ❌(까먹음) → 컴파일 에러💥 를 주는 친절함 !!
  • 당연히, 상수별 메소드 구현 에는, 상수별 데이터 ( 인스턴스 필드 ) 를 사용할 수 있다.

상수마다 메소드 구현 → “공유되는 로직”은 어떻게?

만약 위와 같이, 추상메소드로 선언해 둔다면 상수 마다 해당 메소드를 구현해 줘야한다. 

만약 상수들끼리 공유하는 로직이 존재한다면? -> 중복하여 작성을 해줘야하는 걸까???

 

1️⃣ (BAD) 가장 심플 : switch,if else 구문 → 위에서 봤지만 문제가 있었음..

enum PayrollDay {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY,
    SATURDAY, SUNDAY;
	
		private static final int MINS_PER_SHIFT = 8 * 60;
		int pay(int minutesWorked, int payRate){
			/*
			1. 평일 : basePay + 오버타임에 대한 추가수당
			2. 주말 : 모든 시간이 오버타임 페이. 시간당 basepay*1.5이므로 
			basePay 자체는 공통로직으로 두고 💥
			1. 평일 -> 추가시간에 대하여 주중 오버타임에 대한 rate로 계산
			2. 주말 -> 전체 시간에 대하여 basePay에 0.5를 더 곱해주면 오버타임페이가 된다. 
			*/
			int basePay = minutesWorked * payRate; // 공통로직 
			// 오버타임 페이 -> 두 가지 종류 1.주중에 오버타임 2. 주말 모든 시간 (basepay*1.5)
			int overtimePay;
			switch(this){
				case SATURDAY: case SUNDAY: // 주말
					overtimePay = basePay/2; 
					beak;
				default : // 주중
					overtimePay = minutesWorked <= MINS_PER_SHIFT ? 0 :
																						( minutesWorked - MINS_PER_SHIFT)*payRate/2;
			}
			return basePay + overtimePay;
		}
}
			

2️⃣ (BAD)상수별 메소드 구현 : 2가지 방법

  1. 잔업수당 계산코드를 모든 상수에 중복해서 넣기
    • 평일 메소드들 사이에 중복 코드, 주말 메소드들 사이에 중복 코드...
  2. 헬퍼 메소드 작성 → 각 상수의 메소드 구현에서, 필요한 헬퍼메소드를 호출
    • 매우 장황해지겠다 →가독성 떨어짐.
    • 오류 발생 가능성 증가

3️⃣ (BAD)기본 메소드를 구현해둠 : ex) 평일 잔업수당 계산용 메소드로 overtimePay를 구현해두고, 주말상수들에서는, 해당 메소드를 override

  • 문제점 : 💥 주말 수당을 받는 상수 추가시, 메소드 override를 까먹으면 → 평일 잔업수당으로 받게됨..

4️⃣ 🚀 전략 열거 타입 패턴🚀

( 일종의 Composition 처럼 느꼈다 _ 갈아끼울 수 있도록 , 그리고 그 갈아끼우는 애가 해당 기능 관련해서는 책임을 ㅏ하도록 기능을 분리 해 놓는 것 _ 전략패턴과 유사해서 "전략"이라는 이름을 붙여놓은 것 같다.

상수 추가 시 , 해당 상수의 “잔업수당 🚀전략을 선택”하도록 하는 것 ( 생성자를 통해서 받도록 하면 되겠다 )

그렇다면, 잔업수당 전략은 어떤식으로 ? → private nested enum

  • 각 전략에 맞는, 잔업수당 상수에서 , 해당 전략에 맞는 메소드를 구현한다. ( abstract method 를 선언해 놓아 → 강제적으로 오버라이드 하도록 한다 )
enum PayrollDay {
    MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
    THURSDAY(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);
    }

    // The strategy enum type
    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);
        private static final int MINS_PER_SHIFT = 8 * 60;

        int pay(int minsWorked, int payRate) {
            int basePay = minsWorked * payRate;
            return basePay + overtimePay(minsWorked, payRate);
        }
    }
}

switch문은 아예 쓰지 않는게 좋은가? no → 기존 열거타입의 상수별 동작을 혼합해서 넣고 싶을 때

  1. 드물게 하나의 상수에 대해서만 다른 동작을 하는 경우에는, switch문을 쓸 수 있겠다.
  2. 서드파티에서 가져온 Operation 열거타입이 있다고 하자.

각 연산의 반대 연산을 하고 싶다 .

public static Operation inverse(Operation op) {
        switch(op) {
            case PLUS:   return Operation.MINUS;
            case MINUS:  return Operation.PLUS;
            case TIMES:  return Operation.DIVIDE;
            case DIVIDE: return Operation.TIMES;

            default:  throw new AssertionError("Unknown op: " + op);
        }
    }

그래서 열거타입은 언제 쓰는게 좋을까?

  • 메모리나 성능적으로 “정수상수”보다 그렇게 뛰어난 건 아님. 
  • “필요한 원소”를 “컴파일 타임” 에 다 알 수 있는 상수 집합
  •  ( ❓이부분은잘 모르겠다❓ )정의된 상수 개수가 영원히 고정불변일 필요는 없다. ==> 상수가 추가되어도, binary 수준에서 호환되도록 설계되었다. 
  • enum 은 컴파일 타임에 정의되어 있어야만 한다.
    • 런타임에 수정할 수는 없다.
    • “동적” enumeration을 위해서는 차라리 Map<String, Object> 같은 Collection을 쓸 수 밖에..

직접 해보는 리팩토링

헤드퍼스트 디자인 패턴 스테이트 패턴의 첫 번째 예시코드는 다음과 같았다.

package headfirst.state.gumball;

import headfirst.state.gumball.mypractice.GumballState;

public class GumballMachine {

	final static int SOLD_OUT = 0;
	final static int NO_QUARTER = 1;
	final static int HAS_QUARTER = 2;
	final static int SOLD = 3;
 
	int state = SOLD_OUT;
	int count = 0;
  
	public GumballMachine(int count) {
		this.count = count;
		if (count > 0) {
			state = NO_QUARTER;
		}
	}
  
	public void insertQuarter() {
		if (state == HAS_QUARTER) {
			System.out.println("You can't insert another quarter");
		} else if (state == NO_QUARTER) {
			state = HAS_QUARTER;
			System.out.println("You inserted a quarter");
		} else if (state == SOLD_OUT) {
			System.out.println("You can't insert a quarter, the machine is sold out");
		} else if (state == SOLD) {
        	System.out.println("Please wait, we're already giving you a gumball");
		}
	}

	public void ejectQuarter() {
		if (state == HAS_QUARTER) {
			System.out.println("Quarter returned");
			state = NO_QUARTER;
		} else if (state == NO_QUARTER) {
			System.out.println("You haven't inserted a quarter");
		} else if (state == SOLD) {
			System.out.println("Sorry, you already turned the crank");
		} else if (state == SOLD_OUT) {
        	System.out.println("You can't eject, you haven't inserted a quarter yet");
		}
	}
 
	public void turnCrank() {
		if (state == SOLD) {
			System.out.println("Turning twice doesn't get you another gumball!");
		} else if (state == NO_QUARTER) {
			System.out.println("You turned but there's no quarter");
		} else if (state == SOLD_OUT) {
			System.out.println("You turned, but there are no gumballs");
		} else if (state == HAS_QUARTER) {
			System.out.println("You turned...");
			state = SOLD;
			dispense();
		}
	}
 
	public void dispense() {
		if (state == SOLD) {
			System.out.println("A gumball comes rolling out the slot");
			count = count - 1;
			if (count == 0) {
				System.out.println("Oops, out of gumballs!");
				state = SOLD_OUT;
			} else {
				state = NO_QUARTER;
			}
		} else if (state == NO_QUARTER) {
			System.out.println("You need to pay first");
		} else if (state == SOLD_OUT) {
			System.out.println("No gumball dispensed");
		} else if (state == HAS_QUARTER) {
			System.out.println("No gumball dispensed");
		}
	}
 
	public void refill(int numGumBalls) {
		this.count = numGumBalls;
		state = NO_QUARTER;
	}

	public String toString() {
		StringBuffer result = new StringBuffer();
		result.append("\\nMighty Gumball, Inc.");
		result.append("\\nJava-enabled Standing Gumball Model #2004\\n");
		result.append("Inventory: " + count + " gumball");
		if (count != 1) {
			result.append("s");
		}
		result.append("\\nMachine is ");
		if (state == SOLD_OUT) {
			result.append("sold out");
		} else if (state == NO_QUARTER) {
			result.append("waiting for quarter");
		} else if (state == HAS_QUARTER) {
			result.append("waiting for turn of crank");
		} else if (state == SOLD) {
			result.append("delivering a gumball");
		}
		result.append("\\n");
		return result.toString();
	}
}
  • named int const 를 사용하여 “상태를 관리” 하고 있었다.

물론 책에서는 state 패턴을 사용

지금 생각해보니 → 헤드퍼스트 디자인 패턴에서는, 상태패턴을 사용하면서, “확장가능한 열거패턴” 이라고도 불리는 interface를 사용하는 State 패턴을 사용하여 해결하였다. 사실 이게 더 적절하다. 
지금부터 Enum을 사용할 경우, 결국 조건문이 복잡하게 얽힐 가능성에 대해 보게 될 것이다.. 물론 현재 상황에서는 기존 코드보다야 낫지만.. 확장가능성을 생각한다면 State pattern이 더 맞는 듯 하다

하지만 나는 열거타입 연습겸, 열거타입으로 일종의 리팩토링 하는 것을 생각 해 보았다.


  1. Enum class 를 생성한다고 해 보자 단순히 const를 선언해 놓는 것 외에,
    • 상수와 관련된 인스턴스 필드를 추가
    • 공통 메소드를 선언 할 수 있겠다

GumballMachine에서는, state에 따라 각 메소드에서 다른 동작을 할 수 있도록 if else 문을 사용하고 있다.

→ 💥 번잡해 보인다


만약, Enum class에 insertQuarter() 같은 것을 추상 메소드를 선언 한다면????

  • Enum class의 abstract method로 선언하여, 그저 현재 state.insertQuater() 를 호출하도록 할 수 있다.
  • 하지만 .. 코드가 매우 장황해질 것. 하나의 상수에서 구현해야할 메소드가 너무 많음 ( insertQuarter, ejectQuarter, turnTrunk,dispense ..
public enum GumballState{
	SOLD{
		@Override
    	public void insertQuater() {
          	System.out.println("Please wait, we're already giving you a gumball"); 
        },
    ....

따라서 , Enum 상수에 관련된 데이터로 각 상수에 대한 출력문 값들을 세팅했다

/**
 * GumBallMachine -> GumballState 와 MyGumballMachine으로 나타냄
 * */
public enum GumballState {
    SOLD("delivering a gumball","Please wait, we're already giving you a gumball","Sorry, you already turned the crank","Turning twice doesn't get you another gumball!","A gumball comes rolling out the slot")
    ,NO_QUARTER("waiting for quarter","You inserted a quarter","You haven't inserted a quarter","You turned but there's no quarter","You need to pay first")
    ,SOLD_OUT("sold out","You can't insert a quarter, the machine is sold out","You can't eject, you haven't inserted a quarter yet","You turned, but there are no gumballs","No gumball dispensed")
    ,HAS_QUARTER("waiting for turn of crank","You can't insert another quarter","Quarter returned","You turned...","No gumball dispensed");

    /*
    default constructor를 주석처리하지 않으면 -> final instance field들에 대하여, might have not been initialized라는 컴파일 에러가 뜬다
    GumballState() {
    }
     */

    GumballState(String state,String insertQuarter, String ejectQuarter, String turnCrank, String dispense) {
        this.state = state;
        this.insertQuarter = insertQuarter;
        this.ejectQuarter = ejectQuarter;
        this.turnCrank = turnCrank;
        this.dispense = dispense;
    }
    // 각 상수에 대한 출력문 데이터를 관리
    private final String state;
    private final String insertQuarter;
    private final String ejectQuarter;
    private final String dispense;
    private final String turnCrank;

    public String getInsertQuarter() {
        return insertQuarter;
    }

    public String getEjectQuarter() {
        return ejectQuarter;
    }

    public String getDispense() {
        return dispense;
    }

    public String getTurnCrank() {
        return turnCrank;
    }

    public String getState() {
        return state;
    }
}

이를 사용하면 Machine의 코드는 아래와 같아진다.

public class MyGumballMachine {

    GumballState state = GumballState.SOLD_OUT;
    int count = 0;

    public MyGumballMachine(int count) {
        this.count = count;
        if(count > 0){
            state = GumballState.NO_QUARTER;
        }
    }
    public void insertQuarter(){
        System.out.println(state.getInsertQuarter());
        switch (state){
            case NO_QUARTER:
                state = GumballState.HAS_QUARTER;
        }
    }
    public void ejectQuarter(){
        System.out.println(state.getEjectQuarter());
        switch (state){
            case HAS_QUARTER:
                state = GumballState.NO_QUARTER;
        }
    }
    public void turnCrank(){
        System.out.println(state.getTurnCrank());
        // An enum switch case label must be the unqualified name of an enumeration constant
        // class 명 적지 말고 오직 enum 의 값만 적으면 된다
        switch (state){
            case HAS_QUARTER:
                state = GumballState.SOLD;
                dispense();
                break;
        }
    }
    public void dispense(){
        System.out.println(state.getDispense());
        switch (state){
            case SOLD:
                count--;
                if(count == 0){
                    state = GumballState.SOLD_OUT;
                }else{
                    state = GumballState.NO_QUARTER;
                }
        }
    }
    @Override
    public String toString(){
        StringBuffer bf = new StringBuffer();
        bf.append("\\nMighty Gumball, Inc.");
        bf.append("\\nJava-enabled Standing Gumball Model #2004\\n");
        bf.append("Inventory: " + count + " gumball");
        if (count != 1) {
            bf.append("s");
        }
        bf.append("\\nMachine is ");
        bf.append(state.getState());
        bf.append("\\n");
        return bf.toString();

    }
}

달라진점

  • 수 많은 if else구문을 줄이고 → 하나의 condition에 대한 switch구문으로 변경하였다.
  • 하지만 문제는 이거다 → Enum을 사용하여 상태를 관리할 경우 → State의 개수가 늘어나면 위의 switch 구문과 같은 조건문들이 더욱 복잡해질 것이다.

위의 코드는 헤드퍼스트 디자인 패턴의 “스테이트 패턴” 단원에서 나온 부분이다. 현 상황에서는 Enum을 사용하는게 적절해 보이진 않는다.

  • enum 에서 조건문을 이용한 방식은 코드를 복잡하게 만들어 유지 보수를 어렵게 할 가능성이 있다.. 

상태 패턴(State Pattern)을 사용해보자

 

 

 


참조

Java Enum 활용기 | 우아한형제들 기술블로그

 

Java Enum 활용기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요? 우아한 형제들에서 결제/정산 시스템을 개발하고 있는 이동욱입니다. 이번 사내 블로그 포스팅 주제로 저는 Java Enum 활용 경험을 선택하였습니다. 이전에 개인 블로그에 E

techblog.woowahan.com

effective-java-3e-source-code/Inverse.java at master · noogu/effective-java-3e-source-code

 

GitHub - noogu/effective-java-3e-source-code: The source code from the third edition of Effective Java, with minor additions as

The source code from the third edition of Effective Java, with minor additions as necessary to make it runnable. - GitHub - noogu/effective-java-3e-source-code: The source code from the third editi...

github.com

https://github.com/bethrobson/Head-First-Design-Patterns

 

GitHub - bethrobson/Head-First-Design-Patterns: Code for Head First Design Patterns book (2014)

Code for Head First Design Patterns book (2014). Contribute to bethrobson/Head-First-Design-Patterns development by creating an account on GitHub.

github.com

 

복사했습니다!