1. 개요
Spring AOP(Aspect-Oriented Programming)를 활용하면,
권한 체크 로직을 모든 컨트롤러 메서드마다 반복해서 작성할 필요 없이
관점 지향 프로그래밍(Aspect-Oriented Programming) 방식으로 깔끔하게 분리할 수 있다.
Spring AOP를 이용하여 API 호출 시 특정 역할(Role)만 접근 가능하도록 제한하는 방법을 정리하겠다.
2. 구현
2 - 1. @RequireRole 어노테이션 생성
먼저, 권한 체크를 적용할 메서드에 사용할 커스텀 어노테이션을 생성한다.
package com.example.infra.aspect;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) // 메서드에만 적용 가능
@Retention(RetentionPolicy.RUNTIME) // 런타임에도 유지됨
public @interface RequireRole {
String[] value(); // 허용된 권한 리스트
}
이제 @RequireRole({"MASTER", "HUB"}) 와 같이 특정 역할을 가진 사용자만 접근할 수 있도록 설정할 수 있다.
2 - 2. AOP 클래스 구현
package com.example.infra.aspect;
import com.example.common.exception.ErrorCode;
import com.example.common.exception.CustomException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class RoleCheckAspect {
private final HttpServletRequest request;
@Before("@annotation(requireRole)")
public void checkRole(RequireRole requireRole) {
// 헤더에서 역할(Role) 정보 가져오기
String role = request.getHeader("X-Role");
if (role == null) {
throw new CustomException(ErrorCode.MISSING_ROLE);
}
log.info("Checking role: {}", role);
// 허용된 권한 체크
List<String> allowedRoles = List.of(requireRole.value());
if (!allowedRoles.contains(role)) {
throw new CustomException(ErrorCode.FORBIDDEN_ACCESS);
}
}
}
✨ 주요 로직 설명
- HTTP 요청 헤더에서 X-Role 값을 가져온다.
- 헤더 값이 없으면 예외를 발생시킨다. (ErrorCode.MISSING_ROLE)
- 해당 역할이 어노테이션에 명시된 값과 일치하지 않으면 예외를 발생시킨다. (ErrorCode.FORBIDDEN_ACCESS)
- AOP를 적용하여 메서드 실행 전에 @RequireRole이 붙어 있는 경우 권한을 체크한다.
2 - 3. AOP 설정 활성화
Spring에서 AOP를 활성화하려면 @EnableAspectJAutoProxy를 설정해야 한다.
package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true) // CGLIB 기반 프록시 사용
public class AopConfig {
}
🔍 proxyTargetClass = true를 사용하는 이유
- Spring AOP는 기본적으로 JDK 동적 프록시(인터페이스 기반)와 CGLIB 프록시(클래스 기반) 두 가지 방식이 있다.
- proxyTargetClass = true를 설정하면 인터페이스가 없어도 프록시 적용 가능하며,
- 클래스 기반(CGLIB) 프록시를 강제 사용합니다.
- 인터페이스가 없는 클래스에도 AOP 적용이 필요할 때 유용합니다.
2 - 4. 컨트롤러에 적용
이제 실제 컨트롤러에서 @RequireRole을 활용하여 특정 API에 대해 권한 체크를 수행할 수 있다.
package com.example.controller;
import com.example.common.response.ApiResponse;
import com.example.dto.CompanyResponse;
import com.example.service.CompanyService;
import com.example.infra.aspect.RequireRole;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/api/companies")
@RestController
public class CompanyController {
private final CompanyService companyService;
/**
* 업체 생성 (MASTER, ADMIN 역할만 가능)
*/
@RequireRole({"MASTER", "HUB"})
@PostMapping
public ApiResponse<CompanyResponse> createCompany(
@RequestHeader("X-User-Id") String userId,
@RequestHeader("X-Role") String role) {
log.info("User ID: {}, Role: {}", userId, role);
CompanyResponse response = companyService.createCompany(userId);
return ApiResponse.success(response);
}
}
이제 @RequireRole({"MASTER", "HUB"})이 적용된 createCompany() 메서드는
MASTER 또는 HUB 역할을 가진 사용자만 호출할 수 있습니다.
3. 테스트 하기
3 - 1. MASTER, HUB 가 아닌 권한으로 createCompany를 호출 했을 때
3 - 2. MASTER 또는, HUB 권한으로 createCompany를 호출 했을 때
4. 정리
- AOP를 사용하여 API의 권한 체크 로직을 깔끔하게 분리할 수 있었다.
- 중복되는 코드 없이 어노테이션만으로 특정 API의 접근 권한을 쉽게 설정할 수 있다.
- Spring AOP를 사용할 때 @EnableAspectJAutoProxy(proxyTargetClass = true) 설정을 확인해야 한다.
이제 새로운 API를 만들 때마다 일일이 권한 체크 로직을 작성할 필요 없이 @RequireRole 어노테이션을 추가하는 것만으로 간단하게 적용할 수 있게 되었다. 🚀
'Spring' 카테고리의 다른 글
LocalDateTime Jackson 직렬화 오류 해결하기 (1) | 2025.01.22 |
---|---|
운영 환경에서 Spring Cloud Config Server를 의존하지 않으려면 (0) | 2025.01.21 |
Spring Boot 에서 Snake Case 형식으로 JSON 응답하기 (0) | 2025.01.08 |