DevLog ๐ถ
[QueryDSL] QueryDSL ์ฌ์ฉ ์ NPE๊ฐ ๋ฐ์ํ์ ๋ ํด๊ฒฐํ๊ธฐ ๋ณธ๋ฌธ
[QueryDSL] QueryDSL ์ฌ์ฉ ์ NPE๊ฐ ๋ฐ์ํ์ ๋ ํด๊ฒฐํ๊ธฐ
dolmeng2 2023. 8. 14. 14:01๐ฑ ๋ฌธ์ ์ํฉ
ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ ๋์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ผ์ด ๋ง์ QueryDSL์ ๋์ ํ๊ฒ ๋์๋๋ฐ, ์ด๋ฒ์ ์๋ค๋ฅธ ์ค๋ฅ๋ฅผ ๋ฐ๊ฒฌํ๊ฒ ๋์๋ค.
java.lang.NullPointerException: Cannot read field "id" because "co.kirikiri.domain.goalroom.QGoalRoomMember.goalRoomMember.goalRoom.roadmapContent.roadmap" is null
NPE๋ฅผ ๋ณธ ๊ฑด ์ค๋๋ง์ด์ด์ ์กฐ๊ธ ์ฝ์ง์ ์งํํ๋ค.
๐ฑ ๋๋ฉ์ธ ๊ตฌ์กฐ
์ง๋ ํฌ์คํ ๊ณผ ๊ฐ์ ํ๋ก์ ํธ์ด๊ธฐ ๋๋ฌธ์ ๋๋ฉ์ธ ๊ตฌ์กฐ๋ ๊ฑฐ์ ๋์ผํ๋ฐ, ์ ๋ฌธ์ ์ํฉ์ ์ดํดํ๊ธฐ ์ํ ์ถ๊ฐ์ ์ธ ๋๋ฉ์ธ ์ ๋ณด๊ฐ ํ์ํ๋ค.
ํ๋์ ๋ก๋๋งต ๋ณธ๋ฌธ(RoadmapContent) ์ ๋ณด์ ๋ํด์ ์ฌ๋ฌ ๊ฐ์ ๊ณจ๋ฃธ(GoalRoom)์ด ์์ฑ๋ ์ ์๊ณ , ํ๋์ ๊ณจ๋ฃธ์ ๋ํด์ ์ฌ๋ฌ ๊ฐ์ ๊ณจ๋ฃธ ๋๊ธฐ ๋ฉค๋ฒ(GoalRoomPendingMembers)๊ฐ ์๊ธธ ์ ์๋ค๋ ๊ฒ์ด๋ค.
์ ๋ฌธ์ ๋ฅผ ๋ฐ๊ฒฌํ๊ฒ ๋ ํ ์คํธ ์ฝ๋๋ ์๋์ ๊ฐ๋ค.
@Test
void ์นดํ
๊ณ ๋ฆฌ_์กฐ๊ฑด_์์ด_์ฃผ์ด์ง_๋ก๋๋งต_์ด์ ์_๋ฐ์ดํฐ๋ฅผ_์ฐธ๊ฐ์ค์ธ_์ธ์์ด_๋ง์์์ผ๋ก_์กฐํํ๋ค() {
...
final Roadmap gameRoadmap1 = ๋
ธ๋_์ ๋ณด๋ฅผ_ํฌํจํ_๋ก๋๋งต์_์์ฑํ๋ค("๊ฒ์ ๋ก๋๋งต", creator, gameCategory);
final Roadmap gameRoadmap2 = ๋
ธ๋_์ ๋ณด๋ฅผ_ํฌํจํ_๋ก๋๋งต์_์์ฑํ๋ค("๊ฒ์ ๋ก๋๋งต2", creator, gameCategory);
final Roadmap travelRoadmap = ๋
ธ๋_์ ๋ณด๋ฅผ_ํฌํจํ_๋ก๋๋งต์_์์ฑํ๋ค("์ฌํ ๋ก๋๋งต", creator, travelCategory);
final GoalRoom gameRoadmap1GoalRoom = ๊ณจ๋ฃธ์_์์ฑํ๋ค(gameRoadmap1.getContents().getValues().get(0), creator);
final GoalRoom gameRoadmap2GoalRoom = ๊ณจ๋ฃธ์_์์ฑํ๋ค(gameRoadmap2.getContents().getValues().get(0), creator);
final GoalRoom travelRoadmapGoalRoom = ๊ณจ๋ฃธ์_์์ฑํ๋ค(travelRoadmap.getContents().getValues().get(0), creator);
// gameRoadmap1 : ์ฐธ๊ฐ์ธ์ 0๋ช
// gameRoadmap2 : ์ฐธ๊ฐ์ธ์ 1๋ช
final List<GoalRoomMember> gameRoadmap2GoalRoomMembers = List.of(
new GoalRoomMember(GoalRoomRole.LEADER, LocalDateTime.now(), gameRoadmap2GoalRoom, creator));
goalRoomMemberRepository.saveAll(gameRoadmap2GoalRoomMembers);
// travelRoadmap : ์ฐธ๊ฐ์ธ์ 2๋ช
final List<GoalRoomMember> travelRoadmapGoalRoomMembers = List.of(
new GoalRoomMember(GoalRoomRole.LEADER, LocalDateTime.now(), travelRoadmapGoalRoom, creator),
new GoalRoomMember(GoalRoomRole.FOLLOWER, LocalDateTime.now(), travelRoadmapGoalRoom, follower));
goalRoomMemberRepository.saveAll(travelRoadmapGoalRoomMembers);
final RoadmapCategory category = null;
final RoadmapFilterType orderType = RoadmapFilterType.PARTICIPANT_COUNT;
// when
final List<Roadmap> firstRoadmapRequest = roadmapRepository.findRoadmapsByCategory(category, orderType,
null, 2);
final List<Roadmap> secondRoadmapRequest = roadmapRepository.findRoadmapsByCategory(category, orderType,
gameRoadmap2.getId(), 10);
...
}
์ฝ๋๊ฐ ์๋นํ ๋ณต์กํ์ง๋ง ๋ณ๊ฑฐ์๋ค. ๊ทธ๋ฅ ๋ก๋๋งต์ ์์ฑํ๊ณ , ๋ก๋๋งต์ ๋ํ ์ฐธ๊ฐ์ธ์์ ์ถ๊ฐํ๋ ์ฝ๋์ด๋ค.
ํด๋น ์ฝ๋๋ ์๋์ ๊ฐ์ด ๊ตฌํ์ด ๋์ด ์๋ค.
@Override
public List<Roadmap> findRoadmapsByCategory(final RoadmapCategory category, final RoadmapFilterType orderType,
final Long lastId, final int pageSize) {
return selectFrom(roadmap)
...
.where(
lessThanLastId(lastId, orderType)
)
.fetch();
}
private BooleanExpression lessThanLastId(final Long lastId, final RoadmapFilterType orderType) {
...
final NumberPath<Long> goalRoomMemberRoadmapId = goalRoomMember.goalRoom.roadmapContent.roadmap.id;
return participantCountCond(goalRoomMemberRoadmapId.eq(roadmap.id))
.lt(participantCountCond(goalRoomMemberRoadmapId.eq(lastId)));
}
์ธ์๋ก ๋ฐ์ lastId (ํ์ฌ๋ ๋ก๋๋งต์ PK ๊ฐ์ด ๋ค์ด์ค๊ณ ์๋ค.)์ ํด๋นํ๋ ๋ก๋๋งต์ ์ฐธ์ฌ์ ์๋ณด๋ค ๋ ์ ์ ์ฐธ์ฌ์ ์๋ฅผ ๊ตฌํ๋ ๋ก๋๋งต์ ๊ตฌํ๋ ์ฟผ๋ฆฌ์ด๋ค.
๐ฑ NPE๊ฐ ๋ฐ์ํ ์ง์ ์ ์ฐพ์๋ณด๊ธฐ
์ฒ์์ ์ด ์ค๋ฅ๋ฅผ ์ ํ์ ๋๋ ์๋์ ๊ฐ์ ์์ธ ๋ฉ์์ง ์ค์์ ๋จ์ํ๊ฒ ์ด ๋ถ๋ถ์๋ง ์ง์ค์ ํ์๋ค.
co.kirikiri.domain.goalroom.QGoalRoomMember.goalRoomMember.goalRoom.roadmapContent.roadmap is null.
๊ทธ๋์ goalRoomMember๋ฅผ ์ ์ฅํ ํ์ ๋ก๋๋งต์ ๋ํ ์ ๋ณด๊ฐ ์ ๋๋ก ์ ์ฅ์ด ์ ๋์ด ์๋ ์ถ์ด์ findAll๋ก ์ฐพ์๋ณด์๋ค.
final List<GoalRoomMember> goalRoomMembers = goalRoomMemberRepository.findAll();
assertThat(goalRoomMembers.get(0).getGoalRoom().getRoadmapContent().getRoadmap().getId()).isNotNull();
assertThat(goalRoomMembers.get(1).getGoalRoom().getRoadmapContent().getRoadmap().getId()).isNotNull();
assertThat(goalRoomMembers.get(2).getGoalRoom().getRoadmapContent().getRoadmap().getId()).isNotNull();
NPE๋๊น ๋น์ฐํ roadmap์ id ๊ฐ์ด Null์ด ๋์ด์ ํ ์คํธ๊ฐ ์คํจํ๊ฒ ์ง?๋ผ๊ณ ์๊ฐํ์๋ค.
ํ์ง๋ง ํ ์คํธ๊ฐ ๋๋๊ฒ๋ ์ฑ๊ณตํ ๊ฒ์ ๋ณผ ์ ์์๋ค. ๊ทธ๋์ ๊ฐ ์์ฒด๋ ์ ๋๋ก ๋ค์ด๊ฐ๊ฒ ๊ตฌ๋ ์ถ์ด์ ๋ค๋ฅธ ๋ฐฉํฅ์ ํ์ํด๋ณด์๋ค.
๐ฑ QueryDSL์ ๊ฐ์ฒด ๊ทธ๋ํ
final NumberPath<Long> goalRoomMemberRoadmapId = goalRoomMember.goalRoom.roadmapContent.roadmap.id;
๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ๋ผ์ธ์ ์ ํํ๊ฒ ์ ๋ผ์ธ์ด์๋ค.
์ด๋ ํ ๊ฐ์ด ๋ค์ด๊ฐ๊ณ ๋ง๊ณ ์ ๋ฌธ์ ๋ฅผ ๋ ๋์, ๊ฐ์ฒด ๊ทธ๋ํ๋ฅผ ํ์ํ ๋ ๋ญ๊ฐ ๋ฌธ์ ๊ฐ ์์ ๊ฒ ๊ฐ๋ค๋ ์๊ฐ์ด ๋ค์๋ค.
๊ทธ๋์ ์ด์ฌํ ๊ตฌ๊ธ๋ง์ ํด๋ณธ ๊ฒฐ๊ณผ, ์๋์ ๊ฐ์ ๊ธ์ ๋ฐ๊ฒฌํ๊ฒ ๋์๋ค.
๊ทธ๋ฆฌ๊ณ ๋ต๋ณ ๋ด์ฉ์์ ๋งํด ์ฃผ์ ๊ณต์ ๋ฌธ์๋ฅผ ํ ๋ฒ ์ฝ์ด๋ณด์๋ค.
By default Querydsl initializes only reference properties of the first two levels.
In cases where longer initialization paths are required, these have to be annotated in the domain types via
com.mysema.query.annotations.QueryInit annotations.
QueryInit is used on properties where deep initializations are needed.
๊ธฐ๋ณธ์ ์ผ๋ก QueryDSL์ ์ด๊ธฐ 2๋จ๊ณ์ ๋ ๋ฒจ์ ์๋ ํ๋กํผํฐ๋ง ์ด๊ธฐํํ๊ฒ ๋๋ค.
๋ง์ฝ ๊ทธ ์ด์์ path๋ฅผ ์ด๊ธฐํํ๊ณ ์ถ๋ค๋ฉด @QueryInit ์ด๋ ธํ ์ด์ ์ ํตํด์ ์ง์ ์ง์ ํด์ค ์ ์๋ค.
ํ ๋ฒ ์ฐ๋ฆฌ์ ๋ฌธ์ ์ํฉ์ ์ ์ฉํด๋ณด์.
๐ฑ @QueryInit
@QueryInit์ ๊ฒฝ์ฐ ๊ธฐ๋ณธ์ ์ผ๋ก @Entity ์ด๋ ธํ ์ด์ ์ด ๋ถ์ด ์๋ ํด๋์ค์ ํ๋์ ๋ํด์ ์ ์ฉ์ด ๊ฐ๋ฅํ๋ค.
๋ด๋ถ ์ธ์๋ก ์ด๊ธฐํํ๊ณ ์ถ์ path๋ฅผ ์ง์ ํด์ค ์ ์์ผ๋ฉฐ, *์ ํตํด ์์ผ๋์นด๋๋ฅผ ํตํด์๋ ์ง์ ์ด ๊ฐ๋ฅํ๋ค.
๋จผ์ , @QueryInit์ ์ง์ ํ์ง ์์์ ๋์ ํํ์ผ์ ๋๋ฒ๊น ํด๋ณด์๋ค. (QGoalRoom)
ํ์ธํด๋ณด๋ฉด GoalRoom์์ QRoadmapContent๊น์ง์ ๊ฐ์ ์ ๊ฐ์ ธ์ค์ง๋ง, QRoadmap์ ๋ํด์๋ ์ ์๊ฐ ๋์ด ์์ง ์์ ๊ฒ์ ๋ณผ ์ ์๋ค. ์ฌ๊ธฐ์ QRoadmap ๊ฐ์ด null๋ก ์ ์๋์๊ธฐ ๋๋ฌธ์ ํ์์ด ๋ถ๊ฐ๋ฅํ๋ ๊ฒ์ด๋ค.
public class GoalRoomMember {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "goal_room_id", nullable = false)
@QueryInit(value = {"roadmapContent.roadmap"})
protected GoalRoom goalRoom;
}
๊ทธ๋์, ์์ ๊ฐ์ด GoalRoomMember ์ํฐํฐ๊ฐ GoalRoom ์ํฐํฐ๋ฅผ ์ ์ํ ํ๋์ @QueryInit์ ์ ์ํด์ฃผ์๋ค.
์ด๋ ๊ฒ ๋๋ฉด QGoalRoom์ด ์ด๊ธฐํ๋ ๋ RoadmapContent์ Roadmap ์ ๋ณด๊น์ง ํจ๊ป ์ด๊ธฐํ๊ฐ ๊ฐ๋ฅํ ๊ฒ์ด๋ค.
์ฐธ๊ณ ๋ก, ๋ด๋ถ value์ ๊ฐ์ goalRoom ์ํฐํฐ๊ฐ ์ฐธ์กฐํ๊ณ ์๋ ์ค์ ํ๋๋ช ์ผ๋ก ์์ฑํด์ผ ํ๋ค.
์ด๋ฐ ์์ผ๋ก ์๋ชป๋ ํ๋ ๊ฐ์ ์ ๋ ฅํ๊ฒ ๋๋ฉด ์ปดํ์ผ ํ์ ๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค.
goalRoom์ด ์ค์ ๋ก ์์กดํ๊ณ ์๋ ํ๋๋ช ์ผ๋ก ์์ฑํด์ผ ํ๋ค.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "goal_room_id", nullable = false)
@QueryInit(value = {"roadmapContent.*"})
protected GoalRoom goalRoom;
์ถ๊ฐ์ ์ผ๋ก, ์ด๋ ๊ฒ ์์ผ๋์นด๋๋ก ๋ช ์ํด๋ ์ ๋์ํ๋ค.
์์ ํ์ ๋ค์ ๋๋ฒ๊น ์ ์งํํด๋ณด๋ฉด ์ด๋ฒ์๋ roadmap์ ๋ํ ์ ๋ณด๋ ์ ์ฑ์์ง ๊ฒ์ ํ์ธํ ์ ์๋ค.
ํ ์คํธ๋ ์ฑ๊ณต!
+) ์ถ๊ฐ์ ์ผ๋ก, ์ด๋ ๊ฒ @QueryInit์ ํน์ ํ๋์ ์ ์ฉํ๊ณ ๋๋ฉด ๊ธฐ์กด์ ์ ๋์๊ฐ๋ ์ฟผ๋ฆฌ๋ค์ด ์ ๋์๊ฐ ์๋ ์๋ค.
๋ ๊ฐ์ ๊ฒฝ์ฐ์๋ ๋น์ทํ ๋ค๋ฅธ ์ฟผ๋ฆฌ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์๋๋ฐ, 2๋จ๊ณ ๋ ๋ฒจ์ ์๋ ํ๋์ ๋ํด์ NPE๊ฐ ๋ฐ์ํ์๋ค.
๋น์ทํ ์ค๋ฅ์ง๋ง goalRoomMember -> member -> identifier (VO์ฌ์ ๋จ๊ณ์ ๋ฐ์ํ์ง ์๋ ๊ฒ ๊ฐ๋ค. ์ฒ์์๋ 3๋จ๊ณ๋ผ๊ณ ์๊ฐํ๋๋ฐ ๊ณต์๋ฌธ์์์ 2๋จ๊ณ๋ผ๊ณ ์ ํํ๊ฒ ๋ช ์ํ์ผ๋๊น...)์ ๋ํด์ ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒ์ด์๋ค.
@QueryInit์ ๋ฌ์ง ์์์ ๋๋ 3๋จ๊ณ๊น์ง ์ ๊ฐ์๋๋ฐ, ๋ช ์์ ์ผ๋ก ์ด๋ ธํ ์ด์ ์ ์ง์ ํด์ฃผ๋๊น ์ด๋ ๊ฒ ๋ ๊ฒ ๊ฐ๋ค.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "goal_room_id", nullable = false)
@QueryInit(value = {"roadmapContent.*"})
protected GoalRoom goalRoom;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
@QueryInit(value = {"identifier"})
protected Member member;
๊ทธ๋์ member ํ๋์ ๋ํด์๋ ์ด๋ ๊ฒ identifier์ ๋ํด์๋ ์ ์๋ฅผ ํด์ฃผ์๋ค.
์ฌ์ค ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์์ฃผ ๊ฐ๋จํ๋๋ฐ ์ฒ์ ๋ง์ฃผํ ์ค๋ฅ์ฌ์ ํด๊ฒฐํ๋ ๋ฐ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ ธ๋ค...!