Skip to content

Conversation

@Junad-Park
Copy link
Contributor

@Junad-Park Junad-Park commented Nov 25, 2025

✨ 작업 내용

1️⃣ N+1 쿼리 해결

  • 개선 전: 게시글 1개당 4개의 추가 쿼리가 실행되어, 페이지당 최대 41개의 쿼리 발생
  • 개선 후: 일괄 조회로 연관 데이터를 한 번에 가져오도록 수정, 페이지당 쿼리 횟수를 고정 5개로 감소

[성능 향상]

  • 개선 후 부하 테스트 결과, 평균 응답 시간을 83% 단축하고 초당 처리량(TPS)은 약 4.4배 증가
  • 💡 N+1 해결 후 추가 확인 결과, 남은 응답 시간(997ms)의 약 50%가 로깅 객체의 직렬화 및 I/O 대기 시간인 것을 확인했습니다
    ※ 로깅 문제 해결 시, 응답 속도 492ms까지 추가 단축 가능 확인

📊 성능 개선 효과 비교

테스트 환경

  • 테스트 도구: K6 (부하 테스트)
  • 테스트 사용자: 1,000명
  • 동시 접속자(VU): 최대 53명
  • 테스트 시간: 5분
  • 테스트 시나리오: 4가지 (기본 목록 조회, 카테고리 필터링, 복잡한 검색, 커서 페이지네이션)

주요 지표

1. N+1 해결

지표 (Metrics) 개선 전 N+1 개선 개선 효과
평균 응답시간 5.97초 997ms 83% 단축
P95 응답시간 11.9초 2.15초 82% 단축
초당 처리량(TPS) 7.19 31.51 338% 증가
5분간 처리 요청 수 2,186건 9,660건 342% 증가
체크 성공률 67.00% 92.12% +25.12%p

2. N+1 및 로깅 개선

지표 (Metrics) 개선 전 N+1 및 로깅 개선 개선 효과
평균 응답시간 5.97초 492ms 91.8% 단축
P95 응답시간 11.9초 963ms 91.9% 단축
초당 처리량(TPS) 7.19 57.92 705.6% 증가
5분간 처리 요청 수 2,186건 17,544건 702.8% 증가
체크 성공률 67.00% 99.62% +32.62%p

📌 53명의 동시 사용자 환경에서 5분간 테스트 결과:

  • N+1 해결: 처리 요청 수 2,186건 → 9,660건 (4.4배 증가)
  • N+1 및 로깅 개선: 처리 요청 수 2,186건 → 17,544건 (8배 증가)

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 소셜 컨텐츠 대량 조회 시 성능을 최적화했습니다.
    • 이미지, 댓글, 좋아요 정보를 효율적으로 함께 로드하는 기능을 추가했습니다.
  • 개선

    • 데이터 조회 속도를 개선하기 위해 배치 처리 로직을 강화했습니다.
    • 쿼리 최적화로 응답 시간을 단축했습니다.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 25, 2025

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'auto_resolve_threads', 'spring_specific'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

개요

소셜 도메인에서 개별 데이터 조회 대신 배치 처리를 위한 매핑 유틸리티와 저장소 메서드들이 추가되었습니다. 여러 도메인 서비스에서 단일 엔티티 기반 메서드가 배치 조회 메서드로 변경 또는 확장되었습니다.

변경 사항

응집 / 파일(s) 변경 요약
저장소 계층 - 배치 조회 메서드 추가
src/main/java/im/toduck/domain/social/persistence/repository/SocialImageFileRepository.java, CommentRepository.java, LikeRepository.java, SocialCategoryLinkRepository.java
각 저장소에 배치 조회를 위한 커스텀 쿼리 메서드 추가 (findAllBySocialIdIn, countBySocialIdIn, findSocialIdsByUserAndSocialIdIn, findAllBySocialIdInWithCategory).
프로젝션 인터페이스
src/main/java/im/toduck/domain/social/persistence/projection/SocialCommentCount.java
소셜 ID별 댓글 수 조회용 새 프로젝션 인터페이스 추가 (getSocialId(), getCount() 메서드 포함).
서비스 계층 - 배치 조회 메서드 추가
src/main/java/im/toduck/domain/social/domain/service/SocialBoardService.java, SocialInteractionService.java
getSocialImagesBySocialIds, countCommentsBySocialIds, getLikedSocialIdsByUserAndSocialIds 등 배치 데이터 조회 메서드 추가.
서비스 계층 - 메서드 시그니처 변경
src/main/java/im/toduck/domain/social/domain/service/SocialCategoryService.java
getSocialCategoryDtosBySocial(Social)getSocialCategoryDtosBySocialIds(List<Long>)로 변경; 반환 타입을 List<SocialCategoryDto>에서 Map<Long, List<SocialCategoryDto>>로 변경.
매퍼 유틸리티
src/main/java/im/toduck/domain/social/common/mapper/SocialMapper.java
toSocialResponses 배치 매핑 메서드 추가. 이미지, 댓글 수, 카테고리, 좋아요 정보를 Map/Set으로 수령하여 SocialResponse 리스트 생성.
유스케이스 계층
src/main/java/im/toduck/domain/social/domain/usecase/SocialBoardUseCase.java, SocialProfileUseCase.java
개별 응답 구성 로직을 buildSocialResponses 헬퍼 메서드로 대체. 배치 데이터 조회 후 매퍼를 통해 일괄 변환.
저장소 커스텀 구현
src/main/java/im/toduck/domain/social/persistence/repository/querydsl/SocialRepositoryCustomImpl.java
findSocialsExcludingBlocked, searchSocialsExcludingBlocked 쿼리에 user, routine Fetch Join 추가.

시퀀스 다이어그램

sequenceDiagram
    participant UC as SocialBoardUseCase
    participant SCS as SocialCategoryService
    participant SIS as SocialInteractionService
    participant SBS as SocialBoardService
    participant SM as SocialMapper
    participant Repo as Repository Layer

    rect rgb(200, 220, 240)
    Note over UC,Repo: 기존 흐름 (개별 조회)
    UC->>UC: forEach Social
    UC->>SCS: getSocialCategoryDtosBySocial(single)
    SCS->>Repo: query per social
    Repo-->>SCS: result
    SCS-->>UC: result
    end

    rect rgb(220, 240, 200)
    Note over UC,Repo: 새로운 흐름 (배치 조회)
    UC->>UC: collect socialIds
    UC->>SBS: getSocialImagesBySocialIds(ids)
    SBS->>Repo: findAllBySocialIdIn(ids)
    Repo-->>SBS: imagesByIdMap
    SBS-->>UC: imagesByIdMap
    
    UC->>SIS: countCommentsBySocialIds(ids)
    SIS->>Repo: countBySocialIdIn(ids)
    Repo-->>SIS: commentCountMap
    SIS-->>UC: commentCountMap
    
    UC->>SIS: getLikedSocialIdsByUserAndSocialIds(user, ids)
    SIS->>Repo: findSocialIdsByUserAndSocialIdIn
    Repo-->>SIS: likedIdSet
    SIS-->>UC: likedIdSet
    
    UC->>SCS: getSocialCategoryDtosBySocialIds(ids)
    SCS->>Repo: findAllBySocialIdInWithCategory(ids)
    Repo-->>SCS: categoryDtosByIdMap
    SCS-->>UC: categoryDtosByIdMap
    
    UC->>SM: toSocialResponses(socials, images, counts, likes, categories)
    SM-->>UC: SocialResponse List
    end
Loading

예상 코드 리뷰 난이도

🎯 3 (중간) | ⏱️ ~25분

  • 특히 검토 필요한 부분:
    • SocialBoardUseCase.javaSocialProfileUseCase.javabuildSocialResponses 로직 및 데이터 흐름 일관성 확인
    • SocialCategoryService.getSocialCategoryDtosBySocialIds 메서드의 스트림 그룹화 로직 정확성
    • 저장소 계층의 새 쿼리 메서드들이 N+1 문제를 올바르게 해결하는지 확인 (특히 Fetch Join 적용)
    • SocialMapper.toSocialResponses에서 getOrDefault 사용 시 null/빈 데이터 처리의 안정성
    • 트랜잭션 범위와 읽기 전용 설정이 모든 배치 메서드에서 적절한지 확인

🐰 배치로 모아 한번에
데이터베이스 가벼워져
N+1의 악몽 사라지고
쿼리는 우아하게 날아다니네
토끼도 박수 짝짝!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 '게시글 검색 및 목록 조회 성능 개선'은 변경 사항의 주요 목적을 명확하게 요약하며, N+1 쿼리 문제 해결을 통한 성능 최적화라는 핵심을 잘 전달합니다.
Description check ✅ Passed PR 설명이 필수 섹션을 모두 포함하고 있으며, 작업 내용, 성능 개선 효과, 테스트 환경을 명확하게 설명하고 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch perf/social-list-optimization

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/main/java/im/toduck/domain/social/persistence/repository/querydsl/SocialRepositoryCustomImpl.java (1)

37-38: 페치 조인으로 N+1 문제를 효과적으로 해결했습니다.

userroutine 연관관계에 대한 페치 조인을 추가하여 게시글 목록 조회 시 발생하던 N+1 쿼리를 해결했습니다. leftJoin().fetchJoin() 패턴을 사용한 것이 적절합니다. 검증 결과 SocialMapper.toSocialResponse() 메서드에서 routine 필드가 실제로 응답 매핑에 사용되므로, line 82의 findUserSocials 메서드도 routine에 대한 페치 조인을 추가하기를 권장합니다. 이렇게 하면 다른 조회 메서드들과의 일관성을 유지하고 N+1 쿼리 문제를 완전히 방지할 수 있습니다.

.leftJoin(qSocial.user).fetchJoin()
.leftJoin(qSocial.routine).fetchJoin()
src/main/java/im/toduck/domain/social/domain/usecase/SocialBoardUseCase.java (1)

270-298: N+1 쿼리 해결을 위한 일괄 데이터 조회 방식 적용이 잘 되어 있습니다.

빈 리스트에 대한 early return, 각 연관 데이터(이미지, 댓글 수, 좋아요, 카테고리)의 일괄 조회 및 매핑 로직이 깔끔하게 구현되어 있습니다.

한 가지 고려사항: sif.getSocial().getId() 호출 시 getSocial()이 프록시인 경우 추가 쿼리가 발생할 수 있습니다. Repository 쿼리에서 @Query로 social ID를 직접 가져오거나, projection을 사용하면 더 최적화할 수 있습니다.

// SocialImageFileRepository에서 projection 또는 직접 조인으로 social_id를 가져오는 방안 검토
// 예: List<Object[]> findBySocialIdInWithSocialId(List<Long> socialIds);
// 또는 @Query("SELECT sif FROM SocialImageFile sif JOIN FETCH sif.social WHERE sif.social.id IN :socialIds")
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ebe6783 and dc90940.

📒 Files selected for processing (12)
  • src/main/java/im/toduck/domain/social/common/mapper/SocialMapper.java (2 hunks)
  • src/main/java/im/toduck/domain/social/domain/service/SocialBoardService.java (1 hunks)
  • src/main/java/im/toduck/domain/social/domain/service/SocialCategoryService.java (2 hunks)
  • src/main/java/im/toduck/domain/social/domain/service/SocialInteractionService.java (3 hunks)
  • src/main/java/im/toduck/domain/social/domain/usecase/SocialBoardUseCase.java (4 hunks)
  • src/main/java/im/toduck/domain/social/domain/usecase/SocialProfileUseCase.java (5 hunks)
  • src/main/java/im/toduck/domain/social/persistence/projection/SocialCommentCount.java (1 hunks)
  • src/main/java/im/toduck/domain/social/persistence/repository/CommentRepository.java (2 hunks)
  • src/main/java/im/toduck/domain/social/persistence/repository/LikeRepository.java (2 hunks)
  • src/main/java/im/toduck/domain/social/persistence/repository/SocialCategoryLinkRepository.java (2 hunks)
  • src/main/java/im/toduck/domain/social/persistence/repository/SocialImageFileRepository.java (2 hunks)
  • src/main/java/im/toduck/domain/social/persistence/repository/querydsl/SocialRepositoryCustomImpl.java (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/im/toduck/domain/social/domain/usecase/SocialBoardUseCase.java (1)
src/main/java/im/toduck/global/util/PaginationUtil.java (1)
  • PaginationUtil (8-82)
src/main/java/im/toduck/domain/social/domain/service/SocialBoardService.java (1)
src/test/java/im/toduck/domain/social/domain/usecase/SocialBoardUseCaseTest.java (1)
  • Transactional (61-1179)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CI
🔇 Additional comments (12)
src/main/java/im/toduck/domain/social/persistence/projection/SocialCommentCount.java (1)

3-7: N+1 문제 해결을 위한 프로젝션 인터페이스가 잘 설계되었습니다.

Spring Data JPA 프로젝션 패턴을 올바르게 따르고 있으며, CommentRepository.countBySocialIdIn 메서드와 함께 여러 게시글의 댓글 수를 일괄 조회하는 데 사용됩니다. 이는 PR 목표인 N+1 쿼리 문제 해결에 핵심적인 역할을 합니다.

src/main/java/im/toduck/domain/social/persistence/repository/SocialImageFileRepository.java (1)

17-18: 이미지 파일 일괄 조회 메서드가 잘 구현되었습니다.

여러 게시글의 이미지를 한 번의 쿼리로 조회하여 N+1 문제를 해결합니다. deletedAt IS NULL 조건으로 소프트 삭제된 이미지를 올바르게 필터링하고 있습니다.

src/main/java/im/toduck/domain/social/persistence/repository/CommentRepository.java (1)

37-40: 댓글 수 일괄 집계 쿼리가 효율적으로 구현되었습니다.

여러 게시글의 댓글 수를 한 번의 쿼리로 집계하여 N+1 문제를 해결합니다. GROUP BY를 사용한 집계 방식이 적절하며, 소프트 삭제된 댓글을 제외한 것도 올바릅니다. SocialCommentCount 프로젝션과의 통합도 잘 되어 있습니다.

src/main/java/im/toduck/domain/social/persistence/repository/LikeRepository.java (1)

19-21: 좋아요 상태 일괄 조회가 효율적으로 구현되었습니다.

사용자가 좋아요한 게시글 ID만 조회하여 불필요한 엔티티 로딩을 방지하고, 여러 게시글에 대한 좋아요 상태를 한 번의 쿼리로 확인합니다. 이는 좋아요 상태 확인 시 발생하던 N+1 문제를 효과적으로 해결합니다.

src/main/java/im/toduck/domain/social/domain/service/SocialBoardService.java (1)

190-196: 일괄 이미지 조회 메서드가 잘 구현되었습니다.

null/empty 입력에 대한 방어 로직이 적절하며, @Transactional(readOnly = true)를 사용하여 읽기 전용 트랜잭션임을 명시했습니다. 이 메서드는 buildSocialResponses에서 사용되어 N+1 문제 해결에 기여합니다.

src/main/java/im/toduck/domain/social/domain/usecase/SocialProfileUseCase.java (2)

160-188: N+1 문제 해결을 위한 일괄 처리 패턴이 탁월하게 구현되었습니다.

buildSocialResponses 메서드는 다음과 같은 효율적인 처리 흐름을 구현합니다:

  1. 게시글 ID 수집
  2. 모든 관련 데이터를 일괄 조회 (이미지, 댓글 수, 좋아요, 카테고리)
  3. 데이터를 Map/Set으로 구성하여 효율적인 조회 가능
  4. SocialMapper.toSocialResponses로 일괄 매핑

이 패턴은 PR 목표대로 페이지당 쿼리 수를 41개에서 5개로 대폭 감소시키는 핵심 개선사항입니다. 빈 리스트에 대한 조기 반환도 적절합니다.


110-111: 게시글 제한 로직이 올바른 위치에 배치되었습니다.

buildSocialResponses 호출 전에 limit(actualLimit)를 적용하여 불필요한 데이터 처리를 방지합니다. 이는 메모리 효율성과 성능 모두에 긍정적인 영향을 미칩니다.

src/main/java/im/toduck/domain/social/persistence/repository/SocialCategoryLinkRepository.java (1)

17-20: 카테고리 링크 일괄 조회에 페치 조인이 효과적으로 적용되었습니다.

JOIN FETCH scl.socialCategory를 사용하여 카테고리 엔티티를 즉시 로딩함으로써, 카테고리 데이터 접근 시 발생할 수 있는 추가 쿼리를 방지합니다. 이는 SocialCategoryService에서 카테고리 DTO를 구성할 때 성능을 크게 향상시킵니다.

src/main/java/im/toduck/domain/social/domain/usecase/SocialBoardUseCase.java (1)

184-185: 페이지네이션 로직이 올바르게 구현되었습니다.

hasMorenextCursor 계산 후 actualLimit으로 제한한 뒤 응답을 빌드하는 순서가 PaginationUtil의 의도대로 잘 적용되어 있습니다.

src/main/java/im/toduck/domain/social/domain/service/SocialInteractionService.java (2)

263-271: 좋아요 상태 일괄 조회 메서드가 잘 구현되었습니다.

빈 입력에 대한 early return과 Set 반환으로 O(1) lookup이 가능합니다.

Minor: Line 266의 Set.of()는 불변 Set을, Line 270의 new HashSet<>()은 가변 Set을 반환합니다. 호출자가 수정하지 않으므로 문제는 없지만, 일관성을 위해 둘 다 Set.copyOf()new HashSet<>()으로 통일하는 것도 고려해볼 수 있습니다.


249-261: 쿼리 설계가 이미 중복 키를 방지합니다.

@Query 주석의 GROUP BY c.social.id 절이 각 socialId마다 정확히 하나의 결과를 보장하므로, Collectors.toMap에서 중복 키 문제는 발생하지 않습니다. null/empty 처리도 적절하고, 일괄 조회로 N+1 문제를 해결합니다. merge function 추가는 불필요합니다.

src/main/java/im/toduck/domain/social/common/mapper/SocialMapper.java (1)

91-110: 일괄 매핑 메서드가 잘 구현되었습니다.

getOrDefault를 사용한 안전한 null 처리와 Set.contains()를 통한 O(1) 좋아요 상태 조회가 적절합니다. 기존 toSocialResponse 메서드를 재사용하여 코드 중복을 방지한 점도 좋습니다.

Comment on lines +22 to 38
public Map<Long, List<SocialCategoryDto>> getSocialCategoryDtosBySocialIds(final List<Long> socialIds) {
if (socialIds == null || socialIds.isEmpty()) {
return Map.of();
}

List<SocialCategoryLink> socialCategoryLinks = socialCategoryLinkRepository.findAllBySocialIdInWithCategory(
socialIds);

return socialCategoryLinks.stream()
.map(SocialCategoryLink::getSocialCategory)
.map(SocialCategoryMapper::toSocialCategoryDto)
.toList();
.collect(Collectors.groupingBy(
link -> link.getSocial().getId(),
Collectors.mapping(
link -> SocialCategoryMapper.toSocialCategoryDto(link.getSocialCategory()),
Collectors.toList()
)
));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

일괄 카테고리 조회로 변경이 잘 되었습니다.

API 시그니처 변경과 그룹핑 로직이 적절합니다.

link.getSocial().getId() 호출 시 N+1 쿼리 방지를 위해 findAllBySocialIdInWithCategory 쿼리가 Social도 함께 fetch하는지 확인이 필요합니다.


🏁 Script executed:

#!/bin/bash
# Repository 메서드가 Social을 JOIN FETCH 하는지 확인
rg -n "findAllBySocialIdInWithCategory" --type java -A 5

Length of output: 1037



---

🏁 Script executed:

```shell
#!/bin/bash
# View the full SocialCategoryLinkRepository.java file to see @Query annotation
cat src/main/java/im/toduck/domain/social/persistence/repository/SocialCategoryLinkRepository.java

Length of output: 845



---

🏁 Script executed:

```shell
#!/bin/bash
# Check if SocialCategoryLink entity has socialId field to avoid N+1
rg -n "class SocialCategoryLink" --type java -A 30

Length of output: 3568


findAllBySocialIdInWithCategory 쿼리에 Social JOIN FETCH 누락으로 N+1 쿼리 발생

SocialCategoryLinkRepository의 @query에서 socialCategory는 JOIN FETCH되지만 social은 fetch되지 않습니다. SocialCategoryService 32번 줄의 link.getSocial().getId() 호출 시 각 링크마다 별도의 쿼리가 발생하여 N+1 문제가 발생합니다.

수정 필요:

@Query("SELECT scl FROM SocialCategoryLink scl "
	+ "JOIN FETCH scl.social "
	+ "JOIN FETCH scl.socialCategory "
	+ "WHERE scl.social.id IN :socialIds AND scl.deletedAt IS NULL")
List<SocialCategoryLink> findAllBySocialIdInWithCategory(@Param("socialIds") List<Long> socialIds);

src/main/java/im/toduck/domain/social/persistence/repository/SocialCategoryLinkRepository.java:17

🤖 Prompt for AI Agents
In
src/main/java/im/toduck/domain/social/domain/service/SocialCategoryService.java
(around lines 22-38) the stream uses link.getSocial().getId() which triggers N+1
because the repository query doesn't fetch the Social; update the repository
query in
src/main/java/im/toduck/domain/social/persistence/repository/SocialCategoryLinkRepository.java
(around line 17) to JOIN FETCH scl.social as well as JOIN FETCH
scl.socialCategory (i.e. SELECT scl FROM SocialCategoryLink scl JOIN FETCH
scl.social JOIN FETCH scl.socialCategory WHERE scl.social.id IN :socialIds AND
scl.deletedAt IS NULL), keep the same method signature and @Param, and run tests
to verify the N+1 is resolved.

@github-actions
Copy link

📝 Jacoco Test Coverage

Overall Project 50.77% -0.05% 🍏
Files changed 92.55% 🍏

File Coverage
SocialRepositoryCustomImpl.java 99.32% 🍏
SocialMapper.java 98.78% 🍏
SocialBoardUseCase.java 95.37% -0.69% 🍏
SocialCategoryService.java 95% -5% 🍏
SocialProfileUseCase.java 90.99% -1.24% 🍏
SocialInteractionService.java 90.53% -2.51% 🍏
SocialBoardService.java 84.65% -0.5% 🍏

@github-actions
Copy link

🧪 Test Results

397 tests   393 ✅  24s ⏱️
121 suites    4 💤
121 files      0 ❌

Results for commit dc90940.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants