feat: api wrapping

This commit is contained in:
2025-11-17 14:58:12 +09:00
parent 6c15c6167c
commit c081892ae7
8 changed files with 319 additions and 38 deletions

View File

@@ -5,8 +5,12 @@ import com.kamco.cd.kamcoback.postgres.entity.ZooEntity;
import com.kamco.cd.kamcoback.postgres.repository.ZooRepository;
import com.kamco.cd.kamcoback.zoo.dto.ZooDto;
import jakarta.persistence.EntityNotFoundException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -56,10 +60,23 @@ public class ZooCoreService implements BaseCoreService<ZooDto.Detail, Long, ZooD
@Override
public Page<ZooDto.Detail> search(ZooDto.SearchReq searchReq) {
Page<ZooEntity> zooEntities = zooRepository.listZoo(searchReq);
return zooEntities.map(this::toDetailDto);
// N+1 문제 해결: 한 번의 쿼리로 모든 Zoo의 animal count 조회
List<Long> zooIds =
zooEntities.getContent().stream().map(ZooEntity::getUid).collect(Collectors.toList());
Map<Long, Long> animalCountMap = zooRepository.countActiveAnimalsByZooIds(zooIds);
// DTO 변환
List<ZooDto.Detail> details =
zooEntities.getContent().stream()
.map(zoo -> toDetailDtoWithCountMap(zoo, animalCountMap))
.collect(Collectors.toList());
return new PageImpl<>(details, zooEntities.getPageable(), zooEntities.getTotalElements());
}
// Entity -> Detail DTO 변환 (동물 개수 포함)
// Entity -> Detail DTO 변환 (동물 개수 포함) - 단건 조회용
private ZooDto.Detail toDetailDto(ZooEntity zoo) {
Long activeAnimalCount = zooRepository.countActiveAnimals(zoo.getUid());
return new ZooDto.Detail(
@@ -72,4 +89,18 @@ public class ZooCoreService implements BaseCoreService<ZooDto.Detail, Long, ZooD
zoo.getModifiedDate(),
activeAnimalCount);
}
// Entity -> Detail DTO 변환 (동물 개수 포함) - 배치 조회용 (N+1 해결)
private ZooDto.Detail toDetailDtoWithCountMap(ZooEntity zoo, Map<Long, Long> countMap) {
Long activeAnimalCount = countMap.getOrDefault(zoo.getUid(), 0L);
return new ZooDto.Detail(
zoo.getUid(),
zoo.getUuid().toString(),
zoo.getName(),
zoo.getLocation(),
zoo.getDescription(),
zoo.getCreatedDate(),
zoo.getModifiedDate(),
activeAnimalCount);
}
}

View File

@@ -2,6 +2,8 @@ package com.kamco.cd.kamcoback.postgres.repository;
import com.kamco.cd.kamcoback.postgres.entity.ZooEntity;
import com.kamco.cd.kamcoback.zoo.dto.ZooDto;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.data.domain.Page;
@@ -14,4 +16,12 @@ public interface ZooRepositoryCustom {
Optional<ZooEntity> getZooByUid(Long uid);
Long countActiveAnimals(Long zooId);
/**
* 여러 Zoo의 활성 동물 수를 한 번에 조회 (N+1 문제 해결)
*
* @param zooIds Zoo ID 목록
* @return Map<Zoo ID, 활성 동물 수>
*/
Map<Long, Long> countActiveAnimalsByZooIds(List<Long> zooIds);
}

View File

@@ -7,7 +7,9 @@ import com.kamco.cd.kamcoback.zoo.dto.ZooDto;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
@@ -75,6 +77,39 @@ public class ZooRepositoryImpl implements ZooRepositoryCustom {
return count != null ? count : 0L;
}
@Override
public Map<Long, Long> countActiveAnimalsByZooIds(List<Long> zooIds) {
if (zooIds == null || zooIds.isEmpty()) {
return new HashMap<>();
}
// QueryDSL group by로 한 번에 조회
List<com.querydsl.core.Tuple> results =
queryFactory
.select(qAnimal.zoo.uid, qAnimal.count())
.from(qAnimal)
.where(qAnimal.zoo.uid.in(zooIds), qAnimal.isDeleted.eq(false))
.groupBy(qAnimal.zoo.uid)
.fetch();
// Map으로 변환
Map<Long, Long> countMap = new HashMap<>();
for (com.querydsl.core.Tuple tuple : results) {
Long zooId = tuple.get(qAnimal.zoo.uid);
Long count = tuple.get(qAnimal.count());
if (zooId != null && count != null) {
countMap.put(zooId, count);
}
}
// 조회되지 않은 Zoo는 0으로 설정
for (Long zooId : zooIds) {
countMap.putIfAbsent(zooId, 0L);
}
return countMap;
}
private BooleanExpression nameContains(String name) {
return name != null && !name.isEmpty() ? qZoo.name.contains(name) : null;
}