
1. 인가를 다시 고민하게 된 이유
그동안 인가 처리는 자연스럽게 AOP로 구현하는 영역이라고 생각해 왔다. 컨트롤러 메소드에 어노테이션을 붙이고, AOP에서 이를 가로채 권한을 검증하는 방식은 구현도 간단하고 이해하기도 쉬웠다. 그래서 인가를 굳이 Spring Security 레이어에서 처리해야겠다는 생각을 깊게 해보지 않았던 것 같다.
또 하나의 이유는, 이전까지 참여했던 프로젝트들에서는 메뉴 기반 권한을 본격적으로 고민할 일이 많지 않았기 때문이다. 대부분은 역할 단위로 충분했고, 복잡한 권한 모델이 필요하지 않았다. 그러다 보니 Spring Security는 주로 인증을 담당하는 도구로만 사용하고 있었고, 인가는 보안 필터를 통과한 이후의 문제로 인식하고 있었다. 자연스럽게 인가 로직은 컨트롤러와 AOP에 흩어져 자리 잡게 되었다.
새로운 프로젝트를 진행하면서 이번에는 Spring Security를 단순하게 사용하는 데서 그치지 않고 프레임워크가 의도한 방식대로 더 적극적으로 활용해 보고 싶다는 생각이 들었다. 특히 인증과 인가가 요청 흐름 어디에서 어떤 책임을 가지는지 명확히 이해하고 싶었고, 그 과정에서 인증 로직을 AbstractAuthenticationProcessingFilter로 상속받아 처리해 보기로 했다.
이후에는 메뉴와 액션 단위로 권한이 나뉘는 구조를 설계하면서, 인가 역시 컨트롤러나 AOP가 아니라 Security 레이어에서 일관되게 처리해 보고 싶었다. 그리고 그 과정에서 AuthorizationManager를 들여다보게 되었다.
2. AuthorizationManager 소개
AuthorizationManager를 처음 제대로 보게 된 건 Spring Security 6의 변경 사항을 정리하면서였다. 이전까지는 인가라고 하면 자연스럽게 @PreAuthorize같은 어노테이션이나 AOP 기반 처리만 떠올렸기 때문에, Security 내부에 이렇게 중요한 확장 포인트가 있다는 사실을 깊게 생각해 보지 않았던 것 같다.
Spring Security 6부터는 인가 판단의 중심이 AuthorizationManager로 옮겨졌다. 이제는 AuthorizationFilter가 AuthorizationManager를 호출하여 요청에 대한 최종적인 권한 부여 결정을 내린다. 공식 문서에서도 기존에 AccessDecisionManager나 AccessDecisionVoter를 커스터마이징하여 사용하던 애플리케이션은 AuthorizationManager 기반 구조로 전환하는 것을 권장하고 있다. AuthorizationManager는 check 메서드를 통해 인증 정보와 보안 객체를 전달받아 최종적인 접근 여부를 판단한다.
스프링 시큐리티 6에서 가장 중요한 변화 중 하나는 권한 부여(Authorization) 아키텍처의 현대화입니다.
• AuthorizationManager의 도입: 기존의 AccessDecisionManager와 AccessDecisionVoter는 이제 AuthorizationManager로 대체되었습니다.
• 권장 사항: 기존에 AccessDecisionManager나 AccessDecisionVoter를 커스터마이징하여 사용하던 애플리케이션은 AuthorizationManager를 사용하는 방식으로 전환하는 것이 권장됩니다.
• 작동 방식: AuthorizationManager는 check 메서드를 통해 인증 정보(Authentication)와 보안 객체(예: 메서드 호출 또는 웹 요청)를 전달받아 최종적인 액세스 결정(승인, 거부, 기권)을 내립니다.
출처 : Spring 공식 문서 - Authorization Architecture
Spring Security는 자주 사용되는 인가 시나리오를 위해 이미 여러 AuthorizationManager 구현체를 제공하고 있다.

| 구분 | 구현체 | 설명 |
| 요청 기반 | RequestMatcherDelegatingAuthorizationManager | 요청 조건에 따라 적절한 AuthorizationManager에게 판단을 위임 |
| AuthenticatedAuthorizationManager | 익명 사용자, 완전히 인증된 사용자, 또는 'Remember-me'를 통해 인증된 사용자를 구분하여 처리 | |
| AuthorityAuthorizationManager | 현재 사용자가 특정 권한을 가지고 있는지 확인하는 가장 일반적인 매니저 | |
| 메서드 기반 | PreAuthorizaeAuthorizationManager | 메소드 실행 전에 권한 검사 (@PreAuthorize) |
| PostAuthorizeAuthorizationManager | 메소드 실행 후에 권한 검사 (@PostAuthorize) | |
| SecuredAuthorizationManager | 메소드 접근 특정 권한 검사 (@Secured) | |
| Jsr250AuthorizationManager | JSR-250 어노테이션을 통해 권한 검사 (@PermitAll, DenyAll 등) |
자세한 설명과 예제는 스프링 공식 문서에 잘 정리되어 있어 이 글에서는 생략한다. 결과적으로 공식 문서를 살펴 보면서 Spring Security는 단순히 정해진 인가 방식을 제공하는 데 그치지 않고 서비스의 권한 모델에 맞게 인가 전략을 설계하도록 유도하는 방향으로 발전하고 있다는 점이었다.
그래서 이번 프로젝트에서는 기본 제공 메커니즘 위에서 우리 서비스의 메뉴 기반 권한 모델을 표현하기 위해 AuthorizationManager를 직접 활용해 보기로 했다.
3. 메뉴 기반 권한 모델과 설계 선택
우리 서비스의 권한 모델은 일반적인 ADMIN, USER 같은 역할로 나뉘는 구조가 아니라 메뉴(Resource) + 액션(Action) 단위로 관리된다. 예를 들면 같은 관리자라도 A 관리자는 ADMIN 메뉴를 조회만 할 수 있고 B 관리자는 ADMIN 메뉴에 대해 조회와 생성을 모두 할 수 있다.
이런 구조에서는 ROLE_ADMIN과 같은 단일 Role 기반 인가 방식이 잘 맞지 않는다. 또한 각 API가 어떤 메뉴의 어떤 액션에 해당하는지 코드 상에서 명확하게 드러나길 원했다. 그래서 컨트롤러 메소드에 권한을 선언하고 실제 인가 판단은 Security 레이어에서 수행하는 구조로 설계를 하게 되었다.
4. AuthorizationManager 구현 방식
SecurityConfig에서는 미리 허용해 둔 URL을 제외한 모든 요청에 대해 커스텀 AuthorizationManager에게 인가 판단을 위임했다. 이제 사전에 허용한 요청을 제외하면 모든 요청은 CustomAuthorizationManager를 거쳐 인가 판단을 받는다.
.authorizeHttpRequests(registry ->
registry.requestMatchers(PERMIT_API_URLS).permitAll()
.anyRequest().access(customAuthorizationManager)
)
*참고로 현재는 ADMIN, MEMBER 구분 없이 접근 가능한 API와 모바일 웹 MEMBER만 접근할 수 있는 API가 함께 존재하고 있어 AuthorizationManager를 세분화했고, 인증만 필요한 API는 authenticated() 메소드로 별도 처리하고 있다.
CustomAuthorizationManager에서는 다음과 같은 흐름으로 동작한다.
- Authentication 정보 확인
- 사용자 타입에 따른 빠른 분기 (예: SUPER_ADMIN은 항상 허용)
- HTTP 요청을 실제 컨트롤러 메소드로 매핑
- 메소드에 선언된 @PermissionCheck 어노테이션 확인
- DB를 조회해 해당 권한이 존재하는지 판단
핵심은 URL 기준이 아니라 컨트롤러 메소드 기준으로 인가를 판단한다는 점이다. 이 덕분에 컨트롤러에서는 다음과 같이 이 API가 어떤 권한을 요구하는지를 명확히 표현할 수 있다.
@PermissionCheck(resource = ResourceType.ADMIN, action = ActionType.CREATE)
5. AOP 인가와의 비교
물론 동일한 구조를 AOP로도 구현할 수 있고 이 방식은 구현이 단순하고 이해하기 쉽다는 장점이 있다. 하지만 사용하면서 아쉬웠던 점도 있었다.
- Security Filter Chain과 분리됨
- 컨트롤러 진입 후 인가 실패
- 401 / 403 처리 흐름을 Security와 일관되게 가져가기 어려움
반면 AuthorizationManager 방식은 구현 난이도는 더 높지만, 아래와 같은 장점이 있었다.
- 인증과 인가가 같은 보안 흐름 안에 있음
- 인가 실패가 표준 Security 방식으로 처리됨
- 보안 정책이 한 곳에 모임
이 차이는 어떻게 보면 구현 방식의 차이가 아니라 보안 책임을 어디에 두느냐에 대한 선택이라고 생각한다.
마무리
AuthorizationManager를 사용한 이 방식이 모든 서비스에 정답이라고 생각하지는 않는다. 권한 모델이 단순하다면, 굳이 AuthorizationManager를 직접 구현하지 않고 hasRole 기반의 기본적인 인가 방식만으로도 충분할 수 있다. 다만 권한이 역할 단위를 넘어 메뉴와 액션처럼 더 세분화된 기준으로 관리되어야 하거나, 인증과 인가의 책임을 명확하게 Security 레이어에서 정리하고 싶다면 AuthorizationManager는 충분히 고민해볼 만한 선택지라고 생각한다.
'Spring' 카테고리의 다른 글
| [Spring] 빈은 순서를 가진다 - Error creating bean 문제 해결 (2) | 2025.01.19 |
|---|---|
| [Spring] Custom Filter로 로그 파밍하기 (CloudWatch X Logback) (4) | 2024.10.09 |
| [Spring] Server-Sent Events로 알람 서비스 개선하기 (0) | 2023.08.28 |
| [Spring] 로컬 캐시를 활용한 Refresh Token 구현기 (feat. Caffeine) (0) | 2023.05.28 |
| [Spring Security] POST 테스트 : 403 Forbidden 에러 해결 (0) | 2023.05.17 |