commit
This commit is contained in:
@@ -42,6 +42,14 @@
|
|||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
<artifactId>spring-cloud-starter</artifactId>
|
<artifactId>spring-cloud-starter</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.openfeign</groupId>
|
||||||
|
<artifactId>feign-okhttp</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
|||||||
@@ -43,5 +43,10 @@ public class BizBaseException extends RuntimeException {
|
|||||||
super(exceptionRule.getMessage());
|
super(exceptionRule.getMessage());
|
||||||
this.errorRule = exceptionRule;
|
this.errorRule = exceptionRule;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BizBaseException(String message) {
|
||||||
|
super(message);
|
||||||
|
this.errorRule = ExceptionRule.SYSTEM_ERROR.message(message);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.spring.common.error;
|
package com.spring.common.error;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
@@ -45,5 +48,17 @@ public class GlobalExceptionHandler {
|
|||||||
public BizErrorResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {
|
public BizErrorResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||||
return BizErrorResponse.fromFieldError(e.getFieldErrors());
|
return BizErrorResponse.fromFieldError(e.getFieldErrors());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(DataAccessException.class)
|
||||||
|
public BizErrorResponse handleDataAccessException(DataAccessException e) {
|
||||||
|
Throwable cause = e.getMostSpecificCause();
|
||||||
|
return BizErrorResponse.valueOf(
|
||||||
|
new BizBaseException(
|
||||||
|
Optional.ofNullable(cause)
|
||||||
|
.map(Throwable::getMessage)
|
||||||
|
.orElse(ExceptionRule.SYSTEM_ERROR.getMessage()))
|
||||||
|
.getErrorRule()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.spring.common.properties;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.boot.context.properties.ConstructorBinding;
|
||||||
|
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ConstructorBinding
|
||||||
|
@ConfigurationProperties(prefix = "path")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PathProperties {
|
||||||
|
|
||||||
|
private final Map<String, PathConfig> paths;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class PathConfig {
|
||||||
|
private final String upload;
|
||||||
|
private final String dowonload;
|
||||||
|
|
||||||
|
public PathConfig(
|
||||||
|
@DefaultValue("") String upload,
|
||||||
|
@DefaultValue("") String dowonload
|
||||||
|
) {
|
||||||
|
this.upload = upload;
|
||||||
|
this.dowonload = dowonload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.spring.common.util;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ProfileUtils {
|
||||||
|
|
||||||
|
private final Environment environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 활성화된 프로파일 목록을 반환합니다.
|
||||||
|
*
|
||||||
|
* @return 현재 활성화된 프로파일 배열
|
||||||
|
*/
|
||||||
|
public String[] getActiveProfiles() {
|
||||||
|
return environment.getActiveProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 프로파일이 'prod'인지 확인하는 메소드.
|
||||||
|
*
|
||||||
|
* @return true if the current profile is 'prod', false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isProdProfile() {
|
||||||
|
return isProfileActive("prod");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 프로파일이 'dev'인지 확인하는 메소드.
|
||||||
|
*
|
||||||
|
* @return true if the current profile is 'dev', false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isDevProfile() {
|
||||||
|
return isProfileActive("dev");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 프로파일이 활성화되어 있는지 확인하는 메소드.
|
||||||
|
*
|
||||||
|
* @param profile 확인할 프로파일 이름
|
||||||
|
* @return true if the specified profile is active, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isProfileActive(String profile) {
|
||||||
|
return Arrays.asList(environment.getActiveProfiles()).contains(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 활성화된 프로파일이 여러 개일 경우, 그 중 하나라도 주어진 프로파일이 포함되어 있는지 확인하는 메소드.
|
||||||
|
*
|
||||||
|
* @param profiles 확인할 프로파일 이름 배열
|
||||||
|
* @return true if any of the specified profiles are active, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isAnyProfileActive(String... profiles) {
|
||||||
|
for (String profile : profiles) {
|
||||||
|
if (isProfileActive(profile)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 활성화된 프로파일 중 가장 우선순위가 높은 프로파일을 반환합니다.
|
||||||
|
*
|
||||||
|
* @return 가장 우선순위가 높은 프로파일 이름, 없으면 null
|
||||||
|
*/
|
||||||
|
public String getHighestPriorityProfile() {
|
||||||
|
String[] activeProfiles = getActiveProfiles();
|
||||||
|
return activeProfiles.length > 0 ? activeProfiles[0] : null; // 첫 번째 프로파일이 가장 우선순위가 높다고 가정
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기본 프로파일이 설정되어 있는지 확인하는 메소드.
|
||||||
|
*
|
||||||
|
* @return true if the default profile is active, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isDefaultProfileActive() {
|
||||||
|
return isProfileActive("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 주어진 프로파일이 활성화되어 있지 않은지 확인하는 메소드.
|
||||||
|
*
|
||||||
|
* @param profile 확인할 프로파일 이름
|
||||||
|
* @return true if the specified profile is not active, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isProfileInactive(String profile) {
|
||||||
|
return !isProfileActive(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import org.springframework.batch.core.step.tasklet.Tasklet;
|
|||||||
import org.springframework.batch.repeat.RepeatStatus;
|
import org.springframework.batch.repeat.RepeatStatus;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.spring.domain.email.service.EmailSendService;
|
||||||
import com.spring.domain.post.repository.PostRepository;
|
import com.spring.domain.post.repository.PostRepository;
|
||||||
import com.spring.infra.batch.AbstractBatchTask;
|
import com.spring.infra.batch.AbstractBatchTask;
|
||||||
import com.spring.infra.batch.BatchJobInfo;
|
import com.spring.infra.batch.BatchJobInfo;
|
||||||
@@ -26,10 +27,11 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
public class EmailSendBatch extends AbstractBatchTask {
|
public class EmailSendBatch extends AbstractBatchTask {
|
||||||
|
|
||||||
private final PostRepository postRepository;
|
private final PostRepository postRepository;
|
||||||
|
private final EmailSendService emailSendService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<Step> createSteps() {
|
protected List<Step> createSteps() {
|
||||||
log.info("EmailSendBatch -> createSteps");
|
// log.info("EmailSendBatch -> createSteps");
|
||||||
return List.of(
|
return List.of(
|
||||||
addStep("emailSendJobStep1", createTasklet()),
|
addStep("emailSendJobStep1", createTasklet()),
|
||||||
addStep("emailSendJobStep2", createSendTasklet())
|
addStep("emailSendJobStep2", createSendTasklet())
|
||||||
@@ -40,13 +42,14 @@ public class EmailSendBatch extends AbstractBatchTask {
|
|||||||
protected Tasklet createTasklet() {
|
protected Tasklet createTasklet() {
|
||||||
log.info("EmailSendBatch -> createTasklet");
|
log.info("EmailSendBatch -> createTasklet");
|
||||||
return ((contribution, chunkContext) -> {
|
return ((contribution, chunkContext) -> {
|
||||||
postRepository.findAll();
|
emailSendService.sendEmail();
|
||||||
|
// postRepository.findAll();
|
||||||
return RepeatStatus.FINISHED;
|
return RepeatStatus.FINISHED;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Tasklet createSendTasklet() {
|
private Tasklet createSendTasklet() {
|
||||||
log.info("EmailSendBatch -> createSendTasklet");
|
// log.info("EmailSendBatch -> createSendTasklet");
|
||||||
return ((contribution, chunkContext) -> RepeatStatus.FINISHED);
|
return ((contribution, chunkContext) -> RepeatStatus.FINISHED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.spring.domain.email.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.spring.common.properties.PathProperties;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EmailSendService {
|
||||||
|
|
||||||
|
private final PathProperties pathProperties;
|
||||||
|
|
||||||
|
public void sendEmail() {
|
||||||
|
log.info("pathProperties: {}", pathProperties);
|
||||||
|
log.info("paths: {}", pathProperties.getPaths());
|
||||||
|
|
||||||
|
String upload = pathProperties.getPaths().get("path1").getUpload();
|
||||||
|
log.info(upload);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,31 +16,31 @@ import com.spring.infra.db.orm.jpa.SecondaryJpaConfig;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Slf4j
|
// @Slf4j
|
||||||
@Component
|
// @Component
|
||||||
@BatchJobInfo(
|
// @BatchJobInfo(
|
||||||
group = "${batch-info.post-batch.group}",
|
// group = "${batch-info.post-batch.group}",
|
||||||
jobName = "${batch-info.post-batch.job-name}",
|
// jobName = "${batch-info.post-batch.job-name}",
|
||||||
cronExpression = "${batch-info.post-batch.cron-expression}",
|
// cronExpression = "${batch-info.post-batch.cron-expression}",
|
||||||
description = "${batch-info.post-batch.description}"
|
// description = "${batch-info.post-batch.description}"
|
||||||
)
|
// )
|
||||||
@RequiredArgsConstructor
|
// @RequiredArgsConstructor
|
||||||
public class PostCreateBatch extends AbstractBatchTask {
|
public class PostCreateBatch { //extends AbstractBatchTask {
|
||||||
|
|
||||||
private final PostMapper postMapper;
|
// private final PostMapper postMapper;
|
||||||
|
|
||||||
@Autowired
|
// @Autowired
|
||||||
@Override
|
// @Override
|
||||||
public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
|
// public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
|
||||||
super.setTransactionManager(transactionManager);
|
// super.setTransactionManager(transactionManager);
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Override
|
// @Override
|
||||||
protected Tasklet createTasklet() {
|
// protected Tasklet createTasklet() {
|
||||||
return ((contribution, chunkContext) -> {
|
// return ((contribution, chunkContext) -> {
|
||||||
postMapper.save(Post.builder().title("testTitle").content("testPost").build());
|
// postMapper.save(Post.builder().title("testTitle").content("testPost").build());
|
||||||
return RepeatStatus.FINISHED;
|
// return RepeatStatus.FINISHED;
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,106 +36,106 @@ import com.spring.infra.db.orm.jpa.SecondaryJpaConfig;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Slf4j
|
// @Slf4j
|
||||||
@Component
|
// @Component
|
||||||
@BatchJobInfo(
|
// @BatchJobInfo(
|
||||||
group = "${batch-info.post-create-batch.group}",
|
// group = "${batch-info.post-create-batch.group}",
|
||||||
jobName = "${batch-info.post-create-batch.job-name}",
|
// jobName = "${batch-info.post-create-batch.job-name}",
|
||||||
cronExpression = "${batch-info.post-create-batch.cron-expression}",
|
// cronExpression = "${batch-info.post-create-batch.cron-expression}",
|
||||||
description = "${batch-info.post-create-batch.description}"
|
// description = "${batch-info.post-create-batch.description}"
|
||||||
)
|
// )
|
||||||
@RequiredArgsConstructor
|
// @RequiredArgsConstructor
|
||||||
public class PostCreateBatchChunk extends AbstractBatchChunk {
|
public class PostCreateBatchChunk { //extends AbstractBatchChunk {
|
||||||
|
|
||||||
@Autowired
|
// @Autowired
|
||||||
@Override
|
// @Override
|
||||||
public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
|
// public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
|
||||||
super.setTransactionManager(transactionManager);
|
// super.setTransactionManager(transactionManager);
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Qualifier(SecondaryJpaConfig.ENTITY_MANAGER_FACTORY)
|
// @Qualifier(SecondaryJpaConfig.ENTITY_MANAGER_FACTORY)
|
||||||
private final EntityManagerFactory entityManagerFactory;
|
// private final EntityManagerFactory entityManagerFactory;
|
||||||
|
|
||||||
private final PostRepository postRepository;
|
// private final PostRepository postRepository;
|
||||||
private final PostBackUpRepository postBackUpRepository;
|
// private final PostBackUpRepository postBackUpRepository;
|
||||||
|
|
||||||
private List<Post> list = new ArrayList<>();
|
// private List<Post> list = new ArrayList<>();
|
||||||
|
|
||||||
@Override
|
// @Override
|
||||||
public Job createJob() {
|
// public Job createJob() {
|
||||||
return new JobBuilder(batchJobInfoData.getJobName())
|
// return new JobBuilder(batchJobInfoData.getJobName())
|
||||||
.repository(jobRepository)
|
// .repository(jobRepository)
|
||||||
.incrementer(new RunIdIncrementer())
|
// .incrementer(new RunIdIncrementer())
|
||||||
.start(readListStep())
|
// .start(readListStep())
|
||||||
.next(decider())
|
// .next(decider())
|
||||||
.from(decider()).on("PROCESS").to(processStep())
|
// .from(decider()).on("PROCESS").to(processStep())
|
||||||
.from(decider()).on("TERMINATE").to(terminateStep())
|
// .from(decider()).on("TERMINATE").to(terminateStep())
|
||||||
.end()
|
// .end()
|
||||||
.build();
|
// .build();
|
||||||
}
|
// }
|
||||||
|
|
||||||
private JobExecutionDecider decider() {
|
// private JobExecutionDecider decider() {
|
||||||
return (JobExecution jobExecution, StepExecution stepExecution) ->
|
// return (JobExecution jobExecution, StepExecution stepExecution) ->
|
||||||
!list.isEmpty() ? new FlowExecutionStatus("PROCESS") : new FlowExecutionStatus("TERMINATE");
|
// !list.isEmpty() ? new FlowExecutionStatus("PROCESS") : new FlowExecutionStatus("TERMINATE");
|
||||||
}
|
// }
|
||||||
|
|
||||||
private Step readListStep() {
|
// private Step readListStep() {
|
||||||
return new StepBuilder("readListStep")
|
// return new StepBuilder("readListStep")
|
||||||
.repository(jobRepository)
|
// .repository(jobRepository)
|
||||||
.transactionManager(transactionManager)
|
// .transactionManager(transactionManager)
|
||||||
.tasklet(readListTasklet())
|
// .tasklet(readListTasklet())
|
||||||
.build();
|
// .build();
|
||||||
}
|
// }
|
||||||
|
|
||||||
private Tasklet readListTasklet() {
|
// private Tasklet readListTasklet() {
|
||||||
return (contribution, chunkContext) -> {
|
// return (contribution, chunkContext) -> {
|
||||||
list = postRepository.findAll();
|
// list = postRepository.findAll();
|
||||||
return RepeatStatus.FINISHED;
|
// return RepeatStatus.FINISHED;
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
private Step processStep() {
|
// private Step processStep() {
|
||||||
return new StepBuilder("processStep")
|
// return new StepBuilder("processStep")
|
||||||
.repository(jobRepository)
|
// .repository(jobRepository)
|
||||||
.transactionManager(transactionManager)
|
// .transactionManager(transactionManager)
|
||||||
.<Post, PostBackUp>chunk(5)
|
// .<Post, PostBackUp>chunk(5)
|
||||||
.reader(testReader())
|
// .reader(testReader())
|
||||||
.processor(testProcessor())
|
// .processor(testProcessor())
|
||||||
.writer(testWriter())
|
// .writer(testWriter())
|
||||||
.build();
|
// .build();
|
||||||
}
|
// }
|
||||||
|
|
||||||
private JpaPagingItemReader<Post> testReader() {
|
// private JpaPagingItemReader<Post> testReader() {
|
||||||
return new JpaPagingItemReaderBuilder<Post>()
|
// return new JpaPagingItemReaderBuilder<Post>()
|
||||||
.name("testReader")
|
// .name("testReader")
|
||||||
.entityManagerFactory(entityManagerFactory)
|
// .entityManagerFactory(entityManagerFactory)
|
||||||
.pageSize(5)
|
// .pageSize(5)
|
||||||
.queryString("select p from Post p")
|
// .queryString("select p from Post p")
|
||||||
.build();
|
// .build();
|
||||||
}
|
// }
|
||||||
|
|
||||||
private ItemProcessor<Post, PostBackUp> testProcessor() {
|
// private ItemProcessor<Post, PostBackUp> testProcessor() {
|
||||||
return post ->
|
// return post ->
|
||||||
PostBackUp.builder().postId(post.getPostId()).content(post.getContent()).title(post.getTitle()).build();
|
// PostBackUp.builder().postId(post.getPostId()).content(post.getContent()).title(post.getTitle()).build();
|
||||||
}
|
// }
|
||||||
|
|
||||||
private ItemWriter<PostBackUp> testWriter() {
|
// private ItemWriter<PostBackUp> testWriter() {
|
||||||
return postBackUpRepository::saveAll;
|
// return postBackUpRepository::saveAll;
|
||||||
}
|
// }
|
||||||
|
|
||||||
private Step terminateStep() {
|
// private Step terminateStep() {
|
||||||
return new StepBuilder("terminateStep")
|
// return new StepBuilder("terminateStep")
|
||||||
.repository(jobRepository)
|
// .repository(jobRepository)
|
||||||
.transactionManager(transactionManager)
|
// .transactionManager(transactionManager)
|
||||||
.tasklet(terminateTasklet())
|
// .tasklet(terminateTasklet())
|
||||||
.build();
|
// .build();
|
||||||
}
|
// }
|
||||||
|
|
||||||
private Tasklet terminateTasklet() {
|
// private Tasklet terminateTasklet() {
|
||||||
return (contribution, chunkContext) -> {
|
// return (contribution, chunkContext) -> {
|
||||||
log.error("List Read Error : List is null");
|
// log.error("List Read Error : List is null");
|
||||||
return RepeatStatus.FINISHED;
|
// return RepeatStatus.FINISHED;
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
|||||||
*/
|
*/
|
||||||
private PlatformTransactionManager transactionManager;
|
private PlatformTransactionManager transactionManager;
|
||||||
|
|
||||||
|
private BatchExceptionListener batchExceptionListener;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 기본 생성자입니다.
|
* 기본 생성자입니다.
|
||||||
*
|
*
|
||||||
@@ -163,6 +165,11 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
|||||||
this.transactionManager = transactionManager;
|
this.transactionManager = transactionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setBatchExceptionListener(BatchExceptionListener batchExceptionListener) {
|
||||||
|
this.batchExceptionListener = batchExceptionListener;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 배치 작업을 생성합니다.
|
* 배치 작업을 생성합니다.
|
||||||
*
|
*
|
||||||
@@ -180,6 +187,7 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
|||||||
var jobBuilder = new JobBuilder(batchJobInfoData.getJobName())
|
var jobBuilder = new JobBuilder(batchJobInfoData.getJobName())
|
||||||
.incrementer(new RunIdIncrementer())
|
.incrementer(new RunIdIncrementer())
|
||||||
.repository(jobRepository)
|
.repository(jobRepository)
|
||||||
|
.listener(batchExceptionListener)
|
||||||
.start(steps.get(0));
|
.start(steps.get(0));
|
||||||
for (int i = 1; i < steps.size(); i++) {
|
for (int i = 1; i < steps.size(); i++) {
|
||||||
jobBuilder = jobBuilder.next(steps.get(i));
|
jobBuilder = jobBuilder.next(steps.get(i));
|
||||||
@@ -213,6 +221,7 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
|||||||
return new StepBuilder(stepName)
|
return new StepBuilder(stepName)
|
||||||
.repository(jobRepository)
|
.repository(jobRepository)
|
||||||
.transactionManager(transactionManager)
|
.transactionManager(transactionManager)
|
||||||
|
.listener(batchExceptionListener)
|
||||||
.tasklet(tasklet)
|
.tasklet(tasklet)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.spring.infra.batch;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.batch.core.BatchStatus;
|
||||||
|
import org.springframework.batch.core.ExitStatus;
|
||||||
|
import org.springframework.batch.core.JobExecution;
|
||||||
|
import org.springframework.batch.core.JobExecutionListener;
|
||||||
|
import org.springframework.batch.core.StepExecution;
|
||||||
|
import org.springframework.batch.core.StepExecutionListener;
|
||||||
|
import org.springframework.dao.DataAccessException;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class BatchExceptionListener implements JobExecutionListener, StepExecutionListener {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeJob(@NonNull JobExecution jobExecution) {
|
||||||
|
log.info("Job 시작: {}", jobExecution.getJobInstance().getJobName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterJob(@NonNull JobExecution jobExecution) {
|
||||||
|
if (jobExecution.getStatus() == BatchStatus.FAILED) {
|
||||||
|
handleJobFailure(jobExecution);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeStep(@NonNull StepExecution stepExecution) {
|
||||||
|
log.info("Step 시작: {}", stepExecution.getStepName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public ExitStatus afterStep(@NonNull StepExecution stepExecution) {
|
||||||
|
if (!stepExecution.getFailureExceptions().isEmpty()) {
|
||||||
|
Throwable ex = stepExecution.getFailureExceptions().get(0);
|
||||||
|
|
||||||
|
if (ex instanceof DataAccessException) {
|
||||||
|
// SQL 예외 처리
|
||||||
|
DataAccessException sqlEx = (DataAccessException) ex;
|
||||||
|
log.error("SQL 오류 발생: {}", sqlEx.getMostSpecificCause().getMessage());
|
||||||
|
return ExitStatus.FAILED;
|
||||||
|
} else {
|
||||||
|
// 기타 예외 처리
|
||||||
|
log.error("예외 발생: ", ex);
|
||||||
|
return ExitStatus.FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stepExecution.getExitStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleJobFailure(JobExecution jobExecution) {
|
||||||
|
List<Throwable> exceptions = jobExecution.getAllFailureExceptions();
|
||||||
|
for (Throwable ex : exceptions) {
|
||||||
|
if (ex instanceof DataAccessException) {
|
||||||
|
handleSqlException((DataAccessException) ex, jobExecution);
|
||||||
|
} else {
|
||||||
|
handleGeneralException(ex, jobExecution);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSqlException(DataAccessException ex, JobExecution jobExecution) {
|
||||||
|
log.error("Job {} 실행 중 SQL 오류 발생: {}",
|
||||||
|
jobExecution.getJobInstance().getJobName(),
|
||||||
|
ex.getMostSpecificCause().getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleGeneralException(Throwable ex, JobExecution jobExecution) {
|
||||||
|
log.error("Job {} 실행 중 오류 발생: {}",
|
||||||
|
jobExecution.getJobInstance().getJobName(),
|
||||||
|
ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.spring.infra.feign.client;
|
||||||
|
|
||||||
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
|
import org.springframework.cloud.openfeign.SpringQueryMap;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
|
import com.spring.infra.feign.config.CommonFeignConfig;
|
||||||
|
import com.spring.infra.feign.dto.TestRequest;
|
||||||
|
|
||||||
|
@FeignClient(name = "client1", url = "${feign.clients.client1.url}", configuration = CommonFeignConfig.class)
|
||||||
|
public interface Client1FeignClient {
|
||||||
|
|
||||||
|
@PostMapping(value = "/test", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||||
|
String getItemById(@SpringQueryMap TestRequest request);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package com.spring.infra.feign.config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Proxy;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.springframework.cloud.openfeign.FeignFormatterRegistrar;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
|
import feign.Client;
|
||||||
|
import feign.Logger;
|
||||||
|
import feign.Request;
|
||||||
|
import feign.RequestInterceptor;
|
||||||
|
import feign.Response;
|
||||||
|
import feign.RetryableException;
|
||||||
|
import feign.Retryer;
|
||||||
|
import feign.codec.ErrorDecoder;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feign 클라이언트에 대한 공통 설정을 제공하는 설정 클래스입니다.
|
||||||
|
* 이 클래스는 기본 Feign 빌더 설정과 클라이언트별 커스텀 설정을 제공합니다.
|
||||||
|
*
|
||||||
|
* @author mindol
|
||||||
|
* @version 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CommonFeignConfig {
|
||||||
|
|
||||||
|
private final FeignClientProperties properties;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Client feignClient() {
|
||||||
|
return new Client() {
|
||||||
|
@Override
|
||||||
|
public Response execute(Request request, Request.Options options) throws IOException {
|
||||||
|
String clientName = request.requestTemplate().feignTarget().name();
|
||||||
|
FeignClientProperties.ClientConfig config = properties.getClients().get(clientName);
|
||||||
|
|
||||||
|
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(config.getConnectTimeout(), TimeUnit.MILLISECONDS)
|
||||||
|
.readTimeout(config.getReadTimeout(), TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
|
if (config.isUseProxy()) {
|
||||||
|
clientBuilder.proxy(new Proxy(
|
||||||
|
Proxy.Type.HTTP,
|
||||||
|
new InetSocketAddress(config.getProxyHost(), config.getProxyPort())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
OkHttpClient okHttpClient = clientBuilder.build();
|
||||||
|
// OkHttp 요청 생성
|
||||||
|
okhttp3.Request.Builder okHttpRequestBuilder = new okhttp3.Request.Builder()
|
||||||
|
.url(request.url())
|
||||||
|
.method(request.httpMethod().name(), getRequestBody(request)); // 요청 본문 설정
|
||||||
|
|
||||||
|
// Feign 요청의 헤더를 OkHttp 요청에 추가
|
||||||
|
request.headers().forEach((key, values) -> {
|
||||||
|
for (String value : values) {
|
||||||
|
okHttpRequestBuilder.addHeader(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
okhttp3.Request okHttpRequest = okHttpRequestBuilder.build();
|
||||||
|
|
||||||
|
try (okhttp3.Response okHttpResponse = okHttpClient.newCall(okHttpRequest).execute()) {
|
||||||
|
return Response.builder()
|
||||||
|
.status(okHttpResponse.code())
|
||||||
|
.reason(okHttpResponse.message())
|
||||||
|
.headers(convertHeaders(okHttpResponse.headers()))
|
||||||
|
.body(okHttpResponse.body().bytes())
|
||||||
|
.request(request)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Collection<String>> convertHeaders(okhttp3.Headers headers) {
|
||||||
|
Map<String, Collection<String>> convertedHeaders = new HashMap<>();
|
||||||
|
for (String name : headers.names()) {
|
||||||
|
convertedHeaders.put(name, headers.values(name));
|
||||||
|
}
|
||||||
|
return convertedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RequestBody getRequestBody(Request request) {
|
||||||
|
String contentType = MediaType.APPLICATION_JSON_VALUE;
|
||||||
|
|
||||||
|
if (request.requestTemplate().headers().containsKey("Content-Type")) {
|
||||||
|
contentType = request.requestTemplate().headers().get("Content-Type").iterator().next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.body() != null) {
|
||||||
|
return RequestBody.create(request.body(), okhttp3.MediaType.parse(contentType));
|
||||||
|
} else {
|
||||||
|
return RequestBody.create("", okhttp3.MediaType.parse(contentType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
RequestInterceptor headerInterceptor() {
|
||||||
|
return requestTemplate -> {
|
||||||
|
String clientName = requestTemplate.feignTarget().name();
|
||||||
|
if (properties.getClients().containsKey(clientName)) {
|
||||||
|
var config = properties.getClients().get(clientName);
|
||||||
|
if (!config.getHeaders().isEmpty()) {
|
||||||
|
config.getHeaders().forEach(requestTemplate::header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Retryer retryer() {
|
||||||
|
// 0.1초의 간격으로 시작해 최대 3초의 간격으로 점점 증가하며, 최대5번 재시도한다.
|
||||||
|
return new Retryer.Default(100L, TimeUnit.SECONDS.toMillis(3L), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Logger.Level feignLoggerLevel() {
|
||||||
|
return Logger.Level.FULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
FeignFormatterRegistrar dateTimeFormatterRegistrar() {
|
||||||
|
return registry -> {
|
||||||
|
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
|
||||||
|
registrar.setUseIsoFormat(true);
|
||||||
|
registrar.registerFormatters(registry);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ErrorDecoder errorDecoder() {
|
||||||
|
return (methodKey, response) ->
|
||||||
|
(response.status() >= 500 && response.status() <= 504)
|
||||||
|
? new RetryableException(
|
||||||
|
response.status(),
|
||||||
|
String.format("Server error %d: %s, retrying...", response.status(), response.reason()),
|
||||||
|
response.request().httpMethod(),
|
||||||
|
null,
|
||||||
|
response.request()
|
||||||
|
)
|
||||||
|
: new Exception(String.format("Error %d: %s", response.status(), response.reason()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.spring.infra.feign.config;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.boot.context.properties.ConstructorBinding;
|
||||||
|
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ConstructorBinding
|
||||||
|
@ConfigurationProperties(prefix = "feign")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FeignClientProperties {
|
||||||
|
|
||||||
|
private final Map<String, ClientConfig> clients;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class ClientConfig {
|
||||||
|
private final String url;
|
||||||
|
private final int connectTimeout;
|
||||||
|
private final int readTimeout;
|
||||||
|
private final String proxyHost;
|
||||||
|
private final int proxyPort;
|
||||||
|
private final boolean useProxy;
|
||||||
|
private final Map<String, String> headers;
|
||||||
|
|
||||||
|
public ClientConfig(
|
||||||
|
String url,
|
||||||
|
@DefaultValue("5000") int connectTimeout,
|
||||||
|
@DefaultValue("5000") int readTimeout,
|
||||||
|
@DefaultValue("") String proxyHost,
|
||||||
|
@DefaultValue("0") int proxyPort,
|
||||||
|
@DefaultValue("false") boolean useProxy,
|
||||||
|
Map<String, String> headers
|
||||||
|
) {
|
||||||
|
this.url = url;
|
||||||
|
this.connectTimeout = connectTimeout;
|
||||||
|
this.readTimeout = readTimeout;
|
||||||
|
this.proxyHost = proxyHost;
|
||||||
|
this.proxyPort = proxyPort;
|
||||||
|
this.useProxy = useProxy;
|
||||||
|
this.headers = headers != null ? headers : new HashMap<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.spring.infra.feign.dto;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class TestRequest {
|
||||||
|
private String id;
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
@@ -94,7 +94,7 @@ public class QuartzConfig {
|
|||||||
factory.setDataSource(dataSource);
|
factory.setDataSource(dataSource);
|
||||||
factory.setTransactionManager(transactionManager);
|
factory.setTransactionManager(transactionManager);
|
||||||
factory.setJobFactory(jobFactory);
|
factory.setJobFactory(jobFactory);
|
||||||
factory.setAutoStartup(false);
|
factory.setAutoStartup(true);
|
||||||
factory.setWaitForJobsToCompleteOnShutdown(true);
|
factory.setWaitForJobsToCompleteOnShutdown(true);
|
||||||
return factory;
|
return factory;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package com.spring.infra.quartz;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
|
||||||
|
import org.quartz.JobExecutionContext;
|
||||||
|
import org.quartz.Scheduler;
|
||||||
|
import org.quartz.SchedulerException;
|
||||||
|
import org.quartz.Trigger;
|
||||||
|
import org.quartz.TriggerKey;
|
||||||
|
import org.quartz.impl.matchers.GroupMatcher;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.context.event.ContextClosedEvent;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quartz 스케줄러의 안전한 종료를 관리하는 리스너 클래스
|
||||||
|
*
|
||||||
|
* <p>이 클래스는 Spring 애플리케이션 컨텍스트가 종료될 때 Quartz 스케줄러의 모든 작업을
|
||||||
|
* 안전하게 종료하고 정리하는 역할을 합니다. 실행 중인 작업이 정상적으로 완료될 때까지
|
||||||
|
* 대기하며, 모든 트리거를 정리합니다.</p>
|
||||||
|
*
|
||||||
|
* <p>주요 기능:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>실행 중인 작업의 안전한 종료 처리</li>
|
||||||
|
* <li>트리거 상태 모니터링 및 정리</li>
|
||||||
|
* <li>스케줄러 리소스의 정상적인 해제</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author [작성자 이름]
|
||||||
|
* @version 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class QuartzShutdownListener implements ApplicationListener<ContextClosedEvent> {
|
||||||
|
|
||||||
|
/** 작업 완료 대기 최대 시간 (밀리초) */
|
||||||
|
private static final long SHUTDOWN_WAIT_TIME = 30000L; // 30초
|
||||||
|
|
||||||
|
/** Quartz 스케줄러 인스턴스 */
|
||||||
|
private final Scheduler scheduler;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring 컨텍스트 종료 이벤트 발생 시 실행되는 메서드
|
||||||
|
*
|
||||||
|
* @param event 컨텍스트 종료 이벤트
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onApplicationEvent(@NonNull ContextClosedEvent event) {
|
||||||
|
log.info("Quartz 셧다운 프로세스 시작...");
|
||||||
|
shutdownQuartzGracefully();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quartz 스케줄러의 안전한 종료를 처리하는 메서드
|
||||||
|
*
|
||||||
|
* <p>다음의 단계를 순차적으로 실행합니다:</p>
|
||||||
|
* <ol>
|
||||||
|
* <li>새로운 작업 스케줄링 중지</li>
|
||||||
|
* <li>현재 실행 중인 작업 목록 확인</li>
|
||||||
|
* <li>실행 중인 작업의 상태 로깅</li>
|
||||||
|
* <li>작업 완료 대기</li>
|
||||||
|
* <li>트리거 정리</li>
|
||||||
|
* <li>스케줄러 종료</li>
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
private void shutdownQuartzGracefully() {
|
||||||
|
try {
|
||||||
|
// 1. 새로운 작업 스케줄링 중지
|
||||||
|
scheduler.standby();
|
||||||
|
log.info("Scheduler를 대기 모드로 전환했습니다.");
|
||||||
|
|
||||||
|
// 2. 현재 실행 중인 작업 목록 확인
|
||||||
|
List<JobExecutionContext> currentJobs = scheduler.getCurrentlyExecutingJobs();
|
||||||
|
if (!currentJobs.isEmpty()) {
|
||||||
|
log.info("현재 실행 중인 작업 수: {}", currentJobs.size());
|
||||||
|
|
||||||
|
// 3. 실행 중인 작업의 정보 로깅
|
||||||
|
logRunningJobs(currentJobs);
|
||||||
|
|
||||||
|
// 4. 실행 중인 작업이 완료될 때까지 대기
|
||||||
|
waitForJobCompletion(currentJobs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 모든 트리거 상태 확인 및 정리
|
||||||
|
cleanupTriggers();
|
||||||
|
|
||||||
|
// 6. 스케줄러 종료
|
||||||
|
performSchedulerShutdown();
|
||||||
|
|
||||||
|
} catch (SchedulerException e) {
|
||||||
|
log.error("Quartz 종료 중 오류 발생", e);
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
log.error("Quartz 종료 대기 중 인터럽트 발생", e);
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실행 중인 작업들의 상세 정보를 로깅
|
||||||
|
*
|
||||||
|
* @param currentJobs 현재 실행 중인 작업 목록
|
||||||
|
*/
|
||||||
|
private void logRunningJobs(List<JobExecutionContext> currentJobs) {
|
||||||
|
for (JobExecutionContext job : currentJobs) {
|
||||||
|
log.info("실행 중인 작업: {}, 트리거: {}, 시작 시간: {}",
|
||||||
|
job.getJobDetail().getKey(),
|
||||||
|
job.getTrigger().getKey(),
|
||||||
|
job.getFireTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실행 중인 작업이 완료될 때까지 대기
|
||||||
|
*
|
||||||
|
* @param currentJobs 현재 실행 중인 작업 목록
|
||||||
|
* @throws InterruptedException 대기 중 인터럽트 발생 시
|
||||||
|
* @throws SchedulerException 스케줄러 관련 오류 발생 시
|
||||||
|
*/
|
||||||
|
private void waitForJobCompletion(List<JobExecutionContext> currentJobs)
|
||||||
|
throws InterruptedException, SchedulerException {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
while (!currentJobs.isEmpty() &&
|
||||||
|
(System.currentTimeMillis() - startTime) < SHUTDOWN_WAIT_TIME) {
|
||||||
|
log.info("실행 중인 작업이 완료될 때까지 대기 중...");
|
||||||
|
Thread.sleep(1000);
|
||||||
|
currentJobs = scheduler.getCurrentlyExecutingJobs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 모든 트리거를 정리
|
||||||
|
*
|
||||||
|
* @throws SchedulerException 스케줄러 관련 오류 발생 시
|
||||||
|
*/
|
||||||
|
private void cleanupTriggers() throws SchedulerException {
|
||||||
|
List<String> groups = scheduler.getTriggerGroupNames();
|
||||||
|
for (String group : groups) {
|
||||||
|
Set<TriggerKey> triggerKeys = scheduler.getTriggerKeys(GroupMatcher.groupEquals(group));
|
||||||
|
for (TriggerKey triggerKey : triggerKeys) {
|
||||||
|
try {
|
||||||
|
Trigger trigger = scheduler.getTrigger(triggerKey);
|
||||||
|
if (trigger != null) {
|
||||||
|
scheduler.unscheduleJob(triggerKey);
|
||||||
|
log.info("트리거 제거됨: {}", triggerKey);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("트리거 제거 중 오류 발생: {}", triggerKey, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스케줄러 종료 수행
|
||||||
|
*
|
||||||
|
* @throws SchedulerException 스케줄러 관련 오류 발생 시
|
||||||
|
*/
|
||||||
|
private void performSchedulerShutdown() throws SchedulerException {
|
||||||
|
scheduler.clear();
|
||||||
|
scheduler.shutdown(true); // true = 실행 중인 작업이 완료될 때까지 대기
|
||||||
|
log.info("Quartz 스케줄러가 정상적으로 종료되었습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 애플리케이션 컨텍스트 종료 전 실행되는 정리 메서드
|
||||||
|
*/
|
||||||
|
@PreDestroy
|
||||||
|
public void preDestroy() {
|
||||||
|
try {
|
||||||
|
if (!scheduler.isShutdown()) {
|
||||||
|
log.info("ApplicationContext 종료 전 Quartz 정리 작업 수행");
|
||||||
|
shutdownQuartzGracefully();
|
||||||
|
}
|
||||||
|
} catch (SchedulerException e) {
|
||||||
|
log.error("PreDestroy 단계에서 Quartz 종료 중 오류 발생", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -102,7 +102,7 @@ batch-info:
|
|||||||
email-send-batch:
|
email-send-batch:
|
||||||
group: "EMAIL"
|
group: "EMAIL"
|
||||||
job-name: "emailSendJob"
|
job-name: "emailSendJob"
|
||||||
cron-expression: "*/20 * * * * ?"
|
cron-expression: "*/10 * * * * ?"
|
||||||
description: "이메일배치작업"
|
description: "이메일배치작업"
|
||||||
post-batch:
|
post-batch:
|
||||||
group: "POST"
|
group: "POST"
|
||||||
@@ -113,7 +113,36 @@ batch-info:
|
|||||||
group: "POST"
|
group: "POST"
|
||||||
job-name: "postCreateJob"
|
job-name: "postCreateJob"
|
||||||
cron-expression: "0/30 * * * * ?"
|
cron-expression: "0/30 * * * * ?"
|
||||||
description: "테스트배치작업"
|
description: "테스트배치작업"
|
||||||
|
|
||||||
|
feign:
|
||||||
|
okhttp:
|
||||||
|
enabled: true
|
||||||
|
client:
|
||||||
|
config:
|
||||||
|
default:
|
||||||
|
connectTimeout: 5000
|
||||||
|
readTimeout: 5000
|
||||||
|
loggerLevel: full
|
||||||
|
compression:
|
||||||
|
request:
|
||||||
|
enabled: true
|
||||||
|
response:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
clients:
|
||||||
|
client1:
|
||||||
|
url: http://localhost:8082
|
||||||
|
connectTimeout: 5000
|
||||||
|
readTimeout: 5000
|
||||||
|
useProxy: true
|
||||||
|
proxyHost: http://localhost:8083
|
||||||
|
proxyPort: 8083
|
||||||
|
client2:
|
||||||
|
url: http://api1.example.com
|
||||||
|
headers:
|
||||||
|
Client1-Specific-Header: Value1
|
||||||
|
Another-Client1-Header: AnotherValue1
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
access-token:
|
access-token:
|
||||||
@@ -123,6 +152,14 @@ jwt:
|
|||||||
secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc=
|
secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc=
|
||||||
expiration: 10080
|
expiration: 10080
|
||||||
|
|
||||||
|
path:
|
||||||
|
paths:
|
||||||
|
path1:
|
||||||
|
upload: /path1/upload
|
||||||
|
path2:
|
||||||
|
upload: /path2/upload
|
||||||
|
dowonload: /path2/dowonload
|
||||||
|
|
||||||
management:
|
management:
|
||||||
endpoints:
|
endpoints:
|
||||||
web:
|
web:
|
||||||
|
|||||||
Reference in New Issue
Block a user