TIL 87 - [ Spring 최종 프로젝트 ( Day 23 ) - 문제 조회 성능 최적화 ]

2025. 6. 30. 16:55·[내일배움캠프-Sparta]/Spring 6기
반응형

만약, 갑자기 서버에 많은 유저들이 조회를 서버에 걸어온다면 어떻게될까? 서버에 부하가 생겨서 부담이 커지기 때문에 서버가 다운될수도 있다. → 이를 대비하기 위해 성능 최적화 필요.

여기서 가장 많이 트래픽이 모이는 곳이 CRUD중에서 R인 “조회” 에서 많이 일어난다.

 

현재 코드에는 정적 쿼리에서 동적 쿼리로 바꾸려고 적용했었던 적이 있었다.

동적 쿼리 적용을위해 QueryDSL을 사용했으며, 이제 해당하는 그 코드에 대해서 조회 성능을 개선을 위한 로직을 구현 해보았다.

🚄 문제 조회 성능 최적화

1️⃣ QueryDSL 리팩토링

@Repository
@RequiredArgsConstructor
public class ProblemQueryRepositoryImpl implements ProblemRepositoryCustom {

	private final JPAQueryFactory jpaQueryFactory;

	@Override
	public Page<Problem> searchByCondition(Pageable pageable, ProblemSearchCondition searchCondition) {

		QProblem problem = QProblem.problem;
		QProblemCategory problemCategory = QProblemCategory.problemCategory;
		QCategory category = QCategory.category;

		// where 조건을 깔끔 하게 조립
		BooleanBuilder builder = new BooleanBuilder();

		builder.and(problem.isDeleted.isFalse());

		JPAQuery<Problem> query = jpaQueryFactory
			.selectDistinct(problem)
			.from(problem)
			.leftJoin(problemCategory).on(problem.eq(problemCategory.problem))
			.leftJoin(problemCategory.category, category)
			.where(builder);

		// 카테고리 필터링
		if (searchCondition.category() != null) {
			builder.and(category.code.eq(searchCondition.category())
				.or(category.korName.eq(searchCondition.category())));
		}

		// 난이도 필터링
		if (searchCondition.difficulty() != null) {
			builder.and(problem.difficulty.eq(Difficulty.valueOf(searchCondition.difficulty())));
		}

		List<Problem> content = query
			.offset(pageable.getOffset())  /* offset + limit를 통한 페이징 최적화 */
			.limit(pageable.getPageSize())
			.orderBy(problem.createdAt.desc())
			.fetch();

		Long total = jpaQueryFactory
			.select(problem.countDistinct())
			.from(problem)
			.leftJoin(problemCategory).on(problem.eq(problemCategory.problem)) // <-- leftJoin 적용
			.leftJoin(problemCategory.category, category)
			.where(builder)
			.fetchOne();

		return new PageImpl<>(content, pageable, total != null ? total : 0);
	}
}

주요 쿼리 전략

  • Problem ↔ ProblemCategory ↔ Category 간 다대다(N:N) 관계를 leftJoin을 통해 조인
  • 카테고리 필터는 code 또는 korName으로 지원
  • 동적 조건을 BooleanBuilder로 유연하게 조립
  • distinct, countDistinct()로 중복 문제 제거
  • offset + limit으로 페이징 최적화
    • offset은 시작위치, limit는 데이터 개수 제한을 의미함.
  • Soft-delete 문제(isDeleted = false) 필터링
최적화 포인트 설명
카테고리 단방향 설계 대응 Problem ↔ ProblemCategory ↔ Category 조인을 명시적으로 수행
중복 방지 selectDistinct, countDistinct로 문제 중복 제거
조건별 동적 쿼리 조건이 있을 때만 .and(...)로 추가되므로 쿼리 성능 손실 최소화
N+1 방지 직접 조인 수행 (엔티티 관계를 통한 간접 접근 아님)
페이징 최적화 .offset(), .limit() 적용하여 한 번에 필요한 데이터만 조회

2️⃣ EAGER 즉시로딩 사용

최근에 AWS S3을 활용해 문제 이미지 기능을 구현하였다.

문제 이미지는 상세 조회시에만 쓰이기 때문에, 단건 조회 이므로 EAGER를 사용하는 경우에는 적합하게 쓰인다.

그래서 현재 Problem 엔티티 내 imageUrl이라는 필드가 추가 되어있고, Lazy로 사용중이다.

 

Lazy ( 지연 로딩 ) 사용 할 경우,

problem.getImageUrl(); 
// → 이 시점에 추가 SELECT 쿼리 1번 더 발생

즉, DB에 두 번 접근:

  • 1번: Problem 엔티티
  • 1번: imageUrl 리스트

Eager( 즉시 로딩 ) 사용 할 경우,

// 처음 SELECT 시에 imageUrl 함께 로딩
select ... from problem
left outer join problem_image_url ...

→ 한 번의 쿼리로 모두 로딩, 쿼리 수 줄어듦 → 이게 바로 성능 최적화

 

그래서 Problem 엔티티 내 해당하는 필드를 EAGER로 바꿔주었다.

@ElementCollection(fetch = FetchType.EAGER) // 즉시 로딩으로 변경
private List<String> imageUrl = new ArrayList<>();

Eager를 사용할때 상세 조회 에서는 효과적으로 쓰일 수 있지만, 리스트 조회( 목록 조회 ) 같은 경우에는 과도한 로딩이 발생할 수 있기 때문에 적절하지 않을수도 있다.

반응형

'[내일배움캠프-Sparta] > Spring 6기' 카테고리의 다른 글

TIL 89 - [ Spring 최종 프로젝트 ( Day 25 ) - 문제 테스트 코드 작성 ]  (0) 2025.07.02
TIL 88 - [ Spring 최종 프로젝트 ( Day 24 ) - 파일 크기 제한, 문제 이미지 수정/삭제 DB 잔존 현상 리팩토링 ]  (0) 2025.07.01
TIL 86 - [ Spring 최종 프로젝트 ( Day 22 ) - S3 문제 이미지 수정/삭제 기능 구현 ]  (0) 2025.06.28
TIL 85 - [ Spring 최종 프로젝트 ( Day 21 ) - Category, Difficulty 리팩토링 ]  (0) 2025.06.26
TIL 84 - [ Spring 최종 프로젝트 ( Day 20 ) - S3 문제 이미지 기능 구현, contextLoads() 에러 ]  (0) 2025.06.25
'[내일배움캠프-Sparta]/Spring 6기' 카테고리의 다른 글
  • TIL 89 - [ Spring 최종 프로젝트 ( Day 25 ) - 문제 테스트 코드 작성 ]
  • TIL 88 - [ Spring 최종 프로젝트 ( Day 24 ) - 파일 크기 제한, 문제 이미지 수정/삭제 DB 잔존 현상 리팩토링 ]
  • TIL 86 - [ Spring 최종 프로젝트 ( Day 22 ) - S3 문제 이미지 수정/삭제 기능 구현 ]
  • TIL 85 - [ Spring 최종 프로젝트 ( Day 21 ) - Category, Difficulty 리팩토링 ]
dimenshun
dimenshun
한 소년의 개발 일기
    반응형
  • dimenshun
    Dev Life Notes
    dimenshun
  • 전체
    오늘
    어제
    • 분류 전체보기 (268)
      • CS (23)
        • 자료구조 (0)
        • 알고리즘 (0)
        • 컴퓨터 구조 (8)
        • 네트워크 (6)
        • 운영체제 (3)
        • DB ( + SQLD ) (5)
        • SW공학 (1)
      • 프로그래밍 (3)
        • Java (0)
        • Spring (0)
        • HTML,CSS (3)
        • JavaScript (0)
      • 개발 툴 (7)
        • Git(버전관리) (1)
        • Docker (3)
        • AWS (2)
        • JSP (1)
      • 코딩테스트(Algorithm) (125)
        • 백준 (6)
        • 프로그래머스 (119)
      • [내일배움캠프-Sparta] (110)
        • Spring 6기 (106)
        • KPT 회고 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    알고리즘
    컴퓨터구조
    cs
    트랜잭션
    운영체제
    AWS
    SQLD
    Testcode
    Java
    web
    CPU
    docker
    내일배움캠프
    KPT
    웹
    세션
    네트워크
    Til
    OS
    spring
    network
    백엔드
    배포
    db
    Python
    코딩테스트
    메모리
    SQL
    It
    개발자
  • hELLO· Designed By정상우.v4.10.3
dimenshun
TIL 87 - [ Spring 최종 프로젝트 ( Day 23 ) - 문제 조회 성능 최적화 ]
상단으로

티스토리툴바