방학동안 진행한 프로젝트에서 권한과 관련된 부분을 맡게 되었다. 그동안 프로젝트를 하면서 권한에 대해 깊게 고민해본 적이 없었는데, 이번 기회를 통해 다양한 경험을 해볼 수 있었다. 그 중에서 권한 로직을 구현하면서 사용한 SpEL과 AOP에 대해 정리해보려고 한다.
목차는 다음과 같다.
- SpEL
- SpEL이란?
- SpEL을 사용하는 이유와 이점
- AOP
- AOP란?
- AOP를 사용하는 이유와 이점
- 프로젝트에서의 사용기
1. SpEL (Spring Expression Language)
SpEL이란,
SpEL은 Spring 프레임워크에서 제공하는 표현식 언어로, Spring 컨테이너와 런타임객체를 대상으로 값을 evaluate 할 수 있는 표현식 언어이다.
SpEL로 값을 평가하는 방법
SpEL은 Spring 컨테이너(Bean)와 런타임 객체(메서드 파라미터/인증 객체 등)를 EvaluationContext라는 그릇에 올려두고, 표현식을 그 컨텍스트 위에서 평가한다고 이해하면 된다.
SpEL 문자열을 파싱해서 Expression 트리를 생성하고, 해당 Expression을 EvaluationContext 위에서 실행해 실제 값을 얻어오는 방식이다. 문자열을 값으로 바꾸는 방식이고, 그 값은 컨텍스트에 올라와있는 객체들로부터 얻어올 수 있다.
나는 #만 사용했으나, 어떤 기호를 사용하느냐에 따라 어디에서 값을 얻어오는지가 달라진다.
- 런타임 객체 (메서드 파라미터 등) : # 이용
- Spring 컨테이너 (빈) : @ 이용
SpEL을 이용해 아래와 같은 부분들에 대해 문자열 형태로 작성하고 런타임 시점에 동적으로 평가할 수 있다.
- 객체의 property 접근
- 메서드 호출
- 연산
- 조건식 (삼항연산자)
- 컬렉션 필터링
- Spring Bean 참조
- 등등..
예를 들어보자.
@Value("#{2 + 3}")
private int result; // 5
이렇게 #을 이용해 문자열로 연산 값을 주입할 수 있다. 사실 위 예시로는 SpEL을 사용하면 뭐가 좋은지 체감하기 힘들다.
SpEL은 동적으로 평가할 수 있다는 점이 최대 강점이다!
SpEL을 왜 사용해야 할까?
SpEL을 사용하는 이유는 아래와 같다.
- Spring 런타임 컨텍스트를 기반으로, 값을 동적으로 계산하고 참조할 수 있다.
- 코드가 아닌 문자열로 런타임 객체에 접근할 수 있다!
- 자바였다면 코드를 수정해야 하지만, SpEL을 사용하면 설정영역만 바꿔주면 된다.
이렇게 SpEL을 이용하면 정책을 선언하고, 로직을 공동화 할 수 있어 편리하다. 보통 Spring Security에서 특정 메서드에 대한 Role을 검사할 때 많이 사용한다.
@PreAuthorize("hasRole('ADMIN') or #userId == principal.id")
SpEL의 가장 큰 장점은 동적 평가가 가능하다는 것이기 때문에, 설정에서 동적 계산이 필요한 경우 사용하면 좋다.
다만 비즈니스 로직 영역에서는 사용을 지양하는 것이 좋다. 표현식이 복잡해지면 가독성이 떨어지고, 디버깅이 어려울 수 있다.
그리고 꼭! 팀 내 문서화를 해두는 것이 좋다.
2. AOP (Aspect-Oriented Programming)
AOP란,
AOP는 횡단 관심사를 분리하는 프로그래밍 패러다임이다. 쉽게 말하면 비즈니스 로직과 직접적인 관련은 없지만 반복적으로 필요한 부분을 분리하는 패러다임이다. 여기서 말하는 횡단 관심사란
- 로깅
- 트랜잭션 처리
- 예외처리
- 인증/인가
- 캐싱
등을 이야기 한다.
Spring에서는 이러한 것들을 구현하기 위한 AOP 기능을 제공한다.
AOP를 왜 사용해야 할까?
만약 AOP를 사용하지 않고 권한을 관리한다고 가정한다면, 모든 기능에 권한을 위한 로직이 들어가야 한다. 이렇게 되면 중복 코드가 많아지고, 정책이 변경된다면 전체 수정을 하게 될 수도 있다.
AOP를 쓰게 된다면 어떨까? 권한 체크는 한 곳에서 처리하고, 각 API에서는 선언만 하면 된다! 즉,
정책은 선언하고, 집행은 공동 처리가 가능해진다는 것이다. 비즈니스 로직과 권한 로직을 분리함으로서 관리하기도 용이해진다.
이처럼
1. 누락되면 위험한 로직이다 (보안, 권한 관련)
2. 비즈니스 로직과 분리되어야 하는 로직이다
위와 같은 경우 AOP를 사용하면 좋다.
AOP를 사용하는 방법
스프링AOP는 기본적으로 다음과 같이 동작한다.
클라이언트 → 프록시 객체 → (Aspect 실행) → 실제 타겟 메서드
빈을 직접 호출하는 것이 아니라, 프록시 객체를 만들어서 앞에 Aspect를 끼워넣는 방식으로 동작한다.
Aspect? AOP 구성요소 정리
- Aspect : 공통 로직이 들어있는 클래스
- Advice : 언제 실행할지 정의 (ex| @Before, @Around ...)
- Pointcut : 어떤 메서드를 가로챌지 정의
- JoinPoint : 실제 메서드 호출 시점 (실행 지점)
AOP 사용을 위한 순서는 대략 다음과 같다.
- 어노테이션 정의 (선택적)
- @Aspect 작성
- 이게 붙은 클래스가 AOP 역할을 함
- Pointcut 정의
- Advice 작성
- proceed() 호출
3. 프로젝트 도입기
우리 프로젝트는 권한 체계가 상당히 복잡해서 어떻게 설계해야할지 고민이 정말 많았다. 일회성 프로젝트가 아니기 때문에 일일히 하드코딩 하는 건 장기적으로 좋지 않다고 판단했고, 오랜 회의 끝에 AOP을 이용한 RBAC + ABAC 구조를 가져가기로 했다.
AWS의 IAM을 참고해서 크게 세가지로 나누어 생각했다.
- Resource : 우리가 다루고자 하는 대상 (ex| 커뮤니티, 공지 등..)
- Permission : 하고자 하는 행위 (ex| 조회, 삭제, 생성, 관리 등..)
- Subject : 해당 행위를 하는 주인공의 속성 (ex| 우리프로젝트 내에서는 해당 챌린저의 학교, 지부, 기수, 파트 등의 정보)
각 리소스마다 허용되는 행위가 다르기 때문에 위처럼 나누었고, 미리 정의만 해두면 되었다. 예를 들면, "공지사항은 AOP를 통해 조회, 삭제에 대한 검사만 가능하다" 이런식으로 정의해둘 수 있었다. 그 이후에는 만들어둔 커스텀 어노테이션을 컨트롤러단에 붙이기만 하면 되었다. 로직에 직접 추가하는 것보다 훨씬 쉽게 사용할 수 있었다.
또, 리소스별로 evaluator를 따로 구현해서 권한 평가를 각각의 필요에 맞게 구현해두었다!
@CheckAccess(
resourceType = ResourceType.NOTICE,
resourceId = "#noticeId",
permission = PermissionType.READ
)
이런식으로 붙이고, SpEL 표현식을 통해서 공지ID를 동적으로 받아올 수 있도록 만들었다.
1차적으로 완성된 권한 관리 구현은 다음과 같다.
┌─────────────────────────────────────────────────────────────────────┐
│ HTTP Request │
└──────────────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Security Filter Chain │
│ │
│ 1. @Public 체크 → 공개 엔드포인트면 인증 건너뜀 │
│ 2. JwtAuthenticationFilter │
│ ├─ Authorization 헤더에서 Bearer 토큰 추출 │
│ ├─ JwtTokenProvider로 토큰 검증 (서명, 만료) │
│ ├─ memberId, roles 추출 │
│ └─ MemberPrincipal 생성 → SecurityContextHolder에 저장 │
└──────────────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Controller Layer │
│ │
│ @CheckAccess( │
│ resourceType = ResourceType.NOTICE, │
│ resourceId = "#noticeId", ← SpEL 표현식 │
│ permission = PermissionType.READ │
│ ) │
│ void getNotice(@PathVariable Long noticeId) { ... } │
└──────────────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ AccessControlAspect (AOP @Around) │
│ │
│ 1. SecurityContext에서 MemberPrincipal → memberId 추출 │
│ 2. SpEL 파싱: "#noticeId" → 실제 파라미터 값 (예: 42L) │
│ 3. ResourcePermission 객체 생성 │
│ ResourcePermission.of(NOTICE, "42", READ) │
│ 4. CheckPermissionUseCase.check(memberId, permission) 호출 │
└──────────────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ AuthorizationService (CheckPermissionUseCase) │
│ │
│ 1. ResourceType으로 적절한 Evaluator 선택 (Strategy 패턴) │
│ Map<ResourceType, ResourcePermissionEvaluator> │
│ │
│ 2. SubjectAttributes 구성 (사용자 속성 수집) │
│ ├─ memberId, schoolId │
│ ├─ gisuChallengerInfos (기수별 챌린저/챕터/파트 정보) │
│ └─ roleAttributes (역할 목록) │
│ ├─ roleType (CENTRAL_PRESIDENT, SCHOOL_PART_LEADER 등) │
│ ├─ organizationType (CENTRAL / CHAPTER / SCHOOL) │
│ ├─ organizationId │
│ └─ responsiblePart │
│ │
│ 3. evaluator.evaluate(subjectAttributes, resourcePermission) │
└──────────────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ ResourcePermissionEvaluator (리소스별 구현체) │
│ │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ NoticePermissionEvaluator│ │WorkbookSubmission │ │
│ │ │ │ PermissionEvaluator │ ... │
│ │ READ: 대상 기수/지부/학교 │ │ │ │
│ │ CHECK: 조직 레벨별 판단 │ │ │ │
│ └──────────────────────────┘ └──────────────────────────┘ │
│ │
│ SubjectAttributes(RBAC + ABAC)로 판단 │
│ ├─ RBAC: 역할 기반 (총괄, 회장, 파트장 등) │
│ └─ ABAC: 속성 기반 (소속 학교, 기수, 지부, 파트) │
└──────────────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────┐
│ true → 메서드 실행 │
│ false → 403 Forbidden│
└─────────────────────┘
사실 공지 작성의 경우, 해당 요소들 + 공지 대상에 대한 속성이 필요하기 때문에 AOP를 사용하지 못하고 로직에서 직접 구현해 넣었다. 이러한 도메인이 생각보다 많아서 AOP를 뽕뽑아 쓰지 못한 기분이라,, 상당히 아쉬움이 남는다. 해당 부분은 더 고민해보고 공부해서 다시 정리해 두어야 할 거 같다!
'백엔드 > 스프링' 카테고리의 다른 글
| [Spring] @Valid를 이용한 유효성 검증 알아보기 (feat. @NotBlank, @NotNull, @NotEmpty) (2) | 2025.07.19 |
|---|---|
| [Spring] JAVA 로깅 알아보기 (로그레벨, @Slf4j) (0) | 2024.04.03 |
| [Spring] 용어정리 - DTO, DAO, TDD (0) | 2024.03.29 |
| [Spring] 기초 어노테이션 학습 - @NoArgsContructor, @Data, @Builder (0) | 2024.03.28 |
| [Spring] 5. 빈 생명주기 콜백- 스프링 핵심원리 기본편 (1) | 2024.02.09 |