Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] 베스트 셀러 Redis 조회 기능 및 스케쥴링 구현 #55

Merged
merged 13 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/jisungin_dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,16 @@ jobs:
# SSH Key로 서버에 접속하고 docker-compose image를 pull 받고 실행하기
- name: Access Server with SSH Key, pull and execute docker-compose image
uses: appleboy/[email protected]
env:
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.KEY }}
port: 22
script: |
cd jisungin
echo "REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" > .env
sudo docker-compose down
sudo docker-compose pull
sudo docker-compose up -d
Expand Down
11 changes: 11 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ dependencies {
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
// Spring Data JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Spring Data Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// Spring Retry
implementation 'org.springframework.retry:spring-retry'
// Spring Aspects
implementation 'org.springframework:spring-aspects'
// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
// Spring Web
Expand All @@ -60,6 +66,11 @@ dependencies {
implementation 'org.jsoup:jsoup:1.16.2'
// JsonPath Parse Json Library
implementation 'com.jayway.jsonpath:json-path:2.9.0'
// Json Date Time Formatter
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
// Test Container
testImplementation "org.junit.jupiter:junit-jupiter:5.8.1"
testImplementation "org.testcontainers:junit-jupiter:1.17.3"
}

tasks.named('bootBuildImage') {
Expand Down
11 changes: 11 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ services:
networks:
- my_network

redis:
image: redis
container_name: redis-cache
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD}
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}", "--port", "6379"]
ports:
- 6379:6379
networks:
- my_network

networks:
my_network:
driver: bridge
6 changes: 6 additions & 0 deletions src/main/java/com/jisungin/JisunginApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableCaching
@EnableScheduling
@EnableRetry
@ConfigurationPropertiesScan
public class JisunginApplication {
Comment on lines 10 to 15
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

애플리케이션 실행 시 캐싱, 스케줄링, 리트라이가 적용되는 건가요 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Enablexxx 어노테이션들은 관련된 라이브러리의 기능을 활성화하는 역할 입니다!


Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/jisungin/api/book/BookController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

import com.jisungin.api.ApiResponse;
import com.jisungin.api.book.request.BookCreateRequest;
import com.jisungin.api.book.request.BookPageRequest;
import com.jisungin.application.PageResponse;
import com.jisungin.application.book.BestSellerService;
import com.jisungin.application.book.BookService;
import com.jisungin.application.book.response.BestSellerResponse;
import com.jisungin.application.book.response.BookResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -19,12 +24,18 @@
public class BookController {

private final BookService bookService;
private final BestSellerService bestSellerService;

@GetMapping("/books/{isbn}")
public ApiResponse<BookResponse> getBook(@PathVariable("isbn") String isbn) {
return ApiResponse.ok(bookService.getBook(isbn));
}

@GetMapping("/books/best-seller")
public ApiResponse<PageResponse<BestSellerResponse>> getBestSellers(@ModelAttribute BookPageRequest page) {
return ApiResponse.ok(bestSellerService.getBestSellers(page.toServiceRequest()));
}

@PostMapping("/books")
public ApiResponse<BookResponse> createBook(@RequestBody @Valid BookCreateRequest request) {
return ApiResponse.ok(bookService.createBook(request.toServiceRequest()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand Down Expand Up @@ -67,7 +66,7 @@ private LocalDateTime convertToLocalDateTime(String dateTime) {
}

private String convertToString(String[] authors) {
return Arrays.toString(authors);
return String.join(", ", authors);
}

}
30 changes: 30 additions & 0 deletions src/main/java/com/jisungin/api/book/request/BookPageRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.jisungin.api.book.request;

import com.jisungin.application.book.request.BookServicePageRequest;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class BookPageRequest {

Integer page;
Integer size;

@Builder
private BookPageRequest(Integer page, Integer size) {
this.page = page != null ? page : 1;
this.size = size != null ? size : 5;
}

public BookServicePageRequest toServiceRequest() {
return BookServicePageRequest.builder()
.page(page)
.size(size)
.build();
}

}
35 changes: 35 additions & 0 deletions src/main/java/com/jisungin/application/book/BestSellerService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.jisungin.application.book;

import com.jisungin.application.PageResponse;
import com.jisungin.application.book.event.BestSellerUpdatedEvent;
import com.jisungin.application.book.request.BookServicePageRequest;
import com.jisungin.application.book.response.BestSellerResponse;
import com.jisungin.domain.book.repository.BestSellerRepository;
import com.jisungin.infra.crawler.Crawler;
import com.jisungin.infra.crawler.CrawlingBook;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class BestSellerService {

private final Crawler crawler;
private final BestSellerRepository bestSellerRepository;
private final ApplicationEventPublisher eventPublisher;

public PageResponse<BestSellerResponse> getBestSellers(BookServicePageRequest page) {
return bestSellerRepository.findBestSellerByPage(page);
}


public void updateBestSellers() {
Map<Long, CrawlingBook> crawledBooks = crawler.crawlBestSellerBook();

bestSellerRepository.updateAll(crawledBooks);
eventPublisher.publishEvent(new BestSellerUpdatedEvent(crawledBooks));
}

}
13 changes: 11 additions & 2 deletions src/main/java/com/jisungin/application/book/BookService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.jisungin.exception.BusinessException;
import com.jisungin.exception.ErrorCode;
import com.jisungin.infra.crawler.Crawler;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -36,9 +37,17 @@ public BookResponse createBook(BookCreateServiceRequest request) {
throw new BusinessException(ErrorCode.BOOK_ALREADY_EXIST);
}

request.addCrawlingData(crawler.crawlBook(request.getIsbn()));
BookCreateServiceRequest newServiceRequest = crawler.crawlBook(request.getIsbn()).toServiceRequest();

return BookResponse.of(bookRepository.save(request.toEntity()));
return BookResponse.of(bookRepository.save(newServiceRequest.toEntity()));
}

@Transactional
public void addNewBooks(List<BookCreateServiceRequest> requests) {
requests.stream()
.filter(request -> !bookRepository.existsBookByIsbn(request.getIsbn()))
.map(BookCreateServiceRequest::toEntity)
.forEach(bookRepository::save);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.jisungin.application.book.event;

import com.jisungin.infra.crawler.CrawlingBook;
import java.util.Map;
import lombok.Getter;

@Getter
public class BestSellerUpdatedEvent {

private final Map<Long, CrawlingBook> crawledBooks;

public BestSellerUpdatedEvent(Map<Long, CrawlingBook> crawledBooks) {
this.crawledBooks = crawledBooks;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.jisungin.application.book.event;

import com.jisungin.application.book.BookService;
import com.jisungin.application.book.request.BookCreateServiceRequest;
import com.jisungin.infra.crawler.CrawlingBook;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class BestSellerUpdatedEventListener {

private final BookService bookService;

@EventListener
public void handleBestSellerUpdatedEvent(BestSellerUpdatedEvent event) {
Map<Long, CrawlingBook> crawledBook = event.getCrawledBooks();

List<BookCreateServiceRequest> bookCreateServiceRequests = crawledBook.values().stream()
.map(CrawlingBook::toServiceRequest)
.toList();

bookService.addNewBooks(bookCreateServiceRequests);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,4 @@ public Book toEntity() {
.build();
}

public void addCrawlingData(CrawlingBook crawlingBook) {
this.imageUrl = crawlingBook.getImageUrl();
this.contents = crawlingBook.isBlankContent() ? this.contents : crawlingBook.getContent();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.jisungin.application.book.request;

import lombok.Builder;
import lombok.Getter;

@Getter
public class BookServicePageRequest {

private Integer page;
private Integer size;

@Builder
private BookServicePageRequest(Integer page, Integer size) {
this.page = page;
this.size = size;
}

public Integer extractStartIndex() {
return (page * size) - size + 1;
}

public Integer extractEndIndex() {
return page * size;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.jisungin.application.book.response;

import java.time.LocalDateTime;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class BestSellerResponse {

private Long ranking;
private String isbn;
private String title;
private String publisher;
private String thumbnail;
private String[] authors;
private LocalDateTime dateTime;

@Builder
private BestSellerResponse(Long ranking, String isbn, String title, String publisher, String thumbnail,
String[] authors, LocalDateTime dateTime) {
this.ranking = ranking;
this.isbn = isbn;
this.title = title;
this.publisher = publisher;
this.thumbnail = thumbnail;
this.authors = authors;
this.dateTime = dateTime;
}

public static BestSellerResponse of(Long ranking, String isbn, String title, String publisher, String thumbnail,
String[] authors, LocalDateTime dateTime) {
return BestSellerResponse.builder()
.ranking(ranking)
.isbn(isbn)
.title(title)
.publisher(publisher)
.thumbnail(thumbnail)
.authors(authors)
.dateTime(dateTime)
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ private BookResponse(String title, String content, String isbn, String publisher
this.publisher = publisher;
this.imageUrl = imageUrl;
this.thumbnail = thumbnail;
this.authors = convertAuthorsToString(authors);
this.authors = convertAuthorsToArr(authors);
this.ratingAverage = parseRatingAverage(ratingAverage);
this.dateTime = dateTime;
}
Expand Down Expand Up @@ -62,8 +62,8 @@ public static BookResponse of(Book book, Double ratingAverage) {
.build();
}

private String[] convertAuthorsToString(String authors) {
return authors.split(", ");
private String[] convertAuthorsToArr(String authors) {
return authors.split(",");
}

private Double parseRatingAverage(Double ratingAverage) {
Expand Down
Loading
Loading