AOP 기본 개념

3줄 요약

  • AOP는 핵심 비즈니스 로직과 여러 곳에 반복되는 부가 기능을 분리하는 설계 방식이다.
  • 로깅, 성능 측정, 보안, 트랜잭션, 예외 처리처럼 여러 계층과 메서드에 걸쳐 나타나는 기능을 횡단 관심사라고 부른다.
  • 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에서 공유될 때 가독성과 변경 비용을 줄인다.

헷갈리는 지점

  • executionwithin을 같은 기준으로 이해하기 쉽다. 둘 다 패키지와 클래스명을 표현할 수 있기 때문이다.
    • 핵심은 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가 필요한가?
    • @Around가 필요하다.