QueryDSL을 사용할 경우에 종종 두 가지 엔티티를 조인해서 특정 컬럼만을 반환하거나, 한가지 엔티티에서 특정 컬럼만을 반환해야 할 경우가 생길 수 있습니다.
이 때 사용하는 것이 @QueryProjection
입니다.
Friend에 대한 기능을 만들던 중에 꽤 복잡한 로직이 필요하다는 것을 깨달았습니다.
제가 만든 friend는 간략하게 다음과 같은 형태를 띠고 있습니다.
- friend_id
- requester_id
- respondent_id
- friend_status : Y, N, IN_PROGRESS
이 형태에서 친구 목록을 불러오기 위해서는 다음과 같은 조건이 필요합니다.
로그인한 유저의 id가 requester_id일때는 respondent_id를, 반대일 경우에는 requester_id를 친구라고 판단해야 합니다.
단순히 Friend 엔티티를 통째로 조회해서 service에서 위의 로직을 넣을 수도 있지만, 도메인 비즈니스라고 생각이 되었습니다.
한 번 시도했다가 서비스 비즈니스가 아닌 것 같아서 수정했는데.. 사실 아직도 잘 모르겠습니다.
아무튼 도메인의 책임이라고 생각하고, 쿼리에서 처리하기로 했습니다.
이를 위해서는 일단 Repository에서 Friend를 통째로 넘기면 안되고, 쿼리에서 위의 로직을 넣어주어야 합니다.
즉, Friend라는 한 엔티티에서 특정 컬럼만을 select해주어야 합니다.
Tuple을 사용해서 번거롭게 VO를 생성하지 않고 넘겨주는 방법도 있습니다. 하지만 이는 서비스 비즈니스가 도메인을 의존해야 하기 때문에 책임에도 어긋나고, 가독성이나 유지보수에도 좋지 않기 때문에 VO를 사용하는 것이 바람직합니다.
@Getter
public class FriendFindResultVO {
private final Long friendId;
private final Member friend;
@QueryProjection
public FriendFindResultVO(Long friendId, Member friend) {
this.friendId = friendId;
this.friend = friend;
}
}
@QueryProjection
을 생성자로 만들어서 QueryFactory를 통해 쿼리가 작동합니다. 이 VO는 엔티티 대신 반환할 수 있습니다.
단, VO도 QClass를 생성해야 하므로 재빌드가 필요합니다.
@Override
public List<FriendFindResultVO> findAllFriends(Long memberId) {
return queryFactory
.select(new QFriendFindResultVO(
friend.id,
new CaseBuilder()
.when(friend.requester.id.eq(memberId)).then(friend.respondent)
.otherwise(friend.requester)
))
.from(friend)
.where(requesterIdEq(memberId).or(respondentIdEq(memberId))
,friendStatusEq(FriendStatus.Y))
.fetch();
}
CaseBuilder가 끼게 되면서 조금 더 복잡한 형태의 쿼리가 완성되었는데요, 이러한 특수한 형태가 아니라면 단순히 join이나 fetchjoin으로 좀 더 간단한 쿼리를 만들 수 있을 것 같습니다.