어노테이션?
어노테이션은 메서드처럼 실행되는 로직이 아니다. 단순히 특정 위치(ex.클래스,메서드,파라미터)에 부착된 메타데이터다.
어노테이션으로 인해 수행되는 동작은 해당 어노테이션을 탐색하고 해석하는 프레임워크 컴포넌트에 의해서 결정된다.

위 표에서 보듯이 어노테이션을 해석하는 주체는 다양하지만, 실무에서 자주 만들어지는 커스텀 어노테이션의 상당수는 메서드 실행 전/후에 공통 로직을 적용하기 위한 목적을 가진다.
이러한 경우, 메서드 실행 흐름에 개입할 수 있는 Spring AOP가 해석 주체로 선택되는 경우가 많다.
Spring AOP?
먼저 AOP는 프로그래밍 방식이고, Spring AOP는 이 AOP라는 프로그래밍 방식을 구현한 스프링의 AOP엔진(여러 컴포넌트들의 묶)이다.
*정확히 말하자면, Spring AOP는 AOP 프로그래밍 방식을 '프록시 기반 런타임 방식'으로 구현한 스프링의 AOP엔진이다.
AOP는 어떤 프로그래밍 방식인가?
공통 관심사를 비즈니스 코드(핵심 관심사) 밖으로 분리하는 '프로그래밍 방식'이다.
여기서 말하는 공통 관심사는 여러 객체나 메서드에서 공통적으로 사용되는 코드를 말하고,
핵심 관심사는 각 객체나 메서가 가져야할 본래의 기능이다.
여러 개의 메서드에서 반복해서 사용하는 코드가 있다면 즉 공통적으로 사용되는 코드가 있다면,
해당 코드를 모듈화하여 공통 관심사로 분리하는 것이다. 그리고 이렇게 분리한 공통 관심사를 Aspect라고 정의한다.
예시로 게시글 수정과 같은 아래와 같은 코드가 있다고 해보자.
//게시글 수정
public void updatePost() {
log.info("start"); // 로그(운영 관심사 ->공통 관심사)
checkLogin(); // 권한 체크(보안 관심사 ->공통 관심사)
startTx(); // 트랜잭션 시작(인프라 관심사->공통 관심사)
doUpdate(); // 수정(실제 비즈니스 관심사->핵심 관심사)
commitTx(); // 트랜잭션 종료(인프라 관심사->공통 관심사)
}
우리가 객체지향(OOP)로 개발하다보면 하나의 기능을 위해 여러 관심사가 섞이게 된다.
그리고 예시로 든 게시글 수정 뿐만 아니라 여러 메서드에 로그,권한 체크, 트랜잭션 관련한 동일한 코드가 계속해서 반복될 것이다.
이러한 비즈니스 로직과 상관없는 공통적인 관심사(운영,보안,인프라)와 같은 코드를 분리하자는게 바로 이 AOP의 시작점이다.
AOP의 개념
- AOP 프록시 : 이 AOP(관점지향프로그래밍)을 구현 하기 위해 만든 프록시 객체.
- Aspect : 공통적인 기능들을 모듈화 한것을 의미
- Join point : Aspect가 적용될 수 있는 시점(ex.메서드 실행 전, 후 등)
- Advice : 실제 이 Aspect의 기능을 정의 한 곳. 실제 실행되는 코드를 의미
- Point cut : Advice를 적용할 메서드의 범위를 지정하는 것을 의미
- Target: Advice의 대상이 되는 객체, 포인트 컷으로 결정 됨
AOP 적용 방식
- 컴파일 시점(.java -> .class 로 컴파일 할 때) -AspectJ
- 클래스 로딩 시점(JVM 이 .class를 메모리에 올릴 때) -AspectJ
- 런타임 시점(애플리케이션 실행중일 때) -Spring AOP
컴파일 시점과 클래스 로딩 시점 적용 방식은 AspectJ라는 프레임워크를 직접 사용해야한다.
하지만 이에 대한 설정이 너무 복잡하다.
그래서 스프링 프레임워크의 Spring AOP도 런타임 시점에 AOP를 적용하는 방식을 택하고 있다.
메서드 실행 전·후에 공통 로직을 적용하기 위한 커스텀 어노테이션을 만드는 경우에는
Spring AOP를 적용하게 된다. 이 Spring AOP를 적용하기 위해서는 먼저 아래와 같은 의존성을 추가하면 된다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
그런데 여기서 의문이 들 것이다.
이미 이런 의존성을 추가하기 전에, 나는 해석 주체가 Spring AOP인 @Transactional 어노테이션을 잘 실행하고 있었다.
그렇다는 것은 이미 Spring AOP가 내장되어있다는 건데 왜? Spring AOP의존성을 추가할까?
Spring AOP는 하나의 엔진이지만 두 가지 역할로 사용되기 때문이다.
스프링은 프레임워크로써, 이미 Spring AOP를 내부적으로 사용하고 있지만,
이 경우 개발자가 직접 AOP를 정의하거나 제어하진 않는다.
즉, Spring AOP는 프레임워크 내부 기능으로는 기본 포함되어 있지만,개발자가 직접 Aspect를 정의하고 커스텀 어노테이션과 연계하려면 관련 설정과 자동 구성이 활성화되어야 한다.
그래서 이러한 의존성 추가가 필요하다.
Spring AOP의 작동원리
이 의존성을 추가하고 커스텀 어노테이션을 만들기 전에, 이 AOP의 작동원리에 대해서 조금 더 깊게 들어가보자.
첫 문단에서 아래와 같이 작성한 부분이 있다.
*정확히 말하자면, Spring AOP는 AOP 프로그래밍 방식을 '프록시 기반 런타임 방식'으로 구현한 스프링의 AOP엔진이다.
앞선 설명으로 인해 Spring AOP는 AOP프로그래밍 방식을 런타임 방식으로 구현한 엔진이라는 개념은 이해했을 것이다.
그런데 프록시 기반이라는게 무엇인가?
아래와 같이 Transactional어노테이션이 붙은 메서드가 있다고 하자.
Transactional은 해당 어노테이션이 붙은 메서드가 호출되기 전에 트랜잭션을 열고 메서드가 종료되면 트랜잭션을 닫는 역할을 하는 어노테이션이다.
//게시글 수정
@Service
public class PostService{
@Transactional
public void updatePost() {
doUpdate();
}
}
그런데 문제는 자바라는 언어에서는 이 updatePost()메서드를 실행하면 doUpdate()라는 내부 로직을 실행하지 메서드 바깥의 어노테이션을 실행하는 기능은 없다. 다시 말하자면, 자바에는 메서드가 실행되기 직전에 자동으로 어노테이션을 실행해주는 기능이 없다는 것이다. 그래서 필요한 것이 프록시 객체다.
자바에서 postService.updatePost()가 실행되면 postService객체를 먼저 가져오는 것이 아니라 postServiceProxy라는 postService를 감싼 프록시 객체를 먼저 가져온다.
그리곤 메서드 바깥의 어노테이션을 해석해서 그에 맞는 동작을 수행하는 것이다. 그러곤 진짜 postService객체를 가지고 와서 updatePost()를 실행하고 종료되면 postProxy.updatePost도 종료되는 것이다.
클라이언트
↓
postServiceProxy.updatePost()
↓
[트랜잭션 시작]
↓
postService.updatePost()
↓
[트랜잭션 종료]
다시 말해서 프록시 객체란, 실제 객체와 동일한 타입을 가지면서 메서드 호출을 대신 받아 추가 로직을 수행한 뒤 실제 객체에게 처리를 위임하는 대리객체다.
이처럼 Spring AOP는 실제 객체를 직접 수정하지 않고,해당 객체를 감싼 프록시 객체를 통해 메서드 호출 흐름에 개입하는 방식을 사용한다. 이를 프록시 기반 AOP라고 한다.
프록시 생성 방식
그러면 스프링은 어떻게 이 프록시 객체를 만드는 걸까?
스프링은 아래와 같은 두 가지 프록시 생성 방식을 가진다.
- JDK 동적 프록시 (자바 기본 기능)
- CGLIB 프록시 (바이트코드 조작)
JDK 동적프록시
이 방식은 인터페이스를 기반으로 인터페이스를 구현한 가짜 객체를 만드는 방식이다.
public interface PostService {
void updatePost();
}
public class PostServiceImpl implements PostService {
public void updatePost() { ... }
}
위와 같은 코드가 있을 때 자동으로 PostService 인터페이스를 구현한 프록시 객체를 만드는 것이다.
위 방식처럼 작동되려면 보시다시피 먼저 인터페이스가 구현되어있어야한다.
CGLIB 방식
JDK 동적 프록시를 사용하기 위해서는 일일이 인터페이스를 만들어줘야한다. 그래서 인터페이스가 없는 클래스에도 프록시객체를 만들기 위해서 나온 것이 이 CGLIB방식으로 런타임에 바이트코드를 생성하여 대상 클래스를 상속한 프록시 객체를 만든다.
(그런데 클래스가 final인 경우 상속이 불가하므로 CGLIB 프록시가 작동되지않는다)
스프링은 인터페이스가 있으면 JDK동적 프록시를 사용하고 인터페이스가 없으면 CGLIB방식을 사용하는 것이 기본 설정이다.
Spring AOP에 대한 설명이 길어졌다.
어찌됐든 커스텀 어노테이션을 만든다는 것은 어노테이션 자체를 실행하는 것이 아니라,
어떤 시점에 어떤 프레임워크 컴포넌트가 그 의미를 해석하도록 맡길지를 설계하는 일이다.
이제는 메서드 실행 시간을 측정하는 커스텀 어노테이션을 만들어보려고 한다.
이것은 메서드 실행 시간 측정이라는 공통 관심사로서 AOP 프로그래밍 방식으로서의 목적과
메서드 실행 전/후에 개입되어야기 때문에 Spring AOP 컴포넌트가 해석하도록 해야하는게 명확하기 때문이다.
'스프링부트 > Code Tip' 카테고리의 다른 글
| Presigned URL을 통한 S3 업로드 (1) | 2026.02.02 |
|---|---|
| [Spring] 커스텀 어노테이션 만들기 - 2.메서드 실행시간 측정 어노테이션 (1) | 2025.12.22 |
| TMDB API를 통한 드라마/영화 정보 받아오기 (1) | 2025.09.15 |
| Amazon S3를 통해 이미지 업로드할 때 (0) | 2025.09.06 |