일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 스프링
- DynamicProgramming
- 해시맵
- 이분탐색
- 해시
- Algorithm
- Spring
- 깊이우선탐색
- programmers
- BFS
- 네트워크
- ReactiveProgramming
- 백트래킹
- Network
- DFS
- 동적계획법
- 너비우선탐색
- Backtracking
- 부분수열의합
- dynamic programming
- 구현
- greedy
- boj
- 브루트포스
- 프로그래머스
- 알고리즘
- HashMap
- 그리디
- 백준
- DP
- Today
- Total
옌의 로그
Querydsl transform() 사용 시 발생할 수 있는 커넥션 누수 이슈 본문
조회쿼리 작성시 연관관계를 갖는 경우, transform을 처음에 사용하려했었는데,
이 때 발견한 커넥션 leack 문제에 대해 포스팅하고자 한다.
Querydsl transform()이란?
transform()은 Querydsl에서 그룹핑된 데이터를 원하는 구조로 변환할 때 사용하는 메서드이다.
일반적인 fetch()는 단순히 List<Tuple> 형태로 데이터를 반환하지만,
transform()은 결과를 Map이나 DTO 등으로 그룹핑 및 가공해서 반환할 수 있게 해준다.
groupBy().transform() 형태로 많이 사용됨
예를 들어, 아래처럼 1:N 관계의 데이터를 조회할 때
- 하나의 작가가 여러 권의 책을 갖고 있을 때
- 하나의 주문에 여러 주문상품이 연결되어 있을 때
- 하나의 게시글에 여러 댓글이 달릴 때 등등
@Entity
public class Author {
@Id Long id;
String name;
@OneToMany(mappedBy = "author")
List<Book> books;
}
@Entity
public class Book {
@Id Long id;
String title;
@ManyToOne
Author author;
}
transform() 사용 예
Map<String, List<String>> result = queryFactory
.select(author.name, book.title)
.from(book)
.join(book.author, author)
.transform(
groupBy(author.name)
.as(list(book.title))
);
결과
{
"한강": ["소년이 온다", "채식주의자"],
"정유정": ["7년의 밤", "완전한 행복"],
...
}
DTO로 변환
List<AuthorWithBooksDto> result = queryFactory
.select(author.id, author.name, book.title)
.from(book)
.join(book.author, author)
.transform(
groupBy(author.id)
.list(Projections.constructor(
AuthorWithBooksDto.class,
author.id,
author.name,
list(book.title)
))
);
public class AuthorWithBooksDto {
Long authorId;
String authorName;
List<String> bookTitles;
public AuthorWithBooksDto(Long authorId, String authorName, List<String> bookTitles) {
this.authorId = authorId;
this.authorName = authorName;
this.bookTitles = bookTitles;
}
}
Connection leack 발생
transform()은 내부적으로 ResultSet을 lazy하게 순회
- == 프록시처럼 순차적으로 돌면서 결과를 처리하는 것
- 즉, 커넥션이 열려 있는 상태에서 모든 데이터를 다 순회하기 전까지는 커넥션이 반납되지 않는다 (헐;)
(원인)
- Querydsl 내부적으로 transform() 은 HibernateHandler#iterate(...) 를 통해 실행
- 이때 Hibernate의 Query.scroll(ScrollMode) 을 호출하면서 새로운 커넥션을 획득
- 문제는 이 커넥션을 반환하는 종료(close) 처리 로직이 transform() 경로에는 명시적으로 포함되지 않는다는 것
- 반면, fetch(), fetchOne(), fetchResults() 같은 메서드는 JPA의 getResultList(), getSingleResult() 를 호출하는데, 이들은 커넥션 반환을 트리거하는 종료 메서드(termination methods) 에 포함되어 있어 문제가 발생하지 않는다
즉, transform() 은 Querydsl/JPA 통합 계층에서 “쿼리를 종료하는 메서드”로 인식되지 않아 커넥션 반환이 누락된다
transform() 내부 동작 흐름
- query.transform(...) 호출
- FetchableQueryBase / AbstractJPAQuery 에서 실행 경로 진입
- 내부적으로 HibernateHandler#iterate(...) 실행
- Query.scroll(ScrollMode) 호출 → ScrollableResults 획득 (커넥션 점유 시작)
- ScrollableResultsIterator 로 감싸 결과 변환 진행
- 커넥션을 닫는 명시적 호출이 누락됨 → 커넥션 반환 불가
- 트랜잭션이 없는 경우, 커넥션이 풀에서 IN_USE 상태로 방치됨
따라서, @Transactional(readOnly = true) 붙여서 커넥션을 트랜잭션으로 관리해야함
- 스프링은 메서드에 트랜잭션 설정이 없으면 명시적으로 커넥션 관리하지 않음
→ 커넥션이 자동 반납되지 않음 - transform()은 커넥션을 오래 붙잡고 있음 → 누수 발생
1:N 응답 데이터 조회 해결 방법
1. @Transactional(readOnly = true) 명시하기
@Transactional(readOnly = true)
public Map<Long, List<BookDto>> findBooks() {
return queryFactory
.select(...)
.from(book)
.join(...)
.transform(...);
}
- readOnly = true (트랜잭션은 읽기전용으로 작동)
- Spring이 커넥션을 조기에 반납할 수 있음
- transform()을 사용해도 커넥션 누수 없이 안전
2. fetch() + 직접 DTO 조립하기
List<Tuple> tuples = queryFactory
.select(author.id, book.title)
.from(book)
.join(book.author, author)
.fetch();
Map<Long, List<String>> grouped = new LinkedHashMap<>();
for (Tuple tuple : tuples) {
Long authorId = tuple.get(author.id);
String bookTitle = tuple.get(book.title);
grouped.computeIfAbsent(authorId, k -> new ArrayList<>()).add(bookTitle);
}
- 모든 데이터를 한 번에 가져옴 → 커넥션 즉시 반납
- 수동 조립이 필요하긴 하지만 커넥션 누수 X
- HikariCP 6.x 환경에서도 완전 호환
HikariCP 6.x + Querydsl 5.x 에서는 transform() 사용 주의
- HikariCP 6부터는 Statement/ResultSet이 명확히 닫히지 않으면 예외 발생
- transform() 내부 구현이 auto-closeable을 완전히 보장하지 않기 때문에 예외 발생 가능
- 일부 버전에선 transform()이 사실상 Deprecated 취급
마무리하며 .. . .
사실, 처음엔 transform을 쓸 생각도 못한게, 현재 플젝에서 hikaricp 6+ 버전을 쓰고 있는데, transform이 deprecated 되서 써지지도 않았다. 그래서, hikaricp 버전 다운해서 쓰려고 찾아보다가, transform의 커넥션 누수 문제를 확인하게 되었다.
트래픽이 많은 경우가 아니라면, transform을 사용하면 좋을 것 같고(물론 트랜잭션으로 커넥션 누수를 방지해야한다)
그게 아니라면, fetch() + DTO 조립을 활용하는게 장기적으로 옳은 방향인 것 같다.
'스터디 > 스프링' 카테고리의 다른 글
[AOP] 비동기 저장을 안전하게? @Async, @Transactional, @TransactionalEventListener (2) | 2025.09.22 |
---|---|
[JPA] @OneToMany 연관관계, List vs Set 뭐가 더 나을까? (0) | 2025.09.17 |
Redis 캐시 저장, DTO 직렬화/역직렬화 깨짐 해결기 (3) | 2025.09.12 |
[Spring] Swagger에서 ErrorCode Enum 자동화하기 (1) | 2025.08.27 |
[Spring] JPA 연관관계 매핑과 성능 최적화 (Fetch Join vs. Open-in-view) (7) | 2025.08.25 |