학교 프로젝트에서 학우 전용 인증/인가 시스템을 개발하던 중이었습니다. 회원가입 플로우를 구현하고 테스트를 진행하는데, 한 가지 큰 문제점을 발견했습니다.
"이메일 인증 버튼 누르고 5초나 기다려야 하네?"
이메일 전송에 5초나 걸리는 동안 사용자는 아무것도 하지 못한 채 기다려야 했습니다. 개발자인 저도 테스트하면서 답답함을 느꼈는데, 실제 사용자라면 더 큰 불편을 겪을 것이 분명했습니다.
코드를 살펴보니 원인을 찾을 수 있었습니다. 회원가입 로직이 하나의 스레드에서 순차적으로 처리되고 있었습니다.
@Service
public class UserService {
public void registerUser(UserDto userDto) {
// 1. 회원 정보 저장
User user = new User(userDto);
userRepository.save(user);
// 2. 이메일 전송 (여기서 5초 대기)
emailService.sendVerificationEmail(user);
// 3. 이메일 전송이 완료될 때까지 다음 단계로 진행 불가
}
}
"꼭 이메일이 발송될 때까지 기다려야 할까? 사용자가 다른 정보를 먼저 입력할 수는 없을까?"
해결 방안: 멀티스레드와 화면 분리
문제를 해결하기 위해 두 가지 방향으로 접근했습니다.
- 백엔드: 이메일 전송을 별도 스레드로 분리하자.
- 프론트엔드: 회원가입 화면을 두 단계로 나누어 자연스러운 흐름을 구성하자.
Spring의 비동기 처리 기능을 찾아보던 중 @Async 어노테이션을 발견했습니다. 회원가입 처리와 이메일 전송을 별도의 스레드로 나눌 수 있겠다는 생각이 들었습니다.
1. Thread Pool 구성
먼저 이메일 전송을 담당할 스레드 풀을 설정했습니다.
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2); // 기본 작업 스레드 수
executor.setMaxPoolSize(5); // 최대 스레드 수
executor.setQueueCapacity(500); // 작업 대기 큐 크기
executor.setThreadNamePrefix("EmailThread-");
executor.initialize();
return executor;
}
}
2. 비동기 이메일 서비스 구현
이메일 전송 로직을 별도 스레드에서 처리하도록 변경했습니다.
@Service
public class EmailService {
@Async
public void sendVerificationEmailAsync(User user) {
// 이메일 전송 로직 (5초 소요)
// 메인 스레드는 차단되지 않고 즉시 다음 작업 진행
}
}
2. 회원가입 UI 개선
기존의 단일 페이지 회원가입을 두 단계로 나누어 재설계했습니다.
Step 1: 회원정보 입력 페이지
- 이메일 입력 후 "인증하기" 버튼 클릭 → 즉시 "이메일이 발송되었습니다"라는 알림 표시
- 사용자는 이메일 발송까지 기다리지 않고, 나머지 회원 정보를 입력하고 다음 단계로 진행
Step 2: 인증번호 확인 페이지
- 인증번호 입력 필드만 있는 간단한 화면
- 필요시 인증번호 재발송 버튼도 제공됨
리팩토링 후 느낀 점
리팩토링 후 회원가입 플로우가 훨씬 자연스러워졌습니다.
- 사용자가 이메일 인증 버튼을 클릭하면 즉시 “인증번호가 발송되었습니다” 메시지가 표시됩니다.
- 이메일 전송이 별도의 비동기 스레드에서 처리되므로, 사용자는 다른 회원정보를 입력할 수 있습니다.
- 모든 정보 입력 후 인증 페이지로 이동하면, 그때쯤 이메일이 도착해 있어 바로 인증이 가능합니다.
테스트 과정에서 “어, 이제 기다리는 느낌이 안 드네?“라는 생각이 들었을 때, 정말 기분이 좋았습니다.
@Async의 동작 방식과 주의점
리팩토링 과정에서 @Async의 내부 동작 방식에 대해서도 깊이 이해하게 되었습니다.
@Async는 Spring AOP(Aspect Oriented Programming)의 프록시 방식으로 동작합니다. 쉽게 말해, Spring이 해당 클래스를 감싸는 프록시 클래스를 생성하고, 이 프록시가 비동기 처리를 담당하는 방식입니다.
// 실제 코드
@Service
public class EmailService {
@Async
public void sendEmail() {
// 이메일 전송 로직
}
}
// Spring이 내부적으로 생성하는 프록시 형태
public class EmailServiceProxy extends EmailService {
public void sendEmail() {
// 별도 스레드에서 실행
executor.submit(() -> {
super.sendEmail();
});
}
}
이러한 동작 방식 때문에 두 가지 중요한 제약사항이 있었습니다.
- private 메소드에는 @Async를 사용할 수 없습니다.
- 프록시는 원본 클래스를 상속하므로, private 메소드는 접근이 불가능합니다.
- 같은 클래스 내의 메소드 호출(self-invocation)에는 @Async가 적용되지 않습니다.
- 내부 호출은 프록시를 거치지 않고 직접 호출되기 때문입니다.
'개발일지' 카테고리의 다른 글
[SnowTaxi] WebSocket + STOMP로 실시간 채팅 구현 (0) | 2024.02.10 |
---|