Compare commits
13 Commits
ac844f4ca3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6fba4f09f | ||
|
|
e3569a06b2 | ||
|
|
624a46def7 | ||
|
|
bb4277ed19 | ||
|
|
fe001eedad | ||
|
|
b01e150c9a | ||
|
|
fc98b4ffc0 | ||
|
|
12e595b728 | ||
|
|
23e1641644 | ||
|
|
df373d5d27 | ||
|
|
d63b268765 | ||
|
|
868bb01453 | ||
|
|
d5e8941d5d |
@@ -42,6 +42,14 @@
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter</artifactId>
|
||||
</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>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
|
||||
@@ -12,15 +12,42 @@ import com.spring.common.converter.CommHttpMessageConverter;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 웹 애플리케이션의 설정을 정의하는 구성 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring MVC의 웹 설정을 구성하며, HTTP 메시지 변환기와 같은
|
||||
* 웹 관련 빈을 설정합니다.</p>
|
||||
*
|
||||
* <p>주요 기능:</p>
|
||||
* <ul>
|
||||
* <li>커스텀 HTTP 메시지 변환기 등록</li>
|
||||
* <li>Spring MVC의 기본 설정을 확장</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* Jackson ObjectMapper 객체입니다.
|
||||
*
|
||||
* <p>JSON 데이터의 직렬화 및 역직렬화에 사용됩니다.</p>
|
||||
*/
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* HTTP 메시지 변환기를 확장합니다.
|
||||
*
|
||||
* <p>기본 HTTP 메시지 변환기 목록에 커스텀 변환기를 추가합니다.</p>
|
||||
*
|
||||
* @param converters HTTP 메시지 변환기 목록
|
||||
*/
|
||||
@Override
|
||||
public void extendMessageConverters(@NonNull List<HttpMessageConverter<?>> converters) {
|
||||
converters.add(0, new CommHttpMessageConverter(objectMapper));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -8,17 +8,46 @@ import org.springframework.lang.Nullable;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
|
||||
/**
|
||||
* 커스텀 HTTP 메시지 변환기 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring의 MappingJackson2HttpMessageConverter를 확장하여
|
||||
* JSON 데이터의 직렬화 및 역직렬화를 처리합니다.</p>
|
||||
*
|
||||
* <p>Java 8의 날짜 및 시간 API를 지원하기 위해 JavaTimeModule을 등록합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
public class CommHttpMessageConverter extends MappingJackson2HttpMessageConverter {
|
||||
|
||||
|
||||
/**
|
||||
* 기본 생성자입니다.
|
||||
*
|
||||
* <p>주어진 ObjectMapper를 사용하여 CommHttpMessageConverter를 초기화합니다.</p>
|
||||
*
|
||||
* <p>JavaTimeModule을 등록하여 Java 8의 날짜 및 시간 API를 지원합니다.</p>
|
||||
*
|
||||
* @param objectMapper JSON 변환에 사용할 ObjectMapper
|
||||
*/
|
||||
public CommHttpMessageConverter(ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
setObjectMapper(objectMapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 클래스 타입과 미디어 타입에 대해 이 변환기가 쓸 수 있는지 확인합니다.
|
||||
*
|
||||
* <p>미디어 타입이 지원되는 경우 true를 반환합니다.</p>
|
||||
*
|
||||
* @param clazz 변환할 클래스 타입
|
||||
* @param mediaType 변환할 미디어 타입
|
||||
* @return 지원하는 경우 true, 그렇지 않으면 false
|
||||
*/
|
||||
@Override
|
||||
public boolean canWrite(@NonNull Class<?> clazz, @Nullable MediaType mediaType) {
|
||||
return canWrite(mediaType);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -2,19 +2,62 @@ package com.spring.common.error;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 비즈니스 로직에서 발생할 수 있는 예외를 나타내는 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 RuntimeException을 확장하여 비즈니스 로직에서 발생하는
|
||||
* 특정 오류를 처리하는 데 사용됩니다.</p>
|
||||
*
|
||||
* <p>각 예외는 ErrorRule을 통해 정의된 오류 규칙을 기반으로 하며,
|
||||
* 해당 규칙에 대한 메시지를 포함합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Getter
|
||||
public class BizBaseException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 오류 규칙을 나타내는 객체입니다.
|
||||
*
|
||||
* <p>이 필드는 발생한 예외의 유형을 정의하는 ErrorRule 객체입니다.</p>
|
||||
*/
|
||||
private final ErrorRule errorRule;
|
||||
|
||||
private final String customMessage;
|
||||
|
||||
/**
|
||||
* 기본 생성자입니다.
|
||||
*
|
||||
* <p>기본 오류 규칙인 SYSTEM_ERROR를 사용하여 BizBaseException을 초기화합니다.</p>
|
||||
*/
|
||||
public BizBaseException() {
|
||||
super(ExceptionRule.SYSTEM_ERROR.getMessage());
|
||||
this.errorRule = ExceptionRule.SYSTEM_ERROR;
|
||||
this.customMessage = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 오류 규칙을 사용하여 BizBaseException을 초기화합니다.
|
||||
*
|
||||
* @param exceptionRule 발생한 예외의 오류 규칙
|
||||
*/
|
||||
public BizBaseException(ErrorRule exceptionRule) {
|
||||
super(exceptionRule.getMessage());
|
||||
this.errorRule = exceptionRule;
|
||||
this.customMessage = null;
|
||||
}
|
||||
|
||||
public BizBaseException(ErrorRule errorRule, String message) {
|
||||
super(message);
|
||||
this.errorRule = errorRule;
|
||||
this.customMessage = message;
|
||||
}
|
||||
|
||||
public BizBaseException(String message) {
|
||||
super(ExceptionRule.SYSTEM_ERROR.getMessage());
|
||||
this.errorRule = ExceptionRule.SYSTEM_ERROR;
|
||||
this.customMessage = message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,23 +7,68 @@ import org.springframework.validation.FieldError;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 비즈니스 오류 응답을 나타내는 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 ErrorResponse를 확장하여 비즈니스 로직에서 발생한 오류에 대한
|
||||
* 응답을 구성하는 데 사용됩니다.</p>
|
||||
*
|
||||
* <p>오류 규칙과 관련된 추가적인 오류 정보를 포함할 수 있습니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Getter
|
||||
public class BizErrorResponse extends ErrorResponse {
|
||||
|
||||
public BizErrorResponse(ErrorRule exceptionRule, List<RejectedValue> errors) {
|
||||
/**
|
||||
* 오류 목록을 나타내는 필드입니다.
|
||||
*
|
||||
* <p>비즈니스 로직에서 발생한 오류에 대한 세부 정보를 포함합니다.</p>
|
||||
*/
|
||||
private final List<RejectedValue> errors;
|
||||
|
||||
/**
|
||||
* 주어진 오류 규칙과 오류 목록을 사용하여 BizErrorResponse를 초기화합니다.
|
||||
*
|
||||
* @param exceptionRule 발생한 예외의 오류 규칙
|
||||
* @param errors 발생한 오류 목록
|
||||
*/
|
||||
public BizErrorResponse(ErrorRule exceptionRule, List<RejectedValue> errors, String customMessage) {
|
||||
super(exceptionRule);
|
||||
this.errors = errors;
|
||||
this.customMessage = customMessage;
|
||||
}
|
||||
|
||||
public static BizErrorResponse valueOf(ErrorRule exceptionRule) {
|
||||
return new BizErrorResponse(exceptionRule, null);
|
||||
/**
|
||||
* 주어진 오류 규칙을 사용하여 BizErrorResponse 객체를 생성합니다.
|
||||
*
|
||||
* <p>오류 목록은 null로 설정됩니다.</p>
|
||||
*
|
||||
* @param exceptionRule 발생한 예외의 오류 규칙
|
||||
* @return 생성된 BizErrorResponse 객체
|
||||
*/
|
||||
public static BizErrorResponse fromErrorRule(ErrorRule exceptionRule) {
|
||||
return new BizErrorResponse(exceptionRule, null, null);
|
||||
}
|
||||
|
||||
public static BizErrorResponse of(ErrorRule exceptionRule, String message) {
|
||||
return new BizErrorResponse(exceptionRule, null, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 필드 오류 목록을 기반으로 BizErrorResponse 객체를 생성합니다.
|
||||
*
|
||||
* <p>필드 오류를 RejectedValue 객체로 변환하여 오류 목록을 구성합니다.</p>
|
||||
*
|
||||
* @param fieldErrors 필드 오류 목록
|
||||
* @return 생성된 BizErrorResponse 객체
|
||||
*/
|
||||
public static BizErrorResponse fromFieldError(List<FieldError> fieldErrors) {
|
||||
List<RejectedValue> errors = fieldErrors.stream()
|
||||
.map(fieldError -> RejectedValue.of(fieldError.getField(), fieldError.getRejectedValue(), fieldError.getDefaultMessage()))
|
||||
.collect(Collectors.toList());
|
||||
return new BizErrorResponse(ExceptionRule.UNPROCESSABLE_ENTITY, errors);
|
||||
return new BizErrorResponse(ExceptionRule.UNPROCESSABLE_ENTITY, errors, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,11 +6,34 @@ import lombok.Getter;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 오류 응답을 나타내는 추상 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 비즈니스 로직에서 발생한 오류에 대한 응답을 구성하는 데 사용됩니다.</p>
|
||||
*
|
||||
* <p>각 오류 응답은 오류 규칙과 관련된 오류 목록을 포함할 수 있습니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public abstract class ErrorResponse {
|
||||
|
||||
/**
|
||||
* 오류 규칙을 나타내는 필드입니다.
|
||||
*
|
||||
* <p>발생한 오류의 유형을 정의하는 ErrorRule 객체입니다.</p>
|
||||
*/
|
||||
protected final ErrorRule errorRule;
|
||||
|
||||
/**
|
||||
* 오류 목록을 나타내는 필드입니다.
|
||||
*
|
||||
* <p>비즈니스 로직에서 발생한 오류에 대한 세부 정보를 포함합니다.</p>
|
||||
*/
|
||||
protected List<RejectedValue> errors;
|
||||
|
||||
protected String customMessage;
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,31 @@ package com.spring.common.error;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* 오류 규칙을 정의하는 인터페이스입니다.
|
||||
*
|
||||
* <p>이 인터페이스는 비즈니스 로직에서 발생하는 오류에 대한 상태 코드와 메시지를 제공하는
|
||||
* 메서드를 정의합니다.</p>
|
||||
*
|
||||
* <p>구현 클래스는 이 인터페이스를 구현하여 특정 오류 규칙에 대한 정보를 제공해야 합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface ErrorRule {
|
||||
|
||||
/**
|
||||
* 오류에 대한 HTTP 상태 코드를 반환합니다.
|
||||
*
|
||||
* @return 오류에 해당하는 HttpStatus 객체
|
||||
*/
|
||||
HttpStatus getStatus();
|
||||
|
||||
/**
|
||||
* 오류에 대한 메시지를 반환합니다.
|
||||
*
|
||||
* @return 오류 메시지 문자열
|
||||
*/
|
||||
String getMessage();
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,17 @@ import org.springframework.http.HttpStatus;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 예외 규칙을 정의하는 열거형입니다.
|
||||
*
|
||||
* <p>이 열거형은 다양한 오류 상황에 대한 HTTP 상태 코드와 메시지를 정의합니다.</p>
|
||||
*
|
||||
* <p>각 열거 상수는 특정 오류 상황을 나타내며, 이를 통해 비즈니스 로직에서 발생하는
|
||||
* 오류를 일관되게 처리할 수 있습니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum ExceptionRule implements ErrorRule {
|
||||
@@ -19,16 +30,6 @@ public enum ExceptionRule implements ErrorRule {
|
||||
UNPROCESSABLE_ENTITY(HttpStatus.UNPROCESSABLE_ENTITY, "요청을 처리할 수 없습니다.");
|
||||
|
||||
private final HttpStatus status;
|
||||
private String message;
|
||||
|
||||
ExceptionRule(HttpStatus status, String message) {
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public ExceptionRule message(final String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
private final String message;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,28 +1,90 @@
|
||||
package com.spring.common.error;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.boot.web.servlet.error.ErrorController;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
/**
|
||||
* 전역 오류 처리를 위한 컨트롤러 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring MVC의 ErrorController 인터페이스를 구현하여
|
||||
* 애플리케이션에서 발생하는 오류를 처리합니다.</p>
|
||||
*
|
||||
* <p>HTTP 오류가 발생했을 때 사용자에게 오류 페이지를 표시하는 기능을 제공합니다.</p>
|
||||
*
|
||||
* <p>이 컨트롤러는 JSON 요청과 HTML 요청을 구분하여 각각에 맞는 오류 처리를 수행합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Controller
|
||||
public class GlobalErrorController implements ErrorController {
|
||||
|
||||
@ExceptionHandler(Throwable.class)
|
||||
|
||||
/**
|
||||
* 오류를 처리하는 메서드입니다.
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @param model 모델 객체
|
||||
* @return JSON 요청일 경우 JSON 오류 응답, HTML 요청일 경우 HTML 오류 페이지
|
||||
*/
|
||||
@GetMapping("/error")
|
||||
public String handleError(HttpServletRequest request, Model model) {
|
||||
public Object handleError(HttpServletRequest request, Model model) {
|
||||
if (isJsonRequest(request)) {
|
||||
return handleJsonError(request);
|
||||
} else {
|
||||
return handleHtmlError(request, model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청이 JSON 형식인지 확인하는 메서드입니다.
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @return JSON 요청이면 true, 그렇지 않으면 false
|
||||
*/
|
||||
private boolean isJsonRequest(HttpServletRequest request) {
|
||||
return Objects.equals(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 요청에 대한 오류를 처리하는 메서드입니다.
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @return BizErrorResponse 객체와 HTTP 상태 코드를 포함한 ResponseEntity
|
||||
*/
|
||||
private ResponseEntity<BizErrorResponse> handleJsonError(HttpServletRequest request) {
|
||||
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
|
||||
int statusCode = status != null ? Integer.parseInt(status.toString()) : HttpStatus.INTERNAL_SERVER_ERROR.value();
|
||||
HttpStatus httpStatus = HttpStatus.valueOf(statusCode);
|
||||
return new ResponseEntity<>(BizErrorResponse.fromErrorRule(ExceptionRule.NOT_FOUND), httpStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 요청에 대한 오류를 처리하는 메서드입니다.
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @param model 모델 객체
|
||||
* @return 오류 페이지의 경로
|
||||
*/
|
||||
private String handleHtmlError(HttpServletRequest request, Model model) {
|
||||
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
|
||||
String errorMessage = String.valueOf(request.getAttribute(RequestDispatcher.ERROR_MESSAGE));
|
||||
String statusMsg = status.toString();
|
||||
HttpStatus httpStatus = HttpStatus.valueOf(Integer.valueOf(statusMsg));
|
||||
model.addAttribute("message", statusMsg + " " + httpStatus.getReasonPhrase());
|
||||
if (StringUtils.hasText(errorMessage)) model.addAttribute("errorMessage", errorMessage);
|
||||
if (StringUtils.hasText(errorMessage)) {
|
||||
model.addAttribute("errorMessage", errorMessage);
|
||||
}
|
||||
return "pages/error/error";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,110 @@
|
||||
package com.spring.common.error;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.validation.ConstraintViolationException;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* 전역 예외 처리를 위한 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring의 @RestControllerAdvice를 사용하여 애플리케이션 전역에서 발생하는
|
||||
* 예외를 처리합니다.</p>
|
||||
*
|
||||
* <p>특정 예외 유형에 대해 적절한 응답을 생성하여 클라이언트에 반환하는 기능을 제공합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
|
||||
/**
|
||||
* BizBaseException 예외를 처리하는 메서드입니다.
|
||||
*
|
||||
* <p>BizBaseException이 발생했을 때 호출되며, 해당 예외의 오류 규칙을 기반으로
|
||||
* BizErrorResponse 객체를 생성하여 반환합니다.</p>
|
||||
*
|
||||
* @param e 발생한 BizBaseException 예외
|
||||
* @return 생성된 BizErrorResponse 객체
|
||||
*/
|
||||
@ExceptionHandler(BizBaseException.class)
|
||||
public BizErrorResponse handleCustomException(BizBaseException e) {
|
||||
return BizErrorResponse.valueOf(e.getErrorRule());
|
||||
return BizErrorResponse.fromErrorRule(e.getErrorRule());
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 일반 예외를 처리하는 메서드입니다.
|
||||
*
|
||||
* <p>예기치 않은 예외가 발생했을 때 호출되며, 시스템 오류에 대한 BizErrorResponse 객체를 생성하여 반환합니다.</p>
|
||||
*
|
||||
* @param e 발생한 일반 예외
|
||||
* @return 생성된 BizErrorResponse 객체
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public BizErrorResponse handleException(Exception e) {
|
||||
return BizErrorResponse.fromErrorRule(ExceptionRule.SYSTEM_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* MethodArgumentNotValidException 예외를 처리하는 메서드입니다.
|
||||
*
|
||||
* <p>MethodArgumentNotValidException이 발생했을 때 호출되며, 필드 오류 목록을 기반으로
|
||||
* BizErrorResponse 객체를 생성하여 반환합니다.</p>
|
||||
*
|
||||
* @param e 발생한 MethodArgumentNotValidException 예외
|
||||
* @return 생성된 BizErrorResponse 객체
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public BizErrorResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||
return BizErrorResponse.fromFieldError(e.getFieldErrors());
|
||||
}
|
||||
|
||||
/**
|
||||
* ConstraintViolationException 예외를 처리하는 메서드입니다.
|
||||
*
|
||||
* <p>ConstraintViolationException이 발생했을 때 호출되며, 제약 조건 위반에 대한 필드 오류 목록을 기반으로
|
||||
* BizErrorResponse 객체를 생성하여 반환합니다.</p>
|
||||
*
|
||||
* @param e 발생한 ConstraintViolationException 예외
|
||||
* @return 생성된 BizErrorResponse 객체
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public BizErrorResponse constraintViolationException(ConstraintViolationException e) {
|
||||
List<FieldError> fieldErrors = e.getConstraintViolations().stream()
|
||||
.map(violation -> new FieldError(
|
||||
violation.getRootBeanClass().getName(),
|
||||
violation.getPropertyPath().toString(),
|
||||
violation.getMessage()))
|
||||
.collect(Collectors.toList());
|
||||
return BizErrorResponse.fromFieldError(fieldErrors);
|
||||
}
|
||||
|
||||
/**
|
||||
* DataAccessException 예외를 처리하는 메서드입니다.
|
||||
*
|
||||
* <p>데이터 접근 중 예외가 발생했을 때 호출되며, 가장 구체적인 원인 예외를 기반으로
|
||||
* BizErrorResponse 객체를 생성하여 반환합니다.</p>
|
||||
*
|
||||
* @param e 발생한 DataAccessException 예외
|
||||
* @return 생성된 BizErrorResponse 객체
|
||||
*/
|
||||
@ExceptionHandler(DataAccessException.class)
|
||||
public BizErrorResponse handleDataAccessException(DataAccessException e) {
|
||||
Throwable cause = e.getMostSpecificCause();
|
||||
return BizErrorResponse.of(
|
||||
ExceptionRule.SYSTEM_ERROR,
|
||||
Optional.ofNullable(cause)
|
||||
.map(Throwable::getMessage)
|
||||
.orElse(ExceptionRule.SYSTEM_ERROR.getMessage())
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,16 +3,53 @@ package com.spring.common.error;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 요청에서 거부된 값을 나타내는 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 유효성 검사에서 실패한 필드와 관련된 정보를 포함합니다.</p>
|
||||
*
|
||||
* <p>각 RejectedValue 객체는 필드 이름, 거부된 값, 그리고 거부 사유를 저장합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class RejectedValue {
|
||||
|
||||
|
||||
/**
|
||||
* 거부된 필드의 이름입니다.
|
||||
*
|
||||
* <p>유효성 검사에서 실패한 필드의 이름을 나타냅니다.</p>
|
||||
*/
|
||||
private final String field;
|
||||
|
||||
/**
|
||||
* 거부된 값입니다.
|
||||
*
|
||||
* <p>유효성 검사에서 실패한 값입니다.</p>
|
||||
*/
|
||||
private final Object value;
|
||||
|
||||
/**
|
||||
* 거부 사유입니다.
|
||||
*
|
||||
* <p>유효성 검사에서 실패한 이유를 설명하는 메시지입니다.</p>
|
||||
*/
|
||||
private final String reason;
|
||||
|
||||
/**
|
||||
* RejectedValue 객체를 생성하는 정적 팩토리 메서드입니다.
|
||||
*
|
||||
* <p>주어진 필드 이름, 값, 및 사유를 사용하여 RejectedValue 객체를 생성합니다.</p>
|
||||
*
|
||||
* @param field 거부된 필드의 이름
|
||||
* @param value 거부된 값
|
||||
* @param reason 거부 사유
|
||||
* @return 생성된 RejectedValue 객체
|
||||
*/
|
||||
public static RejectedValue of(String field, Object value, String reason) {
|
||||
return new RejectedValue(field, value, reason);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -14,23 +14,57 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 모든 감사 엔티티의 기본 클래스를 정의하는 추상 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 생성일, 생성자, 수정일 및 수정자를 포함하여
|
||||
* 감사 관련 정보를 관리하는 데 사용됩니다.</p>
|
||||
*
|
||||
* <p>JPA의 @MappedSuperclass 어노테이션을 사용하여 이 클래스를 상속받는
|
||||
* 모든 엔티티가 이 필드를 포함할 수 있도록 합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Getter
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public abstract class AuditEntity {
|
||||
|
||||
/**
|
||||
* 엔티티가 생성된 날짜 및 시간입니다.
|
||||
*
|
||||
* <p>이 필드는 엔티티가 처음 생성될 때 자동으로 설정되며,
|
||||
* 업데이트할 수 없습니다.</p>
|
||||
*/
|
||||
@CreatedDate
|
||||
@Column(name = "CREATED_DATE", updatable = false, insertable = true)
|
||||
protected LocalDateTime createdDate;
|
||||
|
||||
/**
|
||||
* 엔티티를 생성한 사용자의 ID입니다.
|
||||
*
|
||||
* <p>이 필드는 엔티티가 생성될 때 자동으로 설정되며,
|
||||
* 업데이트할 수 없습니다.</p>
|
||||
*/
|
||||
@CreatedBy
|
||||
@Column(name = "CREATED_BY", updatable = false, insertable = true, length = 50)
|
||||
protected String createdBy;
|
||||
|
||||
/**
|
||||
* 엔티티가 마지막으로 수정된 날짜 및 시간입니다.
|
||||
*
|
||||
* <p>이 필드는 엔티티가 수정될 때 자동으로 설정됩니다.</p>
|
||||
*/
|
||||
@LastModifiedDate
|
||||
@Column(name = "MODIFIED_DATE", updatable = true, insertable = true)
|
||||
protected LocalDateTime modifiedDate;
|
||||
|
||||
/**
|
||||
* 엔티티를 마지막으로 수정한 사용자의 ID입니다.
|
||||
*
|
||||
* <p>이 필드는 엔티티가 수정될 때 자동으로 설정됩니다.</p>
|
||||
*/
|
||||
@LastModifiedBy
|
||||
@Column(name = "MODIFIED_BY", updatable = true, insertable = true, length = 50)
|
||||
protected String modifiedBy;
|
||||
|
||||
@@ -5,12 +5,30 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.domain.AuditorAware;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
|
||||
/**
|
||||
* JPA 감사 기능을 설정하는 구성 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring Data JPA의 감사 기능을 활성화하고,
|
||||
* 사용자 감사 정보를 제공하는 AuditorAware 빈을 정의합니다.</p>
|
||||
*
|
||||
* <p>감사 기능은 엔티티의 생성 및 수정 시점을 기록하는 데 사용됩니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Configuration
|
||||
@EnableJpaAuditing
|
||||
public class JpaAuditConfig {
|
||||
|
||||
|
||||
/**
|
||||
* 사용자 감사 정보를 제공하는 AuditorAware 빈을 생성합니다.
|
||||
*
|
||||
* <p>이 메서드는 현재 사용자의 ID를 반환하는 UserAuditorAware 인스턴스를 생성합니다.</p>
|
||||
*
|
||||
* @return AuditorAware<String> 현재 사용자의 ID를 제공하는 AuditorAware 인스턴스
|
||||
*/
|
||||
@Bean
|
||||
AuditorAware<String> auditorProvider(){
|
||||
AuditorAware<String> auditorProvider() {
|
||||
return new UserAuditorAware();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.spring.common.jpa;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class SpecBuilder {
|
||||
|
||||
public static <T> Builder<T> builder(Class<T> type) {
|
||||
return new Builder<>();
|
||||
}
|
||||
|
||||
public static class Builder<T> {
|
||||
private List<Specification<T>> specs = new ArrayList<>();
|
||||
|
||||
private void addSpec(Specification<T> spec) {
|
||||
if (spec != null) {
|
||||
specs.add(spec);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder<T> and(Specification<T> spec) {
|
||||
addSpec(spec);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> ifHasText(String str, Specification<T> spec) {
|
||||
if (StringUtils.hasText(str)) {
|
||||
addSpec(spec);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> ifTrue(Boolean cond, Supplier<Specification<T>> specSupplier) {
|
||||
if (cond != null && cond.booleanValue()) {
|
||||
addSpec(specSupplier.get());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public <V> Builder<T> ifNotNull(V value, Function<V, Specification<T>> specSupplier) {
|
||||
if (value != null) {
|
||||
addSpec(specSupplier.apply(value));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Specification<T> toSpec() {
|
||||
Specification<T> spec = Specification.where(null);
|
||||
for (Specification<T> s : specs) {
|
||||
spec = spec.and(s);
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,8 +9,27 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import com.spring.infra.security.domain.UserPrincipal;
|
||||
|
||||
/**
|
||||
* 현재 사용자의 감사 정보를 제공하는 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring Data JPA의 AuditorAware 인터페이스를 구현하여,
|
||||
* 현재 인증된 사용자의 ID를 반환합니다.</p>
|
||||
*
|
||||
* <p>사용자는 SecurityContextHolder를 통해 인증 정보를 가져오며,
|
||||
* 인증되지 않은 경우에는 빈 값을 반환합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
public class UserAuditorAware implements AuditorAware<String> {
|
||||
|
||||
|
||||
/**
|
||||
* 현재 인증된 사용자의 ID를 반환합니다.
|
||||
*
|
||||
* <p>사용자가 인증되지 않았거나 인증 정보가 없는 경우에는 빈 Optional을 반환합니다.</p>
|
||||
*
|
||||
* @return 현재 사용자의 ID가 포함된 Optional 객체, 인증되지 않은 경우 빈 Optional
|
||||
*/
|
||||
@Override
|
||||
public @NonNull Optional<String> getCurrentAuditor() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,16 +5,67 @@ import java.time.LocalDateTime;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* API 응답을 나타내는 제네릭 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 API 요청에 대한 응답을 구성하는 데 사용되며,
|
||||
* 응답의 상태 코드, 경로, 데이터, 메시지 등을 포함합니다.</p>
|
||||
*
|
||||
* <p>제네릭 타입 T를 사용하여 다양한 데이터 형식을 지원합니다.</p>
|
||||
*
|
||||
* @param <T> 응답 데이터의 타입
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Getter
|
||||
public class ApiResponse<T> {
|
||||
|
||||
/**
|
||||
* 응답 생성 시각입니다.
|
||||
*
|
||||
* <p>응답이 생성된 시각을 나타내며, LocalDateTime 객체로 저장됩니다.</p>
|
||||
*/
|
||||
private final LocalDateTime timestamp = LocalDateTime.now();
|
||||
|
||||
/**
|
||||
* HTTP 상태 코드입니다.
|
||||
*
|
||||
* <p>응답의 상태를 나타내는 정수 값입니다.</p>
|
||||
*/
|
||||
private int statusCode;
|
||||
|
||||
/**
|
||||
* 요청 경로입니다.
|
||||
*
|
||||
* <p>응답이 생성된 API의 경로를 나타냅니다.</p>
|
||||
*/
|
||||
private String path;
|
||||
|
||||
/**
|
||||
* 응답 데이터입니다.
|
||||
*
|
||||
* <p>제네릭 타입 T로 정의된 응답의 실제 데이터를 포함합니다.</p>
|
||||
*/
|
||||
private T data;
|
||||
|
||||
/**
|
||||
* 응답 메시지입니다.
|
||||
*
|
||||
* <p>응답에 대한 설명이나 상태 메시지를 포함합니다.</p>
|
||||
*/
|
||||
private String message;
|
||||
|
||||
@Builder
|
||||
/**
|
||||
* ApiResponse 객체를 생성하는 생성자입니다.
|
||||
*
|
||||
* <p>상태 코드, 경로, 데이터, 메시지를 사용하여 ApiResponse 객체를 초기화합니다.</p>
|
||||
*
|
||||
* @param statusCode 응답의 HTTP 상태 코드
|
||||
* @param path 요청 경로
|
||||
* @param data 응답 데이터
|
||||
* @param message 응답 메시지
|
||||
*/
|
||||
@Builder
|
||||
private ApiResponse(int statusCode, String path, T data, String message) {
|
||||
this.statusCode = statusCode;
|
||||
this.path = path;
|
||||
|
||||
@@ -21,23 +21,62 @@ import com.spring.common.error.GlobalExceptionHandler;
|
||||
import com.spring.common.validation.CollectionValidator;
|
||||
import com.spring.infra.security.error.SecurityExceptionHandler;
|
||||
|
||||
/**
|
||||
* API 응답을 래핑하는 전역 응답 처리기 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring의 @RestControllerAdvice를 사용하여 특정 패키지 내의 모든
|
||||
* 컨트롤러에서 발생하는 응답을 처리합니다.</p>
|
||||
*
|
||||
* <p>응답 본문을 ApiResponse 형식으로 변환하며, 오류 응답을 처리하는 기능을 제공합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@RestControllerAdvice(
|
||||
basePackages = "com.spring.domain",
|
||||
basePackageClasses = { GlobalExceptionHandler.class, SecurityExceptionHandler.class }
|
||||
)
|
||||
public class ResponseWrapper implements ResponseBodyAdvice<Object> {
|
||||
|
||||
/**
|
||||
* 유효성 검사기를 나타내는 필드입니다.
|
||||
*
|
||||
* <p>LocalValidatorFactoryBean을 사용하여 유효성 검사를 수행합니다.</p>
|
||||
*/
|
||||
private final LocalValidatorFactoryBean validator;
|
||||
|
||||
/**
|
||||
* ResponseWrapper 생성자입니다.
|
||||
*
|
||||
* <p>주입된 LocalValidatorFactoryBean을 사용하여 ResponseWrapper를 초기화합니다.</p>
|
||||
*
|
||||
* @param validator 유효성 검사기
|
||||
*/
|
||||
public ResponseWrapper(LocalValidatorFactoryBean validator) {
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 바인딩 초기화 메서드입니다.
|
||||
*
|
||||
* <p>WebDataBinder에 CollectionValidator를 추가하여 컬렉션 유효성 검사를 수행합니다.</p>
|
||||
*
|
||||
* @param binder WebDataBinder 객체
|
||||
*/
|
||||
@InitBinder
|
||||
public void initBinder(WebDataBinder binder) {
|
||||
binder.addValidators(new CollectionValidator(validator));
|
||||
}
|
||||
|
||||
/**
|
||||
* 응답 본문을 처리할 수 있는지 여부를 확인합니다.
|
||||
*
|
||||
* <p>CommHttpMessageConverter를 사용하여 응답 본문을 처리할 수 있는지 판단합니다.</p>
|
||||
*
|
||||
* @param returnType 반환할 메서드의 MethodParameter
|
||||
* @param converterType 사용된 HttpMessageConverter의 클래스
|
||||
* @return 처리할 수 있는 경우 true, 그렇지 않으면 false
|
||||
*/
|
||||
@Override
|
||||
public boolean supports(
|
||||
@NonNull MethodParameter returnType,
|
||||
@@ -46,6 +85,19 @@ public class ResponseWrapper implements ResponseBodyAdvice<Object> {
|
||||
return CommHttpMessageConverter.class.isAssignableFrom(converterType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 응답 본문을 작성하기 전에 호출되는 메서드입니다.
|
||||
*
|
||||
* <p>응답 본문이 ErrorResponse인 경우, ApiResponse 형식으로 변환하여 반환합니다.</p>
|
||||
*
|
||||
* @param body 응답 본문
|
||||
* @param returnType 반환할 메서드의 MethodParameter
|
||||
* @param selectedContentType 선택된 미디어 타입
|
||||
* @param selectedConverterType 선택된 HttpMessageConverter의 클래스
|
||||
* @param request HTTP 요청 객체
|
||||
* @param response HTTP 응답 객체
|
||||
* @return 변환된 ApiResponse 객체
|
||||
*/
|
||||
@Override
|
||||
public Object beforeBodyWrite(
|
||||
@Nullable Object body,
|
||||
@@ -66,7 +118,7 @@ public class ResponseWrapper implements ResponseBodyAdvice<Object> {
|
||||
.statusCode(errorRule.getStatus().value())
|
||||
.path(path)
|
||||
.data(errorData.getErrors())
|
||||
.message(errorRule.getMessage())
|
||||
.message(errorData.getCustomMessage() != null ? errorData.getCustomMessage() : errorRule.getMessage())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
184
batch-quartz/src/main/java/com/spring/common/util/TypeUtils.java
Normal file
184
batch-quartz/src/main/java/com/spring/common/util/TypeUtils.java
Normal file
@@ -0,0 +1,184 @@
|
||||
package com.spring.common.util;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class TypeUtils {
|
||||
|
||||
public static TypeInfo extractParameterInfo(Parameter parameter, String requestMethod) {
|
||||
return TypeInfo.builder()
|
||||
.type(extractTypeName(parameter.getParameterizedType()))
|
||||
.paramName(parameter.getName())
|
||||
.sourceType(determineSourceType(parameter, requestMethod))
|
||||
.typeDetails(extractTypeDetails(parameter.getParameterizedType()))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static TypeInfo extractReturnTypeInfo(Type returnType) {
|
||||
return TypeInfo.builder()
|
||||
.type(extractTypeName(returnType))
|
||||
.typeDetails(extractTypeDetails(returnType))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static String extractRequestMethod(Method method) {
|
||||
return Arrays.stream(method.getAnnotations())
|
||||
.filter(annotation -> annotation.annotationType().getSimpleName().endsWith("Mapping"))
|
||||
.findFirst()
|
||||
.map(annotation -> {
|
||||
if (annotation instanceof RequestMapping) {
|
||||
RequestMethod[] methods = ((RequestMapping) annotation).method();
|
||||
return methods.length > 0 ? methods[0].name() : HttpMethod.GET.name();
|
||||
}
|
||||
return annotation.annotationType().getSimpleName()
|
||||
.replace("Mapping", "")
|
||||
.toUpperCase();
|
||||
})
|
||||
.orElse(HttpMethod.GET.name());
|
||||
}
|
||||
|
||||
private static String extractTypeName(Type type) {
|
||||
if (type instanceof Class<?>) {
|
||||
return ((Class<?>) type).getSimpleName();
|
||||
}
|
||||
if (type instanceof ParameterizedType) {
|
||||
ParameterizedType paramType = (ParameterizedType) type;
|
||||
String rawType = ((Class<?>) paramType.getRawType()).getSimpleName();
|
||||
String typeArgs = Arrays.stream(paramType.getActualTypeArguments())
|
||||
.map(TypeUtils::extractTypeName)
|
||||
.collect(Collectors.joining(", "));
|
||||
return String.format("%s<%s>", rawType, typeArgs);
|
||||
}
|
||||
return type.getTypeName();
|
||||
}
|
||||
|
||||
private static TypeDetails extractTypeDetails(Type type) {
|
||||
Class<?> baseClass = getBaseClass(type);
|
||||
Class<?> actualTypeClass = getActualType(type);
|
||||
|
||||
// Collection 타입이면서 제네릭 타입이 복잡한 객체인 경우
|
||||
if (Collection.class.isAssignableFrom(baseClass) && isComplexType(actualTypeClass)) {
|
||||
return TypeDetails.builder()
|
||||
.isCollection(true)
|
||||
.fields(extractFields(actualTypeClass))
|
||||
.build();
|
||||
}
|
||||
// 일반 복잡한 객체인 경우
|
||||
else if (isComplexType(baseClass)) {
|
||||
return TypeDetails.builder()
|
||||
.isCollection(Collection.class.isAssignableFrom(baseClass))
|
||||
.fields(extractFields(baseClass))
|
||||
.build();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Class<?> getBaseClass(Type type) {
|
||||
if (type instanceof Class<?>) {
|
||||
return (Class<?>) type;
|
||||
}
|
||||
if (type instanceof ParameterizedType) {
|
||||
return (Class<?>) ((ParameterizedType) type).getRawType();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Class<?> getActualType(Type type) {
|
||||
if (type instanceof Class<?>) {
|
||||
return (Class<?>) type;
|
||||
}
|
||||
if (type instanceof ParameterizedType) {
|
||||
ParameterizedType paramType = (ParameterizedType) type;
|
||||
Type[] typeArgs = paramType.getActualTypeArguments();
|
||||
if (typeArgs.length > 0) {
|
||||
if (typeArgs[0] instanceof Class) {
|
||||
return (Class<?>) typeArgs[0];
|
||||
} else if (typeArgs[0] instanceof ParameterizedType) {
|
||||
// 중첩된 제네릭 타입의 경우 (ex: List<Map<String, Object>>)
|
||||
return (Class<?>) ((ParameterizedType) typeArgs[0]).getRawType();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isComplexType(Class<?> clazz) {
|
||||
return clazz != null &&
|
||||
!clazz.isPrimitive() &&
|
||||
!clazz.getName().startsWith("java.") &&
|
||||
!clazz.isEnum();
|
||||
}
|
||||
|
||||
private static List<FieldInfo> extractFields(Class<?> clazz) {
|
||||
if (clazz == null) return Collections.emptyList();
|
||||
|
||||
return Arrays.stream(clazz.getDeclaredFields())
|
||||
.filter(field -> !Modifier.isStatic(field.getModifiers()))
|
||||
.map(field -> new FieldInfo(
|
||||
extractTypeName(field.getGenericType()),
|
||||
field.getName()
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static SourceType determineSourceType(Parameter parameter, String requestMethod) {
|
||||
if (parameter.isAnnotationPresent(RequestBody.class)) return SourceType.BODY;
|
||||
if (parameter.isAnnotationPresent(RequestParam.class)) return SourceType.QUERY;
|
||||
if (parameter.isAnnotationPresent(PathVariable.class)) return SourceType.PATH;
|
||||
if (parameter.isAnnotationPresent(ModelAttribute.class) || isComplexType(parameter.getType())) {
|
||||
return "GET".equalsIgnoreCase(requestMethod) ? SourceType.QUERY : SourceType.FORM;
|
||||
}
|
||||
return SourceType.QUERY;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public static class TypeInfo {
|
||||
private final String type;
|
||||
private final String paramName;
|
||||
private final SourceType sourceType;
|
||||
private final TypeDetails typeDetails;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public static class TypeDetails {
|
||||
private final boolean isCollection;
|
||||
private final List<FieldInfo> fields;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public static class FieldInfo {
|
||||
private final String type;
|
||||
private final String name;
|
||||
}
|
||||
|
||||
public enum SourceType {
|
||||
BODY, QUERY, PATH, FORM
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,57 @@ import org.springframework.validation.ValidationUtils;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
|
||||
/**
|
||||
* 컬렉션의 유효성을 검사하는 Validator 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring의 Validator 인터페이스를 구현하여,
|
||||
* 컬렉션 내의 각 요소에 대해 유효성 검사를 수행합니다.</p>
|
||||
*
|
||||
* <p>주어진 LocalValidatorFactoryBean을 사용하여 각 요소의 유효성을 검사합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
public class CollectionValidator implements Validator {
|
||||
/**
|
||||
* 요소의 유효성을 검사하는 Validator 객체입니다.
|
||||
*
|
||||
* <p>LocalValidatorFactoryBean을 사용하여 초기화됩니다.</p>
|
||||
*/
|
||||
private final Validator validator;
|
||||
|
||||
/**
|
||||
* CollectionValidator 생성자입니다.
|
||||
*
|
||||
* <p>주어진 LocalValidatorFactoryBean을 사용하여 CollectionValidator를 초기화합니다.</p>
|
||||
*
|
||||
* @param validatorFactory 유효성 검사기 팩토리
|
||||
*/
|
||||
public CollectionValidator(LocalValidatorFactoryBean validatorFactory) {
|
||||
this.validator = validatorFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 클래스 타입에 대해 이 Validator가 지원하는지 확인합니다.
|
||||
*
|
||||
* <p>현재 구현에서는 모든 클래스 타입을 지원하도록 true를 반환합니다.</p>
|
||||
*
|
||||
* @param clazz 검사할 클래스 타입
|
||||
* @return 항상 true
|
||||
*/
|
||||
@Override
|
||||
public boolean supports(@NonNull Class<?> clazz) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 객체의 유효성을 검사합니다.
|
||||
*
|
||||
* <p>객체가 컬렉션인 경우, 컬렉션의 각 요소에 대해 유효성 검사를 수행합니다.</p>
|
||||
*
|
||||
* @param target 검사할 객체
|
||||
* @param errors 유효성 검사 중 발생한 오류를 저장할 객체
|
||||
*/
|
||||
@Override
|
||||
public void validate(@NonNull Object target, @NonNull Errors errors) {
|
||||
if (target instanceof Collection<?>) {
|
||||
|
||||
@@ -8,15 +8,41 @@ import javax.validation.ConstraintValidatorContext;
|
||||
|
||||
import org.quartz.CronExpression;
|
||||
|
||||
/**
|
||||
* Cron 표현식의 유효성을 검사하는 제약 조건 검증기 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring의 ConstraintValidator 인터페이스를 구현하여,
|
||||
* 주어진 문자열이 유효한 Cron 표현식인지 확인합니다.</p>
|
||||
*
|
||||
* <p>유효한 Cron 표현식은 다음 유효성 검사를 통과해야 합니다:</p>
|
||||
* <ul>
|
||||
* <li>Null 또는 빈 문자열이 아니어야 합니다.</li>
|
||||
* <li>CronExpression.isValidExpression 메서드를 통해 유효한 표현식인지 확인해야 합니다.</li>
|
||||
* <li>현재 시간 이후의 유효한 실행 시간을 가져올 수 있어야 합니다.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
public class CronExpressionValidator implements ConstraintValidator<ValidCronExpression, String> {
|
||||
|
||||
/**
|
||||
* 주어진 Cron 표현식의 유효성을 검사합니다.
|
||||
*
|
||||
* <p>유효성 검사 과정에서 Cron 표현식이 null이거나 빈 문자열인 경우 false를 반환합니다.
|
||||
* 유효한 Cron 표현식인 경우, 다음 실행 시간을 확인하여 true 또는 false를 반환합니다.</p>
|
||||
*
|
||||
* @param cronExpression 검사할 Cron 표현식
|
||||
* @param context 제약 조건 검증기 컨텍스트
|
||||
* @return 유효한 Cron 표현식인 경우 true, 그렇지 않으면 false
|
||||
*/
|
||||
@Override
|
||||
public boolean isValid(String cronExpression, ConstraintValidatorContext context) {
|
||||
if (cronExpression == null || cronExpression.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
boolean result = false;
|
||||
if(CronExpression.isValidExpression(cronExpression)){
|
||||
if (CronExpression.isValidExpression(cronExpression)) {
|
||||
try {
|
||||
CronExpression targetExpression = new CronExpression(cronExpression);
|
||||
if (targetExpression.getNextValidTimeAfter(new Date(System.currentTimeMillis())) != null) {
|
||||
|
||||
@@ -8,12 +8,51 @@ import java.lang.annotation.Target;
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
|
||||
/**
|
||||
* Enum 값의 유효성을 검사하기 위한 제약 조건 어노테이션입니다.
|
||||
*
|
||||
* <p>이 어노테이션은 필드, 메서드 또는 매개변수에 적용되어 해당 값이 지정된 Enum 타입의
|
||||
* 유효한 값인지 확인합니다.</p>
|
||||
*
|
||||
* <p>EnumValidator 클래스를 사용하여 유효성 검사를 수행합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Constraint(validatedBy = {EnumValidator.class})
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface EnumValid {
|
||||
|
||||
/**
|
||||
* 유효성 검사 실패 시 반환할 메시지입니다.
|
||||
*
|
||||
* @return 유효성 검사 실패 메시지
|
||||
*/
|
||||
String message() default "Invalid Enum Value.";
|
||||
|
||||
/**
|
||||
* 제약 조건 그룹을 지정합니다.
|
||||
*
|
||||
* <p>유효성 검사 그룹을 정의하여 특정 상황에서만 유효성 검사를 수행할 수 있습니다.</p>
|
||||
*
|
||||
* @return 제약 조건 그룹
|
||||
*/
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
/**
|
||||
* 페이로드를 지정합니다.
|
||||
*
|
||||
* <p>제약 조건에 대한 추가 정보를 제공하는 데 사용됩니다.</p>
|
||||
*
|
||||
* @return 페이로드 클래스 배열
|
||||
*/
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
||||
/**
|
||||
* 유효성을 검사할 대상 Enum 클래스입니다.
|
||||
*
|
||||
* @return 검사할 Enum 클래스
|
||||
*/
|
||||
Class<? extends java.lang.Enum<?>> target();
|
||||
}
|
||||
|
||||
@@ -3,14 +3,46 @@ package com.spring.common.validation;
|
||||
import javax.validation.ConstraintValidator;
|
||||
import javax.validation.ConstraintValidatorContext;
|
||||
|
||||
/**
|
||||
* Enum 값의 유효성을 검사하는 제약 조건 검증기 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring의 ConstraintValidator 인터페이스를 구현하여,
|
||||
* 주어진 Enum 값이 지정된 Enum 타입의 유효한 값인지 확인합니다.</p>
|
||||
*
|
||||
* <p>EnumValid 어노테이션을 사용하여 유효성 검사를 수행합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
public class EnumValidator implements ConstraintValidator<EnumValid, Enum<?>> {
|
||||
/**
|
||||
* EnumValid 어노테이션 인스턴스입니다.
|
||||
*
|
||||
* <p>유효성 검사에 필요한 설정 정보를 포함합니다.</p>
|
||||
*/
|
||||
private EnumValid annotation;
|
||||
|
||||
/**
|
||||
* EnumValid 어노테이션을 초기화합니다.
|
||||
*
|
||||
* <p>유효성 검사기 초기화 시 호출되며, 주어진 어노테이션을 저장합니다.</p>
|
||||
*
|
||||
* @param constraintAnnotation 초기화할 EnumValid 어노테이션
|
||||
*/
|
||||
@Override
|
||||
public void initialize(EnumValid constraintAnnotation) {
|
||||
this.annotation = constraintAnnotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 Enum 값의 유효성을 검사합니다.
|
||||
*
|
||||
* <p>값이 null인 경우 false를 반환하며, 지정된 Enum 타입의 유효한 값인지 확인합니다.</p>
|
||||
*
|
||||
* @param value 검사할 Enum 값
|
||||
* @param context 제약 조건 검증기 컨텍스트
|
||||
* @return 유효한 Enum 값인 경우 true, 그렇지 않으면 false
|
||||
*/
|
||||
@Override
|
||||
public boolean isValid(Enum<?> value, ConstraintValidatorContext context) {
|
||||
if (value == null) return false;
|
||||
|
||||
@@ -9,12 +9,43 @@ import java.lang.annotation.Target;
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
|
||||
/**
|
||||
* Cron 표현식의 유효성을 검사하기 위한 제약 조건 어노테이션입니다.
|
||||
*
|
||||
* <p>이 어노테이션은 필드에 적용되어 해당 값이 유효한 Cron 표현식인지 확인합니다.</p>
|
||||
*
|
||||
* <p>CronExpressionValidator 클래스를 사용하여 유효성 검사를 수행합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Documented
|
||||
@Constraint(validatedBy = CronExpressionValidator.class)
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ValidCronExpression {
|
||||
/**
|
||||
* 유효성 검사 실패 시 반환할 메시지입니다.
|
||||
*
|
||||
* @return 유효성 검사 실패 메시지
|
||||
*/
|
||||
String message() default "Invalid cron expression";
|
||||
|
||||
/**
|
||||
* 제약 조건 그룹을 지정합니다.
|
||||
*
|
||||
* <p>유효성 검사 그룹을 정의하여 특정 상황에서만 유효성 검사를 수행할 수 있습니다.</p>
|
||||
*
|
||||
* @return 제약 조건 그룹
|
||||
*/
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
/**
|
||||
* 페이로드를 지정합니다.
|
||||
*
|
||||
* <p>제약 조건에 대한 추가 정보를 제공하는 데 사용됩니다.</p>
|
||||
*
|
||||
* @return 페이로드 클래스 배열
|
||||
*/
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.spring.domain.post.repository.PostRepository;
|
||||
import com.spring.domain.email.service.EmailSendService;
|
||||
import com.spring.infra.batch.AbstractBatchTask;
|
||||
import com.spring.infra.batch.BatchJobInfo;
|
||||
|
||||
@@ -25,11 +25,10 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@RequiredArgsConstructor
|
||||
public class EmailSendBatch extends AbstractBatchTask {
|
||||
|
||||
private final PostRepository postRepository;
|
||||
private final EmailSendService emailSendService;
|
||||
|
||||
@Override
|
||||
protected List<Step> createSteps() {
|
||||
log.info("EmailSendBatch -> createSteps");
|
||||
return List.of(
|
||||
addStep("emailSendJobStep1", createTasklet()),
|
||||
addStep("emailSendJobStep2", createSendTasklet())
|
||||
@@ -40,13 +39,12 @@ public class EmailSendBatch extends AbstractBatchTask {
|
||||
protected Tasklet createTasklet() {
|
||||
log.info("EmailSendBatch -> createTasklet");
|
||||
return ((contribution, chunkContext) -> {
|
||||
postRepository.findAll();
|
||||
emailSendService.sendEmail();
|
||||
return RepeatStatus.FINISHED;
|
||||
});
|
||||
}
|
||||
|
||||
private Tasklet createSendTasklet() {
|
||||
log.info("EmailSendBatch -> createSendTasklet");
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.spring.domain.endpoint.api;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.spring.domain.endpoint.dto.EndpointInfo;
|
||||
import com.spring.domain.endpoint.service.EndpointCollectorService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/endpoint")
|
||||
public class EndpointApi {
|
||||
|
||||
private final EndpointCollectorService endpointCollectorService;
|
||||
|
||||
@GetMapping
|
||||
public List<EndpointInfo> getEndpoints() {
|
||||
return endpointCollectorService.collectApiEndpoints();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.spring.domain.endpoint.dto;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.spring.common.util.TypeUtils.TypeInfo;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class EndpointInfo {
|
||||
|
||||
private final String uri;
|
||||
private final Set<String> httpMethods;
|
||||
private final String controllerName;
|
||||
private final String methodName;
|
||||
private final List<TypeInfo> parameters;
|
||||
private final TypeInfo returnType;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.spring.domain.endpoint.service;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import com.spring.common.util.TypeUtils;
|
||||
import com.spring.common.util.TypeUtils.TypeInfo;
|
||||
import com.spring.domain.endpoint.dto.EndpointInfo;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* EndpointCollectorService는 Spring MVC의 @RestController로 정의된 API 엔드포인트를 수집하는 서비스입니다.
|
||||
* 이 서비스는 RequestMappingHandlerMapping을 사용하여 모든 핸들러 메서드를 가져오고,
|
||||
* 각 메서드의 URI, HTTP 메서드, 클래스 이름, 메서드 이름, 파라미터 정보 및 반환 타입 정보를 포함하는 EndpointInfo 객체를 생성합니다.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EndpointCollectorService {
|
||||
|
||||
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
|
||||
|
||||
/**
|
||||
* collectApiEndpoints 메서드는 모든 API 엔드포인트를 수집하여 EndpointInfo 객체의 리스트를 반환합니다.
|
||||
* @return 수집된 API 엔드포인트의 리스트
|
||||
*/
|
||||
public List<EndpointInfo> collectApiEndpoints() {
|
||||
List<EndpointInfo> endpoints = new ArrayList<>();
|
||||
|
||||
// 핸들러 메서드 맵 가져오기
|
||||
Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
|
||||
|
||||
handlerMethods.forEach((info, method) -> {
|
||||
// @RestController가 붙은 클래스의 메서드만 처리
|
||||
if (method.getBeanType().isAnnotationPresent(RestController.class)) {
|
||||
|
||||
Set<String> patterns = Optional.ofNullable(info.getPathPatternsCondition())
|
||||
.map(condition -> condition.getPatternValues())
|
||||
.orElse(Collections.emptySet());
|
||||
|
||||
Set<String> methods = new HashSet<>();
|
||||
Optional.ofNullable(info.getMethodsCondition())
|
||||
.ifPresent(methodsCondition ->
|
||||
methodsCondition.getMethods()
|
||||
.forEach(requestMethod -> methods.add(requestMethod.name()))
|
||||
);
|
||||
|
||||
String className = method.getBeanType().getSimpleName();
|
||||
String methodName = method.getMethod().getName();
|
||||
List<TypeInfo> parameterInfos = extractParameterInfos(method.getMethod());
|
||||
TypeInfo returnTypeInfo = extractReturnTypeInfo(method.getMethod());
|
||||
|
||||
patterns.forEach(pattern -> {
|
||||
EndpointInfo endpoint = new EndpointInfo(
|
||||
pattern,
|
||||
methods.isEmpty() ? Collections.singleton("ALL") : methods,
|
||||
className,
|
||||
methodName,
|
||||
parameterInfos,
|
||||
returnTypeInfo
|
||||
);
|
||||
endpoints.add(endpoint);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return endpoints.stream()
|
||||
.sorted(Comparator.comparing(EndpointInfo::getUri))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* extractParameterInfos 메서드는 주어진 메서드의 파라미터 정보를 추출하여 리스트로 반환합니다.
|
||||
* @param method 파라미터 정보를 추출할 메서드
|
||||
* @return 메서드의 파라미터 정보 리스트
|
||||
*/
|
||||
private List<TypeInfo> extractParameterInfos(Method method) {
|
||||
String requestMethod = TypeUtils.extractRequestMethod(method);
|
||||
return Arrays.stream(method.getParameters())
|
||||
.map(parameter -> TypeUtils.extractParameterInfo(parameter, requestMethod))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* extractReturnTypeInfo 메서드는 주어진 메서드의 반환 타입 정보를 추출하여 반환합니다.
|
||||
* @param method 반환 타입 정보를 추출할 메서드
|
||||
* @return 메서드의 반환 타입 정보
|
||||
*/
|
||||
private TypeInfo extractReturnTypeInfo(Method method) {
|
||||
return TypeUtils.extractReturnTypeInfo(method.getGenericReturnType());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -65,12 +65,11 @@ public class PostCreateBatchChunk extends AbstractBatchChunk {
|
||||
public Job createJob() {
|
||||
return new JobBuilder(batchJobInfoData.getJobName())
|
||||
.repository(jobRepository)
|
||||
.incrementer(new RunIdIncrementer())
|
||||
.start(readListStep())
|
||||
.next(decider())
|
||||
.from(decider()).on("PROCESS").to(processStep())
|
||||
.from(decider()).on("TERMINATE").to(terminateStep())
|
||||
.end()
|
||||
// .incrementer(new RunIdIncrementer())
|
||||
.start(processStep())
|
||||
// .next(decider())
|
||||
// .from(decider()).on("PROCESS").to(processStep())
|
||||
// .from(decider()).on("TERMINATE").to(terminateStep())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ package com.spring.domain.schedule.api;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
@@ -17,13 +20,17 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequestMapping("/api/dashboard")
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@Validated
|
||||
public class DashBoardApi {
|
||||
|
||||
private final BatchChartService batchChartService;
|
||||
private final DashBoardJobService dashBoardJobService;
|
||||
|
||||
@GetMapping("/chart")
|
||||
public BatchChartResponse getBatchJobExecutionData(@RequestParam int year, @RequestParam int month) {
|
||||
public BatchChartResponse getBatchJobExecutionData(
|
||||
@NotNull(message = "년도는 필수값 입니다.") @RequestParam(required = false) Integer year,
|
||||
@NotNull(message = "월은 필수값 입니다.") @RequestParam(required = false) Integer month
|
||||
) {
|
||||
return batchChartService.getBatchJobExecutionData(year, month);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.spring.domain.schedule.dto.BatchJobHistoryResponse;
|
||||
import com.spring.domain.schedule.dto.FindJobHistoryRequest;
|
||||
import com.spring.domain.schedule.dto.ReScheduleJobRequest;
|
||||
import com.spring.domain.schedule.dto.ScheduleJobResponse;
|
||||
import com.spring.domain.schedule.service.FindJobHistoryService;
|
||||
import com.spring.domain.schedule.service.FindScheduleJobService;
|
||||
import com.spring.domain.schedule.service.ReScheduleJobService;
|
||||
import com.spring.domain.schedule.service.ScheduleControlService;
|
||||
@@ -25,10 +28,11 @@ import lombok.RequiredArgsConstructor;
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class ScheduleJobApi {
|
||||
|
||||
|
||||
private final FindScheduleJobService findScheduleJobService;
|
||||
private final ReScheduleJobService reScheduleJobService;
|
||||
private final ScheduleControlService scheduleControlService;
|
||||
private final FindJobHistoryService findJobHistoryService;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
|
||||
@@ -66,4 +70,14 @@ public class ScheduleJobApi {
|
||||
return reScheduleJobService.rescheduleJob(request);
|
||||
}
|
||||
|
||||
@GetMapping("/history")
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
|
||||
public List<BatchJobHistoryResponse> getJobHistory(
|
||||
@Valid FindJobHistoryRequest request,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "50") int size
|
||||
) {
|
||||
return findJobHistoryService.getJobHistory(request, page, size);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.spring.domain.schedule.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public interface BatchJobHistoryProjection {
|
||||
String getJobName();
|
||||
LocalDateTime getStartTime();
|
||||
LocalDateTime getEndTime();
|
||||
String getStatus();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.spring.domain.schedule.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import com.spring.domain.schedule.entity.BatchJobExecution;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class BatchJobHistoryResponse {
|
||||
|
||||
private final String jobName;
|
||||
private final LocalDateTime startTime;
|
||||
private final LocalDateTime endTime;
|
||||
private final String status;
|
||||
|
||||
public static BatchJobHistoryResponse fromEntity(BatchJobExecution batch) {
|
||||
return new BatchJobHistoryResponse(batch.getJobInstance().getJobName(), batch.getStartTime(), batch.getEndTime(), batch.getStatus());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.spring.domain.schedule.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import javax.persistence.criteria.Join;
|
||||
import javax.persistence.criteria.JoinType;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import com.spring.common.jpa.SpecBuilder;
|
||||
import com.spring.domain.schedule.entity.BatchJobExecution;
|
||||
import com.spring.domain.schedule.entity.BatchJobInstance;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class FindJobHistoryRequest {
|
||||
|
||||
@NotNull(message = "시작일자는 필수값 입니다.")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private final LocalDateTime fromDate;
|
||||
|
||||
@NotNull(message = "종료일자는 필수값 입니다.")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private final LocalDateTime toDate;
|
||||
|
||||
private final String jobName;
|
||||
|
||||
public Specification<BatchJobExecution> betweenCreateTime() {
|
||||
return (root, query, builder) -> builder.between(root.get("createTime"), fromDate, toDate);
|
||||
}
|
||||
|
||||
public Specification<BatchJobExecution> equalJobName() {
|
||||
return (root, query, builder) -> {
|
||||
Join<BatchJobExecution, BatchJobInstance> join = root.join("jobInstance", JoinType.INNER);
|
||||
return builder.equal(join.get("jobName"), jobName);
|
||||
};
|
||||
}
|
||||
|
||||
public Specification<BatchJobExecution> toPredicate() {
|
||||
return SpecBuilder.builder(BatchJobExecution.class)
|
||||
.and(betweenCreateTime())
|
||||
.ifHasText(jobName, equalJobName())
|
||||
.toSpec();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +1,33 @@
|
||||
package com.spring.domain.schedule.repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import com.spring.domain.schedule.dto.BatchJobAverageDurationProjection;
|
||||
import com.spring.domain.schedule.dto.BatchJobExecutionProjection;
|
||||
import com.spring.domain.schedule.dto.BatchJobHistoryProjection;
|
||||
import com.spring.domain.schedule.dto.BatchJobHourProjection;
|
||||
import com.spring.domain.schedule.dto.BatchJobStatusCountProjection;
|
||||
import com.spring.domain.schedule.entity.BatchJobExecution;
|
||||
|
||||
public interface BatchJobExecutionRepository extends JpaRepository<BatchJobExecution, Long> {
|
||||
|
||||
public interface BatchJobExecutionRepository extends JpaRepository<BatchJobExecution, Long>, JpaSpecificationExecutor<BatchJobExecution> {
|
||||
|
||||
@Query("SELECT bji.jobName AS jobName, " +
|
||||
"bje.startTime AS startTime, " +
|
||||
"bje.endTime AS endTime, " +
|
||||
"bje.status AS status " +
|
||||
"FROM BatchJobExecution bje " +
|
||||
"JOIN bje.jobInstance bji " +
|
||||
"WHERE bje.createTime BETWEEN :fromDate AND :toDate " +
|
||||
"ORDER BY bje.createTime DESC")
|
||||
List<BatchJobHistoryProjection> findJobHistory(@Param("fromDate") LocalDateTime fromDate, @Param("toDate") LocalDateTime toDate, Pageable pageable);
|
||||
|
||||
@Query("SELECT bji.jobName AS jobName, " +
|
||||
"bje.startTime AS startTime, " +
|
||||
"bje.endTime AS endTime " +
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.spring.domain.schedule.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.spring.domain.schedule.dto.BatchJobHistoryResponse;
|
||||
import com.spring.domain.schedule.dto.FindJobHistoryRequest;
|
||||
import com.spring.domain.schedule.repository.BatchJobExecutionRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FindJobHistoryService {
|
||||
|
||||
private final BatchJobExecutionRepository batchJobExecutionRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<BatchJobHistoryResponse> getJobHistory(FindJobHistoryRequest request, int page, int size) {
|
||||
return batchJobExecutionRepository.findAll(request.toPredicate(), PageRequest.of(page, size)).stream()
|
||||
.map(BatchJobHistoryResponse::fromEntity)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,6 +51,7 @@ public class SignUpRequest {
|
||||
.approved(false)
|
||||
.userRole(userRole)
|
||||
.createdBy(userId)
|
||||
.modifiedBy(userId)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public class UserManagementResponse {
|
||||
private final String userId;
|
||||
private final String userName;
|
||||
private final String email;
|
||||
private final boolean isApproved;
|
||||
private final boolean approved;
|
||||
private final AgentUserRole userRole;
|
||||
|
||||
public static UserManagementResponse valueOf(AgentUser user) {
|
||||
|
||||
@@ -51,7 +51,7 @@ public class AgentUser extends AuditEntity {
|
||||
private AgentUserRole userRole;
|
||||
|
||||
@Builder
|
||||
public AgentUser(String userId, String password, String name, AgentUserRole userRole, String email, boolean approved, String createdBy) {
|
||||
public AgentUser(String userId, String password, String name, AgentUserRole userRole, String email, boolean approved, String createdBy, String modifiedBy) {
|
||||
this.userId = userId;
|
||||
this.password = password;
|
||||
this.name = name;
|
||||
@@ -59,6 +59,7 @@ public class AgentUser extends AuditEntity {
|
||||
this.email = email;
|
||||
this.approved = approved;
|
||||
this.createdBy = createdBy;
|
||||
this.modifiedBy = modifiedBy;
|
||||
}
|
||||
|
||||
public void changePassword(String newPassword) {
|
||||
|
||||
@@ -47,8 +47,4 @@ public class AgentUserToken extends AuditEntity {
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
public boolean validateRefreshToken(String refreshToken) {
|
||||
return this.refreshToken.equals(refreshToken);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,16 +19,6 @@ public enum UserRule implements ErrorRule {
|
||||
NEW_PASSWORD_SAME_AS_CURRENT(HttpStatus.BAD_REQUEST, "새 비밀번호는 현재 비밀번호와 달라야 합니다.");
|
||||
|
||||
private final HttpStatus status;
|
||||
private String message;
|
||||
|
||||
UserRule(HttpStatus status, String message) {
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public UserRule message(final String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
private final String message;
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.spring.domain.user.dto.UserManagementResponse;
|
||||
import com.spring.domain.user.entity.AgentUser;
|
||||
import com.spring.domain.user.error.UserNotFoundException;
|
||||
import com.spring.domain.user.repository.AgentUserRepository;
|
||||
import com.spring.domain.user.repository.AgentUserTokenRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -21,6 +22,7 @@ import lombok.RequiredArgsConstructor;
|
||||
public class UserManagementService {
|
||||
|
||||
private final AgentUserRepository agentUserRepository;
|
||||
private final AgentUserTokenRepository agentUserTokenRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<UserManagementResponse> getUsers(UserFindRequest request) {
|
||||
@@ -42,8 +44,10 @@ public class UserManagementService {
|
||||
|
||||
@Transactional
|
||||
public void deleteUser(String id) {
|
||||
AgentUser user = agentUserRepository.findById(UUID.fromString(id))
|
||||
.orElseThrow(UserNotFoundException::new);
|
||||
AgentUser user = agentUserRepository.findById(UUID.fromString(id)).orElseThrow(UserNotFoundException::new);
|
||||
if (agentUserTokenRepository.findById(UUID.fromString(id)).isPresent()) {
|
||||
agentUserTokenRepository.deleteById(UUID.fromString(id));
|
||||
}
|
||||
agentUserRepository.delete(user);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@ public class UserRefreshTokenService implements RefreshTokenService {
|
||||
@Transactional
|
||||
@Override
|
||||
public void deleteRefreshToken(String key) {
|
||||
agentUserTokenRepository.deleteById(UUID.fromString(key));
|
||||
if (agentUserTokenRepository.findById(UUID.fromString(key)).isPresent()) {
|
||||
agentUserTokenRepository.deleteById(UUID.fromString(key));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,16 +2,52 @@ package com.spring.infra.batch;
|
||||
|
||||
import org.springframework.batch.core.Job;
|
||||
|
||||
/**
|
||||
* 배치 작업을 정의하는 추상 인터페이스입니다.
|
||||
*
|
||||
* <p>이 인터페이스는 배치 작업의 초기화, 등록 및 생성과 관련된 메서드를 정의합니다.
|
||||
* 구현 클래스는 이 인터페이스를 구현하여 특정 배치 작업의 로직을 제공해야 합니다.</p>
|
||||
*
|
||||
* <p>주요 메서드로는 배치 작업 정보를 초기화하고, 작업 빈을 등록하며,
|
||||
* 배치 작업을 생성하는 기능이 포함됩니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface AbstractBatch {
|
||||
|
||||
/**
|
||||
* 배치 작업 정보를 초기화합니다.
|
||||
*
|
||||
* <p>배치 작업을 실행하기 전에 필요한 초기 설정을 수행합니다.</p>
|
||||
*/
|
||||
void initializeBatchJobInfo();
|
||||
|
||||
/**
|
||||
* 배치 작업에 필요한 빈을 등록합니다.
|
||||
*
|
||||
* <p>배치 작업에서 사용할 JobBean을 등록하는 메서드입니다.</p>
|
||||
*/
|
||||
void registerJobBean();
|
||||
|
||||
/**
|
||||
* 배치 작업을 생성합니다.
|
||||
*
|
||||
* <p>배치 작업을 생성하고 반환합니다.</p>
|
||||
*
|
||||
* @return 생성된 Job 객체
|
||||
*/
|
||||
Job createJob();
|
||||
|
||||
/**
|
||||
* 스코프가 지정된 타겟 프리픽스를 제거합니다.
|
||||
*
|
||||
* <p>주어진 빈 이름에서 "scopedTarget." 접두사를 제거하여 반환합니다.</p>
|
||||
*
|
||||
* @param beanName 빈 이름
|
||||
* @return 수정된 빈 이름
|
||||
*/
|
||||
default String removeScopedTargetPrefix(String beanName) {
|
||||
return beanName.startsWith("scopedTarget.") ? beanName.substring("scopedTarget.".length()) : beanName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,21 +15,66 @@ import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
/**
|
||||
* 배치 청크 처리를 위한 추상 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring Batch의 청크 기반 처리 로직을 구현하기 위한 기본 클래스로,
|
||||
* 배치 작업의 초기화 및 등록, JobRepository 및 TransactionManager와의 통합을 제공합니다.</p>
|
||||
*
|
||||
* <p>구현 클래스는 이 클래스를 상속받아 특정 배치 작업의 로직을 정의해야 합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Configuration
|
||||
public abstract class AbstractBatchChunk implements AbstractBatch, ApplicationContextAware, InitializingBean {
|
||||
|
||||
/**
|
||||
* 배치 작업 정보.
|
||||
*
|
||||
* <p>BatchJobInfo 어노테이션을 통해 정의된 배치 작업의 메타데이터를 포함합니다.</p>
|
||||
*/
|
||||
private final BatchJobInfo batchJobInfo;
|
||||
|
||||
/**
|
||||
* 배치 작업 정보 서비스.
|
||||
*
|
||||
* <p>배치 작업 정보를 관리하는 서비스 객체입니다.</p>
|
||||
*/
|
||||
private BatchJobInfoService batchJobInfoService;
|
||||
|
||||
/**
|
||||
* 애플리케이션 컨텍스트.
|
||||
*
|
||||
* <p>Spring의 ApplicationContext를 통해 필요한 빈을 가져옵니다.</p>
|
||||
*/
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
/**
|
||||
* 배치 작업 정보 데이터.
|
||||
*
|
||||
* <p>배치 작업의 실행에 필요한 데이터를 포함합니다.</p>
|
||||
*/
|
||||
protected BatchJobInfoData batchJobInfoData;
|
||||
|
||||
/**
|
||||
* JobRepository.
|
||||
*
|
||||
* <p>배치 작업의 상태를 관리하는 리포지토리입니다.</p>
|
||||
*/
|
||||
protected JobRepository jobRepository;
|
||||
|
||||
/**
|
||||
* 트랜잭션 매니저.
|
||||
*
|
||||
* <p>배치 작업의 트랜잭션 관리를 담당합니다.</p>
|
||||
*/
|
||||
protected PlatformTransactionManager transactionManager;
|
||||
|
||||
|
||||
/**
|
||||
* 기본 생성자입니다.
|
||||
* <p>
|
||||
* BatchJobInfo 어노테이션을 찾고, 없으면 예외를 발생시킵니다.
|
||||
* </p>
|
||||
*
|
||||
* <p>BatchJobInfo 어노테이션을 찾고, 없으면 예외를 발생시킵니다.</p>
|
||||
*/
|
||||
protected AbstractBatchChunk() {
|
||||
this.batchJobInfo = AnnotationUtils.findAnnotation(getClass(), BatchJobInfo.class);
|
||||
@@ -38,11 +83,24 @@ public abstract class AbstractBatchChunk implements AbstractBatch, ApplicationCo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 애플리케이션 컨텍스트를 설정합니다.
|
||||
*
|
||||
* @param applicationContext 설정할 ApplicationContext
|
||||
* @throws BeansException 빈 설정 중 발생할 수 있는 예외
|
||||
*/
|
||||
@Override
|
||||
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 빈의 속성이 설정된 후 호출됩니다.
|
||||
*
|
||||
* <p>배치 작업 정보 서비스 초기화 및 배치 작업 등록을 수행합니다.</p>
|
||||
*
|
||||
* @throws Exception 초기화 중 발생할 수 있는 예외
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
this.batchJobInfoService = applicationContext.getBean(BatchJobInfoService.class);
|
||||
@@ -50,6 +108,11 @@ public abstract class AbstractBatchChunk implements AbstractBatch, ApplicationCo
|
||||
registerJobBean();
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 작업 정보를 초기화합니다.
|
||||
*
|
||||
* <p>배치 작업의 메타데이터를 로드하여 설정합니다.</p>
|
||||
*/
|
||||
@Override
|
||||
public void initializeBatchJobInfo() {
|
||||
String beanName = applicationContext.getBeanNamesForType(this.getClass())[0];
|
||||
@@ -58,6 +121,8 @@ public abstract class AbstractBatchChunk implements AbstractBatch, ApplicationCo
|
||||
|
||||
/**
|
||||
* 배치 작업을 Spring의 Bean으로 등록합니다.
|
||||
*
|
||||
* <p>JobRepository에 배치 작업을 등록하여 실행할 수 있도록 합니다.</p>
|
||||
*/
|
||||
@Override
|
||||
public void registerJobBean() {
|
||||
@@ -90,6 +155,13 @@ public abstract class AbstractBatchChunk implements AbstractBatch, ApplicationCo
|
||||
this.transactionManager = transactionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 작업을 생성합니다.
|
||||
*
|
||||
* <p>구현 클래스에서 정의해야 하는 추상 메서드입니다.</p>
|
||||
*
|
||||
* @return 생성된 Job 객체
|
||||
*/
|
||||
@Override
|
||||
public abstract Job createJob();
|
||||
|
||||
|
||||
@@ -25,9 +25,11 @@ import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
/**
|
||||
* 배치 작업을 정의하는 추상 클래스입니다.
|
||||
* <p>
|
||||
* 이 클래스는 배치 작업의 기본 설정 및 실행을 관리합니다.
|
||||
* </p>
|
||||
*
|
||||
* <p>이 클래스는 배치 작업의 기본 설정 및 실행을 관리하며,
|
||||
* 배치 작업의 메타데이터를 초기화하고, JobRepository에 등록하는 기능을 제공합니다.</p>
|
||||
*
|
||||
* <p>구현 클래스는 이 클래스를 상속받아 특정 배치 작업의 로직을 정의해야 합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
@@ -35,18 +37,54 @@ import org.springframework.transaction.PlatformTransactionManager;
|
||||
@Configuration
|
||||
public abstract class AbstractBatchTask implements AbstractBatch, ApplicationContextAware, InitializingBean {
|
||||
|
||||
/**
|
||||
* 배치 작업 정보.
|
||||
*
|
||||
* <p>BatchJobInfo 어노테이션을 통해 정의된 배치 작업의 메타데이터를 포함합니다.</p>
|
||||
*/
|
||||
private final BatchJobInfo batchJobInfo;
|
||||
|
||||
/**
|
||||
* 배치 작업 정보 서비스.
|
||||
*
|
||||
* <p>배치 작업 정보를 관리하는 서비스 객체입니다.</p>
|
||||
*/
|
||||
private BatchJobInfoService batchJobInfoService;
|
||||
|
||||
/**
|
||||
* 배치 작업 정보 데이터.
|
||||
*
|
||||
* <p>배치 작업의 실행에 필요한 데이터를 포함합니다.</p>
|
||||
*/
|
||||
private BatchJobInfoData batchJobInfoData;
|
||||
|
||||
/**
|
||||
* 애플리케이션 컨텍스트.
|
||||
*
|
||||
* <p>Spring의 ApplicationContext를 통해 필요한 빈을 가져옵니다.</p>
|
||||
*/
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
/**
|
||||
* JobRepository.
|
||||
*
|
||||
* <p>배치 작업의 상태를 관리하는 리포지토리입니다.</p>
|
||||
*/
|
||||
private JobRepository jobRepository;
|
||||
|
||||
/**
|
||||
* 트랜잭션 매니저.
|
||||
*
|
||||
* <p>배치 작업의 트랜잭션 관리를 담당합니다.</p>
|
||||
*/
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
private BatchExceptionListener batchExceptionListener;
|
||||
|
||||
/**
|
||||
* 기본 생성자입니다.
|
||||
* <p>
|
||||
* BatchJobInfo 어노테이션을 찾고, 없으면 예외를 발생시킵니다.
|
||||
* </p>
|
||||
*
|
||||
* <p>BatchJobInfo 어노테이션을 찾고, 없으면 예외를 발생시킵니다.</p>
|
||||
*/
|
||||
protected AbstractBatchTask() {
|
||||
this.batchJobInfo = AnnotationUtils.findAnnotation(getClass(), BatchJobInfo.class);
|
||||
@@ -55,18 +93,36 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 애플리케이션 컨텍스트를 설정합니다.
|
||||
*
|
||||
* @param applicationContext 설정할 ApplicationContext
|
||||
* @throws BeansException 빈 설정 중 발생할 수 있는 예외
|
||||
*/
|
||||
@Override
|
||||
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
this.batchJobInfoService = applicationContext.getBean(BatchJobInfoService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 빈의 속성이 설정된 후 호출됩니다.
|
||||
*
|
||||
* <p>배치 작업 정보 초기화 및 배치 작업 등록을 수행합니다.</p>
|
||||
*
|
||||
* @throws Exception 초기화 중 발생할 수 있는 예외
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
initializeBatchJobInfo();
|
||||
registerJobBean();
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 작업 정보를 초기화합니다.
|
||||
*
|
||||
* <p>배치 작업의 메타데이터를 로드하여 설정합니다.</p>
|
||||
*/
|
||||
@Override
|
||||
public void initializeBatchJobInfo() {
|
||||
String beanName = applicationContext.getBeanNamesForType(this.getClass())[0];
|
||||
@@ -75,6 +131,8 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
||||
|
||||
/**
|
||||
* 배치 작업을 Spring의 Bean으로 등록합니다.
|
||||
*
|
||||
* <p>JobRepository에 배치 작업을 등록하여 실행할 수 있도록 합니다.</p>
|
||||
*/
|
||||
@Override
|
||||
public void registerJobBean() {
|
||||
@@ -107,9 +165,16 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
||||
this.transactionManager = transactionManager;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setBatchExceptionListener(BatchExceptionListener batchExceptionListener) {
|
||||
this.batchExceptionListener = batchExceptionListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 작업을 생성합니다.
|
||||
*
|
||||
* <p>구현 클래스에서 정의해야 하는 추상 메서드입니다.</p>
|
||||
*
|
||||
* @return 생성된 Job 객체
|
||||
* @throws IllegalStateException STEP이 정의되지 않은 경우 예외 발생
|
||||
*/
|
||||
@@ -122,6 +187,7 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
||||
var jobBuilder = new JobBuilder(batchJobInfoData.getJobName())
|
||||
.incrementer(new RunIdIncrementer())
|
||||
.repository(jobRepository)
|
||||
.listener(batchExceptionListener)
|
||||
.start(steps.get(0));
|
||||
for (int i = 1; i < steps.size(); i++) {
|
||||
jobBuilder = jobBuilder.next(steps.get(i));
|
||||
@@ -132,6 +198,8 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
||||
/**
|
||||
* 배치 작업의 STEP을 생성합니다.
|
||||
*
|
||||
* <p>기본적으로 하나의 STEP을 생성하여 반환합니다.</p>
|
||||
*
|
||||
* @return 생성된 Step 리스트
|
||||
*/
|
||||
protected List<Step> createSteps() {
|
||||
@@ -143,6 +211,8 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
||||
/**
|
||||
* STEP을 추가합니다.
|
||||
*
|
||||
* <p>주어진 이름과 Tasklet을 사용하여 Step 객체를 생성합니다.</p>
|
||||
*
|
||||
* @param stepName STEP의 이름
|
||||
* @param tasklet STEP에서 실행할 Tasklet
|
||||
* @return 생성된 Step 객체
|
||||
@@ -151,6 +221,7 @@ public abstract class AbstractBatchTask implements AbstractBatch, ApplicationCon
|
||||
return new StepBuilder(stepName)
|
||||
.repository(jobRepository)
|
||||
.transactionManager(transactionManager)
|
||||
.listener(batchExceptionListener)
|
||||
.tasklet(tasklet)
|
||||
.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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,14 +13,46 @@ import org.springframework.stereotype.Component;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 배치 작업 정보에 대한 BeanPostProcessor입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring의 BeanPostProcessor 인터페이스를 구현하여,
|
||||
* 배치 작업에 대한 메타데이터를 처리하고, 배치 작업이 초기화될 때
|
||||
* 관련 정보를 설정하는 역할을 합니다.</p>
|
||||
*
|
||||
* <p>배치 작업에 대한 @BatchJobInfo 어노테이션을 처리하여,
|
||||
* 배치 작업의 정보를 서비스에 저장합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class BatchJobInfoBeanPostProcessor implements BeanPostProcessor {
|
||||
|
||||
/**
|
||||
* 배치 작업 정보 서비스를 제공하는 서비스 객체입니다.
|
||||
*/
|
||||
private final BatchJobInfoService batchJobInfoService;
|
||||
|
||||
/**
|
||||
* JobRegistry 객체입니다.
|
||||
*
|
||||
* <p>Spring Batch의 JobRegistry를 사용하여 배치 작업을 등록합니다.</p>
|
||||
*/
|
||||
private final JobRegistry jobRegistry;
|
||||
|
||||
/**
|
||||
* 초기화 전에 호출되는 메서드입니다.
|
||||
*
|
||||
* <p>배치 작업 정보 어노테이션을 처리하여 관련 정보를 설정합니다.</p>
|
||||
*
|
||||
* @param bean 초기화할 Bean 객체
|
||||
* @param beanName Bean의 이름
|
||||
* @return 초기화된 Bean 객체
|
||||
* @throws BeansException Bean 초기화 중 발생할 수 있는 예외
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException {
|
||||
@@ -28,6 +60,16 @@ public class BatchJobInfoBeanPostProcessor implements BeanPostProcessor {
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 초기화 후에 호출되는 메서드입니다.
|
||||
*
|
||||
* <p>배치 작업이 등록되지 않은 경우 JobRegistry에 등록합니다.</p>
|
||||
*
|
||||
* @param bean 초기화된 Bean 객체
|
||||
* @param beanName Bean의 이름
|
||||
* @return 초기화된 Bean 객체
|
||||
* @throws BeansException Bean 초기화 중 발생할 수 있는 예외
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException {
|
||||
@@ -46,6 +88,14 @@ public class BatchJobInfoBeanPostProcessor implements BeanPostProcessor {
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 작업 정보 어노테이션을 처리합니다.
|
||||
*
|
||||
* <p>주어진 Bean에서 @BatchJobInfo 어노테이션을 찾아 관련 정보를 설정합니다.</p>
|
||||
*
|
||||
* @param bean 처리할 Bean 객체
|
||||
* @param beanName Bean의 이름
|
||||
*/
|
||||
private void processBatchJobInfoAnnotations(Object bean, String beanName) {
|
||||
if (bean.getClass().isAnnotationPresent(BatchJobInfo.class)) {
|
||||
BatchJobInfo batchJobInfo = bean.getClass().getAnnotation(BatchJobInfo.class);
|
||||
@@ -53,6 +103,14 @@ public class BatchJobInfoBeanPostProcessor implements BeanPostProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 스코프가 지정된 타겟 프리픽스를 제거합니다.
|
||||
*
|
||||
* <p>주어진 빈 이름에서 "scopedTarget." 접두사를 제거하여 반환합니다.</p>
|
||||
*
|
||||
* @param beanName 빈 이름
|
||||
* @return 수정된 빈 이름
|
||||
*/
|
||||
private String removeScopedTargetPrefix(String beanName) {
|
||||
return beanName.startsWith("scopedTarget.") ? beanName.substring("scopedTarget.".length()) : beanName;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,46 @@ package com.spring.infra.batch;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 배치 작업에 대한 정보를 저장하는 데이터 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 배치 작업의 메타데이터를 포함하며,
|
||||
* 배치 작업의 이름, 그룹, 설명 및 cron 표현식 등을 저장합니다.</p>
|
||||
*
|
||||
* <p>배치 작업의 실행 및 관리에 필요한 정보를 제공하는 역할을 합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class BatchJobInfoData {
|
||||
|
||||
/**
|
||||
* 배치 작업의 그룹 이름입니다.
|
||||
*
|
||||
* <p>배치 작업이 속하는 그룹을 식별하는 데 사용됩니다.</p>
|
||||
*/
|
||||
private final String jobGroup;
|
||||
|
||||
/**
|
||||
* 배치 작업의 이름입니다.
|
||||
*
|
||||
* <p>각 배치 작업을 고유하게 식별하는 이름입니다.</p>
|
||||
*/
|
||||
private final String jobName;
|
||||
|
||||
/**
|
||||
* 배치 작업의 cron 표현식입니다.
|
||||
*
|
||||
* <p>배치 작업의 실행 주기를 정의하는 cron 표현식입니다.</p>
|
||||
*/
|
||||
private final String cronExpression;
|
||||
|
||||
/**
|
||||
* 배치 작업에 대한 설명입니다.
|
||||
*
|
||||
* <p>배치 작업의 목적이나 기능에 대한 설명을 포함합니다.</p>
|
||||
*/
|
||||
private final String description;
|
||||
}
|
||||
|
||||
@@ -8,21 +8,68 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 배치 작업 정보를 관리하는 서비스 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 배치 작업의 메타데이터를 저장하고 관리하며,
|
||||
* 배치 작업에 대한 CRUD(Create, Read, Update, Delete) 작업을 수행합니다.</p>
|
||||
*
|
||||
* <p>배치 작업의 정보는 데이터베이스 또는 메모리에 저장될 수 있으며,
|
||||
* 이 서비스를 통해 배치 작업의 상태를 조회하고 업데이트할 수 있습니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BatchJobInfoService {
|
||||
|
||||
|
||||
/**
|
||||
* Spring의 Environment 객체입니다.
|
||||
*
|
||||
* <p>환경 설정에서 플레이스홀더를 해석하는 데 사용됩니다.</p>
|
||||
*/
|
||||
private final Environment environment;
|
||||
|
||||
/**
|
||||
* 배치 작업 정보를 저장하는 맵입니다.
|
||||
*
|
||||
* <p>배치 작업의 이름을 키로 사용하여 BatchJobInfoData 객체를 저장합니다.</p>
|
||||
*/
|
||||
private final Map<String, BatchJobInfoData> batchJobInfoMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 배치 작업 정보를 설정합니다.
|
||||
*
|
||||
* <p>주어진 배치 작업 정보를 맵에 저장합니다.</p>
|
||||
*
|
||||
* @param beanName 배치 작업의 이름
|
||||
* @param batchJobInfo 배치 작업 정보 객체
|
||||
*/
|
||||
public void setBatchJobInfoData(String beanName, BatchJobInfo batchJobInfo) {
|
||||
batchJobInfoMap.put(beanName, processBatchJobInfo(batchJobInfo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 작업 정보를 조회합니다.
|
||||
*
|
||||
* <p>주어진 배치 작업 이름에 해당하는 배치 작업 정보를 반환합니다.</p>
|
||||
*
|
||||
* @param beanName 배치 작업의 이름
|
||||
* @return 해당 배치 작업 정보 객체, 없으면 null
|
||||
*/
|
||||
public BatchJobInfoData getBatchJobInfo(String beanName) {
|
||||
return batchJobInfoMap.get(beanName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 작업 정보를 처리합니다.
|
||||
*
|
||||
* <p>주어진 BatchJobInfo 객체를 기반으로 BatchJobInfoData 객체를 생성합니다.</p>
|
||||
*
|
||||
* @param batchJobInfo 배치 작업 정보 객체
|
||||
* @return 생성된 BatchJobInfoData 객체
|
||||
*/
|
||||
private BatchJobInfoData processBatchJobInfo(BatchJobInfo batchJobInfo) {
|
||||
return new BatchJobInfoData(
|
||||
resolvePlaceHolder(batchJobInfo.group()),
|
||||
@@ -32,8 +79,15 @@ public class BatchJobInfoService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 값을 처리하여 플레이스홀더를 해석합니다.
|
||||
*
|
||||
* <p>환경 설정에서 플레이스홀더를 해석하여 실제 값을 반환합니다.</p>
|
||||
*
|
||||
* @param value 해석할 값
|
||||
* @return 해석된 값
|
||||
*/
|
||||
private String resolvePlaceHolder(String value) {
|
||||
return environment.resolvePlaceholders(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public class PrimaryJpaConfig {
|
||||
|
||||
public static final String TRANSACTION_MANAGER = "primaryTransactionManager";
|
||||
public static final String ENTITY_MANAGER_FACTORY = "primaryEntityManagerFactory";
|
||||
private static final String BASE_PACKAGE = "com.spring.domain";
|
||||
public static final String BASE_PACKAGE = "com.spring.domain";
|
||||
private static final String PERSISTENCE_UNIT = "primaryPersistenceUnit";
|
||||
|
||||
private final JpaProperties jpaProperties;
|
||||
|
||||
@@ -53,7 +53,7 @@ public class SecondaryJpaConfig {
|
||||
|
||||
public static final String TRANSACTION_MANAGER = "secondaryTransactionManager";
|
||||
public static final String ENTITY_MANAGER_FACTORY = "secondaryEntityManagerFactory";
|
||||
private static final String BASE_PACKAGE = "com.spring.domain";
|
||||
public static final String BASE_PACKAGE = "com.spring.domain";
|
||||
private static final String PERSISTENCE_UNIT = "secondaryPersistenceUnit";
|
||||
|
||||
private final JpaProperties jpaProperties;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.spring.infra.db.orm.mybatis;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
@@ -11,7 +14,10 @@ import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
import org.springframework.core.io.support.ResourcePatternUtils;
|
||||
|
||||
import com.spring.infra.db.PrimaryDataSourceConfig;
|
||||
import com.spring.infra.db.orm.mybatis.annotation.PrimaryMapper;
|
||||
@@ -37,10 +43,10 @@ import com.spring.infra.db.orm.mybatis.annotation.PrimaryMapper;
|
||||
)
|
||||
public class PrimaryMybatisConfig {
|
||||
|
||||
private static final String BASE_PACKAGE = "com.spring.domain.*.mapper";
|
||||
public static final String BASE_PACKAGE = "com.spring.domain.*.mapper";
|
||||
public static final String SESSION_FACTORY = "primarySqlSessionFactory";
|
||||
private static final String ALIASES_PACKAGE = "com.spring.domain.*.dto,com.spring.domain.*.entity";
|
||||
private static final String MAPPER_RESOURCES = "classpath:mapper/**/*.xml";
|
||||
private static final String SESSION_FACTORY = "primarySqlSessionFactory";
|
||||
|
||||
/**
|
||||
* 주 데이터베이스용 SqlSessionFactory를 생성합니다.
|
||||
@@ -52,10 +58,11 @@ public class PrimaryMybatisConfig {
|
||||
@Primary
|
||||
@Bean(name = SESSION_FACTORY)
|
||||
SqlSessionFactory sqlSessionFactory(@Qualifier(PrimaryDataSourceConfig.DATASOURCE) DataSource dataSource) throws Exception {
|
||||
var resolver = ResourcePatternUtils.getResourcePatternResolver(new DefaultResourceLoader());
|
||||
var sessionFactory = new SqlSessionFactoryBean();
|
||||
sessionFactory.setConfiguration(mybatisConfiguration());
|
||||
sessionFactory.setDataSource(dataSource);
|
||||
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_RESOURCES));
|
||||
sessionFactory.setMapperLocations(Optional.ofNullable(getResourcesSafely(resolver, MAPPER_RESOURCES)).orElse(new Resource[0]));
|
||||
sessionFactory.setTypeAliasesPackage(ALIASES_PACKAGE);
|
||||
sessionFactory.setVfs(SpringBootVFS.class);
|
||||
return sessionFactory.getObject();
|
||||
@@ -75,4 +82,12 @@ public class PrimaryMybatisConfig {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
private Resource[] getResourcesSafely(ResourcePatternResolver resolver, String pattern) {
|
||||
try {
|
||||
return resolver.getResources(pattern);
|
||||
} catch (IOException e) {
|
||||
return new Resource[0];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.spring.infra.db.orm.mybatis;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
@@ -10,7 +13,10 @@ import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
import org.springframework.core.io.support.ResourcePatternUtils;
|
||||
|
||||
import com.spring.infra.db.SecondaryDataSourceConfig;
|
||||
import com.spring.infra.db.orm.mybatis.annotation.SecondaryMapper;
|
||||
@@ -36,10 +42,10 @@ import com.spring.infra.db.orm.mybatis.annotation.SecondaryMapper;
|
||||
)
|
||||
public class SecondaryMybatisConfig {
|
||||
|
||||
private static final String BASE_PACKAGE = "com.spring.domain.*.mapper";
|
||||
public static final String BASE_PACKAGE = "com.spring.domain.*.mapper";
|
||||
public static final String SESSION_FACTORY = "secondarySqlSessionFactory";
|
||||
private static final String ALIASES_PACKAGE = "com.spring.domain.*.dto,com.spring.domain.*.entity";
|
||||
private static final String MAPPER_RESOURCES = "classpath:mapper/**/*.xml";
|
||||
private static final String SESSION_FACTORY = "secondarySqlSessionFactory";
|
||||
|
||||
/**
|
||||
* 보조 데이터베이스용 SqlSessionFactory를 생성합니다.
|
||||
@@ -50,10 +56,11 @@ public class SecondaryMybatisConfig {
|
||||
*/
|
||||
@Bean(name = SESSION_FACTORY)
|
||||
SqlSessionFactory sqlSessionFactory(@Qualifier(SecondaryDataSourceConfig.DATASOURCE) DataSource dataSource) throws Exception {
|
||||
var resolver = ResourcePatternUtils.getResourcePatternResolver(new DefaultResourceLoader());
|
||||
var sessionFactory = new SqlSessionFactoryBean();
|
||||
sessionFactory.setConfiguration(mybatisConfiguration());
|
||||
sessionFactory.setDataSource(dataSource);
|
||||
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_RESOURCES));
|
||||
sessionFactory.setMapperLocations(Optional.ofNullable(getResourcesSafely(resolver, MAPPER_RESOURCES)).orElse(new Resource[0]));
|
||||
sessionFactory.setTypeAliasesPackage(ALIASES_PACKAGE);
|
||||
sessionFactory.setVfs(SpringBootVFS.class);
|
||||
return sessionFactory.getObject();
|
||||
@@ -73,4 +80,12 @@ public class SecondaryMybatisConfig {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
private Resource[] getResourcesSafely(ResourcePatternResolver resolver, String pattern) {
|
||||
try {
|
||||
return resolver.getResources(pattern);
|
||||
} catch (IOException e) {
|
||||
return new Resource[0];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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(), request.httpMethod() == Request.HttpMethod.GET ? null : 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;
|
||||
}
|
||||
@@ -31,8 +31,25 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequiredArgsConstructor
|
||||
public class QuartzConfig {
|
||||
|
||||
/**
|
||||
* Quartz 스케줄러의 속성을 설정하는 객체입니다.
|
||||
*
|
||||
* <p>Quartz의 다양한 설정을 포함하고 있으며, JobFactory 및 SchedulerFactoryBean에 사용됩니다.</p>
|
||||
*/
|
||||
private final QuartzProperties quartzProperties;
|
||||
|
||||
/**
|
||||
* 데이터 소스 객체입니다.
|
||||
*
|
||||
* <p>Quartz 스케줄러가 사용할 데이터베이스 연결을 제공합니다.</p>
|
||||
*/
|
||||
private final DataSource dataSource;
|
||||
|
||||
/**
|
||||
* 트랜잭션 매니저 객체입니다.
|
||||
*
|
||||
* <p>Quartz 작업의 트랜잭션 관리를 담당합니다.</p>
|
||||
*/
|
||||
private final PlatformTransactionManager transactionManager;
|
||||
|
||||
/**
|
||||
@@ -77,7 +94,7 @@ public class QuartzConfig {
|
||||
factory.setDataSource(dataSource);
|
||||
factory.setTransactionManager(transactionManager);
|
||||
factory.setJobFactory(jobFactory);
|
||||
factory.setAutoStartup(true);
|
||||
factory.setAutoStartup(false);
|
||||
factory.setWaitForJobsToCompleteOnShutdown(true);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@@ -36,10 +36,32 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@RequiredArgsConstructor
|
||||
public class QuartzJobLauncher extends QuartzJobBean {
|
||||
|
||||
/**
|
||||
* JobParameters의 인스턴스 키입니다.
|
||||
*
|
||||
* <p>Job 실행 시 사용되는 인스턴스 ID를 저장합니다.</p>
|
||||
*/
|
||||
private static final String JOB_PARAMETERS_INSTANCE_KEY = "InstanceId";
|
||||
|
||||
/**
|
||||
* JobParameters의 타임스탬프 키입니다.
|
||||
*
|
||||
* <p>Job 실행 시 현재 시간을 저장합니다.</p>
|
||||
*/
|
||||
private static final String JOB_PARAMETERS_TIMESTAMP_KEY = "timestamp";
|
||||
|
||||
|
||||
/**
|
||||
* JobLauncher 객체입니다.
|
||||
*
|
||||
* <p>Spring Batch Job을 실행하는 데 사용됩니다.</p>
|
||||
*/
|
||||
private final JobLauncher jobLauncher;
|
||||
|
||||
/**
|
||||
* JobRegistry 객체입니다.
|
||||
*
|
||||
* <p>Spring Batch에서 등록된 Job을 조회하는 데 사용됩니다.</p>
|
||||
*/
|
||||
private final JobRegistry jobRegistry;
|
||||
|
||||
/**
|
||||
@@ -53,6 +75,7 @@ public class QuartzJobLauncher extends QuartzJobBean {
|
||||
* </ol>
|
||||
*
|
||||
* @param context Quartz JobExecutionContext 객체
|
||||
* @throws JobExecutionException Job 실행 중 발생할 수 있는 예외
|
||||
*/
|
||||
@Override
|
||||
protected void executeInternal(@NonNull JobExecutionContext context) throws JobExecutionException {
|
||||
@@ -68,7 +91,7 @@ public class QuartzJobLauncher extends QuartzJobBean {
|
||||
jobLauncher.run(job, params);
|
||||
} catch (Exception e) {
|
||||
log.error("Job execution exception! - {}", e.getMessage(), e);
|
||||
throw new JobExecutionException();
|
||||
throw new JobExecutionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ package com.spring.infra.quartz;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -18,44 +18,67 @@ import lombok.RequiredArgsConstructor;
|
||||
/**
|
||||
* Quartz 작업 등록기 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring의 ApplicationContext 내에서 Quartz 작업을 등록하고 관리하는 역할을 합니다.</p>
|
||||
*
|
||||
* <p>주요 기능:</p>
|
||||
* <ul>
|
||||
* <li>애플리케이션 컨텍스트 내의 모든 빈을 검사</li>
|
||||
* <li>QuartzJob 어노테이션이 붙은 메소드 식별</li>
|
||||
* <li>AbstractBatchJob을 상속받은 클래스 식별</li>
|
||||
* <li>식별된 메소드를 Quartz 작업으로 등록</li>
|
||||
* <li>각 작업에 대한 JobDetail 및 Trigger 생성</li>
|
||||
* <li>애플리케이션 컨텍스트 내의 모든 빈을 검사하여 QuartzJob 어노테이션이 붙은 메소드를 식별합니다.</li>
|
||||
* <li>AbstractBatchJob을 상속받은 클래스를 식별하고, 해당 메소드를 Quartz 작업으로 등록합니다.</li>
|
||||
* <li>각 작업에 대한 JobDetail 및 Trigger를 생성합니다.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>이 클래스는 {@link ApplicationListener}를 구현하여
|
||||
* {@link ContextRefreshedEvent}가 발생할 때 자동으로 실행됩니다.</p>
|
||||
* <p>이 클래스는 {@link ApplicationListener}를 구현하여 {@link ApplicationReadyEvent}가 발생할 때 자동으로 실행됩니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class QuartzJobRegistrar implements ApplicationListener<ContextRefreshedEvent> {
|
||||
public class QuartzJobRegistrar implements ApplicationListener<ApplicationReadyEvent> {
|
||||
|
||||
/**
|
||||
* 애플리케이션 컨텍스트 객체입니다.
|
||||
*
|
||||
* <p>Spring의 ApplicationContext를 통해 필요한 빈을 가져옵니다.</p>
|
||||
*/
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
/**
|
||||
* Quartz 스케줄 서비스 객체입니다.
|
||||
*
|
||||
* <p>Quartz 작업을 등록하고 관리하는 데 사용됩니다.</p>
|
||||
*/
|
||||
private final QuartzScheduleService quartzScheduleService;
|
||||
|
||||
/**
|
||||
* 애플리케이션 컨텍스트가 리프레시될 때 호출되는 메소드입니다.
|
||||
* QuartzJob 어노테이션이 붙은 메소드와 AbstractBatchJob을 상속받은 클래스를 등록합니다.
|
||||
*
|
||||
*
|
||||
* <p>QuartzJob 어노테이션이 붙은 메소드와 AbstractBatchJob을 상속받은 클래스를 등록합니다.</p>
|
||||
*
|
||||
* @param event ContextRefreshedEvent 객체
|
||||
*/
|
||||
@Override
|
||||
public void onApplicationEvent(@NonNull ContextRefreshedEvent event) {
|
||||
public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
|
||||
refreshJobs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 리프레시된 스코프에서 Quartz 작업을 등록합니다.
|
||||
*
|
||||
* <p>리프레시된 스코프에서 Quartz 작업을 등록하기 위해 호출됩니다.</p>
|
||||
*
|
||||
* @param event RefreshScopeRefreshedEvent 객체
|
||||
*/
|
||||
@EventListener
|
||||
public void onRefreshScopeRefreshed(@NonNull RefreshScopeRefreshedEvent event) {
|
||||
refreshJobs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Quartz 작업을 새로 고칩니다.
|
||||
*
|
||||
* <p>기존의 Quartz 작업을 지우고 새로운 작업을 등록합니다.</p>
|
||||
*/
|
||||
private void refreshJobs() {
|
||||
quartzScheduleService.clearJobs();
|
||||
registerBatchJobs();
|
||||
@@ -63,6 +86,9 @@ public class QuartzJobRegistrar implements ApplicationListener<ContextRefreshedE
|
||||
|
||||
/**
|
||||
* AbstractBatchJob을 상속받은 모든 클래스를 찾아 Quartz 스케줄러에 등록합니다.
|
||||
*
|
||||
* <p>애플리케이션 컨텍스트에서 AbstractBatch 타입의 빈을 검색하고,
|
||||
* AOP 프록시가 아닌 경우 Quartz 작업을 생성합니다.</p>
|
||||
*/
|
||||
private void registerBatchJobs() {
|
||||
Map<String, AbstractBatch> batchJobs = applicationContext.getBeansOfType(AbstractBatch.class);
|
||||
@@ -74,6 +100,14 @@ public class QuartzJobRegistrar implements ApplicationListener<ContextRefreshedE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 스코프가 지정된 타겟 프리픽스를 제거합니다.
|
||||
*
|
||||
* <p>주어진 빈 이름에서 "scopedTarget." 접두사를 제거하여 반환합니다.</p>
|
||||
*
|
||||
* @param beanName 빈 이름
|
||||
* @return 수정된 빈 이름
|
||||
*/
|
||||
private String removeScopedTargetPrefix(String beanName) {
|
||||
return beanName.startsWith("scopedTarget.") ? beanName.substring("scopedTarget.".length()) : beanName;
|
||||
}
|
||||
|
||||
@@ -15,21 +15,56 @@ import com.spring.infra.batch.BatchJobInfoService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Quartz 스케줄러를 관리하는 서비스 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Quartz 스케줄러의 작업을 생성, 삭제 및 관리하는 기능을 제공합니다.</p>
|
||||
*
|
||||
* <p>주요 기능:</p>
|
||||
* <ul>
|
||||
* <li>작업 및 트리거 생성</li>
|
||||
* <li>작업 삭제 및 스케줄러 초기화</li>
|
||||
* <li>작업 상태 관리</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class QuartzScheduleService {
|
||||
|
||||
/**
|
||||
* Quartz 스케줄러 객체입니다.
|
||||
*
|
||||
* <p>스케줄러의 작업을 관리하는 데 사용됩니다.</p>
|
||||
*/
|
||||
private final Scheduler scheduler;
|
||||
private final BatchJobInfoService batchJobInfoService;
|
||||
|
||||
/**
|
||||
* 모든 작업을 삭제합니다.
|
||||
*
|
||||
* <p>스케줄러에 등록된 모든 작업을 삭제하고 초기화합니다.</p>
|
||||
*
|
||||
* @throws IllegalStateException 작업 삭제 중 오류가 발생한 경우
|
||||
*/
|
||||
public void clearJobs() {
|
||||
try {
|
||||
scheduler.clear();
|
||||
} catch (SchedulerException e) {
|
||||
throw new IllegalStateException();
|
||||
throw new IllegalStateException("Failed to clear jobs from scheduler", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 새로운 작업 트리거를 생성합니다.
|
||||
*
|
||||
* <p>주어진 배치 작업 이름을 기반으로 작업과 트리거를 생성하고 스케줄러에 등록합니다.</p>
|
||||
*
|
||||
* @param beanName 배치 작업의 이름
|
||||
* @throws IllegalStateException 작업 스케줄링 중 오류가 발생한 경우
|
||||
*/
|
||||
public void createJobTrigger(String beanName) {
|
||||
BatchJobInfoData batchJobInfoData = batchJobInfoService.getBatchJobInfo(beanName);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,14 @@ package com.spring.infra.security.config;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 애플리케이션에서 허용된 URI를 정의하는 열거형입니다.
|
||||
*
|
||||
* <p>이 열거형은 보안 설정에서 접근이 허용된 URI 경로를 관리합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum PermittedURI {
|
||||
|
||||
@@ -52,11 +52,15 @@ public class SecurityConfig {
|
||||
/**
|
||||
* Spring Security의 필터 체인을 구성합니다.
|
||||
*
|
||||
* @param http HttpSecurity 객체
|
||||
* @param tokenService JWT 토큰 서비스
|
||||
* @param authenticationEntryPoint JWT 인증 진입점
|
||||
* @param accessDeniedHandler JWT 접근 거부 핸들러
|
||||
* @return 구성된 SecurityFilterChain
|
||||
* <p>이 메서드는 HTTP 보안 설정을 정의하고, JWT 인증 및 접근 제어를 설정합니다.</p>
|
||||
*
|
||||
* @param http HttpSecurity 객체, 보안 설정을 구성하는 데 사용됩니다.
|
||||
* @param tokenService JWT 토큰 서비스, 인증 및 토큰 관리를 담당합니다.
|
||||
* @param authenticationEntryPoint JWT 인증 진입점, 인증 실패 시 처리합니다.
|
||||
* @param accessDeniedHandler JWT 접근 거부 핸들러, 접근 권한이 없는 경우 처리합니다.
|
||||
* @param authenticationProcessingFilter 사용자 정의 인증 필터, 인증 프로세스를 처리합니다.
|
||||
* @return 구성된 SecurityFilterChain, 보안 필터 체인을 반환합니다.
|
||||
* @throws Exception 보안 설정 중 발생할 수 있는 예외를 처리합니다.
|
||||
*/
|
||||
@Bean
|
||||
@Order(1)
|
||||
@@ -107,7 +111,11 @@ public class SecurityConfig {
|
||||
/**
|
||||
* 특정 요청에 대해 보안 검사를 무시하도록 설정합니다.
|
||||
*
|
||||
* @return 구성된 SecurityFilterChain
|
||||
* <p>이 메서드는 정적 리소스에 대한 접근을 허용하고, 보안 검사를 비활성화합니다.</p>
|
||||
*
|
||||
* @param http HttpSecurity 객체, 보안 설정을 구성하는 데 사용됩니다.
|
||||
* @return 구성된 SecurityFilterChain, 보안 필터 체인을 반환합니다.
|
||||
* @throws Exception 보안 설정 중 발생할 수 있는 예외를 처리합니다.
|
||||
*/
|
||||
@Bean
|
||||
@Order(0)
|
||||
@@ -124,18 +132,39 @@ public class SecurityConfig {
|
||||
/**
|
||||
* 비밀번호 인코더를 구성합니다.
|
||||
*
|
||||
* @return PasswordEncoder 객체
|
||||
* <p>이 메서드는 비밀번호 인코딩 및 검증을 위한 PasswordEncoder 객체를 반환합니다.</p>
|
||||
*
|
||||
* @return PasswordEncoder 객체, 비밀번호 인코딩 및 검증을 담당합니다.
|
||||
*/
|
||||
@Bean
|
||||
PasswordEncoder passwordEncoder() {
|
||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 인증 관리자를 구성합니다.
|
||||
*
|
||||
* <p>이 메서드는 사용자 인증을 처리하는 AuthenticationManager 객체를 반환합니다.</p>
|
||||
*
|
||||
* @param provider 사용자 인증 제공자, 인증 프로세스를 처리합니다.
|
||||
* @return AuthenticationManager 객체, 인증 요청을 처리합니다.
|
||||
*/
|
||||
@Bean
|
||||
AuthenticationManager authenticationManager(UserAuthenticationProvider provider) {
|
||||
return new ProviderManager(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 정의 인증 필터를 구성합니다.
|
||||
*
|
||||
* <p>이 메서드는 인증 요청을 처리하는 AuthenticationProcessingFilter 객체를 반환합니다.</p>
|
||||
*
|
||||
* @param objectMapper JSON 변환을 위한 ObjectMapper 객체입니다.
|
||||
* @param authenticationManager 인증 요청을 처리하는 AuthenticationManager 객체입니다.
|
||||
* @param signinSuccessHandler 인증 성공 시 처리하는 핸들러입니다.
|
||||
* @param signinFailureHandler 인증 실패 시 처리하는 핸들러입니다.
|
||||
* @return AuthenticationProcessingFilter 객체, 인증 요청을 처리합니다.
|
||||
*/
|
||||
@Bean
|
||||
AuthenticationProcessingFilter authenticationProcessingFilter(
|
||||
ObjectMapper objectMapper,
|
||||
|
||||
@@ -3,6 +3,14 @@ package com.spring.infra.security.config;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 애플리케이션에서 사용되는 보안 관련 URI를 정의하는 열거형입니다.
|
||||
*
|
||||
* <p>이 열거형은 보안 설정에서 사용되는 URI 경로를 관리하며, 각 URI는 특정 기능이나 페이지에 대한 접근을 나타냅니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum SecurityURI {
|
||||
|
||||
@@ -14,20 +14,54 @@ import com.spring.domain.user.entity.AgentUserRole;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 사용자 정보를 나타내는 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring Security의 UserDetails 인터페이스를 구현하여,
|
||||
* 인증된 사용자의 정보를 제공하는 역할을 합니다.</p>
|
||||
*
|
||||
* <p>사용자 정보는 AgentUser 객체를 통해 관리되며, 사용자 권한 및 상태 정보를 포함합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class UserPrincipal implements UserDetails {
|
||||
|
||||
/**
|
||||
* 인증된 사용자 정보를 담고 있는 AgentUser 객체입니다.
|
||||
*/
|
||||
private final transient AgentUser agentUser;
|
||||
|
||||
/**
|
||||
* AgentUser 객체를 기반으로 UserPrincipal 객체를 생성합니다.
|
||||
*
|
||||
* @param agentUser 인증된 사용자 정보가 담긴 AgentUser 객체
|
||||
* @return 생성된 UserPrincipal 객체
|
||||
*/
|
||||
public static UserPrincipal valueOf(AgentUser agentUser) {
|
||||
return new UserPrincipal(agentUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 ID와 사용자 이름을 기반으로 UserPrincipal 객체를 생성합니다.
|
||||
*
|
||||
* @param userId 사용자 ID
|
||||
* @param userName 사용자 이름
|
||||
* @return 생성된 UserPrincipal 객체
|
||||
*/
|
||||
public static UserPrincipal of(String userId, String userName) {
|
||||
return new UserPrincipal(AgentUser.builder().userId(userId).name(userName).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 권한을 반환합니다.
|
||||
*
|
||||
* <p>사용자의 역할에 따라 GrantedAuthority 객체의 컬렉션을 반환합니다.</p>
|
||||
*
|
||||
* @return 사용자의 권한 목록
|
||||
*/
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return Arrays.stream(AgentUserRole.values())
|
||||
@@ -37,40 +71,80 @@ public class UserPrincipal implements UserDetails {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정이 만료되지 않았는지 여부를 반환합니다.
|
||||
*
|
||||
* @return 계정이 만료되지 않았으면 true, 그렇지 않으면 false
|
||||
*/
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return true;
|
||||
}
|
||||
public boolean isAccountNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 계정이 잠겨 있지 않았는지 여부를 반환합니다.
|
||||
*
|
||||
* @return 계정이 잠겨 있지 않으면 true, 그렇지 않으면 false
|
||||
*/
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 자격 증명이 만료되지 않았는지 여부를 반환합니다.
|
||||
*
|
||||
* @return 자격 증명이 만료되지 않았으면 true, 그렇지 않으면 false
|
||||
*/
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 계정이 활성화되었는지 여부를 반환합니다.
|
||||
*
|
||||
* @return 계정이 활성화되었으면 true, 그렇지 않으면 false
|
||||
*/
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 비밀번호를 반환합니다.
|
||||
*
|
||||
* @return 사용자의 비밀번호
|
||||
*/
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return agentUser.getPassword();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 사용자 ID를 반환합니다.
|
||||
*
|
||||
* @return 사용자의 사용자 ID
|
||||
*/
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return agentUser.getUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 고유 키를 반환합니다.
|
||||
*
|
||||
* @return 사용자의 고유 키
|
||||
*/
|
||||
public String getKey() {
|
||||
return agentUser.getId().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 이름을 반환합니다.
|
||||
*
|
||||
* @return 사용자의 이름
|
||||
*/
|
||||
public String getMemberName() {
|
||||
return agentUser.getName();
|
||||
}
|
||||
|
||||
@@ -17,37 +17,107 @@ import io.jsonwebtoken.MalformedJwtException;
|
||||
import io.jsonwebtoken.security.SignatureException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 보안 관련 예외를 처리하는 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring의 @RestControllerAdvice를 사용하여
|
||||
* 보안 관련 예외를 전역적으로 처리합니다.</p>
|
||||
*
|
||||
* <p>각 예외에 대해 적절한 응답을 생성하여 클라이언트에 반환합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityExceptionHandler {
|
||||
|
||||
/**
|
||||
* 접근 거부 핸들러.
|
||||
*
|
||||
* <p>AccessDeniedException이 발생했을 때 호출되며,
|
||||
* 사용자에게 접근 거부 응답을 반환합니다.</p>
|
||||
*/
|
||||
private final AccessDeniedHandler accessDeniedHandler;
|
||||
|
||||
/**
|
||||
* 사용자 인증 예외를 처리합니다.
|
||||
*
|
||||
* <p>SecurityAuthException이 발생했을 때 호출되며,
|
||||
* 해당 예외에 대한 응답을 생성합니다.</p>
|
||||
*
|
||||
* @param e 발생한 SecurityAuthException
|
||||
* @return SecurityErrorResponse 인증 오류에 대한 응답
|
||||
*/
|
||||
@ExceptionHandler(SecurityAuthException.class)
|
||||
public SecurityErrorResponse handleAuthenticationException(SecurityAuthException e) {
|
||||
return SecurityErrorResponse.valueOf(e.getExceptionRule());
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증 예외를 처리합니다.
|
||||
*
|
||||
* <p>AuthenticationException이 발생했을 때 호출되며,
|
||||
* 사용자 인증이 실패했음을 나타내는 응답을 생성합니다.</p>
|
||||
*
|
||||
* @return SecurityErrorResponse 사용자 인증 실패에 대한 응답
|
||||
*/
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
public SecurityErrorResponse handleAuthenticationException() {
|
||||
return SecurityErrorResponse.valueOf(SecurityExceptionRule.USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 서명 예외를 처리합니다.
|
||||
*
|
||||
* <p>SignatureException이 발생했을 때 호출되며,
|
||||
* 서명 오류에 대한 응답을 생성합니다.</p>
|
||||
*
|
||||
* @return SecurityErrorResponse 서명 오류에 대한 응답
|
||||
*/
|
||||
@ExceptionHandler(SignatureException.class)
|
||||
public SecurityErrorResponse handleSignatureException() {
|
||||
return SecurityErrorResponse.valueOf(SecurityExceptionRule.SIGNATURE_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 잘못된 JWT 예외를 처리합니다.
|
||||
*
|
||||
* <p>MalformedJwtException이 발생했을 때 호출되며,
|
||||
* 잘못된 JWT에 대한 응답을 생성합니다.</p>
|
||||
*
|
||||
* @return SecurityErrorResponse 잘못된 JWT 오류에 대한 응답
|
||||
*/
|
||||
@ExceptionHandler(MalformedJwtException.class)
|
||||
public SecurityErrorResponse handleMalformedJwtException() {
|
||||
return SecurityErrorResponse.valueOf(SecurityExceptionRule.MALFORMED_JWT_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 만료된 JWT 예외를 처리합니다.
|
||||
*
|
||||
* <p>ExpiredJwtException이 발생했을 때 호출되며,
|
||||
* 만료된 JWT에 대한 응답을 생성합니다.</p>
|
||||
*
|
||||
* @return SecurityErrorResponse 만료된 JWT 오류에 대한 응답
|
||||
*/
|
||||
@ExceptionHandler(ExpiredJwtException.class)
|
||||
public SecurityErrorResponse handleExpiredJwtException() {
|
||||
return SecurityErrorResponse.valueOf(SecurityExceptionRule.EXPIRED_JWT_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 접근 거부 예외를 처리합니다.
|
||||
*
|
||||
* <p>AccessDeniedException이 발생했을 때 호출되며,
|
||||
* 접근 거부 핸들러를 사용하여 적절한 응답을 반환합니다.</p>
|
||||
*
|
||||
* @param ex 발생한 AccessDeniedException
|
||||
* @param request HTTP 요청 객체
|
||||
* @param response HTTP 응답 객체
|
||||
* @throws IOException 입출력 예외
|
||||
* @throws ServletException 서블릿 예외
|
||||
*/
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public void handleAccessDeniedException(
|
||||
AccessDeniedException ex, HttpServletRequest request, HttpServletResponse response
|
||||
|
||||
@@ -25,16 +25,6 @@ public enum SecurityExceptionRule implements ErrorRule {
|
||||
EXPIRED_JWT_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다. 다시 로그인해주세요.");
|
||||
|
||||
private final HttpStatus status;
|
||||
private String message;
|
||||
|
||||
SecurityExceptionRule(HttpStatus status, String message) {
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public SecurityExceptionRule message(final String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
private final String message;
|
||||
|
||||
}
|
||||
|
||||
@@ -23,19 +23,64 @@ import com.spring.infra.security.dto.SignInRequest;
|
||||
import com.spring.infra.security.error.SecurityAuthException;
|
||||
import com.spring.infra.security.error.SecurityExceptionRule;
|
||||
|
||||
/**
|
||||
* 사용자 인증 요청을 처리하는 필터 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring Security의 AbstractAuthenticationProcessingFilter를 확장하여,
|
||||
* 사용자 로그인 요청을 처리하고 인증을 수행합니다.</p>
|
||||
*
|
||||
* <p>HTTP POST 요청을 통해 사용자 이름과 비밀번호를 받아 인증을 시도하며,
|
||||
* 인증 결과에 따라 적절한 응답을 반환합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
public class AuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
|
||||
|
||||
/**
|
||||
* 기본 로그인 요청 URI.
|
||||
*/
|
||||
private static final String DEFAULT_LOGIN_REQUEST_URI = PermittedURI.USER_SIGN_IN.getUri();
|
||||
private static final String HTTP_METHOD = "POST";
|
||||
private static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URI, HTTP_METHOD);
|
||||
|
||||
/**
|
||||
* HTTP 메서드 (POST).
|
||||
*/
|
||||
private static final String HTTP_METHOD = "POST";
|
||||
|
||||
/**
|
||||
* 기본 로그인 경로 요청 매처.
|
||||
*/
|
||||
private static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER =
|
||||
new AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URI, HTTP_METHOD);
|
||||
|
||||
/**
|
||||
* JSON 변환을 위한 ObjectMapper 객체.
|
||||
*/
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* AuthenticationProcessingFilter 생성자.
|
||||
*
|
||||
* @param objectMapper JSON 변환을 위한 ObjectMapper 객체
|
||||
*/
|
||||
public AuthenticationProcessingFilter(ObjectMapper objectMapper) {
|
||||
super(DEFAULT_LOGIN_PATH_REQUEST_MATCHER);
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 인증을 시도합니다.
|
||||
*
|
||||
* <p>HTTP 요청에서 사용자 이름과 비밀번호를 추출하고,
|
||||
* 인증 매니저를 사용하여 인증을 수행합니다.</p>
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @param response HTTP 응답 객체
|
||||
* @return 인증된 Authentication 객체
|
||||
* @throws AuthenticationException 인증 실패 시 발생하는 예외
|
||||
* @throws IOException 입출력 예외
|
||||
* @throws ServletException 서블릿 예외
|
||||
*/
|
||||
@Override
|
||||
public Authentication attemptAuthentication(
|
||||
HttpServletRequest request,
|
||||
@@ -49,7 +94,7 @@ public class AuthenticationProcessingFilter extends AbstractAuthenticationProces
|
||||
if (!isValidRequest(signInRequest)) {
|
||||
throw new SecurityAuthException(SecurityExceptionRule.USER_BAD_REQUEST);
|
||||
}
|
||||
var token = new UsernamePasswordAuthenticationToken(signInRequest.getUsername(), signInRequest.getPassword());
|
||||
var token = new UsernamePasswordAuthenticationToken(signInRequest.getUsername(), signInRequest.getPassword());
|
||||
var authentication = this.getAuthenticationManager().authenticate(token);
|
||||
|
||||
if (authentication.getPrincipal() instanceof UserPrincipal) {
|
||||
@@ -62,11 +107,27 @@ public class AuthenticationProcessingFilter extends AbstractAuthenticationProces
|
||||
return authentication;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청의 유효성 유형을 검사합니다.
|
||||
*
|
||||
* <p>요청 메서드가 POST이고, 콘텐츠 타입이 JSON인지 확인합니다.</p>
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @return 요청이 유효하면 true, 그렇지 않으면 false
|
||||
*/
|
||||
private boolean isValidRequestType(HttpServletRequest request) {
|
||||
return Objects.equals(request.getMethod(), HttpMethod.POST.name()) &&
|
||||
Objects.equals(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 로그인 요청의 유효성을 검사합니다.
|
||||
*
|
||||
* <p>사용자 이름과 비밀번호가 모두 존재하는지 확인합니다.</p>
|
||||
*
|
||||
* @param signInRequest 로그인 요청 객체
|
||||
* @return 요청이 유효하면 true, 그렇지 않으면 false
|
||||
*/
|
||||
private boolean isValidRequest(SignInRequest signInRequest) {
|
||||
return StringUtils.hasText(signInRequest.getUsername()) ||
|
||||
StringUtils.hasText(signInRequest.getPassword());
|
||||
|
||||
@@ -26,13 +26,23 @@ import lombok.RequiredArgsConstructor;
|
||||
*
|
||||
* <p>이 필터는 요청마다 JWT 토큰을 검증하고, 필요한 경우 토큰을 갱신합니다.</p>
|
||||
*
|
||||
* <p>인증된 사용자의 요청을 처리하기 위해 JWT 토큰을 사용하며,
|
||||
* 유효한 토큰이 없거나 만료된 경우에는 리프레시 토큰을 사용하여 새로운 액세스 토큰을 발급합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public final class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
/**
|
||||
* JWT 토큰 서비스를 제공하는 서비스 객체입니다.
|
||||
*/
|
||||
private final JwtTokenService jwtTokenService;
|
||||
|
||||
/**
|
||||
* 예외 속성 이름.
|
||||
*/
|
||||
private static final String EXCEPTION_ATTRIBUTE = "exception";
|
||||
|
||||
/**
|
||||
@@ -43,6 +53,8 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
* @param request HTTP 요청 객체
|
||||
* @param response HTTP 응답 객체
|
||||
* @param filterChain 필터 체인
|
||||
* @throws ServletException 서블릿 예외
|
||||
* @throws IOException 입출력 예외
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
@@ -51,49 +63,71 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
@NonNull FilterChain filterChain
|
||||
) throws ServletException, IOException {
|
||||
|
||||
// 허용된 URI에 대한 요청은 필터 체인을 계속 진행합니다.
|
||||
if (isPermittedURI(request.getRequestURI())) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 요청에서 액세스 토큰을 파싱합니다.
|
||||
String accessToken = parseBearerToken(request, HttpHeaders.AUTHORIZATION);
|
||||
if (jwtTokenService.validateAccessToken(accessToken)) {
|
||||
// 유효한 액세스 토큰인 경우 인증 정보를 설정합니다.
|
||||
setAuthenticationToContext(accessToken);
|
||||
} else {
|
||||
// 액세스 토큰이 유효하지 않은 경우 리프레시 토큰을 사용하여 새로운 액세스 토큰을 발급합니다.
|
||||
String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX);
|
||||
jwtTokenService.validateToken(refreshToken);
|
||||
jwtTokenService.validateRefreshToken(refreshToken);
|
||||
String reissuedAccessToken = jwtTokenService.getRefreshToken(refreshToken);
|
||||
Authentication authentication = jwtTokenService.getAuthentication(reissuedAccessToken);
|
||||
jwtTokenService.saveRefreshToken(authentication.getName(), jwtTokenService.generateRefreshToken(response, authentication));
|
||||
setAuthenticationToContext(jwtTokenService.generateAccessToken(response, authentication));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 예외 발생 시 쿠키를 삭제하고 예외 정보를 요청 속성에 설정합니다.
|
||||
jwtTokenService.deleteCookie(response);
|
||||
request.setAttribute(EXCEPTION_ATTRIBUTE, e);
|
||||
}
|
||||
|
||||
// 필터 체인을 계속 진행합니다.
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증 정보를 SecurityContext에 설정합니다.
|
||||
*
|
||||
* @param token 토큰
|
||||
* @param token JWT 토큰
|
||||
*/
|
||||
private void setAuthenticationToContext(final String token) {
|
||||
Authentication authentication = jwtTokenService.getAccessAuthentication(token);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bearer 토큰을 파싱하여 반환합니다.
|
||||
*
|
||||
* <p>HTTP 요청 헤더에서 Bearer 토큰을 추출하고, 쿠키에서 액세스 토큰을 가져옵니다.</p>
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @param headerName 헤더 이름
|
||||
* @return 파싱된 Bearer 토큰
|
||||
*/
|
||||
private String parseBearerToken(final HttpServletRequest request, final String headerName) {
|
||||
return Optional.ofNullable(request.getHeader(headerName))
|
||||
.filter(token -> token.substring(0, 7).equalsIgnoreCase(JwtTokenRule.BEARER_PREFIX.getValue()))
|
||||
.map(token -> token.substring(7))
|
||||
.orElse(jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.ACCESS_PREFIX));
|
||||
.orElseGet(() -> jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.ACCESS_PREFIX));
|
||||
}
|
||||
|
||||
/**
|
||||
* 허용된 URI인지 확인합니다.
|
||||
*
|
||||
* <p>요청 URI가 허용된 URI 목록에 포함되어 있는지 확인합니다.</p>
|
||||
*
|
||||
* @param requestURI 요청 URI
|
||||
* @return 허용된 URI이면 true, 그렇지 않으면 false
|
||||
*/
|
||||
private boolean isPermittedURI(String requestURI) {
|
||||
return Arrays.stream(PermittedURI.values())
|
||||
.map(PermittedURI::getUri)
|
||||
|
||||
@@ -15,21 +15,50 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import com.spring.infra.security.config.PermittedURI;
|
||||
import com.spring.infra.security.config.SecurityURI;
|
||||
|
||||
/**
|
||||
* 사용자가 인증된 경우 특정 URI로 리다이렉트하는 필터 클래스입니다.
|
||||
*
|
||||
* <p>이 필터는 사용자가 인증된 상태에서 루트 URI에 접근할 경우,
|
||||
* 지정된 리다이렉트 URI로 이동하도록 처리합니다.</p>
|
||||
*
|
||||
* <p>인증된 사용자가 루트 URI에 접근하면, 사용자는 대시보드와 같은
|
||||
* 다른 페이지로 리다이렉트됩니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
public class RedirectIfAuthenticatedFilter extends OncePerRequestFilter {
|
||||
|
||||
|
||||
/**
|
||||
* 필터의 내부 로직을 구현하는 메서드입니다.
|
||||
*
|
||||
* <p>HTTP 요청을 검사하여 사용자가 인증된 경우 리다이렉트를 수행합니다.</p>
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @param response HTTP 응답 객체
|
||||
* @param filterChain 필터 체인
|
||||
* @throws ServletException 서블릿 예외
|
||||
* @throws IOException 입출력 예외
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain
|
||||
) throws ServletException, IOException {
|
||||
|
||||
String requestURI = request.getRequestURI();
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
// 사용자가 인증된 상태이고 요청 URI가 루트 URI인 경우 리다이렉트 수행
|
||||
if (auth != null && auth.isAuthenticated() && PermittedURI.ROOT_URI.getUri().equals(requestURI)) {
|
||||
response.sendRedirect(SecurityURI.REDIRECT_URI.getUri());
|
||||
return;
|
||||
}
|
||||
|
||||
// 필터 체인을 계속 진행
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,27 +11,55 @@ import org.springframework.web.util.ServletRequestPathUtils;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* HTTP 요청의 엔드포인트 존재 여부를 확인하는 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 주어진 HTTP 요청이 유효한 엔드포인트에 해당하는지 검사합니다.
|
||||
* Spring의 RequestMappingHandlerMapping을 사용하여 요청에 대한 핸들러를 조회합니다.</p>
|
||||
*
|
||||
* <p>요청 경로가 파싱되지 않은 경우, 이를 파싱하고 캐시하여 이후 요청에서 재사용할 수 있도록 합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class HttpRequestEndpointChecker {
|
||||
|
||||
|
||||
/**
|
||||
* 요청 매핑 핸들러 매핑 객체입니다.
|
||||
*
|
||||
* <p>Spring MVC의 요청 매핑을 처리하는 데 사용됩니다.</p>
|
||||
*/
|
||||
@Qualifier("requestMappingHandlerMapping")
|
||||
private final RequestMappingHandlerMapping requestMapping;
|
||||
|
||||
/**
|
||||
* 주어진 HTTP 요청이 유효한 엔드포인트인지 확인합니다.
|
||||
*
|
||||
* <p>요청이 유효한 엔드포인트에 해당하면 true를 반환하고, 그렇지 않으면 false를 반환합니다.</p>
|
||||
*
|
||||
* @param request 확인할 HTTP 요청 객체
|
||||
* @return 요청이 유효한 엔드포인트이면 true, 그렇지 않으면 false
|
||||
*/
|
||||
public boolean isEndpointExist(HttpServletRequest request) {
|
||||
try {
|
||||
// 요청 경로가 파싱되지 않은 경우 파싱합니다.
|
||||
if (!ServletRequestPathUtils.hasParsedRequestPath(request)) {
|
||||
ServletRequestPathUtils.parseAndCache(request);
|
||||
}
|
||||
// 요청에 대한 핸들러를 조회합니다.
|
||||
HandlerExecutionChain handler = requestMapping.getHandler(request);
|
||||
if (handler != null) {
|
||||
HandlerMethod method = (HandlerMethod) handler.getHandler();
|
||||
// 핸들러가 존재하면 true를 반환합니다.
|
||||
if (method != null) return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 예외 발생 시 false를 반환합니다.
|
||||
return false;
|
||||
}
|
||||
// 핸들러가 존재하지 않으면 false를 반환합니다.
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,10 +21,12 @@ import com.spring.infra.security.error.SecurityExceptionRule;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* JWT 인증에서 접근 거부 상황을 처리하는 핸들러 클래스입니다.
|
||||
* 접근 거부 상황을 처리하는 핸들러 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 사용자가 인증은 되었지만 특정 리소스에 대한 접근 권한이 없는 경우를 처리합니다.
|
||||
* 이런 경우 SC_FORBIDDEN (403) 응답을 반환합니다.</p>
|
||||
* <p>이 클래스는 사용자가 인증되었지만 특정 리소스에 대한 접근 권한이 없는 경우를 처리합니다.
|
||||
* 이 경우 SC_FORBIDDEN (403) 응답을 반환합니다.</p>
|
||||
*
|
||||
* <p>API 요청과 일반 요청을 구분하여 각각에 대해 적절한 응답을 생성합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
@@ -33,15 +35,34 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
/**
|
||||
* 애플리케이션 컨텍스트 객체입니다.
|
||||
*
|
||||
* <p>Spring의 ApplicationContext를 사용하여 필요한 빈을 가져옵니다.</p>
|
||||
*/
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
|
||||
/**
|
||||
* 접근 거부 상황을 처리합니다.
|
||||
*
|
||||
* <p>HTTP 요청을 검사하여 API 요청인 경우 HandlerExceptionResolver를 사용하여
|
||||
* 예외를 처리하고, 일반 요청인 경우 403 Forbidden 응답을 반환합니다.</p>
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @param response HTTP 응답 객체
|
||||
* @param accessDeniedException 접근 거부 예외
|
||||
* @throws IOException 입출력 예외
|
||||
* @throws ServletException 서블릿 예외
|
||||
*/
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response,
|
||||
AccessDeniedException accessDeniedException) throws IOException, ServletException {
|
||||
if (isApiRequest(request)) {
|
||||
// API 요청인 경우 예외를 처리합니다.
|
||||
HandlerExceptionResolver resolver = applicationContext.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
|
||||
resolver.resolveException(request, response, null, new SecurityAuthException(SecurityExceptionRule.USER_FORBIDDEN));
|
||||
} else {
|
||||
// 일반 요청인 경우 403 Forbidden 응답을 설정합니다.
|
||||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpServletResponse.SC_FORBIDDEN);
|
||||
request.setAttribute(RequestDispatcher.ERROR_MESSAGE, SecurityExceptionRule.USER_FORBIDDEN.getMessage());
|
||||
@@ -50,6 +71,14 @@ public class SecurityAccessDeniedHandler implements AccessDeniedHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청이 API 요청인지 확인합니다.
|
||||
*
|
||||
* <p>HTTP 요청의 Accept 헤더를 검사하여 JSON 형식의 응답을 요구하는지 확인합니다.</p>
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @return API 요청이면 true, 그렇지 않으면 false
|
||||
*/
|
||||
private boolean isApiRequest(HttpServletRequest request) {
|
||||
String accept = request.getHeader(HttpHeaders.ACCEPT);
|
||||
return accept != null && accept.contains(MediaType.APPLICATION_JSON_VALUE);
|
||||
|
||||
@@ -19,7 +19,12 @@ import com.spring.infra.security.config.PermittedURI;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* JWT 인증에서 인증되지 않은 접근을 처리하는 진입점 클래스입니다.
|
||||
* 인증되지 않은 접근을 처리하는 진입점 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring Security의 AuthenticationEntryPoint 인터페이스를 구현하여,
|
||||
* 인증되지 않은 사용자가 보호된 리소스에 접근할 때 적절한 응답을 반환합니다.</p>
|
||||
*
|
||||
* <p>API 요청과 일반 요청을 구분하여 각각에 대해 적절한 처리 로직을 수행합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
@@ -28,34 +33,82 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
/**
|
||||
* 예외 속성 이름.
|
||||
*/
|
||||
private static final String EXCEPTION_ATTRIBUTE = "exception";
|
||||
|
||||
/**
|
||||
* 예외 처리 리졸버.
|
||||
*
|
||||
* <p>Spring의 HandlerExceptionResolver를 사용하여 예외를 처리합니다.</p>
|
||||
*/
|
||||
@Qualifier("handlerExceptionResolver")
|
||||
private final HandlerExceptionResolver resolver;
|
||||
|
||||
/**
|
||||
* HTTP 요청의 엔드포인트 존재 여부를 확인하는 체크 클래스.
|
||||
*/
|
||||
private final HttpRequestEndpointChecker endpointChecker;
|
||||
|
||||
/**
|
||||
* 인증되지 않은 사용자가 보호된 리소스에 접근할 때 호출됩니다.
|
||||
*
|
||||
* <p>요청된 엔드포인트가 존재하지 않으면 404 Not Found 응답을 반환하고,
|
||||
* API 요청인 경우에는 예외를 처리하여 적절한 응답을 반환합니다.</p>
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @param response HTTP 응답 객체
|
||||
* @param authException 인증 예외
|
||||
* @throws IOException 입출력 예외
|
||||
* @throws ServletException 서블릿 예외
|
||||
*/
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authException) throws IOException, ServletException {
|
||||
if (!endpointChecker.isEndpointExist(request)) {
|
||||
// 요청된 엔드포인트가 존재하지 않는 경우 404 응답을 반환합니다.
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
} else if (isApiRequest(request)) {
|
||||
// API 요청인 경우 예외를 처리합니다.
|
||||
handleApiRequest(request, response, authException);
|
||||
} else {
|
||||
// 일반 요청인 경우 루트 URI로 리다이렉트합니다.
|
||||
response.sendRedirect(PermittedURI.ROOT_URI.getUri());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청이 API 요청인지 확인합니다.
|
||||
*
|
||||
* <p>HTTP 요청의 Accept 헤더를 검사하여 JSON 형식의 응답을 요구하는지 확인합니다.</p>
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @return API 요청이면 true, 그렇지 않으면 false
|
||||
*/
|
||||
private boolean isApiRequest(HttpServletRequest request) {
|
||||
String accept = request.getHeader(HttpHeaders.ACCEPT);
|
||||
return accept != null && accept.contains(MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
|
||||
private void handleApiRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
|
||||
/**
|
||||
* API 요청에 대한 예외를 처리합니다.
|
||||
*
|
||||
* <p>인증 예외가 발생한 경우 적절한 응답을 생성하여 클라이언트에 반환합니다.</p>
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @param response HTTP 응답 객체
|
||||
* @param authException 인증 예외
|
||||
*/
|
||||
private void handleApiRequest(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authException) {
|
||||
if (request.getAttribute(EXCEPTION_ATTRIBUTE) != null) {
|
||||
resolver.resolveException(request, response, null, (Exception) request.getAttribute(EXCEPTION_ATTRIBUTE));
|
||||
// 요청 속성에 예외가 설정된 경우 해당 예외를 처리합니다.
|
||||
resolver.resolveException(request, response, null,
|
||||
(Exception) request.getAttribute(EXCEPTION_ATTRIBUTE));
|
||||
} else {
|
||||
// 인증 예외를 처리합니다.
|
||||
resolver.resolveException(request, response, null, authException);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,15 +11,46 @@ import com.spring.infra.security.jwt.JwtTokenService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 사용자 로그아웃 처리를 담당하는 핸들러 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring Security의 LogoutHandler 인터페이스를 구현하여,
|
||||
* 사용자가 로그아웃할 때 필요한 작업을 수행합니다.</p>
|
||||
*
|
||||
* <p>로그아웃 시 JWT 리프레시 토큰을 삭제하고, 관련 쿠키를 제거합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class SignOutHandler implements LogoutHandler {
|
||||
|
||||
/**
|
||||
* JWT 토큰 서비스를 제공하는 서비스 객체입니다.
|
||||
*
|
||||
* <p>로그아웃 시 리프레시 토큰을 삭제하고 쿠키를 제거하는 데 사용됩니다.</p>
|
||||
*/
|
||||
private final JwtTokenService jwtTokenService;
|
||||
|
||||
/**
|
||||
* 사용자가 로그아웃할 때 호출되는 메서드입니다.
|
||||
*
|
||||
* <p>HTTP 요청에서 리프레시 토큰을 추출하고, 해당 토큰을 삭제한 후,
|
||||
* 관련 쿠키를 제거합니다.</p>
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @param response HTTP 응답 객체
|
||||
* @param authentication 인증 정보
|
||||
*/
|
||||
@Override
|
||||
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
||||
// 요청에서 리프레시 토큰을 추출합니다.
|
||||
String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX);
|
||||
|
||||
// 리프레시 토큰을 삭제합니다.
|
||||
jwtTokenService.deleteRefreshToken(jwtTokenService.getUserPk(refreshToken));
|
||||
|
||||
// 관련 쿠키를 제거합니다.
|
||||
jwtTokenService.deleteCookie(response);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,19 +14,47 @@ import org.springframework.web.servlet.HandlerExceptionResolver;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 사용자 로그인 실패를 처리하는 핸들러 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring Security의 AuthenticationFailureHandler 인터페이스를 구현하여,
|
||||
* 사용자 인증 실패 시 적절한 처리를 수행합니다.</p>
|
||||
*
|
||||
* <p>로그인 실패 시 발생하는 예외를 처리하고, 이를 기반으로 적절한 응답을 생성합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SigninFailureHandler implements AuthenticationFailureHandler {
|
||||
|
||||
/**
|
||||
* 예외 처리 리졸버.
|
||||
*
|
||||
* <p>Spring의 HandlerExceptionResolver를 사용하여 예외를 처리합니다.</p>
|
||||
*/
|
||||
@Qualifier("handlerExceptionResolver")
|
||||
private final HandlerExceptionResolver resolver;
|
||||
|
||||
/**
|
||||
* 사용자 인증 실패 시 호출되는 메서드입니다.
|
||||
*
|
||||
* <p>인증 실패 시 발생하는 예외를 처리하고, 적절한 응답을 생성합니다.</p>
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @param response HTTP 응답 객체
|
||||
* @param exception 인증 예외
|
||||
* @throws IOException 입출력 예외
|
||||
* @throws ServletException 서블릿 예외
|
||||
*/
|
||||
@Override
|
||||
public void onAuthenticationFailure(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException exception
|
||||
) throws IOException, ServletException {
|
||||
// 인증 실패 시 예외를 처리합니다.
|
||||
resolver.resolveException(request, response, null, exception);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,14 +22,54 @@ import com.spring.infra.security.jwt.JwtTokenService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 사용자 로그인 성공을 처리하는 핸들러 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring Security의 AuthenticationSuccessHandler 인터페이스를 구현하여,
|
||||
* 사용자 인증 성공 시 적절한 처리를 수행합니다.</p>
|
||||
*
|
||||
* <p>로그인 성공 시 JWT 토큰을 생성하고, 사용자를 지정된 URL로 리다이렉트합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SigninSuccessHandler implements AuthenticationSuccessHandler {
|
||||
|
||||
/**
|
||||
* JWT 토큰 서비스를 제공하는 서비스 객체입니다.
|
||||
*
|
||||
* <p>사용자 인증 성공 시 JWT 토큰을 생성하는 데 사용됩니다.</p>
|
||||
*/
|
||||
private final JwtTokenService jwtTokenService;
|
||||
|
||||
/**
|
||||
* JSON 변환을 위한 ObjectMapper 객체입니다.
|
||||
*
|
||||
* <p>로그인 성공 시 사용자 정보를 JSON 형식으로 변환하는 데 사용됩니다.</p>
|
||||
*/
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 요청 캐시 객체입니다.
|
||||
*
|
||||
* <p>사용자가 인증되기 전에 요청된 URL을 저장하는 데 사용됩니다.</p>
|
||||
*/
|
||||
private RequestCache requestCache = new HttpSessionRequestCache();
|
||||
|
||||
/**
|
||||
* 사용자 인증 성공 시 호출되는 메서드입니다.
|
||||
*
|
||||
* <p>인증 성공 시 JWT 액세스 토큰과 리프레시 토큰을 생성하고,
|
||||
* 사용자를 지정된 URL로 리다이렉트합니다.</p>
|
||||
*
|
||||
* @param request HTTP 요청 객체
|
||||
* @param response HTTP 응답 객체
|
||||
* @param authentication 인증 정보
|
||||
* @throws IOException 입출력 예외
|
||||
* @throws ServletException 서블릿 예외
|
||||
*/
|
||||
@Override
|
||||
public void onAuthenticationSuccess(
|
||||
HttpServletRequest request,
|
||||
@@ -37,16 +77,19 @@ public class SigninSuccessHandler implements AuthenticationSuccessHandler {
|
||||
Authentication authentication
|
||||
) throws IOException, ServletException {
|
||||
|
||||
// 액세스 토큰 생성
|
||||
jwtTokenService.generateAccessToken(response, authentication);
|
||||
// 리프레시 토큰 저장
|
||||
jwtTokenService.saveRefreshToken(authentication.getName(), jwtTokenService.generateRefreshToken(response, authentication));
|
||||
|
||||
// 요청 캐시에서 저장된 요청을 가져옵니다.
|
||||
SavedRequest savedRequest = requestCache.getRequest(request, response);
|
||||
String targetUrl = (savedRequest != null) ? savedRequest.getRedirectUrl() : SecurityURI.REDIRECT_URI.getUri();
|
||||
|
||||
// 응답 설정
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
response.getWriter().write(objectMapper.writeValueAsString(SignResponse.of(true, targetUrl)));
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ import lombok.RequiredArgsConstructor;
|
||||
/**
|
||||
* JWT 토큰을 생성하는 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 액세스 토큰과 리프레시 토큰을 생성하는 기능을 제공합니다.</p>
|
||||
* <p>이 클래스는 액세스 토큰과 리프레시 토큰을 생성하는 기능을 제공합니다.
|
||||
* 사용자의 인증 정보를 기반으로 JWT 토큰을 생성하며, 토큰의 유효 기간과 서명 알고리즘을 설정합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
@@ -28,12 +29,22 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequiredArgsConstructor
|
||||
public class JwtTokenGenerator {
|
||||
|
||||
/**
|
||||
* JWT 토큰의 속성을 설정하는 클래스입니다.
|
||||
*/
|
||||
private final JwtProperties jwtProperties;
|
||||
|
||||
/**
|
||||
* JWT 서명 키를 제공하는 유틸리티 클래스입니다.
|
||||
*/
|
||||
private final JwtTokenUtil jwtTokenUtil;
|
||||
|
||||
/**
|
||||
* 액세스 토큰을 생성합니다.
|
||||
*
|
||||
* <p>사용자의 인증 정보를 기반으로 JWT 액세스 토큰을 생성합니다.
|
||||
* 생성된 토큰은 지정된 만료 시간과 서명 알고리즘을 사용하여 서명됩니다.</p>
|
||||
*
|
||||
* @param authentication 인증 정보
|
||||
* @return 생성된 액세스 토큰
|
||||
*/
|
||||
@@ -52,6 +63,9 @@ public class JwtTokenGenerator {
|
||||
/**
|
||||
* 리프레시 토큰을 생성합니다.
|
||||
*
|
||||
* <p>사용자의 인증 정보를 기반으로 JWT 리프레시 토큰을 생성합니다.
|
||||
* 생성된 토큰은 지정된 만료 시간과 서명 알고리즘을 사용하여 서명됩니다.</p>
|
||||
*
|
||||
* @param authentication 인증 정보
|
||||
* @return 생성된 리프레시 토큰
|
||||
*/
|
||||
@@ -68,6 +82,9 @@ public class JwtTokenGenerator {
|
||||
/**
|
||||
* JWT 헤더를 생성합니다.
|
||||
*
|
||||
* <p>JWT 토큰의 헤더 정보를 설정하는 메서드입니다.
|
||||
* 헤더에는 토큰의 유형과 서명 알고리즘이 포함됩니다.</p>
|
||||
*
|
||||
* @return JWT 헤더 맵
|
||||
*/
|
||||
private Map<String, Object> createHeader() {
|
||||
@@ -80,7 +97,10 @@ public class JwtTokenGenerator {
|
||||
/**
|
||||
* JWT 클레임을 생성합니다.
|
||||
*
|
||||
* @param authentication 인증 정보
|
||||
* <p>사용자의 정보를 기반으로 JWT 클레임을 설정하는 메서드입니다.
|
||||
* 클레임에는 사용자 ID, 이름 및 권한 정보가 포함됩니다.</p>
|
||||
*
|
||||
* @param user 사용자 정보
|
||||
* @return JWT 클레임 맵
|
||||
*/
|
||||
private Map<String, Object> createClaims(UserPrincipal user) {
|
||||
|
||||
@@ -28,9 +28,12 @@ import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
|
||||
/**
|
||||
* JWT 토큰 관련 서비스를 제공하는 클래스입니다.
|
||||
* JWT 토큰을 생성하고 검증하는 서비스를 제공하는 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 JWT 토큰의 생성, 검증, 해석 등 다양한 토큰 관련 기능을 제공합니다.</p>
|
||||
* <p>이 클래스는 JWT 토큰의 생성, 검증, 파싱 및 사용자 인증 정보를 관리하는 기능을 제공합니다.</p>
|
||||
*
|
||||
* <p>액세스 토큰과 리프레시 토큰을 생성하고, 토큰의 유효성을 검사하며,
|
||||
* 쿠키에서 토큰을 추출하는 등의 작업을 수행합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
@@ -38,24 +41,80 @@ import io.jsonwebtoken.Jwts;
|
||||
@Service
|
||||
public class JwtTokenService {
|
||||
|
||||
private final JwtTokenUtil jwtTokenUtil;
|
||||
private final JwtTokenGenerator jwtTokenGenerator;
|
||||
private final UserAuthenticationService userAuthenticationService;
|
||||
private final RefreshTokenService refreshTokenService;
|
||||
/**
|
||||
* JWT 비밀 키.
|
||||
*
|
||||
* <p>토큰의 서명을 생성하고 검증하는 데 사용되는 비밀 키입니다.</p>
|
||||
*/
|
||||
private final Key accessSecretKey;
|
||||
|
||||
/**
|
||||
* JWT 리프레시 비밀 키.
|
||||
*
|
||||
* <p>리프레시 토큰의 서명을 생성하고 검증하는 데 사용되는 비밀 키입니다.</p>
|
||||
*/
|
||||
private final Key refreshSecretKey;
|
||||
|
||||
/**
|
||||
* 액세스 토큰의 만료 시간.
|
||||
*
|
||||
* <p>토큰이 유효한 시간을 설정하는 데 사용됩니다.</p>
|
||||
*/
|
||||
private final long accessExpiration;
|
||||
|
||||
/**
|
||||
* 리프레시 토큰의 만료 시간.
|
||||
*
|
||||
* <p>리프레시 토큰이 유효한 시간을 설정하는 데 사용됩니다.</p>
|
||||
*/
|
||||
private final long refreshExpiration;
|
||||
|
||||
/**
|
||||
* JWT 토큰 생성기.
|
||||
*
|
||||
* <p>액세스 토큰과 리프레시 토큰을 생성하는 데 사용됩니다.</p>
|
||||
*/
|
||||
private final JwtTokenGenerator jwtTokenGenerator;
|
||||
|
||||
/**
|
||||
* JWT 유틸리티 클래스.
|
||||
*
|
||||
* <p>토큰의 서명 및 검증을 위한 유틸리티 메서드를 제공합니다.</p>
|
||||
*/
|
||||
private final JwtTokenUtil jwtTokenUtil;
|
||||
|
||||
/**
|
||||
* 사용자 인증 서비스.
|
||||
*
|
||||
* <p>사용자 인증 정보를 관리하는 서비스입니다.</p>
|
||||
*/
|
||||
private final UserAuthenticationService userAuthenticationService;
|
||||
|
||||
/**
|
||||
* 리프레시 토큰 서비스.
|
||||
*
|
||||
* <p>리프레시 토큰을 관리하는 서비스입니다.</p>
|
||||
*/
|
||||
private final RefreshTokenService refreshTokenService;
|
||||
|
||||
/**
|
||||
* JwtTokenService 생성자.
|
||||
*
|
||||
* @param jwtTokenGenerator JWT 토큰 생성기
|
||||
* @param jwtTokenUtil JWT 유틸리티 클래스
|
||||
* @param userAuthenticationService 사용자 인증 서비스
|
||||
* @param refreshTokenService 리프레시 토큰 서비스
|
||||
* @param jwtProperties JWT 속성
|
||||
*/
|
||||
public JwtTokenService(
|
||||
JwtTokenUtil jwtTokenUtil,
|
||||
JwtTokenGenerator jwtTokenGenerator,
|
||||
JwtTokenUtil jwtTokenUtil,
|
||||
UserAuthenticationService userAuthenticationService,
|
||||
RefreshTokenService refreshTokenService,
|
||||
JwtProperties jwtProperties
|
||||
) {
|
||||
this.jwtTokenUtil = jwtTokenUtil;
|
||||
this.jwtTokenGenerator = jwtTokenGenerator;
|
||||
this.jwtTokenUtil = jwtTokenUtil;
|
||||
this.userAuthenticationService = userAuthenticationService;
|
||||
this.refreshTokenService = refreshTokenService;
|
||||
this.accessSecretKey = jwtTokenUtil.getSigningKey(jwtProperties.getAccessToken().getSecret());
|
||||
@@ -80,7 +139,7 @@ public class JwtTokenService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 리프레시 토큰을 생성하고 쿠키에 저장한다.
|
||||
* 리프레시 토큰을 생성하고 쿠키에 저장합니다.
|
||||
*
|
||||
* @param response HTTP 응답 객체
|
||||
* @param authentication 인증 정보
|
||||
@@ -112,15 +171,6 @@ public class JwtTokenService {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 액세스 토큰의 유효성을 검증합니다.
|
||||
*
|
||||
* @param token 검증할 토큰
|
||||
*/
|
||||
public void validateToken(String token) {
|
||||
jwtTokenUtil.tokenStatus(token, accessSecretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 액세스 토큰의 유효성을 검증합니다.
|
||||
*
|
||||
@@ -137,8 +187,8 @@ public class JwtTokenService {
|
||||
* @param token 검증할 토큰
|
||||
* @return 토큰의 유효성 여부
|
||||
*/
|
||||
public boolean validateRefreshToken(String token) {
|
||||
return jwtTokenUtil.getTokenStatus(token, refreshSecretKey) == JwtTokenStatus.AUTHENTICATED;
|
||||
public void validateRefreshToken(String token) {
|
||||
jwtTokenUtil.tokenStatus(token, refreshSecretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,14 +237,31 @@ public class JwtTokenService {
|
||||
return new UsernamePasswordAuthenticationToken(user, "", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리프레시 토큰을 저장합니다.
|
||||
*
|
||||
* @param userId 사용자 ID
|
||||
* @param token 리프레시 토큰
|
||||
*/
|
||||
public void saveRefreshToken(String userId, String token) {
|
||||
refreshTokenService.saveRefreshToken(userId, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리프레시 토큰을 가져옵니다.
|
||||
*
|
||||
* @param token JWT 토큰
|
||||
* @return 리프레시 토큰
|
||||
*/
|
||||
public String getRefreshToken(String token) {
|
||||
return refreshTokenService.getRefreshToken(getUserPk(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* 리프레시 토큰을 삭제합니다.
|
||||
*
|
||||
* @param key 사용자 ID
|
||||
*/
|
||||
public void deleteRefreshToken(String key) {
|
||||
refreshTokenService.deleteRefreshToken(key);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ import io.jsonwebtoken.security.SignatureException;
|
||||
*
|
||||
* <p>이 클래스는 JWT 토큰의 상태 확인, 쿠키에서 토큰 추출, 서명 키 생성 등의 기능을 제공합니다.</p>
|
||||
*
|
||||
* <p>JWT 토큰의 생성 및 검증에 필요한 다양한 메서드를 포함하고 있습니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@@ -32,6 +34,8 @@ public class JwtTokenUtil {
|
||||
/**
|
||||
* JWT 토큰의 상태를 확인합니다.
|
||||
*
|
||||
* <p>주어진 JWT 토큰이 유효한지, 만료되었는지, 또는 잘못된 것인지 확인합니다.</p>
|
||||
*
|
||||
* @param token 검증할 JWT 토큰
|
||||
* @param secretKey 토큰 검증에 사용할 비밀 키
|
||||
* @return 토큰의 상태 (AUTHENTICATED, EXPIRED, INVALID)
|
||||
@@ -53,8 +57,14 @@ public class JwtTokenUtil {
|
||||
/**
|
||||
* JWT 토큰의 상태를 확인하고 예외를 던집니다.
|
||||
*
|
||||
* <p>주어진 JWT 토큰이 유효한지 확인하고, 유효하지 않은 경우 적절한 예외를 발생시킵니다.</p>
|
||||
*
|
||||
* @param token 검증할 JWT 토큰
|
||||
* @param secretKey 토큰 검증에 사용할 비밀 키
|
||||
* @throws ExpiredJwtException 토큰이 만료된 경우 발생
|
||||
* @throws SignatureException 서명 오류가 발생한 경우 발생
|
||||
* @throws MalformedJwtException 잘못된 JWT 형식인 경우 발생
|
||||
* @throws JwtException 기타 JWT 관련 오류가 발생한 경우 발생
|
||||
*/
|
||||
public void tokenStatus(String token, Key secretKey) {
|
||||
try {
|
||||
@@ -76,6 +86,8 @@ public class JwtTokenUtil {
|
||||
/**
|
||||
* 쿠키에서 특정 접두사를 가진 토큰을 추출합니다.
|
||||
*
|
||||
* <p>주어진 쿠키 배열에서 특정 접두사를 가진 JWT 토큰을 찾아 반환합니다.</p>
|
||||
*
|
||||
* @param cookies 쿠키 배열
|
||||
* @param tokenPrefix 토큰 접두사
|
||||
* @return 추출된 토큰 값 (없으면 빈 문자열)
|
||||
@@ -91,6 +103,8 @@ public class JwtTokenUtil {
|
||||
/**
|
||||
* 주어진 비밀 키로 서명 키를 생성합니다.
|
||||
*
|
||||
* <p>주어진 비밀 키 문자열을 기반으로 HMAC 서명 키를 생성합니다.</p>
|
||||
*
|
||||
* @param secretKey 비밀 키 문자열
|
||||
* @return 생성된 서명 키
|
||||
*/
|
||||
@@ -102,6 +116,8 @@ public class JwtTokenUtil {
|
||||
/**
|
||||
* 문자열을 Base64로 인코딩합니다.
|
||||
*
|
||||
* <p>주어진 문자열을 Base64 형식으로 인코딩하여 반환합니다.</p>
|
||||
*
|
||||
* @param secretKey 인코딩할 문자열
|
||||
* @return Base64로 인코딩된 문자열
|
||||
*/
|
||||
@@ -112,6 +128,8 @@ public class JwtTokenUtil {
|
||||
/**
|
||||
* 토큰을 리셋하는 쿠키를 생성합니다.
|
||||
*
|
||||
* <p>주어진 토큰 접두사에 대해 만료된 쿠키를 생성합니다.</p>
|
||||
*
|
||||
* @param tokenPrefix 토큰 접두사
|
||||
* @return 생성된 쿠키 객체
|
||||
*/
|
||||
|
||||
@@ -15,30 +15,81 @@ import com.spring.infra.security.error.SecurityExceptionRule;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 사용자 인증을 처리하는 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 Spring Security의 AuthenticationProvider 인터페이스를 구현하여,
|
||||
* 사용자 이름과 비밀번호를 기반으로 인증을 수행합니다.</p>
|
||||
*
|
||||
* <p>사용자의 비밀번호는 암호화된 형태로 저장되며, 입력된 비밀번호와 비교하여
|
||||
* 인증의 유효성을 검사합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class UserAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
/**
|
||||
* 비밀번호 인코더.
|
||||
*
|
||||
* <p>사용자의 비밀번호를 암호화하고 검증하는 데 사용됩니다.</p>
|
||||
*/
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
/**
|
||||
* 사용자 세부 정보를 로드하는 서비스.
|
||||
*
|
||||
* <p>사용자 이름을 기반으로 사용자 정보를 가져오는 데 사용됩니다.</p>
|
||||
*/
|
||||
private final UserDetailsService userDetailsService;
|
||||
|
||||
/**
|
||||
* 인증을 수행합니다.
|
||||
*
|
||||
* <p>주어진 인증 정보를 기반으로 사용자를 인증하고, 인증된 사용자 정보를 반환합니다.</p>
|
||||
*
|
||||
* @param authentication 인증 요청 정보
|
||||
* @return 인증된 Authentication 객체
|
||||
* @throws AuthenticationException 인증 실패 시 발생하는 예외
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String username = authentication.getName();
|
||||
String password = String.valueOf(authentication.getCredentials());
|
||||
|
||||
// 사용자 정보를 로드합니다.
|
||||
UserPrincipal user = (UserPrincipal) userDetailsService.loadUserByUsername(username);
|
||||
|
||||
// 비밀번호가 일치하는지 확인합니다.
|
||||
if (isNotMatches(password, user.getPassword())) {
|
||||
throw new SecurityAuthException(SecurityExceptionRule.USER_NOT_PASSWORD);
|
||||
}
|
||||
|
||||
// 인증된 사용자 정보를 반환합니다.
|
||||
return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 인증 클래스가 지원되는지 확인합니다.
|
||||
*
|
||||
* @param authentication 인증 클래스
|
||||
* @return 지원되는 경우 true, 그렇지 않으면 false
|
||||
*/
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return authentication.equals(UsernamePasswordAuthenticationToken.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호와 암호화된 비밀번호가 일치하지 않는지 확인합니다.
|
||||
*
|
||||
* @param password 입력된 비밀번호
|
||||
* @param encodePassword 암호화된 비밀번호
|
||||
* @return 일치하지 않으면 true, 일치하면 false
|
||||
*/
|
||||
private boolean isNotMatches(String password, String encodePassword) {
|
||||
return !passwordEncoder.matches(password, encodePassword);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,42 @@
|
||||
package com.spring.infra.security.service;
|
||||
|
||||
/**
|
||||
* 리프레시 토큰을 관리하는 서비스 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 리프레시 토큰의 저장, 조회 및 삭제 기능을 제공합니다.
|
||||
* 사용자의 리프레시 토큰을 데이터베이스에 저장하고, 필요 시 이를 조회하거나 삭제합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface RefreshTokenService {
|
||||
|
||||
/**
|
||||
* 리프레시 토큰을 저장합니다.
|
||||
*
|
||||
* <p>주어진 사용자 ID와 리프레시 토큰을 데이터베이스에 저장합니다.</p>
|
||||
*
|
||||
* @param userId 사용자 ID
|
||||
* @param refreshToken 저장할 리프레시 토큰
|
||||
*/
|
||||
void saveRefreshToken(String userId, String refreshToken);
|
||||
|
||||
/**
|
||||
* 리프레시 토큰을 조회합니다.
|
||||
*
|
||||
* <p>주어진 리프레시 토큰에 대한 정보를 조회합니다.</p>
|
||||
*
|
||||
* @param refreshToken 조회할 리프레시 토큰
|
||||
* @return 해당 리프레시 토큰의 정보
|
||||
*/
|
||||
String getRefreshToken(String refreshToken);
|
||||
|
||||
/**
|
||||
* 리프레시 토큰을 삭제합니다.
|
||||
*
|
||||
* <p>주어진 키에 해당하는 리프레시 토큰을 데이터베이스에서 삭제합니다.</p>
|
||||
*
|
||||
* @param key 삭제할 리프레시 토큰의 키 (예: 사용자 ID)
|
||||
*/
|
||||
void deleteRefreshToken(String key);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,27 @@ package com.spring.infra.security.service;
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
/**
|
||||
* 사용자 인증 관련 서비스를 정의하는 인터페이스입니다.
|
||||
*
|
||||
* <p>이 인터페이스는 사용자 인증 정보를 가져오는 메서드를 정의하며,
|
||||
* 사용자 세부 정보를 기반으로 인증을 수행하는 데 사용됩니다.</p>
|
||||
*
|
||||
* <p>구현 클래스는 사용자 정보를 데이터베이스에서 조회하거나,
|
||||
* 다른 소스에서 가져오는 로직을 포함해야 합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface UserAuthenticationService {
|
||||
|
||||
/**
|
||||
* 주어진 키에 해당하는 사용자 세부 정보를 가져옵니다.
|
||||
*
|
||||
* <p>사용자 ID 또는 사용자 이름을 기반으로 사용자 정보를 조회합니다.</p>
|
||||
*
|
||||
* @param key 사용자 ID 또는 사용자 이름
|
||||
* @return 사용자 세부 정보 객체
|
||||
*/
|
||||
UserDetails getUserDetails(String key);
|
||||
}
|
||||
|
||||
@@ -12,23 +12,35 @@ import lombok.RequiredArgsConstructor;
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Menus {
|
||||
|
||||
|
||||
DASHBOARD(
|
||||
"/dashboard",
|
||||
"Dashboard",
|
||||
"bi bi-grid",
|
||||
"/dashboard",
|
||||
"Dashboard",
|
||||
"bi bi-grid",
|
||||
List.of(AgentUserRole.ROLE_SUPER, AgentUserRole.ROLE_ADMIN, AgentUserRole.ROLE_USER)
|
||||
),
|
||||
SCHEDULE(
|
||||
"/schedule",
|
||||
"Schedule",
|
||||
"bi bi-menu-button-wide",
|
||||
"/schedule",
|
||||
"Schedule",
|
||||
"bi bi-menu-button-wide",
|
||||
List.of(AgentUserRole.ROLE_SUPER, AgentUserRole.ROLE_ADMIN)
|
||||
),
|
||||
JOB_HISTORY(
|
||||
"/schedule/history",
|
||||
"Job History",
|
||||
"bi bi-clock-history",
|
||||
List.of(AgentUserRole.ROLE_SUPER, AgentUserRole.ROLE_ADMIN)
|
||||
),
|
||||
USER_MANAGEMENT(
|
||||
"/user/management",
|
||||
"User Management",
|
||||
"bi bi-person",
|
||||
"/user/management",
|
||||
"User Management",
|
||||
"bi bi-person",
|
||||
List.of(AgentUserRole.ROLE_SUPER)
|
||||
),
|
||||
END_POINT(
|
||||
"/endpoint",
|
||||
"API Explorer",
|
||||
"bi bi-search",
|
||||
List.of(AgentUserRole.ROLE_SUPER)
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.spring.web.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/endpoint")
|
||||
public class EndPointController {
|
||||
|
||||
@GetMapping
|
||||
public String endpoint() {
|
||||
return "pages/endpoint/endpoint";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,11 +8,17 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@Controller
|
||||
@RequestMapping("/schedule")
|
||||
public class ScheduleController {
|
||||
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
|
||||
public String schedule() {
|
||||
return "pages/schedule/schedule";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/history")
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
|
||||
public String history() {
|
||||
return "pages/schedule/history";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ spring:
|
||||
devtools:
|
||||
restart:
|
||||
enabled: false
|
||||
add-properties: false
|
||||
add-properties: false
|
||||
application:
|
||||
name: spring-batch-quartz
|
||||
datasource:
|
||||
primary:
|
||||
driver-class-name: org.h2.Driver
|
||||
url: 'jdbc:h2:mem:app'
|
||||
username: mindol1004
|
||||
url: 'jdbc:h2:file:D:/projects/h2-db/app'
|
||||
username: test
|
||||
password: 1111
|
||||
hikari:
|
||||
pool-name: HikariPool-1
|
||||
@@ -24,8 +24,8 @@ spring:
|
||||
idle-timeout: 60000
|
||||
secondary:
|
||||
driver-class-name: org.h2.Driver
|
||||
url: 'jdbc:h2:mem:mob'
|
||||
username: mindol1004
|
||||
url: 'jdbc:h2:file:D:/projects/h2-db/mob'
|
||||
username: test
|
||||
password: 1111
|
||||
hikari:
|
||||
pool-name: HikariPool-2
|
||||
@@ -44,7 +44,7 @@ spring:
|
||||
database-platform: org.hibernate.dialect.H2Dialect
|
||||
#show-sql: true
|
||||
hibernate:
|
||||
ddl-auto: create
|
||||
ddl-auto: update
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.H2Dialect
|
||||
@@ -102,7 +102,7 @@ batch-info:
|
||||
email-send-batch:
|
||||
group: "EMAIL"
|
||||
job-name: "emailSendJob"
|
||||
cron-expression: "*/20 * * * * ?"
|
||||
cron-expression: "*/10 * * * * ?"
|
||||
description: "이메일배치작업"
|
||||
post-batch:
|
||||
group: "POST"
|
||||
@@ -113,7 +113,36 @@ batch-info:
|
||||
group: "POST"
|
||||
job-name: "postCreateJob"
|
||||
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:
|
||||
access-token:
|
||||
@@ -123,6 +152,14 @@ jwt:
|
||||
secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc=
|
||||
expiration: 10080
|
||||
|
||||
path:
|
||||
paths:
|
||||
path1:
|
||||
upload: /path1/upload
|
||||
path2:
|
||||
upload: /path2/upload
|
||||
dowonload: /path2/dowonload
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
@@ -131,7 +168,10 @@ management:
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: info
|
||||
org:
|
||||
springframework:
|
||||
web: debug
|
||||
hibernate:
|
||||
SQL: debug
|
||||
type:
|
||||
|
||||
BIN
batch-quartz/src/main/resources/guide/batch-quartz.png
Normal file
BIN
batch-quartz/src/main/resources/guide/batch-quartz.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
1842
batch-quartz/src/main/resources/guide/developer-guide.md
Normal file
1842
batch-quartz/src/main/resources/guide/developer-guide.md
Normal file
File diff suppressed because it is too large
Load Diff
207
batch-quartz/src/main/resources/guide/exception-guide.md
Normal file
207
batch-quartz/src/main/resources/guide/exception-guide.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# 스프링 애플리케이션 예외 처리 가이드
|
||||
|
||||
## 목차
|
||||
1. [개요](#개요)
|
||||
2. [전역 예외 처리 아키텍처](#전역-예외-처리-아키텍처)
|
||||
3. [예외 처리 구현 가이드](#예외-처리-구현-가이드)
|
||||
4. [예외 응답 형식](#예외-응답-형식)
|
||||
5. [예외 처리 모범 사례](#예외-처리-모범-사례)
|
||||
6. [문제 해결 가이드](#문제-해결-가이드)
|
||||
|
||||
## 개요
|
||||
|
||||
이 문서는 스프링 기반 애플리케이션에서의 예외 처리 메커니즘을 상세히 설명합니다.
|
||||
체계적인 예외 처리를 통해 애플리케이션의 안정성을 높이고 클라이언트에게 명확한 오류 정보를 제공할 수 있습니다.
|
||||
|
||||
## 예외 처리 가이드라인
|
||||
- 도메인별 error 패키지 생성: 각 도메인 패키지 내에 error 패키지를 생성하여 예외를 구조화합니다.
|
||||
- BizBaseException 상속: 도메인별 예외 클래스는 공통 예외 로직을 재사용하기 위해 BizBaseException을 상속받습니다.
|
||||
- ErrorRule 구현: 도메인별로 발생 가능한 예외 규칙을 ErrorRule로 정의합니다.
|
||||
- 에러 메시지 관리: 메시지는 절대 하드코딩하지 않고 ErrorRule을 통해 관리합니다.
|
||||
|
||||
## 전역 예외 처리 아키텍처
|
||||
|
||||
### GlobalExceptionHandler 클래스
|
||||
|
||||
```java
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
// 예외 처리 메서드들이 구현됨
|
||||
}
|
||||
```
|
||||
|
||||
`@RestControllerAdvice` 어노테이션을 사용하여 전역 예외 처리기를 구현합니다. 이 클래스는 다음과 같은 주요 기능을 제공합니다:
|
||||
|
||||
- 애플리케이션 전반의 예외를 일관되게 처리
|
||||
- HTTP 응답 상태 코드 및 본문 자동 생성
|
||||
- 구조화된 오류 응답 제공
|
||||
|
||||
## 예외 처리 구현 가이드
|
||||
|
||||
### 1. 비즈니스 예외 처리 (BizBaseException)
|
||||
|
||||
```java
|
||||
@ExceptionHandler(BizBaseException.class)
|
||||
public BizErrorResponse handleCustomException(BizBaseException e) {
|
||||
return BizErrorResponse.valueOf(e.getErrorRule());
|
||||
}
|
||||
```
|
||||
|
||||
#### 주요 특징
|
||||
- 비즈니스 로직에서 발생하는 모든 커스텀 예외 처리
|
||||
- `ErrorRule`을 통한 체계적인 오류 분류
|
||||
- 일관된 응답 형식 제공
|
||||
|
||||
#### 사용 예시
|
||||
```java
|
||||
if (data == null) {
|
||||
throw new BizBaseException();
|
||||
}
|
||||
```
|
||||
|
||||
#### 응답 예시
|
||||
```json
|
||||
{
|
||||
"timestamp": "2024-11-18T10:59:51.6131807",
|
||||
"statusCode": 500,
|
||||
"path": "/",
|
||||
"data": null,
|
||||
"message": "시스템 오류 입니다."
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 유효성 검증 예외 처리 (MethodArgumentNotValidException)
|
||||
|
||||
```java
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public BizErrorResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||
return BizErrorResponse.fromFieldError(e.getFieldErrors());
|
||||
}
|
||||
```
|
||||
|
||||
#### 처리되는 상황
|
||||
- 요청 DTO의 필드 유효성 검증 실패
|
||||
- `@Valid` 또는 `@Validated` 어노테이션 사용 시 발생하는 예외
|
||||
- 잘못된 데이터 형식이나 필수 필드 누락
|
||||
|
||||
#### 응답 예시
|
||||
```json
|
||||
{
|
||||
"timestamp": "2024-11-18T11:03:05.7627081",
|
||||
"statusCode": 422,
|
||||
"path": "/api/user/sign-up",
|
||||
"data": [
|
||||
{
|
||||
"field": "userPassword",
|
||||
"value": "",
|
||||
"reason": "비밀번호는 필수값 입니다."
|
||||
}
|
||||
],
|
||||
"message": "요청을 처리할 수 없습니다."
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 데이터베이스 예외 처리 (DataAccessException)
|
||||
|
||||
```java
|
||||
@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()
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 주요 처리 상황
|
||||
- 데이터베이스 연결 실패
|
||||
- 중복 키 위반
|
||||
- 데이터 무결성 제약 조건 위반
|
||||
- SQL 문법 오류
|
||||
|
||||
## 도메인별 예외 처리
|
||||
도메인별로 발생할 수 있는 예외를 정의하고, GlobalExceptionHandler에서 처리되도록 구조화합니다.
|
||||
|
||||
### 1. 도메인별 error 패키지 생성
|
||||
각 도메인 패키지 내에 error 패키지를 생성합니다. 예를 들어, 사용자 도메인의 경우 아래와 같은 구조를 가집니다.
|
||||
- com.skcc.domain.user.error
|
||||
|
||||
### 2. BizBaseException 상속
|
||||
모든 도메인별 예외 클래스는 공통 예외 처리 로직을 재사용하기 위해 BizBaseException을 상속받습니다.
|
||||
|
||||
```java
|
||||
import com.spring.common.error.BizBaseException;
|
||||
|
||||
public class UserNotFoundException extends BizBaseException {
|
||||
public UserNotFoundException() {
|
||||
super(UserRule.USER_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. ErrorRule 구현
|
||||
|
||||
#### 3.1 ErrorRule 인터페이스 정의
|
||||
도메인별 예외 규칙을 정의하기 위해 ErrorRule 인터페이스를 구현합니다. 이를 통해 예외 메시지와 HTTP 상태 코드를 일관되게 관리할 수 있습니다.
|
||||
|
||||
```java
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public interface ErrorRule {
|
||||
HttpStatus getStatus();
|
||||
String getMessage();
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 도메인별 ErrorRule 정의
|
||||
각 도메인에서 발생할 수 있는 예외를 enum으로 정의합니다. 예외 메시지와 HTTP 상태 코드를 설정하여 한 곳에서 관리할 수 있습니다.
|
||||
|
||||
```java
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum UserRule implements ErrorRule {
|
||||
|
||||
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
|
||||
USER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "사용자 인증에 실패 하였습니다.");
|
||||
|
||||
private final HttpStatus status;
|
||||
private String message;
|
||||
|
||||
UserRule(HttpStatus status, String message) {
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public UserRule message(final String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 에러 메시지 관리
|
||||
|
||||
#### 4.1 하드코딩 방지
|
||||
- 에러 메시지는 절대 하드코딩하지 않고, 각 도메인의 ErrorRule을 통해 관리합니다.
|
||||
- 이를 통해 에러 메시지의 일관성을 유지하고, 메시지 변경 시 코드 수정 범위를 최소화할 수 있습니다.
|
||||
|
||||
```java
|
||||
public class UserNotFoundException extends BizBaseException {
|
||||
public UserNotFoundException() {
|
||||
super(UserRule.USER_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<div style="text-align: left; margin: 20px 0;">
|
||||
<a href="#" style="color: white; text-decoration: none;">
|
||||
<button style="background-color: #007BFF; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;">
|
||||
Top
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
432
batch-quartz/src/main/resources/guide/thymeleaf-guide.md
Normal file
432
batch-quartz/src/main/resources/guide/thymeleaf-guide.md
Normal file
@@ -0,0 +1,432 @@
|
||||
## Thymeleaf 사용 가이드
|
||||
|
||||
### 1. 기본 설정
|
||||
|
||||
#### 1.1 의존성 추가
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
#### 1.2 application.yml 설정
|
||||
```yaml
|
||||
spring:
|
||||
thymeleaf:
|
||||
cache: false # 개발 환경에서는 캐시 비활성화
|
||||
prefix: classpath:/templates/ # 템플릿 위치
|
||||
suffix: .html # 템플릿 확장자
|
||||
mode: HTML # 템플릿 모드
|
||||
encoding: UTF-8 # 인코딩
|
||||
```
|
||||
|
||||
### 2. 레이아웃 구성
|
||||
|
||||
#### 2.1 기본 레이아웃 (layout.html)
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title layout:title-pattern="$CONTENT_TITLE - 배치 모니터링">기본 타이틀</title>
|
||||
|
||||
<!-- 공통 CSS -->
|
||||
<link rel="stylesheet" th:href="@{/css/common.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/layout.css}">
|
||||
|
||||
<!-- 페이지별 CSS 영역 -->
|
||||
<th:block layout:fragment="css"></th:block>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 헤더 영역 -->
|
||||
<header th:replace="fragments/header :: header"></header>
|
||||
|
||||
<!-- 사이드바 영역 -->
|
||||
<aside th:replace="fragments/sidebar :: sidebar"></aside>
|
||||
|
||||
<!-- 본문 영역 -->
|
||||
<main layout:fragment="content">
|
||||
<!-- 각 페이지의 내용이 여기에 들어감 -->
|
||||
</main>
|
||||
|
||||
<!-- 푸터 영역 -->
|
||||
<footer th:replace="fragments/footer :: footer"></footer>
|
||||
|
||||
<!-- 공통 JavaScript -->
|
||||
<script th:src="@{/js/common.js}"></script>
|
||||
|
||||
<!-- 페이지별 JavaScript 영역 -->
|
||||
<th:block layout:fragment="script"></th:block>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
#### 2.2 페이지 구현 예시 (batch-jobs.html)
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layouts/layout}">
|
||||
<head>
|
||||
<title>배치 작업 목록</title>
|
||||
<!-- 페이지별 CSS -->
|
||||
<th:block layout:fragment="css">
|
||||
<link rel="stylesheet" th:href="@{/css/batch-jobs.css}">
|
||||
</th:block>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 본문 영역 -->
|
||||
<main layout:fragment="content">
|
||||
<h2>배치 작업 목록</h2>
|
||||
<div class="job-list">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>작업명</th>
|
||||
<th>상태</th>
|
||||
<th>마지막 실행</th>
|
||||
<th>다음 실행</th>
|
||||
<th>작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="job : ${jobs}">
|
||||
<td th:text="${job.name}">작업명</td>
|
||||
<td th:text="${job.status}">상태</td>
|
||||
<td th:text="${#temporals.format(job.lastExecuted, 'yyyy-MM-dd HH:mm:ss')}">
|
||||
마지막 실행
|
||||
</td>
|
||||
<td th:text="${#temporals.format(job.nextExecute, 'yyyy-MM-dd HH:mm:ss')}">
|
||||
다음 실행
|
||||
</td>
|
||||
<td>
|
||||
<button th:onclick="'executeJob(\'' + ${job.id} + '\')'">실행</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 페이지별 JavaScript -->
|
||||
<th:block layout:fragment="script">
|
||||
<script th:src="@{/js/batch-jobs.js}"></script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 3. Thymeleaf 문법 가이드
|
||||
|
||||
#### 3.1 기본 표현식
|
||||
```html
|
||||
<!-- 텍스트 출력 -->
|
||||
<span th:text="${message}">기본 메시지</span>
|
||||
|
||||
<!-- HTML 출력 -->
|
||||
<div th:utext="${htmlContent}">기본 HTML</div>
|
||||
|
||||
<!-- 변수 표현식 -->
|
||||
<span th:text="${user.name}">사용자명</span>
|
||||
|
||||
<!-- URL 표현식 -->
|
||||
<a th:href="@{/jobs/{id}(id=${job.id})}">상세보기</a>
|
||||
|
||||
<!-- 조건부 표현식 -->
|
||||
<div th:if="${not #lists.isEmpty(jobs)}">
|
||||
작업이 있습니다.
|
||||
</div>
|
||||
<div th:unless="${not #lists.isEmpty(jobs)}">
|
||||
작업이 없습니다.
|
||||
</div>
|
||||
|
||||
<!-- 반복문 -->
|
||||
<tr th:each="job, stat : ${jobs}">
|
||||
<td th:text="${stat.count}">1</td>
|
||||
<td th:text="${job.name}">작업명</td>
|
||||
</tr>
|
||||
|
||||
<!-- Switch-Case -->
|
||||
<div th:switch="${job.status}">
|
||||
<p th:case="'RUNNING'">실행 중</p>
|
||||
<p th:case="'COMPLETED'">완료</p>
|
||||
<p th:case="*">기타</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 3.2 유틸리티 객체
|
||||
```html
|
||||
<!-- 날짜 포맷팅 -->
|
||||
<span th:text="${#temporals.format(job.startTime, 'yyyy-MM-dd HH:mm:ss')}">
|
||||
2024-01-01 12:00:00
|
||||
</span>
|
||||
|
||||
<!-- 숫자 포맷팅 -->
|
||||
<span th:text="${#numbers.formatDecimal(amount, 1, 'COMMA', 2, 'POINT')}">
|
||||
1,234.56
|
||||
</span>
|
||||
|
||||
<!-- 문자열 처리 -->
|
||||
<span th:text="${#strings.abbreviate(description, 100)}">
|
||||
긴 설명...
|
||||
</span>
|
||||
|
||||
<!-- 컬렉션 처리 -->
|
||||
<div th:if="${#lists.isEmpty(jobs)}">
|
||||
작업이 없습니다.
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 3.3 폼 처리
|
||||
```html
|
||||
<!-- 폼 바인딩 -->
|
||||
<form th:action="@{/jobs}" th:object="${jobForm}" method="post">
|
||||
<div>
|
||||
<label>작업명:</label>
|
||||
<input type="text" th:field="*{name}">
|
||||
<span th:if="${#fields.hasErrors('name')}"
|
||||
th:errors="*{name}"
|
||||
class="error">
|
||||
이름 오류
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Cron 표현식:</label>
|
||||
<input type="text" th:field="*{cronExpression}">
|
||||
<span th:if="${#fields.hasErrors('cronExpression')}"
|
||||
th:errors="*{cronExpression}"
|
||||
class="error">
|
||||
Cron 표현식 오류
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button type="submit">저장</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### 4. 공통 컴포넌트
|
||||
|
||||
#### 4.1 페이지네이션 (fragments/pagination.html)
|
||||
```html
|
||||
<div th:fragment="pagination (page)">
|
||||
<div class="pagination"
|
||||
th:if="${page.totalPages > 0}">
|
||||
<!-- 이전 페이지 -->
|
||||
<a th:href="@{${#httpServletRequest.requestURI}(page=${page.number - 1})}"
|
||||
th:class="${page.first} ? 'disabled' : ''"
|
||||
th:if="${not page.first}">
|
||||
이전
|
||||
</a>
|
||||
|
||||
<!-- 페이지 번호 -->
|
||||
<span th:each="pageNum : ${#numbers.sequence(0, page.totalPages - 1)}">
|
||||
<a th:href="@{${#httpServletRequest.requestURI}(page=${pageNum})}"
|
||||
th:text="${pageNum + 1}"
|
||||
th:class="${pageNum == page.number} ? 'active' : ''">
|
||||
1
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<!-- 다음 페이지 -->
|
||||
<a th:href="@{${#httpServletRequest.requestURI}(page=${page.number + 1})}"
|
||||
th:class="${page.last} ? 'disabled' : ''"
|
||||
th:if="${not page.last}">
|
||||
다음
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 4.2 알림 메시지 (fragments/alert.html)
|
||||
```html
|
||||
<div th:fragment="alert (type, message)">
|
||||
<div th:if="${message != null}"
|
||||
th:class="'alert alert-' + ${type}">
|
||||
<span th:text="${message}">알림 메시지</span>
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 5. JavaScript 연동
|
||||
|
||||
#### 5.1 타임리프와 JavaScript 데이터 전달
|
||||
```html
|
||||
<!-- 단일 값 전달 -->
|
||||
<script th:inline="javascript">
|
||||
const message = /*[[${message}]]*/ '기본 메시지';
|
||||
|
||||
// 객체 전달
|
||||
const job = /*[[${job}]]*/ {};
|
||||
|
||||
// 배열 전달
|
||||
const jobs = /*[[${jobs}]]*/ [];
|
||||
</script>
|
||||
|
||||
<!-- AJAX 호출 예시 -->
|
||||
<script th:inline="javascript">
|
||||
function executeJob(jobId) {
|
||||
const csrfToken = /*[[${_csrf.token}]]*/ '';
|
||||
const csrfHeader = /*[[${_csrf.headerName}]]*/ 'X-CSRF-TOKEN';
|
||||
|
||||
fetch(`/api/jobs/${jobId}/execute`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
[csrfHeader]: csrfToken
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('작업이 실행되었습니다.');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('작업 실행에 실패했습니다.');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 6. 보안 처리
|
||||
|
||||
#### 6.1 Spring Security 통합
|
||||
```html
|
||||
<!-- 인증 여부 확인 -->
|
||||
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||
<span th:text="${#authentication.name}">사용자명</span>
|
||||
</div>
|
||||
|
||||
<!-- 권한 기반 표시 -->
|
||||
<div th:if="${#authorization.expression('hasRole(''ADMIN'')')}">
|
||||
관리자 메뉴
|
||||
</div>
|
||||
|
||||
<!-- CSRF 토큰 -->
|
||||
<form th:action="@{/jobs}" method="post">
|
||||
<input type="hidden"
|
||||
th:name="${_csrf.parameterName}"
|
||||
th:value="${_csrf.token}" />
|
||||
<!-- 폼 내용 -->
|
||||
</form>
|
||||
```
|
||||
|
||||
### 7. 에러 페이지
|
||||
|
||||
#### 7.1 에러 페이지 구현 (error.html)
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layouts/layout}">
|
||||
<head>
|
||||
<title>에러 발생</title>
|
||||
</head>
|
||||
<body>
|
||||
<main layout:fragment="content">
|
||||
<div class="error-container">
|
||||
<h2>오류가 발생했습니다</h2>
|
||||
<div class="error-details">
|
||||
<p>
|
||||
<strong>상태:</strong>
|
||||
<span th:text="${status}">500</span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>메시지:</strong>
|
||||
<span th:text="${error}">Internal Server Error</span>
|
||||
</p>
|
||||
<div th:if="${trace}" class="error-trace">
|
||||
<pre th:text="${trace}">스택 트레이스</pre>
|
||||
</div>
|
||||
</div>
|
||||
<a th:href="@{/}" class="btn btn-primary">홈으로 돌아가기</a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 8. 성능 최적화
|
||||
|
||||
#### 8.1 캐시 설정
|
||||
```yaml
|
||||
spring:
|
||||
thymeleaf:
|
||||
cache: true # 운영 환경에서는 캐시 활성화
|
||||
```
|
||||
|
||||
#### 8.2 프래그먼트 캐싱
|
||||
```html
|
||||
<!-- 캐시 키 지정 -->
|
||||
<div th:fragment="userInfo (user)"
|
||||
th:cache="${user.id}">
|
||||
<!-- 사용자 정보 표시 -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### 9. 국제화 (i18n)
|
||||
|
||||
#### 9.1 메시지 설정
|
||||
```yaml
|
||||
spring:
|
||||
messages:
|
||||
basename: messages
|
||||
encoding: UTF-8
|
||||
```
|
||||
|
||||
#### 9.2 메시지 사용
|
||||
```html
|
||||
<!-- 메시지 출력 -->
|
||||
<h1 th:text="#{page.title}">제목</h1>
|
||||
|
||||
<!-- 파라미터가 있는 메시지 -->
|
||||
<p th:text="#{message.welcome(${user.name})}">환영합니다</p>
|
||||
|
||||
<!-- 언어 선택 -->
|
||||
<div class="language-selector">
|
||||
<a th:href="@{''(lang=ko)}">한국어</a>
|
||||
<a th:href="@{''(lang=en)}">English</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 10. 모범 사례
|
||||
|
||||
#### 10.1 코드 구조화
|
||||
```text
|
||||
templates/
|
||||
├── layouts/
|
||||
│ └── layout.html
|
||||
├── fragments/
|
||||
│ ├── header.html
|
||||
│ ├── footer.html
|
||||
│ ├── sidebar.html
|
||||
│ ├── pagination.html
|
||||
│ └── alert.html
|
||||
├── batch/
|
||||
│ ├── list.html
|
||||
│ ├── detail.html
|
||||
│ └── form.html
|
||||
├── error/
|
||||
│ ├── 404.html
|
||||
│ ├── 500.html
|
||||
│ └── error.html
|
||||
└── index.html
|
||||
```
|
||||
|
||||
#### 10.2 네이밍 컨벤션
|
||||
- 레이아웃 템플릿: layout.html
|
||||
- 페이지 템플릿: 기능명/동작.html (예: batch/list.html)
|
||||
- 프래그먼트: fragments/컴포넌트명.html
|
||||
- 에러 페이지: error/에러코드.html
|
||||
|
||||
#### 10.3 주석 처리
|
||||
```html
|
||||
<!-- /* 타임리프 주석 - 클라이언트에 전송되지 않음 */ -->
|
||||
<!--/* 여러 줄
|
||||
타임리프 주석 */-->
|
||||
|
||||
<!-- 일반 HTML 주석 - 클라이언트에 전송됨 -->
|
||||
```
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
|
||||
import apiClient from '../common/axios-instance.js';
|
||||
|
||||
const endPointService = {
|
||||
|
||||
getEndpoints: async () => {
|
||||
const response = await apiClient.get('/api/endpoint');
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default endPointService;
|
||||
@@ -39,6 +39,12 @@ const scheduleService = {
|
||||
refreshJob: async () => {
|
||||
await apiClient.post('/actuator/refresh');
|
||||
return true;
|
||||
},
|
||||
|
||||
getJobHistory: async (searchParams) => {
|
||||
console.log(searchParams);
|
||||
const response = await apiClient.get('/api/schedule/history', { params: searchParams });
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@ import apiClient, { saveAccessToken, removeTokens } from '../common/axios-instan
|
||||
|
||||
const signService = {
|
||||
|
||||
signIn: async (username, password) => {
|
||||
const response = await apiClient.post('/sign-in', { username, password });
|
||||
signIn: async (params) => {
|
||||
const response = await apiClient.post('/sign-in', params);
|
||||
const accessToken = response.headers['authorization'].split(' ')[1];
|
||||
saveAccessToken(accessToken);
|
||||
return response.data;
|
||||
@@ -24,8 +24,8 @@ const signService = {
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
changePassword: async (userId, email, newPassword) => {
|
||||
const response = await apiClient.post('/api/user/change-password', { userId, email, newPassword });
|
||||
changePassword: async (params) => {
|
||||
const response = await apiClient.post('/api/user/change-password', params);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
dayjs.locale('ko');
|
||||
|
||||
export const addEvents = (eventMap) => {
|
||||
Object.entries(eventMap).forEach(([selector, { event, handler }]) => {
|
||||
const elements = selector.startsWith('#') || selector.startsWith('.') || document.getElementsByName(selector).length > 0
|
||||
? document.querySelectorAll(selector)
|
||||
: document.querySelectorAll(`[id="${selector}"], [class="${selector}"], [name="${selector}"]`);
|
||||
|
||||
elements.forEach(element => {
|
||||
element.addEventListener(event, (e) => {
|
||||
e.preventDefault();
|
||||
handler(e);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const formatDateTime = (dateTimeString) => {
|
||||
if (!dateTimeString) return '-';
|
||||
const date = new Date(dateTimeString);
|
||||
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user