
프로젝트에서 게시글에 선호 나이대를 설정하는 부분이 있다. Board Entity에 preferAgeRange를 필드로 갖고자 했다. 처음에는 그냥 Enum으로 "TWENTIES", "THIRTIES" 값으로 하고자 했지만, 팀원들과 추가로 의논해보니 사용자가 여러 나이대를 선호하게 되는 경우가 가능할 것이라 결론이 지어졌다.
(ex: 20대, 30대를 모두 선호한다던가..)
그럼 결국 Enum을 컬렉션으로 저장하는 형태가 되어버리는데.. 전에 이거랑 비슷한 고민을 한 적이 있었다. (여행 스타일 저장 형태 고민하기)
그때는 결론적으로 유저 고유 아이디를 식별 관계로 두어 travel_style의 PK가 되도록 만들고, 각 스타일에 대해 flag 필드로 저장했다.
선호 나이대는 게시글의 일부이고 그리 중요도가 높은 필드도 아닌데 Entity까지 만들어야 할까..? 라는 생각에 어떻게든 Entity로 빼지 않는 방법을 찾고자 했다.
그렇게 발견한 개념이 값 타입 컬렉션!
말 그대로 값 타입을 컬렉션에 담아서 사용하는 것을 의미한다. 값 타입을 여러 개 저장해야 할 때 사용한다. 즉, 일대다(1:N) 관계를 표현하는 것이다.
Entity에 @ElementCollection 으로 필드를 저장하면 컬렉션을 저장하기 위해 해당 이름으로 DB에서는 새로운 테이블이 생성된다.
@ElementCollection
@Enumerated(EnumType.STRING)
private List<Age> preferAgeRange = new ArrayList<>();
실행했을 때도 prefer_age_range 이름의 테이블이 잘 생성됐다. 해당 테이블에는 board_id, prefer_age_range를 필드로 갖고 있었고 이 둘을 조합해 복합 기본키로 구성되었다. 값 타입 컬렉션 관리를 JPA에게 위임하니 굉장히 편했다.
사용하기에는 굉장히 편했는데 값 타입 컬렉션에 대해서는 단점이 여러가지 존재했다.
- 교체하고 싶은 객체가 있으면 해당 객체를 삭제하고 새로운 내용의 객체를 삽입하는 방식이다. (Dirty Checking이 불가능하다.)
- 값 타입에는 식별자가 없기에 변경 값이 추적 불가능하기 때문이다.
- 값타입 컬렉션 또한 1:N 관계이기에 JPQL로 DTO 변환 할 때 직접적
대입이 불가능하다.
내 프로젝트에 영향을 끼친 단점은 위 두 가지였는데,
- preferAgeRange 필드 값은 게시글 수정에 따라 내용이 변동될 수 있다.
- preferAgeRange가 속한 Board Entity를 JPQL로 조회할 때 DTO 변환을 해야한다.
DTO에 필요한 값이 많아서 복잡하긴 하지만.. QueryDSL을 사용한 조회 쿼리는 아래와 같았다.
@Query("SELECT distinct new com.tripbros.server.board.dto.GetBoardResponseDTO("
+ "b.id, b.user.nickname, b.user.profileImage, b.hit, "
+ "CASE WHEN bookmark.id IS NULL OR :userId IS NULL THEN false ELSE true END, "
+ "b.user.age, b.user.sex, b.title, b.content, b.purpose, b.schedule.locate.country, b.schedule.locate.city, "
+ "b.bookmarkedCount, b.preferSex, b.schedule.startDate, b.schedule.endDate, b.requiredHeadCount, "
+ "b.nowHeadCount, b.schedule.placeId, b.schedule.placeName, b.schedule.placeUrl, b.chatCount, b.createdAt, b.preferAgeRange) "
+ "FROM Board b "
+ "LEFT OUTER JOIN FETCH BookmarkedBoard bookmark "
+ "ON (bookmark.board.id = b.id AND bookmark.user.id = :userId) ")
List<GetBoardResponseDTO> findAllGetDTO(@Param("userId") Long userId);
Board의 값들을 GetBoardResponseDTO에 바로 매핑하여 결과를 반환하고자 해서 위같이 구성했다. 처음에는 1:N 대입이 안되는 줄도 모르고 그냥 preferAgeRange도 결국 Board Entity에 속한 객체로서 저렇게 삽입해도 될 줄 알았다.
계속 쿼리 오류가 발생해서 찾아보니.. 저런 점들이 문제였다.
쿼리에 대입이 안되는 것도 문제지만, 무엇보다 Dirty Checking이 안되는 것도 성능 손해라고 생각해서 결국 @ElementCollection 사용은 포기했다.
많은 사람들이 이미 권장했듯이, 값 타입을 collection으로 사용했을 때의 역할이 확실할 때만 사용하고 나머지는 Entity로 정의해서 사용하기를 권장하고 있다.
그래서 preferAgeRange는 Entity로 따로 정의해서 사용하게 만들었다.
@Getter
@Entity
@NoArgsConstructor
public class PreferAgeRange {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private boolean twentiesFlag;
private boolean thirtiesFlag;
private boolean fortiesFlag;
private boolean fiftiesFlag;
private boolean sixtiesAboveFlag;
private boolean unrelatedFlag;
...
}
preferAgeRange를 각 나이대에 대해 선호하는지 아닌지 flag 필드로 만들었기에 하나의 게시글에는 하나의 선호 나이대 테이블이 대응하므로 일대일(1:1) 관계로 만들었다.
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "prefer_age_range_id")
private PreferAgeRange preferAgeRange;
위처럼 수정하니 앞서 JPQL 조회 쿼리도 정상 작동하고 선호 나이대 수정에 있어서도 Dirty Checking이 가능해졌다.