먼저, 이 글에서는 Spring Events를 활용한 코드 리팩터링을 다룹니다. 하지만 Spring Events가 익숙하지 않은 분들은 아래 링크를 통해 기본 개념을 먼저 확인하시면 이해에 도움이 될 것입니다:
➡️ Spring Events란? - Gahacman의 블로그
저의 코드는 크롤링중에 새로운 기사가 크롤링 된다면 알림서비스로 새로운 기사가 발행되었다고 알림을 보냅니다.
이후 데이터베이스에 새로운 기사들을 저장합니다.
기존의 코드의 문제점
package com.hnptech.stocknewscuckoo.article.service;
import com.hnptech.stocknewscuckoo.article.dto.response.ArticleResponse;
import com.hnptech.stocknewscuckoo.article.mapper.ArticleMapper;
import com.hnptech.stocknewscuckoo.article.model.Article;
import com.hnptech.stocknewscuckoo.article.repository.ArticleRepository;
import com.hnptech.stocknewscuckoo.notification.notifier.Notifier;
import com.hnptech.stocknewscuckoo.notification.service.NotificationService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ArticleServiceImpl implements ArticleService {
private final ArticleRepository articleRepository;
private final ArticleMapper articleMapper;
private final NotificationService notificationService;
@Override
public List<ArticleResponse> getLatestArticles() {
return articleMapper.toResponseList(articleRepository.findTop20ByOrderByPublishedAtDesc());
}
@Override
public void saveArticles(List<Article> articles) {
List<Article> notDuplicatedArticles = articles.stream()
.filter(article -> !articleRepository.existsById(article.getUrl()))
.toList();
// 알림서비스
for (Article article : notDuplicatedArticles) {
notificationService.sendNotification(article);
}
articleRepository.saveAll(notDuplicatedArticles);
}
}
- 로직의 결합도가 높음
- saveArticles 메서드 내에 기사 저장 로직과 알림 로직이 함께 포함되어 있어 단일 책임 원칙(SRP)을 위반하고 있습니다.
- 이로 인해 로직 간의 강한 결합도가 발생하며, 코드의 유지보수성과 확장성이 저하됩니다.
- 동기 방식으로 인한 성능 문제
- 알림 전송 로직이 동기적으로 처리되기 때문에, 알림 전송에 시간이 오래 걸릴 경우 saveArticles 메서드 전체가 대기 상태에 들어갑니다.
- 이는 서비스의 성능 저하로 이어질 수 있습니다.
- 테스트 작성의 어려움
- 비즈니스 로직(기사 저장)과 알림 로직이 결합되어 있어, 기사 저장 로직을 테스트하려면 알림 로직까지 모킹(mocking)하거나 테스트 환경을 구성해야 합니다.
- 이로 인해 단위 테스트 작성이 복잡해지고 테스트의 신뢰성을 확보하기 어렵습니다.
해결 과정
리팩터링: Spring Events 적용
Spring Events를 활용하여 문제를 해결하였습니다. 이벤트 기반 구조로 분리함으로써 각 로직의 결합도를 줄이고 비동기 처리를 통해 성능을 개선하였습니다.
1. 이벤트 클래스 정의
이벤트 데이터를 담을 ArticleCreatedEvent와 CrawlerFetchedEvent 클래스를 작성하였습니다.
public record ArticleCreatedEvent(List<Article> articles) implements Event {
}
2. 이벤트 퍼블리셔 (Publisher) 구현
이벤트를 발행하는 클래스를 작성하여, 비즈니스 로직에서 이벤트 발행을 간단히 수행할 수 있도록 설계했습니다.
@Component
@RequiredArgsConstructor
public class ArticleEventPublisher {
private final EventPublisher eventPublisher;
public void publishArticleCreated(List<Article> articles) {
ArticleCreatedEvent event = new ArticleCreatedEvent(articles);
eventPublisher.publish(event);
}
}
3. 이벤트 리스너 (Listener) 구현
리스너를 통해 알림 전송 로직을 분리하고 비동기 방식으로 처리하도록 설정하였습니다.
@Component
@RequiredArgsConstructor
@Slf4j
public class ArticleEventListener {
private final NotificationService notificationService;
@Async
@EventListener
public void handleArticleCreatedEvent(@NonNull ArticleCreatedEvent event) {
log.info("알림 보내기 성공");
notificationService.sendNotification(event);
}
}
최종 리팩토링 코드
@Override
public void saveArticles(CrawlerFetchedEvent articles) {
// 중복 검사
List<Article> notDuplicatedArticles = articles.articles().stream()
.filter(article -> !articleRepository.existsById(article.getUrl()))
.toList();
// 이벤트 발행
articleEventPublisher.publishArticleCreated(notDuplicatedArticles);
// 데이터 저장
articleRepository.saveAll(notDuplicatedArticles);
}
'Spring' 카테고리의 다른 글
[KAFKA] Spring Boot로 KAFKA 사용해보기 (0) | 2025.04.02 |
---|---|
[해결 방안] Spring STOMP content-length 초과 에러 해결하기 (0) | 2025.03.31 |
[해결 방안] Spring Gateway를 통해 Stomp를 설정했을때 헤더가 두개오는 문제 해결방안 (1) | 2025.03.31 |
[Spring] MongoDB와 JPA Repository 충돌 해결 (0) | 2025.02.09 |
[Spring] Spring Events 이란? (2) | 2024.12.22 |