๐Ÿ“ฌ Spring Event ๋„์ž…๊ณผ Class์˜ ์ฑ…์ž„ ๋ถ„๋ฆฌ

๋ฌด์ง€์„ฑ๊ฐœ๋ฐœ์žยท2025๋…„ 6์›” 15์ผ
0

๊ฐœ์š”

ํ”ฝ๋ธ”์—ฝ์„ ์ง„ํ–‰ํ•˜๋ฉด์„œ ํ•˜๋‚˜์˜ ๊ธฐ๋Šฅ์— ์—ฌ๋Ÿฌ ๋ถ€๊ฐ€ ๊ธฐ๋Šฅ์ด ๋”ฐ๋ผ์˜ค๊ฒŒ ๋˜์—ˆ๋‹ค.
์˜ˆ: ์—ฝ์„œ ๋“ฑ๋ก โ†’ DB ์ €์žฅ โ†’ ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฉ”์ผ ๋ฐœ์†ก

์ด์ฒ˜๋Ÿผ ํ•˜๋‚˜์˜ ์•ก์…˜์— ์—ฌ๋Ÿฌ ์ฑ…์ž„์ด ๋”ธ๋ ค๋ถ™๊ธฐ ์‹œ์ž‘ํ•˜๋ฉด ๊ฐ์ฒด์ง€ํ–ฅ 5์›์น™ ์ค‘ ํ•˜๋‚˜์ธ ๋‹จ์ผ ์ฑ…์ž„ ์›์น™(SRP)์ด ์‰ฝ๊ฒŒ ๊นจ์ง„๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Spring Event๋ฅผ ๋„์ž…ํ–ˆ๊ณ , ์ ์šฉ ๋ฐฉ๋ฒ•๊ณผ ์‹ค์ „ ์ ์šฉ๊ธฐ(ํ”ฝ๋ธ”์—ฝ ์˜ˆ์ œ)๋ฅผ ์•„๋ž˜์— ์ •๋ฆฌํ•ด๋ดค๋‹ค.

Spring Event๋ž€?

Spring Event๋Š” ์ปดํฌ๋„ŒํŠธ ๊ฐ„์˜ ๋А์Šจํ•œ ์—ฐ๊ฒฐ(loose coupling)์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ๊ตฌ์กฐ๋‹ค.
๋‚ด๋ถ€์ ์œผ๋กœ๋Š” Observer ํŒจํ„ด์œผ๋กœ ๊ตฌ์„ฑ๋ผ์žˆ๋‹ค.

๋ˆ„๊ตฐ๊ฐ€ ์–ด๋–ค ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๋ฉด,
๊ด€์‹ฌ ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ์Šค๋„ˆ๋กœ์„œ ๊ทธ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์•„ ์ฒ˜๋ฆฌ.

๐Ÿง  ํ•ต์‹ฌ ๊ฐœ๋…

์—ญํ• ์„ค๋ช…
Publisher์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ์ชฝ
Event์ „๋‹ฌ๋˜๋Š” ๋ฐ์ดํ„ฐ ๊ฐ์ฒด
Listener์ด๋ฒคํŠธ๋ฅผ ๊ตฌ๋…ํ•˜๊ณ  ์ฒ˜๋ฆฌํ•˜๋Š” ์ชฝ

๐Ÿ”จ ์ ์šฉ ๋ฐฉ๋ฒ•

์ด๋ฒคํŠธ ๊ฐ์ฒด

public class UserRegisteredEvent {
    private final String email;

    public UserRegisteredEvent(String email) {
        this.email = email;
    }

    public String getEmail() {
        return email;
    }
}

์ด๋ฒคํŠธ ๋ฐœํ–‰

@Component
@RequiredArgsConstructor
public class UserService {

    private final ApplicationEventPublisher publisher;

    public void registerUser(String email) {
        // ์‚ฌ์šฉ์ž ๋“ฑ๋ก ๋กœ์ง ...

        // ์ด๋ฒคํŠธ ๋ฐœํ–‰
        publisher.publishEvent(new UserRegisteredEvent(email));
    }
}

์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ

@Component
public class WelcomeEmailListener {

    @EventListener(UserRegisteredEvent.class)
    public void handleUserRegistered(UserRegisteredEvent event) {
        System.out.println("ํ™˜์˜ ๋ฉ”์ผ ์ „์†ก ๋Œ€์ƒ: " + event.getEmail());
        // ๋ฉ”์ผ ์ „์†ก ๋กœ์ง ...
    }
}

โœ๏ธ ํ”ฝ๋ธ”์—ฝ ์‹ค์ „ ์ ์šฉ๊ธฐ

ํ”ฝ๋ธ”์—ฝ์—์„œ๋Š” ์—ฝ์„œ๋ฅผ ์Šน์ธํ•  ๋•Œ ๋‹ค์Œ 3๊ฐ€์ง€ ๊ธฐ๋Šฅ์ด ํ•œ ๋ฒˆ์— ์‹คํ–‰๋œ๋‹ค.

  1. ์—ฝ์„œ ์ƒํƒœ ๋ณ€๊ฒฝ (pre_postsํ…Œ์ด๋ธ” status์ปฌ๋Ÿผ ์ˆ˜์ •)
  2. ์‹ค์ œ ์—ฝ์„œ ๋“ฑ๋ก (posts ํ…Œ์ด๋ธ”)
  3. ์ž‘์„ฑ์ž์—๊ฒŒ ์Šน์ธ ๋ฉ”์ผ ์ „์†ก

์ด๋•Œ 1~2๋ฒˆ์€ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๊ณ , 3๋ฒˆ์€ ๋ถ€๊ฐ€๊ธฐ๋Šฅ์ด๋ฏ€๋กœ ์„ฑ๊ณต/์‹คํŒจ๊ฐ€ ํŠธ๋žœ์žญ์…˜ ์ž‘์—…์— ์˜ํ–ฅ์ด ์—†์–ด์•ผํ•˜๋Š” ์กฐ๊ฑด์ด ์žˆ์—ˆ๋‹ค.


์—ฝ์„œ ์Šน์ธ Class

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class ConfirmPrePostService {

    private final ApplicationEventPublisher eventPublisher;
    private final GetPrePostService getPrePostService;
    private final ConfirmPrePostValidator confirmPrePostValidator;

    public void confirm(UpdatePrePostStatusDto dto) {
        log.info("Confirm pre post: {}", dto);
        PrePost prePost = getPrePostService.getPrePost(dto.prePostId());
        confirmPrePostValidator.validateStatus(prePost.getStatus());
        prePost.updateStatus(dto.status(), dto.updatedBy());
        eventPublisher.publishEvent(ConfirmPrePostStatus.confirmPrePost(prePost, dto.updatedBy()));
    }

์—ฝ์„œ ๋“ฑ๋ก Class

@TransactionalEventListener(classes = ConfirmPrePostStatus.class, phase = TransactionPhase.BEFORE_COMMIT)
    public void registerPost(ConfirmPrePostStatus event) {
        log.info("EVENT - Register post: {}", event);
        String geohash = geoHashUtil.encode(event.latitude(), event.longitude());
        MultipartFile img = fileCodecUtil.decodeToMultipartFile(geohash, event.img());
        s3Util.uploadFile(event.type() + "-" + geohash, img);
        Post post = new Post(
                geohash,
                event.name(),
                event.latitude(),
                event.longitude(),
                event.type(),
                event.requester(),
                event.noImg(),
                event.confirmedBy()
        );
        postRepository.save(post);
    }

์—ฌ๊ธฐ์„œ @TransactionalEventListener์–ด๋…ธํ…Œ์ด์…˜๊ณผ phase = TransactionPhase.BEFORE_COMMIT ๋‘๊ฐœ๋ฅผ ์ž˜ ๋ด์•ผํ•œ๋‹ค.

  • @TransactionalEventListener : ์ด๋ฒคํŠธ๋ฅผ ํŠธ๋žœ์ ์…˜์— ํฌํ•จ์‹œํ‚ค๋Š” Listener.
  • phase = TransactionPhase.BEFORE_COMMIT : ํŠธ๋žœ์žญ์…˜์ด ์ž‘์—…์ด ์ปค๋ฐ‹ ๋˜๊ธฐ์ „์— ์‹คํ–‰.
    • ์‹คํŒจ ์‹œ ์ „์ฒด ์ž‘์—…์ด rollback ๋œ๋‹ค.

Mail ์ „์†ก Class

@Slf4j
@Service
@RequiredArgsConstructor
public class ConfirmPrePostMailService {

    private final JavaMailSender javaMailSender;
    private final ConfirmPrePostTemplate confirmPrePostTemplate;

    @Value("${spring.mail.username}")
    private String from;

    @TransactionalEventListener(classes = ConfirmPrePostStatus.class, phase = TransactionPhase.AFTER_COMMIT)
    public void confirmPrePost(ConfirmPrePostStatus event) {
        log.info("EVENT - Sending confirm prePost mail: {}", event);
        String html = confirmPrePostTemplate.build(event);
        MimeMessage message = javaMailSender.createMimeMessage();
        try {
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
            helper.setTo(event.requester().getEmail());
            helper.setSubject("์—ฝ์„œ ๋“ฑ๋ก ์Šน์ธ");
            helper.setFrom(from);
            helper.setText(html, true);
            helper.addInline("img", confirmPrePostTemplate.bindImg(event.img()));
        } catch (MessagingException e) {
            log.error("sendConfirmPrePostMail Exception", e);
            throw new MessagingBuildException("ConfirmPrePostMail");
        }
        javaMailSender.send(message);
    }
}

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•˜๊ฒŒ ๋ด์•ผํ•˜๋Š” ๊ฑด phase = TransactionPhase.AFTER_COMMIT์ด๋‹ค.

  • phase = TransactionPhase.AFTER_COMMIT : ํŠธ๋žœ์žญ์…˜ ์ž‘์—…์„ ์ปค๋ฐ‹ ํ›„ ์ž‘์—…์„ ์ง„ํ–‰.
    • ๋ฉ”์ผ ๋ฐœ์†ก ์‹คํŒจ๊ฐ€ DB ์ž‘์—…์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค.

๊ฒฐ๋ก 

๊ธฐ์กด์—๋Š” ํ•˜๋‚˜์˜ ์—ฝ์„œ ์Šน์ธ Class์—์„œ ์Šน์ธ ์ฒ˜๋ฆฌ + ๋“ฑ๋ก + ๋ฉ”์ผ ์ „์†ก์„ ๋ชจ๋‘ ์ฒ˜๋ฆฌํ–ˆ๋‹ค.
์ด ๋ฐฉ์‹์€ ํด๋ž˜์Šค๊ฐ€ 3๊ฐ€์ง€ ์ฑ…์ž„์„ ๋™์‹œ์— ๊ฐ€์ง€๊ฒŒ ๋˜์–ด ์œ ์ง€๋ณด์ˆ˜์™€ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ์„ ๋ถ™์ด๊ธฐ๊ฐ€ ์–ด๋ ค์› ๋‹ค.

Spring Event๋ฅผ ๋„์ž…ํ•œ ํ›„์—๋Š”

  • ์Šน์ธ ํด๋ž˜์Šค๋Š” ์˜ค์ง ์ƒํƒœ ๋ณ€๊ฒฝ๋งŒ ์ˆ˜ํ–‰
  • ๋“ฑ๋ก/๋ฉ”์ผ ๋“ฑ ํ›„์ฒ˜๋ฆฌ๋Š” ๋ณ„๋„ ๋ฆฌ์Šค๋„ˆ์—์„œ ์ฒ˜๋ฆฌ
  • ๊ฒฐํ•ฉ๋„๋Š” ๋‚ฎ๊ณ , ๊ธฐ๋Šฅ ํ™•์žฅ์„ฑ์€ ๋†’์•„์ง

โœ”๏ธ ์ƒˆ๋กœ์šด ํ›„์ฒ˜๋ฆฌ๋ฅผ ๋ถ™์ผ ๋•Œ๋Š” ๊ธฐ์กด ํด๋ž˜์Šค ์ˆ˜์ • ์—†์ด
@TransactionalEventListener๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋!

์ด ์ฒ˜๋Ÿผ Spring Event๋ฅผ ๋„์ž… ํ›„ ์œ ์ง€๋ณด์ˆ˜์™€ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ์‹œ ์ด์ ์„ ํ•จ๊นจ ๊ฐ€์ ธ๊ฐ€๊ฒŒ ๋˜์—ˆ๋‹ค.

profile
no-intelli ๊ฐœ๋ฐœ์ž ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜๋„ intellij๋Š” ์”๋‹ˆ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€