Java8 enum 의 활용

2018. 9. 4. 17:3999. 정리전 - IT/11. Java

참조 : http://woowabros.github.io/tools/2017/07/10/java-enum-uses.html

1. 데이터들 간의 연관관계 표현

Worse Case

package com.donzbox._01_Relation._1_WorseCase;

class LegacyCase {
	public String getValue01(final String value) {
		if("Y".equals(value)) {
			return "1";
		} else {
			return "2";
		}
	}
	public boolean getValue02(final String value) {
		if("Y".equals(value)) {
			return true;
		} else {
			return false;
		}
	}
}

public class _Application {
	public static void main(final String [] args) {
		LegacyCase lc = new LegacyCase();
		/*
		 * 01. “Y”, “1”, true는 모두 같은 의미라는 것을 알 수 없습니다.
		 *      Y란 값은 “1”이 될 수도 있고, true가 될 수도 있다는 것을 확인하려면 항상 위에서 선언된 클래스와 메소드를 찾아야만 합니다.
		 * 02. 불필요한 코드량이 많습니다.
		 *      Y, N 외에 R, S 등의 추가 값이 필요한 경우 if문을 포함한 메소드 단위로 코드가 증가하게 됩니다.
		 *      동일한 타입의 값이 추가되는것에 비해 너무 많은 반복성 코드가 발생하게 됩니다.
		 */
		System.out.println(lc.getValue01("Y"));
		System.out.println(lc.getValue02("N"));
	}
}

Best Case

package com.donzbox._01_Relation._2_BestCase;

enum Value {
	Y("1", true),
	N("0", false);

	private String value01;
	private boolean value02;

	Value(final String value01, final boolean value02) {
		this.value01 = value01;
		this.value02 = value02;
	}

	public String getValue01() {
		return value01;
	}
	public boolean getValue02() {
		return value02;
	}
}

public class _Application {

	public static void main(final String [] args) {
		/*
		 * “Y”, “1”, true 가 한 묶음으로, “N”, “0”, false가 한 묶음이 된 것을 코드로 바로 확인할 수 있습니다.
		 * 또한 추가 타입이 필요한 경우에도 Enum 상수와 get메소드만 추가하면 됩니다.
		 */
		Value value = Value.Y;
		System.out.println(value.getValue01());
		System.out.println(value.getValue02());
	}
}

2. 상태와 행위를 한곳에서 관리

Worse Case

package com.donzbox._02_StatusAction._1_WorseCase;

class Calculator {
	public static long calculate(final String code, final long value) {
		if("CALC_x1".equals(code)) {
			return value*1;
		} else if("CALC_x5".equals(code)) {
			return value*5;
		} else if("CALC_x9".equals(code)) {
			return value*9;
		} else {
			return 0;
		}
	}
}

public class _Application {
	public static void main(final String [] args) {
		/*
		 * 코드는 코드대로 조회하고
		 * 계산은 별도의 클래스&메소드를 통해 진행해야 함
		 * Calculator의 메소드와 code는 서로 관계가 있음을 코드로 표현할 수가 없기 때문
		 * Code에 따라 지정된 메소드에서만 계산되길 원하는데, 현재 상태로는 강제할 수 있는 수단이 없음
		 *
		 * 01. 똑같은 기능을 하는 메소드를 중복 생성할 수 있습니다.
		 *     히스토리가 관리 안된 상태에서 신규화면이 추가되어야 할 경우 계산 메소드가 있다는 것을 몰라 다시 만드는 경우가 빈번합니다.
		 *     만약 기존 화면의 계산 로직이 변경 될 경우, 신규 인력은 2개의 메소드의 로직을 다 변경해야하는지, 해당 화면만 변경해야하는지 알 수 없습니다.
		 *     관리 포인트가 증가할 확률이 매우 높습니다.
		 * 02. 계산 메소드를 누락할 수 있습니다.
		 *     결국 문자열과 메소드로 분리 되어 있기 때문에 이 계산 메소드를 써야함을 알 수 없어 새로운 기능 생성시 계산 메소드 호출이 누락될 수 있습니다.
		 */
		String someOutput = "CALC_x5";
		long someValue = 10000L;
		System.out.println(Calculator.calculate(someOutput, someValue));
	}
}

Best Case

package com.donzbox._02_StatusAction._2_BestCase;

import java.util.function.Function;

enum CalculatorCode {

	/* 
	 * java7 (상수별 메소드) 구현
	 * Enum의 필드로 추상메소드를 선언하고, 이를 상수들이 구현하도록 하면
	 * Java8의 Function 인터페이스를 사용한 것과 동일한 효과를 보실수 있습니다.
		CALC_x1 {
			long calculate(long value) { return value*1; }
		},
		CALC_x5 {
			long calculate(long value) { return value*5; }
		},
		CALC_x9 {
			long calculate(long value) { return value*9; }
		},
		CALC_xx {
			long calculate(long value) { return 0L; }
		};
		abstract long calculate(long value);
	 */

	// java8 구현
	CALC_x1(value -> value*1),
	CALC_x5(value -> value*5),
	CALC_x9(value -> value*9),
	CALC_xx(value -> 0L);

	private Function<Long, Long> expression;
	CalculatorCode(final Function<Long, Long> expression) {
		this.expression = expression;
	}
	public long calculate(final long value) {
		return expression.apply(value);
	}
}

public class _Application {
	public static void main(final String [] args) {
		/*
		 * “DB의 테이블에서 뽑은 특정 값은 지정된 메소드와 관계가 있다.”
		 * 역활과 책임이라는 관점으로 봤을때, 위 메세지는 Code에 책임이 있음
		 * Entity 클래스에 선언하실 경우에는 String이 아닌 enum을 선언
		 */
		CalculatorCode code = CalculatorCode.CALC_x5;
		long someValue = 10000L;

		/*
		 * 값(상태)과 메소드(행위)가 어떤 관계가 있는지에 대해 더이상 다른 곳을 찾을 필요가 없음
		 * 코드내에 전부 표현되어 있고, Enum 상수에게 직접 물어보면 되기 때문
		 */
		System.out.println(code.calculate(someValue));
	}
}

3. 데이터 그룹관리

Worse Case

package com.donzbox._03_DataGroup._1_WorseCase;

class PayGroup {
	public static String getPayGroup(final String code) {
		if("계좌이체".equals(code) || "무통장입금".equals(code) || "현장결재".equals(code) || "토스".equals(code)) {
			return "현금";
		}
		else if("신용카드".equals(code) || "카카오페이".equals(code) || "페이코".equals(code) || "배민페이".equals(code)) {
			return "카드";
		}
		else if("포인트".equals(code) || "쿠폰".equals(code)) {
			return "기타";
		} else {
			return "없음";
		}
	}
}

public class _Application {
	public static void main(final String [] args) {
		/*
		 * 01. 둘의 관계를 파악하기가 어렵습니다.
		 *     위 메소드는 포함관계를 나타내는 것일까요? 아니면 단순한 대체값을 리턴한것일까요?
		 *     현재는 결제종류가 결제수단을 포함하고 있는 관계인데, 메소드만으로 표현이 불가능합니다.
		 * 02. 입력값과 결과값이 예측 불가능합니다.
		 *     결제 수단의 범위를 지정할수 없어서 문자열이면 전부 파라미터로 전달 될 수 있습니다.
		 *     마찬가지로 결과를 받는 쪽에서도 문자열을 받기 때문에 결제종류로 지정된 값만 받을 수 있도록 검증코드가 필요하게 됩니다.
		 * 03. 그룹별 기능을 추가하기가 어렵습니다.
		 *     결제 종류에 따라 추가 기능이 필요할 경우 현재 상태라면 어떻게 구현 할수 있을까요?
		 *     또다시 결제종류에 따른 if문으로 메소드를 실행하는 코드를 작성해야 할까요?
		 */
		String payCode = "배민페이";
		String payMethod = PayGroup.getPayGroup(payCode);
		System.out.println(payMethod);

		/*
		 * 각각의 메소드는 원하는 떄에 사용하기 위해 독립적으로 구성할 수 밖에 없는데,
		 * 그럴때마다 결제종류를 분기하는 코드가 필수적으로 필요하게 됩니다.
		 * 이건 좋지 못한 방법이라는 생각이였습니다.
		 */
		if("현금".equals(payMethod)) {
			System.out.println(payMethod + "로직수행");
		} else if("카드".equals(payMethod)) {
			System.out.println(payMethod + "로직수행");
		} else if("기타".equals(payMethod)) {
			System.out.println(payMethod + "로직수행");
		} else {
			System.out.println("로직수행안함");
		}
	}
}

Good Case

package com.donzbox._03_DataGroup._2_GoodCase;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

enum PayGroup {
	/* 결제수단이 문자열인 것입니다.
	 * DB 테이블의 결제수단 컬럼에 잘못된 값을 등록하거나,
	 * 파라미터로 전달된 값이 잘못되었을 경우가 있을 때 전혀 관리가 안됩니다.
	 * 그래서 이 결제수단 역시 Enum으로 전환하여야 함
	 */
	CASH("현금", Arrays.asList("계좌이체", "무통장입금", "현장결재", "토스")),
	CARD("카드", Arrays.asList("신용카드", "카카오페이", "페이코", "배민페이")),
	ETC("기타", Arrays.asList("포인트", "쿠폰")),
	EMPTY("없음", Collections.emptyList());

	private String title;
	private List<String> payList;
	PayGroup(final String title, final List<String> payList) {
		this.title = title;
		this.payList = payList;
	}

	/*
	 * 각 Enum 상수들은 본인들이 갖고 있는 문자열들을 확인하여
	 * 문자열 인자값이 어느 Enum 상수에 포함되어있는지 확인할 수 있게 되었습니다.
	 */
	public static PayGroup findByPayCode(final String code) {
		//PayGroup의 Enum 상수들을 순회하며
		return Arrays.stream(PayGroup.values())
				// payCode를 갖고 있는지 확인
				.filter(payGroup -> payGroup.hasPayCode(code))
				.findAny()
				.orElse(EMPTY)
				;
	}
	public boolean hasPayCode(final String code) {
		return payList.stream()
				.anyMatch(payCode -> payCode.equals(code));
	}
	public String getTitle() {
		return title;
	}
}

public class _Application {
	public static void main(final String [] args) {
		// 관리 주체를 PayGroup에게 준 결과로, 이젠 PayGroup에게 직접 물어보면 됩니다.
		String payCode = "배민페이";
		PayGroup payGroup = PayGroup.findByPayCode(payCode);
		System.out.println(payGroup.name());
		System.out.println(payGroup.getTitle());
	}
}

Best Case

package com.donzbox._03_DataGroup._3_BestCase;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

enum PayType {
	/*
	 * DB 혹은 API에서 PayType으로 데이터를 받아,
	 * 타입 안전성까지 확보하여 PayGroup 관련된 처리를 진행할 수 있게 되었습니다.
	 */
	ACCOUN_TRANSFER("계좌이체"),
	REMITTANCE("무통장입금"),
	ON_SITE_PAYMENT("현장결재"),
	TOSS("토스"),
	CARD("신용카드"),
	KAKAO_PAY("카카오페이"),
	PAYCO("페이코"),
	BAEMIN_PAY("배민페이"),
	POINT("포인트"),
	COUPON("쿠폰"),
	EMPTY("없음");

	private String title;
	PayType(final String title) { this.title = title; }
	public String getTitle() { return title; }
}
enum PayGroup {
	CASH("현금", Arrays.asList(PayType.ACCOUN_TRANSFER, PayType.REMITTANCE, PayType.ON_SITE_PAYMENT, PayType.TOSS)),
	CARD("카드", Arrays.asList(PayType.CARD, PayType.KAKAO_PAY, PayType.PAYCO, PayType.BAEMIN_PAY)),
	ETC("기타", Arrays.asList(PayType.POINT, PayType.COUPON)),
	EMPTY("없음", Collections.emptyList());

	private String title;
	private List<PayType> payList;
	PayGroup(final String title, final List<PayType> payList) {
		this.title = title;
		this.payList = payList;
	}

	public static PayGroup findByPayCode(final PayType code) {
		return Arrays.stream(PayGroup.values())
				.filter(payGroup -> payGroup.hasPayCode(code))
				.findAny()
				.orElse(EMPTY)
				;
	}
	public boolean hasPayCode(final PayType code) {
		return payList.stream()
				.anyMatch(payCode -> payCode == code);
	}
	public String getTitle() {
		return title;
	}
}

public class _Application {
	public static void main(final String [] args) {
		PayType payCode = PayType.BAEMIN_PAY;
		PayGroup payGroup = PayGroup.findByPayCode(payCode);
		System.out.println(payGroup.name());
		System.out.println(payGroup.getTitle());
	}
}

4. 관리 주체를 DB에서 객체로

Good Case

package com.donzbox._04_DBtoObject._1_GoodCase;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/*
 * DB의 코드 테이블을 별도로 두고 이를 조회하여 사용하다보니, 계속해서 문제가 발생했습니다.
 * 01. 코드명만 봐서는 무엇을 나타내는지 알 수가 없었습니다.
 *     처음 프로젝트에 투입되는데, 모든 메소드들이 01, 1 등등의 매직넘버를 if 조건의 기준으로 되어있다고 상상해보겠습니다.
 *     01이란 코드가 뭔지 알기 위해서 서버코드에서 실행되는 코드를 보고 Grp_cd를 찾아내고, DB에서 조회해야만 했습니다.
 *     문서화가 되어있다 하더라도, 문서 업데이트가 잘되어있는지 확신할 수 없기에 DB를 다시 찾아봐야했습니다.
 * 02. 항상 코드 테이블 조회 쿼리가 실행되어야만 했습니다.
 *     특별히 조회를 하지 않음에도 UI를 그리기 위해 항상 코드 테이블을 조회해야만 했습니다.
 *     물론 캐시등을 적절히 활용하는 방법이 있습니다.
 * 03. 카테고리 코드를 기반으로한 서비스 로직을 추가할 때 그 위치가 애매했습니다.
 *     1 ~ 3 사례들과 비슷한 경우인데, 해당 코드에 따라 수행되야 하는 기능이 있을 경우 메소드의 위치는 Service 혹은 유틸 클래스가 될 수 밖에 없었습니다.
 *
 * 특히나 카테고리의 경우 6개월에 1~2개가 추가될까말까한 영역인데 굳이 테이블로 관리하는 것은 장점보다 단점이 더 많다고 생각하였습니다.
 * 카테고리성 데이터를 Enum으로 전환하고, 팩토리와 인터페이스 타입을 선언하여 일관된 방식으로 관리되고 사용할 수 있도록 진행하게 되었습니다.
 */
interface EnumMapperType {
	String getCode();
	String getTitle();
}

class EnumMapperValue {
	private String code;
	private String title;
	public EnumMapperValue(final EnumMapperType enumMapperType) {
		this.code = enumMapperType.getCode();
		this.title = enumMapperType.getTitle();
	}
	public String getCode() { return code; }
	public String getTitle() { return title; }
}

enum FeeType implements EnumMapperType {
	PERCENT("정율"),
	PRICE("정액");
	private String title;
	FeeType(final String title) { this.title = title; }
	@Override
	public String getCode() { return name();}
	@Override
	public String getTitle() { return title; }
}

public class _Application {
	public static void main(final String [] args) {
		List<EnumMapperValue> list = Arrays.stream(FeeType.values())
				.map(EnumMapperValue::new)
				.collect(Collectors.toList());
		list.forEach(ev -> System.out.println(ev.getCode() + " / " + ev.getTitle()));
	}
}

Best Case

package com.donzbox._04_DBtoObject._2_BestCase;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

interface EnumMapperType {
	String getCode();
	String getTitle();
}

class EnumMapperValue {
	private String code;
	private String title;
	public EnumMapperValue(final EnumMapperType enumMapperType) {
		this.code = enumMapperType.getCode();
		this.title = enumMapperType.getTitle();
	}
	public String getCode() { return code; }
	public String getTitle() { return title; }
}

enum FeeType implements EnumMapperType {
	PERCENT("정율"),
	PRICE("정액");
	private String title;
	FeeType(final String title) { this.title = title; }
	@Override
	public String getCode() { return name();}
	@Override
	public String getTitle() { return title; }
}

// 런타임에 Enum의 상수들이 변경될 일이 없기에, 관리 대상인 Enum들은 미리 Bean에 등록하여 사용하도록 변경
class EnumMapper {
	private Map<String, List<EnumMapperValue>> factory = new LinkedHashMap<>();

	// EnumMapperType 인터페이스 구현체만 오도록 제한함
	public void put(final String key, final Class<? extends EnumMapperType> e) {
		factory.put(key, toEnumValues(e));
	}
	private List<EnumMapperValue> toEnumValues(final Class<? extends EnumMapperType> e) {
		return Arrays.stream(e.getEnumConstants())
				.map(EnumMapperValue::new)
				.collect(Collectors.toList());
	}
	public List<EnumMapperValue> get(final String key) { return factory.get(key); }
	public Map<String, List<EnumMapperValue>> get(final List<String> keys) {
		if(keys == null || keys.size() == 0) {
			return new LinkedHashMap<>();
		}
		return keys.stream()
				.collect(Collectors.toMap(Function.identity(), key -> factory.get(key)));
	}
	public Map<String, List<EnumMapperValue>> getAll() { return factory; }
}

/*
 * Enum을 사용하는데 있어 가장 큰 허들은 “변경이 어렵다“ 입니다.
 * 코드를 추가하거나 변경해야 하는 일이 빈번하다면, 매번 Enum 코드를 변경하고 배포하는것보다
 * 관리자 페이지에서 관리자가 직접 변경하는 것이 훨씬 편리할 수 있다고 생각합니다.
 * 하지만 우리가 관리하는 이 코드 테이블은 과연 얼마나 자주 변경되나요?
 *
 * 한번 생성된 코드들은 얼마나 많은 테이블에서 사용되시나요?
 * 사용되는 테이블이 많아 변경하게 되면 관련된 테이블 데이터를 전부다 변경해야 하진 않으신가요?
 * 한번 생성된 코드테이블의 코드들을 변경할 일이 자주 있으셨나요?
 * 추가되는 코드는 한달에 몇번이나 발생하시나요?
 * 1년에 몇번 발생하시나요? 매일 발생하시나요? 하루에 배포는 몇번을 하시나요?
 *
 * 만약 위와 같은 상황이라면 테이블로 관리함으로써 얻는 장점이
 * 정적언어를 활용함으로써 얻는 장점을 버릴정도로
 * 더 큰지 고민해봐야할 문제라고 생각합니다.
 */
public class _Application {
	// like define Bean
	public static EnumMapper enumMapper() {
		EnumMapper enumMapper = new EnumMapper();
		enumMapper.put("FeeType", FeeType.class);
		return enumMapper;
	}
	public static void main(final String [] args) {
		// like define Annotation
		EnumMapper enumMapper = enumMapper();
		List<EnumMapperValue> list = enumMapper.get("FeeType");
		list.forEach(ev -> System.out.println(ev.getCode() + " / " + ev.getTitle()));
	}
}