객체 지향 프로그래밍 패러다임을 보완하는 기술로 메소드나 객체의 기능을 핵심 관심사(Core Concern)와 공통 관심사(Cross-cutting Concern)로 나누어 프로그래밍하는 것을 말한다. 핵심 관심사는 각 객체가 가져야 할 본래의 기능이며, 공통 관심사는 여러 객체에서 공통적으로 사용되는 코드를 말한다.
여러 개의 클래스에서 반복해서 사용하는 코드가 있다면 해당 코드를 모듈화하여 공통 관심사로 분리한다. 이렇게 분리한 공통 관심사를 Aspect로 정의하고 Aspect를 적용할 메소드나 클래스에 Advice를 적용하여 공통 관심사와 핵심 관심사를 분리할 수 있다. 이렇게 AOP에서는 공통 관심사를 별도의 모듈로 분리하여 관리하며, 이를 통해 코드의 재사용성과 유지 보수성을 높일 수 있다.
Spring AOP는 스프링 프레임워크에서 제공하는 기능 중 하나로 관점 지향 프로그래밍을 지원하는 기술이다. Spring AOP는 로깅, 보안, 트랜잭션 관리 등과 같은 공통적인 관심사를 모듈화 하여 코드 중복을 줄이고 유지 보수성을 향상하는데 도움을 준다.
@Transactional: 스프링의 @Transactional 은 내부적으로 AOP 활용한다. 메서드 실행 전에 트랜잭션 시작하고, 종료시 커밋, 예외 발생 시 롤백하는 AOP로 구현되어있다. 스프링 AOP는 프록시 기반으로 동작한다. 예를 들어, UserService라는 클래스가 있다면, 스프링은 실제 UserService를 직접 사용하는 게 아니라 UserService를 감싼 프록시를 만들어서 사용한다. 클라이언트가 메서드를 호출하면 프록시가 먼저 받아서 AOP 로직(로깅, 트랜잭션 등)을 실행한 후, 실제 메서드를 호출하는 것이다.
스프링에서 프록시는 주로 JDK 다이나믹 프록시와 CGLIB 두 가지 방식으로 구현되는데, JDK 동적 프록시와 CGLIB이다.
public interface UserService {
void saveUser(User user);
}
public class UserServiceImpl {
void saveUser(User user){...}
}
// 스프링이 자동으로 만드는 프록시 (실제는 더 복잡)
public class UserServiceProxy extends UserService {
private UserService realUserService; // 실제 객체
private LoggingAspect loggingAspect; // Aspect
@Override
public void saveUser(User user) {
// 1. Aspect 실행
loggingAspect.before();
// 2. 실제 메서드 실행
realUserService.saveUser(user);
// 3. Aspect 실행
loggingAspect.after();
}
}
spring.aop.proxy-target-class=true)이 CGLIB를 사용하도록 되어있다.개발자가 인터페이스를 만들지 않은 경우에도 AOP가 정상 작동해야 하며, 구체 클래스 타입으로 빈을 주입받는 상황에서도 안정적으로 동작하기 때문이다.
@Service
public class MyService {
@Transactional
public void methodA() {
methodB(); // 내부 호출
}
@Transactional
public void methodB() {
System.out.println("Method B");
}
}
위 예시 코드를 보면 methodA()에서는 methodB()를 호출하고 있다. 그러나 methodB()에 적용된 트랜잭션은 동작하지 않는다. 내부 호출은 프록시 객체를 거치지 않기 때문에 AOP가 적용되지 않는다. 이 문제를 해결하려면, 내부 호출 대신 프록시 객체를 통해 호출해야 한다. 또는 설계를 변경하여 내부 호출을 피해야 한다.
AOP의 목표는 원본 코드를 건드리지 않고 부가 기능을 추가하는 것이다. 그러기 위해서는 클라이언트와 원본 객체 사이에서 가로채어 동작을 수행해야한다. 이때 프록시가 중간 다리 역할을 하여 메서드 호출을 가로채고 원본 메서드를 호출하는 것이다.
AOP 프레임워크 중 하나인 AspectJ에서는 프록시 방식이 아닌 다른 방식을 사용한다.
하지만 위 방식은 JVM 옵션을 추가해야하거나 AspectJ 컴파일러가 별도로 필요하다. AOP를 실행하기 위한 러닝커브가 존재한다.
스프링에서는 복잡한 것을 단순하게 제공하는 것이 목적이기 때문에 제한적이기만 단순하고 많은 설정없이 바로 사용가능한 프록시 패턴을 선택한 것이다.
상황에 따라 스프링에서도 스프링 AOP대신 AspectJ를 사용할 수도 있다. 그럼 프록시 패턴이 아닌 위의 방식처럼 스프링 컨테이너 밖에서 위빙이 일어난다.