Spring AOP

이주오·2022년 1월 27일
0

spring

목록 보기
2/4

AOP(Aspect Oriented Programming)

AOP는 관점 지향 프로그래밍. Spring의 핵심 개념중 하나인 DI가 애플리케이션 모듈들 간의 결합도를 낮춰준다면, AOP는 애플리케이션 전체에 걸쳐 사용되는 기능을 재사용하도록 지원하는 것

쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다.

예로들어 핵심적인 관점은 결국 우리가 적용하고자 하는 핵심 비즈니스 로직이 된다. 또한 부가적인 관점은 핵심 로직을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.

AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어서 모듈화하겠다는 의미다. 이때, 소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는 데 이것을 흩어진 관심사 (Crosscutting Concerns)라 부른다.

위와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지다.


기존 핵심 비즈니스 로직

@Service
@RequiredArgsConstructor
public class UserService {

    private final PlatformTransactionManager transactionManager;

    public void someSevice() {
			TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
		// 부가 기능 - 로깅, 보안 등등
		// 핵심 기능
		someServiceMethod();
		// 부가 기능
		transactionManager.commit(transaction);
        } catch (RuntimeException runtimeException) {
            transactionManager.rollback(transaction);
            throw runtimeException;
        }
    }
}

서비스 로직의 원자성 보장을 위해 내부적으로 트랜잭션을 적용한 코드입니다. 문제는 UserService의 클래스에는 someServiceMethod() 핵심 비즈니스 로직 이외에도 트랜잭션 경계 설정이라는 부가 기능 관심사들이 존재한다.

현재 예제 코드는 부가 기능 관심사가 트랜잭션 하나 뿐이지만, 또 다른 부가 기능의 관심사가 추가된다면 부가 기능이 필요한 메서드마다 비슷한 코드를 중복해서 작성해야 한다.

가장 큰 문제는 UserService와 비슷하게 수행해야 하는 클래스가 100개가 더 있을 수 있기 때문에 필요한 클래스 마다 UserService와 같이 중복되는 코드를 반복해서 작성해야 함을 의미한다.

만약 코드를 변경한다면 클래스를 변경하는 이유는 비즈니스 로직의 변경 및 부가 기능 코드 또한 변경해야 하기 때문에 서비스 클래스의 응집도가 떨어지고 가독성 또한 나빠지며, 변경할 부분이 명확하게 드러나지 않게 되는등 유지보수 측면에서 아쉬운 점이 많아진다.


Proxy를 사용하여 개선

프록시 객체에 트랜잭션 등 부가 기능 관련 로직을 위치시키고, 클라이언트 요청이 발생하면 실제 타깃 객체는 프록시로부터 요청을 위임받아 핵심 비즈니스 로직을 실행합니다. 이를 데코레이터 패턴이라고 한다.

public interface UserService {

    void someSevice();
}
@Service
@Primary
@RequiredArgsConstructor
public class SimpleUserService implements UserService {

    @Override
    public void someSevice() {
				...
    }
}
@Service
@RequiredArgsConstructor
public class UserServiceProxy implements UserService {

    private final UserService target;
    private final PlatformTransactionManager transactionManager;

    @Override
    public void someSevice() {
			TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
		// 부가 기능 - 로깅, 보안 등등
		// 핵심 기능
		someServiceMethod();
		// 부가 기능
		transactionManager.commit(transaction);
        } catch (RuntimeException runtimeException) {
            transactionManager.rollback(transaction);
            throw runtimeException;
        }
    }
}

프록시 객체를 이용하여 핵심 메소드가 호출되기 이전에 부가 기능을 적용하였다.


기존 프록시의 문제점

  • 인터페이스의 모든 메소드를 구현해 위임하도록 코드를 만들어야 한다.
  • 부가기능인 기능이 모든 메소드에 중복돼서 나타난다.

프록시 객체를 이용하여 핵심 비즈니스 로직과 부가 기능 관심사를 분리할 수 있었지만 여전히 한계가 존재한다.. 100개의 클래스가 이와 비슷한 기능을 요구한다면, 100개의 프록시 클래스를 생성하고 인터페이스 메서드를 일일이 구현해야 합니다.

다행히 이러한 별도의 프록시를 번거롭게 생성하는 작업을 생략하는 방법이 존재합니다. Java의 Reflection API를 이용하거나, Spring의 ProxyFactoryBean 등을 사용하는 방법이 존재한다.

  • 더 자세한 설명은 토비의 스프링 or 해당 글 참고!!

Spring AOP

Spring AOP는 Proxy를 기반으로 한 Runtime Weaving 방식이다

  • 프록시 패턴 기반의 AOP 구현체
  • 스프링 빈에만 AOP를 적용 가능
  • 메소드 조인포인트만 제공

스프링이 사용하는 다이나믹 프록시에는 2가지 방법이 있다.

  • JDK Dynamic Proxy
  • CGLib Proxy

스프링에서는 기본적으로 jdk dynamic proxy, 스프링 부트에서는 CGLib Proxy 방식으로 AOP를 사용한다.

JDK Dynamin Proxy, CGLib Proxy의 차이를 알아보자.


JDK Dynamic Proxy

JDK Dynamic Proxy는 Proxy Factory에 의해 런타임시 동적으로 만들어지는 오브젝트이다. JDK Dynamic Proxy는 반드시 인터페이스가 정의되어있고, 인터페이스에 대한 명세를 기준으로 Proxy를 생성한다. 즉, 인터페이스 선언에 대한 강제성이 있다는 단점이 있다.

내부적으로 JDK Dynamic Proxy에서는 InvationHandler라는 인터페이스를 구현해 만들어지는데, invoke 함수를 오버라이딩하여 Proxy의 위임 기능을 수행한다. 이 과정에서 객체에 대한 Reflection 기능을 사용해 구현하기 때문에 퍼포먼스 하락의 원인이 되기도 한다.

  • 이 방식이 Spring AOP의 근간이 되는 방식이다.
  • 인터페이스를 기준으로 Proxy 객체를 생성해준다.
  • 인터페이스가 반드시 필요하다.
  • 리플렉션을 사용하기때문에 성능적으로 좋지 못 하다.

CGLib Proxy

CGLIB Proxy는 순수 Java JDK 라이브러리를 이용하는 것이 아닌 CGLIB라는 외부 라이브러리를 추가해야만 사용할 수 있다. CGLIB의 Enhancer 클래스를 바탕으로 Proxy를 생성하며, 인터페이스가 없어도 Proxy를 생성할 수 있다. CGBLIB Proxy는 타겟 클래스를 상속받아 생성하기 때문에 Proxy를 생성하기 위해 인터페이스를 만들어야하는 수고를 덜 수 있다.

하지만, 상속을 이용하므로 final이나 private와 같이 상속에 대해 오버라이딩을 지원하지 않는 경우에는 Aspect를 적용할 수 없다는 단점이 있다.

CGLIB Proxy는 바이트 코드를 조작해서 프록시 객체를 생성하므로 JDK Dynamic Proxy보다 퍼포먼스가 빠른 장점이 있다.

  • 상속을 이용하기 때문에 클래스나 메소드에 final이 있으면 안된다.
  • 스프링 부트에서 AOP 사용을 위해 채택했다.
  • CGLIB은 고성능의 코드 생성 라이브러리로 인터페이스를 필요로 하는 JDK Dynamic Proxy 대신 사용될 수 있다. 바이트코드를 조작하는 프레임워크인 ASM을 사용하여 리플렉션보다 빠르다.

스프링 부트에서는 왜 cglib?

  • 스프링에선 CGLib은 3가지 한계가 존재했다.
    • 해당 라이브러리를 추가해야 한다.
    • CGLib을 구현하기 위해 반드시 파라미터가 없는 defalut 생성자가 필요하다.
    • 생성된 Proxy의 메소드를 호출하게 되면 타깃의 생성자가 2번 호출된다.
  • 하지만 문제되는 부분들을 개선하여 안정화 시켰다.
  • 스프링 부트에서는 AOP를 사용할 때 인터페이스로 선언되어 있어도 CGLib 방식으로 프록시 객체를 생성한다.

참고 출처 : https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html


AOP 주요 개념

  • Target : Aspect를 적용하는 곳 (클래스, 메서드 .. )
  • Aspect : 위에서 설명한 흩어진 관심사를 모듈화 한 것. 주로 부가기능을 모듈화함.
  • Advice : 실질적으로 어떤 일을 해야할 지에 대한 것, 실질적인 부가기능을 담은 구현체
  • JointPoint : Advice가 적용될 위치, 끼어들 수 있는 지점. 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용가능
  • PointCut : JointPoint의 상세한 스펙을 정의한 것. 'A란 메서드의 진입 시점에 호출할 것'과 같이 더욱 구체적으로 Advice가 실행될 지점을 정할 수 있음

참고 출처

profile
동료들이 같이 일하고 싶어하는 백엔드 개발자가 되고자 합니다!

0개의 댓글