
유저가 워크스페이스 생성을 요청할 때, 초대할 사용자 이메일을 받는다. 이때 우리 서비스는 사용자 요청을 DB에 저장하고, 초대한 사용자들에게 이메일을 보낸다.
@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가 호출된 결과를 보면 문제가 있는 것을 알 수 있다.


- 지금 이 문제가 발생하진 않았지만, 박스 부분에서 `exception`이 터졌다거나, 이 트랜잭션이 예상하지 못 한 이유로 `rollback` 되는 경우에 문제가 발생할 수 있다.
- DB에는 이 워크스페이스가 생성되지 않았지만 워크스페이스 초대 메일이 전송되는 문제가 발생할 수 있다.
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)
}
createWorkspace는 private 메서드로 만들고 싶을 수 있는데 @Transactional의 기본 트랜잭션 매니저는 Java의 reflect에서 제공하는 동적 프록시로 트랜잭션을 열기 때문에 private 메서드에 대해 @Transactional을 동작시킬 수 없다.@Transactional도 관리해 줄 수 있다.
@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를 활용해 사용자에게 응답을 주고, 메일을 전송하는 것은 비동기적으로 동작하게 해주었다.결과화면을 보면
