Spring Event와 @Aysnc를 사용해 회원가입 개선

2024. 6. 9. 12:38프로젝트/[EATceed] 몸무게 증량 어플

728x90

 

이번 포스팅에서는 기존에 회원가입 코드를 Spring Event와 @Async를 사용해서 개선하는 과정을 포스팅하려합니다.

 

 

필요성 - Event

 

저희 서비스의 회원가입 기능안에는 해당 회원이 회원가입을 할 수 있는 유저인가 확인, 특정 이메일로 인증 메일 보내기 그리고 회원 정보 생성으로 이루어져있습니다.

 

위와 같이 코드를 작성하게 되면, "메일 보내기"와 "회원가입"의 의존성이 강해집니다.

의존성이 강해진다는 것은 재사용성이 낮아지고 단위 테스트가 어려워집니다.

 

특히, 이메일 보내기는 여러 도메인에서 사용될 가능성이 높은 경우이기 때문에 Event를 사용해서 회원가입 서비스에서 이메일 서비스에대한 의존성을 제거하여 재사용성을 높인다는 것이 큰 이점이라 판단했습니다.

 

필요성 - 비동기 처리

비동기 처리 전에는 배포 직후 첫 번째 요청에서 Request Time Out이 발생했었습니다.

ALB에서의 idle time out 시간을 50s로 설정해놓았기 때문에 회원가입 API 요청이 50s 이상 걸렸다는 것을 알 수 있습니다.

이에 애플리케이션에서 "처리 속도"를 향상 시킬 수 있는 "무언가"를 할 필요가 있었습니다.

코드 개선

 

public ApiResponse<CustomBody<Void>> signUpMember(
            @RequestBody @Valid SignUpMemberRequest signUpMemberRequest) {
    validateEmailUsecase.execute(new ValidateEmailCommand(signUpMemberRequest.email()));
    sendEmailUsecase.execute(new SendEmailCommand(signUpMemberRequest.email()));
    createMemberUsecase.execute(signUpMemberRequest);
    return ApiResponseGenerator.success(HttpStatus.OK);
}

@Service
public class SendSignupEmailService implements SendEmailUsecase {

	...
    
    @Override
    @Transactional
    public void execute(SendEmailCommand sendEmailCommand) {
        // 이메일 인증 코드 만들기 - 암호화 사용
        // 이메일 보내는 코드
    }
}

 

 

기존에는 SendEmailUsecase의 execute 메서드에서 직접적으로 이메일 인증 코드를 만들고, 이메일을 보내고 있었습니다.

또한 이메일 보내는 코드에서 이메일의 반환 여부를 제공하고 있는 블락킹 방식을 사용하고 있었습니다.

 

 

이를 Spring Event를 사용해서 아래와 같이 개선하였습니다.

 

 

@Service
@RequiredArgsConstructor
public class SendSignupEmailService implements SendEmailUsecase {
    @Override
    @Transactional
    @EventPublisherStatus
    public void execute(SendEmailCommand sendEmailCommand) {
        Events.raise(SendEmailEvent.from(sendEmailCommand.email()));
    }
}

 

이메일 보내는 이벤트를 발행하고 있습니다.

 

참고) AOP를 사용하여 커스텀 애노테이션(@EventPublisherStatus)이 있는 메서드에서 Publisher가 세팅되도록 하였습니다.


https://github.com/JNU-econovation/EATceed/pull/260

 

[BE/feat] EventPublisherAspect 개선 by hwangdaesun · Pull Request #260 · JNU-econovation/EATceed

⚠️ 관련 이슈 #259 📢 주요 변경사항 @EventPublisherStatus 생성 EventPublisherAspect의 조인포인트 트랜잭션 애노테이션에서 @EventPublisherStatus로 변경 리뷰어에게 EventPublisherStatus에서는 각 스레드마다 pub

github.com

 

 

@Component
@RequiredArgsConstructor
public class SendEmailEventListener {

	...

    @TransactionalEventListener(classes = SendEmailEvent.class)
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Timer
    @Async
    public void handle(SendEmailEvent event) {
        // 이메일 인증 코드 만들기 - 암호화 사용
    	String code = encryption.encrypt(uuid);
        // 이메일 보내기
	emailPort.sendEmail(code);
    }
}

 

 

기본적으로 Spring Event는 동기로 작동하기 때문에, @Async 애노테이션을 사용해서 비동기로 작동하도록 하였고, @Transactional(Propagation.REQUIRED_NEW)를 사용하여 이벤트를 Listen할 때 새로운 트랜잭션을 만들어 기존 트랜잭션에서 전파가 되지 않도록 만들었습니다. (이메일 발송에 실패하였다고, 회원가입 또한 실패하면 안 되기때문에 REQUIRES_NEW 사용)

 

그리고, 이메일의 성공 여부는 회원가입시 사용자에게 보여줄 필요가 없다고 판단해 sendEmail retun type을 Void로 바꿔 논 블락킹이 되게끔 만들었습니다.

 

마지막으로, ThreadPoolTaskExecutor를 만들어 비동기로 코드가 실행될 때 해당 스레드 풀을 사용하도록 하였습니다.

@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(16);
        executor.setMaxPoolSize(25);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(60);
        executor.setRejectedExecutionHandler(
                (r, exec) -> {
                    throw MailSendException.EXECPTION;
                });
        executor.initialize();
        return executor;
    }
}

 

 

스레드 풀 설정에 대해서는 다음 포스팅에서 다루도록 하겠습니다.

 

 

결국, 비동기 + 논블락킹 방식을 사용했습니다.

 

 

그리고, 코드를 작성하면서 @Async를 handle 메서드에서 사용할 지 이메일을  보내는 sendEmail 메서드에 사용할 지를 고민했었습니다.

 

@Async를 handle 메서드에서 사용 vs sendEmail 메서드에서 사용

 

@Async를 사용하면 새로운 스레드가 만들어져 작업을 처리합니다.

이때, handle 메서드에서 @Async를 사용하면, 하나의 스레드가 암호화 작업 및 이메일 보내는 작업을 수행합니다. 

반면, sendEmail 메서드에 @Async를 사용하게 되면, 기존 스레드가 암호화 작업을 수행하고 새로운 스레드가 이메일 보내는 작업을 수행합니다. 이렇게 되면, 기존 스레드가 가진 정보를 새로운 스레드에 제공해줘야하기 때문에 스레드 컨텍스트 스위칭 비용이 발생하게 됩니다.

따라서, 추가적인 비용이 발생하지 않는 handle 메서드에 @Async를 사용하였습니다.

 

 

결과

 

 

비동기 처리 전

 

 

 

비동기 처리 후

 

 

 

비동기 처리 후 해당 API의 속도는 약 12배 증가하였습니다.

 

 

728x90