Entity 필드가 가지는 Enum값의 목록은 어떻게 가져와야 할까?
kindof
·2022. 5. 20. 13:51
1. 문제
엔티티가 가지는 필드의 값을 특정한 값들로 고정하고 싶을 때 Enum 타입을 사용할 수 있습니다.
Enum 타입은 정해진 값들을 하나의 클래스에 모아서 관리함으로써 관리가 편하고, 클라이언트는 서버에서 관리되고 있는 Enum 타입만을 사용할 수 있도록 환경이 구성된다면(Select Box, Category 등) 잘못된 데이터를 요청하거나 전달받는 일이 사라집니다.
그런데 Enum이 가져다주는 이점이 있는 만큼, Enum은 신경써서(고민해서) 사용해야 하는 부분도 존재해야 하는데요.
이번 글에서는 이러한 포인트 중 하나인 "Entity 필드가 가지는 Enum값의 목록은 어떻게 가져와야 할까?" 라는 문제에 대해 정리해보려고 합니다.
코드를 보면서 구체적으로 설명을 해보겠습니다.
[Community.java]
@Entity @Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Community extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "COMMUNITY_ID")
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String description;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MEMBER_ID")
private Member member;
@Convert(converter = CommunityCategoryConverter.class)
private CommunityCategory communityCategory; // <-- Enum 타입 사용
...
}
Community 라는 엔티티는 CommunityCategory 라는 필드를 가지고 있습니다. 관리자는 게시물들이 속할 수 있는 카테고리를 미리 설정해두고, 게시물들은 각각 어떤 카테고리에 속해야 한다는 설계입니다.
그리고 카테고리는 아래와 같이 Enum 클래스로 작성했습니다.
[CommunityCategory.java]
@Getter
public enum CommunityCategory {
QUESTION("동네질문"),
FOOD("동네맛집"),
ANNOUNCE("동네알림"),
FIND("분실/실종센터"),
INVEST("투자/주식"),
LOVE("연애"),
CAREER("진로/직장"),
HEALTH("헬스/건강"),
HOBBY("취미");
private final String value;
CommunityCategory(String value) {
this.value = value;
}
public static CommunityCategory fromCode(String dbData) {
return Arrays.stream(CommunityCategory.values())
.filter(v -> v.getValue().equals(dbData))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(String.format("커뮤니티 카테고리에 %s가 존재하지 않습니다.", dbData)));
}
}
몇 가지 카테고리가 (Key, Value) 형태로 관리되고 있으며, fromCode()는 다른 부분에서 Converting을 위해 사용되는 코드입니다.
이 때, 제가 하고 싶은 일은 아래 사이트처럼 각 게시물의 카테고리를 나열해두고, 해당 카테고리를 클릭하면 이에 해당하는 게시물들만 가져오는 것이었는데요.
단순하게 생각하면, 클라이언트에서 서버의 Enum 값에 해당하는 값들을 직접 작성해서 화면에 보여주면 문제는 해결됩니다.
하지만 그렇게 하면 아래와 같은 문제가 생깁니다.
- 클라이언트 코드가 지저분해진다.
- 서버에서 Enum 타입의 변경이 있을 때 클라이언트 코드도 일일이 수정해야 한다.
- 만약 클라이언트와 서버의 타입 동기화가 이루어지지 않으면, 에러가 발생한다.
따라서, 이 문제를 해결하기 위해서 클라이언트는 서버의 Enum 값들(CommunityCategory)의 리스트를 요청해서 받아와서 사용해야만 합니다. 어떻게 코드를 짜야 할까요?
2. Enum 값 리스트 가져오기
먼저 CommunityCategory 클래스에 아래와 같은 DTO를 정적으로 생성해주었습니다.
@Getter
public enum CommunityCategory {
QUESTION("동네질문"),
FOOD("동네맛집"),
...
...
@Data
public static class CommunityCategoryResponse{
private String key;
private String value;
public CommunityCategoryResponse(String key, CommunityCategory communityCategory){
this.key = key;
this.value = communityCategory.getValue();
}
}
}
그리고 CommunityController 클래스에 아래와 같은 메서드를 추가했습니다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/communities")
public class CommunityController {
private final CommunityService communityService;
private static final int PAGE_DEFAULT_SIZE = 10;
// 카테고리 리스트 조회
@GetMapping("/categories")
public List<CommunityCategory.CommunityCategoryResponse> categoryList(){
Class c = CommunityCategory.class;
Object[] keys = c.getEnumConstants();
return Arrays.stream(keys).map((key)
-> new CommunityCategory.CommunityCategoryResponse(
key.toString(), CommunityCategory.valueOf(key.toString())))
.collect(Collectors.toList());
}
...
}
getEnumConstants()는 Enum 클래스가 가지고 있는 키값들을 리턴해주는 함수이며, 아래 링크에서 자세한 사용 문법을 찾을 수 있습니다.
어쨌든, getEnumConstants() 메서드로 키값들을 배열에 담고, Java8 스트림을 통해 DTO로 만든 생성자 형식에 맞춰 카테고리 리스트들을 응답으로 생성합니다.
그리고 아래와 같이 포스트맨을 통해 요청을 보내보면 정상적으로 카테고리에 속한 Key, Value 값들을 뽑아올 수 있었습니다.
원하는 결과는 얻을 수 있지만, 이러한 방식의 구현에서는 몇 가지 문제를 생각해볼 수 있습니다.
3. 발전시키기 - 인터페이스와 DTO 설계
Enum 필드는 Community 엔티티 외에 다른 엔티티에도 존재할 수 있습니다. 예를 들어, 제품(Product)에도 제품의 카테고리가 존재할 수 있죠.
따라서, Enum 클래스가 가지는 공통 메서드는 인터페이스를 생성해서 정의하고 DTO도 클래스 내에 정적으로 선언하는 것이 아니라, 별개의 클래스를 만들어서 관리하는 것이 낫다고 생각합니다.
그래서 아래와 같이 인터페이스를 정의하고, 그 안에 메서드를 선언하겠습니다.
[EnumManager.java]
public interface EnumManager {
String getKey();
String getValue();
}
그리고 CommunityCategory를 아래와 같이 수정하겠습니다. @Getter 대신에 메서드를 오버라이딩해주고(그래야 Interface에서 정의한 메서드를 사용할 수 있습니다), 해당 클래스에만 종속적인 DTO를 지웠습니다.
[CommunityCategory.java]
public enum CommunityCategory implements EnumManager {
QUESTION("동네질문"),
FOOD("동네맛집"),
...
private final String value;
CommunityCategory(String value) {
this.value = value;
}
...
@Override
public String getKey() {
return name();
}
@Override
public String getValue() {
return value;
}
}
그리고 이제 모든 Enum 타입의 리스트를 응답할 수 있는 DTO를 따로 생성합니다.
[EnumDTO.java]
@Data
public class EnumDTO {
private String key;
private String value;
public EnumDTO(EnumManager e){
this.key = e.getKey();
this.value = e.getValue();
}
}
EnumDTO는 EnumManager 타입의 객체를 매개변수로 받아서 응답을 생성합니다. 그러면 CommunityCategory 뿐만 아니라, EnumManager를 implements하고 있는 모든 Enum 타입들을 파라미터로 받을 수 있게 됩니다.
마지막으로 CommunityController도 아래와 같이 수정하겠습니다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/communities")
public class CommunityController {
private final CommunityService communityService;
private static final int PAGE_DEFAULT_SIZE = 10;
// 카테고리 리스트 조회
@GetMapping("/categories")
public List<EnumDTO> categoryList(){
Class<? extends EnumManager> e = CommunityCategory.class;
return Arrays
.stream(e.getEnumConstants())
.map(EnumDTO::new)
.collect(Collectors.toList());
}
...
}
이렇게 해서 최종적으로 Enum 필드의 값 목록을 조금 더 확장성 있게 가져올 수 있게 되었습니다.
이번 글을 작성할 때 처음에는 단순히 Enum 리스트를 가져오는 방법이 궁금했는데, 아래 이동욱님의 블로그 글을 보면서 어떻게 하면 다형성을 살리면서 확장 가능한 코드를 짤 수 있는지에 대해 배울 수 있었습니다.
여기 블로그(Enum 활용 & Enum 리스트 가져오기)에서는 제가 설명한 것보다 조금 더 자세하고 많은 내용을 끝까지 다루니 참고하시면 좋을 것 같습니다.
감사합니다.
'Spring & Springboot' 카테고리의 다른 글
Spring AOP 스터디 - (1) AOP의 필요성과 기본적인 동작 원리 (0) | 2022.10.18 |
---|---|
Spring Security - SecurityContextHolder에서 로그인 사용자 정보 가져오기 (1) | 2022.08.03 |
리액트 / 스프링 데이터 JPA 환경에서 커서를 통한 페이지네이션(Pagination) 구현하기 (0) | 2022.05.18 |
@ControllerAdvice로 Validation 예외 처리하기 (1) | 2022.03.27 |
Springboot와 JWT를 이용한 권한(Authorization) 처리 (1) | 2022.02.15 |