1. 열거형이 필요한 이유
1.1. 타입 안정성 - String 활용 시
// DiscountService
public class DiscountService {
public int discount(String grade, int price) {
int discountPercent = 0;
switch (grade) {
case "BASIC" -> discountPercent = 10;
case "GOLD" -> discountPercent = 20;
case "DIAMOND" -> discountPercent = 30;
default -> System.out.println(grade + ": 할인X");
}
return price * discountPercent / 100;
}
}
// Main
public class StringGradeEx0_1 {
public static void main(String[] args) {
int price = 10000;
DiscountService discountService = new DiscountService();
System.out.println("BASIC 등급의 할인 금액: " + discountService.discount("BASIC", price));
System.out.println("GOLD 등급의 할인 금액: " + discountService.discount("GOLD", price));
System.out.println("DIAMOND 등급의 할인 금액: " + discountService.discount("DIAMOND", price));
// 존재 x
System.out.println("VIP 등급의 할인 금액: " + discountService.discount("VIP", price));
//오타 or 소문자
System.out.println("DIAMOND 등급의 할이 금액: " + discountService.discount("DAIMOND", price));
}
}
String 사용 시 타입 안전성 부족 문제
- 개발자가 정해지지 않은 임의의 String 입력
- 오타
DIAMOND
->DAIMOND
- 소문자 입력데이터 일관성
DIAMOND
,diamond처럼
일관되지 않은 문자열
1 ) 컴파일 시 오류 감지 불가
- 이러한 값은 컴파일 시에는 잡을 수 없고 런타임에서 발견되기 때문에 문제 파악이 어려울 수 있음
2) 타입 안정성 - 상수로 활용하여 개선
public class StringGrade {
public static final String BASIC = "BASIC";
public static final String GOLD = "GOLD";
public static final String DIAMOND = "DIAMOND";
}
이런 방식으로 상수를 지정하여 사용하면 오탈자 혹은 일관성에 대한 문제는 해결할 수 있으나 근본적인 문제들은 해결이 되지 않았다.
이러한 부분을 조금 더 개선한 타입 안전 열거형 패턴에 대해 알아보자.
2. 타입 안전 열거형 패턴 (Type-Safe Enum Pattern)
위의 문제들을 해결하기 위해 등장한 것이 타입 안전 열거형 패턴
이다.
또한 이 패턴을 많은 사람들이 사용하자 사용하기 편하게 만들어준 것이 결국 Enum
이다.
우선 이 패턴을 구현하며 작동 방식에 대해 알아보도록 하자.
2.1. 구현
public class ClassGrade {
public static final ClassGrade BASIC = new ClassGrade();
public static final ClassGrade GOLD = new ClassGrade();
public static final ClassGrade DIAMOND = new ClassGrade();
private ClassGrade() {
}
// 우선 새로운 타입을 생성하는 것을 방지하기 위해 생성자를 private로 변경해준다.
public class ClassGradeEx2_2 {
public static void main(String[] args) {
int price = 10000;
DiscountService discountService = new DiscountService();
// 컴파일 에러
ClassGrade classGrade = new ClassGrade(); System.out.println("BASIC 등급의 할인 금액: " + discountService.discount(classGrade, price));
}
}
// 새로운 타입을 넣으려고 하면 컴파일 에러로 잡아준다.
// 즉 미리 생성해둔 BASIC, GOLD, DIAMOND 만 사용 가능하게 된다.
타입 안전 열거형 패턴의 장점
- 타입 안정성 향상 : 정해진 객체만 사용 가능 하므로 잘못된 값을 입력하는 문제를 방지
- 데이터 일관성 : 위와 같은 이유로 데이터 일관성 보장
단점
이런 패턴을 사용하려면 코드를 꽤 많이 작성해야 한다.
위에서 말했듯이 이런 부분을 사용하기 편하게 해 준 것이 enum
의 등장 배경이라고도 볼 수 있다.
3. 열거형 (Enum Type)
3.1. 열거형 개요
열거형 사용 방식
public enum Grade {
BASIC,
GOLD,
DIAMOND
}
- class 대신 enum 사용
- 원하는 상수의 이름을 나열 (대문자)
위의 DiscountService
를 enum
을 사용하여 작성해보자.
// enum 사용
public class DiscountService {
public int discount(Grade grade, int price) {
int discountPercent = 0;
switch (grade) {
case BASIC -> discountPercent = 10;
case GOLD -> discountPercent = 20;
case DIAMOND -> discountPercent = 30;
default -> System.out.println(grade + ": 할인X");
}
return price * discountPercent / 100;
}
}
// static import를 활용하여 Grade.BASIC -> BASIC으로 사용 가능
위에 고민하던 부분들이 모두 해결
열거형의 장점
가볍게 키워드만 작성할 테니 그 이유는 직접 생각해 보기로 하자
- 타입 안정성 향상
- 간결성 및 일관성
- 확장성
4. 열거형 활용
4.1. 팩토리 클래스
개발을 하다 보면 같은 인터페이스의 구현체가 여러 개 있는 경우가 있다.
이런 경우 if문으로 분기를 하기 마련인데 이를 enum을 활용한 팩토리클래스로 만들면 조금 더 보기 편하게 코드를 작성할 수 있다.
다음 예시는 Spring에서 자주 활용될만한 코드를 Java로 변환하였다.
Enum을 사용하지 않은 코드
public class LoginFactory {
private final KakaoLogin kakaoLogin;
private final NaverLogin naverLogin;
private final GoogleLogin googleLogin
public LoginService type(LoginType loginType) {
if(loginType == LoginType.KAKAO) {
return kakaoLogin;
} else if(loginType == LoginType.NAVER) {
return naverLogin;
} else if(loginType == LoginType.GOOGLE) {
return googleLogin;
} else (loginType == LoginType.FAIL) {
return ExceptionCode.LoginFail
}
}
}
보통 이런 방식으로 팩토리를 작성하여 사용한다. 그러나 여기서 불편한 문제가 발생한다
- 새로운 기능의 추가 시 if ~ else 문의 수정이 필요
새로운 LoginType의 추가에 따라 Factory 또한 수정이 들어간다.
이를 우선 유연한 Factory로 전환을 해보도록 하자.
public class LoginFactory {
private final List<LoginService> loginServiceList;
private final Map<LoginType, LoginService> loginServiceMap;
public LoginFactory(List<LoginService> loginServiceList, Map<LoginType, LoginService> loginServiceMap) {
this.loginServiceList = loginServiceList;
this.loginServiceMap = loginServiceMap;
}
public LoginService findFromList(final LoginType loginType) {
LoginService loginService = loginServiceMap.get(loginType);
if (loginService != null) {
return loginService;
}
loginService = loginServiceList.stream()
.filter(l -> l.type(loginType))
.findFirst()
.orElseThrow();
loginServiceMap.put(loginType, loginService);
return loginService;
}
public LoginService findFromMap(final LoginType loginType) {
return loginServiceMap.get(loginType);
}
}
유연한 팩토리로 전환
- 새로운
loginService
가 추가되어도 팩토리는 부분이 없어짐 Map
에 값을 미리 캐싱해 놓아서 매번 stream()을 도는 부분을 개선함
더 발전시키기
우선 저는 여기까지 작성하고 직장의 사수분과 어쩌다가 enum에 대한 이야기를 하며 이 부분을 보여줬더니 조금 더 발전시킬 수 있겠다고 리뷰해 주셨습니다.
그러면서 우아한 기술 블로그의 해당 글을 추천해 주면서 조금 더 객체지향적으로 코드를 변경해 보기를 추천해 주셨습니다.
해당 내용은 조금 더 다듬어서 포스팅하도록 하겠습니다 :)
**참고자료
- 인프런 - 김영한의 자바 중급 (1)
- Youtube - 제미니의 개발실무
- 블로그 - 망나니개발자 https://mangkyu.tistory.com/252
- 블로그 - 우아한 기술 블로그 https://techblog.woowahan.com/2527/