로깅, 성능 측정, 보안, 트랜잭션, 예외 처리처럼 여러 계층과 메서드에 걸쳐 나타나는 기능을 횡단 관심사라고 부른다.
Spring AOP는 Spring Bean의 메서드 실행 경계에 횡단 관심사를 붙여 중복을 줄이고, 서비스 코드는 업무 규칙에 집중하게 만든다.
핵심 정리
핵심 관심사는 애플리케이션이 실제로 해결해야 하는 업무 규칙이다. 주문 생성, 회원 가입, 게시글 저장 같은 기능이 여기에 해당한다.
횡단 관심사는 여러 핵심 기능 앞뒤에 반복해서 붙는 공통 기능이다. 로깅, 성능 측정, 트랜잭션, 캐싱, 메서드 보안, 예외 변환이 대표적이다.
AOP가 필요한 이유는 부가 기능을 없애기 위해서가 아니라, 부가 기능이 핵심 로직 안에 흩어지면서 코드의 의도와 변경 지점이 흐려지기 때문이다.
같은 로깅 정책이나 트랜잭션 규칙이 여러 메서드에 복사되어 있으면 정책 하나를 바꿀 때 여러 파일을 찾아 수정해야 한다. AOP는 이 변경 지점을 한곳으로 모은다.
AOP는 객체지향을 대체하는 기술이 아니라 객체지향 코드에서 반복적으로 섞이는 관심사를 별도의 관점으로 분리해 보완하는 방식이다. Spring AOP에서는 그 분리 지점이 주로 Bean 메서드 호출 경계라는 점까지 함께 기억해야 한다.
헷갈리는 지점
부가 기능을 분리한다고 해서 부가 기능이 덜 중요하다는 뜻은 아니다. 이름이 부가 기능이라서 선택적인 기능처럼 들리기 때문이다.
핵심은 중요도와 위치를 구분하는 것이다.
트랜잭션과 보안은 중요하지만 여러 핵심 로직에 반복되므로 분리해서 관리하는 편이 낫다.
AOP를 쓰면 비즈니스 로직이 사라진다고 오해하기 쉽다. AOP가 메서드 실행 전후에 코드를 끼워 넣기 때문이다.
핵심은 AOP가 핵심 로직을 대체하는 것이 아니라는 점이다.
핵심 로직이 실행되는 흐름 주변에 공통 처리를 붙인다고 이해해야 한다.
AOP를 HTTP 요청 앞단의 모든 공통 처리에 쓰면 된다고 생각하기 쉽다. 횡단 관심사라는 말이 넓게 들리기 때문이다.
핵심은 Spring AOP가 Servlet 요청 전체가 아니라 Spring Bean 메서드 실행을 감싼다는 점이다.
CORS, 원시 request/response 조작, Security FilterChain 문제는 Filter나 MVC 확장점이 더 자연스러울 수 있다.
중복 코드가 몇 줄 안 되면 AOP가 필요 없다고 판단하기 쉽다. 현재 줄 수만 보기 때문이다.
핵심은 줄 수보다 변경 지점의 개수다.
같은 정책이 여러 곳에 흩어져 있다면 작은 코드라도 유지보수 비용은 커진다.
확인 질문
AOP가 해결하려는 가장 기본적인 문제는 무엇인가?
핵심 비즈니스 로직 안에 로깅, 보안, 트랜잭션 같은 횡단 관심사가 섞여 중복과 변경 비용이 커지는 문제다.
횡단 관심사는 왜 별도로 모아야 하는가?
여러 메서드와 계층에 반복되기 때문에 한 정책을 바꿀 때 수정 지점을 줄이고 일관성을 유지하기 위해서다.
AOP는 객체지향 설계를 대신하는가?
아니다. 객체지향으로 나눈 도메인 코드 위에서 반복적으로 섞이는 공통 관심사를 추가로 분리하는 보완적 설계 방식이다.
AOP 주요 용어
3줄 요약
Target은 AOP가 적용될 핵심 로직 객체이고, Aspect는 횡단 관심사를 모아 둔 모듈이다.
Advice는 실제로 실행될 부가 기능 코드이고, Pointcut은 어떤 Join point에 Advice를 적용할지 고르는 조건이다.
Weaving은 Aspect를 Target의 실행 흐름에 연결하는 과정이며, Spring AOP에서는 주로 프록시 생성으로 이루어진다.
핵심 정리
Target은 핵심 비즈니스 로직을 가진 Spring Bean이다. AOP가 적용되면 외부 호출자는 Target을 직접 호출하는 것이 아니라 Target을 감싼 Proxy를 호출한다.
Aspect는 횡단 관심사를 하나의 관점으로 모은 단위다. 보통 @Aspect 클래스로 작성하고, 내부에 Advice와 Pointcut을 함께 둔다.
Advice는 언제 무엇을 실행할지 정의한 메서드다. 실행 시점에 따라 @Before, @After, @AfterReturning, @AfterThrowing, @Around로 나뉜다.
Join point는 Advice가 적용될 수 있는 지점이다. Spring AOP에서는 필드 접근이나 생성자 호출이 아니라 Spring Bean의 메서드 실행 시점으로 제한된다.
Pointcut은 여러 Join point 중 Advice를 실제로 적용할 대상을 고르는 표현식이다. 메서드 시그니처, 패키지, 클래스, 빈 이름, 애너테이션 등을 기준으로 고를 수 있다.
Weaving은 Aspect가 핵심 로직에 연결되는 과정이다. Spring AOP는 컴파일 결과를 직접 바꾸기보다 런타임에 Proxy를 만들어 호출 흐름을 우회시킨다.
헷갈리는 지점
Aspect와 Advice를 같은 것으로 보기 쉽다. 둘 다 횡단 관심사를 설명할 때 같이 등장하기 때문이다.
핵심은 Aspect는 묶음이고 Advice는 실행 코드라는 점이다.
Aspect 안에 Pointcut과 Advice가 함께 들어가며, Advice는 특정 시점에 실제로 동작한다.
Join point와 Pointcut을 혼동하기 쉽다. 둘 다 적용 위치와 관련된 말이기 때문이다.
핵심은 Join point는 후보 지점이고 Pointcut은 선택 조건이라는 점이다.
Spring AOP에서는 메서드 실행 지점들이 후보가 되고, Pointcut이 그중 Advice를 붙일 대상을 고른다.
Target과 Proxy를 같은 객체처럼 생각하기 쉽다. 외부에서 주입받는 타입이 보통 Target의 인터페이스나 클래스 타입이기 때문이다.
핵심은 호출자가 받는 객체가 실제 Target이 아니라 Proxy일 수 있다는 점이다.
Advice는 Target 자체가 아니라 Proxy를 통과하는 호출 흐름에서 실행된다.
확인 질문
Advice와 Pointcut의 역할 차이는 무엇인가?
Advice는 실행할 부가 기능 코드이고, Pointcut은 그 코드를 어디에 적용할지 고르는 조건이다.
Spring AOP에서 Join point는 무엇으로 제한되는가?
Spring Bean의 메서드 실행 시점으로 제한된다.
Weaving은 Spring AOP에서 주로 어떤 방식으로 일어나는가?
Target을 감싸는 Proxy를 만들고 그 Proxy가 Advice와 Target 호출을 연결하는 방식으로 일어난다.
Spring AOP 프록시 동작 원리
3줄 요약
Spring AOP는 원본 클래스 바이트코드를 직접 수정하지 않고 Proxy를 만들어 외부 호출 앞뒤에 Advice를 실행한다.
@Aspect와 @Component로 Aspect를 Bean으로 등록하고, spring-boot-starter-aop 의존성을 통해 AOP 자동 설정을 활성화한다.
Spring은 Bean 생성 과정에서 Pointcut을 검사해 AOP 적용 대상이면 원본 Bean 대신 Proxy를 컨테이너에 노출한다.
핵심 정리
Spring AOP의 핵심은 Proxy다. 클라이언트가 Spring Bean을 호출하면 Proxy가 먼저 요청을 받고, Pointcut에 맞는 Advice를 실행한 뒤 Target 메서드를 호출한다.
@Aspect는 해당 클래스가 AOP 관점이라는 뜻이고, @Component는 그 클래스를 Spring Bean으로 등록하겠다는 뜻이다. 둘 중 하나만으로는 일반적인 Spring AOP 적용 흐름이 완성되지 않는다.
spring-boot-starter-aop는 AspectJ 표현식 해석과 Spring AOP 자동 설정에 필요한 의존성을 제공한다. 의존성이 없으면 @Aspect를 작성해도 자동 프록시 생성 흐름이 동작하지 않을 수 있다.
JDK Dynamic Proxy는 인터페이스를 기준으로 Proxy를 만든다. 인터페이스 중심 설계에서 자연스럽고, Proxy 타입도 인터페이스 타입으로 다루는 것이 안정적이다.
CGLIB은 클래스를 상속해 Proxy를 만든다. Spring Boot 자동 설정에서는 클래스 기반 Proxy가 기본값이지만, final 클래스, final 메서드, private 메서드는 상속과 오버라이딩 제약 때문에 Advice 대상이 될 수 없다.
Spring AOP는 외부에서 Proxy를 통해 들어오는 호출에 강하다. 같은 클래스 내부에서 this.someMethod()처럼 자기 자신을 호출하면 Proxy를 거치지 않아 Advice가 적용되지 않는 self-invocation 문제가 생길 수 있다.
헷갈리는 지점
AOP가 원본 메서드 안에 코드를 삽입한다고 생각하기 쉽다. 실행 결과만 보면 메서드 앞뒤에 코드가 들어간 것처럼 보이기 때문이다.
핵심은 원본 코드는 그대로 두고 Proxy가 호출 흐름을 감싼다는 점이다.
그래서 Proxy를 통과하지 않는 호출은 Advice 적용 대상이 아닐 수 있다.
@Aspect만 붙이면 바로 동작한다고 오해하기 쉽다. 애너테이션 이름이 기능 전체를 대표하는 것처럼 보이기 때문이다.
핵심은 Aspect 클래스도 Spring Bean이어야 한다는 점이다.
AOP 자동 설정과 Proxy 생성 조건까지 맞아야 실제 호출 흐름에 Advice가 붙는다.
JDK Proxy와 CGLIB 중 하나만 정답이라고 생각하기 쉽다. Spring Boot에서 CGLIB을 기본처럼 접하는 경우가 많기 때문이다.
핵심은 인터페이스 기반 프록시는 JDK Proxy로 가능하고, 클래스 자체를 프록시해야 하면 CGLIB이 필요하다는 점이다.
설계 방식, 주입 타입, final 제약에 따라 선택과 한계가 달라진다.
내부 메서드 호출에도 AOP가 항상 적용된다고 생각하기 쉽다. 같은 객체의 메서드를 호출하므로 Spring이 관리한다고 느껴지기 때문이다.
핵심은 Proxy를 통과한 호출인지 확인하는 것이다.
this.someMethod() 호출은 이미 Target 내부에서 일어나므로 Proxy를 다시 지나지 않는다.
확인 질문
Spring AOP가 원본 코드를 수정하지 않고 부가 기능을 적용하는 방법은 무엇인가?
Target을 감싸는 Proxy를 만들고, Spring Bean 외부 호출이 Proxy를 지나가게 해서 Advice를 실행한다.
@Aspect와 @Component는 각각 어떤 역할을 하는가?
@Aspect는 AOP 관점 클래스임을 표시하고, @Component는 Spring 컨테이너가 관리하는 Bean으로 등록한다.
self-invocation에서 Advice가 적용되지 않을 수 있는 이유는 무엇인가?
같은 객체 내부의 this 호출은 Spring Proxy를 거치지 않기 때문이다.
Pointcut 작성 가이드
3줄 요약
Pointcut은 Advice를 적용할 메서드 실행 지점을 선별하는 조건식이다.
execution은 메서드 시그니처 기준, within은 타입과 패키지 범위 기준, bean은 Bean 이름 기준, @annotation은 애너테이션 기준으로 대상을 고른다.
*, .., &&, ||, !, named pointcut을 조합하면 넓은 범위와 좁은 예외 조건을 함께 표현할 수 있다.
핵심 정리
execution은 가장 자주 쓰는 Pointcut 지정자다. 반환 타입, 패키지, 클래스, 메서드 이름, 파라미터 조건을 이용해 메서드 실행 시점을 고른다.
execution(* com.example..service.*Service.*(..))는 com.example 아래 여러 하위 경로 중 service 패키지 바로 아래의 *Service 클래스에 있는 모든 파라미터의 모든 메서드를 대상으로 삼는 식이다.
within은 특정 타입이나 패키지 범위 안의 메서드를 고를 때 읽기 쉽다. 메서드 이름보다 클래스나 패키지 소속이 더 중요한 경우에 적합하다.
bean은 Spring Bean 이름을 기준으로 대상을 고른다. 구현 클래스의 패키지보다 Bean 등록 이름이 정책 기준일 때 사용한다.
@annotation은 특정 애너테이션이 붙은 메서드를 고른다. 보안 검사, 감사 로그, 성능 측정처럼 명시적 표시가 필요한 기능에 적합하다.
*는 한 구간의 임의 값을 뜻하고, ..는 패키지 경로나 파라미터 목록에서 0개 이상을 뜻한다. 둘은 비슷해 보이지만 표현하는 폭이 다르다.
named pointcut은 반복되는 Pointcut 표현식을 메서드 이름으로 분리해 재사용하는 방식이다. Pointcut이 길어지거나 여러 Advice에서 공유될 때 가독성과 변경 비용을 줄인다.
헷갈리는 지점
execution과 within을 같은 기준으로 이해하기 쉽다. 둘 다 패키지와 클래스명을 표현할 수 있기 때문이다.
핵심은 execution은 메서드 시그니처 중심이고 within은 타입이 속한 범위 중심이라는 점이다.
메서드 이름과 파라미터까지 중요하면 execution, 소속 타입과 패키지가 중요하면 within을 먼저 떠올린다.
*와 ..를 모두 아무거나라는 뜻으로만 기억하기 쉽다. 둘 다 범위를 넓히는 와일드카드이기 때문이다.
핵심은 *는 한 자리의 임의 값이고 ..는 여러 단계나 여러 파라미터를 허용한다는 점이다.
패키지 경로와 파라미터 목록에서 ..가 의미하는 폭을 특히 조심해야 한다.
Pointcut을 넓게 잡는 것이 편하다고 느끼기 쉽다. 처음에는 많은 대상을 한 번에 처리하는 것이 효율적으로 보이기 때문이다.
핵심은 AOP가 의도하지 않은 메서드에도 조용히 적용될 수 있다는 점이다.
넓게 시작하더라도 예외 조건, 애너테이션, 이름 규칙으로 적용 범위를 관리해야 한다.
Spring AOP Pointcut에서 AspectJ의 모든 지정자를 쓸 수 있다고 생각하기 쉽다. 표현식 문법이 AspectJ와 닮았기 때문이다.
핵심은 Spring AOP가 메서드 실행 Join point만 지원하므로 call, get, set 같은 일부 지정자는 지원하지 않는다는 점이다.
필드 접근이나 생성자 호출까지 필요하면 Spring AOP가 아니라 AspectJ weaving 범위인지 다시 판단한다.
확인 질문
메서드 이름과 파라미터까지 기준으로 삼고 싶다면 어떤 지정자가 적합한가?
execution이 적합하다.
특정 애너테이션이 붙은 메서드에만 Advice를 적용하고 싶다면 무엇을 쓰는가?
@annotation Pointcut을 사용한다.
named pointcut을 쓰는 이유는 무엇인가?
반복되는 Pointcut 표현식을 한곳에 이름으로 모아 재사용하고 변경 비용을 줄이기 위해서다.
Advice 타입 비교
3줄 요약
Advice는 Pointcut이 고른 메서드 실행 지점에서 실제로 동작하는 부가 기능 코드다.
@Before, @After, @AfterReturning, @AfterThrowing은 실행 시점이 정해진 Advice이고, @Around는 호출 여부, 인자, 반환값, 예외 흐름까지 가장 넓게 제어한다.
단순 기록과 검사는 좁은 Advice를 쓰고, 실행 시간 측정, 파라미터 교체, 반환값 대체, 예외 변환처럼 흐름 제어가 필요하면 @Around를 검토한다.
핵심 정리
@Before는 Target 메서드 실행 전에 동작한다. 사전 조건 검사, 입력 검증, 호출 로그처럼 원본 메서드 전에 확인할 일이 있을 때 적합하다.
@After는 성공과 실패 여부와 관계없이 Target 메서드 종료 후 실행된다. Java의 finally처럼 정리 작업이나 종료 로그에 어울린다.
@AfterReturning은 Target 메서드가 정상적으로 값을 반환한 뒤 실행된다. 반환 객체를 읽어 로그나 메트릭을 남기는 데 적합하다. 반환 객체 내부 상태를 바꾸는 코드는 호출자 입장에서 추적이 어려우므로 제한하고, 반환 객체 자체를 다른 객체로 교체하는 용도는 @Around가 맡는다.
@AfterThrowing은 Target 메서드에서 예외가 발생했을 때 실행된다. 예외 로그, 알림, 예외 분류처럼 실패 상황에 대한 공통 처리를 모으는 데 적합하다.
@Around는 Target 호출 전후를 모두 감싸며 ProceedingJoinPoint.proceed()를 호출할지, 언제 호출할지, 어떤 인자를 넘길지, 어떤 값을 반환할지 결정할 수 있다.
@Around는 강력하지만 흐름을 가장 많이 숨길 수 있다. 단순한 전처리나 후처리는 가능한 한 더 좁은 Advice를 선택하는 편이 읽기 쉽다.
헷갈리는 지점
@After와 @AfterReturning을 같은 후처리로 생각하기 쉽다. 둘 다 Target 실행 뒤에 동작하기 때문이다.
핵심은 @After는 성공과 실패 모두에서 실행되고, @AfterReturning은 정상 반환에서만 실행된다는 점이다.
정리 작업은 @After, 정상 결과 관찰은 @AfterReturning으로 나누어 생각한다.
@AfterReturning이 반환값을 자유롭게 바꿀 수 있다고 오해하기 쉽다. 반환값을 파라미터로 받을 수 있기 때문이다.
핵심은 반환 객체를 관찰하거나 참조 객체 내부를 변경할 수는 있지만 반환 객체 자체를 대체하지는 못한다는 점이다.
호출자에게 전달할 값을 바꾸려면 @Around에서 반환값을 직접 결정해야 한다.
@Around가 가장 강력하므로 항상 쓰면 된다고 생각하기 쉽다. proceed()로 모든 흐름을 제어할 수 있기 때문이다.
핵심은 강력한 도구일수록 호출 누락, 예외 처리, 반환값 처리 실수가 커진다는 점이다.
필요한 제어 범위가 좁다면 더 구체적인 Advice를 선택하는 편이 안전하다.
proceed()를 단순한 메서드 호출처럼 보기 쉽다. 코드상으로는 한 줄의 호출처럼 보이기 때문이다.
핵심은 proceed()가 Target 메서드 실행을 실제로 진행시키는 지점이라는 점이다.
proceed()를 호출하지 않으면 Target 메서드는 실행되지 않는다.
확인 질문
성공과 실패 여부와 관계없이 항상 실행해야 하는 정리 작업에는 어떤 Advice가 적합한가?
@After가 적합하다.
정상 반환 결과를 읽어 로그를 남기려면 어떤 Advice가 적합한가?
@AfterReturning이 적합하다.
Target 호출 여부, 인자, 반환값을 모두 제어해야 한다면 어떤 Advice가 필요한가?