Pink Spider/Java에서 event를 활용한 구현 예시

Created Mon, 01 Sep 2025 16:13:53 +0900 Modified Mon, 08 Dec 2025 08:41:47 +0900
838 Words 4 min

1) 이벤트 정의 (POJO/record 권장)

// src/main/java/com/example/user/event/UserSignedUpEvent.java
package com.example.user.event;

import java.time.Instant;

public record UserSignedUpEvent(Long userId, String email, Instant occurredAt) {}

Spring 6+/Boot 3에서는 ApplicationEvent를 상속할 필요가 없습니다. 평범한 POJO/record가 좋아요.


2) 이벤트 발행 (raise)

// src/main/java/com/example/user/service/SignUpService.java
package com.example.user.service;

import com.example.user.event.UserSignedUpEvent;
import com.example.user.model.User;
import com.example.user.repo.UserRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;

@Service
public class SignUpService {

    private final UserRepository userRepository;
    private final ApplicationEventPublisher publisher;

    public SignUpService(UserRepository userRepository, ApplicationEventPublisher publisher) {
        this.userRepository = userRepository;
        this.publisher = publisher;
    }

    @Transactional
    public Long signUp(String email, String name) {
        User user = userRepository.save(new User(email, name));  // JPA 엔티티 저장 가정
        // 이벤트 발행 (트랜잭션 안에서)
        publisher.publishEvent(new UserSignedUpEvent(user.getId(), user.getEmail(), Instant.now()));
        return user.getId();
    }
}

3) 동기 리스너 (기본)

// src/main/java/com/example/user/listener/AuditLogListener.java
package com.example.user.listener;

import com.example.user.event.UserSignedUpEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
public class AuditLogListener {
    private static final Logger log = LoggerFactory.getLogger(AuditLogListener.class);

    @Order(0) // 여러 리스너가 있을 때 순서 제어 가능 (낮을수록 먼저)
    @EventListener
    public void onUserSignedUp(UserSignedUpEvent event) {
        log.info("AUDIT: userId={}, email={}, at={}", event.userId(), event.email(), event.occurredAt());
    }
}

기본은 “동기 실행”이라 발행 시점에 즉시 호출됩니다.


4) 비동기 리스너 (@Async)

// src/main/java/com/example/config/AsyncConfig.java
package com.example.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

@Configuration
@EnableAsync
public class AsyncConfig { }
// src/main/java/com/example/user/listener/WelcomeEmailListener.java
package com.example.user.listener;

import com.example.user.event.UserSignedUpEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class WelcomeEmailListener {
    private static final Logger log = LoggerFactory.getLogger(WelcomeEmailListener.class);

    @Async
    @EventListener(condition = "#event.email.endsWith('co.kr')") // 조건부 처리도 가능
    public void sendWelcomeEmail(UserSignedUpEvent event) {
        log.info("SEND EMAIL: welcome -> {}", event.email());
        // 실제 메일 전송 로직 ...
    }
}

@EnableAsync + @Async로 발행자 스레드와 분리합니다. 실패가 본 트랜잭션에 영향을 주지 않도록 하고 싶을 때 유용.


5) 트랜잭션 완료 후 처리 (@TransactionalEventListener)

DB 커밋이 성공한 뒤에만 실행하고 싶다면:

// src/main/java/com/example/user/listener/PostCommitListener.java
package com.example.user.listener;

import com.example.user.event.UserSignedUpEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
public class PostCommitListener {
    private static final Logger log = LoggerFactory.getLogger(PostCommitListener.class);

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void afterCommit(UserSignedUpEvent event) {
        log.info("AFTER_COMMIT: create coupon for userId={}", event.userId());
        // 쿠폰 발급, 외부 연동 등 "커밋 보장"이 필요할 때
    }
}

같은 publishEvent(...) 호출이라도, 이 리스너는 트랜잭션 커밋 이후에 실행됩니다. 반대로 롤백 시 특정 처리를 하고 싶으면 phase = TransactionPhase.AFTER_ROLLBACK도 가능.


6) 엔티티/리포지토리 예시 (참고용)

// src/main/java/com/example/user/model/User.java
package com.example.user.model;

import jakarta.persistence.*;

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;

    private String email;
    private String name;

    protected User() {}
    public User(String email, String name) { this.email = email; this.name = name; }

    public Long getId() { return id; }
    public String getEmail() { return email; }
    public String getName() { return name; }
}
// src/main/java/com/example/user/repo/UserRepository.java
package com.example.user.repo;

import com.example.user.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> { }

사용 팁 요약

  • 이벤트 클래스는 POJO/record로 가볍게.
  • 빠른 응답이 필요한 비즈니스 로직 ↔ 부가 작업(메일/알림/로그)을 느슨하게 분리.
  • 트랜잭션에 의존하는 후속 작업은 @TransactionalEventListener(AFTER_COMMIT).
  • 부하/지연이 큰 작업은 @Async로 분리(필요 시 전용 TaskExecutor 설정).
  • 조건부 실행(condition)과 순서(@Order)로 복잡성 제어.

코드 예제