Enum 활용(공통코드를 Enum으로..)

wooSim·2023년 7월 8일
0

최근 회사 프로젝트에서 장부를 작성하여 지출 및 수입 내역을 관리하는 DB 설계와 개발을 하였습니다. 처음 설계를 할때는 지출 수입 구분 코드를 DB공통코드로. 그리고 지출에 해당하는 지출 항목, 수입에 해당하는 수입 항목을 또 다시 DB 공통코드로 만들었습니다.

장부 테이블의 경우 아래와 같이 DB 모델링 하였습니다.

  • 장부 일련번호
  • 작성일자
  • 지출 수입 구분 코드
  • 항목 코드
  • 금액
  • 장부내용
    ...

위의 진하게 표시한 논리명이 공통 코드로 개발한 컬럼입니다. 최근 공부를 하면서 해당 부분은 Enum을 활용했으면 어땠을까?-? 라는 생각을 하게 되었고 프로젝트 당시에 바쁘다는 핑계로 하지못한 리팩토링을 개인적으로 공부 겸 하게 되었고 블로깅하게 되었습니다. 😅

해당 글을 배달의 민족 기술 블로그 Java Enum 활용기를 참고 하였습니다.



DB 공통코드로 변경 시 Enum 고려사항


먼저 DB 공통코드로 만들지, Enum으로 만들지 고민해봐야 합니다. 각각의 장단점이 있기 때문입니다.

□ Enum 장점

  • IDE의 적극적인 지원
  • 허용 가능한 값들로 제한 가능
  • 리팩토링 범위의 최소화

Enum에는 이러한 장점들이 있지만 꼭 Enum으로 바꾸는 것이 좋지만은 않습니다. Enum과 공통코드 사이에서 가장 중요한 부분은 얼마나 자주 추가 및 변경이 일어나는가? 일 것 입니다. 만약 추가 및 변경이 자주 일어난다면 위의 Enum 장점을 포기하더라도 공통코드로 관리하는 것이 정답입니다.


제가 만든 공통코드의 경우

  • 지출 수입 구분 공통 코드 : 거의 변경이 일어나지 않습니다.
  • 지출 코드, 수입 공통 코드 : 추가하는 경우가 발생할 수 있겠지만 자주 발생하지는 않습니다.

저는 이러한 고민 뒤, Enum 으로 변경을 해보았습니다.



DB 공통코드를 Enum으로 변경


먼저 제가 설계한 DB 공통 코드입니다.

Enum을 활용해 많은 이득을 볼 수 있지만 저의 경우 2 가지 관점에서 많은 고민을 하였습니다.

  1. 데이터 그룹 관리
    • 지출에 해당하는 항목 코드와 수입에 해당하는 항목 코드를 그룹하고 싶었습니다.
  2. 자주 발생하는 코드 테이블 조회쿼리를 Enum으로 관리

□ 데이터 그룹 관리

먼저 아래와 같이 지출 및 수입 구분코드와 지출 항목 코드, 수입 항목 코드를 그릅화할 필요가 있었습니다.

이를 Enum으로 표현하면 코드만 보고도 제 유지보수하는 개발자는 해당 관계를 바로 파악할 수 있을 것입니다.

이를 위해 저는 먼저 지출 및 수입 구분 코드와 항목코드(지출, 수입)를 Enum 클래스로 만들었습니다.

@Getter
@RequiredArgsConstructor
public enum LedgerDsc {
    EXPENDITURE("지출"),
    INCOME("수입");

    private final String title;
  
    public String getTitle() {
        return title;
    }
    
}

@RequiredArgsConstructor
public enum Item {

    FOOD("식비"),
    SHOPPING("쇼핑"),
    COFFEE("커피"),
    DATE("데이트통장"),
    ALCOHOL("음주비"),
    TRANSPORTATION("교통비"),
    ETC_EXPENDITURE("기타지출"),
    SALARY("월급"),
    BONUS("성과급"),
    CLOTHING_PAYMENT("피복비"),
    GIFT_CARD("상품권"),
    ETC_INCOME("기타수입");
    
    private final String title;
    
    public String getTitle() {
        return title;
    }

}

그리고 지출 및 수입 별 항목을 그룹화 하기 위해 아래 코드와 같이 LedgerDsc를 아래와 같이 변경 해주었습니다.

@RequiredArgsConstructor
public enum LedgerDsc {
    EXPENDITURE("지출", Arrays.asList(Item.FOOD, Item.SHOPPING, Item.COFFEE, Item.DATE, Item.ALCOHOL, Item.TRANSPORTATION, Item.ETC_EXPENDITURE)),
    INCOME("수입", Arrays.asList(Item.SALARY, Item.BONUS, Item.CLOTHING_PAYMENT, Item.GIFT_CARD, Item.ETC_INCOME));


    private final String title;

    private final List<Item> itemList;
  
    public String getTitle() {
        return title;
    }

    public List<Item> getItemList() {
        return itemList;
    }
  

이제 Enum 클랙스만 보고도 해당 데이터간의 관계를 확실히 알 수 있게 되었습니다.


□ 관리주체를 DB에서 Enum으로

DB에서 관리 했을 때는 view에서 코드가 필요할 때 마다 코드 테이블을 조회해서 사용해야 했습니다. 하지만 이제 Enum으로 관리하기 때문에 Enum의 데이터를 view에서 활용할 수 있게 만들어야 합니다.

Enum 데이터를 바로 Json을 리턴하게 될 경우 Enum의 name만 출력하게 됩니다.

// 지출 및 수입 내역 데이터를 바로 json으로 리턴
Stream.of(LedgerDsc.values()).collect(Collectors.toList());
//
/* 
 * 출력:
 * [
 *  "EXPENDITURE",
 *   "INCOME"
 * ]
*/

View layer에서 활용하기 위해서는 name 값과 title 값이 함께 필요하기 때문에 새로운 클래스가 필요합니다.

먼저, 어느 Enum 클래스에서도 활용할 수 있도록 인터페이스를 생성하였습니다.

public interface EnumMapperType 인터페이스를 생성자로 받았습니다. 
{
    String getCode();
    String getTitle();
}

이제 Enum 값을 담기 위한 VO 클래스를 만들고 EnumMapperType 인터페이스를 생성자로 받았습니다.

public class EnumMapperValue {
    private final String code;
    private final 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 클래스에서는 name과 title을 implements 받아 구현해줍니다.

public enum LedgerDsc implements EnumMapperType {
    EXPENDITURE("지출", Arrays.asList(Item.FOOD, Item.SHOPPING, Item.COFFEE, Item.DATE, Item.ALCOHOL, Item.TRANSPORTATION, Item.ETC_EXPENDITURE)),
    INCOME("수입", Arrays.asList(Item.SALARY, Item.BONUS, Item.CLOTHING_PAYMENT, Item.GIFT_CARD, Item.ETC_INCOME));

    private final String title;

    private final List<Item> itemList;

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

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

    public List<Item> getItemList() {
        return itemList;
    }
}
  
public enum Item implements EnumMapperType {

    FOOD("식비"),
    SHOPPING("쇼핑"),
    COFFEE("커피"),
    DATE("데이트통장"),
    ALCOHOL("음주비"),
    TRANSPORTATION("교통비"),
    ETC_EXPENDITURE("기타지출"),
    SALARY("월급"),
    BONUS("성과급"),
    CLOTHING_PAYMENT("피복비"),
    GIFT_CARD("상품권"),
    ETC_INCOME("기타수입");


    private final String title;

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

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

이제 EnumMapperValue 클래스에 값을 답아 보내면 결과가 잘 나오는 것을 확인할 수 있습니다.

Stream.of(LedgerDsc.values()).map(EnumMapperValue::new).collect(Collectors.toList());

이제 Enum으로 관리하면서 View 계층에서도 데이터를 활용할 수 있지만 LedgerDsc.values() 부분을 통해 계속 인스턴스를 생성하는 아쉬운이 있습니다. 이를 해결하기 위해서 Enum들을 Bean에 등록하여 사용하도록 변경하겠습니다. 먼저 factory 클래스를 만들어줍니다.

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

    public EnumMapper() {}

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

    private List<EnumMapperValue> toEnumValues(Class<? extends EnumMapperType> e) { // EnumMapperType 인터페이스 구현체만 오도록 제한

        return Arrays.stream(e.getEnumConstants())
                .map(EnumMapperValue::new)
                .collect(Collectors.toList());
    }

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

    public Map<String, List<EnumMapperValue>> get(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;}

}

이를 Bean으로 등록하겠습니다.

@Configuration
public class AppConfig {

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


}

Api를 다시 호출하면 잘 호출 되는 것을 확인할 수 있습니다.

@GetMapping("/api/v1/ledger-dsc")
public ResponseEntity<List<EnumMapperValue>> getLedgerDscList() {
	return ResponseEntity.ok(enumMapper.get("ledgerDsc"));
}

추가로 수입 항목과 지출 항목 각각을 View layer에서 활용할 수 있도록 코드를 더 추가해 보도록 하겠습니다. 지출 항목과 수입 항목을 그룹화한 지출 수입 구분 Enum 클래스에서 static 함수를 추가하였습니다.

@RequiredArgsConstructor
public enum LedgerDsc implements EnumMapperType {
    EXPENDITURE("지출", Arrays.asList(Item.FOOD, Item.SHOPPING, Item.COFFEE, Item.DATE, Item.ALCOHOL, Item.TRANSPORTATION, Item.ETC_EXPENDITURE)),
    INCOME("수입", Arrays.asList(Item.SALARY, Item.BONUS, Item.CLOTHING_PAYMENT, Item.GIFT_CARD, Item.ETC_INCOME));


    private final String title;

    private final List<Item> itemList;

    public static List<EnumMapperValue> findByIncomeItemList(){ // 수입 내역 
        return INCOME.getItemList().stream().map(EnumMapperValue::new)
				.collect(Collectors.toList());
    }

    public static List<EnumMapperValue> findByExpenditureItemList(){ // 지출 내역

        return EXPENDITURE.getItemList().stream().map(EnumMapperValue::new)
				.collect(Collectors.toList());
    }

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

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

    public List<Item> getItemList() {
        return itemList;
    }
}

EnumMapper 클래스에서도 factory에 추가할 수 있또록 put함수를 오버로딩하겠습니다.

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

    public EnumMapper() {}

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

	public void put(String key, List<EnumMapperValue> e){
        factory.put(key, e);
    }
    
    ...

factory에 값을 추가할때 지출 항목과 수입 항목도 추가해줍니다.

@Bean
public EnumMapper enumMapper() {
	EnumMapper enumMapper = new EnumMapper();
	enumMapper.put("ledgerDsc", LedgerDsc.class);
	enumMapper.put("expenditureItem", LedgerDsc.findByExpenditureItemList());
	enumMapper.put("incomeItem", LedgerDsc.findByIncomeItemList());
	return enumMapper;
}

@GetMapping("/api/v1/ledger-dsc")
public ResponseEntity<Map<String, List<EnumMapperValue>>> getLedgerDscList() {
        return ResponseEntity
			.ok(enumMapper.get(Arrays.asList("ledgerDsc", "expenditureItem", "incomeItem")));
}




코드테이블을 Enum으로 활용해본 경험을 적어보았습니다. 사실 Enum 활용에 대해서는 이미 많은 블로그에서도 기술하였고 저보다 다양하게 활용한 내용이 많습니다. 하지만 개인적으로 고민해보고 찾아본 내용에 대해 한번 더 기록하며 기억하는 것에 이의를 두며 작성해보게 되었습니다. 부족한 부분이 많을텐데 읽어주셔서 감사합니다!! 혹시 틀린 내용이나 코드가 있을 경우 언제든지 말씀해주시면 감사하겠습니다!!

profile
daily study

0개의 댓글