💡 오늘 학습 키워드
- Spring Security 구조 설계
🎯 학습 내용 정리
Spring Security를 처음 적용할 때 가장 많이 하는 고민 중 하나는 Security 관련 코드를 어디에 위치시켜야 하는가이다.
프로젝트 규모가 작은 경우에는 config, filter, jwt, service 등을 하나의 패키지에 모두 작성하기도 한다. 하지만 프로젝트가 점점 커질수록 인증(Authentication), 인가(Authorization), JWT, Filter 등이 하나의 패키지에 섞여 유지보수가 어려워진다.
이번에서는 도메인 중심 구조(Domain-Oriented Package Structure) 를 유지하면서 Security 또한 하나의 독립적인 인프라 영역으로 설계하였다.
Spring Security를 처음 적용하면 다음과 같은 구조를 자주 사용한다.
security
├── SecurityConfig
├── JwtProvider
├── JwtFilter
├── JwtUtil
├── UserDetailsService
├── UserDetails
└── AuthenticationFacade
프로젝트 초반에는 큰 문제가 없어 보인다.
하지만 기능이 추가되기 시작하면
- OAuth2
- Refresh Token
- Redis
- 권한 검증
- 인증 객체
- 예외 처리
등이 모두 하나의 패키지 안으로 들어오게 된다.
결과적으로 Security 패키지가 지나치게 커지고, 인증과 인가의 책임이 섞이는 문제가 발생한다.
따라서 이번 프로젝트에서는 Security를 Infrastructure 계층으로 분리하였다.
src/main/java/com/example
global
└── infrastructure
├── config
│ └── security
│ └── SecurityConfig.java
│
└── security
├── AuthenticationFacade.java
├── AuthorizationValidator.java
├── CustomUserDetails.java
├── CustomUserDetailsService.java
├── JwtAuthenticationFilter.java
└── JwtProvider.java
doamin
├── user
└── order
이 구조에서는 인증 및 보안 관련 코드는 모두 global.infrastructure.security에서 관리하고, 각 도메인은 자신의 비즈니스 로직에만 집중하도록 구성하였다.
그렇다면 왜 Infrastructure인가?
인증(Authentication)과 인증 객체 생성은 비즈니스 로직이 아니라 애플리케이션을 위한 기술적인 인프라이기 때문이다.
예를 들어 리뷰 삭제를 생각해 보면
리뷰 삭제
↓
JWT 인증
↓
권한 확인
↓
리뷰 삭제
리뷰 삭제는 Review Domain의 책임이다.
반면
- JWT 검증
- Authentication 생성
- SecurityContext 저장
등은 Spring Security Framework를 이용한 기술적인 처리이다.
따라서 Infrastructure 계층에 위치시키는 것이 적절하다.
이번 프로젝트는 다음과 같이 계층을 구분하였다.
| 계층 | 역할 |
| presentation | Controller, Request/Response DTO |
| application | 비즈니스 로직(Service) |
| domain | Entity, Repository Interface |
| infrastructure | Framework, Database, Security, 외부 API |
각 계층은 자신의 책임만 수행한다.
예를 들어
- Controller에서는 JWT를 직접 검증하지 않는다.
- Service는 SecurityContextHolder를 직접 조회하지 않고 AuthenticationFacade 등을 통해 현재 사용자 정보를 전달받는다.
- Repository에서는 권한 검사를 수행하지 않는다.
이처럼 계층별 책임을 명확히 분리하면 유지보수성이 크게 향상된다.
SecurityConfig는 다음 위치에 작성하였다.
global
└── infrastructure
└── config
└── security
└── SecurityConfig.java
설정(Configuration)은 보통 config 패키지에 위치시키는 것이 일반적인 관례이다.
반면 JWT Provider나 Filter는 실제 인증 기능을 수행하는 컴포넌트이므로 security 패키지에 배치하였다.
그렇다면 이제 SecurityConfig를 구현해보자!
@Configuration
@EnableWebSecurity
/*
* Spring Security의 메서드 단위 권한 검사를 활성화한다.
*
* @PreAuthorize
* @PostAuthorize
* @Secured
*
* 등을 사용할 수 있게 된다.
*/
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
/*
* JWT 인증 필터
*/
private final JwtAuthenticationFilter jwtAuthenticationFilter;
/*
* 비밀번호 암호화 객체
*
* BCrypt는 Spring Security에서 가장 많이 사용하는
* PasswordEncoder 구현체이다.
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/*
* AuthenticationManager Bean 등록
*
* 로그인 시
*
* AuthenticationManager.authenticate()
*
* 에서 사용된다.
*/
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration configuration
) throws Exception {
return configuration.getAuthenticationManager();
}
/*
* Spring Security Filter Chain
*
* 요청이 들어왔을 때 어떤 순서로
* Security Filter를 수행할 것인지 정의한다.
*/
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http
) throws Exception {
http
/*
* JWT 기반 인증을 사용하므로
* CSRF는 비활성화한다.
*/
.csrf(AbstractHttpConfigurer::disable)
/*
* Form Login 사용 안함
*
* Spring Security 기본 로그인 페이지 제거
*/
.formLogin(AbstractHttpConfigurer::disable)
/*
* HTTP Basic 인증 사용 안함
*
* Authorization: Basic xxxx
*
* 방식 제거
*/
.httpBasic(AbstractHttpConfigurer::disable)
/*
* Session을 생성하지 않는다.
*/
.sessionManagement(session ->
session.sessionCreationPolicy(
SessionCreationPolicy.STATELESS
)
)
/*
* URL별 접근 권한 설정
*/
.authorizeHttpRequests(auth -> auth
/*
* 로그인 API는 인증 없이 접근 가능
*/
.requestMatchers("/api/auth/login")
.permitAll()
/*
* 나머지는 인증 필요
*/
.anyRequest()
.authenticated()
)
/*
* UsernamePasswordAuthenticationFilter보다
* 먼저 JWT Filter를 실행한다.
*/
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
}
왜 JwtAuthenticationFilter를 먼저 실행하는가?
Spring Security는 여러 개의 Filter를 순차적으로 실행한다. JWT 인증에서는 먼저 토큰을 검증하여 인증 객체(Authentication)를 생성해야 한다. 그래야 이후의 Filter와 Controller에서 현재 로그인한 사용자를 사용할 수 있다.
따라서
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
)
를 사용하여 JWT Filter를 먼저 실행하도록 설정하였다.
왜 Form Login을 비활성화하는가?
Spring Security는 기본적으로 Form Login 기반의 인증 방식을 제공한다. 별도의 설정을 하지 않으면 로그인 페이지(/login)를 자동으로 생성하고, 사용자가 아이디와 비밀번호를 입력하면 Session 기반 인증을 수행한다.
하지만 이번 프로젝트는 JWT 기반의 Stateless 인증 방식을 사용한다. 로그인은 REST API를 통해 수행되며, 인증이 성공하면 Access Token과 Refresh Token을 발급한다.
즉, Spring Security가 제공하는 기본 로그인 페이지와 Form Login 인증 과정은 사용하지 않는다.
따라서
/*
* Form Login 사용 안함
*
* Spring Security 기본 로그인 페이지 제거
*/
.formLogin(AbstractHttpConfigurer::disable)
를 사용하여 Spring Security의 기본 Form Login 기능을 비활성화하였다.
왜 HTTP Basic 인증을 비활성화하는가?
Spring Security는 기본적으로 HTTP Basic 인증을 지원한다.
HTTP Basic 인증은 요청마다 Authorization 헤더에 아이디와 비밀번호를 Base64로 인코딩하여 전송하는 방식이다.
Authorization: Basic dXNlcjpwYXNzd29yZA==
HTTP Basic은 Base64 인코딩만 수행하므로 HTTPS가 없으면 인증 정보가 쉽게 노출될 수 있다. JWT 역시 HTTPS를 사용하는 것이 필수이지만, 매 요청마다 비밀번호를 반복해서 전송하지 않는다는 점에서 차이가 있다.
이번 프로젝트에서는 인코딩 수행 보다는 올바르게 작성이 되었는지 확인하기 위한 목적이므로 HTTP Basic 인증은 사용하지 않는다.
/*
* HTTP Basic 인증 사용 안함
*
* Authorization: Basic xxxx
*
* 방식 제거
*/
.httpBasic(AbstractHttpConfigurer::disable)
를 사용하여 Spring Security의 기본 HTTP Basic 인증을 비활성화하였다.
왜 SessionCreationPolicy.STATELESS를 사용하는가?
JWT 인증에서는 인증 정보를 Session에 저장하지 않는다.
클라이언트는 매 요청마다 Access Token을 전송하며, 서버는 토큰을 검증한 후 인증 정보를 생성한다.
즉,
Client
↓
JWT
↓
Server
↓
검증
↓
Authentication 생성
↓
응답
↓
Authentication 제거
요청이 종료되면 SecurityContext가 정리되고 Authentication도 함께 사라진다.
이러한 방식은 서버가 별도의 세션을 유지하지 않으므로 Stateless 구조라고 한다.
이번 프로젝트에서는 인증(Authentication), 권한(Authorization), 설정(Configuration)을 각각 독립적인 컴포넌트로 분리하였다.
그 결과 다음과 같은 장점을 얻을 수 있었다.
- 인증 로직과 비즈니스 로직이 분리된다.
- Security 관련 코드의 응집도가 높아진다.
- 도메인(Service)이 Spring Security API에 직접 의존하지 않는다.
- 인증 방식이 JWT 단독 인증에서 OAuth2 Login 기반 인증으로 변경되더라도 Security 관련 컴포넌트만 수정하면 되므로 영향 범위를 줄일 수 있다.
- 프로젝트 규모가 커져도 Security 구조를 일관성 있게 유지할 수 있다.
- 테스트가 쉬워진다.
Spring Security를 프로젝트에 적용하기 전에 패키지 구조와 계층의 책임을 어떻게 설계했는지를 살펴보았다.
프로젝트 초기에 구조를 명확하게 설계하면 이후 JWT, Filter, 권한 검증, 로그인 API를 구현할 때도 각 클래스의 역할이 분명해지고 유지보수가 쉬워진다.
📚 한줄 정리
패키지 구조와 계층 책임에 정답은 없지만 설계를 했다면 왜 그렇게 설계했는지에 대해서 고민해보자!
'Bootcamp > Implementation' 카테고리의 다른 글
| UserDetails와 UserDetailsService (0) | 2026.07.09 |
|---|---|
| Authentication과 SecurityContextHolder (0) | 2026.07.08 |
| JWT 기반 로그인 구현과 Access Token 발급 (0) | 2026.07.07 |
| OAuth (0) | 2026.06.28 |
| Spring Security + JWT (0) | 2026.06.27 |