최근 회사 프로젝트에서 장부를 작성하여 지출 및 수입 내역을 관리하는 DB 설계와 개발을 하였습니다. 처음 설계를 할때는 지출 수입 구분 코드를 DB공통코드로. 그리고 지출에 해당하는 지출 항목, 수입에 해당하는 수입 항목을 또 다시 DB 공통코드로 만들었습니다.
장부 테이블의 경우 아래와 같이 DB 모델링 하였습니다.
위의 진하게 표시한 논리명이 공통 코드로 개발한 컬럼입니다. 최근 공부를 하면서 해당 부분은 Enum을 활용했으면 어땠을까?-? 라는 생각을 하게 되었고 프로젝트 당시에 바쁘다는 핑계로 하지못한 리팩토링을 개인적으로 공부 겸 하게 되었고 블로깅하게 되었습니다. 😅
해당 글을 배달의 민족 기술 블로그 Java Enum 활용기를 참고 하였습니다.
먼저 DB 공통코드로 만들지, Enum으로 만들지 고민해봐야 합니다. 각각의 장단점이 있기 때문입니다.
Enum에는 이러한 장점들이 있지만 꼭 Enum으로 바꾸는 것이 좋지만은 않습니다. Enum과 공통코드 사이에서 가장 중요한 부분은 얼마나 자주 추가 및 변경이 일어나는가? 일 것 입니다. 만약 추가 및 변경이 자주 일어난다면 위의 Enum 장점을 포기하더라도 공통코드로 관리하는 것이 정답입니다.
제가 만든 공통코드의 경우
저는 이러한 고민 뒤, Enum 으로 변경을 해보았습니다.
먼저 제가 설계한 DB 공통 코드입니다.
Enum을 활용해 많은 이득을 볼 수 있지만 저의 경우 2 가지 관점에서 많은 고민을 하였습니다.
먼저 아래와 같이 지출 및 수입 구분코드와 지출 항목 코드, 수입 항목 코드를 그릅화할 필요가 있었습니다.
이를 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에서 관리 했을 때는 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 활용에 대해서는 이미 많은 블로그에서도 기술하였고 저보다 다양하게 활용한 내용이 많습니다. 하지만 개인적으로 고민해보고 찾아본 내용에 대해 한번 더 기록하며 기억하는 것에 이의를 두며 작성해보게 되었습니다. 부족한 부분이 많을텐데 읽어주셔서 감사합니다!! 혹시 틀린 내용이나 코드가 있을 경우 언제든지 말씀해주시면 감사하겠습니다!!