28일 차 TIL

2026. 3. 9. 23:14·내배캠

오늘 한 것 :
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
'내배캠' 카테고리의 다른 글
  • 30일 차 TIL - 프로젝트하며 얻은 것 리스트
  • 29일차 TIL - RestDocs 사용법
  • 27일 차 TIL
  • [내일배움캠프] 25일 차 TIL - CodeRabbit 코드 리뷰
MvA
MvA
백엔드 개발자 김재현입니다. 주로 공부하면서 느낀점을 기록합니다.
  • MvA
    Man vs Ai
    MvA
  • 전체
    오늘
    어제
    • 분류 전체보기 (94)
      • Java (6)
      • Python (8)
        • 딥러닝 (1)
        • 머신러닝 (7)
      • JavaScript (2)
      • 내배캠 (60)
      • 개인 프로젝트 (11)
      • 책 후기 (5)
      • 기타 (1)
  • 블로그 메뉴

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

  • 공지사항

  • 인기 글

  • 태그

    아키텍처
    배포
    내일배움캠프
    Riot API
    TiL
    머신러닝
    딥러닝
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
MvA
28일 차 TIL
상단으로

티스토리툴바