AOP 관점지향프로그래밍
AOP
직역하면 관점지향 프로그래밍(Aspect-Oriented Programming) 인데, 한국말로 들으면 영 와닿지 않습니다.
좀 더 쉽게 풀어보면, 여러 곳에 흩어져 반복되는 공통 기능을 한 군데로 모아 재사용하게 해주는 것입니다.
로깅, 트랜잭션, 권한 체크, 실행 시간 측정처럼: 핵심 비즈니스 로직은 아니지만 여기저기 똑같이 끼어드는 코드가 있습니다. 이런 걸 횡단 관심사(cross-cutting concern) 라고 합니다.
사실 ‘횡단’이라는 번역은 개인적으로 좀 어색하게 느껴집니다.
직관적으로는 이렇게 이해하면 편합니다. 우리 코드가 기능별로 여러 층(주문, 결제, 회원…)으로 나뉘어 있다고 할 때, 로깅이나 트랜잭션 같은 기능은 특정 한 층에만 속하지 않고 모든 층을 가로질러 똑같이 필요합니다. 건물로 치면 층마다 따로 까는 게 아니라 전 층을 관통해 지나가는 전기·수도 배관 같은 거죠. 이렇게 여러 곳을 공통으로 가로지른다고 해서 ‘cross-cutting(가로지르는)‘이라고 부릅니다.
AOP는 바로 이 공통 기능을 본 코드 곳곳에 흩뿌리지 않고, 한 군데로 떼어내 따로 관리하게 해줍니다.
예를 들어 모든 서비스 메서드 앞뒤에 로그를 남기고 싶다면, 메서드마다 로그 코드를 일일이 넣는 대신 “서비스 메서드가 실행되기 전후에 로그를 찍어라”라는 규칙 하나만 AOP로 정의하면 됩니다.
이 글에서는 AOP를 구현하는 방법들만 이론적으로 짚어보겠습니다.
AOP를 구현하는 방법
-
컴파일 타임 위빙(weaving) 소스를 컴파일할 때 AspectJ가 AOP 코드를 끼워 넣은 것처럼 컴파일해줍니다. (전용 컴파일러가 필요합니다.)
-
로드 타임 위빙 (바이트코드 조작) 컴파일된
.class파일 자체에는 AOP 코드가 들어있지 않지만, 클래스 로더가 클래스를 읽어 들이는 시점에 메모리상에서 바이트코드를 조작해 AOP를 적용합니다. (이것 역시 AspectJ가 지원합니다.) -
런타임 프록시 스프링이 사용하는 핵심 방식입니다. 원래 객체를 감싼 프록시(proxy) 객체를 만들어 두고, 클라이언트는 평소처럼 호출하지만 실제로는 프록시가 공통 기능을 먼저(혹은 나중에) 실행한 뒤 원본 객체를 호출합니다.
프록시(Proxy): 같은 인터페이스를 구현해 원래 객체를 대신하는 대체 객체. 클라이언트 코드를 바꾸지 않으면서, 호출을 가로채 공통 기능을 끼워 넣을 수 있습니다. 스프링은 이 프록시 객체에 AOP를 적용합니다.
스프링 AOP를 활용하다 마주치는 문제들
이론은 위가 전부인데, 실제로 써보면 여기서부터 걸리는 게 나옵니다. 대표격 두 가지만 짚어보겠습니다.
① 같은 빈 안에서의 자기 호출은 프록시를 안 탄다. “분명 @Transactional을 붙였는데 트랜잭션이 안 걸려요” 하는 상황의 단골 원인입니다. 스프링 AOP는 외부에서 빈의 메서드를 호출할 때만 프록시가 가로채는데, 한 빈 안에서 this.method()로 자기 메서드를 부르면 프록시를 건너뛰고 원본에서 바로 실행됩니다. 그래서 그 메서드의 @Transactional이 무시됩니다. 해결은 메서드를 다른 빈으로 분리하는 식으로 합니다.
② final이면 프록시를 못 만든다. 스프링은 인터페이스가 있으면 JDK 동적 프록시를, 없으면 CGLIB으로 원본 클래스를 상속해 프록시를 만듭니다. 상속으로 만들다 보니 final 클래스나 메서드는 프록시 대상이 안 됩니다.
참고
- https://jojoldu.tistory.com/71
- “백기선”님의 스프링 프레임워크 AOP 강좌
댓글
GitHub(giscus) 댓글은 설정 완료 후 활성화됩니다.