🌈 댓글 등록 성능테스트
웹툰 도메인의 특징에는 웹툰이 게시되는 특정 시간대에 댓글 등록 요청 트래픽이 높아질 수 있다고 생각하였다.
많은 요구사항을 처리하기 위해서는 댓글 등록 요청에 대한 응답시간을 줄이고, 최대한 많은 요청에 대해 커넥션이 연결되어야 한다.
요구사항에는 댓글 등록 시 비속어가 검출되면 사용자 경고를 누적하는 기능이 포함 되어 있다. 비속어 검출 작업은 어떤 알고리즘 혹은 외부API를 사용하는지에 따라 시간이 오래 소요되는 작업이라고 생각하였고, 편의상 약 3초가 소요되도록 로직을 구성하였다.
테스트 도구는 빠르고 직관적으로 결과를 확인할 수 있도록 Locust를 활용하였고,아래 처럼 구성하여 간단하게 성능테스트를 진행하였다.
테스트 조건
100명의 사용자가 댓글 등록을 요청하는 상황
진행시간 : 3분(180초)
1~10초 사이에는 웜업시간으로 초당 10명씩 증가시켰다.
10~180초 사이에는 100명의 사용자가 댓글 요청을 보내는 상황이다.
결과는 초당 4.3개의 요청 처리
그리고 몇몇 요청은 DB커텍션 시관초과로 실패하였다.(약 30초 이상 기다리는 요청에 대해서 DB 커넥션 에러가 발생함)
평균 응답시간은 약 25초가 소요되었다.
결과적으로 요청에 3초가 소요되는 작업이 있기 때문에 계쏙해서 요청이 밀리고 DB커넥션이 대기하는 상황이 생기면서 평균 응답시간이 증가하고, DB커넥션 에러도 발생하는 것을 알 수 있다.
2024-05-08T23:08:55.158+09:00 ERROR 2037 --- [devtoon] [o-8080-exec-390] o.h.engine.jdbc.spi.SqlExceptionHelper : HikariPool-1 - Connection is not available, request timed out after 30001ms.
2024-05-08T23:08:55.160+09:00 ERROR 2037 --- [devtoon] [o-8080-exec-389] y.d.c.exception.GlobalControllerAdvice : Error occurs org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction
위와 같은 상황에서 비속어 검출 로직에 비동기 방식을 적용해 보고자 한다.
🌈 스프링 비동기 적용해보기
직접 자바의 스레드풀을 선언하고 적용하는 것보다는, 스프링에서 제공해주는 @Async를 활용하여 비동기 동작을 구현해보고자 하였다.
사용방법은 아래와 같이 간단하다.
1. 스프링 @Configuration 파일에 @EnableAsync를 추가해준다.
2. @Configuration 파일에 @Async에서 사용할 스페드풀을 정의해준다.
<기존의 댓글 등록 서비스 로직>
// --------------- 댓글 등록 서비스 로직 ----------------------
@Transactional
public void create(final CommentCreateRequest request) {
// 1. 비속어 검사 -> 비속어 존재하면 비속어 카운트 테이블에 경고 누적
validateBadWords(request);
// 2. 댓글을 게시할 웹툰 조회(존재하는지 확인)
WebtoonEntity webtoon = webtoonService.retrieve(request.getWebtoonId());
// 3. 댓글 Entity 생성 후 저장
CommentEntity comment = CommentEntity.create(
webtoon.getId(),
request.getDetailId(),
request.getWebtoonViewerId(),
request.getContent()
);
commentRepository.save(comment);
}
private void validateBadWords(final CommentCreateRequest request) {
int badWordsCount = badWordsDetector.detectUsingExternalApi(request.getContent());
if (badWordsCount > 0) {
badWordsWarningCountService.increase(request.getWebtoonViewerId());
}
}
// ----------- 비동기 서비스 로직 --------------
@Component
public class BadWordsDetector {
public int detectUsingExternalApi(String content) {
// 비속어 검출 : 시간이 오래 소요 되는 작업(임의로 3초 소요 되도록 구성)
try {
Thread.sleep(3000);
}catch (Exception e) {
log.info(e.getMessage());
}
return 0;
}
}
<비동기를 적용한 서비스 로직>
@Transactional
public void create(final CommentCreateRequest request) {
log.info("현재 create() 호출 스레드: {}", Thread.currentThread().getName());
/**
* @Async를 활용한 비동기 동작
*/
badWordsDetector.validateBadWords(request);
WebtoonEntity webtoon = webtoonService.retrieve(request.getWebtoonId());
CommentEntity comment = CommentEntity.create(
webtoon.getId(),
request.getDetailId(),
request.getWebtoonViewerId(),
request.getContent()
);
commentRepository.save(comment);
}
@Slf4j
@RequiredArgsConstructor
@Component
public class BadWordsDetector {
private final BadWordsWarningCountService badWordsWarningCountService;
@Async("EventThreadPool")
public void validateBadWords(final CommentCreateRequest request) {
int badWordsCount = detectUsingExternalApi(request.getContent());
if (badWordsCount > 0) {
badWordsWarningCountService.increase(request.getWebtoonViewerId());
}
}
private int detectUsingExternalApi(String content) {
// 비속어 검출 : 시간이 오래 소요 되는 작업
try {
Thread.sleep(3000);
} catch (Exception e) {
log.info(e.getMessage());
}
return 1;
}
}
비동기로 변경 후 성능테스트 결과
실패한 요청은 없었다.
평균 응답시간은 약 200ms
초당 요청 성공 횟수는 669.6 이었고 Database에 86571개의 댓글 데이터가 생성되었다.
🌈 주의 사항
@Async를 사용해서 비동기 처리를 하였고, 올바르게 동작하는지 확인하였다.
@Async를 사용할때에는 아래와 같은 주의사항이 있었다.
- @Async를 사용하면 별도의 스레드를 생성하여 동작하기 때문에, 예외가 발생하더라도 Spring에서 잡아주지 못한다는 특징이 있다.
- 같은 클래스 내부에서 @Async를 호출하면 비동기로 동작하지 않는다.
이 밖에도 몇몇 특징이 더 있는데 @Async가 내부적으로 어떻게 동작하는지를 잘 알고 있어야 프로그래밍할 때 실수하지 않을 수 있다.
🌈 더 나아가기
현재는 @Async를 활용하여 프로젝트에 적용 시켜보았다.
이제는 사용한 기술들에 대해서 조금 더 깊게 알아보기 위해서 공부 할 내용들을 정리해본다.
- @Async 동작 방식 알아보기
- Spring AOP 알아보기
- 스프링 프록시 객체 알아보기
- 스레드 풀
'프로젝트 > 데브툰' 카테고리의 다른 글
[데브툰] CI 적용하기 (0) | 2024.07.16 |
---|---|
[프로젝트] @Async를 알아보자(feat. 빈 후처리기) (0) | 2024.05.08 |
[프로젝트] @Async를 알아보자(feat. Spring과 Proxy) (0) | 2024.05.07 |
[프로젝트] @Async를 알아보자(feat. Spring AOP) (0) | 2024.05.06 |
[프로젝트] 프로젝트 목표 및 기획 (0) | 2024.04.30 |