안녕하세요~ 꾸준함의 미더덕입니다.
이전 게시물에선 Enum의 field name 자체로 의미를 가진 경우에 대하여 간단한 Enum 클래스를 적용해보았습니다.
이번에 리팩토링할 대상은, 값 자체로는 특정한 도메인 의미를 갖지 않는 코드값들에 대한 처리입니다.
일단 작성한 Enum 클래스들을 확인해볼까요?
상황) 상수명과 코드값이 일치하지 않음
@Getter
@AllArgsConstructor
public enum CategoryCode {
CUSTOMER_INQUIRY("CT000001"),
FREE_BOARD("CT000002"),
REFUND_INQUIRY("CT000003"),
//...
private String code;
}
위의 Enum클래스를 보시면, 상수명과 코드값이 명시적으로 일치하지 않는 상황인 것을 알 수 있습니다.
문제) 코드를 하드코딩하여 비즈니스 로직이 보이지 않는다.
그렇기 때문에 Enum처리가 되어 있지 않았던 예전 코드에선
코드를 통해 어떠한 비즈니스 로직이라는 것을 유추해내기 무척 어려웠습니다..
@Component
public class Validator {
public void validateCategory(PostForm postForm) {
String categoryCode = postForm.categoryCode();
if (categoryCode.equals("CT000001") || // 무슨 카테고리인거 같긴 한데..
categoryCode.equals("CT000002") ||
categoryCode.equals("CT000003") ||
categoryCode.equals("CT000004")) {
throw new IllegalArgumentException("잘못된 카테고리입니다");
}
if(categoryCode.equals("CT000005")){
throw new IllegalStateException("이상한 카테고리입니다");
}
}
}
적용 사항 1) Enum 적용하기
아래는 Enum을 적용하는 과정입니다.
인텔리제이에서 친절하게도 상수명에 해당하는 코드값도 보여주므로
게비스콘 한 봉을 비운 것처럼 시-원하게 코드를 작성할 수 있습니다.
@Component
public class Validator {
public void validateCategory(PostForm postForm) {
String categoryCode = postForm.categoryCode();
if (categoryCode.equals(CategoryCode.CUSTOMER_INQUIRY.getCode()) ||
categoryCode.equals(CategoryCode.FREE_BOARD.getCode()) ||
categoryCode.equals(CategoryCode.REFUND_INQUIRY.getCode()) ||
categoryCode.equals(CategoryCode.TECH_SUPPORT.getCode())) {
throw new IllegalArgumentException("잘못된 카테고리입니다");
}
if(categoryCode.equals(CategoryCode.PRODUCT_REVIEWS.getCode())){
throw new IllegalStateException("이상한 카테고리입니다");
}
}
}
Enum을 적용한 결과 코드입니다 !
Enum을 적용하기 전에는 코드만으로 비즈니스를 이해할 수 없었는데
이제는 해당 로직이 어떠한 행위를 하는가하는 충분히 유추해볼만한 가독성이 생겼습니다 !!
여기에 CategoryCode 이늄 클래스를 static으로 import하면 좀 더 깔끔하게 코드를 정리해볼 수도 있습니다.
import static goal.in.next.demo.constant.CategoryCode.*;
@Component
public class Validator {
public void validateCategory(PostForm postForm) {
String categoryCode = postForm.categoryCode();
if (!(categoryCode.equals(CUSTOMER_INQUIRY.getCode()) ||
categoryCode.equals(FREE_BOARD.getCode()) ||
categoryCode.equals(REFUND_INQUIRY.getCode()) ||
categoryCode.equals(TECH_SUPPORT.getCode()))) {
throw new IllegalArgumentException("사용 중이 아닌 카테고리입니다.");
}
}
}
적용 사항 2) 이늄에 행동 추가하기
그런데, 아직도 눈에 띄는 중복 코드가 많이 보이는 것 같습니다.
equals()를 하는 반복적인 or 조건문을 깔끔하게 처리해볼까요?
해당 처리는 Enum으로 선언한 "카테고리 코드"의 특정 비즈니스적 행동으로 생각해볼 수 있습니다.
아래와 같은 방식으로 리팩터링 한다면 가독성 뿐만아니라 비즈니스적 의미도 담아낼 수 있을 것 같습니다.
public void validateCategory(PostForm postForm) {
CategoryCode categoryCode = postForm.categoryCode();
if (!categoryCode.isUsable()) {
throw new IllegalArgumentException("사용 중이 아닌 카테고리입니다.");
}
}
-> 짠~ 처음과 비교했을 때 코드가 훨~씬 깔끔해진것 같죠?
서비스 단에서 비즈니스 로직에만 집중하기 위해서 Enum에 정의했던 getCode()나 equals() 까지 명시하지 않아도 되도록 메소드를 사용했습니다. 아래는 Enum 내부에 작성한 메소드입니다
@Getter
@AllArgsConstructor
public enum CategoryCode {
CUSTOMER_INQUIRY("CT000001"),
FREE_BOARD("CT000002"),
REFUND_INQUIRY("CT000003"),
TECH_SUPPORT("CT000004"),
PRODUCT_REVIEWS("CT000005"),
DISCUSSIONS("CT000006"),
USER_MANUALS("CT000007");
private String code;
// 특정 비즈니스 로직을 담은 이늄 객체의 행동을 정의함
public boolean isUsable(){
List usableList = List.of(CUSTOMER_INQUIRY, REFUND_INQUIRY, TECH_SUPPORT);
return usableList.contains(this);
}
}
위와 같은 메소드를 사용하여 코드를 훨씬 가독성있게 해줄 수 있었습니다.
이제 모든 작업이 끝난 것일까요??
@Service
@RequiredArgsConstructor
public class PostService {
private final Validator validator;
private final PostRepository postRepository;
@Transactional
public void insertPost(PostForm postForm) {
validator.validateCategory(postForm);
Post post = postForm.toEntity();
postRepository.save(post);
}
}
위의 서비스레이어를 이용해서 값을 저장해보고 작성한 코드가 맞는지 빠르게 테스트 해보겠습니다.
@Test
public void 포스트_저장_테스트(){
Post post = Post.builder()
.categoryCode(CategoryCode.CUSTOMER_INQUIRY)
.someCode(SomeCode.THING)
.expenditureCode(ExpenditureCode.FOOD)
.title("테스트 제목")
.content("테스트 내용")
.createdAt(LocalDateTime.now())
.deleteType(DeleteType.N)
.build();
Post savedPost = postRepository.save(post);
assertThat(savedPost.getCategoryCode()).isEqualTo(CategoryCode.CUSTOMER_INQUIRY); //성공
}
테스트도 성공하였습니다.
우리의 Enum 작업은 이대로 끝낼 수 있을까요?
미해결 문제 1) 의도한 값이 DB에 Insert 되지 않고 있음
마지막으로 혹시나 하는 마음으로 생성된 쿼리를 확인해봅니다.
이럴수가.. ?
Enum 값으로 코드가 제대로 들어가고 있지 않네요?
걱정마세요~ 이 경우는 간단히 수정해 줄 수 있습니다.
@Entity
@Table(name = "post")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@IdClass(PostId.class)
public class Post {
@Id
@Enumerated(EnumType.STRING) // 주의 !!
@Column(name = "category_code")
private CategoryCode categoryCode;
@Id
@Enumerated(EnumType.STRING) //주의 !!
@Column(name="some_code")
private SomeCode someCode;
@Enumerated(EnumType.STRING)
@Column(name = "expenditure_code")
private ExpenditureCode expenditureCode;
@Column(name="title")
private String title;
@Column(name="content")
private String content;
@Column(name = "created_at")
private LocalDateTime createdAt;
//..
엔티티 해당 필드에 @Enumerated(EnumType.String)을 설정해주지 않으면, Enum 클래스 필드에 선언한 순서대로 숫자값이 들어간다는 것을 깜빡했습니다.
그렇다면 이제 다시 테스트 코드를 실행해볼까요
두둔??
복합키로 설정한 SomeCode과 CategoryCode에는 Enum 선언 순서값으로 생성되어 아직도 문자열 설정이 되지 않았습니다.
또한 일반 필드인 ExpenditurCode는 Enuml 필드의 name으로 SQL이 생성되고 있습니다.
이게 어찌된 일일까요 ~
꼬꼬무가 되어서 Enum 시리즈를 끝낼 수가 없네요 ~ ㅠ
이 문제는 다음 게시물에서 알아볼게요 ~!
'백엔드 > Java' 카테고리의 다른 글
천사와 악마 사이 추상클래스... - 인터페이스 및 정적 유틸리티 클래스 비교 - 2 (5) | 2024.02.13 |
---|---|
천사와 악마 사이 추상클래스... - 인터페이스 및 정적 유틸리티 클래스 비교 - 1 (0) | 2024.02.01 |
[이넘아 놀자] 4. @Converter 사용하기 (0) | 2023.09.04 |
[이넘아 놀자] 2. 일단 만들까 이넘 (0) | 2023.08.28 |
[이넘아 놀자] 1. 기존 하드코딩되어 있던 코드 문제점 분석 (복합키+하드코딩) (0) | 2023.08.27 |