Java8 enum 의 활용
2018. 9. 4. 17:39ㆍ99. 정리전 - 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())); } }