
관점 지향 프로그래밍 (Aspect Oriented Programming, AOP)이란 쉽게 말하면 비즈니스 로직 (핵심관심) 과 공통으로 필요한 기능 (예를 들면 로깅, 트랜잭션, 보안 등의 횡단관심)을 분리해서 관리하는 방법이다.
기존에는 비즈니스 로직 코드 안에 이런 공통 기능들이 산재하게 되어있어서 코드가 복잡해지고, 변경할 때 여러 군데를 수정해야 하는 불편함이 있었다. 하지만 AOP를 사용하면 이런 공통 관심사 (= 횡단관심)를 별도의 모듈 (Aspect)로 빼내서, 핵심 로직은 깔끔하게 유지하면서도 필요한 곳에 해당 모듈을 자동으로 적용할 수 있다.
스프링 AOP는 주로 프록시 패턴을 이용해서 동작한다. 즉, 실제 객체 대신 프록시 객체를 사용해서 메소드 실행 전후에 원하는 부가 기능 (Advice)를 삽입하는 방식이다.
자바 진영에서 사용되는 AOP 도구 중 대표적인 것으로 AspectJ, JBossAOP, SpringAOP가 존재한다.
build.gradle파일에서 AOP 관련 의존성을 추가하고 싶다면 다음을 추가해주면 된다.
✍️ 작성
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
}
AOP 관련 용어로는 다음과 같은 용어들이 있다. 잘 숙지해두자.
| 개념 | 설명 | 예시 |
|---|---|---|
| Joinpoint | 애플리케이션을 실행할 때 특정 작업이 실행되는 시점을 의미. AOP가 끼어들 수 있는 모든 가능한 실행 지점임. | 메소드 실행 시점, 예외 발생 시점 등 |
| Advice | Joinpoint에 실행되어야 하는 코드로 AOP로 구현하고 싶은 기능을 의미. 로깅, 보안 체크, 트랜잭션 관리 등이 해당. | 메소드 실행 전후에 로그를 찍는 코드 |
| Target | AOP가 적용될 실제 객체나 메소드. 실질적인 비지니스 로직을 구현하고 있는 코드를 의미함. | 서비스 클래스 내 비즈니스 로직을 수행하는 메소드 |
| Pointcut | 어떤 Joinpoint에 Advice를 적용할지 결정하는 조건 (필터)임. | 특정 패키지의 모든 메소드 실행 전 또는 특정 메소 드 이름 패턴에 해당하는 메소드에만 적용 |
| Aspect | 하나 이상의 Advice와 Pointcut을 묶은 AOP 모듈 즉 "어느 시점에, 어떤 기능을 실행해라"라고 정의해놓은 모듈 특정 기능 (예: 로깅, 보안)을 모듈화해서 관리함. | "로그 관리" aspect는 여러 메소드에 로깅 기능 을 적용하는 Advice와 그 적용 조건을 포함 |
| Weaving | Aspect의 Advice를 Target 객체에 결합하는 과정. 애플리케이션 실행 시 자동으로 수행됨. Weaving 하는 작업을 도와주는 것이 AOP 툴의 역할. | 스프링이 실행 시 Target 객체의 특정 Joinpoint에 로깅 Advice를 삽입하여 하나의 객체로 만드는 과정 |
다음으로 스프링 프레임워크에서 지원하는 어드바이스 (Advice) 유형이다. 이것도 잘 숙지해두자.
| 유형 | 설명 |
|---|---|
| Before | 조인 포인트 전에 실행. 예외가 발생하는 경우만 제외하고 항상 실행. |
| After Returning | 조인 포인트가 정상적으로 종료한 후에 실행. 예외가 발생하면 실행되지 않는다. |
| After Throwing | 조인 포인트에서 예외가 발생했을 때 실행. 예외가 발생하지 않고 정상적으로 종료하면 실행되지 않음. |
| After | 조인 포인트에서 처리가 완료된 후 실행. 예외 발생이나 정상 종료 여부와 상관없이 항상 실행. |
| Around | 조인 포인트 전후에 실행. |
스프링은 Aspect의 적용 대상 (target)이 되는 객체에 대한 프록시(Proxy)를 만들어 제공한다. 여기서 Proxy란 뭘까?
프록시 (Proxy)는 말 그대로 대리인 객체를 의미한다. 즉, 클라이언트 (코드를 사용하는 쪽)는 실제 객체 (Target)가 아니라, 그 대리인인 프록시를 호출한다. 프록시는 호출된 작업을 대신 실행하면서 추가 기능 (예: 로깅, 보안 체크, 트랜잭션 관리 등)을 수행할 수 있다.
스프링에서는 AOP를 적용할 때 실제 대상 객체 (Target)를 직접 호출하는 대신, 그 대상 객체를 감싸는 프록시 객체를 만든다. 이 프록시가 호출되면 먼저 공통 기능 (Advice)을 실행하고, 그 후에 실제 대상 객체의 메서드를 호출하거나, 혹은 메서드 호출 후에 공통 기능을 추가로 실행한다.
즉, 프록시를 통해서 대상 객체에 접근함으로써, AOP에서 정의한 부가적인 로직 (예: 로깅, 보안, 트랜잭션 등)을 호출 시점에 끼워넣을 수 있게 된다.
예를 들면 다음과 같다.
대상 객체 (Target)
실제 비즈니스 로직을 수행하는 객체이다. (예: MyService)
프록시 객체 (Proxy)
프록시 객체가 MyService를 감싸고 있어서, 우리가 실제로 MyService를 호출하면 실제로는 Proxy가 호출되고, Proxy는 그 전에 필요한 AOP Advice를 실행한 뒤에 MyService의 메서드를 호출한다. (경우에 따라 반대의 경우도 있을 수 있다.)
해당 예시에서 알 수 있듯 이렇게 프록시를 사용하면, 대상 객체의 코드를 변경하지 않고도 공통 관심사 (Aspect)를 쉽게 추가할 수 있다.
포인트컷 표현식이란 AOP에서 "어떤 메소드 (혹은 joinpoint)에 advice를 적용할지" 결정하는 조건을 기술하는 식이다.
해당 표현식의 구문은 다음과 같다. 여기서 execution은 가장 많이 사용되는 포인트컷 지시자로 메서드 실행을 기준으로 매칭하며 메서드 실행 시 조인 포인트를 찾는데 사용된다. 여기서는 execution을 중심으로 알아보자.
✍️ 작성
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-
pattern(param-pattern) throws-pattern?)
각각의 요소에 대해 순서대로 살펴보자.
| 패턴 요소 | 설명 | 예시 |
|---|---|---|
| modifiers-pattern | 메소드의 접근제한자 (public, private 등)을 지정. 보통 생략하거나 *로 모든 제한자를 의미. | public, * |
| ret-type-pattern | 메소드의 반환 타입을 지정. | * (모든 반환 타입) |
| declaring-type-pattern | 메소드가 속한 클래스 또는 인터페이스를 지정. 지정한 패키지나 클래스에 해당하는 메소드만 매칭. | com.example.demo.service.* |
| name-pattern | 메소드 이름을 지정. | * (모든 메소드 이름) |
| param-pattern | 메소드의 파라미터를 지정. ( .. )는 파라미터의 개수와 타입에 관계없이 모두 매칭됨. | ( .. ) |
| throws-pattern | 메소드가 던질 수 있는 예외를 지정. 보통 생략하거나 *로 모든 예외를 의미. | 생략하거나 * |
위 표를 바탕으로 다음의 포인트컷 표현식을 해석해보자
- execution( set(..))
-> 어떤 클래스에 있든 이름이 "set"으로 시작하는 모든 메소드를 대상으로 AOP를 적용- execution( com.example.demo.service..*(..))
-> com.example.demo.service 패키지 및 그 하위 패키지 내의 모든 클래스의 모든 메소드를 대상으로 AOP를 적용
자바 기반 설정 방식에서의 어드바이스 정의는 어노테이션을 활용해서 이루어진다.
관련 어노테이션을 정리하면 다음과 같다.
스프링 빈 (객체)에 @Aspect를 명시하면 해당 빈이 Aspect로 작동한다.
클래스 레벨에 @Order를 명시하여 @Aspect 빈 간의 작동 순서를 정할 수 있다. int 타입의 정수로 순서를 정할 수 있는데 값이 낮을수록 우선순위가 높다. 기본값은 가장 낮은 우선순위를 가지는 Ordered.LOWEST_PRECEDENCE이다.
@Aspect가 명시된 빈에는 어드바이스 (Advice)라 불리는 메소드를 작성할 수 있다. 대상 스프링 빈의 메소드의 호출에 끼어드는 시점과 방법에 따라 @Before, @After, @AfterReturning, @AfterThrowing, @Around 등을 명시할수 있다.
대상 객체의 메서드 호출 전에 공통 기능을 실행한다.
지정한 Pointcut에 해당하는 메서드 실행 후 결과와 상관없이 무조건 Advice를 실행하도록 하는 어노테이션이다. 해당 어드바이스를 명시하면 대상 메서드의 실행 후에 끼어 들어 원하는 작업을 할 수 있다. 역시 끼어들기만 할 뿐 대상 메서드의 제어나 가공은 불가능하다.
@Aspect 클래스의 메소드 레벨에 @AfterReturning을 명시하면 해당 메서드의 실행이 종료되어 값을 리턴할 때 끼어 들 수 있다. 리턴 값을 확인할 수 있을 뿐 대상 메소드의 제어나 가공은 불가능하다.
@Around 어드바이스는 앞 서 설명한 어드바이스의 기능을 모두 포괄하는 종합선물세트와도 같다. 대상 메소드를 감싸는 느낌으로 실행 전후 시점에 원하는 작업을 할 수 있다. 대상 메소드의 실행 제어 및 리턴 값 가공도 가능하다.
지금까지 많은 내용을 공부해왔으니 마지막으로 관련 예제를 살펴보자. 이 예제를 통해서 AOP가 실제로 어떻게 쓰이는지 쉽게 이해할 수 있을 것이다.
✍️ 작성
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class MyService {
public String doSomething(String input) {
System.out.println("실제 비즈니스 로직 실행: " + input);
return "결과: " + input;
}
}
MyService 클래스는 비즈니스 로직을 담당하는 클래스로 Target에 해당한다.
doSomething 메소드가 호출되면 실제 로직을 수행하고 결과를 반환한다.
✍️ 작성
package com.example.demo.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
// @Before: MyService의 모든 메서드 실행 전에 호출
@Before("execution(* com.example.demo.service.MyService.*(..))")
public void beforeAdvice() {
System.out.println("[Before] 메서드 실행 전에 호출");
}
// @After: MyService의 모든 메서드 실행 후에 호출 (정상/예외 관계없이 항상 실행)
@After("execution(* com.example.demo.service.MyService.*(..))")
public void afterAdvice() {
System.out.println("[After] 메서드 실행 후에 호출");
}
// @AfterReturning: doSomething 메서드가 정상적으로 실행된 후 반환값을
// 어드바이스 메서드의 파라미터 "result"가 받는다는 의미
@AfterReturning(pointcut="execution(* com.example.demo.service.MyService.doSomething(..))",
returning="result")
public void afterReturningAdvice(Object result) {
System.out.println("[AfterReturning] 메서드 정상 실행 후 결과: " + result);
}
// @Around: doSomething 메서드 실행 전후를 모두 제어할 수 있음
@Around("execution(* com.example.demo.service.MyService.doSomething(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("[Around] 메서드 실행 전");
// 실제 메서드 실행 (target 메서드 호출)
Object result = joinPoint.proceed();
System.out.println("[Around] 메서드 실행 후");
return result;
}
}
해당 클래스는 @Aspect 클래스로 각 메서드의 기능은 각 메서드별 주석에 표기하였다.
여기서 마지막 @Around 메서드만 짚고 넘어가자. 해당 메서드를 이해할려면 먼저 JoinPoint 인터페이스에 대해 알아야 한다.
JoinPoint란 AOP에서 "지금 이 코드가 실행되는 지점"에 대한 정보를 담은 객체이다.
쉽게 말하면, AOP 어드바이스 (Advice)가 실행될 때, 어떤 메서드가 호출되고 있는지, 그 메서드의 이름이나 파라미터, 대상 객체 등 다양한 정보를 담고 있어서, 우리가 로깅이나 디버깅, 조건문 등에서 활용할 수 있게 해준다.
이러한 내용을 바탕으로 해당 메서드의 매개변수 타입인 ProceedingJoinPoint를 알아보면 해당 타입은 @Around 어드바이스 메서드에서 매개변수로 사용되는데, 해당 클래스는 일반 JoinPoint의 확장판으로, 현재 조인 포인트 (메서드 실행 지점)에 대해 제어할 수 있는 메서드 (proceed())를 제공한다.
매개변수 joinPoint의 인자는 스프링 AOP 프레임워크가 어드바이스 메서드를 호출할 때 자동으로 주입된다. 즉, AOP가 메서드 실행 시점의 정보를 캡슐화한 객체를 생성해서, @Around 같은 어드바이스 메서드의 인자로 전달하는 것이다.
그 후 받아온 인자를 이용해 proceed()를 호출해서 실제 대상 메서드를 실행시키고, 그 결과를 받아오거나, 중간에 로직을 끼워넣어서 메서드 실행 흐름을 조절할 수 있게 해준다.
우리 예제를 통해 살펴보면 메서드 aroundAdvice에서 joinPoint.proceed()를 호출하면 실제 대상 메서드(doSomething())가 실행된다. 즉, aroundAdvice 안에서 메서드 실행 전후에 원하는 코드를 실행할 수 있도록, 실제 메서드 호출을 제어하는 역할을 해주는 것이다.
해당 애플리케이션을 실행하면, MyService의 doSomething() 메서드를 호출할 때 AOP가 적용돼서 다음과 같이 출력된다.
🖥️ 결과
[Around] 메서드 실행 전
[Before] 메서드 실행 전에 호출
실제 비즈니스 로직 실행: 입력값
[Around] 메서드 실행 후
[AfterReturning] 메서드 정상 실행 후 결과: 결과: 입력값
[After] 메서드 실행 후에 호출
@Around 덕분에 메서드 호출 전후로 추가 로직을 실행하고, @Before, @After, @AfterReturning이 각각 해당 시점에 맞춰 호출되면서 로그를 출력하는 걸 확인할 수 있다.
멋쟁이사자처럼 강의자료