오늘 한 것 :
1. 메서드 별 유저 권한 체크
2. 삭제 시 삭제자 처리
3. 가게 등록 시 유저 정보 추가
4. n+1 조회 해결하기 ( getStoreList() )
5. 컨트롤러 테스트 다시 작성
내일 해야 하는 것 :
1. 페이징처리 ( 10, 30, 50 제한, 나머지는 10으로 )
2. dto 변환 convertToStoreReponse 메서드 개선하기
삭제 시, 삭제자 추가
- 이전 코드
@Transactional
@Override
public StoreCreationResponse createStore(StoreCreationRequest request, Long username) {
User user = userRepository.findById(username)
.orElseThrow(() -> new IllegalArgumentException("해당 사용자를 찾을 수 없습니다."));
// 가게 주소를 가게 주소 테이블에 저장, 중복 체크
StoreAddress storeAddress = storeAddressService.getOrCreateAddress(
request.sigungu(),
request.sido(),
request.dong()
);
Store store = Store.builder()
.user(user)
.storeAddress(storeAddress)
.name(request.name())
.detailAddress(request.detailAddress())
.build();
storeRepository.save(store);
// 4. 카테고리 처리 (중복 제거 및 등록)
List<UUID> savedCategoryIds = processCategories(store, request.categoryNames());
return StoreCreationResponse.from(store, savedCategoryIds);
}
userRepository에 findByUsername이 있어서 사용하려 했는데
기존의 PrincipatDetails가 유저명을 반환하는 함수가 없어
findById를 만들고 사용했다.
가게 목록 조회 시 n+1 조회 해결
- 이전 코드
@Transactional(readOnly = true)
@Override
public Page<StoreListResponse> getStoreList(StoreSearchCondition condition, Pageable pageable) {
Page<Store> storePage = storeRepository.searchStores(condition, pageable);
return storePage.map(store -> {
String mainCategory = storeCategoryRepository.findFirstByStore(store)
.map(sc -> sc.getCategory().getCategoryName())
.orElse("미지정");
return StoreListResponse.from(store, mainCategory);
});
}
결과를 dto로 반환하는 과정에서 다시 repository를 호출하기 때문에 카테고리 수 n번만큼 쿼리문이 추가로 나갔다.
- 수정 후 코드
@Transactional(readOnly = true)
@Override
public Page<StoreListResponse> getStoreList(StoreSearchCondition condition, Pageable pageable) {
// 1. 가게 목록 조회 (Querydsl - 1번)
Page<Store> storePage = storeRepository.searchStores(condition, pageable);
List<Store> stores = storePage.getContent();
if (stores.isEmpty()) {
return Page.empty(pageable);
}
// 2. 카테고리 일괄 조회 (Querydsl - 1번, N+1 방지)
List<StoreCategory> allStoreCategories = storeCategoryRepository.findAllByStoreIn(stores);
// 3. 메모리 매핑 및 DTO 변환
Map<UUID, List<String>> categoryMap = allStoreCategories.stream()
.collect(Collectors.groupingBy(
sc -> sc.getStore().getStoreId(),
Collectors.mapping(sc -> sc.getCategory().getCategoryName(), Collectors.toList())
));
return storePage.map(store -> {
List<String> categories = categoryMap.getOrDefault(store.getStoreId(), List.of());
return StoreListResponse.from(store, categories.isEmpty() ? "미지정" : categories.get(0));
});
}
양방향이라면 객체 그래프탐색과 fetchJoin을 사용하면 쉽지만 단방향 조회를 유지하면서 하다 보니 좀 어려운 것 같다.
findFirstByStore에서
findAllByStoreIn로 바꿨다.
- findAllByStoreIn 코드
@Override
public List<StoreCategory> findAllByStoreIn(List<Store> stores) {
return queryFactory
.selectFrom(storeCategory)
.join(storeCategory.category, category).fetchJoin() // Category를 한 번에 가져옴
.where(storeCategory.store.in(stores))
.fetch();
}
결과 예시를 표로 나타내면 아래와 같다.
| store_category_id ( UUID | store_id ( UUID ) | category_name ( UUID ) |
| 101 | 가게 A | 치킨 |
| 102 | 가게 A | 피자 |
| 103 | 가게 B | 짜장면 |
| 104 | 가게 C | 카레 |
실제로는 UUID의 형태로 나오지만 이해를 돕기 위해 숫자와 문자열로 표시했다.
가게 A는 카테고리를 2개 가졌기에 2개의 행이 출력된다.
결과는 findAllByStoreIn(store) 호출 후 저장한 변수 allStoreCategories에 저장된다.
// 2. 카테고리 일괄 조회 (Querydsl - 1번, N+1 방지)
List<StoreCategory> allStoreCategories = storeCategoryRepository.findAllByStoreIn(stores);
- Map을 사용해 DTO로 변환하는 부분
Map<UUID, List<String>> categoryMap = allStoreCategories.stream() // 가게를 기준으로 카테고리 배열이 저장
.collect(Collectors.groupingBy(
sc -> sc.getStore().getStoreId(), // "가게 ID를 기준으로 모여라!" (Key)
Collectors.mapping(
sc -> sc.getCategory().getCategoryName(), // "카테고리 이름만 쏙 뽑아라!" (Value)
Collectors.toList()
)
));
중간 테이블이 categoryMap이고,
그 중간 테이블의 한 행을
가게 id가 가진 카테고리들을 가져와서 리스트로 저장한다.
- 이후 반환하는 부분
return storePage.map(store -> {
List<String> categories = categoryMap.getOrDefault(store.getStoreId(), List.of());
return StoreListResponse.from(store, categories.isEmpty() ? "미지정" : categories.getFirst());
});
다시 store와
위에서 가게를 기준으로 카테고리 리스트를 저장한 categoryMap를 짝지어서 반환한다.
'내배캠' 카테고리의 다른 글
| 30일 차 TIL - 프로젝트하며 얻은 것 리스트 (0) | 2026.03.11 |
|---|---|
| 29일차 TIL - RestDocs 사용법 (0) | 2026.03.10 |
| 27일 차 TIL (0) | 2026.03.08 |
| [내일배움캠프] 25일 차 TIL - CodeRabbit 코드 리뷰 (0) | 2026.03.06 |
| [내일배움캠프] 24일 차 TIL 클린 아키텍처와 DDD (0) | 2026.03.05 |
