spring 버전 변경

This commit is contained in:
mindol1004
2024-08-27 15:47:04 +09:00
parent f1ddadfb9b
commit cc8ca2ae51
57 changed files with 1177 additions and 287 deletions

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<version>2.7.18</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.spring</groupId>
@@ -27,7 +27,7 @@
<url/>
</scm>
<properties>
<java.version>17</java.version>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>

View File

@@ -1,63 +0,0 @@
package com.spring.common.util;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ApplicationUtil {
public static ServletRequestAttributes getServletRequestAttributes(){
return (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
}
//Request 객체 얻기
public static HttpServletRequest getRequest(){
return getServletRequestAttributes().getRequest();
}
//Set Request Attribute
public static void setRequestAttributes(String key, Object obj){
getServletRequestAttributes().setAttribute(key, obj, RequestAttributes.SCOPE_REQUEST);
}
//Get Request Attribute
public static Object getRequestAttributes(String key){
return getServletRequestAttributes().getAttribute(key, RequestAttributes.SCOPE_REQUEST);
}
//Session 객체 얻기
public static HttpSession getSession(){
return getRequest().getSession();
}
//Set Session Attributes
public static void setSessionAttributes(String key, Object obj){
getServletRequestAttributes().setAttribute(key, obj, RequestAttributes.SCOPE_SESSION);
}
//Get Session Attributes
public static Object getSessionAttributes(String key){
return getServletRequestAttributes().getAttribute(key, RequestAttributes.SCOPE_SESSION);
}
//HttpServletResponse 객체 얻기
public static HttpServletResponse getResponse(){
return getServletRequestAttributes().getResponse();
}
//beanName을 통해서 Bean을 얻을 수 있다.
public static Object getBean(String beanName){
var context = ContextLoader.getCurrentWebApplicationContext();
if (context == null) {
throw new IllegalStateException("WebApplicationContext를 찾을 수 없습니다.");
}
return context.getBean(beanName);
}
}

View File

@@ -0,0 +1,69 @@
package com.spring.common.util;
import java.util.concurrent.atomic.AtomicReference;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
/**
* Spring ApplicationContext에 접근하기 위한 유틸리티 클래스입니다.
*
* <p>이 클래스는 ApplicationContextAware를 구현하여 ApplicationContext를 저장하고,
* 애플리케이션 전역에서 Spring 빈에 접근할 수 있는 정적 메소드를 제공합니다.</p>
*
* @author mindol
* @version 1.0
*/
@Component
public class ContextUtils implements ApplicationContextAware {
private static final AtomicReference<ApplicationContext> applicationContext = new AtomicReference<>();
/**
* Spring에 의해 호출되어 ApplicationContext를 설정합니다.
*
* @param context 설정할 ApplicationContext
* @throws BeansException 빈 예외 발생 시
*/
@Override
public void setApplicationContext(@NonNull ApplicationContext context) throws BeansException {
applicationContext.set(context);
}
/**
* 지정된 이름과 타입의 빈을 반환합니다.
*
* @param <T> 반환될 빈의 타입
* @param beanName 찾을 빈의 이름
* @param clazz 반환될 빈의 클래스
* @return 지정된 이름과 타입의 빈
* @throws IllegalStateException ApplicationContext가 설정되지 않은 경우
*/
public static <T> T getBean(String beanName, Class<T> clazz) {
ApplicationContext context = applicationContext.get();
if (context == null) {
throw new IllegalStateException("ApplicationContext가 설정되지 않았습니다.");
}
return context.getBean(beanName, clazz);
}
/**
* 지정된 타입의 빈을 반환합니다.
*
* @param <T> 반환될 빈의 타입
* @param clazz 반환될 빈의 클래스
* @return 지정된 타입의 빈
* @throws IllegalStateException ApplicationContext가 설정되지 않은 경우
*/
public static <T> T getBean(Class<T> clazz) {
ApplicationContext context = applicationContext.get();
if (context == null) {
throw new IllegalStateException("ApplicationContext가 설정되지 않았습니다.");
}
return context.getBean(clazz);
}
}

View File

@@ -0,0 +1,117 @@
package com.spring.common.util;
import java.util.Optional;
import java.util.function.Function;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* 서블릿 관련 유틸리티 클래스입니다.
*
* <p>현재 요청의 HttpServletRequest, HttpServletResponse, HttpSession 등에 접근하는 메서드를 제공합니다.</p>
*
* @author mindol
* @version 1.0
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ServletUtils {
/**
* 현재 요청의 ServletRequestAttributes를 Optional로 반환합니다.
*
* @return ServletRequestAttributes를 포함한 Optional 객체
*/
private static Optional<ServletRequestAttributes> getAttributes() {
return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.filter(ServletRequestAttributes.class::isInstance)
.map(ServletRequestAttributes.class::cast);
}
/**
* ServletRequestAttributes에서 특정 속성을 추출합니다.
*
* @param getter ServletRequestAttributes에서 원하는 속성을 추출하는 함수
* @param <T> 반환될 속성의 타입
* @return 추출된 속성 또는 null
*/
private static <T> T getAttribute(Function<ServletRequestAttributes, T> getter) {
return getAttributes().map(getter).orElse(null);
}
/**
* 현재 요청의 HttpServletRequest를 반환합니다.
*
* @return 현재 HttpServletRequest 또는 null
*/
public static HttpServletRequest getRequest() {
return getAttribute(ServletRequestAttributes::getRequest);
}
/**
* 현재 요청의 속성을 설정합니다.
*
* @param key 속성의 키
* @param obj 설정할 속성 값
*/
public static void setRequestAttribute(String key, Object obj) {
getAttributes().ifPresent(attr -> attr.setAttribute(key, obj, RequestAttributes.SCOPE_REQUEST));
}
/**
* 현재 요청의 속성을 반환합니다.
*
* @param key 속성의 키
* @return 요청 속성 값 또는 null
*/
public static Object getRequestAttribute(String key) {
return getAttribute(attr -> attr.getAttribute(key, RequestAttributes.SCOPE_REQUEST));
}
/**
* 현재 요청의 HttpSession을 반환합니다.
*
* @return 현재 HttpSession 또는 null
*/
public static HttpSession getSession() {
return Optional.ofNullable(getRequest()).map(HttpServletRequest::getSession).orElse(null);
}
/**
* 현재 세션의 속성을 설정합니다.
*
* @param key 속성의 키
* @param obj 설정할 속성 값
*/
public static void setSessionAttribute(String key, Object obj) {
getAttributes().ifPresent(attr -> attr.setAttribute(key, obj, RequestAttributes.SCOPE_SESSION));
}
/**
* 현재 세션의 속성을 반환합니다.
*
* @param key 속성의 키
* @return 세션 속성 값 또는 null
*/
public static Object getSessionAttribute(String key) {
return getAttribute(attr -> attr.getAttribute(key, RequestAttributes.SCOPE_SESSION));
}
/**
* 현재 요청의 HttpServletResponse를 반환합니다.
*
* @return 현재 HttpServletResponse 또는 null
*/
public static HttpServletResponse getResponse() {
return getAttribute(ServletRequestAttributes::getResponse);
}
}

View File

@@ -2,15 +2,14 @@ package com.spring.domain.batch.entity;
import java.sql.Timestamp;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.ForeignKey;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.Getter;
// @Entity

View File

@@ -1,12 +1,11 @@
package com.spring.domain.batch.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.ForeignKey;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import lombok.Getter;
// @Entity

View File

@@ -2,14 +2,13 @@ package com.spring.domain.batch.entity;
import java.io.Serializable;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.ForeignKey;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@@ -37,7 +36,7 @@ public class BatchJobExecutionParams {
@Embeddable
@Getter
@EqualsAndHashCode
public class BatchJobExecutionParamsId implements Serializable {
public static class BatchJobExecutionParamsId implements Serializable {
@Column(name = "JOB_EXECUTION_ID")
private Long jobExecutionId;

View File

@@ -1,12 +1,10 @@
package com.spring.domain.batch.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import lombok.Getter;
// @Entity

View File

@@ -2,15 +2,14 @@ package com.spring.domain.batch.entity;
import java.sql.Timestamp;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.ForeignKey;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.Getter;
// @Entity

View File

@@ -1,12 +1,11 @@
package com.spring.domain.batch.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.ForeignKey;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import lombok.Getter;
// @Entity

View File

@@ -3,45 +3,40 @@ package com.spring.domain.email.batch;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import com.spring.infra.quartz.QuartzJob;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class EmailSendBatch {
private final JobRepository jobRepository;
private final PlatformTransactionManager transactionManager;
@QuartzJob(name = "emailSendJob", cronExpression = "*/3 * * * * ?")
@QuartzJob(group = "EMAIL", name = "emailSendJob", cronExpression = "*/3 * * * * ?")
// @JobScope
@Bean(name = "emailSendJob")
Job emailSendJob(Step emailSendStep) {
log.info(">>> emailSendJob");
return new JobBuilder("emailSendJob", jobRepository)
return new JobBuilder("emailSendJob")
.start(emailSendStep)
.build();
}
// @StepScope
@Bean("emailSendStep")
Step emailSendStep(Tasklet emailSendTasklet) {
log.info(">>> emailSendStep");
return new StepBuilder("emailSendStep", jobRepository)
.tasklet(emailSendTasklet, transactionManager).build();
return new StepBuilder("emailSendStep")
.tasklet(emailSendTasklet).build();
}
@Bean
Tasklet emailSendTasklet(){
Tasklet emailSendTasklet() {
return ((contribution, chunkContext) -> {
log.info(">>>>> emailSendTasklet");
return RepeatStatus.FINISHED;

View File

@@ -3,15 +3,16 @@ package com.spring.domain.email.entity;
import java.io.Serializable;
import java.time.LocalDate;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.Lob;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Lob;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -3,11 +3,12 @@ package com.spring.domain.email.entity;
import java.io.Serializable;
import java.time.LocalDate;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -1,14 +1,15 @@
package com.spring.domain.post.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import com.spring.infra.db.SecondaryDataSourceConfig;
import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
@DatabaseSelector(SecondaryDataSourceConfig.DATABASE)

View File

@@ -2,13 +2,12 @@ package com.spring.domain.quartz.entity;
import java.io.Serializable;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -2,11 +2,10 @@ package com.spring.domain.quartz.entity;
import java.io.Serializable;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -2,15 +2,14 @@ package com.spring.domain.quartz.entity;
import java.io.Serializable;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinColumns;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.ForeignKey;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.ManyToOne;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -2,11 +2,10 @@ package com.spring.domain.quartz.entity;
import java.io.Serializable;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -2,11 +2,10 @@ package com.spring.domain.quartz.entity;
import java.io.Serializable;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -2,11 +2,10 @@ package com.spring.domain.quartz.entity;
import java.io.Serializable;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -2,11 +2,10 @@ package com.spring.domain.quartz.entity;
import java.io.Serializable;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -2,11 +2,10 @@ package com.spring.domain.quartz.entity;
import java.io.Serializable;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -2,15 +2,14 @@ package com.spring.domain.quartz.entity;
import java.io.Serializable;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinColumns;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.ForeignKey;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.ManyToOne;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -3,15 +3,14 @@ package com.spring.domain.quartz.entity;
import java.io.Serializable;
import java.math.BigDecimal;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinColumns;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.ForeignKey;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.ManyToOne;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -2,15 +2,14 @@ package com.spring.domain.quartz.entity;
import java.io.Serializable;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinColumns;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.ForeignKey;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.ManyToOne;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -1,5 +1,7 @@
package com.spring.domain.user.api;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
@@ -11,7 +13,6 @@ import com.spring.domain.user.dto.SignInRequest;
import com.spring.domain.user.service.AuthService;
import com.spring.infra.security.jwt.JwtTokenService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
@RestController

View File

@@ -2,15 +2,16 @@ package com.spring.domain.user.entity;
import java.util.Set;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import lombok.Getter;
@Entity

View File

@@ -2,15 +2,16 @@ package com.spring.domain.user.entity;
import java.util.Set;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import lombok.Getter;
@Entity

View File

@@ -2,14 +2,15 @@ package com.spring.domain.user.entity;
import java.io.Serializable;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapsId;
import jakarta.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.MapsId;
import javax.persistence.Table;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -0,0 +1,6 @@
package com.spring.infra.batch;
public class BatchConfig {
}

View File

@@ -11,6 +11,18 @@ import org.springframework.context.annotation.Primary;
import com.zaxxer.hikari.HikariDataSource;
/**
* 주 데이터 소스 설정을 위한 구성 클래스입니다.
*
* <p>이 클래스는 애플리케이션의 주 데이터 소스를 설정하며, 다음과 같은 기능을 제공합니다:</p>
* <ul>
* <li>주 데이터 소스 속성 설정</li>
* <li>HikariCP를 사용한 주 데이터 소스 생성</li>
* </ul>
*
* @author mindol
* @version 1.0
*/
@Configuration
public class PrimaryDataSourceConfig {
@@ -19,6 +31,14 @@ public class PrimaryDataSourceConfig {
private static final String DATASOURCE_PROPERTIES = "primaryDataSourceProperties";
private static final String DATASOURCE_PROPERTIES_PREFIX = "spring.datasource.primary";
/**
* 주 데이터 소스의 속성을 설정합니다.
*
* <p>이 메소드는 'spring.datasource.primary' 접두사로 시작하는 설정을 읽어
* DataSourceProperties 객체를 생성합니다.</p>
*
* @return 설정된 DataSourceProperties 객체
*/
@Primary
@Bean(name = DATASOURCE_PROPERTIES)
@ConfigurationProperties(prefix = DATASOURCE_PROPERTIES_PREFIX)
@@ -26,6 +46,14 @@ public class PrimaryDataSourceConfig {
return new DataSourceProperties();
}
/**
* 주 데이터 소스를 생성합니다.
*
* <p>이 메소드는 설정된 DataSourceProperties를 사용하여 HikariCP 데이터 소스를 생성합니다.</p>
*
* @param properties 데이터 소스 속성
* @return 생성된 DataSource 객체
*/
@Primary
@Bean(name = DATASOURCE)
DataSource dataSource(@Qualifier(DATASOURCE_PROPERTIES) DataSourceProperties properties) {

View File

@@ -10,6 +10,18 @@ import org.springframework.context.annotation.Configuration;
import com.zaxxer.hikari.HikariDataSource;
/**
* 보조 데이터 소스 설정을 위한 구성 클래스입니다.
*
* <p>이 클래스는 애플리케이션의 보조 데이터 소스를 설정하며, 다음과 같은 기능을 제공합니다:</p>
* <ul>
* <li>보조 데이터 소스 속성 설정</li>
* <li>HikariCP를 사용한 보조 데이터 소스 생성</li>
* </ul>
*
* @author mindol
* @version 1.0
*/
@Configuration
public class SecondaryDataSourceConfig {
@@ -18,12 +30,28 @@ public class SecondaryDataSourceConfig {
private static final String DATASOURCE_PROPERTIES = "secondaryDataSourceProperties";
private static final String DATASOURCE_PROPERTIES_PREFIX = "spring.datasource.secondary";
/**
* 보조 데이터 소스의 속성을 설정합니다.
*
* <p>이 메소드는 'spring.datasource.secondary' 접두사로 시작하는 설정을 읽어
* DataSourceProperties 객체를 생성합니다.</p>
*
* @return 설정된 DataSourceProperties 객체
*/
@Bean(name = DATASOURCE_PROPERTIES)
@ConfigurationProperties(prefix = DATASOURCE_PROPERTIES_PREFIX)
DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
/**
* 보조 데이터 소스를 생성합니다.
*
* <p>이 메소드는 설정된 DataSourceProperties를 사용하여 HikariCP 데이터 소스를 생성합니다.</p>
*
* @param properties 데이터 소스 속성
* @return 생성된 DataSource 객체
*/
@Bean(name = DATASOURCE)
DataSource dataSource(@Qualifier(DATASOURCE_PROPERTIES) DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();

View File

@@ -1,5 +1,6 @@
package com.spring.infra.db.orm.jpa;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -25,9 +26,21 @@ import com.spring.infra.db.PrimaryDataSourceConfig;
import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector;
import com.spring.infra.db.orm.jpa.util.EntityScanner;
import jakarta.persistence.EntityManagerFactory;
import lombok.RequiredArgsConstructor;
/**
* 주 데이터베이스에 대한 JPA 설정을 담당하는 구성 클래스입니다.
*
* <p>이 클래스는 다음과 같은 주요 기능을 제공합니다:</p>
* <ul>
* <li>주 데이터베이스용 EntityManagerFactory 설정</li>
* <li>주 데이터베이스용 TransactionManager 설정</li>
* <li>JPA 리포지토리 활성화 및 필터링</li>
* </ul>
*
* @author mindol
* @version 1.0
*/
@Configuration
@EnableJpaRepositories(
basePackages = PrimaryJpaConfig.BASE_PACKAGE,
@@ -46,6 +59,13 @@ public class PrimaryJpaConfig {
private final JpaProperties jpaProperties;
private final HibernateProperties hibernateProperties;
/**
* 주 데이터베이스용 EntityManagerFactory를 생성합니다.
*
* @param builder EntityManagerFactory 빌더
* @param dataSource 주 데이터 소스
* @return 구성된 LocalContainerEntityManagerFactoryBean
*/
@Primary
@Bean(name = ENTITY_MANAGER_FACTORY)
LocalContainerEntityManagerFactoryBean entityManagerFactory(
@@ -62,12 +82,23 @@ public class PrimaryJpaConfig {
.build();
}
/**
* 주 데이터베이스용 TransactionManager를 생성합니다.
*
* @param factory 주 EntityManagerFactory
* @return 구성된 PlatformTransactionManager
*/
@Primary
@Bean(TRANSACTION_MANAGER)
PlatformTransactionManager transactionManager(@Qualifier(ENTITY_MANAGER_FACTORY) EntityManagerFactory factory) {
return new JpaTransactionManager(factory);
}
/**
* 데이터베이스 선택을 위한 커스텀 필터 클래스입니다.
*
* <p>이 필터는 DatabaseSelector 어노테이션이 없는 엔티티만 선택합니다.</p>
*/
public static class DatabaseFilter implements TypeFilter {
@Override
public boolean match(@NonNull MetadataReader reader, @NonNull MetadataReaderFactory factory) {

View File

@@ -2,6 +2,7 @@ package com.spring.infra.db.orm.jpa;
import java.util.Optional;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -26,9 +27,21 @@ import com.spring.infra.db.SecondaryDataSourceConfig;
import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector;
import com.spring.infra.db.orm.jpa.util.EntityScanner;
import jakarta.persistence.EntityManagerFactory;
import lombok.RequiredArgsConstructor;
/**
* 보조 데이터베이스에 대한 JPA 설정을 담당하는 구성 클래스입니다.
*
* <p>이 클래스는 다음과 같은 주요 기능을 제공합니다:</p>
* <ul>
* <li>보조 데이터베이스용 EntityManagerFactory 설정</li>
* <li>보조 데이터베이스용 TransactionManager 설정</li>
* <li>JPA 리포지토리 활성화 및 필터링</li>
* </ul>
*
* @author mindol
* @version 1.0
*/
@Configuration
@EnableJpaRepositories(
basePackages = SecondaryJpaConfig.BASE_PACKAGE,
@@ -47,6 +60,13 @@ public class SecondaryJpaConfig {
private final JpaProperties jpaProperties;
private final HibernateProperties hibernateProperties;
/**
* 보조 데이터베이스용 EntityManagerFactory를 생성합니다.
*
* @param builder EntityManagerFactory 빌더
* @param dataSource 보조 데이터 소스
* @return 구성된 LocalContainerEntityManagerFactoryBean
*/
@Bean(name = ENTITY_MANAGER_FACTORY)
LocalContainerEntityManagerFactoryBean entityManagerFactory(
EntityManagerFactoryBuilder builder,
@@ -62,11 +82,22 @@ public class SecondaryJpaConfig {
.build();
}
/**
* 보조 데이터베이스용 TransactionManager를 생성합니다.
*
* @param factory 보조 EntityManagerFactory
* @return 구성된 PlatformTransactionManager
*/
@Bean(TRANSACTION_MANAGER)
PlatformTransactionManager transactionManager(@Qualifier(ENTITY_MANAGER_FACTORY) EntityManagerFactory factory) {
return new JpaTransactionManager(factory);
}
/**
* 데이터베이스 선택을 위한 커스텀 필터 클래스입니다.
*
* <p>이 필터는 DatabaseSelector 어노테이션이 있고, 그 값이 보조 데이터베이스와 일치하는 엔티티만 선택합니다.</p>
*/
public static class DatabaseFilter implements TypeFilter {
@Override
public boolean match(@NonNull MetadataReader reader, @NonNull MetadataReaderFactory factory) {

View File

@@ -5,8 +5,34 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 엔티티 클래스가 사용할 데이터베이스를 지정하는 어노테이션입니다.
*
* <p>이 어노테이션은 JPA 엔티티 클래스에 적용되며, 해당 엔티티가 어떤 데이터베이스에서
* 사용될지를 지정합니다. 주로 다중 데이터베이스 환경에서 엔티티를 특정 데이터베이스에
* 매핑하는 데 사용됩니다.</p>
*
* <p>사용 예:</p>
* <pre>
* {@code
* @Entity
* @DatabaseSelector("secondary")
* public class MyEntity {
* // 엔티티 필드 및 메소드
* }
* }
* </pre>
*
* @author mindol
* @version 1.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DatabaseSelector {
/**
* 엔티티가 사용할 데이터베이스의 이름을 지정합니다.
*
* @return 데이터베이스 이름
*/
String value();
}

View File

@@ -1,5 +1,7 @@
package com.spring.infra.db.orm.jpa.util;
import javax.persistence.Entity;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.classreading.MetadataReader;
@@ -8,17 +10,38 @@ import org.springframework.util.StringUtils;
import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector;
import jakarta.persistence.Entity;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* JPA 엔티티 클래스를 스캔하는 유틸리티 클래스입니다.
*
* <p>이 클래스는 지정된 패키지 내의 엔티티 클래스를 스캔하고,
* 선택적으로 특정 데이터베이스에 해당하는 엔티티만 필터링할 수 있습니다.</p>
*
* @author mindol
* @version 1.0
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class EntityScanner {
/**
* 지정된 기본 패키지 내의 모든 엔티티를 스캔합니다.
*
* @param basePackage 스캔할 기본 패키지
* @return 스캔된 엔티티 클래스의 패키지 이름 배열
*/
public static String[] scanEntities(String basePackage) {
return scanEntities(basePackage, null);
}
/**
* 지정된 기본 패키지 내의 엔티티를 스캔하고, 선택적으로 특정 데이터베이스에 해당하는 엔티티만 필터링합니다.
*
* @param basePackage 스캔할 기본 패키지
* @param dbName 필터링할 데이터베이스 이름 (null이면 모든 엔티티 반환)
* @return 스캔된 엔티티 클래스의 패키지 이름 배열
*/
public static String[] scanEntities(String basePackage, String dbName) {
var scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter((MetadataReader reader, MetadataReaderFactory factory) -> {

View File

@@ -16,6 +16,19 @@ import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.spring.infra.db.PrimaryDataSourceConfig;
import com.spring.infra.db.orm.mybatis.annotation.PrimaryMapper;
/**
* 주 데이터베이스에 대한 MyBatis 설정을 담당하는 구성 클래스입니다.
*
* <p>이 클래스는 다음과 같은 주요 기능을 제공합니다:</p>
* <ul>
* <li>주 데이터베이스용 SqlSessionFactory 설정</li>
* <li>MyBatis 매퍼 스캔 설정</li>
* <li>MyBatis 관련 설정 (카멜 케이스 변환, null 처리 등)</li>
* </ul>
*
* @author mindol
* @version 1.0
*/
@Configuration
@MapperScan(
basePackages = {PrimaryMybatisConfig.BASE_PACKAGE},
@@ -29,6 +42,13 @@ public class PrimaryMybatisConfig {
private static final String MAPPER_RESOURCES = "classpath:mapper/**/*.xml";
private static final String SESSION_FACTORY = "primarySqlSessionFactory";
/**
* 주 데이터베이스용 SqlSessionFactory를 생성합니다.
*
* @param dataSource 주 데이터 소스
* @return 구성된 SqlSessionFactory 객체
* @throws Exception SqlSessionFactory 생성 중 발생할 수 있는 예외
*/
@Primary
@Bean(name = SESSION_FACTORY)
SqlSessionFactory sqlSessionFactory(@Qualifier(PrimaryDataSourceConfig.DATASOURCE) DataSource dataSource) throws Exception {
@@ -41,6 +61,11 @@ public class PrimaryMybatisConfig {
return sessionFactory.getObject();
}
/**
* MyBatis 설정을 구성합니다.
*
* @return 구성된 MyBatis Configuration 객체
*/
private org.apache.ibatis.session.Configuration mybatisConfiguration() {
var configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);

View File

@@ -15,6 +15,19 @@ import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.spring.infra.db.SecondaryDataSourceConfig;
import com.spring.infra.db.orm.mybatis.annotation.SecondaryMapper;
/**
* 보조 데이터베이스에 대한 MyBatis 설정을 담당하는 구성 클래스입니다.
*
* <p>이 클래스는 다음과 같은 주요 기능을 제공합니다:</p>
* <ul>
* <li>보조 데이터베이스용 SqlSessionFactory 설정</li>
* <li>MyBatis 매퍼 스캔 설정</li>
* <li>MyBatis 관련 설정 (카멜 케이스 변환, null 처리 등)</li>
* </ul>
*
* @author mindol
* @version 1.0
*/
@Configuration
@MapperScan(
basePackages = {SecondaryMybatisConfig.BASE_PACKAGE},
@@ -28,6 +41,13 @@ public class SecondaryMybatisConfig {
private static final String MAPPER_RESOURCES = "classpath:mapper/**/*.xml";
private static final String SESSION_FACTORY = "secondarySqlSessionFactory";
/**
* 보조 데이터베이스용 SqlSessionFactory를 생성합니다.
*
* @param dataSource 보조 데이터 소스
* @return 구성된 SqlSessionFactory 객체
* @throws Exception SqlSessionFactory 생성 중 발생할 수 있는 예외
*/
@Bean(name = SESSION_FACTORY)
SqlSessionFactory sqlSessionFactory(@Qualifier(SecondaryDataSourceConfig.DATASOURCE) DataSource dataSource) throws Exception {
var sessionFactory = new SqlSessionFactoryBean();
@@ -39,6 +59,11 @@ public class SecondaryMybatisConfig {
return sessionFactory.getObject();
}
/**
* MyBatis 설정을 구성합니다.
*
* @return 구성된 MyBatis Configuration 객체
*/
private org.apache.ibatis.session.Configuration mybatisConfiguration() {
var configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);

View File

@@ -5,6 +5,26 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 주 데이터베이스용 MyBatis 매퍼를 지정하는 어노테이션입니다.
*
* <p>이 어노테이션은 인터페이스 레벨에서 사용되며, 해당 인터페이스가 주 데이터베이스와
* 연결된 MyBatis 매퍼임을 나타냅니다. 주로 다중 데이터베이스 환경에서 매퍼 인터페이스를
* 특정 데이터베이스에 연결하는 데 사용됩니다.</p>
*
* <p>사용 예:</p>
* <pre>
* {@code
* @PrimaryMapper
* public interface UserMapper {
* // 매퍼 메소드 정의
* }
* }
* </pre>
*
* @author mindol
* @version 1.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrimaryMapper {

View File

@@ -5,6 +5,26 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 보조 데이터베이스용 MyBatis 매퍼를 지정하는 어노테이션입니다.
*
* <p>이 어노테이션은 인터페이스 레벨에서 사용되며, 해당 인터페이스가 보조 데이터베이스와
* 연결된 MyBatis 매퍼임을 나타냅니다. 주로 다중 데이터베이스 환경에서 매퍼 인터페이스를
* 특정 데이터베이스에 연결하는 데 사용됩니다.</p>
*
* <p>사용 예:</p>
* <pre>
* {@code
* @SecondaryMapper
* public interface UserMapper {
* // 매퍼 메소드 정의
* }
* }
* </pre>
*
* @author mindol
* @version 1.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SecondaryMapper {

View File

@@ -14,6 +14,19 @@ import org.springframework.transaction.PlatformTransactionManager;
import lombok.RequiredArgsConstructor;
/**
* Quartz 스케줄러 설정을 위한 구성 클래스입니다.
*
* <p>이 클래스는 Quartz 스케줄러의 기본 설정을 제공하며, 다음과 같은 기능을 수행합니다:</p>
* <ul>
* <li>JobFactory 빈 생성 및 설정</li>
* <li>SchedulerFactoryBean 생성 및 설정</li>
* <li>Quartz 작업에 대한 의존성 주입 지원</li>
* </ul>
*
* @author mindol
* @version 1.0
*/
@Configuration
@RequiredArgsConstructor
public class QuartzConfig {
@@ -23,10 +36,13 @@ public class QuartzConfig {
private final PlatformTransactionManager transactionManager;
/**
* Quartz Schedule Job 에 의존성 주입
* Quartz Schedule Job에 의존성 주입하기 위한 JobFactory를 생성합니다.
*
* @param beanFactory application context beanFactory
* @return the job factory
* <p>이 메소드는 Spring의 AutowireCapableBeanFactory를 사용하여
* Quartz Job 인스턴스에 자동으로 의존성을 주입합니다.</p>
*
* @param beanFactory Spring의 AutowireCapableBeanFactory
* @return 생성된 JobFactory 인스턴스
*/
@Bean
JobFactory jobFactory(AutowireCapableBeanFactory beanFactory) {
@@ -38,10 +54,19 @@ public class QuartzConfig {
}
/**
* Scheduler 전체를 관리하는 Manager.
* Quartz Scheduler를 생성하고 설정하는 SchedulerFactoryBean을 구성합니다.
*
* <p>이 메소드는 다음과 같은 설정을 수행합니다:</p>
* <ul>
* <li>스케줄러 이름 설정</li>
* <li>Quartz 속성 설정</li>
* <li>데이터 소스 및 트랜잭션 매니저 설정</li>
* <li>JobFactory 설정</li>
* <li>자동 시작 및 종료 시 작업 완료 대기 설정</li>
* </ul>
*
* @param jobFactory job factory
* @return the scheduler factory bean
* @param jobFactory 사용할 JobFactory 인스턴스
* @return 구성된 SchedulerFactoryBean 인스턴스
*/
@Bean
SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory) {

View File

@@ -5,9 +5,50 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Quartz 스케줄러를 통해 실행될 작업을 정의하는 어노테이션입니다.
*
* <p>이 어노테이션은 메소드 레벨에서 사용되며, 해당 메소드를 Quartz 작업으로 등록합니다.</p>
*
* <p>주요 기능:</p>
* <ul>
* <li>작업의 이름 지정</li>
* <li>작업의 실행 주기를 Cron 표현식으로 정의</li>
* </ul>
*
* <p>사용 예:</p>
* <pre>
* {@code
* @QuartzJob(name = "myJob", cronExpression = "0 0 12 * * ?")
* public void myScheduledJob() {
* // 작업 내용
* }
* }
* </pre>
*
* @author mindol
* @version 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface QuartzJob {
/**
* Quartz 작업의 그룹을 지정합니다.
*
* @return 그룹의 이름
*/
String group() default "DEFAULT";
/**
* Quartz 작업의 이름을 지정합니다.
*
* @return 작업의 이름
*/
String name();
/**
* 작업의 실행 주기를 Cron 표현식으로 지정합니다.
*
* @return Cron 표현식
*/
String cronExpression();
}

View File

@@ -13,19 +13,49 @@ import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
/**
* Quartz 작업을 실행하는 Spring Batch Job 실행기 클래스입니다.
*
* <p>이 클래스는 Quartz 스케줄러에 의해 호출되며, 지정된 Spring Batch Job을 실행합니다.</p>
*
* <p>주요 기능:</p>
* <ul>
* <li>JobRegistry에서 지정된 이름의 Job을 조회</li>
* <li>Job 실행을 위한 JobParameters 생성</li>
* <li>JobLauncher를 통한 Job 실행</li>
* </ul>
*
* @author mindol
* @version 1.0
* @see QuartzJobBean
* @see JobLauncher
* @see JobRegistry
*/
@Component
@RequiredArgsConstructor
public class QuartzJobLauncher extends QuartzJobBean {
private final JobLauncher jobLauncher;
private final JobRegistry jobRegistry;
private String jobName;
public void setJobName(String jobName) {
this.jobName = jobName;
}
/**
* Quartz 스케줄러에 의해 호출되는 메소드로, 실제 Job을 실행합니다.
*
* <p>이 메소드는 다음과 같은 작업을 수행합니다:</p>
* <ol>
* <li>JobRegistry에서 지정된 이름의 Job을 조회</li>
* <li>현재 시간을 기반으로 한 고유한 JobParameters 생성</li>
* <li>JobLauncher를 사용하여 Job 실행</li>
* </ol>
*
* @param context Quartz JobExecutionContext 객체
*/
@Override
protected void executeInternal(@NonNull JobExecutionContext context) throws JobExecutionException {
try {

View File

@@ -19,6 +19,29 @@ import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
/**
* Quartz 작업 등록기 클래스입니다.
*
* <p>이 클래스는 애플리케이션 컨텍스트가 리프레시될 때 실행되며,
* {@link QuartzJob} 어노테이션이 붙은 모든 메소드를 찾아 Quartz 스케줄러에 등록합니다.</p>
*
* <p>주요 기능:</p>
* <ul>
* <li>애플리케이션 컨텍스트 내의 모든 빈을 검사</li>
* <li>QuartzJob 어노테이션이 붙은 메소드 식별</li>
* <li>식별된 메소드를 Quartz 작업으로 등록</li>
* <li>각 작업에 대한 JobDetail 및 Trigger 생성</li>
* </ul>
*
* <p>이 클래스는 {@link ApplicationListener}를 구현하여
* {@link ContextRefreshedEvent}가 발생할 때 자동으로 실행됩니다.</p>
*
* @author mindol
* @version 1.0
* @see QuartzJob
* @see ApplicationListener
* @see ContextRefreshedEvent
*/
@Component
@RequiredArgsConstructor
public class QuartzJobRegistrar implements ApplicationListener<ContextRefreshedEvent> {
@@ -26,6 +49,12 @@ public class QuartzJobRegistrar implements ApplicationListener<ContextRefreshedE
private final Scheduler scheduler;
private final ApplicationContext applicationContext;
/**
* 애플리케이션 컨텍스트가 리프레시될 때 호출되는 메소드.
* 모든 빈을 순회하며 QuartzJob 어노테이션이 붙은 메소드를 찾아 Quartz 작업으로 등록합니다.
*
* @param event ContextRefreshedEvent 객체
*/
@Override
public void onApplicationEvent(@NonNull ContextRefreshedEvent event) {
for (String beanName : applicationContext.getBeanDefinitionNames()) {
@@ -40,31 +69,50 @@ public class QuartzJobRegistrar implements ApplicationListener<ContextRefreshedE
}
}
/**
* QuartzJob 어노테이션 정보를 바탕으로 Quartz 작업을 등록합니다.
*
* @param quartzJobAnnotation QuartzJob 어노테이션 객체
* @param jobName 등록할 작업의 이름
*/
private void registerQuartzJob(QuartzJob quartzJobAnnotation, String jobName) {
try {
JobDetail jobDetail = createJobDetail(quartzJobAnnotation, jobName);
Trigger trigger = createTrigger(quartzJobAnnotation, jobDetail);
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
e.printStackTrace();
throw new IllegalStateException("Error scheduling Quartz job", e);
}
}
/**
* QuartzJob 어노테이션 정보를 바탕으로 JobDetail 객체를 생성합니다.
*
* @param quartzJobAnnotation QuartzJob 어노테이션 객체
* @param jobName 작업의 이름
* @return 생성된 JobDetail 객체
*/
private JobDetail createJobDetail(QuartzJob quartzJobAnnotation, String jobName) {
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("jobName", jobName);
return JobBuilder.newJob(QuartzJobLauncher.class)
.withIdentity(quartzJobAnnotation.name())
.withIdentity(quartzJobAnnotation.name(), quartzJobAnnotation.group())
.setJobData(jobDataMap)
.storeDurably()
.build();
}
/**
* QuartzJob 어노테이션 정보를 바탕으로 Trigger 객체를 생성합니다.
*
* @param quartzJobAnnotation QuartzJob 어노테이션 객체
* @param jobDetail 연관된 JobDetail 객체
* @return 생성된 Trigger 객체
*/
private Trigger createTrigger(QuartzJob quartzJobAnnotation, JobDetail jobDetail) {
return TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity(quartzJobAnnotation.name() + "Trigger")
.withIdentity(quartzJobAnnotation.name() + "Trigger", quartzJobAnnotation.group())
.withSchedule(CronScheduleBuilder.cronSchedule(quartzJobAnnotation.cronExpression()))
.build();
}

View File

@@ -11,6 +11,21 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* Quartz 스케줄러의 설정 속성을 관리하는 클래스입니다.
*
* <p>이 클래스는 application.properties 또는 application.yml 파일에서
* "spring.quartz.properties.org.quartz" 접두사로 시작하는 설정을 읽어옵니다.</p>
*
* <p>주요 기능:</p>
* <ul>
* <li>JobStore, Scheduler, ThreadPool 관련 설정 관리</li>
* <li>설정값을 Properties 객체로 변환</li>
* </ul>
*
* @author mindol
* @version 1.0
*/
@Slf4j
@Getter
@RequiredArgsConstructor
@@ -45,18 +60,38 @@ public class QuartzProperties {
private final int threadPriority;
}
/**
* 현재 객체의 모든 속성을 Properties 객체로 변환합니다.
*
* @return 변환된 Properties 객체
*/
public Properties toProperties() {
Properties properties = new Properties();
addProperties(PREFIX, this, properties);
return properties;
}
/**
* 주어진 객체의 모든 필드를 재귀적으로 순회하며 Properties 객체에 추가합니다.
*
* @param prefix 속성 키의 접두사
* @param object 속성을 추출할 객체
* @param properties 속성을 저장할 Properties 객체
*/
private void addProperties(String prefix, Object object, Properties properties) {
Arrays.stream(object.getClass().getDeclaredFields())
.filter(field -> !Modifier.isStatic(field.getModifiers()))
.forEach(field -> setProperties(prefix, object, properties, field));
}
/**
* 주어진 필드의 값을 Properties 객체에 추가합니다.
*
* @param prefix 속성 키의 접두사
* @param object 속성을 추출할 객체
* @param properties 속성을 저장할 Properties 객체
* @param field 처리할 필드
*/
private void setProperties(String prefix, Object object, Properties properties, Field field) {
try {
Object value = field.get(object);
@@ -74,6 +109,12 @@ public class QuartzProperties {
}
}
/**
* 주어진 타입이 단순 타입(primitive, String, Number, Boolean, Character)인지 확인합니다.
*
* @param type 확인할 클래스 타입
* @return 단순 타입이면 true, 그렇지 않으면 false
*/
private boolean isSimpleType(Class<?> type) {
return type.isPrimitive()
|| String.class == type

View File

@@ -23,16 +23,30 @@ import com.spring.infra.security.handler.JwtAccessDeniedHandler;
import com.spring.infra.security.handler.JwtAuthenticationEntryPoint;
import com.spring.infra.security.jwt.JwtTokenService;
import lombok.RequiredArgsConstructor;
/**
* 애플리케이션의 보안 설정을 담당하는 구성 클래스입니다.
*
* <p>이 클래스는 Spring Security를 구성하고 JWT 기반의 인증을 설정합니다.</p>
*
* @author mindol
* @version 1.0
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private static final String[] PERMITTED_URI = {"/favicon.ico", "/api/auth/**", "/signIn", "/h2-console/**"};
/**
* Spring Security의 필터 체인을 구성합니다.
*
* @param http HttpSecurity 객체
* @param tokenService JWT 토큰 서비스
* @param authenticationEntryPoint JWT 인증 진입점
* @param accessDeniedHandler JWT 접근 거부 핸들러
* @return 구성된 SecurityFilterChain
*/
@Bean
SecurityFilterChain securityFilterChain(
HttpSecurity http,
@@ -46,7 +60,7 @@ public class SecurityConfig {
.httpBasic(HttpBasicConfigurer::disable)
.formLogin(FormLoginConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers(PERMITTED_URI).permitAll()
.antMatchers(PERMITTED_URI).permitAll()
.anyRequest().authenticated()
)
.logout(logout -> logout
@@ -63,11 +77,21 @@ public class SecurityConfig {
return http.build();
}
/**
* 특정 요청에 대해 보안 검사를 무시하도록 설정합니다.
*
* @return WebSecurityCustomizer 객체
*/
@Bean
WebSecurityCustomizer ignoringCustomizer() {
return web -> web.ignoring().requestMatchers("/h2-console/**");
return web -> web.ignoring().antMatchers("/h2-console/**");
}
/**
* 비밀번호 인코더를 구성합니다.
*
* @return PasswordEncoder 객체
*/
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();

View File

@@ -12,16 +12,34 @@ import com.spring.domain.user.entity.AppUser;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* Spring Security의 UserDetails 인터페이스를 구현한 사용자 주체(Principal) 클래스입니다.
* <p>애플리케이션의 사용자 정보를 Spring Security에서 사용할 수 있는 형태로 변환합니다.</p>
*
* @author mindol
* @version 1.0
*/
@Getter
@RequiredArgsConstructor
public final class UserPrincipal implements UserDetails {
private final AppUser appUser;
private final transient AppUser appUser;
/**
* AppUser 객체로부터 UserPrincipal 객체를 생성합니다.
*
* @param appUser 변환할 AppUser 객체
* @return 생성된 UserPrincipal 객체
*/
public static UserPrincipal valueOf(AppUser appUser) {
return new UserPrincipal(appUser);
}
/**
* 사용자의 권한 목록을 반환합니다.
*
* @return 사용자의 GrantedAuthority 컬렉션
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return appUser.getAppUserRoleMap().stream()
@@ -30,31 +48,62 @@ public final class UserPrincipal implements UserDetails {
.collect(Collectors.toList());
}
/**
* 계정이 만료되지 않았는지 확인합니다.
*
* @return 계정 만료 여부 (true: 만료되지 않음)
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 계정이 잠기지 않았는지 확인합니다.
*
* @return 계정 잠금 여부 (true: 잠기지 않음)
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 자격 증명(비밀번호)이 만료되지 않았는지 확인합니다.
*
* @return 자격 증명 만료 여부 (true: 만료되지 않음)
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 계정이 활성화되어 있는지 확인합니다.
*
* @return 계정 활성화 여부 (true: 활성화됨)
*/
@Override
public boolean isEnabled() {
return true;
}
/**
* 사용자의 비밀번호를 반환합니다.
* 이 구현에서는 null을 반환합니다.
*
* @return 사용자 비밀번호 (null)
*/
@Override
public String getPassword() {
return null;
}
/**
* 사용자의 로그인 ID를 반환합니다.
*
* @return 사용자 로그인 ID
*/
@Override
public String getUsername() {
return appUser.getLoginId();

View File

@@ -3,6 +3,11 @@ package com.spring.infra.security.filter;
import java.io.IOException;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -11,21 +16,39 @@ import org.springframework.web.filter.OncePerRequestFilter;
import com.spring.infra.security.jwt.JwtTokenRule;
import com.spring.infra.security.jwt.JwtTokenService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
/**
* JWT 인증을 처리하는 필터 클래스입니다.
*
* <p>이 필터는 요청마다 JWT 토큰을 검증하고, 필요한 경우 토큰을 갱신합니다.</p>
*
* @author mindol
* @version 1.0
*/
@RequiredArgsConstructor
public final class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenService jwtTokenService;
private final List<String> permitAllUrls;
/**
* 요청마다 실행되는 필터 메소드입니다.
*
* <p>JWT 토큰을 검증하고, 필요한 경우 토큰을 갱신합니다.</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 {
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
String requestURI = request.getRequestURI();
if (permitAllUrls.stream().anyMatch(requestURI::startsWith)) {
@@ -51,11 +74,15 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
}
jwtTokenService.deleteCookie(response);
}
private void setAuthenticationToContext(String accessToken) {
Authentication authentication = jwtTokenService.getAuthentication(accessToken);
/**
* 인증 정보를 SecurityContext에 설정합니다.
*
* @param token 토큰
*/
private void setAuthenticationToContext(String token) {
Authentication authentication = jwtTokenService.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

View File

@@ -2,20 +2,38 @@ package com.spring.infra.security.handler;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* 유저 정보는 있으나 자원에 접근할 수 있는 권한이 없는 경우 : SC_FORBIDDEN (403) 응답
* JWT 인증에서 접근 거부 상황을 처리하는 핸들러 클래스입니다.
*
* <p>이 클래스는 사용자가 인증은 되었지만 특정 리소스에 대한 접근 권한이 없는 경우를 처리합니다.
* 이런 경우 SC_FORBIDDEN (403) 응답을 반환합니다.</p>
*
* @author mindol
* @version 1.0
*/
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
/**
* 접근 거부 상황을 처리합니다.
*
* <p>사용자가 접근 권한이 없는 리소스에 접근을 시도할 때 호출됩니다.
* 이 메소드는 SC_FORBIDDEN (403) 상태 코드를 응답으로 전송합니다.</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 {

View File

@@ -2,20 +2,38 @@ package com.spring.infra.security.handler;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* 유저 정보 없이 접근한 경우 : SC_UNAUTHORIZED (401) 응답
* JWT 인증에서 인증되지 않은 접근을 처리하는 진입점 클래스입니다.
*
* <p>이 클래스는 사용자가 인증되지 않은 상태에서 보호된 리소스에 접근을 시도할 때 호출됩니다.
* 이런 경우 SC_UNAUTHORIZED (401) 응답을 반환합니다.</p>
*
* @author mindol
* @version 1.0
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
/**
* 인증되지 않은 접근을 처리합니다.
*
* <p>인증되지 않은 사용자가 보호된 리소스에 접근을 시도할 때 호출됩니다.
* 이 메소드는 SC_UNAUTHORIZED (401) 상태 코드를 응답으로 전송합니다.</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 {

View File

@@ -5,6 +5,12 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* JWT(JSON Web Token) 관련 설정 속성을 관리하는 클래스입니다.
* 'jwt' 접두사로 시작하는 설정 속성을 바인딩합니다.
* @author mindol
* @version 1.0
*/
@Getter
@ConfigurationProperties(prefix = "jwt")
@RequiredArgsConstructor

View File

@@ -17,12 +17,26 @@ import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
/**
* JWT 토큰을 생성하는 클래스입니다.
*
* <p>이 클래스는 액세스 토큰과 리프레시 토큰을 생성하는 기능을 제공합니다.</p>
*
* @author mindol
* @version 1.0
*/
@Component
@RequiredArgsConstructor
public class JwtTokenGenerator {
private final JwtProperties jwtProperties;
/**
* 액세스 토큰을 생성합니다.
*
* @param authentication 인증 정보
* @return 생성된 액세스 토큰
*/
public String generateAccessToken(Authentication authentication) {
return Jwts.builder()
.setHeader(createHeader())
@@ -33,6 +47,12 @@ public class JwtTokenGenerator {
.compact();
}
/**
* 리프레시 토큰을 생성합니다.
*
* @param authentication 인증 정보
* @return 생성된 리프레시 토큰
*/
public String generateRefreshToken(Authentication authentication) {
return Jwts.builder()
.setHeader(createHeader())
@@ -42,6 +62,11 @@ public class JwtTokenGenerator {
.compact();
}
/**
* JWT 헤더를 생성합니다.
*
* @return JWT 헤더 맵
*/
private Map<String, Object> createHeader() {
Map<String, Object> header = new HashMap<>();
header.put("typ", "JWT");
@@ -49,6 +74,12 @@ public class JwtTokenGenerator {
return header;
}
/**
* JWT 클레임을 생성합니다.
*
* @param authentication 인증 정보
* @return JWT 클레임 맵
*/
private Map<String, Object> createClaims(Authentication authentication) {
Map<String, Object> claims = new HashMap<>();
claims.put(JwtTokenRule.AUTHORITIES_KEY.getValue(), authentication.getAuthorities().stream()

View File

@@ -3,15 +3,46 @@ package com.spring.infra.security.jwt;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* JWT 토큰 관련 규칙을 정의하는 열거형입니다.
*
* <p>이 열거형은 JWT 토큰 처리에 필요한 다양한 상수 값들을 정의합니다.</p>
*
* @author mindol
* @version 1.0
*/
@RequiredArgsConstructor
@Getter
public enum JwtTokenRule {
/**
* JWT 토큰 발급 시 사용되는 HTTP 헤더 이름입니다.
*/
JWT_ISSUE_HEADER("Set-Cookie"),
/**
* JWT 토큰 해석 시 사용되는 HTTP 헤더 이름입니다.
*/
JWT_RESOLVE_HEADER("Cookie"),
/**
* 액세스 토큰의 접두사입니다.
*/
ACCESS_PREFIX("access"),
/**
* 리프레시 토큰의 접두사입니다.
*/
REFRESH_PREFIX("refresh"),
/**
* Bearer 인증 스키마의 접두사입니다.
*/
BEARER_PREFIX("Bearer "),
/**
* JWT 클레임에서 권한 정보를 나타내는 키입니다.
*/
AUTHORITIES_KEY("auth");
private final String value;

View File

@@ -2,6 +2,10 @@ package com.spring.infra.security.jwt;
import java.security.Key;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseCookie;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
@@ -11,10 +15,15 @@ import org.springframework.stereotype.Service;
import com.spring.infra.security.service.UserPrincipalService;
import io.jsonwebtoken.Jwts;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* JWT 토큰 관련 서비스를 제공하는 클래스입니다.
*
* <p>이 클래스는 JWT 토큰의 생성, 검증, 해석 등 다양한 토큰 관련 기능을 제공합니다.</p>
*
* @author mindol
* @version 1.0
*/
@Service
public class JwtTokenService {
@@ -41,6 +50,13 @@ public class JwtTokenService {
this.refreshExpiration = jwtProperties.getRefreshToken().getExpiration();
}
/**
* 액세스 토큰을 생성하고 응답 헤더에 설정합니다.
*
* @param response HTTP 응답 객체
* @param authentication 인증 정보
* @return 생성된 액세스 토큰
*/
public String generateAccessToken(HttpServletResponse response, Authentication authentication) {
String accessToken = jwtTokenGenerator.generateAccessToken(authentication);
ResponseCookie cookie = setTokenToCookie(JwtTokenRule.ACCESS_PREFIX.getValue(), accessToken, accessExpiration / 1000);
@@ -48,6 +64,13 @@ public class JwtTokenService {
return accessToken;
}
/**
* 리프레시 토큰을 생성하고 응답 헤더에 설정합니다.
*
* @param response HTTP 응답 객체
* @param authentication 인증 정보
* @return 생성된 리프레시 토큰
*/
public String generateRefreshToken(HttpServletResponse response, Authentication authentication) {
String refreshToken = jwtTokenGenerator.generateRefreshToken(authentication);
ResponseCookie cookie = setTokenToCookie(JwtTokenRule.REFRESH_PREFIX.getValue(), refreshToken, refreshExpiration / 1000);
@@ -55,6 +78,14 @@ public class JwtTokenService {
return refreshToken;
}
/**
* 토큰을 쿠키로 설정합니다.
*
* @param tokenPrefix 토큰 접두사
* @param token 토큰 값
* @param maxAgeSeconds 쿠키 유효 시간(초)
* @return 생성된 ResponseCookie 객체
*/
private ResponseCookie setTokenToCookie(String tokenPrefix, String token, long maxAgeSeconds) {
return ResponseCookie.from(tokenPrefix, token)
.path("/")
@@ -65,14 +96,34 @@ public class JwtTokenService {
.build();
}
/**
* 액세스 토큰의 유효성을 검증합니다.
*
* @param token 검증할 토큰
* @return 토큰의 유효성 여부
*/
public boolean validateAccessToken(String token) {
return jwtTokenUtil.getTokenStatus(token, accessSecretKey) == JwtTokenStatus.AUTHENTICATED;
}
/**
* 리프레시 토큰의 유효성을 검증합니다.
*
* @param token 검증할 토큰
* @return 토큰의 유효성 여부
*/
public boolean validateRefreshToken(String token) {
return jwtTokenUtil.getTokenStatus(token, refreshSecretKey) == JwtTokenStatus.AUTHENTICATED;
}
/**
* 쿠키에서 토큰을 추출합니다.
*
* @param request HTTP 요청 객체
* @param tokenPrefix 토큰 접두사
* @return 추출된 토큰
* @throws IllegalStateException 토큰이 없을 경우 발생
*/
public String resolveTokenFromCookie(HttpServletRequest request, JwtTokenRule tokenPrefix) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
@@ -81,11 +132,24 @@ public class JwtTokenService {
return jwtTokenUtil.resolveTokenFromCookie(cookies, tokenPrefix);
}
/**
* 토큰으로부터 인증 정보를 생성합니다.
*
* @param token JWT 토큰
* @return 생성된 Authentication 객체
*/
public Authentication getAuthentication(String token) {
UserDetails principal = userPrincipalService.loadUserByUsername(getUserPk(token, accessSecretKey));
return new UsernamePasswordAuthenticationToken(principal, "", principal.getAuthorities());
}
/**
* 토큰에서 사용자 식별자를 추출합니다.
*
* @param token JWT 토큰
* @param secretKey 비밀 키
* @return 추출된 사용자 식별자
*/
private String getUserPk(String token, Key secretKey) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
@@ -95,6 +159,11 @@ public class JwtTokenService {
.getSubject();
}
/**
* 액세스 토큰과 리프레시 토큰 쿠키를 삭제합니다.
*
* @param response HTTP 응답 객체
*/
public void deleteCookie(HttpServletResponse response) {
Cookie accessCookie = jwtTokenUtil.resetToken(JwtTokenRule.ACCESS_PREFIX);
Cookie refreshCookie = jwtTokenUtil.resetToken(JwtTokenRule.REFRESH_PREFIX);

View File

@@ -3,10 +3,29 @@ package com.spring.infra.security.jwt;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* JWT 토큰의 상태를 나타내는 열거형입니다.
*
* <p>이 열거형은 JWT 토큰의 유효성 검증 결과를 표현하는 데 사용됩니다.</p>
*
* @author mindol
* @version 1.0
*/
@RequiredArgsConstructor
@Getter
public enum JwtTokenStatus {
/**
* 토큰이 유효하고 인증된 상태를 나타냅니다.
*/
AUTHENTICATED,
/**
* 토큰이 만료된 상태를 나타냅니다.
*/
EXPIRED,
/**
* 토큰이 유효하지 않은 상태를 나타냅니다.
*/
INVALID
}

View File

@@ -5,19 +5,35 @@ import java.security.Key;
import java.util.Arrays;
import java.util.Base64;
import javax.servlet.http.Cookie;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.Cookie;
import lombok.extern.slf4j.Slf4j;
/**
* JWT 토큰 관련 유틸리티 기능을 제공하는 클래스입니다.
*
* <p>이 클래스는 JWT 토큰의 상태 확인, 쿠키에서 토큰 추출, 서명 키 생성 등의 기능을 제공합니다.</p>
*
* @author mindol
* @version 1.0
*/
@Slf4j
@Component
public class JwtTokenUtil {
/**
* JWT 토큰의 상태를 확인합니다.
*
* @param token 검증할 JWT 토큰
* @param secretKey 토큰 검증에 사용할 비밀 키
* @return 토큰의 상태 (AUTHENTICATED, EXPIRED, INVALID)
*/
public JwtTokenStatus getTokenStatus(String token, Key secretKey) {
try {
Jwts.parserBuilder()
@@ -34,6 +50,13 @@ public class JwtTokenUtil {
}
}
/**
* 쿠키에서 특정 접두사를 가진 토큰을 추출합니다.
*
* @param cookies 쿠키 배열
* @param tokenPrefix 토큰 접두사
* @return 추출된 토큰 값 (없으면 빈 문자열)
*/
public String resolveTokenFromCookie(Cookie[] cookies, JwtTokenRule tokenPrefix) {
return Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals(tokenPrefix.getValue()))
@@ -42,15 +65,33 @@ public class JwtTokenUtil {
.orElse("");
}
/**
* 주어진 비밀 키로 서명 키를 생성합니다.
*
* @param secretKey 비밀 키 문자열
* @return 생성된 서명 키
*/
public Key getSigningKey(String secretKey) {
String encodedKey = encodeToBase64(secretKey);
return Keys.hmacShaKeyFor(encodedKey.getBytes(StandardCharsets.UTF_8));
}
/**
* 문자열을 Base64로 인코딩합니다.
*
* @param secretKey 인코딩할 문자열
* @return Base64로 인코딩된 문자열
*/
private String encodeToBase64(String secretKey) {
return Base64.getEncoder().encodeToString(secretKey.getBytes());
}
/**
* 토큰을 리셋하는 쿠키를 생성합니다.
*
* @param tokenPrefix 토큰 접두사
* @return 생성된 쿠키 객체
*/
public Cookie resetToken(JwtTokenRule tokenPrefix) {
Cookie cookie = new Cookie(tokenPrefix.getValue(), null);
cookie.setMaxAge(0);

View File

@@ -44,6 +44,8 @@ spring:
show-sql: true
batch:
job:
enabled: true
jdbc:
initialize-schema: always