공부하는 블로그

Spring | @TransactionalEventListener 본문

Spring

Spring | @TransactionalEventListener

치킨닮은닭 2023. 9. 17. 01:00

@TransactionalEventListener

  • 이벤트 처리 로직에서 트랜잭션을 적용해야 하는 경우 사용한다.
  • phase 옵션으로 어떤 식으로 트랜잭션 내에서 동작을 할 지 결정할 수 있다. 
    1. TransactionPhase.AFTER_COMMIT
      • 기본값
      • 트랜잭션이 commit 되었을 때 이벤트 로직을 실행한다.
    2. TransactionPhase.AFTER_ROLLBACK
      • 트랜잭션이 rollback 되었을 때 이벤트 로직을 실행한다.
    3. TransactionPhase.AFTER_COMPLETION
      • 트랜잭션이 완료(commit 또는 rollback) 되었을 때 이벤트 로직을 실행한다.
    4. TransactionPhase.BEFORE_COMMIT
      • 트랜잭션이 commit 되기 전에 이벤트를 실행한다.

 

Example

 옵션값 별로 어떻게 동작하는지 테스트해보자. 간단하게 User를 DB에 저장하고 이벤트를 발행하는 서비스를 작성하였다. DB저장은 Spring data JPA를 이용하여 구현하였다.

 

User
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class User {

  @Id
  private String id;

  @Column(length = 15)
  private String password;

  @Column
  private String name;

  User(String id, String password, String name) {
    this.id = id;
    this.password = password;
    this.name = name;
  }

  public static User from(UserCreationRequest request) {
    return new User(
        request.getId(),
        request.getPassword(),
        request.getName()
    );
  }

  public UserDto toDto() {
    return new UserDto(this.id, this.name);
  }
UserDto
@Getter
@ToString
public class UserDto {

  private final String id;

  private final String name;

  public UserDto(String id, String name) {
    this.id = id;
    this.name = name;
  }
}
UserCreationRequest
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class UserCreationRequest {

  private String id;

  private String password;

  private String name;

  public UserCreationRequest(String id, String password, String name) {
    this.id = id;
    this.password = password;
    this.name = name;
  }
}
CustomEventPublisher
@Component
@RequiredArgsConstructor
public class CustomEventPublisher {

  private final ApplicationEventPublisher publisher;

  public void publish(@NonNull CustomTransactionalEvent event) {
    publisher.publishEvent(event);
  }
}
UserService
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {

  private final CustomEventPublisher publisher;

  private final UserRepository repository;

  @Transactional
  public UserDto create(UserCreationRequest request) {
    repository.findById(request.getId())
        .ifPresent(user -> {
          throw new RuntimeException(user.getId() + " is already exist.");
        });

    final User user = repository.save(User.from(request));

    // 이벤트 발행
    publisher.publish(new CustomTransactionalEvent());

    return user.toDto();
  }
}

 

 UserService는 유저 생성 요청을 인자로 받아 DB에 유저 정보를 저장하고 CustomEventPublisher를 통해 CustomTransactionEvent를 발행한다. 발행한 커스텀 이벤트는 아래에 작성한 4가지의 이벤트 리스너에서 처리된다.

 

@Component
@Slf4j
public class CustomEventListener {
  @TransactionalEventListener(
      classes = { CustomTransactionalEvent.class },
      phase = TransactionPhase.AFTER_COMMIT
  )
  public void onListenAfterCommit(CustomTransactionalEvent event) {
    log.info("##### listening after commit event. event = {}", event);
  }

  @TransactionalEventListener(
      classes = { CustomTransactionalEvent.class },
      phase = TransactionPhase.BEFORE_COMMIT
  )
  public void onListenBeforeCommit(CustomTransactionalEvent event) {
    log.info("##### listening before commit event. event = {}", event);
  }

  @TransactionalEventListener(
      classes = { CustomTransactionalEvent.class },
      phase = TransactionPhase.AFTER_COMPLETION
  )
  public void onListenAfterCompletion(CustomTransactionalEvent event) {
    log.info("##### listening after completion event. event = {}", event);
  }

  @TransactionalEventListener(
      classes = { CustomTransactionalEvent.class },
      phase = TransactionPhase.AFTER_ROLLBACK
  )
  public void onListenAfterRollback(CustomTransactionalEvent event) {
    log.info("##### listening after rollback event. event = {}", event);
  }
}

 

 4가지의 리스너는 UserService에서 발행된 CustomTransactionalEvent를 각각 다른 phase 옵션으로 처리한다. 실제로 설명과 같이 동작하는지 테스트해보자! @SpringBootTest를 이용하여 애플리케이션을 실행시킨 후 로그를 확인해보았다.

 

@SpringBootTest
@ActiveProfiles("test")
class UserServiceTest {

  @Autowired
  private UserService service;

  @Test
  @DisplayName("Commit 이벤트 테스트")
  void triggerCommitTransactionalTest() {
    // given : 정상 commit 처리 되도록 작성
    final UserCreationRequest request = new UserCreationRequest("user1",
        "password1",
        "홍길동");

    // when
    assertDoesNotThrow(() -> service.create(request));
  }

  @Test
  @DisplayName("Rollback 이벤트 테스트")
  void triggerRollbackTransactionalTest() {
    // given : column 설정보다 긴 문자열로 일부러 Rollback 유발
    final UserCreationRequest request = new UserCreationRequest("user2",
        "password_is_tooooooo_long",
        "홍길동");

    // when
    assertThrows(InvalidDataAccessResourceUsageException.class,
        () -> service.create(request));
  }
}

Test Result

Commit 이벤트 테스트 로그

  1. Spring : BEFORE_COMMIT 이벤트 리스너 동작.
  2. DB : Commit Initialize
  3. DB : Commit
  4. Spring : AFTER_COMMIT, AFTER_COMPLETION 이벤트 리스너 동작 

Rollback 이벤트 테스트 로그

  1. Spring : BEFORE_COMMIT 이벤트 리스너 동작.
  2. DB : Commit Initialize
  3. DB : 오류 발생
  4. DB : Rollback
  5. Spring : AFTER_ROLLBACK, AFTER_COMPLETION 이벤트 리스너 동작

 

 

'Spring' 카테고리의 다른 글

Spring | MockServer  (0) 2023.09.24
Spring | Application Event  (0) 2023.09.10
Spring | BDD - Given / When / Then  (0) 2023.09.03
Spring | Mockito Unit Test  (0) 2023.08.27
Spring | Annotations  (0) 2020.09.29
Comments