🧶 '프로그래머스 데브코스 4기 백엔드'에서 진행한 <네이버 웹툰 클론코딩> 팀 프로젝트 기록입니다.
부제: 이용 연령 등급 Enum으로 개선하기
이전 포스팅에서 웹툰 '이용 연령 등급' 데이터를 enum 클래스로 관리하게 된 과정을 이야기했다. 그리고 그렇게 처음 만든 enum 클래스는 아래와 같았다.
@Getter
public enum AgeRating {
ALL(0), // 전체연령가
OVER_12(12), // 12세이용가
OVER_15(15), // 15세이용가
OVER_18(18); // 18세이용가
private final int ageLimit;
AgeRating(int ageLimit) {
this.ageLimit = ageLimit;
}
}
이번 포스팅에서는 JPA가 enum 타입인 엔티티 속성을 데이터베이스에 저장하는 방법에 대해 쓰려고 한다.
JPA가 기본적으로 enum을 다루는 방법
Request JSON
Request DTO
public record WebtoonCreateRequest(
// 다른 속성
AgeRating ageRating
) {}
Controller
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ApiResponse<WebtoonResponse> createWebtoon(@RequestBody WebtoonCreateRequest request) {
// CreateWebtoon 서비스 메서드 호출
}
enum은 기본적으로 순서(숫자)로 저장된다.
엔티티 내 속성 부분이 아래와 같이 되어 있을 때 Webtoon 데이터는 어떻게 저장될까?
@Column(name = "age_rating", nullable = false)
private AgeRating ageRating;
이렇게 숫자로 저장된다!
이것은 enum 타입의 속성에 별도의 설정을 해주지 않으면 기본적으로 @Enumerated(EnumType.ORDINAL)
이 적용되기 때문이다. 이는 enum '순서'를 데이터베이스에 저장한다는 뜻이다. 즉, ALL
은 0, OVER_12
는 1, OVER_15
는 2, OVER_18
은 3이 저장된다.
EnumType.ORDINAL
은 데이터베이스에 저장되는 데이터 크기가 작다는 장점이 있지만, 이미 저장된 enum의 순서를 변경할 수 없다는 단점도 있다.
enum을 이름으로 저장하고 싶다면?
@Column(name = "age_rating", nullable = false)
@Enumerated(EnumType.STRING)
private AgeRating ageRating;
@Enumerated(EnumType.STRING)
설정을 주면 된다.
이는 데이터베이스에 enum의 이름, 즉 문자로 저장한다는 뜻이다. 이는 EnumType.ORDINAL
보다는 데이터베이스에 저장되는 데이터 크기가 커진다는 단점이 있지만, 저장된 enum의 순서가 바뀌거나 enum이 추가되어도 안전하며 유지보수도 용이해진다.
enum 이름으로 저장하는 것도 싫다면 Converter를 이용하자!
ALL
, OVER_12
, OVER_15
, OVER_18
이 코드 가독성 측면에 좋지만, 또 아쉬운 점이 있었다.
요청을 받거나 데이터베이스에 저장할 때 enum의 이름보단 현실 세계의 등급 이름 그대로, '전체 연령가', '12세 이용가', '15세 이용가', '18세 이용가'로 받아 저장하고 싶었던 것이다.
AgeRating Ver.2
그래서 우선 기존의 enum을 변경했다.
@Getter
public enum AgeRating {
ALL("전체 연령가", 0),
OVER_12("12세 이용가", 12),
OVER_15("15세 이용가", 15),
OVER_18("18세 이용가", 18);
private final String ratingName;
private final int ageLimit;
AgeRating(String ratingName, int ageLimit) {
this.ratingName = ratingName;
this.ageLimit = ageLimit;
}
// ratingName 값과 enum을 매핑해주는 메서드
public static AgeRating from(String ratingName) {
return Arrays.stream(AgeRating.values())
.filter(v -> v.getRatingName().equals(ratingName))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(String.format("%s와 일치하는 연령등급이 없습니다.", ratingName)));
}
}
각 enum마다 ratingName
이라는 등급 이름 값을 지정했다. 그리고 이 ratingName
값과 enum을 매핑하는 from
메서드를 만들었다.
Converter
그리고 JPA가 데이터베이스에 ageRating
값을 저장할 때 사용할 컨버터Converter를 만들었다. 컨버터는 AttributeConverter
인터페이스를 구현하면 된다.
@Converter
public class AgeRatingConverter implements AttributeConverter<AgeRating, String> {
// Entity Attribute -> DB column
@Override
public String convertToDatabaseColumn(AgeRating attribute) {
if (attribute == null) return null;
return attribute.getRatingName();
}
// DB column -> Enitity Attribute
@Override
public AgeRating convertToEntityAttribute(String dbData) {
if (dbData == null) return null;
return AgeRating.from(dbData);
}
}
AttributeConverter 인터페이스
"이 인터페이스를 구현한 클래스는 엔티티 속성 상태를 데이터베이스 column 표현으로 변환하거나 다시 되돌릴 수 있다."라고 Javadoc이 아주 친절하게 설명해준다.
그리고 엔티티 속성에 내가 만든 컨버터를 이용할 것이라고 annotation으로 지정해 주어야 한다.
@Column(name = "age_rating", nullable = false)
@Convert(converter = AgeRatingConverter.class)
private AgeRating ageRating;
이후엔 위와 같은 요청 조건에서 데이터베이스에 원하는 값이 들어가는 것을 확인할 수 있다!
결과
데이터베이스에만 '전체 연령가'로 저장할 뿐만 아니라, 요청도 '전체 연령가'로 받고 싶었기 때문에 요청 DTO를 엔티티로 변환할 때 enum 클래스 안에 만들어 둔 AgeRating.from()
메서드로 매핑했다. 또한 DTO에서 ageRating
타입을 String 타입으로 바꿨다.
마찬가지로 응답도 '전체 연령가'로 보내고 싶었기 때문에, 엔티티를 응답 DTO로 변환할 때는 ageRating.getRatingName
을 이용해서 매핑했다
📗 Reference
https://techblog.woowahan.com/2600/
'나의 개발일기' 카테고리의 다른 글
[Java/Spring] SpringSecurity(6.x 버전 이상)를 사용하면서 h2-console에 접속하기 (0) | 2023.10.28 |
---|---|
[Java/Spring] enum으로 코드를 개선해보자(1) : enum을 적용하게 된 과정 (0) | 2023.09.26 |
[Java/Spring] Controller 테스트의 “JPA metamodel must not be empty!” (0) | 2023.09.25 |