@Service
@RequiredArgsConstructor
public class SearchBookUseCase {
private final KakaoApiClient kakaoApiClient;
public SearchBookResponse search(String query, String sort, int size, int page, String target) {
return SearchBookResponse.from(kakaoApiClient.searchBooks(query, sort, page, size, target));
}
}
현재는 단순히 요청을 받고, 외부 API를 호출한 후 그 데이터를 가공해서 응답하는 형태로만 구현이 되어있기 때문에 별도의 트랜잭션을 관리할 필요가 없어보입니다.
하지만, 요구사항이 변경되어 검색어 순위를 보고싶다면, 어떻게 구현을 해야할지 생각을 해보겠습니다. (MySQL 기준)
@Service
@RequiredArgsConstructor
public class SearchBookUseCase {
private final KakaoApiClient kakaoApiClient;
private final BookCountRepository bookCountRepository;
@Transactional
public SearchBookResponse search(String query, String sort, int size, int page, String target) {
bookCountRepository.findByQuery(query)
.ifPresentOrElse(
bookCount -> bookCountRepository.updateCount(bookCount.getId(), bookCount.getCount() + 1),
() -> bookCountRepository.save(new BookCount(query, 1))
);
return SearchBookResponse.from(kakaoApiClient.searchBooks(query, sort, page, size, target));
}
}
위와 같이 구현할 경우, 불필요하게 트랜잭션을 길게 잡아버릴 수 있는 문제점이 존재하기 때문에 분리하는 것을 고려해보면 좋다고 합니다.
퍼사드 패턴을 활용한 분리
퍼사드 패턴을 통해 외부 결합도를 낮춰주는 방식으로 적용할 수 있습니다.
SearchCountService.java
@Service
@RequiredArgsConstructor
public class SearchCountService {
private final BookCountRepository bookCountRepository;
@Transactional
public void incrementSearchCount(String query) {
bookCountRepository.findByQuery(query)
.ifPresentOrElse(
bookCount -> bookCountRepository.updateCount(bookCount.getId(), bookCount.getCount() + 1),
() -> bookCountRepository.save(new BookCount(query, 1))
);
}
}
SearchBookFacade.java
@Service
@RequiredArgsConstructor
public class SearchBookFacade {
private final KakaoApiClient kakaoApiClient;
private final SearchCountService searchCountService;
public SearchBookResponse search(String query, String sort, int size, int page, String target) {
// 외부 API 호출 먼저 수행
SearchBookResponse response = SearchBookResponse.from(
kakaoApiClient.searchBooks(query, sort, page, size, target)
);
// DB 작업은 별도로 수행
searchCountService.incrementSearchCount(query);
return response;
}
}
장점
구현이 단순하고 직관적
실행 순서가 명확함
동기적 처리가 필요한 경우에 적합
단점:
여전히 같은 요청 내에서 처리되므로 전체 응답 시간은 동일
이벤트를 활용한 분리
트랜잭션 완료 후 비동기적으로 처리해도 되는 작업인 경우, 이벤트를 고려해볼 수 있습니다.
SearchCountEventListener.java
@Component
@RequiredArgsConstructor
public class SearchCountEventListener {
private final SearchCountService searchCountService;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleSearchEvent(SearchPerformedEvent event) {
searchCountService.incrementSearchCount(event.getQuery());
}
}
SearchBookService.java
@Service
@RequiredArgsConstructor
public class SearchBookService {
private final KakaoApiClient kakaoApiClient;
private final ApplicationEventPublisher eventPublisher;
public SearchBookResponse search(String query, String sort, int size, int page, String target) {
SearchBookResponse response = SearchBookResponse.from(
kakaoApiClient.searchBooks(query, sort, page, size, target)
);
// 이벤트 발행
eventPublisher.publishEvent(new SearchPerformedEvent(query));
return response;
}
}