Computer Science

Aspect-oriented programming(AOP)

Kim Jinung 2023. 6. 5. 17:55

AOP(Aspect-oriented programming)

직역하면 관점 지향 프로그래밍이다. 객체 지향은 객체에게 책임과 역할을 부여하고 객체 간의 협력을 통해 시스템을 구성하는 방법이다. 그렇다면 관점 지향의 관점은 무엇을 의미하는가.

 

관점 지향 프로그래밍은 Cross-cutting concern을 분리해서 모듈성을 증가시키는 패러다임이다. 여기서 Cross-cutting concern은 직역하면 횡단 관심사인데, 각 컴포넌트의 공통 관심사 라는 표현이 의도에 더 적합한 것 같다.

 

그렇다면 컴포넌트의 공통 관심사란 무엇인가.

https://www.codejava.net/frameworks/spring/understanding-spring-aop

프레젠테이션 레이어, 비지니스 레이어, 데이터 액세스 레이어는 각각 목적에 따른 핵심 기능이 존재한다. 그리고 트랜잭션, 보안, 로깅과 같은 부수적인 기능을 필요로 한다. 여기서 핵심 기능은 각 컴포넌트마다 다르다. 하지만 부가 기능은 위 세 컴포넌트에서 모두 공통적으로 필요로하는 기능이다. 이것이 컴포넌트들의 공통 관심사(Cross-cutting concerns)다.


Cross-cutting conerns in OOP

중복되는 공통 관심사를 컴포넌트로 분리하기 위해서 OOP 패러다임에서는 유틸리티 클래스를 정의하고, 각 컴포넌트에서 해당 객체를 생성해서 사용하는 방법을 선택할 수 있다. 하지만 이를 사용하기 위해서는 각 컴포넌트마다 유틸리티 클래스의 인스턴스를 생성해야 한다. 중복을 제거하려고 했지만 완벽한 중복 제거가 불가능하다. 또한 변경이 필요할 때 유틸리티 클래스가 이를 사용하는 컴포넌트에 역으로 의존 관계가 생겨서 변경에 유연하지 못하다. 유틸리티 클래스의 변경이 필요할 때, 이를 물고있는 컴포넌트를 모두 확인해야한다. 즉 주도권이 유틸리티 클래스에 없다.


How to implement AOP

AOP를 구현하는 방법에는 크게 3가지 방법이 존재한다.

 

  1. 컴파일 타임 위빙
  2. 클래스 로딩 타임 위빙
  3. 런타임 위빙(프록시)

컴파일 타임 위빙은 컴파일 시점에 코드를 덧붙여주는 방법이고, 클래스 로딩 위빙도 마찬가지로 클래스 파일 로딩 타임에 코드를 덧붙여준다. 주류 방법은 프록시를 활용하는 런타임 위빙이다. (구체적인 이유는 여기서 중요하지 않으므로 생략한다. 런타임 위빙의 경우 다형성을 이용해서 유연하게 프록시 객체를 생성할 수 있다 정도만 기억해둔다.)

Runtime weaving

Java Spring framework를 기준으로 런타임 시점에 엔트리 포인트로 진입 후 프로그램이 실행되면 스프링 프레임워크는 스프링 컨테이너에 의존성 객체인 Bean을 주입한다. 이때 AOP를 적용한 객체는 원본 객체를 그대로 주입하는 것이 아니라 Bean post processor를 통해 프록시 객체를 생성하고 프록시 객체를 스프링 컨테이너 주입한다. 해당 프록시 객체에는 부가 기능과 관련된 메서드가 자리잡고, 타겟 객체를 참조해서 원본 클래스의 메서드에 접근한다. 

 

No-proxy

https://spring.io/blog/2012/05/23/transactions-caching-and-aop-understanding-proxy-usage-in-spring

프록시를 사용하지 않는 예시다. AccountService 인터페이스에 대한 의존성 주입에 AccountServiceImpl 클래스를 사용하고 있다.

 

Proxy pattern

https://spring.io/blog/2012/05/23/transactions-caching-and-aop-understanding-proxy-usage-in-spring

AcoountServiceImpl 클래스에 AOP로 작성한 트랜잭션 애노테이션을 추가한 케이스다. 런타임 시점에 스프링 컨테이너는 AcoountServiceImpl 클래스를 가지고 Proxy 객체를 생성해서 스프링 컨테이너에 주입한다. (프록시 객체 생성에서는 CGLIB을 사용한다.) 

 

(스프링 프레임워크는 애노테이션 기반으로 AOP를 적용할 수 있는 기능을 제공한다. 그 대표적인 예시가 @Transactional 애노테이션이다. AccountServiceImpl 클래스는 Proxy 객체로 한 번 래핑되어서 프록시 객체가 스프링 컨테이너에 주입된다.)

 

Scenario

컨트롤러에서 AccountService의 메서드를 요청하면,

 

  1. 프록시 객체는 Pre-processing을 수행한다. 해당 시나리오에서는 트랜잭션 시작이다.
  2. AcoountServiceImpl 객체(타겟)을 참조해서 비지니스 로직을 수행한다. 
  3. 프록시 객체는 Post-processing을 수행한다. 해당 시나리오에서는 트랜잭션 커밋 or 롤백이다.

위 과정에서 2번은 1, 3번과 독립적이다. 그러므로 다른 서비스 레이어 구현체로 바꿔치기 해도 전혀 문제가 되지 않는다. 즉 프록시 패턴을 사용해서 부가 기능과 핵심 기능을 깔끔하게 분리할 수 있다. 위 이미지가 프록시 패턴을 사용해서 구현한 AOP예시다. 

 

Limitations

  • 메서드 호출 시점에서만 적용 가능하다. (위 시나리오에서 pre, post processing이 동작하는 조건이 원본 객체의 메서드 호출이다.)
  • 런타임 시점에 프록시 객체를 동적으로 생성해주어야 하므로 런타임 오버헤드가 발생하게 된다. 

그러나 위 단점과 한계에도 불구하고 공통 관심사를 핵심 비지니스 로직과 분리할 수 있다는 점이 더 매력적이다.


Appendix

Proxy chain

그렇다면 애노테이션 기반의 AOP를 여러 개 적용하면 어떻게 될까?

가령 3개의 애노테이션 AOP를 적용한다고 가정하면 프록시 객체를 다시 프록시 객체로 래핑하고 다시 프록시 객체를 래핑한 프록시 객체를 프록시 객체로 래핑한다. 4단계 뎁스가 생긴다. Proxy(Proxy(Proxy(Origin)))