Skip to content
6 changes: 6 additions & 0 deletions src/main/java/eatda/domain/cheer/Cheer.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ public class Cheer extends AuditingEntity {
@OneToMany(mappedBy = "cheer", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<CheerImage> images = new HashSet<>();

/*
CheerTags가 Embedded이기 때문에 BatchSize를 그대로 적용하지 못함.
성능을 위해서는 Embedded 제거 후 직접 @OneToMany로 매핑 필요함.
현재 데이터가 많지 않음으로 현상 유지하며 모니터링.
추후 재설계 필요
*/
@Embedded
private CheerTags cheerTags;

Expand Down
6 changes: 6 additions & 0 deletions src/main/java/eatda/domain/store/Store.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ public class Store extends AuditingEntity {
@Embedded
private Coordinates coordinates;

/*
현재는 가게당 평균 응원 수가 5개 이하이므로 BatchSize=10이 적절함.
데이터 증가를 고려하여 IN 쿼리 한 번당 최대 30개 Store의 Cheer를 로딩하도록 설정.
향후 응원 수가 증가하거나 Store 리스트 조회 규모가 커질 경우
성능 모니터링 후 BatchSize 조정 및 Fetch 전략 재검토 필요.
*/
@OneToMany(mappedBy = "store")
private List<Cheer> cheers = new ArrayList<>();

Expand Down
14 changes: 7 additions & 7 deletions src/main/java/eatda/repository/cheer/CheerRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,18 @@
import eatda.domain.store.StoreCategory;
import jakarta.persistence.criteria.JoinType;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.lang.Nullable;

public interface CheerRepository extends JpaRepository<Cheer, Long> {

@EntityGraph(attributePaths = {"member", "cheerTags.values"})
List<Cheer> findAllByStoreOrderByCreatedAtDesc(Store store, PageRequest pageRequest);
Page<Cheer> findAllByStoreOrderByCreatedAtDesc(Store store, PageRequest pageRequest);

default List<Cheer> findAllByConditions(@Nullable StoreCategory category,
default Page<Cheer> findAllByConditions(@Nullable StoreCategory category,
List<CheerTagName> cheerTagNames,
List<District> districts, Pageable pageable) {
Specification<Cheer> spec = createSpecification(category, cheerTagNames, districts);
Expand All @@ -36,7 +35,9 @@ private Specification<Cheer> createSpecification(@Nullable StoreCategory categor
}
if (!cheerTagNames.isEmpty()) {
spec = spec.and(((root, query, cb) -> {
query.distinct(true);
if (query != null) {
query.distinct(true);
}
return root.join("cheerTags").join("values", JoinType.LEFT)
.get("name").in(cheerTagNames);
}));
Expand All @@ -47,8 +48,7 @@ private Specification<Cheer> createSpecification(@Nullable StoreCategory categor
return spec;
}

@EntityGraph(attributePaths = {"store", "member", "cheerTags.values"})
List<Cheer> findAll(Specification<Cheer> specification, Pageable pageable);
Page<Cheer> findAll(Specification<Cheer> specification, Pageable pageable);

int countByMember(Member member);

Expand Down
15 changes: 9 additions & 6 deletions src/main/java/eatda/repository/store/StoreRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import eatda.exception.BusinessException;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.lang.Nullable;
Expand All @@ -33,16 +33,15 @@ default Store getById(Long id) {
""")
List<Store> findAllByCheeredMemberId(long memberId);

default List<Store> findAllByConditions(@Nullable StoreCategory category,
default Page<Store> findAllByConditions(@Nullable StoreCategory category,
List<CheerTagName> cheerTagNames,
List<District> districts,
Pageable pageable) {
Specification<Store> spec = createSpecification(category, cheerTagNames, districts);
return findAll(spec, pageable);
}

@EntityGraph(attributePaths = {"cheers"})
List<Store> findAll(Specification<Store> spec, Pageable pageable);
Page<Store> findAll(Specification<Store> spec, Pageable pageable);

private Specification<Store> createSpecification(@Nullable StoreCategory category,
List<CheerTagName> cheerTagNames,
Expand All @@ -52,8 +51,12 @@ private Specification<Store> createSpecification(@Nullable StoreCategory categor
spec = spec.and((root, query, cb) -> cb.equal(root.get("category"), category));
}
if (!cheerTagNames.isEmpty()) {
spec = spec.and(((root, query, cb) ->
root.join("cheers").join("cheerTags").join("values").get("name").in(cheerTagNames)));
spec = spec.and(((root, query, cb) -> {
if (query != null) {
query.distinct(true);
}
return root.join("cheers").join("cheerTags").join("values").get("name").in(cheerTagNames);
}));
}
if (!districts.isEmpty()) {
spec = spec.and((root, query, cb) -> root.get("district").in(districts));
Expand Down
3 changes: 0 additions & 3 deletions src/main/java/eatda/repository/story/StoryRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@
import eatda.domain.story.Story;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StoryRepository extends JpaRepository<Story, Long> {
@EntityGraph(attributePaths = "images")
Page<Story> findAllByOrderByCreatedAtDesc(Pageable pageable);

Page<Story> findAllByMemberIdOrderByCreatedAtDesc(Long memberId, Pageable pageable);

@EntityGraph(attributePaths = {"member", "images"})
Page<Story> findAllByStoreKakaoIdOrderByCreatedAtDesc(String storeKakaoId, Pageable pageable);
}
15 changes: 10 additions & 5 deletions src/main/java/eatda/service/cheer/CheerService.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
Expand Down Expand Up @@ -115,12 +116,15 @@ private void saveCheerImages(Cheer cheer,

@Transactional(readOnly = true)
public CheersResponse getCheers(CheerSearchParameters parameters) {
List<Cheer> cheers = cheerRepository.findAllByConditions(
Page<Cheer> cheerPage = cheerRepository.findAllByConditions(
parameters.getCategory(),
parameters.getCheerTagNames(),
parameters.getDistricts(),
PageRequest.of(parameters.getPage(), parameters.getSize(), Sort.by(Direction.DESC, "createdAt"))
PageRequest.of(parameters.getPage(), parameters.getSize(),
Sort.by(Direction.DESC, "createdAt"))
);

List<Cheer> cheers = cheerPage.getContent();
return toCheersResponse(cheers);
}

Expand All @@ -140,11 +144,12 @@ private CheersResponse toCheersResponse(List<Cheer> cheers) {
@Transactional(readOnly = true)
public CheersInStoreResponse getCheersByStoreId(Long storeId, int page, int size) {
Store store = storeRepository.getById(storeId);
List<Cheer> cheers = cheerRepository.findAllByStoreOrderByCreatedAtDesc(store, PageRequest.of(page, size));
Page<Cheer> cheersPage = cheerRepository.findAllByStoreOrderByCreatedAtDesc(store, PageRequest.of(page, size));

List<CheerInStoreResponse> cheersResponse = cheers.stream()
List<CheerInStoreResponse> cheersResponse = cheersPage.getContent().stream()
.map(CheerInStoreResponse::new)
.toList(); // TODO N+1 문제 해결
.toList();

return new CheersInStoreResponse(cheersResponse);
}
}
3 changes: 2 additions & 1 deletion src/main/java/eatda/service/store/StoreService.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
Expand All @@ -45,7 +46,7 @@ public StoreResponse getStore(long storeId) {
// TODO : N+1 문제 해결
@Transactional(readOnly = true)
public StoresResponse getStores(StoreSearchParameters parameters) {
List<Store> stores = storeRepository.findAllByConditions(
Page<Store> stores = storeRepository.findAllByConditions(
parameters.getCategory(),
parameters.getCheerTagNames(),
parameters.getDistricts(),
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ spring:
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
default_batch_fetch_size: 30

flyway:
enabled: true
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ spring:
max-file-size: 5MB
max-request-size: 20MB

# BatchSize 미적용시를 비교하기 위해 local에는 BatchSize를 추가하지 않음
jpa:
hibernate:
ddl-auto: validate
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ spring:
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
default_batch_fetch_size: 30

flyway:
enabled: true
Expand Down
11 changes: 6 additions & 5 deletions src/test/java/eatda/repository/cheer/CheerRepositoryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

class CheerRepositoryTest extends BaseRepositoryTest {
Expand Down Expand Up @@ -64,7 +65,7 @@ class FindAllByConditions {
Cheer cheer2_2 = cheerGenerator.generateCommon(member2, store2);
Cheer cheer3_2 = cheerGenerator.generateCommon(member2, store3);

List<Cheer> actual = cheerRepository.findAllByConditions(
Page<Cheer> actual = cheerRepository.findAllByConditions(
StoreCategory.KOREAN, List.of(), List.of(), Pageable.unpaged());

assertThat(actual).map(Cheer::getId)
Expand All @@ -90,7 +91,7 @@ class FindAllByConditions {
cheerTagGenerator.generate(cheer2_2, List.of(CheerTagName.CLEAN_RESTROOM));
cheerTagGenerator.generate(cheer3_1, List.of(CheerTagName.ENERGETIC, CheerTagName.QUIET));

List<Cheer> actual = cheerRepository.findAllByConditions(null,
Page<Cheer> actual = cheerRepository.findAllByConditions(null,
List.of(CheerTagName.INSTAGRAMMABLE, CheerTagName.CLEAN_RESTROOM), List.of(), Pageable.unpaged());

assertThat(actual)
Expand All @@ -110,7 +111,7 @@ class FindAllByConditions {
Cheer cheer2_2 = cheerGenerator.generateCommon(member2, store2);
Cheer cheer3_2 = cheerGenerator.generateCommon(member2, store3);

List<Cheer> actual = cheerRepository.findAllByConditions(
Page<Cheer> actual = cheerRepository.findAllByConditions(
null, List.of(), List.of(District.GANGNAM), Pageable.unpaged());

assertThat(actual)
Expand Down Expand Up @@ -145,7 +146,7 @@ class FindAllByConditions {
cheerTagGenerator.generate(cheer4_2, List.of(CheerTagName.INSTAGRAMMABLE));
cheerTagGenerator.generate(cheer5_2, List.of(CheerTagName.CLEAN_RESTROOM, CheerTagName.ENERGETIC));

List<Cheer> actual = cheerRepository.findAllByConditions(StoreCategory.KOREAN,
Page<Cheer> actual = cheerRepository.findAllByConditions(StoreCategory.KOREAN,
List.of(CheerTagName.CLEAN_RESTROOM), List.of(District.GANGNAM), Pageable.unpaged());

assertThat(actual)
Expand All @@ -169,7 +170,7 @@ class FindAllByConditions {
cheerTagGenerator.generate(cheer2_1, List.of(CheerTagName.CLEAN_RESTROOM));
cheerTagGenerator.generate(cheer2_2, List.of(CheerTagName.CLEAN_RESTROOM));

List<Cheer> actual = cheerRepository.findAllByConditions(null, List.of(), List.of(), Pageable.unpaged());
Page<Cheer> actual = cheerRepository.findAllByConditions(null, List.of(), List.of(), Pageable.unpaged());

assertThat(actual)
.map(Cheer::getId)
Expand Down
11 changes: 6 additions & 5 deletions src/test/java/eatda/repository/store/StoreRepositoryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

class StoreRepositoryTest extends BaseRepositoryTest {
Expand Down Expand Up @@ -61,7 +62,7 @@ class FindAllByConditions {
Store store2 = storeGenerator.generate("1236", "서울시 강남구 역삼동 123-45", StoreCategory.WESTERN, startAt);
Store store3 = storeGenerator.generate("1237", "서울시 강남구 역삼동 123-45", StoreCategory.KOREAN, startAt);

List<Store> actual = storeRepository.findAllByConditions(
Page<Store> actual = storeRepository.findAllByConditions(
StoreCategory.KOREAN, List.of(), List.of(), Pageable.unpaged());

assertThat(actual).map(Store::getId)
Expand All @@ -87,7 +88,7 @@ class FindAllByConditions {
cheerTagGenerator.generate(cheer2_2, List.of(CheerTagName.CLEAN_RESTROOM));
cheerTagGenerator.generate(cheer3_1, List.of(CheerTagName.ENERGETIC, CheerTagName.QUIET));

List<Store> actual = storeRepository.findAllByConditions(null,
Page<Store> actual = storeRepository.findAllByConditions(null,
List.of(CheerTagName.INSTAGRAMMABLE, CheerTagName.CLEAN_RESTROOM), List.of(), Pageable.unpaged());

assertThat(actual).map(Store::getId)
Expand All @@ -100,7 +101,7 @@ class FindAllByConditions {
Store store2 = storeGenerator.generate("1236", "서울시 강남구 역삼동 123-45", District.GANGNAM);
Store store3 = storeGenerator.generate("1237", "서울시 성북구 석관동 123-45", District.SEONGBUK);

List<Store> actual = storeRepository.findAllByConditions(
Page<Store> actual = storeRepository.findAllByConditions(
null, List.of(), List.of(District.GANGNAM), Pageable.unpaged());

assertThat(actual).map(Store::getId)
Expand Down Expand Up @@ -134,7 +135,7 @@ class FindAllByConditions {
cheerTagGenerator.generate(cheer4_2, List.of(CheerTagName.INSTAGRAMMABLE));
cheerTagGenerator.generate(cheer5_2, List.of(CheerTagName.CLEAN_RESTROOM, CheerTagName.ENERGETIC));

List<Store> actual = storeRepository.findAllByConditions(StoreCategory.KOREAN,
Page<Store> actual = storeRepository.findAllByConditions(StoreCategory.KOREAN,
List.of(CheerTagName.CLEAN_RESTROOM), List.of(District.GANGNAM), Pageable.unpaged());

assertThat(actual).map(Store::getId)
Expand All @@ -157,7 +158,7 @@ class FindAllByConditions {
cheerTagGenerator.generate(cheer2_1, List.of(CheerTagName.CLEAN_RESTROOM));
cheerTagGenerator.generate(cheer2_2, List.of(CheerTagName.CLEAN_RESTROOM));

List<Store> actual = storeRepository.findAllByConditions(null, List.of(), List.of(), Pageable.unpaged());
Page<Store> actual = storeRepository.findAllByConditions(null, List.of(), List.of(), Pageable.unpaged());

assertThat(actual).map(Store::getId)
.containsExactlyInAnyOrder(store1.getId(), store2.getId(), store3.getId());
Expand Down
Loading
Loading