Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |
Tags
- MySQL
- 맥
- VI
- 이진탐색
- 동적 프로그래밍 방법
- 순차탐색
- mysql 표
- mycli
- 욕심쟁이 방법
- CS스터디
- 분할정복 방법
- 알고리즘
- 오일러 경로
- 터미널
- cs
- spring boot
- 스터디2기
- mysql 표 출력
- Pager
- 인프런워밍업클럽
- 티스토리챌린지
- table status
- 인프런
- zsh
- oh-my-zsh
- 데이크스트라
- zsh theme
- 오블완
- 네트워킹데이
- Less
Archives
- Today
- Total
Develop
[Spring Boot] 게시판 - 19. 비동기 처리로 사용자 응답 속도 개선하기 (feat. K6 성능 테스트) 본문
백엔드/게시판 만들기
[Spring Boot] 게시판 - 19. 비동기 처리로 사용자 응답 속도 개선하기 (feat. K6 성능 테스트)
230801 2025. 12. 30. 05:09안녕하세요~ .ᐟ
회원가입 API를 테스트하다가 문제를 발견했습니다.
이메일 인증 코드를 전송하는데 3.5초나 걸려서, 사용자가 회원가입 버튼을 누르고 3.5초 동안 기다려야 하는 상황이었습니다.
오늘은 이 문제를 Spring의 @Async를 사용해 해결하고, K6로 성능을 측정한 내용을 작성해보겠습니다.
결과 요약 : 3.5초 -> 0.08초로 개선함
문제 상황
: 회원가입 시 인증코드를 이메일로 발송하는 로직이 느림
// 동기 방식
회원가입 요청 → 인증코드 생성 → DB저장 → 이메일전송(5초 대기) → 응답
ex) 응답 시간: 5초
// 비동기 방식
회원가입 요청 → 인증코드 생성 → DB저장 → 이메일전송 요청 → 즉시 응답
↓ (백그라운드)
실제 전송 (5초)
ex) 응답 시간: 0.5초
구현
: 이메일 발송 로직을 백그라운드에서 비동기로 처리하고, 사용자에게 바로 응답하는 방식으로 변경
1. @Async 설정
- 이메일 전송처럼 시간이 오래 걸리는 작업을 별도 스레드에서 처리해서 메인 로직의 응답 속도를 개선해줍니다.
- 이를 통해 사용자는 이메일 전송 완료를 기다리지 않아도 됩니다.
더보기
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2); // 기본 스레드 수
executor.setMaxPoolSize(5); // 최대 스레드 수
executor.setQueueCapacity(100); // 대기 큐 크기
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncExceptionHandler();
}
}
# ThreadPool 설정 이유
- CorePoolSize(2):
- 이메일 발송은 I/O 작업 (CPU 많이 안 씀)
- 평소 2개면 충분 (동시 회원가입 많지 않음)
- MaxPoolSize(5):
- 갑자기 회원가입 몰려도 5개까지 처리
- 너무 많으면 메모리/DB 연결 고갈 위험
- QueueCapacity(100):
- 5개 스레드 모두 바쁘면 100개까지 대기
- 초과 시 RejectedExecutionException 발생
- 비동기 작업에 사용할 스레드 풀 설정
- @Async 어노테이션 활성화
- 스레드 풀 크기, 큐 크기 등 세부 설정
2. EmailService를 비동기로 변경
- SMTP 서버 연결은 네트워크 I/O 작업으로 느리므로 EmailService에서 이메일 발송 로직을 비동기로 변경합니다.
- 기존 동기 메서드에 @Async 추가
- 별도 스레드에서 실행되도록 변경
더보기
@Service
@RequiredArgsConstructor
public class EmailService {
@Async // ← 어노테이션 추가
public void sendVerificationEmail(String toEmail) {
// 기존 코드 그대로
}
}
* @Async 사용 시 주의사항
- 동작하는 경우
- public 메서드
- 다른 Bean 에서 호출
- 동작하지 않는 경우
- private 메서드
- 같은 클래스 내부 호출
- 반환값이 필요한 경우 -> 반환값에 CompletableFuture 사용
3. 비동기 예외 처리
- 비동기 메서드에서 발생한 예외는 호출자가 받을 수 없기 때문에, 별도 예외 핸들러를 생성했습니다.
- 비동기 작업 중 예외 로깅
- 필요시 재시도 또는 알림
- 사용자 회원가입 응답이 이메일 전송 완료까지 기다릴 필요가 없으므로 회원가입은 완료되지만, 계정은 비활성 상태로 처리합니다.
- 인증메일 발송 실패의 경우 나중에 '인증 메일 재발송' 기능으로 해결이 가능합니다.
더보기
@Component
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
log.error("비동기 작업 예외 발생 - 메서드: {}, 예외: {}",
method.getName(), ex.getMessage(), ex);
}
}
4. @Scheduled로 정기 작업 구현
더보기
App - 스케쥴러 활성화
@SpringBootApplication
@EnableScheduling // 추가
public class BoardApplication {
public static void main(String[] args) {
SpringApplication.run(BoardApplication.class, args);
}
}
스케쥴러
@Slf4j
@Service
@RequiredArgsConstructor
public class ScheduledTasks {
private final EmailVerificationRepository tokenRepository;
@Transactional
@Scheduled(cron = "0 0 3 * * *")
public void cleanupExpiredTokens() {
LocalDateTime now = LocalDateTime.now();
long expiredCount = tokenRepository.countByExpiryDateBefore(now);
if (expiredCount > 0) {
tokenRepository.deleteByExpiryDateBefore(now);
log.info("만료된 이메일 인증 토큰 {}개 삭제 완료: {}", expiredCount, now);
} else {
log.debug("삭제할 만료 토큰 없음: {}", now);
}
}
// @Scheduled(fixedRate = 10000)
// public void healthCheck() {
// log.info("스케줄러 동작 확인: {}", LocalDateTime.now());
// };
}
Repository
// 인증기간 만료토큰 개수 세기
long countByExpiryDateBefore(LocalDateTime dateTime);
// 인증기간 만료토큰 삭제
@Modifying
@Transactional
@Query("DELETE FROM EmailVerification e WHERE e.expiryDate < :dateTime")
void deleteByExpiryDateBefore(@Param("dateTime") LocalDateTime dateTime);
왜 필요한가?
- 만료된 인증 토큰 정기적으로 삭제 (DB 정리)
- 매일 자정 통계 집계
- 배치 작업 자동화
무슨 역할인가?
- cron 표현식으로 실행 시간 지정
- 스프링이 자동으로 스케줄링
cron 표현식
초 분 시 일 월 요일
0 0 3 * * * = 매일 3시 정각
0 */10 * * * * = 10분마다
TEST
1. IntelliJ HTTP Client로 측정
- 비동기 처리 후 응답속도 개선 ( 3,544 ms -> 79ms (약 97.8%) )


- 한번 더 테스트해보니 값이 달라졌습니다 .ᐟ
- 아래와 같은 요인들로 값이 변동될 수 있다고 하니 참고해주세요!
- JVM 워밍업 상태
- DB 커넥션 풀 상태
- OS 레벨 캐시
- SMTP 연결 상태

2. K6로 부하 테스트 진행
K6 란?
K6는 Grafana에서 만든 성능 테스트 도구입니다.
사용 방법
- JavaScript로 테스트 시나리오 작성
- 테스트 결과는 .json / .txt 파일로 추출 가능하며, K6 Cloud 웹에서 그래프로 볼 수 있습니다!
- 저는 포트폴리오를 생각해서 K6 Cloud로 시각화 하는방법을 공부하기로 했습니다.
K6 설치
더보기
brew install k6
웹에서 Grafana 설정하기
더보기

1. Grafana 접속 + 계정 생성
2. 좌측 메뉴바 - Teseting & synthetics 선택

3. Project -> Performance 클릭
4. 온보딩 가이드 -> Run a test from the CLI -> 2번 토큰 로그인 명령어 -> 터미널 복붙
k6 cloud login --token 본인의 토큰
성능 테스트 환경
더보기
하드웨어
- MacBook Air 15 (Apple M2)
- RAM: 16GB
소프트웨어
- Java 21.0.6
- Spring Boot 3.5.5
- MySQL 8.0 (로컬)
- HikariCP: max-pool-size=20
제약 사항
- 로컬 환경 (네트워크 지연 없음)
- 단일 서버 (로드밸런서 없음)
- 로컬 DB (네트워크 레이턴시 없음)
- MacBook Air 15 (Apple M2)
- RAM: 16GB
소프트웨어
- Java 21.0.6
- Spring Boot 3.5.5
- MySQL 8.0 (로컬)
- HikariCP: max-pool-size=20
제약 사항
- 로컬 환경 (네트워크 지연 없음)
- 단일 서버 (로드밸런서 없음)
- 로컬 DB (네트워크 레이턴시 없음)
테스트 시나리오 작성하기
- 저는 K6로 아래 4개 테스트를 단계별로 진행했습니다.
Functional Test (5VUs, 기능 테스트)
Load Test (50VUs, 부하 테스트)
Stress Test (100Vus, 스트레스 테스트)
Spike Test (급증 테스트)
* 시나리오 작성시 주의사항
- 회원가입 테스트를 여러번 돌려야하므로, 유저 정보가 중복되지않도록 payload 에 세팅합니다.
- K6 Cloud 무료플랜은 VU를 100명까지 제한하기 때문에, 시나리오에 target을 100 까지 테스트할 수 있습니다.
export default function () {
const url = 'http://localhost:8080/auth/signup';
const uniqueEmail = `load-${__VU}-${__ITER}-${Date.now()}@test.com`;
const payload = JSON.stringify({
email: uniqueEmail,
password: 'Test1234!@',
nickname: `load-${__VU}`,
name: '부하테스트',
});
* 테스트 시 주의사항
- 배포가 안된 로컬 시나리오는 K6 Cloud 에서 테스트를 위해 접속할 수 없습니다.
- 저는 로컬에서 먼저 돌려보고 배포하고 싶어서 로컬에서 테스트를 run 하고 결과를 Cloud로 out 해서 결과를 확인하는 방법을 사용했습니다.
- 실행 명령어
# k6 run --out cloud 경로/시나리오명.js
k6 run --out cloud src/test/k6/functional/signup-test.js
시나리오 예시
더보기
부하테스트.js
import http from 'k6/http';
import { sleep, check } from 'k6';
// 트래픽 시뮬레이션
export const options = {
stages: [
{ duration: '30s', target: 20 }, // 20명으로 증가
{ duration: '1m', target: 50 }, // 50명 유지
{ duration: '30s', target: 0 }, // 종료
],
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.1'],
},
};
export default function () {
const url = 'http://localhost:8080/auth/signup';
const uniqueEmail = `load-${__VU}-${__ITER}-${Date.now()}@test.com`;
const payload = JSON.stringify({
email: uniqueEmail,
password: 'Test1234!@',
nickname: `load-${__VU}`,
name: '부하테스트',
});
const res = http.post(url, payload, {
headers: { 'Content-Type': 'application/json' },
});
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
테스트 결과 - 종합
- 총 25,000개 이상 요청 처리 - 실패 0건 (100% 성공률)
- Peak RPS: 64 req/s (초당 최대 64개 응답 처리)
- 100 VUs까지 안정적으로 처리 (100개의 가상사용자가 스크립트 실행 -> sleep -> 반복)
| 테스트명 | VUs | 총 요청 | 성공률 | P95 응답시간 |
| Functional | 5 | 10 | 100% | 371ms |
| Load | 50 | 2.8K | 100% | 221ms |
| Stress | 100 | 18.1K | 100% | 721ms |
| Spike | 100 | 4.3K | 100% | 909ms |
테스트 결과 - 상세
더보기




1. 테스트 결과 - Signup 기능 동작 Test

2. 테스트 결과 - load Test

3. 테스트 결과 - Stress Test

4. 테스트 결과 - Spike Test

오늘 진행한 K6 테스트의 한계점
- 회원가입 API 만 테스트 함
- 실제론 사용자 행동패턴에 따른 테스트를 진행한다고 한다.
- 예를들면 '회원가입 -> 로그인 -> 게시글 조회 -> 게시글 작성' 과 같은 형태로 시나리오를 작성해서 테스트 해봐야겠다.
- 로컬 환경 테스트인 점
- 네트워크 지연, 인프라 복잡도를 반영하지않았기 때문에 배포환경 테스트를 해봐야겠다.
- 무료플랜 제한으로 테스트를 100 VUs 까지 해본 점
- 실제 대규모 트래픽은 수천~수만건 이상으로 높을텐데 이부분을 테스트할 다른 방법을 찾아봐야겠다.
개선 효과
1. 사용자 관점
- 응답 대기 시간 단축: 3.5초 → 0.08초 (44배 빠른 응답)
- "회원가입 완료" 응답을 즉시 확인 가능
2. 시스템 관점
- 이메일 전송 시간: 여전히 ~3초 소요
- 단, 백그라운드에서 처리되어 응답 차단 안 함
느낀점
- 프로젝트에 비동기처리를 도입해보고자 회원가입 시 이메일발송하는 로직만 건드려봤는데, 찾아보니 파일업로드, 외부 API 호출, 알림 발송, 로그 기록 등 많은 부분에서 비동기 처리를 사용하는 걸 알 수 있었다.
- 오늘 작업에 많은 시간을 썼는데 '다른 비동기처리는 언제 적용하지?' 라는 생각과 '언제 입사할 실력이 되나..' 라는 생각..이 들었다 흑흑..
- 포트폴리오에 기재할 성능개선 결과를 만들어보고 싶어서 K6 를 사용했는데 설정도 간편하고, 시각화돼서 첨부하기도 좋고, 결과값도 대시보드로 한번에 관리하기 편했다.
- 오늘 작업으로 성능개선 사이클(측정 -> 분석 -> 개선 -> 검증)을 경험해본점이 취준생 입장으로 값지다고 생각한다.
- 오늘은 단일 API 테스트를 진행했지만, 조만간 사용자 이용패턴을 몇개 짜서 테스트해보고싶다.
- 스트레스 테스트할때 CPU 미친듯이 치솟아서 컴퓨터꺼지면 어떡하나 ~ 했는데 다행히 버텼다. 이건 스레드를 설정하거나 시나리오를 좀 손봐야겠다.

'백엔드 > 게시판 만들기' 카테고리의 다른 글
| [Spring Boot] 게시판 - 18. 이메일 인증 구현 (0) | 2025.12.19 |
|---|---|
| [Spring Boot] 게시판 - 17. JWT 인증에 권한 관리 더하기 - @PreAuthorize로 메서드 레벨 보안 구현 (0) | 2025.12.18 |
| [Spring Boot] 게시판 - 16. JWT 적용 + 스테이징 배포 트러블슈팅 기록 (1) | 2025.12.17 |
| [Spring Boot] 게시판 - 15. JWT 인증 구현하기 (Token 기반으로 전환) (0) | 2025.12.16 |
| [Spring Boot] 게시판 - 14. Spring Security 로그인 구현 (0) | 2025.12.14 |