GitHub - HeoJunHyoung/bytemall
Contribute to HeoJunHyoung/bytemall development by creating an account on GitHub.
github.com
1. 들어가며
이전 포스팅에서 우리는 Spring Security의 동작 원리와 필터 위임 모델, 그리고 Controller 위임 방식의 당위성을 살펴보았다. 이제 이론을 뒤로하고 실제 코드를 작성할 차례다.
이번 포스팅에서는 Spring Boot 3.x 환경에서 시큐리티, JWT, Redis를 사용하기 위한 필수 의존성(Dependency)을 추가하고, 전체적인 환경 설정(application.yml)과 확장성을 고려한 패키지 구조(Package Structure)를 잡는다. 또한 보안의 기초가 되는 CORS와 Redis 설정을 진행하여 견고한 뼈대를 구축한다.
2. 프로젝트 초기 설정 (Setup)
가장 먼저 build.gradle에 필요한 라이브러리를 추가하고, application.yml에 핵심 설정값을 정의한다. 본 프로젝트는 Java 17과 Spring Boot 3.x 버전을 기준으로 한다.
2.1. 필수 의존성 추가 (build.gradle)
- Spring Security: 인증 및 권한 부여를 위한 핵심 프레임워크.
- Redis: Refresh Token을 저장하고 관리하기 위한 In-Memory DB.
- JWT (jjwt): 토큰 생성 및 파싱을 위한 라이브러리. 0.11.x 버전 이후 메서드명 변경이 많으므로 0.12.x 최신 버전을 사용한다.
dependencies {
// 1. Spring Security & Validation
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// 2. Data (JPA, Redis)
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
runtimeOnly 'com.mysql:mysql-connector-j' // 또는 H2
// 3. JWT (0.12.5 버전)
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
// 4. Lombok & Test
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-security-test'
}
2.2. 환경 변수 설정 (application.yml)
DB 연결 정보와 Redis 설정, 그리고 JWT 암호화 키를 한곳에서 관리하기 위해 application.yml을 작성한다.
- JWT Secret: HS256 알고리즘을 사용하므로 임의의 긴 문자열을 입력한다. (실무에서는 환경변수로 분리 권장)
- JPA 설정: ddl-auto: update로 엔티티 변경 사항을 DB에 자동 반영하며, open-in-view: false로 설정하여 트랜잭션 범위를 명확히 제한한다.
server:
port: 8000
spring:
application:
name: bytemall
# JWT 설정 (Secret Key & 만료 시간)
jwt:
secret: vfr79d8f9q327498fh398fhw98fh3298fh9382hf9832hf98 # 임의의 문자열 (32byte 이상 권장)
access_expiration_time: 3600000 # 1시간 (ms)
refresh_expiration_time: 604800000 # 7일 (ms)
# 데이터베이스 설정 (MySQL)
datasource:
url: jdbc:mysql://localhost:3306/bytemall?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: root
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
# JPA & Hibernate 설정
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
default_batch_fetch_size: 100
show-sql: true
open-in-view: false # OSIV 비활성화 (성능 최적화)
# Redis 설정
data:
redis:
host: localhost
port: 6379
password: 1234
3. 확장성을 고려한 패키지 구조 설계
프로젝트의 폴더 구조는 집의 설계도와 같다. 보안 로직(Security)과 비즈니스 로직(Member, Auth)이 뒤섞이면 코드가 복잡해지고 유지보수가 어려워진다. 따라서 역할과 책임에 따라 패키지를 명확히 분리한다.
3.1. Directory Tree
src/main/java/com/example/bytemallbackend
├── domain <-- [비즈니스 로직 계층]
│ ├── auth # 인증 행위 (로그인, 재발급, 로그아웃)
│ └── member # 회원 정보 (Entity, 가입, 조회)
└── global <-- [인프라 및 전역 설정 계층]
├── config # 설정 (Security, Redis, Cors)
├── error # 예외 처리 (GlobalExceptionHandler)
├── security # Spring Security 핵심 구현체
│ ├── filter # JwtAuthenticationFilter
│ ├── jwt # JwtTokenProvider
│ ├── principal # AuthMember (UserDetails)
│ └── service # CustomUserDetailsService
└── util # 유틸리티 (CookieUtil 등)
3.2. 구조의 핵심 의도
- global vs domain:
- global: 프로젝트 전반에 걸쳐 사용되는 기술적 인프라(설정, 보안, 유틸)를 모아둔다. JWT 발급 로직이나 Security Filter는 비즈니스 로직이 아닌 '기술적 수단'이므로 이곳에 위치한다.
- domain: 실제 사용자의 요구사항을 처리하는 비즈니스 영역이다. member와 auth를 분리함으로써, "회원 정보 관리"와 "로그인 행위"를 독립적으로 다룰 수 있게 설계했다.
- domain/auth의 분리:
- 많은 예제들이 MemberService 안에 로그인 로직을 포함시킨다. 하지만 로그인은 단순한 회원 조회가 아니다. JWT 발급, Redis 저장, 쿠키 설정 등 복잡한 보안 로직이 포함된다.
- 추후 OAuth2나 Sms 인증 등이 추가될 경우 auth 패키지만 확장하면 되므로 member 도메인을 오염시키지 않는다.
4. 인프라 설정 (Infrastructure Configuration)
시큐리티 로직이 동작하기 위해 선행되어야 할 Redis와 CORS 설정을 진행한다.
4.1. 비밀번호 암호화 설정 분리 (PasswordEncoderConfig)
SecurityConfig 내부에 PasswordEncoder 빈을 등록할 경우, CustomUserDetailsService 등 다른 빈들과의 의존성 주입 과정에서 순환 참조(Circular Dependency) 문제가 발생할 수 있다. 이를 방지하기 위해 별도의 설정 클래스로 분리한다.
@Configuration
public class PasswordEncoderConfig {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.2. Redis 설정 (RedisConfig)
Redis는 Refresh Token의 저장소 역할을 한다. JWT Access Token은 Stateless 하지만, Refresh Token은 탈취 시 무효화(Invalidate)를 위해 서버측 저장소가 필요하기 때문이다.
- LettuceConnectionFactory: 비동기 처리에 유리한 Lettuce 클라이언트를 사용하여 Redis와 연결한다.
- StringRedisTemplate: 토큰은 단순 문자열이므로, 일반 RedisTemplate 대신 직렬화 설정이 필요 없는 StringRedisTemplate을 사용한다.
@Configuration
@RequiredArgsConstructor
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port));
}
@Bean
public StringRedisTemplate redisTemplate() {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory());
return template;
}
}
4.3. CORS 설정 (CorsConfig)
프론트엔드(React, Vue 등)와 백엔드 API 서버의 도메인(포트)이 다를 경우 브라우저 보안 정책(SOP)에 의해 요청이 차단된다. 이를 해결하기 위해 CORS 설정을 전역으로 등록한다.
- setAllowedOrigins: 허용할 프론트엔드 도메인을 명시한다. (예: http://localhost:5500)
- setAllowCredentials(true): 중요하다. 쿠키(Cookie)나 인증 헤더(Authorization)를 주고받으려면 반드시 true로 설정해야 한다.
- ExposedHeaders (선택): 만약 클라이언트가 특정 응답 헤더를 읽어야 한다면 노출 설정이 필요할 수 있다.
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("http://localhost:5500"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowCredentials(true); // 쿠키 사용 시 필수
config.setAllowedHeaders(Collections.singletonList("*"));
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
5. Security 기본 뼈대 작성 (SecurityConfig)
마지막으로 Spring Security의 진입점인 SecurityConfig의 뼈대를 작성한다. 아직 필터(Filter)를 구현하지 않았으므로, 기본적인 비활성화 설정만 등록한다.
기본 보안 설정
- csrf.disable(): Rest API 서버이므로 CSRF 보안이 불필요하다.
- formLogin.disable(): 우리는 JWT 방식을 사용하므로 기본 로그인 폼을 끈다.
- sessionManagement: 세션을 생성하지 않도록 STATELESS 정책을 적용한다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CorsConfigurationSource corsConfigurationSource;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 1. 기본 설정 비활성화 (CSRF, FormLogin, HttpBasic)
http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource));
// 2. 세션 설정 (Stateless)
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// 3. 접근 권한 설정 (추후 구체화)
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 로그인/회원가입은 허용
.anyRequest().authenticated()); // 그 외는 인증 필요
return http.build();
}
}
'Spring > Security' 카테고리의 다른 글
| [6] Spring Security Local: Sequence Diagram (0) | 2025.12.16 |
|---|---|
| [5] Spring Security Local: 로컬 로그인과 토큰 재발급 (0) | 2025.12.16 |
| [4] Spring Security Local: JWT 발급과 인증 필터 구현 (1) | 2025.12.16 |
| [3] Spring Security Local: 도메인 설계와 UserDetails (0) | 2025.12.16 |
| [1] Spring Security Local: 동작 원리 이해 (0) | 2025.12.16 |
