N+1 문제 해결 - ORM을 사용하지 않더라도 나타날 수 있다
수상한 트랜잭션을 발견하다
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4dddcf4c]
JDBC Connection [HikariProxyConnection@1022964086 wrapping com.mysql.cj.jdbc.ConnectionImpl@353c6da1] will be managed by Spring
==> Preparing: SELECT * FROM ( ... ) t ORDER BY t.created_at DESC
==> Parameters: ...
<== Columns: ...
<== Row: ...
<== Row: ...
<== Row: ...
<== Total: 3
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4dddcf4c]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4dddcf4c] from current transaction
==> Preparing: SELECT * FROM image WHERE id = ?
==> Parameters: null
<== Total: 0
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4dddcf4c]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4dddcf4c] from current transaction
==> Preparing: SELECT * FROM image WHERE id = ?
==> Parameters: 14(Long)
<== Columns: ...
<== Row: ...
<== Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4dddcf4c]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4dddcf4c] from current transaction
==> Preparing: SELECT * FROM image WHERE id = ?
==> Parameters: 8(Long)
<== Columns: ...
<== Row: ...
<== Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4dddcf4c]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4dddcf4c]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4dddcf4c]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4dddcf4c]
내가 참여한 모임을 조회하는 로직인데, 각 항목에 대해 이미지 url을 조회하는 쿼리가 각각 따로 나가고 있다.
코드를 보니…
원인 발견
public List<MeetingListDto> getJoinedMeetingList(CurrentMemberInfo memberInfo) {
List<MeetingListDto> meetingListDtoList = meetingMapper.findMeetingListJoined(memberInfo.memberId());
for (MeetingListDto dto : meetingListDtoList) {
dto.setImageUrl(imageService.getImageUrl(dto.getImageId()));
}
return meetingListDtoList;
}
imageService.getImageUrl() 메서드의 내부 동작을 들여다보면,
public String getImageUrl(Long imageId) {
Image image = imageMapper.findById(imageId);
if (image == null) {
return null;
}
return image.getUrl();
}
이곳에서 N+1 문제를 일으키고 있었다. dto 리스트에 대해 반복하면서 각각의 dto마다 image의 url을 조회하는 쿼리를 날리고 있었다.
getImageUrl 메서드 내부에서 DAO로 쿼리를 날리고 있었기에 표면적으로 별 대수롭지 않게 넘어간 것이 문제였다.
앞으로는 반복문이 있으면 일단 의심부터 하는 습관을 들이도록 하자.
문제 해결
SELECT
...
i.url AS image_url, -- 추가!
...
FROM meeting m
LEFT JOIN image i ON m.image_id = i.id -- 추가!
...
해당 dto를 가져오는 쿼리에 join을 걸어 image의 url을 엮어주도록 하여 간단히 해결하였다.
public List<MeetingListDto> getJoinedMeetingList(CurrentMemberInfo memberInfo) {
return meetingMapper.findMeetingListJoined(memberInfo.memberId());
}
서비스 계층 코드도 매우 간단해졌다.
JPA와 같은 ORM을 사용하지 않더라도 프로그래머의 부주의로 인해 N+1 문제가 충분히 발생할 수 있다는 것을 알아두자.
댓글남기기