์๋ฐ ๋ฉ์ผ ๋ฐ์ก ์ ์ฅ์ ์ฒ๋ฆฌ
์คํ๋ง ํ๋ ์์ํฌ ๊ธฐ๋ฐ์ ์ ํ๋ฆฌ์ผ์ด์
์๋ฒ์์๋ 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 ํจ์ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌํ ๋ ์ฐ๊ฒฐ์ ์ํํ๊ณ ํด์ ํ๋ฏ๋ก ์ด๋ฉ์ผ์ ํ๋์ฉ ๋ณด๋ด๋๋ก ๊ตฌํํ๋ค๋ฉด ์ด๋ฉ์ผ์ ๋ณด๋ผ๋๋ง๋ค ์ฐ๊ฒฐ์ ์ํํ๋ ๊ฒ์ด๋ค.
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 ์ ๋ํ ์ปค๋ฅ์ ํ์ ์ด์ฉํด๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ผ๋ก ์๊ฐ๋๋ค.