1. 들어가며
프로젝트를 운영하면서 코드 구조와 리팩토링에 대해 늘 고민하게 됩니다.
이번 포스팅에서는 제가 작업 중인
"Mori" 프로젝트의 Enum 클래스 변환 메서드를 점진적으로 개선한 경험을 공유하려고 합니다.
서비스에서 Enum 변환은 정말 자주 등장하는 패턴입니다.
"Mori" 프로젝트는 어린이를 위한 chatGPT 서비스인데,
어린이의 연령대에 맞게 답변을 내려줄 필요가 있어서 아래처럼 of() 메서드를 만들어서 쓰고 있었습니다.
@Getter
@AllArgsConstructor
public enum ChildAgeGroup {
TODDLER("0 ~ 3세"),
KINDERGARTEN("4 ~ 6세"),
LOWER_GRADES("7 ~ 9세"),
UPPER_GRADES("10 ~ 12세"),
MIDDLE_SCHOOL("13세 이상"),
;
private final String description;
// of() 메서드를 통해 String 을 enum 타입으로 변환
public static ChildAgeGroup of(String str) {
return switch (str) {
case "TODDLER" -> TODDLER;
case "KINDERGARTEN" -> KINDERGARTEN;
case "LOWER_GRADES" -> LOWER_GRADES;
case "UPPER_GRADES" -> UPPER_GRADES;
case "MIDDLE_SCHOOL" -> MIDDLE_SCHOOL;
default -> null;
};
}
}
이 접근법에는 큰 단점이 있습니다.
바로 null을 반환하므로 사용하는 쪽에서 항상 null 체크를 해줘야 한다는 점이죠.
아래 코드 처럼 말이죠!
// UserService 내에서 ChildAgeGroup.of()를 호출할 때
ChildAgeGroup childAgeGroup = ChildAgeGroup.of(userRequest.childAgeGroup());
if (childAgeGroup == null) {
throw new MoriException(ErrorCode.UNSUPPORTED_CHILD_AGE_GROUP);
}
2. 리팩토링 목표: Optional로 안전하게
Java 8 이후엔 Optional을 활용해
"존재할 수도, 없을 수도 있는 값"을 명확하게 표현할 수 있습니다.
한마디로 null-safety 해진다는 겁니다.
그런데 이미 of()를 여기저기서 쓰고 있다면, 한 번에 리턴 타입을 바꾸는 게 맞을까요?
마틴 파울러의 리팩토링 철학에 따르면,
한 번에 확 바꾸는 것이 아니라 점진적이고 안전하게 변경하는 것이 중요합니다.
2 - 1. 새로운 메서드 추가
기존 메서드는 그대로 두고,
Optional을 반환하는 새로운 메서드 ofOptional 를 추가합니다.
public static Optional<ChildAgeGroup> ofOptional(String str) {
return Optional.ofNullable(switch (str) {
case "TODDLER" -> TODDLER;
case "KINDERGARTEN" -> KINDERGARTEN;
case "LOWER_GRADES" -> LOWER_GRADES;
case "UPPER_GRADES" -> UPPER_GRADES;
case "MIDDLE_SCHOOL" -> MIDDLE_SCHOOL;
default -> null;
});
}
2 - 2. 새 메서드 사용으로 점진적 마이그레이션
새로운 코드나 수정하는 코드에서는 ofOptional() 사용합니다.
기존 코드는 그대로 of() 를 사용합니다.
2 - 3. 기존 메서드 Deprecated 처리
/**
* @deprecated 이 메서드는 향후 버전에서 제거될 예정입니다.
* {@link #ofOptional(String)} 사용을 권장합니다.
*/
@Deprecated
public static ChildAgeGroup of(String str) {
return switch (str) {
case "TODDLER" -> TODDLER;
case "KINDERGARTEN" -> KINDERGARTEN;
case "LOWER_GRADES" -> LOWER_GRADES;
case "UPPER_GRADES" -> UPPER_GRADES;
case "MIDDLE_SCHOOL" -> MIDDLE_SCHOOL;
default -> null;
};
}
public static Optional<ChildAgeGroup> ofOptional(String str) {
return Optional.ofNullable(switch (str) {
case "TODDLER" -> TODDLER;
case "KINDERGARTEN" -> KINDERGARTEN;
case "LOWER_GRADES" -> LOWER_GRADES;
case "UPPER_GRADES" -> UPPER_GRADES;
case "MIDDLE_SCHOOL" -> MIDDLE_SCHOOL;
default -> null;
});
}
of() 에 @Deprecated 어노테이션을 붙여 놓음으로써 사용자에게 경고를 해줍니다.
2 - 4. 기존 코드를 모두 새 API로 전환 후, 최종 정리
충분한 시간이 지나고 모든 호출 코드가 변경된 후 기존 메서드를 삭제합니다.
필요하다면 기존 메서드 명(of)를 유지하고 싶다면, ofOptional -> of 로 변경합니다.
// ofOptional -> of 로 메서드명 변경
public static Optional<ChildAgeGroup> of(String str) {
return Optional.ofNullable(switch (str) {
case "TODDLER" -> TODDLER;
case "KINDERGARTEN" -> KINDERGARTEN;
case "LOWER_GRADES" -> LOWER_GRADES;
case "UPPER_GRADES" -> UPPER_GRADES;
case "MIDDLE_SCHOOL" -> MIDDLE_SCHOOL;
default -> null;
});
}
3. 마무리
서비스가 성장할수록 코드 구조와 작은 API 설계의 고민이 더욱 필요해지는 것 같습니다.
이번 경험을 기반으로 앞으로도 테스트와 리팩토링에 강한 백엔드 코드를 만들어가고 싶습니다.
여러분은 코드를 어떻게 관리하고 계신가요?
더 좋은 패터이나 경험이 있다면 공유해주세요.
'리팩토링' 카테고리의 다른 글
리팩터링 2판 - Java로 정리 - (3) statement() 함수 쪼개기 (0) | 2024.06.07 |
---|---|
리팩터링 2판 - Java로 정리 - (2) 테스트 코드 (0) | 2024.06.06 |
리팩터링 2판 - Java로 정리 (0) | 2024.06.05 |