[Java] Enum Class

Alex Moon·2023년 9월 21일
0

Java

목록 보기
4/4

Enum Class는 상수들의 집합이며, 열거형 Class라고도 불린다. 이 Class의 상수를 호출하면 자동으로 생성자가 실행되며, 개발자가 직접 생성자를 호출할 수 없다.

우리는 사람이기 때문에 Error Code나 UserRole 같은 값들을 매번 String이나 int와 같은 값으로 직접 작성하거나 변경을 한다고 생각해보자. 이렇게 작성된 코드는 가독성이 떨어지는 것은 물론이고 오타가 나거나 특정 값을 누락하는 실수가 발생할 확률이 매우 높다. Enum Class는 이러한 값들을 상수들로 미리 정의하여 Human Mistake를 방지할 수 있도록 한다. 이를 통해 비즈니스 로직을 간단하고 견고하게 만드는데 유리하다.

이 Class는 다음과 같이 활용할 수 있다.

  • 데이터의 연관 관계 표현
  • 상태에 따른 행위 표현
  • 데이터 그룹 관리
  • DB의 메타 테이블을 객체로 관리

이러한 특징들로 허용 가능한 값을 제한하고 리팩토링 변경 범위를 최소화 할 수 있다.
이 Class의 값을 정의할 때는 대문자로 작성하며, 언더바_를 활용하여 글자를 구분한다.

데이터의 연관 관계 표현

아래와 같이 ErrorType과 ErrorCode를 묶어서 연관성을 표현해줄 수 있다.

public enum ErrorCode {
    UNAUTHORIZED(401),
    NOT_FOUND(404),
    INTERNAL_SERVER_ERROR(500),
    EXPECTATION_FAILED(417),
    SERVICE_UNAVAILABLE(503);

    private final int code;
    
    public String getCode() {
        return code;
    }
}

상태에 따른 행위 표현

만약 회원의 등급마다 할인가가 다르게 적용된 서비스를 제공한다고 가정하자. 단순히 if문을 사용한다면 회원 할인가가 적용된 가격이란 동일 관심사임에도 불구하고, 각 등급마다 분기하여 처리해야 한다. 결국 불필요하게 반복되는 내용들을 작성하게 되어 코드가 길어지게 된다.

public enum MembershipGrade {
    NON_MEMBER,
    FAMLIY,
    SILVER,
    GOLD,
    VIP;
}

public int calculateMembershipPrice(MembershipGrade membershipGrade, int price) {
    if (membershipGrade == MembershipGrade.NON_MEMBER) {
        return price;
    }
    
    if (membershipGrade == MembershipGrade.FAMLIY) {
        return price - 1000;
    }
    
    ...
}

이러한 문제를 회원 등급에 할인가를 적용하는 연산을 Enum Class에 정의하여 해결할 수 있다.

public enum MembershipGrade {
    NON_MEMBER(price -> price),
    FAMLIY(price -> price - 1000),
    SILVER(price -> price - 5000),
    GOLD(price -> price * 0.8),
    VIP(price -> price * 0.7);

    private Function<Integer, Integer> expression;

    MembershipGrade(Function<Integer, Integer> expression) {
        this.expression = expression;
    }
    
    /*
    * price : 실제 가격
    * return : 할인가
    */
    public int calculateMembershipPrice(int price) {
        return expression.apply(price);
    }
}

일반적으로 회원 등급은 DB의 회원 정보에 같이 저장한다. 그렇기 때문에 회원 등급을 Enum 형태로 DB에 저장해 두면, 이후 꺼내왔을 때 Enum Class에 정의된 연산 로직만 호출하기만 하면 된다.

public int getServicePriceByUid(int uid) {
    UserInfo userInfo = UserInfoRepository.findUserInfoByUid(uid);
    
    return userInfo.getMembershipGrade
            .calculateMembershipPrice(userInfo.UsingService.getPrice());
}
  • Enum이 DB에 저장될 때는 VARCHAR로 저장되며 Enum에 정의된 값과 동일한 형태로 저장된다.

데이터 그룹 관리

패션 쇼핑몰에서 카테고리는 대략 다음과 같이 분류될 것이다.

여기서 구매자가 구매한 옷들을 그룹별로 분류하고, 해당 그룹별 기능을 만들 때, if문을 활용하게 된다면 다음과 같이 될 것이다.

public String getClothesGroup(String clothesType) {
    if ("Sweatshirt".equals(clothesType) || "Knitwear".equals(clothesType)
            || "Shirt".equals(clothesType) || "Hoodie".equals(clothesType) {
        return "Top";
    } else if ("DenimPants".equals(clothesType) || "TrainingPants".equals(clothesType)
            || "CottonPants".equals(clothesType) || "Slacks".equals(clothesType) {
        return "Bottoms";
    } else if ("Sneakers".equals(clothesType) || "Loafer".equals(clothesType)
            || "Sandals".equals(clothesType) || "Sports".equals(clothesType) {
        return "Shoes";
    } else {
        return "Empty";
    }
}

public void pushClothesGroup(String clothesType) {
    if ("Top".equals(clothesType)) {
        pushTopMethod();
    } else if ("Bottom".equals(clothesType)) {
        pushBottomMethod();
    } else if ("Shoes".equals(clothesType)) {
        pushShoesMethod();
    } else {
        throw new RuntimeException("clothesType이 존재하지 않습니다.")
    }
}

public void printClothesGroup(String clothesType) {
    if ("Top".equals(clothesType)) {
        printTopMethod();
    } else if ("Bottom".equals(clothesType)) {
        printBottomMethod();
    } else if ("Shoes".equals(clothesType)) {
        printShoesMethod();
    } else {
        throw new RuntimeException("clothesType이 존재하지 않습니다.")
    }
}

이런 코드는 관리하기가 어렵기 때문에, 추후 새로운 타입, 그룹 또는 기능을 추가하기 어려워 확장성이 저하된다. 또한 입력값이 값이 정해지지 않은 String 형태로 들어오기 때문에 출력값 또한 예측할 수 없다.

Enum Class로 변환하게 되면 이러한 문제점들을 해결할 수 있게 된다.

public enum ClothesType {
    SWEATSHIRT("스웨트셔츠"),
    KNITWEAR("니트"),
    SHIRT("셔츠"),
    HOODIE("후드"),
    DENIM_PANTS("데님 팬츠"),
    COTTON_PANTS("코튼 팬츠"),
    SLACKS("슬랙스"),
    TRAINING_PANTS("트레이닝 팬츠"),
    SNEAKERS("스니커즈"),
    LOAFER("로퍼"),
    SANDALS("샌들"),
    SPORTS("스포츠"),
    EMPTY("없음");

    private final String title;

    ClothesType(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }
}

public enum ClothesGroup {
    TOP("상의", 
            Arrays.asList(
                    ClothesType.SWEATSHIRT, ClothesType.KNITWEAR, 
                    ClothesType.SHIRT, ClothesType.HOODIE
            )
    ),
    BOTTOMS("하의", 
            Arrays.asList(
                    ClothesType.DENIM_PANTS, ClothesType.COTTON_PANTS, 
                    ClothesType.SLACKS, ClothesType.TRAINING_PANTS
            )
    ),
    SHOES("신발", 
            Arrays.asList(
                    ClothesType.SNEAKERS, ClothesType.LOAFER, 
                    ClothesType.SANDALS, ClothesType.SPORTS
            )
    ),
    EMPTY("없음", Collections.emptyList());

    private final String title;
    private final List<ClothesType> clothesTypeList;

    ClothesGroup(String title, List<ClothesType> clothesTypeList) {
        this.title = title;
        this.clothesTypeList = clothesTypeList;
    }

    public static ClothesGroup findByClothesType(ClothesType clothesType) {
        return Arrays.stream(ClothesGroup.values())
                .filter(clothesGroup -> clothesGroup.hasPayCode(clothesType))
                .findAny()
                .orElse(EMPTY);
    }

    public boolean hasPayCode(ClothesType clothesType) {
        return clothesTypeList.stream()
                .anyMatch(type -> type == clothesType);
    }
    
    public String getTitle() {
        return title;
    }
}

우선 옷 종류와 그룹이 Enum으로 모두 정의되어 있으므로 입력값과 출력값이 예측 가능해진다. 또한 모든 값들이 Enum으로 관리되고 있어 편하고 새로운 타입이나 그룹을 추가하기 편하다. 또한 새로운 기능을 추가할 때도 그룹은 신경쓰지 않고 기능 구현에 필요한 내용만 작성하면 된다.

public void getClothesGroup(String productId) {
    Product product = productRepository.findByProductId(productId);
    ClothesGroup clothesGroup = ClothesGroup.findByClothesType(product.getClothesType);
}

public void pushClothesGroup(ClothesGroup clothesGroup) {
    pushShoesMethod();
}

public void printClothesGroup(ClothesGroup clothesGroup) {
    printShoesMethod();
}

DB의 메타 테이블을 객체로 관리

도서 정보를 위한 DB 테이블을 설계할 때, 책의 카테고리를 분류하는 id가 포함될 것이고, 이 id에 대한 명세를 가진 테이블을 만들게 될 것이다. 이 테이블은 다음과 같은 특징을 가진다.

  • 변경될 일이 거의 없다.
  • UI에 표시할 때만 테이블을 조회한다.

이러한 경우에는 Enum Class로 대체하여 관리해줄 수 있다.

public enum BookCategory {
    BC101("소설"),
    BC102("시/에세이"),
    BC103("예술/대중문화"),
    BC104("사회과학"),
    BC105("역사와 문화");

    private final String title;

    BookCategory(String title) {
        this.title = title;
    }
    
    public String getTitle() {
        return title;
    }
}

그리고 UI 상에서 필요할 때만 이 Enum 값을 받아서 사용할 수 있게 해준다. 이 Enum Class가 Controller를 통해 반환할 수 있도록 다음과 같이 Class를 작성해준다.

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

public class EnumMapperValue implements Serializable {
    private static final long serialVersionUID = 6230704194885989241L;
    
    private String code;
    private String title;

    public EnumMapperValue(EnumMapperType enumMapperType) {
        this.code = enumMapperType.getCode();
        this.title = enumMapperType.getTitle();
    }

    public String getCode() {
        return code;
    }

    public String getTitle() {
        return title;
    }

    @Override
    public String toString() {
        return "EnumMapperValue{" +
                "code='" + code + '\'' +
                ", title='" + title + '\'' +
                '}';
    }
}

그리고 Enum Class에도 EnumMapperType을 상속받도록 변경한다.

public enum BookCategory {
    BC101("소설"),
    BC102("시/에세이"),
    BC103("예술/대중문화"),
    BC104("사회과학"),
    BC105("역사와 문화");

    private final String title;

    BookCategory(String title) {
        this.title = title;
    }
    
    public String getTitle() {
        return title;
    }
    
    @Override
    public String getCode() {
        return name();
    }

    @Override
    public String getTitle() {
        return name;
    }
}

이제 필요할 때 요청을 받아 Enum 값을 반환할 수 있도록 Controller에 추가해준다.

@GetMapping("/bookCategoryList")
public List<EnumMapperValue> getBookCategoryList() {
    return Arrays.stream(BookCategory.values())
                .map(EnumMapperValue::new)
                .collect(Collectors.toList());
}

하지만 여기서 또 하나의 아쉬운 점이 발생한다. 바로 Enum은 상수값이기 때문에 변경될 일이 없는데, Controller를 통해 반환할 때마다 EnumMapperValue 객체를 생성하게 되는 것이다. 이러한 점을 해결하기 위해 Enum Value들을 담을 팩토리 클래스를 작성한다.

public class EnumMapper {
    private Map<String, List<EnumMapperValue>> factory = new LinkedHashMap<>();

    public EnumMapper() {}

    public void put(String key, Class<? extends EnumMapperValue> e) {
        factory.put(key, toEnumValues(e));
    }

    private List<EnumMapperValue> toEnumValues(Class<? extends EnumMapperValue> e) {
        return Arrays.stream(BookCategory.values())
                .map(EnumMapperValue::new)
                .collect(Collectors.toList());
    }

    public List<EnumMapperValue> get(String key) {
        return factory.get(key);
    }

    public Map<String, List<EnumMapperValue>> get(List<String> keyList) {
        if (keyList == null || keyList.isEmpty()) {
            return new LinkedHashMap<>();
        }

        return keyList.stream()
                .collect(Collectors.toMap(Function.identity(), key -> factory.get(key)));
    }

    public Map<String, List<EnumMapperValue>> getAll() {
        return factory;
    }
}

이 팩토리에서 get을 할 때 파라메터 인자값으로 Class<? extends EnumMapperType> e를 받게 하는 이유는 EnumMapperType을 상속받아 구현한 Class만 접근할 수 있도록 제한하기 위함이다.
마지막으로 이 팩토리를 bean으로 등록해주고 필요한 곳에서 이 bean을 DI하여 사용할 수 있도록 한다.

@Bean
public EnumMapper enumMapper() {
    EnumMapper enumMapper = new EnumMapper();
    
    enumMapper.put("BookCategory", BookCategory.class);
    
    return enumMapper;
}

@GetMapping("/bookCategoryList")
public List<EnumMapperValue> getBookCategoryList() {
    return enumMapper.get("BookCategory");
}

정리

Enum Class는 특정값들을 상수값으로 정의하여 휴먼미스테이크를 방지한다는 점만 알고 있었다. 하지만 이 블로그를 읽어보며 이 Enum Class를 훨씬 다양하게 활용하여 코드를 더욱 명확하고 견고하게 작성할 수 있게 해준다는 것을 알게 되었다. 하지만 이 Enum Class는 변경이 어렵기 때문에 무조건적으로 활용하기보다 상황에 맞게 유연하게 사용해야 한다고 한다.

정확히 어떤 점들이 유리하고 불리한지, 어떤 상황에 활용하는 것이 적절한지는 아래 블로그 링크에 설명되어 있다. 해당 내용들을 여기에 작성하기에는 직접 활용한 경험이나 이해도가 아직 많이 부족하기 때문에 여기서 언급할 내용은 아닌 것 같다...


참고

profile
느리더라도 하나씩 천천히. 하지만 꾸준히

0개의 댓글