๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

Spring & Spring Boot

[Spring Boot] ์Œ์‹ ์ฃผ๋ฌธ ๊ด€๋ฆฌ ํ”Œ๋žซํผ ๊ฐœ๋ฐœ - Redis์™€ Scheduler๋ฅผ ํ™œ์šฉํ•œ ๋ฆฌ๋ทฐ ํ‰์  ๊ตฌํ˜„

๐Ÿ“– ๋ชฉ์ฐจ

 

 

 


SpringBoot๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ AIํ™œ์šฉ ๋น„์ฆˆ๋‹ˆ์Šค ํ”„๋กœ์ ํŠธ๋กœ '์Œ์‹ ์ฃผ๋ฌธ ๊ด€๋ฆฌ ํ”Œ๋žซํผ'์„ ๊ฐœ๋ฐœํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

์Œ์‹์ ๊ณผ ๋ฆฌ๋ทฐ ๋„๋ฉ”์ธ์„ ๋งก์•„ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ฒŒ ๋˜์—ˆ๋Š”๋ฐ,

๊ฐ€๊ฒŒ ์กฐํšŒ ์‹œ ๋ฆฌ๋ทฐ์˜ ํ‰์ ๊ณผ ๊ฐœ์ˆ˜๊ฐ€ ํ•จ๊ป˜ ์กฐํšŒ๋˜์–ด์•ผ ํ•˜๋Š” ์š”๊ตฌ์‚ฌํ•ญ์ด ์žˆ์—ˆ๋‹ค.

 

์—ฌ๋Ÿฌ ๋ฒˆ์˜ ๊ณ ๋ฏผ์„ ๊ฑฐ์ณ ๋ฆฌ๋ทฐ ํ‰์ ๊ณผ ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๋ฅผ N+1 ๋ฌธ์ œ์—†์ด ์กฐํšŒ ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

์–ด๋–ค ๋ฌธ์ œ์ ์ด ์žˆ์—ˆ๋Š”์ง€์™€ ๊ทธ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์ž.

 

 

 

 

 

 

๐Ÿ“Œ  ํ”„๋กœ์ ํŠธ ์š”๊ตฌ์‚ฌํ•ญ

  ์ด ํ”„๋กœ์ ํŠธ์˜ ํ•„์ˆ˜ ๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ ์ค‘ ํ•˜๋‚˜์ด๋‹ค. ๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ์„ ์‚ดํŽด๋ณด๋ฉด, ๊ฐ€๊ฒŒ ๋ชฉ๋ก์„ ์กฐํšŒ ์‹œ ํ‰์ ์„ ๋…ธ์ถœํ•ด์•ผ ํ•œ๋‹ค๊ณ  ๋˜์–ด์žˆ๋‹ค.

์ฒ˜์Œ์— ๊ธฐ๋Šฅ๋งŒ ์ƒ๊ฐํ•˜๊ณ  ๊ตฌํ˜„์„ ํ•˜๋‹ค ๋ณด๋‹ˆ ๊ฐ€๊ฒŒ๋ฅผ ์ƒ์„ธ ์กฐํšŒํ•˜๊ฑฐ๋‚˜ ๊ฒ€์ƒ‰ํ•ด์„œ ๊ฐ€๊ฒŒ ๋ชฉ๋ก์„ ์กฐํšŒํ•  ๋•Œ, ๋ฆฌ๋ทฐ ์ˆ˜์™€ ํ‰์ ์„ ๊ทธ๋•Œ๊ทธ๋•Œ ๊ณ„์‚ฐํ•˜์—ฌ ๋ณด์—ฌ์ฃผ๋„๋ก ๋กœ์ง์„ ์ž‘์„ฑํ–ˆ๋‹ค.

๐Ÿ’ก ๋ฆฌ๋ทฐ ๋ฐ ํ‰์  ๊ธฐ๋Šฅ
โœ”๏ธ ์ฃผ๋ฌธ์„ ํ†ตํ•ด ๊ฐ€๊ฒŒ์˜ ๋ฆฌ๋ทฐ ๋ฐ ํ‰์ ์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
โœ”๏ธ ์ฃผ๋ฌธ ๊ฒ€์ƒ‰ ์กฐํšŒ ์‹œ ์Œ์‹์  ๊ณ ์œณ๊ฐ’์„ ํ†ตํ•ด ํ˜ธ์ถœํ•˜๋ฉด ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
โœ”๏ธ ํ‰์ ์€ 1 - 5์ ์œผ๋กœ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.
โœ”๏ธ ๊ฐ€๊ฒŒ ๋ชฉ๋ก์„ ์กฐํšŒ ์‹œ ํ‰์ ์„ ๋…ธ์ถœํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•ด์•ผ ํ• ์ง€ ๊ณ ๋ฏผํ•ด ๋ด…๋‹ˆ๋‹ค.
    (๊ฐ€๊ฒŒ ๋ชฉ๋ก์„ ํ˜ธ์ถœ ์‹œ์— ํ‰์ ์„ ๊ณ„์‚ฐํ•˜๋ฉด n+1 ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.)

 

@Service
@RequiredArgsConstructor
public class StoreService {

  private final StoreRepository storeRepository;
  private final ReviewRepository reviewRepository;

  @Transactional(readOnly = true)
  public StoreDetailResponseDto getStore(UUID storeId) {
    Store store = findStore(storeId);

    Integer reviewCount = reviewRepository.countByStoreId(storeId);
    BigDecimal ratingAvg = reviewRepository.calculateAverageRatingByStoreId(storeId);

    return new StoreDetailResponseDto(
        storeId,
        String.valueOf(store.getCategory()),
        store.getName(),
        store.getContent(),
        store.getAddress(),
        store.getPhone(),
        ratingAvg,
        reviewCount
    );
  }
}

 

public interface ReviewRepository extends JpaRepository<Review, UUID> {

  @Query("SELECT COUNT(r) FROM Review r WHERE r.store.id = :storeId AND r.deletedAt IS NULL")
  Integer countByStoreId(UUID storeId);

  @Query("SELECT AVG(r.star) FROM Review r WHERE r.store.id = :storeId AND r.deletedAt IS NULL")
  BigDecimal calculateAverageRatingByStoreId(UUID storeId);
}

 

 

 

๊ฐ€๊ฒŒ ์กฐํšŒ ์‹œ์— ๊ณ„์‚ฐ์ด ํ•จ๊ป˜ ์ด๋ฃจ์–ด์ง€๋„๋ก ์ž‘์„ฑํ–ˆ๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋กœ ๊ฐ€๊ฒŒ๋ฅผ ํ•œ ๋ฒˆ ์กฐํšŒํ•  ๋•Œ๋งˆ๋‹ค ์ด๋ ‡๊ฒŒ ์ด 3๋ฒˆ์˜ ์ฟผ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋˜์–ด ๋ฒ„๋ ธ๋‹ค.

<store select ์ฟผ๋ฆฌ ์ƒ์„ฑ>
<๋ฆฌ๋ทฐ ํ‰์  ๊ณ„์‚ฐ์„ ์œ„ํ•œ ์ฟผ๋ฆฌ ์ƒ์„ฑ>
<๋ฆฌ๋ทฐ ์ˆ˜ ๊ณ„์‚ฐ์„ ์œ„ํ•œ ์ฟผ๋ฆฌ ์ƒ์„ฑ>

 

 

 

  ์„ค๊ณ„๋‹จ๊ณ„์—์„œ ์บ์‹ฑ๊นŒ์ง€๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ์ƒ๊ฐ์ด์—ˆ๊ณ , ์šฐ๋ คํ•˜๋˜ N+1๋ฌธ์ œ๋Š” ์•„๋‹ˆ์ง€๋งŒ ๊ฒฐ๊ณผ์ ์œผ๋กœ๋Š” ์„ฑ๋Šฅ์— ์•ˆ ์ข‹์€ ์˜ํ–ฅ์„ ๋ผ์น  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํŒ๋‹จ๋˜์–ด ๋กœ์ง์„ ์ˆ˜์ •ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. 

 

 

 

 

 


 

 

 

 

 

๐Ÿ“Œ  ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜์˜ํ•˜๋Š” ๋ฐฉ์‹

  ์šฐ์„  '๋ฆฌ๋ทฐ๊ฐ€ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ๋  ๋•Œ๋งˆ๋‹ค ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ‰๊ท  ํ‰์ ๊ณผ ๊ฐœ์ˆ˜๋ฅผ ์—…๋ฐ์ดํŠธํ•  ๊ฒƒ์ธ๊ฐ€?'๋ผ๋Š” ๊ณ ๋ฏผ์— ๋Œ€ํ•œ ๋‹ต์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ฐพ์•„๋ณด๋„๋ก ํ•˜์ž. ๋ฆฌ๋ทฐ์˜ ํ‰์ ๊ณผ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜์˜ํ•˜๋Š” ๋ฐฉ์‹์€ ๋‘ ๊ฐ€์ง€๋กœ ๋‚˜๋ˆด๋‹ค. ์ด ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ• ์ค‘์— ์–ด๋–ค ๋ฐฉ์‹์ด ๋” ๋‚˜์€์ง€ ์žฅ๋‹จ์ ์„ ๋น„๊ตํ•ด ๋ณด์ž.

 

  1. DB์— ์ €์žฅํ•˜์ง€ ์•Š๊ณ  ๊ฐ€๊ฒŒ ์กฐํšŒ ์‹œ, ๋ฆฌ๋ทฐ๋ฅผ ๋™์ ์œผ๋กœ ๊ณ„์‚ฐํ•˜์—ฌ ๋งค๋ฒˆ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ์œ ์ง€ํ•œ๋‹ค.
  2. DB์— ์ €์žฅํ•˜์—ฌ ์กฐํšŒ๋ฅผ ํ•ด์˜ค๋ฉฐ, ๋ฆฌ๋ทฐ ๋ณ€๊ฒฝ ์‹œ์—๋งŒ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•œ๋‹ค.
๊ตฌ๋ถ„ ๋™์  ๊ณ„์‚ฐ DB ์ €์žฅ ๋ฐฉ์‹
๋ฆฌ๋ทฐ ๊ฐฏ์ˆ˜ ๋ฐ ํ‰์  ์ €์žฅ ์œ„์น˜ ๋งค ์š”์ฒญ๋งˆ๋‹ค ๋™์ ์œผ๋กœ ๊ณ„์‚ฐ Store ์—”ํ‹ฐํ‹ฐ์˜ reviewCount์™€ ratingAvg ํ•„๋“œ์— ์ €์žฅ
๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ ์œ ์ง€ ํ•ญ์ƒ ์ตœ์‹  ์ƒํƒœ ์œ ์ง€๋จ ๋ฆฌ๋ทฐ ์ถ”๊ฐ€/์ˆ˜์ •/์‚ญ์ œ ์‹œ ์ˆ˜๋™ ์—…๋ฐ์ดํŠธ ํ•„์š”
์„ฑ๋Šฅ ๋งค๋ฒˆ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•˜๋ฏ€๋กœ ์„ฑ๋Šฅ ์ €ํ•˜ ๊ฐ€๋Šฅ ์กฐํšŒ ์‹œ ๋ฐ”๋กœ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด ์„ฑ๋Šฅ ํ–ฅ์ƒ
์œ ์ง€๋ณด์ˆ˜์„ฑ ์„œ๋น„์Šค ๋กœ์ง์ด ๋ณต์žกํ•  ์ˆ˜ ์žˆ์Œ ๋ฆฌ๋ทฐ ๋ณ€๊ฒฝ ์‹œ์—๋งŒ ์—…๋ฐ์ดํŠธ ํ•˜๋ฉด ๋˜๋ฏ€๋กœ ๋‹จ์ˆœ
ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ๋ณ„๋„ ๊ด€๋ฆฌ ํ•„์š” ์—†์Œ ๋ฆฌ๋ทฐ ๋ณ€๊ฒฝ ์‹œ ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ ์—…๋ฐ์ดํŠธ ํ•„์š”

 

 

  ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹์˜ ์žฅ๋‹จ์  ๋น„๊ต๋ฅผ ํ†ตํ•ด ๋งค๋ฒˆ ์ฆ‰์‹œ ๊ณ„์‚ฐ์„ ํ•˜๊ฒŒ ๋˜๋ฉด ๊ทธ๋•Œ๋งˆ๋‹ค ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰๋˜๊ณ , ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ค„ ๊ฒƒ์ด๋ผ๊ณ  ํŒ๋‹จํ•˜์—ฌ DB์— ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ๋‹ค.

 

๊ทธ๋‹ค์Œ์œผ๋กœ ์ƒ๊ฐํ•œ ๋ถ€๋ถ„์€ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ์˜ ์—…๋ฐ์ดํŠธ ์‹œ์ ์ด์—ˆ๋‹ค. ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๋Š” ๋ถ€๋‹ด์ด ๋  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ์‹ค์‹œ๊ฐ„์ด ์•„๋‹Œ ์ผ์ • ์ฃผ๊ธฐ๋งˆ๋‹ค ํ•œ ๋ฒˆ์— ์—…๋ฐ์ดํŠธ๋ฅผ ํ•œ๋‹ค๋ฉด ์„ฑ๋Šฅ์ ์œผ๋กœ ์ด์ ์ด ์žˆ์ง€ ์•Š์„๊นŒ?

 

 

 

 

 

๐Ÿ’ก ๋ณ€๊ฒฝ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ผ์ • ์ฃผ๊ธฐ๋กœ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•ด๋ณผ๊นŒ?

  ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ณ ๋ฏผ์„ ํ•ด๋ณธ ๊ฒฐ๊ณผ, ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•˜๋Š” ๊ฒƒ์€ ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ๋ถ€๋‹ด์ด ๋  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ผ์ • ์ฃผ๊ธฐ๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋„๋ก ์Šคํ”„๋ง ๋ถ€ํŠธ์—์„œ ์ง€์›ํ•˜๋Š” ์Šค์ผ€์ค„๋Ÿฌ์˜ ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

 

 

 

 

 

 

 

๐Ÿ“Œ  ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ์˜ ๋ณ€๊ฒฝ ๊ฐ์ง€

  ์Šค์ผ€์ค„๋Ÿฌ์˜ ๋„์ž…์„ ๊ณ ๋ คํ•˜๋ฉด์„œ ์–ด๋–ค ๋ฆฌ๋ทฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”์ง€ ์ถ”์ ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ํ•„์š”ํ–ˆ๋‹ค. ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ์˜ ๋ณ€๊ฒฝ ๊ฐ์ง€๋Š” ์–ด๋–ป๊ฒŒ ์ด๋ฃจ์–ด์ง€๋Š” ๊ฒƒ์ด ์ข‹์„๊นŒ? ์šฐ์„ , ๋ฆฌ๋ทฐ์˜ ์ด ๊ฐœ์ˆ˜์™€ ํ‰์ ์„ ๊ณ„์‚ฐํ•˜๋ ค๋ฉด ์•„๋ž˜์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ํ™•์ธํ•˜์—ฌ ๋ฐ˜์˜ํ•ด์•ผ ํ•œ๋‹ค. 

โœ”๏ธ ๋ฆฌ๋ทฐ๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์„ ๊ฒฝ์šฐ
→ ์ด ๊ฐฏ์ˆ˜ ์ฆ๊ฐ€, ํ‰๊ท  ํ‰์  ์—…๋ฐ์ดํŠธ ํ•„์š”
โœ”๏ธ ๋ฆฌ๋ทฐ๊ฐ€ ์ˆ˜์ •๋˜์—ˆ์„ ๊ฒฝ์šฐ
→ ํ‰์ ์ด ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํ‰๊ท  ํ‰์  ์—…๋ฐ์ดํŠธ ํ•„์š”
โœ”๏ธ ๋ฆฌ๋ทฐ๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์„ ๊ฒฝ์šฐ
→ ์ด ๊ฐฏ์ˆ˜ ๊ฐ์†Œ, ํ‰๊ท  ํ‰์  ์—…๋ฐ์ดํŠธ ํ•„์š”

์ฆ‰, ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ๋œ ๋ฆฌ๋ทฐ๋ฅผ ๋ชจ๋‘ ์กฐํšŒํ•ด์„œ ์Šคํ† ์–ด๋ณ„๋กœ ํ†ต๊ณ„๋ฅผ ๋‹ค์‹œ ๊ณ„์‚ฐํ•ด์•ผ ํ•œ๋‹ค.

 

 

 

๐Ÿ’ก ์ด๋•Œ, ๋ชจ๋“  ๊ฐ€๊ฒŒ๋ฅผ ์กฐํšŒํ•ด์„œ ๊ฐ ๊ฐ€๊ฒŒ์˜ ๋ฆฌ๋ทฐ ์ˆ˜์™€ ํ‰์ ์„ ๋‹ค์‹œ ๊ณ„์‚ฐํ•˜๊ณ  ์—…๋ฐ์ดํŠธํ•œ๋‹ค๋ฉด ์–ด๋–จ๊นŒ?

  DB์—์„œ createdAt, updateAt, deletedAt์„ ์ง์ ‘ ์กฐํšŒํ•˜๋Š” ๊ฒฝ์šฐ, ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก OR ์กฐ๊ฑด ๋•Œ๋ฌธ์— ์ธ๋ฑ์Šค ํ™œ์šฉ์ด ์–ด๋ ค์›Œ์ง€๊ณ  ์„ฑ๋Šฅ ์ €ํ•˜์˜ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋‹ค. ๋˜ํ•œ, ๋ฆฌ๋ทฐ ํ…Œ์ด๋ธ” ์ „์ฒด๋ฅผ ์กฐํšŒํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๊ธฐ ๋•Œ๋ฌธ์— Full Table Scan์˜ ์œ„ํ—˜์„ฑ๋„ ์žˆ๋‹ค.

์ด๋Ÿฐ ๋ฌธ์ œ์ ์„ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด ์–ด๋–ค ๋ฐฉ์‹์„ ํƒํ•ด์•ผ ํ• ๊นŒ ๊ณ ๋ฏผํ•˜๋˜ ์ค‘์—
'๋ฆฌ๋ทฐ๊ฐ€ ๋ณ€๊ฒฝ๋œ ํ•ด๋‹น ๊ฐ€๊ฒŒ์˜ ID๋งŒ์„ ์‚ฌ์šฉํ•ด์„œ ๊ทธ ๊ฐ€๊ฒŒ์— ๋Œ€ํ•ด์„œ๋งŒ ํ†ต๊ณ„๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋ฉด ์–ด๋–จ๊นŒ?'๋ผ๋Š” ์ƒ๊ฐ์„ ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

 

 

 

 

 

 

 

๐Ÿ“Œ  ๋ณ€๊ฒฝ๋œ ๊ฐ€๊ฒŒ์˜ ID๋ฅผ ๊ธฐ๋กํ•˜๋Š” ๋ฐฉ์‹

  ๊ทธ๋ ‡๋‹ค๋ฉด '๋ฆฌ๋ทฐ๊ฐ€ ๋ณ€๊ฒฝ๋œ ๊ฐ€๊ฒŒ์˜ ID๋Š” ์–ด๋””์— ๊ธฐ๋กํ•˜๋Š” ๊ฒŒ ์ข‹์„๊นŒ? ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธธ ์ˆ˜ ์žˆ๋„๋ก ํ…Œ์ด๋ธ”์„ ํ•˜๋‚˜ ๋” ์ƒ์„ฑํ•ด์•ผ ํ• ๊นŒ?' ์–ด๋–ค ๋ฐฉ๋ฒ•์ด ํšจ์œจ์  ์ผ์ง€ ๊ณ ๋ฏผํ•˜๋‹ค๊ฐ€ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ๊ฐ€ ์Œ“์ผ ๊ฒƒ์„ ๊ณ ๋ คํ•ด์„œ ์ธ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์ธ Redis๋ฅผ ์‚ฌ์šฉํ•ด ๋ณด๋ฉด ์–ด๋–จ๊นŒ ์ƒ๊ฐํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. ๋งˆ์นจ ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์—์„œ Redis๋ฅผ ํ™œ์šฉํ•œ AccessToken๊ณผ RefreshToken์œผ๋กœ ๋กœ๊ทธ์•„์›ƒ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ์—ˆ๊ณ , Redis์˜ Set์„ ์‚ฌ์šฉํ•ด ๋ณด๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค.

 

Redis์˜ Set์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์ž๋ฃŒ๊ตฌ์กฐ ํŠน์„ฑ์˜ ์ด์ ์œผ๋กœ ์ธํ•ด ๋น ๋ฅธ ์กฐํšŒ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ณ , ์ค‘๋ณต์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค. TTL๋„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๊ณ , ๋ฆฌ๋ทฐ ํ†ต๊ณ„๋ฅผ ์—…๋ฐ์ดํŠธ ํ•œ ๋’ค, ํ•ด๋‹น ๋ฆฌ์ŠคํŠธ๋ฅผ ๋น„์šธ ์ˆ˜๋„ ์žˆ๋‹ค.

 

 

 

 

 

โœ”๏ธ ์ด๋ ‡๊ฒŒ ์ตœ์ข…์ ์œผ๋กœ 'Redis Set + Scheduler'์˜ ์กฐํ•ฉ์œผ๋กœ ๊ตฌํ˜„ํ•ด ๋ณด๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค.
ํ•ด๋‹น ๋กœ์ง์— ๋Œ€ํ•ด ๋ฆฌํŒฉํ† ๋ง์„ ์ง„ํ–‰ํ•ด ๋ณด์ž.

 

 

 

 

 

 

 


 

 

 

 

 

 

 

  ์กฐํšŒ๋ฅผ ํ˜ธ์ถœ ์‹œ ๋งค๋ฒˆ ๊ณ„์‚ฐ์ด ์ด๋ฃจ์–ด์ง€๋Š” ๊ฒŒ ์•„๋‹Œ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ์˜ ๋ณ€๊ฒฝ์ด ์žˆ์„ ๊ฒฝ์šฐ์—๋งŒ ๊ณ„์‚ฐ์„ ํ•˜๊ณ  ์ตœ์‹ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜์˜ํ•  ์ˆ˜ ์žˆ๋„๋ก, ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ์—†๋‹ค๋ฉด select ์ฟผ๋ฆฌ๋งŒ ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ์„ ํ•ด๋ณด์ž.

// ๊ธฐ์กด ์„œ๋น„์Šค ๋กœ์ง
@Service
@RequiredArgsConstructor
public class StoreService {

  private final StoreRepository storeRepository;
  private final ReviewRepository reviewRepository;

  @Transactional(readOnly = true)
  public StoreDetailResponseDto getStore(UUID storeId) {
    Store store = findStore(storeId);

    Integer reviewCount = reviewRepository.countByStoreId(storeId);
    BigDecimal ratingAvg = reviewRepository.calculateAverageRatingByStoreId(storeId);

    return new StoreDetailResponseDto(
        storeId,
        String.valueOf(store.getCategory()),
        store.getName(),
        store.getContent(),
        store.getAddress(),
        store.getPhone(),
        ratingAvg,
        reviewCount
    );
  }
}

 

// ์ˆ˜์ • ํ›„ ์„œ๋น„์Šค ๋กœ์ง
@Service
@RequiredArgsConstructor
public class StoreService {

  private final StoreRepository storeRepository;

  @Transactional(readOnly = true)
  public StoreDetailResponseDto getStore(UUID storeId) {
    Store store = findStore(storeId);

    return new StoreDetailResponseDto(
        storeId,
        String.valueOf(store.getCategory()),
        store.getName(),
        store.getContent(),
        store.getAddress(),
        store.getPhone(),
        store.getRatingAvg(), // DB์— ์ €์žฅ๋œ ๋ฆฌ๋ทฐ ํ‰์ 
        store.getReviewCount() // DB์— ์ €์žฅ๋œ ๋ฆฌ๋ทฐ ๊ฐฏ์ˆ˜
    );
  }
}

 

 

  ๊ธฐ์กด์— ์ž‘์„ฑํ–ˆ๋˜ ๋กœ์ง์—์„œ๋Š” StoreService์—์„œ ๊ฐ€๊ฒŒ๋ฅผ ์กฐํšŒํ•  ๋•Œ๋งˆ๋‹ค ๋ฆฌ๋ทฐ ํ‰์ ๊ณผ ๊ฐœ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ–ˆ๋‹ค. DB์— ์ €์žฅ๋œ ๋ฆฌ๋ทฐ ํ‰์ ๊ณผ ๊ฐœ์ˆ˜๋ฅผ ์กฐํšŒํ•ด ์˜ค๋„๋ก ์ˆ˜์ •ํ•ด ์ค€๋‹ค. ๋ฆฌ๋ทฐ ํ†ต๊ณ„์— ๊ด€๋ จํ•ด์„œ๋Š” ReviewService์—์„œ ์ฒ˜๋ฆฌํ•ด ์ฃผ๋„๋ก ์ˆ˜์ •ํ•  ๊ฒƒ์ด๋‹ค.

 

 

 


  ํ•ด๋‹น ๋กœ์ง์—์„œ๋Š” ๋ฆฌ๋ทฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์–ด๋–ค ๊ฐ€๊ฒŒ์—์„œ ๋ฆฌ๋ทฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”์ง€ ์ถ”์ ํ•˜๊ณ , ํ•ด๋‹น ๊ฐ€๊ฒŒ์˜ ID๋งŒ์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ทธ ๊ฐ€๊ฒŒ์— ๋Œ€ํ•ด์„œ๋งŒ ํ†ต๊ณ„๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ๊ตฌํ˜„ํ•ด ๋ณด์•˜๋‹ค. ๋ฆฌ๋ทฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ๋ณ€๊ฒฝ๋œ ๊ฐ€๊ฒŒ์˜ ID๋งŒ์„ ๊ธฐ๋กํ•ด ๋‘๊ณ , ์Šค์ผ€์ค„๋Ÿฌ๋Š” ๊ทธ ๊ธฐ๋ก๋œ ๊ฐ€๊ฒŒ๋“ค๋งŒ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ์ฒ˜๋ฆฌํ•œ๋‹ค. ๋ฆฌ๋ทฐ๊ฐ€ ์ƒ์„ฑ ์ˆ˜์ • ์‚ญ์ œ๋  ๋•Œ๋งˆ๋‹ค trackStoreId ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ•ด๋‹น ๊ฐ€๊ฒŒ์˜ storeId๋ฅผ ๊ธฐ๋กํ•œ๋‹ค.

@Service
@RequiredArgsConstructor
public class ReviewService {

  private final ReviewRepository reviewRepository;
  private final OrderRepository orderRepository;
  private final StoreRepository storeRepository;
  private final ReviewStatisticsScheduler reviewStatisticsScheduler;

  @Transactional(readOnly = true)
  public Page<StoreReviewsResponseDto> getReviewsByStore(
      UUID storeId,
      int page,
      int size,
      String sortedBy,
      Direction direction
  ) {
    Pageable pageable = PageRequest.of(page, size, direction, sortedBy);
    Page<Review> reviewPage = reviewRepository.findAllByStoreId(storeId, pageable);

    Store store = findStore(storeId);

    // DB์— ์ €์žฅ๋œ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ์˜จ๋‹ค.
    Integer reviewCount = store.getReviewCount();
    BigDecimal ratingAvg = store.getRatingAvg();

    return reviewPage.map(review -> new StoreReviewsResponseDto(
        review.getId(), review.getUser().getId(),
        review.getOrder().getId(), review.getStar(), review.getContent(), ratingAvg, reviewCount));
  }

  @Transactional
  public ReviewResponseDto createReview(ReviewRequestDto requestDto, User user) {
    validateCustomer(user);
    Order order = findOrder(requestDto);

    validateOrderUser(user, order);

    // 3์ผ ์ œํ•œ ๊ฒ€์ฆ
    validateReviewPeriod(order);

    // ์ค‘๋ณต ๋ฆฌ๋ทฐ ์ฒดํฌ
    validateReviewDuplicate(requestDto);

    Review review = reviewRepository.save(new Review(requestDto, user, order.getStore(), order));

    // ๋ฆฌ๋ทฐ ์ƒ์„ฑ ์‹œ ํ•ด๋‹น ๊ฐ€๊ฒŒ ID ์ถ”์ 
    reviewStatisticsScheduler.trackStoreId(review.getStore().getId());

    return new ReviewResponseDto(review);
  }
}

 

 

 

 

  Redis๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์˜์กด์„ฑ๊ณผ Config class๋ฅผ ์ถ”๊ฐ€ํ•ด ์ค€ ๋’ค ์Šค์ผ€์ค„๋Ÿฌ ํด๋ž˜์Šค๋ฅผ ์ž‘์„ฑํ•ด ์ค€๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์‹ค์ œ ์Šค์ผ€์ค„๋ง ์ž‘์—…์„ ํ•  ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค. ํ•ด๋‹น ํด๋ž˜์Šค๋Š” @Component๋ฅผ ํ†ตํ•ด ์Šคํ”„๋ง ๋นˆ์— ๋“ฑ๋ก๋œ ํด๋ž˜์Šค์—ฌ์•ผ ํ•œ๋‹ค.

@Scheduled ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด Application Class์— @EnableScheduling์„ ์ถ”๊ฐ€ํ•ด ์ค€๋‹ค.

@SpringBootApplication
@EnableScheduling
public class DeliveryApplication {

  public static void main(String[] args) {
    SpringApplication.run(DeliveryApplication.class, args);
  }
}

 

 


  ์Šค์ผ€์ค„๋Ÿฌ ๋ฉ”์„œ๋“œ๋Š” void ํƒ€์ž…์ด์–ด์•ผ ํ•˜๋ฉฐ, ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋Š” ๋งค๊ฐœ๋ณ€์ˆ˜ ์‚ฌ์šฉ์ด ๋ถˆ๊ฐ€ํ•˜๋‹ค. Cron ํ‘œํ˜„์‹์œผ๋กœ ์ผ์ • ์ฃผ๊ธฐ๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ์Šค์ผ€์ค„๋Ÿฌ์— ๊ด€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์—ฌ๋Ÿฌ ํฌ์ŠคํŒ…์„ ์ฐธ๊ณ ํ•˜๊ธธ ๋ฐ”๋ž€๋‹ค. 

@Component
@RequiredArgsConstructor
@Slf4j
public class ReviewStatisticsScheduler {

  private final ReviewRepository reviewRepository;
  private final StoreRepository storeRepository;
  private final RedisTemplate<String, Object> redisTemplate;

  // Redis key ์„ค์ •
  private static final String UPDATED_STORE_ID_KEY = "review:updated_storeId";
  // TTL 1์‹œ๊ฐ„ ์„ค์ •
  private static final long TTL_IN_SECONDS = 3600;

  // ๋ฆฌ๋ทฐ ๋ณ€๊ฒฝ ์‹œ ํ˜ธ์ถœํ•˜์—ฌ ๋ณ€๊ฒฝ๋œ ๊ฐ€๊ฒŒ๋ฅผ ์ถ”์ 
  public void trackStoreId(UUID storeId) {
    log.info("trackStoreId ํ˜ธ์ถœ๋จ: {}", storeId);
    redisTemplate.opsForSet().add(UPDATED_STORE_ID_KEY, storeId.toString());
    log.info("Redis์— ์ €์žฅ ์š”์ฒญ: {}", storeId);
    redisTemplate.expire(UPDATED_STORE_ID_KEY, TTL_IN_SECONDS, TimeUnit.SECONDS);
  }

  // ์Šค์ผ€์ค„๋Ÿฌ์—์„œ ๋ณ€๊ฒฝ๋œ ๊ฐ€๊ฒŒ๋งŒ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ
  @Scheduled(cron = "0 */30 * * * *")  // 30๋ถ„ ๊ฐ„๊ฒฉ์œผ๋กœ ์—…๋ฐ์ดํŠธ
  public void updateReviewStatistics() {
    log.info("๋ฆฌ๋ทฐ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘");

    Optional<Set<Object>> updatedStoreIdsOptional = Optional.ofNullable(
        redisTemplate.opsForSet().members(UPDATED_STORE_ID_KEY));

    updatedStoreIdsOptional.ifPresent(updatedStoreIds -> {
      if (!updatedStoreIds.isEmpty()) {
        for (Object storeIdObj : updatedStoreIds) {
          UUID storeId = UUID.fromString(storeIdObj.toString());

          Integer reviewCount = reviewRepository.countByStoreId(storeId);
          BigDecimal ratingAvg = reviewRepository.calculateAverageRatingByStoreId(storeId);

          storeRepository.updateReviewStatistics(storeId, reviewCount, ratingAvg);
          log.info("๊ฐ€๊ฒŒ {} ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ", storeId);
        }
        // ์—…๋ฐ์ดํŠธ ํ›„ ์ถ”์  ๋ฆฌ์ŠคํŠธ ๋น„์šฐ๊ธฐ
        log.info("์‚ญ์ œ ์ „ ํ‚ค ์กด์žฌ ์—ฌ๋ถ€: {}", redisTemplate.hasKey(UPDATED_STORE_ID_KEY));
        redisTemplate.delete(UPDATED_STORE_ID_KEY);
        log.info("์‚ญ์ œ ํ›„ ํ‚ค ์กด์žฌ ์—ฌ๋ถ€: {}", redisTemplate.hasKey(UPDATED_STORE_ID_KEY));
      } else {
        log.info("๋ณ€๊ฒฝ๋œ ๊ฐ€๊ฒŒ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.");
      }
    });
  }
}

 

 

 

  ReviewRepository์—์„œ๋Š” ๋ฆฌ๋ทฐ์˜ ๊ฐœ์ˆ˜์™€ ํ‰์ ์„ ๊ณ„์‚ฐํ•˜๋Š” ์ฟผ๋ฆฌ๋ฅผ @Query ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด ์ง์ ‘ ์ž‘์„ฑํ•ด ์ค€๋‹ค. 

public interface ReviewRepository extends JpaRepository<Review, UUID> {

  @Query("SELECT r FROM Review r WHERE r.store.id = :storeId AND r.deletedAt IS NULL")
  Page<Review> findAllByStoreId(UUID storeId, Pageable pageable);

  @Query("SELECT COUNT(r) FROM Review r WHERE r.store.id = :storeId AND r.deletedAt IS NULL")
  Integer countByStoreId(UUID storeId);

  @Query("SELECT AVG(r.star) FROM Review r WHERE r.store.id = :storeId AND r.deletedAt IS NULL")
  BigDecimal calculateAverageRatingByStoreId(UUID storeId);

  boolean existsByOrderId(UUID orderId);
}

 

 

 

  StoreRepository์—์„œ๋Š” @Modifying ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ์กฐํšŒ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ณ , ๋ฐ”๋กœ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋„๋ก ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•ด ์ค€๋‹ค. ์—…๋ฐ์ดํŠธ ํ•ญ๋ชฉ์ด ์—†์„ ๊ฒฝ์šฐ์—๋Š” ์ฟผ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š๋Š”๋‹ค.

@Repository
public interface StoreRepository extends JpaRepository<Store, UUID> {

  @Query("SELECT s FROM Store s WHERE s.name LIKE %:keyword% AND s.deletedAt IS NULL ")
  Page<Store> findAllByName(String keyword, Pageable pageable);

  @Query("SELECT s FROM Store s WHERE s.category = :category AND s.deletedAt IS NULL")
  Page<Store> findAllByCategory(@Param("category") StoreCategory storeCategory, Pageable pageable);

  @Transactional
  @Modifying
  @Query("UPDATE Store s SET s.reviewCount = :reviewCount, s.ratingAvg = :ratingAvg WHERE s.id = :storeId")
  void updateReviewStatistics(UUID storeId, Integer reviewCount, BigDecimal ratingAvg);
}

 

 

 

  ์œ„ ๋กœ์ง์—์„œ๋Š” ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ๋™์ž‘ํ•˜๋Š” ์‹œ๊ฐ„์„ 30๋ถ„ ๊ฐ„๊ฒฉ์œผ๋กœ ์„ค์ •ํ•ด ๋‘์—ˆ์œผ๋‚˜, ๊ธฐ๋Šฅ์ด ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ๋” ์งง์€ ๊ฐ„๊ฒฉ์œผ๋กœ ์ˆ˜์ •ํ•˜์—ฌ ํ…Œ์ŠคํŠธํ•ด ๋ณด์•˜๋‹ค. ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

<๋ฆฌ๋ทฐ ๋ณ€๊ฒฝ์ด ์—†์„ ๊ฒฝ์šฐ>
<๋ฆฌ๋ทฐ ๋ณ€๊ฒฝ์ด ๊ฐ์ง€๋˜์—ˆ์„ ๊ฒฝ์šฐ>
<์—…๋ฐ์ดํŠธ ์‹œ์ž‘>
<ํ‚ค๊ฐ€ Redis์— ์ €์žฅ๋˜์—ˆ๋‹ค๊ฐ€ ์‚ญ์ œ๋œ๋‹ค.>

 

 

 

  ๋ฆฌ๋ทฐ๋ฅผ ์ž‘์„ฑํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ–ˆ์„ ๋•Œ, ๋ณ€๊ฒฝ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ ํ†ต๊ณ„๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ๋˜ํ•œ, ๊ฐ€๊ฒŒ๋ฅผ ์กฐํšŒํ–ˆ์„ ๊ฒฝ์šฐ์— ์—…๋ฐ์ดํŠธ๋œ ํ†ต๊ณ„๋ฅผ select ์ฟผ๋ฆฌ๋งŒ์œผ๋กœ ์กฐํšŒํ•ด ์˜ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ํ‰์  ๋ฐ ๊ฐœ์ˆ˜๋„ ์ œ๋Œ€๋กœ ๊ณ„์‚ฐ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

<๊ฐ€๊ฒŒ ์กฐํšŒ ๊ฒฐ๊ณผ>

 

<๊ฐ€๊ฒŒ ์กฐํšŒ์‹œ ์ƒ์„ฑ๋˜๋Š” ์ฟผ๋ฆฌ>

 

 

 

<๋กœ์ง์˜ ์ „์ฒด์ ์ธ ํ๋ฆ„>

 

 

 

  ์ด๋ ‡๊ฒŒ ๋ฆฌ๋ทฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ ํ•ด๋‹น ๊ฐ€๊ฒŒ๋ฅผ ์ถ”์ ํ•˜์—ฌ ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ํ•ด๋‹น ๊ฐ€๊ฒŒ๋“ค๋งŒ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ๋ฆฌํŒฉํ† ๋ง์„ ํ•ด๋ณด์•˜๋‹ค. ์—…๋ฐ์ดํŠธํ•  ํ•„์š”๊ฐ€ ์—†๋Š” ๊ฐ€๊ฒŒ๋Š” ์กฐํšŒํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ํ›จ์”ฌ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค. ๊ธฐ์กด์— 3๋ฒˆ์˜ ์ฟผ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ๋˜ ๊ฒƒ์„ ์ƒ๊ฐํ•ด ๋ณด๋ฉด ๋ถˆํ•„์š”ํ•œ ์กฐํšŒ ์—†์ด ์„ฑ๋Šฅ์ ์œผ๋กœ ๋‚˜์•„์ง„ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

 

 

 

 

 

์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” Redis์˜ Set๊ณผ ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ํ™œ์šฉํ•ด ์„ฑ๋Šฅ ๊ฐœ์„ ์„ ํ•ด๋ณด์•˜๋‹ค.

๋‹ค์Œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ฉ”์‹œ์ง€ ํ๋ฅผ ํ™œ์šฉํ•ด ๋ณผ ์ˆ˜ ์žˆ๋„๋ก ํ•ด์•ผ๊ฒ ๋‹ค.

 

 

 

 

 

 

 

 

 

๐Ÿ“Œ  ์ฐธ๊ณ 

 

 

[Spring][์‡ผํ•‘๋ชฐ ํ”„๋กœ์ ํŠธ][50] ์ƒํ’ˆ ํ…Œ์ด๋ธ” ํ‰๊ท  ํ‰์  ๋ฐ˜์˜

ํ”„๋กœ์ ํŠธ Github : https://github.com/sjinjin7/Blog_Project ํ”„๋กœ์ ํŠธ ํฌ์ŠคํŒ… ์ƒ‰์ธ(index) : https://kimvampa.tistory.com/188 ๋ชฉํ‘œ ์ƒํ’ˆ ํ…Œ์ด๋ธ” ํ‰์  ์ ์šฉ vam_bookํ…Œ์ด๋ธ”์— ratingAvg์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ๋Œ“๊ธ€ '๋“ฑ๋ก', '์ˆ˜์ •', '

kimvampa.tistory.com