트렌비 백오피스에서 엑셀 다운로드가 504 Gateway Timeout으로 실패하던 문제를 두 단계로 고친 기록이다. JPA N+1 제거로 2분 40초를 6초로, Apache POI의 Workbook 구현체 교체로 생성 시간을 2초에서 400밀리초로 줄였다. 데이터가 3만 건일 때의 차이까지 재서 붙였다.
핵심 포인트- 1,473건을 내려받는 데 2분 넘게 걸렸고 운영에서는 504로 끊겼다.
- 원인은 JpaSpecificationExecutor의 findAll이 호출될 때마다 나던 N+1 쿼리였다.
- fetchJoin을 쓰려 해도 그 인터페이스로는 JPQL을 자유롭게 쓰기 어려워 QueryDSL로 옮겼다.
- 엔티티를 통째로 가져오는 대신 필요한 필드만 담는 Projection으로 조회했다.
- XSSFWorkbook은 데이터를 전부 메모리에 올려 쓰기 때문에 OOM 위험이 있다.
- SXSSFWorkbook은 설정된 창 크기만큼만 메모리에 두는 방식이라 3만 건에서 16초 차이가 났다.
상세 정리- 문제 인지: 구매확정 메뉴에서 엑셀 다운로드 시 504가 반환된다는 제보가 사내 메신저로 들어왔다.
- 측정: 로컬에서 1,473건을 받아보니 2분 40초가 걸렸다. 운영은 스펙과 로그 설정이 달라 더 낫겠지만 방치할 수 없는 수준으로 봤다.
- 원인 확인: 버튼 한 번에 무수히 많은 쿼리가 나가는 것이 로컬 테스트에서 바로 드러났다.
- 문제 지점: 필요한 데이터를 JpaSpecificationExecutor의 findAll로 가져오고 있었고 그때마다 N+1이 발생했다.
- 변경 1의 이유: 그 인터페이스는 개발자가 JPQL을 자유롭게 쓰기 어렵고 호출 시점에 어떤 쿼리가 나갈지 알기 힘들어 유지보수에 걸림돌이 된다고 판단했다.
- 관계의 규모: 대상 엔티티가 파트너, 이력, 배송 안내, 취소 등 많은 객체와 연관을 맺고 있고 그중 일부만 필요했다.
- 중첩 관계 문제: 연관된 객체가 또 다른 객체와 관계를 맺고 있어, 루트에서 여러 depth를 타고 내려가며 전부 fetchJoin을 걸면 불필요한 조인이 과도해진다.
- 변경 2의 선택: 그래서 엔티티 목록을 반환하는 대신 엑셀에 필요한 필드만 담는 DTO를 Projections로 만들어 조회했다.
- 1차 결과: N+1이 사라져 2분 40초가 6초로 줄었다. 이후로는 DB 조회 시간과 엑셀 생성 시간을 나눠 측정했다.
- 남은 불만: 조인 테이블이 많아 조회 시간은 어쩔 수 없다 쳐도, 1,000건으로 엑셀을 만드는 데 2초가 걸리는 것은 수만 건에서 감당할 자신이 없었다.
- 두 번째 발견: 기존 엑셀 유틸의 Workbook 구현체가 XSSFWorkbook으로 되어 있었다.
- 구현체의 차이: XSSF는 데이터를 모두 메모리에 올려 쓰기 때문에 공간을 많이 쓰고 데이터가 많으면 OOM 확률이 높다.
- 공식 문서 근거: SXSSF는 XSSF를 확장한 스트리밍 API로 아주 큰 스프레드시트를 만들어야 하고 힙 공간이 제한적일 때 쓰라고 안내한다.
- 동작 원리: SXSSF는 BigGridDemo 전략을 구현해 기본값 100행 크기의 창만큼만 메모리에 올려 작업한다.
- 2차 결과: 엑셀 생성 시간이 2초에서 400밀리초로 줄어 처음 대비 약 97% 개선됐다.
- 규모 확대 검증: 3만 건으로 두 구현체를 비교하니 16초 차이가 났다. 1,000건에서는 작아 보이던 차이가 데이터가 늘수록 벌어진다.
- 마무리 권고: JPA로 엑셀용 데이터를 조회한다면 Projection을 고려하고, Apache POI를 쓴다면 Workbook 구현체로 SXSSF를 쓰라고 정리한다.
왜 읽나관리자 화면의 엑셀 다운로드가 타임아웃으로 죽고 있다면 조회 쪽과 생성 쪽 어디를 먼저 손댈지, 각각 얼마나 줄어드는지 수치로 확인할 수 있다.