Skip to content

Commit cc4053b

Browse files
authored
Merge pull request #120 from cholog-project/feat/smtp-mock
Feat/ smtp Spring Batch를 사용한 이메일 전송 - Mock
2 parents c030775 + 9c9514f commit cc4053b

11 files changed

Lines changed: 285 additions & 1 deletion

File tree

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ dependencies {
6060
// redisson
6161
implementation 'org.redisson:redisson-spring-boot-starter:3.35.0'
6262

63+
// batch
64+
implementation 'org.springframework.boot:spring-boot-starter-batch'
65+
66+
6367
// QueryDSL (Jakarta 기반)
6468
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
6569
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'

src/main/java/com/example/braveCoward/BraveCowardApplication.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package com.example.braveCoward;
22

3+
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
34
import org.springframework.boot.SpringApplication;
45
import org.springframework.boot.autoconfigure.SpringBootApplication;
6+
import org.springframework.scheduling.annotation.EnableAsync;
57
import org.springframework.scheduling.annotation.EnableScheduling;
68

79
@SpringBootApplication
810
@EnableScheduling
11+
@EnableAsync
12+
@EnableBatchProcessing
913
public class BraveCowardApplication {
1014

1115
public static void main(String[] args) {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.example.braveCoward.batchinsert.email;
2+
3+
import java.time.LocalDate;
4+
import java.util.Arrays;
5+
import java.util.Iterator;
6+
import java.util.List;
7+
8+
import org.springframework.batch.core.configuration.annotation.StepScope;
9+
import org.springframework.batch.item.ItemReader;
10+
import org.springframework.stereotype.Component;
11+
12+
import com.example.braveCoward.model.Plan;
13+
import com.example.braveCoward.repository.PlanRepository;
14+
15+
@Component
16+
@StepScope
17+
public class EmailPlanReader implements ItemReader<Plan> {
18+
private final Iterator<Plan> planIterator;
19+
private static final List<Plan.Status> VALID_STATUSES = Arrays.asList(Plan.Status.NOT_STARTED,
20+
Plan.Status.IN_PROGRESS);
21+
22+
public EmailPlanReader(PlanRepository planRepository){
23+
long start = System.currentTimeMillis();
24+
List<Plan> plans = planRepository.findPlansWithUsers(LocalDate.now(), VALID_STATUSES);
25+
System.out.println(plans.size());
26+
long end = System.currentTimeMillis();
27+
System.out.println("plan 조회 시간 : " + (end - start) + "ms");
28+
29+
this.planIterator = plans.iterator();
30+
}
31+
32+
@Override
33+
public Plan read(){
34+
return planIterator.hasNext() ? planIterator.next() :null;
35+
}
36+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.example.braveCoward.batchinsert.email;
2+
3+
import org.springframework.batch.item.ItemProcessor;
4+
import org.springframework.stereotype.Component;
5+
6+
import com.example.braveCoward.model.Plan;
7+
import com.example.braveCoward.model.User;
8+
9+
@Component
10+
public class EmailProcessor implements ItemProcessor<Plan, String> {
11+
12+
@Override
13+
public String process(Plan plan){
14+
User user = plan.getTeamMember().getUser();
15+
return user.getEmail();
16+
}
17+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.example.braveCoward.batchinsert.email;
2+
3+
import org.springframework.batch.core.configuration.annotation.StepScope;
4+
import org.springframework.batch.item.Chunk;
5+
import org.springframework.batch.item.ItemWriter;
6+
import org.springframework.stereotype.Component;
7+
8+
import com.example.braveCoward.mock.MockEmailService;
9+
10+
@Component
11+
@StepScope
12+
public class EmailWriter implements ItemWriter<String> {
13+
14+
private final MockEmailService mockEmailService;
15+
16+
public EmailWriter(MockEmailService mockEmailService){
17+
this.mockEmailService = mockEmailService;
18+
}
19+
20+
@Override
21+
public void write(Chunk<? extends String> emails){
22+
for (String email : emails){
23+
mockEmailService.sendMockEmail(email);
24+
}
25+
}
26+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.example.braveCoward.config;
2+
3+
import org.springframework.batch.core.Job;
4+
import org.springframework.batch.core.Step;
5+
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
6+
import org.springframework.batch.core.configuration.annotation.JobScope;
7+
import org.springframework.batch.core.job.builder.JobBuilder;
8+
import org.springframework.batch.core.launch.support.RunIdIncrementer;
9+
import org.springframework.batch.core.repository.JobRepository;
10+
import org.springframework.batch.core.step.builder.StepBuilder;
11+
import org.springframework.batch.item.ItemProcessor;
12+
import org.springframework.batch.item.ItemReader;
13+
import org.springframework.batch.item.ItemWriter;
14+
import org.springframework.beans.factory.annotation.Qualifier;
15+
import org.springframework.context.annotation.Bean;
16+
import org.springframework.context.annotation.Configuration;
17+
import org.springframework.core.task.TaskExecutor;
18+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
19+
import org.springframework.transaction.PlatformTransactionManager;
20+
21+
import com.example.braveCoward.model.Plan;
22+
23+
import lombok.extern.slf4j.Slf4j;
24+
25+
@Slf4j
26+
@Configuration
27+
@EnableBatchProcessing
28+
public class EmailBatchConfig {
29+
30+
private final String JOB_NAME = "emailJob";
31+
private final String STEP_NAME = "emailStep";
32+
33+
@Bean
34+
public Job emailJob(
35+
JobRepository jobRepository,
36+
Step emailStep
37+
) {
38+
return new JobBuilder(JOB_NAME, jobRepository)
39+
.incrementer(new RunIdIncrementer())
40+
.start(emailStep)
41+
.build();
42+
}
43+
44+
@Bean
45+
@JobScope
46+
public Step emailStep(
47+
JobRepository jobRepository,
48+
PlatformTransactionManager transactionManager,
49+
ItemReader<Plan> emailReader,
50+
ItemProcessor<Plan, String> emailProcessor,
51+
ItemWriter<String> emailWriter,
52+
@Qualifier("emailBatchExecutor") TaskExecutor emailBatchExecutor
53+
) {
54+
return new StepBuilder(STEP_NAME, jobRepository)
55+
.<Plan, String>chunk(4, transactionManager) // 10개씩 처리
56+
.reader(emailReader)
57+
.processor(emailProcessor)
58+
.writer(emailWriter)
59+
.taskExecutor(emailBatchExecutor) // 병렬 처리 적용
60+
.build();
61+
}
62+
63+
@Bean(name = "emailBatchExecutor")
64+
public TaskExecutor emailBatchExecutor() {
65+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
66+
executor.setCorePoolSize(8);
67+
executor.setMaxPoolSize(16);
68+
executor.setQueueCapacity(40);
69+
executor.setThreadNamePrefix("EmailBatchThread-");
70+
executor.initialize();
71+
return executor;
72+
}
73+
}

src/main/java/com/example/braveCoward/controller/AlarmController.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.springframework.web.bind.annotation.RequestMapping;
55
import org.springframework.web.bind.annotation.RestController;
66

7+
import com.example.braveCoward.mock.MockEmailService;
78
import com.example.braveCoward.service.AlarmService;
89
import com.example.braveCoward.service.PlanNotificationScheduler;
910

@@ -21,5 +22,22 @@ public String sendScheduledEmailNow() {
2122
planNotificationScheduler.sendPlanNotifications();
2223
return "스케줄링된 이메일이 즉시 실행되었습니다.";
2324
}
25+
26+
@GetMapping("/send-one-email")
27+
public String sendOneEmail() {
28+
planNotificationScheduler.sendOneEmail();
29+
return "스케줄링된 이메일이 즉시 실행되었습니다.";
30+
}
31+
32+
@GetMapping("/send-ten-email")
33+
public String sendtenEmail() {
34+
planNotificationScheduler.sendTenEmail();
35+
return "스케줄링된 이메일이 즉시 실행되었습니다.";
36+
}
37+
38+
@GetMapping("/send-mock-email")
39+
public void sendMockEmailNotification(){
40+
planNotificationScheduler.sendMockEmailNotification();
41+
}
2442
}
2543

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.example.braveCoward.controller;
2+
3+
import org.springframework.batch.core.Job;
4+
import org.springframework.batch.core.JobExecution;
5+
import org.springframework.batch.core.JobParameters;
6+
import org.springframework.batch.core.JobParametersBuilder;
7+
import org.springframework.batch.core.launch.JobLauncher;
8+
import org.springframework.web.bind.annotation.*;
9+
10+
import lombok.extern.slf4j.Slf4j;
11+
12+
@Slf4j
13+
@RestController
14+
@RequestMapping("/batch")
15+
public class BatchController {
16+
17+
private final JobLauncher jobLauncher;
18+
private final Job emailJob;
19+
20+
public BatchController(JobLauncher jobLauncher, Job emailJob) {
21+
this.jobLauncher = jobLauncher;
22+
this.emailJob = emailJob;
23+
}
24+
25+
@GetMapping("/sendEmails")
26+
public String sendEmails() {
27+
try {
28+
JobParameters params = new JobParametersBuilder()
29+
.addLong("time", System.currentTimeMillis())
30+
.toJobParameters();
31+
32+
JobExecution jobExecution = jobLauncher.run(emailJob, params);
33+
log.info("📨 이메일 전송 Batch 실행됨. 실행 ID: {}", jobExecution.getId());
34+
35+
return "✅ Spring Batch Job 실행됨! (Execution ID: " + jobExecution.getId() + ")";
36+
} catch (Exception e) {
37+
log.error("배치 실행 실패: {}", e.getMessage());
38+
return "Spring Batch 실행 실패: " + e.getMessage();
39+
}
40+
}
41+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.example.braveCoward.mock;
2+
3+
import java.util.concurrent.TimeUnit;
4+
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
import org.springframework.scheduling.annotation.Async;
8+
import org.springframework.stereotype.Service;
9+
10+
@Service
11+
public class MockEmailService {
12+
private static final Logger logger = LoggerFactory.getLogger(MockEmailService.class);
13+
14+
@Async ("emailBatchExecutor")
15+
public void sendMockEmail(String email) {
16+
try {
17+
TimeUnit.MILLISECONDS.sleep(4500); // 4.5초 대기
18+
logger.info("Mock 이메일 전송 완료: {}", email);
19+
} catch (InterruptedException e) {
20+
Thread.currentThread().interrupt();
21+
logger.error("이메일 전송 실패: {}", email, e);
22+
}
23+
}
24+
}

src/main/java/com/example/braveCoward/repository/PlanRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import org.springframework.data.domain.Page;
66
import org.springframework.data.domain.Pageable;
7+
import org.springframework.data.jpa.repository.EntityGraph;
78
import org.springframework.data.repository.Repository;
89

910
import com.example.braveCoward.model.Plan;
@@ -27,6 +28,11 @@ public interface PlanRepository extends Repository<Plan, Long> {
2728
List<Plan> findByEndDateAndStatusIn(@Param("targetDate") LocalDate targetDate,
2829
@Param("statuses") List<Plan.Status> statuses);
2930

31+
@EntityGraph(attributePaths = {"teamMember.user"})
32+
@Query("SELECT p FROM Plan p WHERE p.endDate = :targetDate AND p.status IN :statuses")
33+
List<Plan> findPlansWithUsers(@Param("targetDate") LocalDate targetDate,
34+
@Param("statuses") List<Plan.Status> statuses);
35+
3036
Page<Plan> findAllByProjectId(Long projectId, Pageable pageable);
3137

3238
List<Plan> findByEndDate(LocalDate endDate);

0 commit comments

Comments
 (0)