# Spring Batch & Quartz 프로젝트 개발자 가이드

## 목차
1. [프로젝트 개요](#프로젝트-개요)
2. [기술 스택](#기술-스택)
3. [프로젝트 구조](#프로젝트-구조)
4. [패키지별 상세 가이드](#패키지별-상세-가이드)
5. [배치 작업 구현 가이드](#배치-작업-구현-가이드)
6. [Spring Data JPA 사용 가이드](#spring-data-jpa-사용-가이드)
7. [MyBatis 사용 가이드](#MyBatis-사용-가이드)
8. [객체지향 개발 규칙 및 클린 코드 가이드](#객체지향-개발-규칙-및-클린-코드-가이드)
9. [스프링 시큐리티 가이드](#스프링-시큐리티-가이드)

## 프로젝트 개요 
이 프로젝트는 Spring Batch와 Quartz를 통합하여 구현된 배치 처리 시스템입니다. 주요 기능으로는 이메일 발송, 게시물 처리 등의 배치 작업을 포함하고 있습니다.

## 기술 스택
- Spring Boot
- Spring Security/JWT
- Spring Batch
- Quartz Scheduler
- Spring Cloud Openfeign
- Spring Data Jpa/Hibernate
- MyBatis
- Oracle Database
- Thymeleaf
- Javascript
- Lombok

## 프로젝트 구조

### 주요 패키지 구조
```
com.spring
├── common
│   ├── config
│   ├── converter
│   ├── error
│   ├── jpa
│   ├── properties
│   ├── support
│   ├── util
│   └── validation
├── domain
│   ├── email
│   │   ├── api
│   │   ├── batch
│   │   ├── dto
│   │   ├── entity
│   │   ├── error
│   │   ├── mapper
│   │   ├── repository
│   │   ├── service
│   │   └── support
│   └── ...
├── infra
│   ├── batch
│   ├── db
│   ├── feign
│   ├── quartz
│   └── security
└── web
    ├── constant
    ├── controller
    └── support    
```

## 패키지별 상세 가이드

### common 패키지
- **config**: 애플리케이션의 전반적인 설정 클래스들이 위치
  - Spring MVC의 웹 설정을 구성 등의 설정 포함
- **converter**: 엔티티와 DTO 간의 변환을 담당하는 컨버터 클래스들
- **error**: 공통 예외 처리 및 에러 응답 관련 클래스
- **jpa**: JPA 관련 공통 설정 및 유틸리티
- **properties**: 애플리케이션 프로퍼티 설정 클래스
- **support**: 공통 지원 클래스들 (REST API 공통응답설정 등)
- **util**: 공통 유틸리티 클래스들
- **validation**: 유효성 검증 관련 클래스들

### domain 패키지
각 도메인별로 다음과 같은 하위 패키지 구조를 가짐:
- **api**: 외부 API 통신 관련 클래스
- **batch**: 도메인별 배치 작업 관련 클래스
- **dto**: 데이터 전송 객체(API, Mybatis, Jpa 요청/응답 클래스 등)
- **entity**: JPA 엔티티 클래스
  - **설명**: Secondary 데이터베이스를 사용하는 경우, 해당 엔티티 클래스에 `@DatabaseSelector(SecondaryDataSourceConfig.DATABASE)` 어노테이션을 추가해야 합니다. 이 어노테이션은 해당 엔티티가 어떤 데이터베이스를 사용할지를 지정합니다.
- **error**: 도메인별 예외 처리
- **mapper**: MyBatis 매퍼 인터페이스(@NxmMapper annotation 추가 필수)
- **repository**: 데이터 접근 계층(Jpa Repository)
- **service**: 비즈니스 로직 처리
- **support**: 도메인별 지원 클래스

### infra 패키지
- **batch**: Spring Batch 관련 설정 및 공통 컴포넌트
- **db**: 데이터베이스 관련 설정 및 유틸리티
- **feign**: Feign Client 설정 및 구현체
- **quartz**: Quartz 스케줄러 관련 설정
- **security**: 보안 관련 설정 및 구현체
  - 인증, 인가, JWT 처리 등

### web 패키지
- **constant**: 웹 계층에서 사용되는 상수 정의
- **controller**: Thymeleaf 템플릿 페이지와 바인딩되는 컨트롤러
  - 배치 작업 모니터링 페이지
  - 스케줄러 관리 페이지
  - 배치 작업 실행 이력 조회 페이지 등
- **support**: 웹 계층 지원 클래스
  - 어드바이스

### 패키지 사용 가이드라인
1. 새로운 도메인 추가 시 domain 패키지 하위에 위 구조를 따라 생성
2. 공통 기능은 common 패키지에 구현
3. 인프라 관련 설정은 infra 패키지에 구현
4. API 엔드포인트는 각 도메인별 api 패키지에 구현

## 배치 작업 구현 가이드

![batch-quartz](batch-quartz.png)

## Task 방식
### 1. 배치 작업 기본 구조
배치 작업은 `AbstractBatchTask`를 상속받아 구현합니다. 주요 구성요소는 다음과 같습니다:

```java
@Component
@BatchJobInfo(
    group = "${batch-info.example.group}",
    jobName = "${batch-info.example.job-name}", 
    cronExpression = "${batch-info.example.cron-expression}",
    description = "${batch-info.example.description}"
)
@RequiredArgsConstructor
public class ExampleBatch extends AbstractBatchTask {
    // 의존성 주입
    private final ExampleSerivce exampleService;

    // 실제 배치 작업 로직 구현
    @Override
    protected Tasklet createTasklet() {
        return ((contribution, chunkContext) -> {
            // 배치 작업 로직
            return RepeatStatus.FINISHED;
        });
    }
}
```

### 2. 필수 어노테이션
- `@Component`: 스프링 빈으로 등록
- `@BatchJobInfo`: 배치 작업 메타 정보 설정
  - group: 배치 그룹명
  - jobName: 배치 작업명
  - cronExpression: 실행 주기
  - description: 작업 설명

### 3. 배치 작업 로직 구현
- `createTasklet` 메서드를 오버라이드하여 실제 배치 작업 로직 구현
- Tasklet 인터페이스를 구현하여 execute 메서드 내에 비즈니스 로직 작성
- 작업 완료 후 `RepeatStatus.FINISHED` 반환

### 4. 프로퍼티 설정
application.yml 또는 application.properties에 배치 정보 설정:
```yaml
batch-info:
  example:
    group: "EXAMPLE_GROUP"
    job-name: "exampleJob"
    cron-expression: "0 0 * * * ?"
    description: "예제 배치 작업"
```

## Chunk 방식
### 1. 배치 작업 기본 구조
배치 작업은 `AbstractBatchChunk`를 상속받아 구현합니다. 주요 구성요소는 다음과 같습니다:

```java
@Slf4j
@Component
@BatchJobInfo(
    group = "${batch-info.example.group}",
    jobName = "${batch-info.example.job-name}",
    cronExpression = "${batch-info.example.cron-expression}",
    description = "${batch-info.example.description}"
)
@RequiredArgsConstructor
public class ExampleBatchChunk extends AbstractBatchChunk {
    // 필수 의존성
    private final EntityManagerFactory entityManagerFactory;
    private final ExampleRepository exampleRepository;
}
```

### 2. Job 구성
Job은 여러 Step으로 구성되며, 다음과 같이 구현합니다:

```java
@Override
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()
            .build();
}
```

### 3. Step 구성
배치 작업은 일반적으로 다음과 같은 Step들로 구성됩니다:

#### 3.1 데이터 읽기 Step
```java
private Step readListStep() {
    return new StepBuilder("readListStep")
            .repository(jobRepository)
            .transactionManager(transactionManager)
            .tasklet(readListTasklet())
            .build();
}

private Tasklet readListTasklet() {
    return (contribution, chunkContext) -> {
        // 초기 데이터 조회 로직
        return RepeatStatus.FINISHED;
    };
}
```

#### 3.2 Chunk 처리 Step
```java
private Step processStep() {
    return new StepBuilder("processStep")
            .repository(jobRepository)
            .transactionManager(transactionManager)
            .<InputType, OutputType>chunk(chunkSize)
            .reader(itemReader())
            .processor(itemProcessor())
            .writer(itemWriter())
            .build();
}
```

### 4. Chunk 처리 컴포넌트

#### 4.1 ItemReader 구현
```java
private JpaPagingItemReader<InputType> itemReader() {
    return new JpaPagingItemReaderBuilder<InputType>()
            .name("itemReader")
            .entityManagerFactory(entityManagerFactory)
            .pageSize(chunkSize)
            .queryString("select e from Entity e")
            .build();
}
```

#### 4.2 ItemProcessor 구현
```java
private ItemProcessor<InputType, OutputType> itemProcessor() {
    return item -> {
        // 데이터 변환 로직
        return transformedItem;
    };
}
```

#### 4.3 ItemWriter 구현
```java
private ItemWriter<OutputType> itemWriter() {
    return items -> {
        // 데이터 저장 로직
        repository.saveAll(items);
    };
}
```

### 5. 주의사항
- Chunk 크기는 메모리 사용량과 성능을 고려하여 적절히 설정
- 트랜잭션 범위는 Chunk 단위로 적용됨
- Reader, Processor, Writer는 각각 독립적으로 구현하여 재사용성 확보

### 6. 프로퍼티 설정
application.yml에 배치 정보 설정:
```yaml
batch-info:
  example:
    group: "EXAMPLE_GROUP"
    job-name: "exampleChunkJob"
    cron-expression: "0 0 * * * ?"
    description: "청크 방식 배치 작업"
```

## Spring Data JPA 사용 가이드

### 1. 엔티티 설계 가이드

#### 1.1 기본 엔티티 구조
```java
@Entity
@Table(name = "TB_USER")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends BaseTimeEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, length = 50)
    private String name;
    
    @Column(nullable = false, unique = true)
    private String email;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private UserStatus status;
    
    @Builder
    private User(String name, String email) {
        this.name = name;
        this.email = email;
        this.status = UserStatus.ACTIVE;
    }
}
```

#### 1.2 엔티티 설계 규칙
- **불변성 유지**: setter 사용을 지양하고 생성자나 빌더를 통해 초기화
- **접근 제어**: 생성자는 protected로 설정하여 직접 생성 방지
- **상속**: 공통 필드(생성일시, 수정일시 등)는 BaseEntity 활용
- **식별자**: auto_increment나 sequence 사용 권장
- **연관관계**: 양방향보다는 단방향을 우선적으로 고려

### 2. 연관관계 매핑 가이드

#### 2.1 연관관계 기본 원칙
- **단방향을 우선적으로 고려**: 양방향은 필요한 경우에만 사용
- **`@ManyToMany` 사용 지양**: 중간 엔티티를 생성하여 `@OneToMany`, `@ManyToOne`으로 풀어서 사용
- **연관관계의 주인은 외래키가 있는 쪽**: 보통 `@ManyToOne`이 있는 쪽이 주인
- **`cascade` 설정은 신중하게**: 라이프사이클이 완전히 동일한 경우에만 사용
- **지연 로딩(`LAZY`) 사용**: 즉시 로딩(`EAGER`)은 예측이 어렵고 성능 문제 발생

#### 2.2 단방향 연관관계

##### 2.2.1 @ManyToOne (단방향)
```java
@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)  // 지연 로딩 필수
    @JoinColumn(name = "user_id")       // 외래키 컬럼명 지정
    private User user;                   // 연관관계 주인
}

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;
    // Order를 참조하지 않음
}
```

##### 2.2.2 @OneToMany (단방향)
```java
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    
    // 단방향 @OneToMany는 추가 UPDATE SQL이 발생하므로 가급적 지양
    @OneToMany
    @JoinColumn(name = "team_id") // 반대쪽 테이블의 외래키
    private List<Member> members = new ArrayList<>();
}

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;
    // Team을 참조하지 않음
}
```

#### 2.3 양방향 연관관계

##### 2.3.1 @OneToMany - @ManyToOne
```java
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "team")  // 연관관계의 주인이 아님을 명시
    private List<Member> members = new ArrayList<>();
    
    // 연관관계 편의 메서드
    public void addMember(Member member) {
        this.members.add(member);
        member.setTeam(this);  // 양쪽 모두 관계 설정
    }
}

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;  // 연관관계의 주인
    
    // 연관관계 편의 메서드
    public void changeTeam(Team team) {
        // 기존 팀과의 관계 제거
        if (this.team != null) {
            this.team.getMembers().remove(this);
        }
        this.team = team;
        team.getMembers().add(this);
    }
}
```

#### 2.4 @ManyToMany 대체 설계

##### 2.4.1 잘못된 설계 (@ManyToMany 사용)
```java
@Entity
public class User {
    @ManyToMany
    @JoinTable(name = "user_role")
    private Set<Role> roles = new HashSet<>();  // 중간 테이블을 엔티티로 만들지 않음
}
```

##### 2.4.2 권장하는 설계 (중간 엔티티 사용)
```java
@Entity
public class User {
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "user")
    private Set<UserRole> userRoles = new HashSet<>();
    
    // 연관관계 편의 메서드
    public void addRole(Role role) {
        UserRole userRole = new UserRole(this, role);
        this.userRoles.add(userRole);
    }
}

@Entity
public class Role {
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "role")
    private Set<UserRole> userRoles = new HashSet<>();
}

@Entity
public class UserRole {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "role_id")
    private Role role;
    
    private LocalDateTime grantedAt;  // 추가 정보 저장 가능
    
    @Builder
    public UserRole(User user, Role role) {
        this.user = user;
        this.role = role;
        this.grantedAt = LocalDateTime.now();
    }
}
```

#### 2.5 Cascade 사용 가이드

##### 2.5.1 Cascade 사용 시 주의사항
- 엔티티의 라이프사이클이 완전히 동일한 경우에만 사용
- 여러 곳에서 참조되는 엔티티에는 사용하지 않음
- CascadeType.REMOVE는 특히 주의 (연쇄 삭제 발생)

```java
@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;
    
    // Order와 라이프사이클이 동일한 OrderItem에만 Cascade 사용
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> orderItems = new ArrayList<>();
    
    // User는 다른 곳에서도 참조되므로 Cascade 사용하지 않음
    @ManyToOne(fetch = FetchType.LAZY)
    private User user;
}
```

##### 2.5.2 Cascade 종류별 특징
```java
public class CascadeExample {
    
    // 저장 시점에 연관 엔티티도 함께 저장
    @OneToMany(cascade = CascadeType.PERSIST)
    private List<Child> persistChildren;
    
    // 삭제 시점에 연관 엔티티도 함께 삭제 (주의 필요)
    @OneToMany(cascade = CascadeType.REMOVE)
    private List<Child> removeChildren;
    
    // 모든 상태 변화를 전파
    @OneToMany(cascade = CascadeType.ALL)
    private List<Child> allChildren;
    
    // 고아 객체 제거 (부모 엔티티와 연관관계가 끊어진 자식 엔티티 삭제)
    @OneToMany(orphanRemoval = true)
    private List<Child> orphanRemovalChildren;
}
```

#### 2.6 연관관계 편의 메서드 작성 가이드

##### 2.6.1 기본 원칙
- 양방향 연관관계에서는 양쪽 모두 관계를 설정
- 한 곳에서 관리하도록 편의 메서드 구현
- 기존 관계 제거도 고려

```java
@Entity
public class Parent {
    @OneToMany(mappedBy = "parent")
    private List<Child> children = new ArrayList<>();
    
    // 연관관계 편의 메서드 - 추가
    public void addChild(Child child) {
        this.children.add(child);
        child.setParent(this);
    }
    
    // 연관관계 편의 메서드 - 제거
    public void removeChild(Child child) {
        this.children.remove(child);
        child.setParent(null);
    }
}

@Entity
public class Child {
    @ManyToOne(fetch = FetchType.LAZY)
    private Parent parent;
    
    // 연관관계 편의 메서드
    public void changeParent(Parent parent) {
        // 기존 부모와의 관계 제거
        if (this.parent != null) {
            this.parent.getChildren().remove(this);
        }
        this.parent = parent;
        // 새로운 부모의 자식 목록에 추가
        if (parent != null) {
            parent.getChildren().add(this);
        }
    }
}
```

#### 2.7 성능 최적화 고려사항

##### 2.7.1 N+1 문제 방지
```java
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    // fetch join으로 N+1 문제 해결
    @Query("select o from Order o join fetch o.orderItems")
    List<Order> findAllWithOrderItems();
    
    // EntityGraph 사용
    @EntityGraph(attributePaths = {"orderItems"})
    List<Order> findAll();
}
```

##### 2.7.2 양방향 관계에서의 무한 루프 방지
```java
@Entity
public class Order {
    @JsonManagedReference  // JSON 직렬화 시 순환 참조 방지
    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<>();
}

@Entity
public class OrderItem {
    @JsonBackReference     // JSON 직렬화 시 순환 참조 방지
    @ManyToOne(fetch = FetchType.LAZY)
    private Order order;
}
```

### 3. Repository 작성 가이드

#### 3.1 기본 Repository 구조
```java
public interface UserRepository extends JpaRepository<User, Long> {
    // 메서드 이름으로 쿼리 생성
    Optional<User> findByEmail(String email);
    
    // @Query 어노테이션 사용
    @Query("select u from User u where u.status = :status")
    List<User> findAllByStatus(@Param("status") UserStatus status);
    
    // Native Query 사용
    @Query(value = "SELECT * FROM TB_USER WHERE EMAIL = ?1", nativeQuery = true)
    User findByEmailNative(String email);
}
```

#### 3.2 QueryDSL 사용
```java
@Repository
@RequiredArgsConstructor
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
    private final JPAQueryFactory queryFactory;
    
    @Override
    public List<User> findActiveUsersInTeam(Long teamId) {
        return queryFactory
            .selectFrom(user)
            .leftJoin(user.team, team)
            .where(
                user.status.eq(UserStatus.ACTIVE),
                user.team.id.eq(teamId)
            )
            .fetch();
    }
}
```

### 4. 성능 최적화 가이드

#### 4.1 N+1 문제 해결
```java
// 페치 조인 사용
@Query("select u from User u join fetch u.team")
List<User> findAllWithTeam();

// EntityGraph 사용
@EntityGraph(attributePaths = {"team"})
List<User> findAll();
```

#### 4.2 벌크 연산
```java
@Modifying
@Query("update User u set u.status = :status where u.lastLoginDate < :date")
int updateUserStatus(@Param("status") UserStatus status, @Param("date") LocalDateTime date);
```

### 5. 트랜잭션 관리

#### 5.1 @Transactional 사용
```java
@Service
@Transactional(readOnly = true)
public class UserService {
    
    @Transactional
    public User createUser(UserCreateRequest request) {
        // 쓰기 작업
    }
    
    public User findUser(Long id) {
        // 읽기 작업
    }
}
```

#### 5.2 트랜잭션 전파 설정
```java
@Transactional(propagation = Propagation.REQUIRED)
public void method1() {
    // 기본 트랜잭션 설정
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void method2() {
    // 새로운 트랜잭션 시작
}
```

### 6. 감사(Auditing) 설정

#### 6.1 BaseEntity 구현
```java
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
    
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;
    
    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}
```

#### 6.2 Auditor 설정
```java
@EnableJpaAuditing
@Configuration
public class JpaConfig {
    @Bean
    public AuditorAware<String> auditorProvider() {
        return () -> Optional.of(SecurityContextHolder.getContext()
                .getAuthentication().getName());
    }
}
```

### 7. 데이터베이스 스키마 관리

#### 7.1 Flyway 설정
```yaml
spring:
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true
```

#### 7.2 마이그레이션 파일 작성
```sql
-- V1__Create_User_Table.sql
CREATE TABLE TB_USER (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE,
    status VARCHAR(20) NOT NULL,
    created_date DATETIME NOT NULL,
    last_modified_date DATETIME NOT NULL
);
```

## MyBatis 사용 가이드

### 1. Mapper 인터페이스 작성 규칙

#### 1.1 기본 구조
```java
@PrimaryMapper  // Primary DB용 Mapper
public interface UserMapper {
    Optional<User> findById(Long id);
    List<User> findAll();
    void save(User user);
}

@SecondaryMapper  // Secondary DB용 Mapper
public interface OrderMapper {
    List<Order> findByUserId(Long userId);
}
```

#### 1.2 명명 규칙
- Mapper 인터페이스: 엔티티명 + Mapper
- XML 파일: resources/mapper/도메인명/엔티티명Mapper.xml
- Mapper ID: 동사 + 명사 형태 (예: findByLoginId, saveUser)

### 2. XML 매핑 파일 작성

#### 2.1 파라미터 타입 지정 규칙
- 단일 파라미터: parameterType 속성으로 타입 명시
- 다중 파라미터: @Param 어노테이션 사용
- Map/DTO 사용: parameterType으로 해당 타입 지정

##### 2.1.1 단일 파라미터
```java
// Mapper 인터페이스
public interface UserMapper {
    User findById(Long id);
}

<!-- XML -->
<select id="findById" parameterType="long" resultType="User">
    SELECT * FROM users WHERE id = #{id}
</select>
```

##### 2.1.2 @Param 어노테이션 사용
```java
// Mapper 인터페이스
public interface UserMapper {
    // 기본 타입 다중 파라미터
    List<User> findByNameAndStatus(
        @Param("name") String name, 
        @Param("status") UserStatus status
    );
    
    // DTO를 @Param으로 처리
    void updateUser(
        @Param("id") Long id, 
        @Param("updateDto") UserUpdateDto updateDto
    );
    
    // 여러 DTO 처리
    List<User> findByCondition(
        @Param("userDto") UserSearchDto userDto,
        @Param("pageDto") PageRequestDto pageDto
    );
}

<!-- XML -->
<!-- 기본 타입 다중 파라미터 -->
<select id="findByNameAndStatus" resultType="User">
    SELECT * FROM users 
    WHERE name = #{name} 
    AND status = #{status}
</select>

<!-- DTO를 @Param으로 처리 -->
<update id="updateUser">
    UPDATE users 
    SET name = #{updateDto.name},
        email = #{updateDto.email},
        status = #{updateDto.status}
    WHERE id = #{id}
</update>

<!-- 여러 DTO 처리 -->
<select id="findByCondition" resultType="User">
    SELECT * FROM users
    <where>
        <if test="userDto.name != null">
            AND name LIKE CONCAT('%', #{userDto.name}, '%')
        </if>
        <if test="userDto.status != null">
            AND status = #{userDto.status}
        </if>
    </where>
    LIMIT #{pageDto.size} 
    OFFSET #{pageDto.offset}
</select>
```

##### 2.1.3 단일 DTO 파라미터
```java
// Mapper 인터페이스
public interface UserMapper {
    // parameterType 지정 방식
    void create(UserCreateDto userDto);
    
    // @Param 생략 방식 (권장)
    void createWithParam(@Param("dto") UserCreateDto userDto);
}

<!-- XML -->
<!-- parameterType 지정 방식 -->
<insert id="create" parameterType="com.spring.domain.user.dto.UserCreateDto">
    INSERT INTO users (name, email, status)
    VALUES (#{name}, #{email}, #{status})
</insert>

<!-- @Param 사용 방식 (권장) -->
<insert id="createWithParam">
    INSERT INTO users (name, email, status)
    VALUES (#{dto.name}, #{dto.email}, #{dto.status})
</insert>
```

##### 2.1.4 Map 파라미터
```java
// Mapper 인터페이스
public interface UserMapper {
    // Map 사용
    void updateStatus(Map<String, Object> params);
    
    // @Param으로 Map 처리 (권장)
    void updateStatusWithParam(@Param("params") Map<String, Object> params);
}

<!-- XML -->
<!-- Map 사용 -->
<update id="updateStatus" parameterType="map">
    UPDATE users 
    SET status = #{status}
    WHERE id = #{id}
</update>

<!-- @Param으로 Map 처리 (권장) -->
<update id="updateStatusWithParam">
    UPDATE users 
    SET status = #{params.status}
    WHERE id = #{params.id}
</update>
```

##### 2.1.5 컬렉션 파라미터
```java
// Mapper 인터페이스
public interface UserMapper {
    // List 파라미터
    List<User> findByIds(@Param("ids") List<Long> ids);
    
    // 컬렉션이 포함된 DTO
    void saveAll(@Param("request") UserBulkCreateDto request);
}

<!-- XML -->
<select id="findByIds" resultType="User">
    SELECT * FROM users
    WHERE id IN
    <foreach item="id" collection="ids"
             open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

<insert id="saveAll">
    INSERT INTO users (name, email, status)
    VALUES 
    <foreach collection="request.users" item="user" separator=",">
        (#{user.name}, #{user.email}, #{user.status})
    </foreach>
</insert>
```

##### 2.1.6 Best Practices
1. **@Param 사용 권장**
   - 파라미터 의미를 명확히 전달
   - 복잡한 객체의 경우 네임스페이스 제공
   - 코드 가독성 향상

2. **DTO 사용 시 고려사항**
   - 단일 DTO: @Param 사용 권장
   - 다중 DTO: 각각 @Param으로 구분
   - 복잡한 검색 조건: 전용 DTO 클래스 생성

3. **Map 사용 지양**
   - 가급적 명시적인 DTO 사용
   - 임시 데이터 처리 시에만 제한적으로 사용

#### 2.2 파라미터 바인딩 방식
- #{} : PreparedStatement 사용, SQL 인젝션 방지
- ${} : 문자열 치환, 동적 테이블명/컬럼명에만 제한적 사용

```xml
<!-- 안전한 파라미터 바인딩 (#{}) -->
<select id="findByName" parameterType="string" resultMap="userResultMap">
    SELECT * FROM users 
    WHERE name = #{name}
</select>

<!-- 동적 테이블명에는 ${} 사용 (주의 필요) -->
<select id="findAllFromTable" parameterType="string" resultMap="userResultMap">
    SELECT * FROM ${tableName}
</select>
```

#### 2.3 복합 파라미터 처리
```xml
<!-- 검색 조건 DTO 사용 -->
<select id="searchUsers" 
        parameterType="com.spring.domain.user.dto.UserSearchCondition" 
        resultMap="userResultMap">
    SELECT * FROM users
    <where>
        <if test="nameKeyword != null">
            AND name LIKE CONCAT('%', #{nameKeyword}, '%')
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
        <if test="startDate != null">
            AND created_at >= #{startDate}
        </if>
    </where>
</select>

<!-- 다중 업데이트 -->
<update id="updateStatuses" parameterType="list">
    UPDATE users 
    SET status = 
    <foreach collection="list" item="item" separator=" " open="CASE id" close="END">
        WHEN #{item.id} THEN #{item.status}
    </foreach>
    WHERE id IN
    <foreach collection="list" item="item" separator="," open="(" close=")">
        #{item.id}
    </foreach>
</update>
```

#### 2.4 결과 매핑 타입
```xml
<!-- 단일 객체 조회 -->
<select id="findById" parameterType="long" resultType="com.spring.domain.user.entity.User">
    SELECT * FROM users WHERE id = #{id}
</select>

<!-- 컬렉션 조회 -->
<select id="findAll" resultType="com.spring.domain.user.entity.User">
    SELECT * FROM users
</select>

<!-- 커스텀 DTO로 조회 -->
<select id="findUserStats" resultType="com.spring.domain.user.dto.UserStatsDto">
    SELECT 
        status,
        COUNT(*) as userCount,
        MAX(created_at) as lastCreatedAt
    FROM users
    GROUP BY status
</select>
```

### 3. 동적 쿼리 작성

#### 3.1 조건부 쿼리
```xml
<select id="findUsers" resultMap="userResultMap">
    SELECT * FROM users
    <where>
        <if test="name != null">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
    </where>
</select>
```

#### 3.2 반복 처리
```xml
<select id="findByIds" resultMap="userResultMap">
    SELECT * FROM users
    WHERE id IN
    <foreach item="id" collection="list"
             open="(" separator="," close=")">
        #{id}
    </foreach>
</select>
```

#### 3.3 동적 정렬
```xml
<select id="findAllWithSort" resultMap="userResultMap">
    SELECT * FROM users
    <if test="orderBy != null">
        ORDER BY ${orderBy} ${direction}
    </if>
</select>
```

### 4. 성능 최적화

#### 4.1 N+1 문제 해결
```xml
<!-- 조인을 통한 한 번의 쿼리로 데이터 조회 -->
<select id="findUsersWithOrders" resultMap="userWithOrdersMap">
    SELECT u.*, 
           o.id as order_id,
           o.order_date as order_date
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
</select>
```

#### 4.2 배치 처리
```xml
<insert id="saveAll">
    INSERT INTO users (name, email)
    VALUES 
    <foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.email})
    </foreach>
</insert>
```

### 5. 타입 핸들러

#### 5.1 커스텀 타입 핸들러 정의
```java
public class JsonTypeHandler extends BaseTypeHandler<Map<String, Object>> {
    private static final ObjectMapper MAPPER = new ObjectMapper();
    
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
            Map<String, Object> parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, toJson(parameter));
    }
    
    @Override
    public Map<String, Object> getNullableResult(ResultSet rs, String columnName) 
            throws SQLException {
        return fromJson(rs.getString(columnName));
    }
    
    private String toJson(Map<String, Object> data) {
        try {
            return MAPPER.writeValueAsString(data);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("JSON 변환 실패", e);
        }
    }
}
```

#### 5.2 타입 핸들러 적용
```xml
<resultMap id="userResultMap" type="User">
    <result property="attributes" column="attributes" 
            typeHandler="com.spring.infra.db.JsonTypeHandler"/>
</resultMap>
```

### 6. 트랜잭션 관리

#### 6.1 선언적 트랜잭션
```java
@Service
@Transactional(readOnly = true)
public class UserService {
    private final UserMapper userMapper;
    
    @Transactional
    public void createUser(User user) {
        userMapper.save(user);
    }
}
```

### 7. 테스트

#### 7.1 Mapper 테스트
```java
@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserMapperTest {
    @Autowired
    private UserMapper userMapper;
    
    @Test
    void findById_존재하는_사용자_조회() {
        // given
        Long userId = 1L;
        
        // when
        Optional<User> user = userMapper.findById(userId);
        
        // then
        assertThat(user).isPresent();
        assertThat(user.get().getId()).isEqualTo(userId);
    }
}
```

### 8. 모범 사례

#### 8.1 쿼리 작성 규칙
- 테이블명, 컬럼명은 대문자로 작성
- 동적 쿼리는 가독성을 위해 적절히 들여쓰기
- 복잡한 쿼리는 주석으로 설명 추가

```xml
<!-- 사용자의 최근 주문 내역 조회 -->
<select id="findRecentOrders" resultMap="orderResultMap">
    SELECT 
        O.ID,
        O.ORDER_DATE,
        O.STATUS,
        OI.PRODUCT_ID,
        OI.QUANTITY
    FROM ORDERS O
    INNER JOIN ORDER_ITEMS OI 
        ON O.ID = OI.ORDER_ID
    WHERE O.USER_ID = #{userId}
        AND O.ORDER_DATE >= #{startDate}
    ORDER BY O.ORDER_DATE DESC
</select>
```

#### 8.2 명명 규칙
- 조회: find*, get*, select*
- 저장: save*, insert*
- 수정: update*
- 삭제: delete*, remove*

## 객체지향 개발 규칙 및 클린 코드 가이드

### 1. 클래스 설계 원칙 (SOLID)

#### 1.1 단일 책임 원칙 (Single Responsibility Principle)
**장점과 필요성:**
- **유지보수성 향상**: 각 클래스가 하나의 책임만 가지므로 코드 변경이 필요할 때 해당 책임과 관련된 클래스만 수정하면 됩니다.
- **테스트 용이성**: 클래스의 책임이 명확하므로 단위 테스트 작성이 쉬워집니다.
- **코드 재사용성**: 작은 단위의 책임으로 분리되어 있어 다른 상황에서도 재사용하기 쉽습니다.
- **버그 발생 위험 감소**: 한 클래스의 변경이 다른 기능에 영향을 미칠 가능성이 줄어듭니다.

```java
// Bad - 여러 책임이 혼재되어 있어 유지보수가 어려움
public class UserService {
    public void saveUser(User user) { /* 사용자 저장 로직 */ }
    public void sendEmail(String to) { /* 이메일 발송 로직 */ }
}

// Good - 각 클래스가 단일 책임을 가짐
public class UserService {
    private final EmailService emailService;
    
    public void saveUser(User user) { /* 사용자 저장 로직 */ }
}

public class EmailService {
    public void sendEmail(String to) { /* 이메일 발송 로직 */ }
}
```

#### 1.2 개방-폐쇄 원칙 (Open-Closed Principle)
**장점과 필요성:**
- **확장성**: 새로운 기능 추가가 기존 코드 수정 없이 가능합니다.
- **위험 감소**: 기존 코드를 수정하지 않으므로 기존 기능이 망가질 위험이 없습니다.
- **재사용성**: 추상화를 통해 공통 기능을 재사용할 수 있습니다.
- **유연성**: 런타임에 동적으로 기능을 변경할 수 있습니다.

```java
// Good - 새로운 결제 방식 추가 시 기존 코드 수정 없이 확장 가능
public interface PaymentProcessor {
    void processPayment(Payment payment);
}

public class CreditCardProcessor implements PaymentProcessor {
    @Override
    public void processPayment(Payment payment) { /* 신용카드 결제 처리 */ }
}

// 새로운 결제 방식 추가 시 인터페이스만 구현하면 됨
public class BlockchainProcessor implements PaymentProcessor {
    @Override
    public void processPayment(Payment payment) { /* 블록체인 결제 처리 */ }
}
```

#### 1.3 리스코프 치환 원칙 (Liskov Substitution Principle)
**장점과 필요성:**
- **다형성의 올바른 사용**: 상위 타입의 객체를 하위 타입의 객체로 치환해도 프로그램의 정확성이 보장됩니다.
- **코드의 신뢰성**: 상속 관계에서 예측 가능한 동작을 보장합니다.
- **재사용성**: 상위 클래스의 로직을 안전하게 재사용할 수 있습니다.
- **유지보수성**: 코드 변경 시 예상치 못한 부작용을 방지합니다.

```java
// Bad - 정사각형이 직사각형의 속성을 위반함
public class Rectangle {
    protected int width;
    protected int height;
    
    public void setWidth(int width) { this.width = width; }
    public void setHeight(int height) { this.height = height; }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width;  // LSP 위반
    }
}

// Good - 인터페이스를 통한 올바른 추상화
public interface Shape {
    double getArea();
}

public class Rectangle implements Shape {
    private final int width;
    private final int height;
    
    @Override
    public double getArea() { return width * height; }
}
```

#### 1.4 인터페이스 분리 원칙 (Interface Segregation Principle)
**장점과 필요성:**
- **불필요한 의존성 제거**: 클라이언트는 필요한 메서드만 구현하면 됩니다.
- **유연성**: 필요한 기능만 조합하여 사용할 수 있습니다.
- **유지보수성**: 인터페이스 변경이 다른 클라이언트에 영향을 미치지 않습니다.
- **테스트 용이성**: 목적에 맞는 인터페이스만 Mock하여 테스트할 수 있습니다.

```java
// Bad - 불필요한 메서드 구현 강제
public interface Worker {
    void work();
    void eat();
    void sleep();
}

// Good - 필요한 기능만 선택적으로 구현 가능
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public class Robot implements Workable {
    @Override
    public void work() { /* 작업 수행 */ }
    // eat, sleep은 필요 없으므로 구현하지 않음
}
```

#### 1.5 의존성 역전 원칙 (Dependency Inversion Principle)
**장점과 필요성:**
- **유연성**: 구체적인 구현체를 쉽게 교체할 수 있습니다.
- **테스트 용이성**: Mock 객체를 사용한 단위 테스트가 용이합니다.
- **재사용성**: 추상화된 인터페이스를 다양한 상황에서 재사용할 수 있습니다.
- **결합도 감소**: 모듈 간의 직접적인 의존성이 줄어듭니다.

```java
// Bad - 구체 클래스에 직접 의존
public class UserService {
    private final MySQLUserRepository userRepository = new MySQLUserRepository();
}

// Good - 추상화에 의존하여 구현체 교체 가능
public class UserService {
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}
```

### 2. 코드 작성 규칙

#### 2.1 네이밍 컨벤션
```java
// 클래스명: 명사형, PascalCase
public class UserService {}

// 인터페이스명: 형용사형 또는 명사형, PascalCase
public interface Manageable {}
public interface UserRepository {}

// 메서드명: 동사형, camelCase
public void saveUser() {}
public User findById() {}

// 상수: 대문자와 언더스코어
public static final int MAX_CONNECTION_COUNT = 100;

// 변수명: camelCase, 명확한 의미 전달
private final UserRepository userRepository;
private List<User> activeUsers;
```

#### 2.2 불변성 유지
**장점과 필요성:**
- **스레드 안전성**: 멀티스레드 환경에서 안전합니다.
- **예측 가능성**: 객체의 상태 변경이 없어 버그 발생 가능성이 줄어듭니다.
- **캐싱 가능**: 불변 객체는 캐시하여 재사용할 수 있습니다.
```java
// Bad
public class User {
    private String name;
    private String email;
    
    public void setName(String name) { this.name = name; }
    public void setEmail(String email) { this.email = email; }
}

// Good
public class User {
    private final String name;
    private final String email;
    
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
    
    public String getName() { return name; }
    public String getEmail() { return email; }
}
```

#### 2.3 메서드 설계
- 메서드는 한 가지 일만 수행해야 합니다.
- 메서드의 길이는 20줄을 넘지 않도록 합니다.
- 파라미터 개수는 3개 이하로 제한합니다.
```java
// Bad
public void processUserData(User user) {
    validateUser(user);
    saveUser(user);
    sendEmail(user);
    updateCache(user);
}

// Good
public void processUserData(User user) {
    userProcessor.process(user);
}

public class UserProcessor {
    private final UserValidator validator;
    private final UserRepository repository;
    private final EmailService emailService;
    private final CacheService cacheService;
    
    public void process(User user) {
        validator.validate(user);
        repository.save(user);
        emailService.sendWelcomeEmail(user);
        cacheService.updateUserCache(user);
    }
}
```

#### 2.4 예외 처리
```java
// Bad
public User findUser(String id) {
    try {
        return userRepository.findById(id);
    } catch (Exception e) {
        log.error("Error", e);
        return null;
    }
}

// Good
public User findUser(String id) {
    try {
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
    } catch (DatabaseException e) {
        throw new SystemException("Database error occurred", e);
    }
}
```

### 3. 테스트 코드 작성 규칙

#### 3.1 단위 테스트 구조
```java
public class UserServiceTest {
    @Test
    void 사용자_생성_성공() {
        // Given
        UserCreateRequest request = new UserCreateRequest("name", "email");
        
        // When
        User user = userService.createUser(request);
        
        // Then
        assertThat(user.getName()).isEqualTo("name");
        assertThat(user.getEmail()).isEqualTo("email");
    }
}
```

#### 3.2 테스트 네이밍
- 테스트 클래스: `대상클래스Test`
- 테스트 메서드: `테스트시나리오_테스트조건_기대결과`
```java
public class UserServiceTest {
    @Test
    void createUser_WithValidData_ShouldSucceed() {}
    
    @Test
    void createUser_WithInvalidEmail_ShouldThrowException() {}
}
```

## 스프링 시큐리티 가이드

### 1. JWT 인증 아키텍처

#### 1.1 전체 구조
현재 프로젝트는 JWT(JSON Web Token)를 사용한 토큰 기반 인증 시스템을 구현하고 있습니다.

**주요 컴포넌트 구성**
1. SecurityConfig
   - Spring Security의 핵심 설정 클래스
   - 필터 체인 구성 및 보안 설정 관리
   - Form 로그인 비활성화 및 JWT 기반 인증 활성화

2. JwtTokenService
   - JWT 토큰 생성 및 검증의 중심 서비스
   - Access Token과 Refresh Token 관리
   - 토큰 기반 인증 처리

3. UserPrincipal
   - Spring Security의 UserDetails 구현
   - 사용자 인증 정보 및 권한 관리
   - AgentUser 엔티티와 연동

#### 1.2 토큰 시스템
**이중 토큰 구조**
1. Access Token
   - 실제 API 요청 인증에 사용
   - 비교적 짧은 만료 시간 (properties 파일에서 설정)
   - JWT 표준 클레임 + 커스텀 클레임(userId, userName, authorities) 포함

2. Refresh Token
   - Access Token 갱신용
   - 더 긴 만료 시간
   - 데이터베이스에 저장되어 관리됨

### 2. 인증 프로세스

#### 2.1 로그인 프로세스
1. **인증 요청 처리**
   ```text
   1. AuthenticationProcessingFilter가 로그인 요청 가로챔
   2. UserAuthenticationProvider가 실제 인증 수행
   3. SigninSuccessHandler에서 인증 성공 처리
   4. JWT 토큰 생성 및 쿠키 설정
   ```

2. **토큰 생성 (JwtTokenGenerator)**
   - Access Token 생성
     - 사용자 식별 정보 포함
     - 권한 정보 포함
     - 만료 시간 설정
   - Refresh Token 생성
     - 사용자 ID만 포함
     - 데이터베이스에 저장

#### 2.2 요청 인증 프로세스
1. **토큰 검증**
   - 모든 보호된 엔드포인트 요청 시 수행
   - JwtTokenUtil을 통한 토큰 유효성 검사
   - 서명 검증 및 만료 여부 확인

2. **인증 상태 관리**
   - SecurityContextHolder에 인증 정보 저장
   - 요청 처리 완료 후 컨텍스트 정리

### 3. 보안 설정

#### 3.1 SecurityConfig 주요 설정
```text
1. 기본 보안 설정
   - formLogin 비활성화
   - httpBasic 비활성화
   - CSRF 보호 비활성화 (JWT 사용으로 인한)
   - JWT 기반 인증을 사용하므로 기존 Spring Security의 기본 인증 방식들을 사용하지 않습니다

2. 필터 체인 구성
   - AuthenticationProcessingFilter: 사용자 정의 인증 처리
   - JwtAuthenticationFilter: JWT 토큰 기반 인증 처리
   - RedirectIfAuthenticatedFilter: 인증된 사용자 리다이렉트 처리      

3. 엔드포인트 보안
   - 공개 URL 설정 (PermittedURI 기반)
   - 인증 필요 URL 설정
   - 권한 기반 접근 제어

4. 세션 관리
   - 무상태(Stateless) 설정
   - JWT를 사용하므로 서버 측 세션을 사용하지 않음
```

#### 3.2 토큰 보안
1. **토큰 설정 (JwtProperties)**
   - 비밀키 관리
   - 토큰 만료 시간 설정
   - Access/Refresh 토큰 분리 설정

2. **쿠키 보안**
   - HttpOnly 플래그 설정
   - Secure 플래그 설정
   - SameSite 설정

### 4. 예외 처리

#### 4.1 JWT 관련 예외
1. **토큰 검증 예외**
   - ExpiredJwtException: 토큰 만료
   - MalformedJwtException: 잘못된 토큰 형식
   - SignatureException: 서명 불일치
   - JwtException: 기타 JWT 관련 예외

2. **예외 처리 전략**
   - SecurityExceptionRule을 통한 표준화된 예외 메시지
   - 클라이언트에 적절한 HTTP 상태 코드 반환

### 5. 토큰 관리

#### 5.1 토큰 생성 (JwtTokenGenerator)
```text
1. Access Token 생성
   - Header: typ: "JWT", alg: "HS512"
   - Payload: 
     - sub: 사용자 ID
     - iat: 발급 시간
     - exp: 만료 시간
     - auth: 권한 정보
     - userId: 사용자 ID
     - userName: 사용자 이름

2. Refresh Token 생성
   - 최소한의 정보만 포함
   - 사용자 식별자만 포함
```

#### 5.2 토큰 저장 및 갱신
1. **토큰 저장**
   - Access Token: 클라이언트 쿠키
   - Refresh Token: 데이터베이스 + 클라이언트 쿠키

2. **토큰 갱신 프로세스**
   - Refresh Token 유효성 검증
   - 새로운 Access Token 발급
   - 필요시 Refresh Token 재발급

### 6. 사용자 권한 관리

#### 6.1 권한 구조
1. **UserPrincipal**
   - Spring Security의 UserDetails 구현
   - AgentUser 엔티티와 연동
   - 권한 정보 제공

2. **권한 검사**
   - URL 기반 권한 검사
   - 메서드 수준 보안
   - JWT 토큰 내 권한 정보 활용

### 7. 필터 체인 상세 구현

#### 7.1 커스텀 필터 구성
1. **RedirectIfAuthenticatedFilter**
   - 이미 인증된 사용자의 로그인 페이지 접근 제어
   - 인증된 사용자를 메인 페이지로 리다이렉트

2. **AuthenticationProcessingFilter**
   ```text
   주요 기능:
   - 로그인 요청(/api/signin) 처리
   - JSON 형식의 로그인 요청 처리
   - 사용자 인증 성공/실패 핸들링
   
   처리 순서:
   1. 요청 미디어 타입 검증
   2. SignInRequest DTO로 요청 변환
   3. 인증 토큰 생성 및 검증
   4. 사용자 승인 상태 확인
   ```

3. **JwtAuthenticationFilter**
   ```text
   주요 기능:
   - 모든 보호된 요청에 대한 JWT 토큰 검증
   - Access Token 만료 시 자동 갱신
   - SecurityContext 관리
   
   처리 순서:
   1. 허용된 URI 확인 및 처리 제외
   2. Authorization 헤더에서 토큰 추출
   3. 토큰 유효성 검증
   4. Access Token 만료 시 Refresh Token으로 갱신
   5. SecurityContext에 인증 정보 설정
   ```

#### 7.2 필터 순서
```text
1. AuthenticationProcessingFilter
2. JwtAuthenticationFilter
3. RedirectIfAuthenticatedFilter
4. 기본 Spring Security 필터들
```

### 8. 인증 핸들러 구현

#### 8.1 성공 핸들러 (SigninSuccessHandler)
```text
처리 과정:
1. JWT Access Token 생성
2. Refresh Token 생성 및 저장
3. 원래 요청 URL 복원 (SavedRequest 사용)
4. JSON 응답 생성 및 반환
   - 성공 상태
   - 리다이렉트 URL
```

#### 8.2 실패 핸들러 (SigninFailureHandler)
```text
처리 과정:
1. 인증 실패 원인 분석
2. 적절한 에러 메시지 생성
3. HTTP 상태 코드 설정
4. JSON 형식의 에러 응답 반환
```

### 9. JWT 토큰 유틸리티

#### 9.1 JwtTokenUtil
```text
주요 기능:
1. 토큰 상태 확인
   - AUTHENTICATED: 유효한 토큰
   - EXPIRED: 만료된 토큰
   - INVALID: 유효하지 않은 토큰

2. 토큰 검증
   - 서명 검증
   - 만료 시간 검증
   - 토큰 형식 검증

3. 쿠키 관리
   - 토큰 쿠키 생성
   - 토큰 쿠키 삭제
   - 쿠키에서 토큰 추출
```

#### 9.2 JwtTokenService
```text
핵심 책임:
1. 토큰 생성
   - Access Token 생성
   - Refresh Token 생성
   - 토큰 쿠키 설정

2. 토큰 검증
   - Access Token 유효성 검증
   - Refresh Token 유효성 검증
   - 토큰 갱신 처리

3. 인증 정보 관리
   - 토큰으로부터 Authentication 객체 생성
   - SecurityContext 업데이트
```

### 10. 보안 속성 관리

#### 10.1 JwtProperties
```text
설정 항목:
1. Access Token 설정
   - 비밀키
   - 만료 시간

2. Refresh Token 설정
   - 비밀키
   - 만료 시간

사용 방법:
- application.yml에서 설정
- @ConfigurationProperties를 통한 자동 바인딩
```
```yaml
jwt:
  access-token:
    secret: your-access-token-secret-key
    expiration: 15  # 15분
  refresh-token:
    secret: your-refresh-token-secret-key
    expiration: 10080  # 7일
```

### 11. 예외 처리 전략

#### 11.1 SecurityAuthenticationEntryPoint
```text
처리 기능:
1. 인증되지 않은 요청 처리
   - API 요청과 일반 요청 구분
   - 적절한 응답 형식 결정

2. 엔드포인트 존재 여부 확인
   - 404 응답 처리
   - API 에러 응답 생성

3. 예외 처리
   - JWT 관련 예외 처리
   - 인증 실패 예외 처리
```   
### 12. 보안 컨텍스트 관리

#### 12.1 SecurityContext 생명주기
```text
1. 컨텍스트 생성
   - JwtAuthenticationFilter에서 토큰 검증 후 생성
   - Authentication 객체 설정
   - SecurityContextHolder에 저장

2. 컨텍스트 사용
   - 요청 처리 중 인증 정보 접근
   - 권한 검사에 활용
   - 사용자 정보 조회

3. 컨텍스트 정리
   - SecurityContextPersistenceFilter가 요청 처리 완료 후 자동으로 SecurityContext 정리
```

### 13. 쿠키 보안 관리

#### 13.1 토큰 쿠키 설정
```text
보안 설정:
1. HttpOnly
   - JavaScript 접근 방지
   - XSS 공격 방어

2. Secure
   - HTTPS 통신 강제
   - 평문 전송 방지

3. SameSite
   - CSRF 공격 방어
   - 크로스 사이트 요청 제한

4. Path
   - 쿠키 범위 제한
   - 최소 권한 원칙 적용
```