์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ ๊ธฐ๋ฐ˜์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋ฒ„์—์„œ๋Š” JavaMailSenderImpl์„ ์‚ฌ์šฉํ•˜์—ฌ ์‰ฝ๊ฒŒ ์ด๋ฉ”์ผ์„ ๋ณด๋‚ด๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, ์ด๋ฉ”์ผ ๋ฐœ์†ก์— ๋Œ€ํ•œ ์ธํ„ฐํŽ˜์ด์Šค์™€ ๊ตฌํ˜„์„ ์ œ๊ณตํ•˜๋ฏ€๋กœ ๋ฉ”์ผ ๋ฐœ์†ก ์‹คํŒจ์— ๋”ฐ๋ฅธ ์žฅ์•  ์ฒ˜๋ฆฌ์— ๋Œ€ํ•ด์„œ๋Š” ๋ณ„๋„๋กœ ๊ณ ๋ คํ•ด์•ผํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๋‹ค์ˆ˜์˜ ์Šค๋ ˆ๋“œ๋กœ ์ด๋ฉ”์ผ ๋Œ€๊ธฐ์—ด ํ๋ฅผ ๋น ๋ฅด๊ฒŒ ์†Œ์ง„ํ•œ๋‹ค๋ฉด Amazon SES ๊ณ„์ •์˜ ๋ฐœ์‹  ํ• ๋‹น๋Ÿ‰๊ณผ ๊ด€๋ จ๋œ ์˜ค๋ฅ˜ ์ค‘ ์ดˆ๋‹น ์ด๋ฉ”์ผ ์ˆ˜์— ๋Œ€ํ•œ ์ œํ•œ๋Ÿ‰์„ ๋„˜์–ด์„œ๋Š” ๊ฒฝ์šฐ 454 Throttling failure: Maximum sending rate exceeded ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

ThreadPoolExecutor๋ฅผ ํ†ตํ•œ ๋ฉ”์ผ ๋Œ€๊ธฐ์—ด ํ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ

BlockingQueue<MimeMessage> waitingQueue = new LinkedBlockingQueue<>(50000);

Thread thread = new Thread(() -> {
    while (true) {
        try {
            MimeMessage message = waitingQueue.take();
            javaMailSender.send(message);
        } catch (Throwable e) {
            log.error(e.getMessage(), e);
        }
    }
});

thread.setName("Mail-Thread");
thread.setDaemon(true);
thread.start();

์œ„ ์ฝ”๋“œ๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ฉ”์ผ ๋Œ€๊ธฐ์—ด ํ๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ์†Œ์ง„ํ•˜๋Š” ๋‹จ์ผ ์Šค๋ ˆ๋“œ๋กœ ์ด๋ฉ”์ผ์„ ๋ฐœ์†กํ•˜๋Š” ์ฝ”๋“œ๋กœ ๋Œ€๊ธฐ์—ด ํ์— ์Œ“์ด๋Š” ๋ฉ”์ผ์˜ ์ˆ˜๊ฐ€ ๋งŽ์•„์ง„๋‹ค๋ฉด ThreadPoolExecutor๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์ผ ๋ฐœ์†ก์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋ฅผ ๋ณ‘๋ ฌ๋กœ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฉ”์ผ ๋ฐœ์†ก ์ฒ˜๋ฆฌ๋ฅผ ๋ณ‘๋ ฌ๋กœ ์ˆ˜ํ–‰ํ•œ๋‹ค๋ฉด SMTP ์„œ๋ฒ„์˜ ๋ฐœ์‹  ํ•œ๋„์— ๋Œ€ํ•ด์„œ ๊ณ ๋ คํ•ด์•ผํ•œ๋‹ค.

int coreSize = Runtime.getRuntime().availableProcessors();
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("Mail-Thread-%d").build();
ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, coreSize, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), threadFactory);

BlockingQueue<MimeMessage> waitingQueue = new LinkedBlockingQueue<>(50000);

Thread thread = new Thread(() -> {
    while (true) {
        if (waitingQueue.isEmpty()) {
            continue;
        }

        executor.execute(() -> {
            try {
                MimeMessage message = waitingQueue.take();
                javaMailSender.send(message);
            } catch (Throwable e) {
                log.error(e.getMessage(), e);
            }
        });
    }
});

thread.setName("Mail-Thread");
thread.setDaemon(true);
thread.start();

Spring Retry๋ฅผ ํ†ตํ•œ ๋ฉ”์ผ ๋ฐœ์†ก ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ์ „๋žต - Guide to Spring Retry

์ด๋ฉ”์ผ ๋ฐœ์†ก ์‹คํŒจ ๊ฑด์— ๋Œ€ํ•ด์„œ Spring Retry๋ฅผ ํ†ตํ•ด ์žฌ์‹œ๋„ ๋กœ์ง์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฉ”์ผ ๋ฐœ์†ก์„ ์œ„ํ•œ RetryTemplate ๊ตฌ์„ฑ ์‹œ ์žฌ์‹œ๋„ ์ „๋žต์„ ๊ตฌ์„ฑํ•˜๊ณ  ๋ฉ”์ผ์„ ๋ฐœ์†กํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ RetryTemplate๋กœ ๊ฐ์‹ธ๋ฉด ๋œ๋‹ค. context.getRetryCount() ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์žฌ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

RetryTemplate retryTemplate = new RetryTemplateBuilder()
        .maxAttempts(3)
        .exponentialBackoff(Duration.ofSeconds(10L), 2, Duration.ofMinutes(1L))
        .retryOn(List.of(MessagingException.class, MailException.class))
        .build();

BlockingQueue<MimeMessage> waitingQueue = new LinkedBlockingQueue<>(50000);

Thread thread = new Thread(() -> {
    while (true) {
        if (waitingQueue.isEmpty()) {
            continue;
        }

        retryTemplate.execute(context -> {
            try {
                MimeMessage message = waitingQueue.take();
                javaMailSender.send(message);
            } catch (Throwable e) {
                log.error(e.getMessage(), e);
            }
        });
    }
});

thread.setName("Mail-Thread");
thread.setDaemon(true);
thread.start();

Guava RateLimiter๋ฅผ ํ†ตํ•œ ์ดˆ๋‹น ์ด๋ฉ”์ผ ๋ฐœ์†ก ์‹œ๋„ ์ œํ•œ - Quick Guide to the Guava RateLimiter

AWS SES์˜ ๋ฐœ์‹  ํ•œ๋„ ์ค‘์—๋Š” ์ดˆ๋‹น ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋Š” ์ด๋ฉ”์ผ ์ˆ˜์— ๋Œ€ํ•œ ์ œํ•œ๋Ÿ‰์ด ์žˆ์œผ๋ฏ€๋กœ Guava RateLimiter๋ฅผ ํ†ตํ•ด ์ดˆ๋‹น ์ด๋ฉ”์ผ ๋ฐœ์†ก ์ˆ˜๋ฅผ ๋„˜์–ด์„œ์ง€ ์•Š๋„๋ก ๋ฐฉ์–ดํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฌผ๋ก , SMTP ์„œ๋ฒ„๋ฅผ ๋‹ค์ˆ˜์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋ฒ„์—์„œ๋„ ์—ฐ๊ฒฐํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์œผ๋ฏ€๋กœ ๋ฐœ์‹  ํ• ๋‹น๋Ÿ‰์— ๋Œ€ํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋ฐœ์‹  ํ•œ๋„๋ฅผ ๋ณ„๋„๋กœ ๊ด€๋ฆฌํ•  ํ•„์š”๋Š” ์žˆ๋‹ค.

RateLimiter rateLimiter = RateLimiter.create(14);

Thread thread = new Thread(() -> {
    while (true) {
        if (waitingQueue.isEmpty()) {
            continue;
        }

        boolean acquire = rateLimiter.tryAcquire(1);
        if (acquire) {
            executor.execute(() -> {
                try {
                    MimeMessage message = waitingQueue.take();
                    javaMailSender.send(message);
                } catch (Throwable e) {
                    log.error(e.getMessage(), e);
                }
            });
        }
    }
});
thread.setName("Mail-Thread");
thread.setDaemon(true);
thread.start();

Guava RateLimiter๋Š” ์ดˆ๋‹น ํ˜ธ์ถœ์— ๋Œ€ํ•œ ์ œํ•œ๋งŒ ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ ๋ถ„๋‹น ์ด๋ฉ”์ผ ๋ฐœ์†ก์„ ์ œํ•œํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด Resilience4j์˜ RateLimiter๋ฅผ ๋„์ž…ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

Resilience4j CircuitBreaker๋ฅผ ํ†ตํ•œ ๋ฉ”์ผ ๋ฐœ์†ก ์ค‘๋‹จ - Guide to Resilience4j

SMTP ์„œ๋ฒ„์˜ ๋ฐœ์‹  ํ•œ๋„ ์ œํ•œ์„ ๋„˜์–ด์„œ๋Š” ๊ฒฝ์šฐ์— ๋Œ€ํ•œ ์žฅ์•  ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด Resilience4j๋ฅผ ๋„์ž…ํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜๋ฃจ๋™์•ˆ ๋ฉ”์ผ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋Š” ํ• ๋‹น๋Ÿ‰์„ ์ดˆ๊ณผํ•˜๊ฑฐ๋‚˜ ์ดˆ๋‹น ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋Š” ์ด๋ฉ”์ผ ์ˆ˜์— ์ œํ•œ์ด ๋˜์—ˆ๋‹ค๋ฉด ์ผ์ • ์‹œ๊ฐ„๋™์•ˆ ์ด๋ฉ”์ผ ๋ฐœ์†ก์„ ์‹œ๋„ํ•˜์ง€ ์•Š๋„๋ก CircuitBreaker๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์žฅ์•  ์ „ํŒŒ ๋ฐฉ์ง€๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค. AWS SES์˜ ๋ฐœ์‹  ํ•œ๋„์ธ ์•„๋ž˜์˜ ๋‘๊ฐœ ํ•ญ๋ชฉ์— ๋Œ€ํ•ด์„œ ์ฒ˜๋ฆฌ๋ฅผ ๊ณ ๋ คํ•˜๋„๋ก ํ•˜์ž.

  • 454 Throttling failure: Maximum sending rate exceeded (1์ดˆ๋‹น ์ด๋ฉ”์ผ ๋ฐœ์†ก ์ˆ˜ ์ œํ•œ๋Ÿ‰)
  • 454 Throttling failure: Daily message quota exceeded (24์‹œ๊ฐ„ ๋‹น ์ด๋ฉ”์ผ ๋ฐœ์†ก ํ• ๋‹น๋Ÿ‰)
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("MailSendingLimitExceeded");
RateLimiter rateLimiter = RateLimiter.create(14);

Thread thread = new Thread(() -> {
    while (true) {
        if (waitingQueue.isEmpty()) {
            continue;
        }

        boolean permission = circuitBreaker.tryAcquirePermission();
        permission &= rateLimiter.tryAcquire(1);
        if (permission) {
            executor.execute(() -> {
                try {
                    MimeMessage message = waitingQueue.take();
                    javaMailSender.send(message);
                } catch (Throwable e) {
                    String failReason = e.getMessage();
                    if (failReason != null
                            && failReason.contains("454 Throttling failure")) {
                        circuitBreaker.transitionToClosedState();
                    }
                    log.error(failReason, e);
                }
            });
        }
    }
});
thread.setName("Mail-Thread");
thread.setDaemon(true);
thread.start();

JavaMailSenderImpl์€ ๋งค๋ฒˆ ์—ฐ๊ฒฐํ•œ๋‹ค?!

์ผ๋ถ€ ์‹œ์Šคํ…œ ํ™˜๊ฒฝ์—์„œ AWS SES์˜ SMTP ์„œ๋ฒ„๋ฅผ ๋™์ผํ•œ ๋ฆฌ์ „์ด ์•„๋‹Œ ์ƒ๋‹นํžˆ ๋ฉ€๋ฆฌ ๋–จ์–ด์ ธ์žˆ๋Š” ๋ฆฌ์ „์— ๊ตฌ์„ฑ๋œ SMTP๋ฅผ ํ†ตํ•ด ๋ฉ”์ผ ๋ฐœ์†ก์„ ์‹œ๋„ํ•˜๋Š” ๊ฒฝ์šฐ ์ปค๋„ฅ์…˜์— ๋Œ€ํ•œ ์†Œ์š” ์‹œ๊ฐ„์ด ํฌ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์—ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, US East (Ohio) ๋ฆฌ์ „์˜ SMTP ์—”๋“œํฌ์ธํŠธ๋ฅผ Asia Pacific (Seoul) ๋ฆฌ์ „์—์„œ ์—ฐ๊ฒฐํ•˜๋Š” ๊ฒฝ์šฐ ์•ฝ 2์ดˆ ์ •๋„์˜ ์‹œ๊ฐ„์ด ์†Œ์š”๋˜๋Š”๋ฐ ๋™์ผํ•œ ๋ฆฌ์ „์—์„œ ์—ฐ๊ฒฐํ•˜๋ฉด ์•ฝ 100ms ๊ฐ€ ๊ฑธ๋ฆฐ๋‹ค.

JavaMailSenderImpl์˜ connectTransport ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ testConnection ๋˜๋Š” doSend ํ•จ์ˆ˜์—์„œ ์—ฐ๊ฒฐ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ MimeMessage ๋ชฉ๋ก์„ send ํ•จ์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „๋‹ฌํ•  ๋•Œ ์—ฐ๊ฒฐ์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ํ•ด์ œํ•˜๋ฏ€๋กœ ์ด๋ฉ”์ผ์„ ํ•˜๋‚˜์”ฉ ๋ณด๋‚ด๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค๋ฉด ์ด๋ฉ”์ผ์„ ๋ณด๋‚ผ๋•Œ๋งˆ๋‹ค ์—ฐ๊ฒฐ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

JavaMailSenderImpl
protected Transport connectTransport() throws MessagingException { String username = this.getUsername(); String password = this.getPassword(); if ("".equals(username)) { username = null; if ("".equals(password)) { password = null; } } Transport transport = this.getTransport(this.getSession()); transport.connect(this.getHost(), this.getPort(), username, password); return transport; }

SimpleJavaMail์˜ Batch Module์€ Transport ์—ฐ๊ฒฐ์— ๋Œ€ํ•œ ์ปค๋„ฅ์…˜ ํ’€์„ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ๋˜์–ด์žˆ์œผ๋ฏ€๋กœ SMTP ์„œ๋ฒ„๋กœ์˜ ์—ฐ๊ฒฐ ์ˆ˜ํ–‰์‹œ๊ฐ„์ด ์˜ค๋ž˜๊ฑธ๋ฆฐ๋‹ค๋ฉด Transport ์— ๋Œ€ํ•œ ์ปค๋„ฅ์…˜ ํ’€์„ ์ด์šฉํ•ด๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์œผ๋กœ ์ƒ๊ฐ๋œ๋‹ค.