Spring AOP를 활용하면, Controller 파라미터에 특정 값을 자동으로 주입하거나 메서드 실행 전·후에 공통 로직을 삽입하는 흐름을 직접 구현할 수 있습니다.

이번 글에서는 @CurrentUserId 라는 커스텀 어노테이션을 만들어, Controller 메서드에서 아래처럼 userId를 직접 받지 않아도 되도록 구성해보겠습니다.

@GetMapping("/recommendations")
public ResponseEntity<Map<String, Object>> getRecommendations(@CurrentUserId Long userId)

이 userId 값은 우리가 만든 AOP @Around 어드바이스가 자동으로 주입하게 됩니다.

프로젝트 초기 

Spring Initializr에서 기본 Dependencies를 추가한 뒤 프로젝트를 생성한 후, pom.xml에 아래 의존성을 추가합니다.

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
			<version>4.0.0-M2</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

전체 흐름 (AOP Flow)

HTTP 요청
 → DispatcherServlet
 → Controller 메서드 호출 시도
 → Spring AOP 프록시 개입(@Around)
 → @CurrentUserId 파라미터 탐색
 → userId 추출 + 검증
 → 원래 Controller 메서드 실행

핵심은 Controller 메서드가 호출되기 전에 AOP가 먼저 실행되어 파라미터 검증과 값 주입을 먼저 처리한다는 점입니다.

Controller (Mock API)

우리가 만들어볼 AOP flow는 다음과 같습니다. 

데이터베이스 없이 바로 테스트 가능한 간단한 Mock API를 구성했습니다.

@RestController
@RequestMapping("/api/v1/mock")
public class AopMockController {

    private final List<User> mockList;

    public AopMockController() {
        this.mockList = createMockData();
    }

    @GetMapping("/recommendations")
    public ResponseEntity<Map<String, Object>> getRecommendations(@CurrentUserId Long userId) {

        Map<Long, String> userPreferredRegion = Map.of(
                1L, "강남구",
                2L, "홍대"
        );

        String preferredCity = userPreferredRegion.getOrDefault(userId, "강남구");

        List<User> recommended = mockList.stream()
                .filter(r -> r.getCity().equals(preferredCity))
                .limit(3)
                .toList();

        Map<String, Object> response = new HashMap<>();
        response.put("userId", userId);
        response.put("recommendedCity", preferredCity);

        return ResponseEntity.ok(response);
    }

    // Mock 데이터 생성
    private List<User> createMockData() {
        List<User> mockList = new ArrayList<>();

        mockList.add(new User(1L, "강남구"));
        mockList.add(new User(2L, "홍대"));
        mockList.add(new User(3L, "이태원"));
        mockList.add(new User(4L, "부산"));
        mockList.add(new User(5L, "대구"));

        return mockList;
    }
}

@CurrentUserId 어노테이션 정의

메서드 파라미터에 붙일 전용 어노테이션입니다.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUserId {
}

Domain (User)

간단한 Mock 도메인 객체입니다.

@Getter
@Setter
@AllArgsConstructor
public class User {
    private Long id;
    private String city;
}

AOP @Around — 파라미터 자동 주입 핵심 로직

Controller 호출 전에 실행되는 AOP 구성입니다.

@Around("execution(* aop.test.aop.demo..controller.*.*(..))") 로 pointcut 은 Controller 패키지의 모든 메서드에서 적용됩니다.

@CurrentUserId 발견하면 userId 를 추출합니다. 그리고 userId 가 1L인지 검증하는 로직을 수행합니다.  

@Aspect
@Component
@Order(1)
public class CurrentUserAspect {

    @Around("execution(* aop.test.aop.demo..controller.*.*(..))")
    public Object injectCurrentUserId(ProceedingJoinPoint joinPoint) throws Throwable {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        Parameter[] parameters = method.getParameters();
        Object[] args = joinPoint.getArgs();

        for (int i = 0; i < parameters.length; i++) {
            if (parameters[i].isAnnotationPresent(CurrentUserId.class)) {

                Long userId = extractUserIdFromRequest();
                validateUserId(userId);

                args[i] = userId;

                System.out.println("[AOP] @CurrentUserId → " + userId + " 주입됨");
            }
        }

        return joinPoint.proceed(args);
    }

 

 

 

 

요청에서 userId 추출하기

private Long extractUserIdFromRequest() {
    ServletRequestAttributes attributes =
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

    if (attributes == null) return null;

    HttpServletRequest request = attributes.getRequest();

    String userIdParam = request.getParameter("userId");
    if (userIdParam != null) {
        return Long.parseLong(userIdParam);
    }

    return null;
}

userId 검증

private void validateUserId(Long userId) {
    final Long VALID_USER_ID = 1L;

    if (userId == null) {
        throw new IllegalArgumentException("userId는 필수입니다. (?userId=1)");
    }

    if (!userId.equals(VALID_USER_ID)) {
        throw new IllegalArgumentException("유효하지 않은 userId입니다.");
    }
}

실행 결과

정상 케이스

GET /api/v1/mock/recommendations?userId=1

AOP가 userId를 인식 → 검증 성공 → Controller 실행

실패 케이스 (userId 2)

GET /api/v1/mock/recommendations?userId=2

 

구성

/src
 ├─ controller/
 ├─ aspect/
 ├─ annotation/
 ├─ domain/
 └─ DemoApplication.java

pom.xml
README.md