@TransactionalEventListener를 이용한 이메일 전송을 포함하는 트랜잭션 성능개선하기

Dltmd202·2024년 7월 5일

현재상황

유저가 워크스페이스 생성을 요청할 때, 초대할 사용자 이메일을 받는다. 이때 우리 서비스는 사용자 요청을 DB에 저장하고, 초대한 사용자들에게 이메일을 보낸다.

WorkspaceFacadeService.kt

@Service
@Transactional(readOnly = true)
class WorkspaceFacadeService(
    private val userService: UserService,
    private val workspaceService: WorkspaceService,
    private val workspaceUserService: WorkspaceUserService,
    private val chatRoomService: ChatRoomService,
    private val chatRoomUserService: ChatRoomUserService,
    private val mailSender: EmailSender,
) {
    
    @Transactional
    fun createWorkspace(
        userId: String,
        workspaceCreateDto: WorkspaceCreateDto,
    ): WorkspaceDto {
        val leader: User = userService.getById(userId)
        val workspace: Workspace = workspaceService.save(
        leader.createWorkspace(workspaceCreateDto.name))
        workspaceUserService.save(workspace.assignLeader(leader))

        workspaceCreateDto.inviteesEmails.map { userService.getByEmail(it) }.forEach {
            workspaceUserService.save(workspace.inviteWorkspace(it))
            mailSender.sendEmail(it.email, 
            "워크스페이스 초대", "${leader.name}님이 워크스페이스에 초대하였습니다.")
        }
        
        val chatRoom: ChatRoom = chatRoomService.save(workspace.createGroupChatRoom())
        chatRoomUserService.save(chatRoom.addUser(leader))

        return WorkspaceDto.of(workspace)
    }
}

이 API가 호출된 결과를 보면 문제가 있는 것을 알 수 있다.

  • 요청 시간이 오래걸렸다.
    • 요청이 처리될 때까지 13초가 걸렸다.
    • 1명당 4초 정도가 걸리는 것 같다.
  • 예외 처리 문제

- 지금 이 문제가 발생하진 않았지만, 박스 부분에서 `exception`이 터졌다거나, 이 트랜잭션이 예상하지 못 한 이유로 `rollback` 되는 경우에 문제가 발생할 수 있다.
- DB에는 이 워크스페이스가 생성되지 않았지만 워크스페이스 초대 메일이 전송되는 문제가 발생할 수 있다.

해결방법

1. 메서드로 래핑한다.

fun createWorkspaceWithMail(
    userId: String,
    workspaceCreateDto: WorkspaceCreateDto,
): WorkspaceDto {
    val workspaceDto: WorkspaceDto = this.createWorkspace(userId, workspaceCreateDto)

    workspaceCreateDto.inviteesEmails.forEach {
        mailSender.sendEmail(it, "워크스페이스 초대", "${workspaceDto.name}님이 워크스페이스에 초대하였습니다.")
    }

    return workspaceDto
}

@Transactional
fun createWorkspace(
    userId: String,
    workspaceCreateDto: WorkspaceCreateDto,
): WorkspaceDto {
    val leader: User = userService.getById(userId)
    val workspace: Workspace = workspaceService.save(leader.createWorkspace(workspaceCreateDto.name))
    workspaceUserService.save(workspace.assignLeader(leader))

    workspaceCreateDto.inviteesEmails.map { userService.getByEmail(it) }.forEach {
        workspaceUserService.save(workspace.inviteWorkspace(it))
    }

    val chatRoom: ChatRoom = chatRoomService.save(workspace.createGroupChatRoom())
    chatRoomUserService.save(chatRoom.addUser(leader))

    return WorkspaceDto.of(workspace)
}
  • 이렇게 하면 기존의 트랜잭션이 종료되어 커밋되었을 때 메일을 전송할 수 있다.
  • 하지만 그렇게 될 경우에 내부 로직이 되는 createWorkspaceprivate 메서드로 만들고 싶을 수 있는데 @Transactional의 기본 트랜잭션 매니저는 Java의 reflect에서 제공하는 동적 프록시로 트랜잭션을 열기 때문에 private 메서드에 대해 @Transactional을 동작시킬 수 없다.
    • AspectJ를 활용하는 TransactionManager를 주입하면 CGLIB라는 바이트 코드를 직접 조작하여 프록시를 만들기 때문에 private 메서드에 걸린 @Transactional도 관리해 줄 수 있다.

  • 이렇게 해도 사실 속도가 느리다는 단점은 피해가기 힘들다.

2. 이벤트 리스너를 활용한다.

@Transactional
fun createWorkspace(
    userId: String,
    workspaceCreateDto: WorkspaceCreateDto,
): WorkspaceDto {
    val leader: User = userService.getById(userId)
    val workspace: Workspace = workspaceService.save(leader.createWorkspace(workspaceCreateDto.name))
    workspaceUserService.save(workspace.assignLeader(leader))

    val invitees = workspaceCreateDto.inviteesEmails.map { userService.getByEmail(it) }
    invitees.forEach {
        workspaceUserService.save(workspace.inviteWorkspace(it))
        eventPublisher.publishEvent(WorkspaceCreatedEvent(leader, invitees))
    }

    val chatRoom: ChatRoom = chatRoomService.save(workspace.createGroupChatRoom())
    chatRoomUserService.save(chatRoom.addUser(leader))
    
    return WorkspaceDto.of(workspace)
}

@Async
@TransactionalEventListener
fun handleWorkspaceCreatedEvent(event: WorkspaceCreatedEvent) {
    event.invitees.forEach {
        logger.info { "email send to ${it.email}" }
        mailSender.sendEmail(it.email, "워크스페이스 초대", "${event.leader.name}님이 워크스페이스에 초대하였습니다.")
    }
}
  • @TransactionalEventListener는 이 이벤트를 발행하는 트랜잭션이 커밋되면 이벤트를 발행하게 된다. 덕분에 트랜잭션이 완전히 성공했을 때 해당 이벤트에 대한 동작을 처리함을 보장할 수 있다.
  • handleWorkspaceCreatedEvent@Async를 활용해 사용자에게 응답을 주고, 메일을 전송하는 것은 비동기적으로 동작하게 해주었다.

결과화면을 보면

  • 시간까지 단축시킨 것을 볼 수 있다.

0개의 댓글