서비스 간의 의존성을 줄이기 위해서 이벤트를 도입하게 되었습니다
지금 프로젝트에서는 간단한 두 도메인 간의 관계이지만, 이후로의 확장이나 디벨롭에 있어서
서로 다른 두 도메인 간의 강한 의존성으로 시스템이 더욱 복잡해질 수 있음을 고려해 이벤트를 고려하였고, 도입하게 되었습니다
스프링에서 자주 쓰이는 이벤트로 @EventListener
와 @TransactionalEventListener
가 있습니다.
프로젝트에서는 둘 중 @TransactionalEventListener
를 이용하였는데, 이 둘의 각각의 차이와 왜 @TransactionalEventListener
를 선택하게 되었는지 천천히 알아봅시다
@EventListener
와 @TransactionalEventListener
소개, 그 둘의 차이에 대해서
@EventListener
@EventListener
는 이벤트를 발행하는 순간에 리스너가 동작합니다
즉, 리스너의 동작 시점을 지정할 수 없습니다
@TransactionalEventListener
@TransactionalEventListener
는 트랜잭션이나 커밋이 롤백이 되는 시점에 리스너가 동작합니다
즉, 이벤트의 실질적인 실행을 트랜잭션의 종료를 기준으로 합니다
참여자 그리고 TimeBlock이 있고
이 둘의 관계는 다음과 같습니다
이벤트를 사용하지 않았더라면 참여자 서비스에서 TimeBlock 서비스에 의존해 TimeBlock을 세팅했을 것입니다
하지만, 두 서비스(참여자, TimeBlock) 간의 강한 의존성을 줄이기 위해서 이벤트를 사용하게 되었습니다
이를 그림으로 나타내면 다음과 같습니다
이벤트 사용 전
이벤트 사용 후
@EventListener
가 아닌 @TransactionalEventListener
를 선택한 이유이벤트를 사용하기로 결정했다면, 이제 @EventListener
와 @TransactionalEventListener
둘 중 어느 이벤트리스너를 사용할 지 결정해야 합니다
아까 EventListener를 소개하는 부분에서
EventListener는 이벤트를 발행하는 순간에 즉시 리스너가 동작을 하게되어 리스너의 동작 시점을 지정할 수가 없다고 했습니다.
즉, EventListener를 사용하면
참여자가 생성되지 않았음에도 리스너가 동작해 TimeBlock을 생성할 수 있으므로
우리는 참여자 생성이 성공시(트랜잭션 성공시) 이벤트를 실행해야 하므로
이벤트가 실행되는 시점
을 트랜잭션 이후
로 지정해주기 위해 @EventListener
가 아닌 @TransactionalEventListener
를 이용하게 되었습니다.
그림으로 두 @EventListener
와 @TransactionalEventListener
의 리스너의 실행 시점을 확인해보면 다음과 같습니다
즉, 참여자가 생성되고 commit 이후에
리스너가 동작하여 timeBlock 을 세팅해 주어야 하므로
@TransactionalEventListener
를 이용하게 되었습니다
코드를 확인하면 다음과 같습니다
// Participant.java
@Entity
@NoArgsConstructor
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"room_uuid", "name"})})
@Slf4j
public class Participant extends AbstractAggregateRoot<Participant> {
private static final Pattern PASSWORD_PATTERN = Pattern.compile("^[0-9]{4}$");
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "room_uuid", nullable = false)
private String roomUuid;
@Column(nullable = false)
private String name;
....
@PostPersist
private void registerCreateEvent() {
registerEvent(new ParticipantCreateEvent(roomUuid, name));
log.info("이벤트 발행완료");
}
// ParticipantCreateEvent.java
@Getter
@AllArgsConstructor
public class ParticipantCreateEvent {
private String roomUuid;
private String name;
}
// TimeBlockCreateService.java
@Service
@RequiredArgsConstructor
@Slf4j
public class TimeBlockCreateService {
private final TimeBlockRepository timeBlockRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void create(ParticipantCreateEvent event) {
timeBlockRepository.save(new TimeBlock(event.getRoomUuid(), event.getName()));
log.info("이벤트 실행 완료, timeBlock 생성 완료");
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
를 써줘야하는 이유?@TransactionalEventListener
는 phase를 통해 언제 실행될지 지정할 수 있습니다. 그리고 위 코드에서는 phase = TransactionPhase.AFTER_COMMIT
으로 지정해 트랜잭션이 성공적으로 마무리(commit)됐을 때 이벤트 실행하도록 하였습니다. 즉 새로운 트랜잭션이 없다면 리스너가 작동하지 않습니다
이미 이전 트랜잭션이 완료된 이후이므로, 이벤트를 처리할 수 있도록 새로운 트랜잭션을 열어주어야 합니다. 즉, 이벤트 리스너 메서드를 이전 트랜잭션과 독립적인 별도의 트랜잭션에서 실행되도록 해주어야 합니다.
이렇게 하면 이전 publisher의 커밋도 보장하고, 이벤트 리스너에서도 새로운 트랜잭션 안에서 데이터 추가 작업을 진행하도록 할 수 있습니다
이를 확인하기위한 테스트코드는 다음과 같습니다.
// ParticipantIntegrationTest.java
@SpringBootTest
@RecordApplicationEvents
public class ParticipantIntegrationTest {
@Autowired
private ParticipantService participantService;
@Autowired
private TimeBlockRepository timeBlockRepository;
@Autowired
private ApplicationEvents events;
@Test
void 참여자가_생성되면_참여자의_TimeBlock이_생성된다() {
participantService.create(ROOM_UUID, "참여자1", "1234");
Optional<TimeBlock> actual = timeBlockRepository.findByRoomUuidAndParticipantName(ROOM_UUID, "참여자1");
assertAll(
() -> assertThat(actual.isPresent()).isTrue(), // TimeBlock 이 만들어졌는지 확인
() -> assertThat(events.stream(ParticipantCreateEvent.class).count()).isEqualTo(1) // 이벤트가 발행되었는지 확인
);
}
}
테스트 break point를 걸고 디버깅으로 호출 순서 확인해보기
잘 읽었습니다.