Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 서버 엔지니어
- tmax tibero
- 2026 하반기 대기업 반드시 갑니다
- 서버 개발자
- java #예외처리 #throw #throws
- 2798블랙잭
- 25304번
- 브루트 포스법
- level3
- 반복문
- server engineer
- software enginner
- ndc2025
- server developer
- 올 겨울은 조금 따뜻할 것 같다.
- 백엔드 개발자 로드맵
- 주니어 백엔드 개발자
- 자바 #자바문법 #자바기초 #참조형 #기본형
- object 클래스 # java
- heap area #stack area #static area #jvm
- 이분탐색
- static #자바 메모리 구조 #멤버 변수
- 나는야 4학년 #5학년 까지 가보자구
- Spring
- Next.js
- java #추상클래스
- 넥슨개발자컨퍼런스
- tibero 7.23
- 정보처리기사 실기 #정처기 실기 #2024년 2회 #정처기 2024년 2회 #공부법 # 꿀팁
- level2
Archives
- Today
- Total
개발자 쿠키
kafka를 활용한 SMTP 메일 전송 (with. SpringBoot, redis) 본문
1. 메일 전송 비동기 플로우
스케줄러 -> redis 캐시 -> kafka 발행 -> Consumer -> STMP 메일 전송 -> Mailtrap(https://mailtrap.io/home) 저장
시퀀스 다이어그램

2. 기술 스택
- 언어/프레임워크: Java 17, Spring Boot 3.4
- DB: MySQL (JPA/Hibernate)
- 캐시: Redis
- 메시징: Apache Kafka
- 스케줄링: Spring Scheduling (@EnableScheduling, @Scheduled)
- 메일: Spring Boot Starter Mail + Mailtrap
- 컨테이너: Docker Compose (Zookeeper, Kafka, Redis)
3. 프로젝트 설정 (build.gradle)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.kafka:spring-kafka'
implementation 'org.springframework.boot:spring-boot-starter-mail'
}
4. Docker Compose로 redis, kafka, zookeper 띄우기

cd docker-compose.yml 위치이동
docker compose down
docker compose up -d
5. application.yml 설정
spring:
redis:
host: localhost
port: 6379
kafka:
bootstrap-servers: localhost:9092
mail:
host: smtp.mailtrap.io
port: 2525
username: username # mailtrap에서 message 참고
password: password
app:
kafka:
topic:
email: email-topic
6. kafkaConfig & 메시징 컴포넌트 정리
6.1 kafkaConfig - 직렬화/역직렬화 설정
@Configuration
@EnableKafka
public class KafkaConfig {
@Value("${spring.kafka.bootstrap-servers:localhost:9092}")
private String bootstrapServers;
@Value("${app.kafka.topic.email:email-topic}")
private String topicName;
// 1) ProducerFactory: 메시지 프로듀서를 생성하는 팩토리
@Bean
public ProducerFactory<String, EmailPayload> producerFactory() {
Map<String,Object> props = new HashMap<>();
// Kafka 브로커 주소
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
// 메시지 키/값 직렬화 방식
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
return new DefaultKafkaProducerFactory<>(props);
}
// 2) KafkaTemplate: 실제로 메시지를 발행(send)할 때 사용하는 핵심 API
@Bean
public KafkaTemplate<String, EmailPayload> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
// 3) ConsumerFactory: 컨슈머 인스턴스를 생성하는 팩토리
@Bean
public ConsumerFactory<String, EmailPayload> consumerFactory() {
JsonDeserializer<EmailPayload> deserializer = new JsonDeserializer<>(EmailPayload.class);
// 패키지 신뢰 설정 (역직렬화 시 보안)
deserializer.addTrustedPackages("com.quoteline.quote_server.notification.domain");
Map<String,Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, "quote-mail");
// 키/값 역직렬화 클래스
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), deserializer);
}
// 4) ListenerContainerFactory: @KafkaListener를 동작시키는 컨테이너 설정
@Bean
public ConcurrentKafkaListenerContainerFactory<String, EmailPayload>
kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, EmailPayload> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
// 옵션: 에러 핸들러, 동시 컨슈머 수 등 추가 설정 가능
return factory;
}
// 5) NewTopic: 애플리케이션 시작 시 토픽이 없으면 자동 생성
@Bean
public NewTopic emailTopic() {
// 파티션 개수:1, 레플리카 팩터:1
return new NewTopic(topicName, 1, (short)1);
}
}producerFactory는 kafka 프로듀서를 구성하는 기본 속성입니다. KafkaTemplate는 메시지를 지정된 토픽으로 보내는 API이고 consummerFactory는 kafka 컨슈머를 구성하는 기본속성을 세팅합니다. kafkaListnerContainerFactory는 @KafkaLinstner가 동작할 컨슈머 컨테이너 팩토리로 병렬 리스닝 및 에러 핸들링을 설정합니다.
emailTopic은 애플리케이션 구동 시 자동으로 email-topic을 생성합니다.
6.2 EmailPayload - 메시지 페이로드 정의
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class EmailPayload {
@Email @NotBlank
private String email;
@NotBlank
private String quote;
}- DTO 역할: 프로듀서가 발행하고, 컨슈머가 수신할 때 사용하는 메시지 모델
- Validation: @Email, @NotBlank (컨트롤러 바인딩 혹은 수동 검증 시에만 동작)
6.3 NotificationProducer.java – Kafka 메시지 발행
@Service
@RequiredArgsConstructor
public class NotificationProducer {
private final KafkaTemplate<String, EmailPayload> kafkaTemplate;
@Value("${app.kafka.topic.email}")
private String topic;
public void send(EmailPayload payload) {
// key: 수신자 이메일, value: EmailPayload
kafkaTemplate.send(topic, payload.getEmail(), payload);
}
}- send() 호출 시 비동기로 브로커에 메시지 전송
- partition key 로 이메일을 사용하면, 같은 이메일 간 메시지는 동일 파티션에 정렬 보장
6.4 NotificationConsumer.java – Kafka 메시지 수신 & 메일 전송
@Service
@Slf4j
@RequiredArgsConstructor
public class NotificationConsumer {
private final JavaMailSender mailSender;
@KafkaListener(
topics = "${app.kafka.topic.email}",
groupId = "quote-mail"
)
public void consume(EmailPayload payload) {
// 1) 메일 작성
SimpleMailMessage msg = new SimpleMailMessage();
msg.setFrom("no-reply@quoteline.com");
msg.setTo(payload.getEmail());
msg.setSubject("오늘의 명언");
msg.setText(payload.getQuote());
// 2) SMTP 전송
mailSender.send(msg);
log.info("Sent email to {} › {}", payload.getEmail(), payload.getQuote());
}
}- @KafkaListener가 메시지를 폴링 → consume() 호출
- JavaMailSender로 동기 메일 전송
- 예외 발생 시 기본 DefaultErrorHandler가 재시도 로직 수행
7. RedisConfig & 캐시 컴포넌트
@Configuration
public class RedisConfig {
// ① LettuceConnectionFactory: 기본 싱글 노드 Redis 연결 팩토리
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory("localhost", 6379);
}
// ② RedisTemplate<String,String>: 문자열 키/값 전용 템플릿
@Bean
@Primary
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory cf) {
RedisTemplate<String, String> tpl = new RedisTemplate<>();
tpl.setConnectionFactory(cf);
// 키·값을 모두 StringSerializer로 직렬화
StringRedisSerializer s = new StringRedisSerializer();
tpl.setKeySerializer(s);
tpl.setValueSerializer(s);
tpl.afterPropertiesSet();
return tpl;
}
}- LettuceConnectionFactory
- localhost:6379에 Redis 접속.
- RedisTemplate
- opsForValue()로 단순 문자열 캐시: get, set(key,val,TTL) 호출 가능.
- @Primary 지정해, Spring이 StringRedisTemplate 대신 여기 빈을 우선 주입하도록 함
8. 스케줄러: DailyNotificationScheduler
@Service
@RequiredArgsConstructor
public class DailyNotificationScheduler {
private final SubscriptionRepository repo;
private final QuoteService quoteService;
private final NotificationProducer producer;
// 매일 오전 9시 실행
@Scheduled(cron = "0 0 9 * * *")
public void sendMorningQuotes() {
send(TimeSegment.AM);
}
// 매일 오후 6시 실행
@Scheduled(cron = "0 0 18 * * *")
public void sendEveningQuotes() {
send(TimeSegment.PM);
}
private void send(TimeSegment seg) {
// 1) 해당 시간대 구독자 조회
List<Subscription> subs = repo.findByTimeSegment(seg);
// 2) 캐시 혹은 신규 명언 발급
String quote = quoteService.getQuote();
// 3) Kafka 발행
subs.forEach(s -> {
EmailPayload payload = new EmailPayload(s.getEmail(), quote);
producer.send(payload);
});
}
}- @EnableScheduling 어노테이션이 QuoteServerApplication에 선언되어 있어야 작동
- cron 표현식으로 매일 오전·오후 스케줄링
- DB → 캐시 → 메시징 → 메일 전송의 트리거 역할
추후, 오전 시간대와 오후 시간대를 늘려서 사용자가 다양한 시간에 알림을 받을 수 있도록 확장할 예정입니다.
9. 전체 흐름 요약
- 스케줄러가 매일 지정된 시각에 구독자를 조회
- Docker Compose로 Zookeeper·Kafka·Redis 로컬 환경 일괄 구성
- QuoteService가 Redis 캐시로 하루 1회 명언 발급
- NotificationProducer가 Kafka 토픽에 EmailPayload 발행
- NotificationConsumer가 Kafka에서 메시지 수신 후 JavaMailSender로 SMTP 전송
- Mailtrap 웹 UI에서 실제 메일 수신 확인
- Docker Compose로 Zookeeper·Kafka·Redis 로컬 환경 일괄 구성