관점 지향 프로그래밍(AOP)
반복되는 부가 기능(로깅, 트랜잭션, 권한 등)을 핵심 로직과 분리하는 프로그래밍 패러다임
관점 지향 프로그래밍(AOP)
서비스를 만들다 보면 곳곳에서 반복되는 작업이 있다. 로그인 확인, 로그 남기기, 트랜잭션 처리, 캐시 갱신, 권한 체크, 성능 측정 같은 일들이 대표적이다. 이런 기능들은 사용자 요청을 처리하는 거의 모든 모듈을 관통한다. 흔히 이런 기능을 횡단 관심사(Cross-cutting Concerns) 라고 부른다.
문제는 이 코드들을 각 함수나 클래스에 매번 직접 넣는 순간부터 시작된다. 비즈니스 로직과 상관없는 코드가 로직 사이사이에 끼어들고, 유지보수가 악몽이 된다. 정책이 바뀔 때마다 모든 파일을 찾아가며 일일이 수정해야 하고, 중복이 늘어나면 버그의 확률도 기하급수적으로 높아진다.
관점 지향 프로그래밍(Aspect-Oriented Programming, AOP)은 이런 고질적인 문제를 풀기 위한 패러다임이다. 아이디어는 단순하다. 공통된 부가 기능을 한 군데 모으고, 실제 코드 앞·뒤 혹은 둘러싼 영역에 자동으로 끼워 넣는다. 비즈니스 로직은 핵심에만 집중하고, 나머지는 알아서 삽입·관리되도록 만드는 것이다.
OOP로는 해결이 어려운 문제
객체 지향 프로그래밍(OOP)은 책임을 나누고 캡슐화해 중복을 줄인다. 하지만 OOP만으로는 해결이 어려운 지점이 있다. 예를 들어 서비스 전체에 “요청이 들어오면 반드시 로그를 남긴다”는 정책을 세운다고 하자. OOP에서는 Logger 클래스를 만들고 각 모듈이 이를 호출하도록 설계할 수 있다. 하지만 결국 모든 메서드가 “로그 찍기 → 원래 로직” 순서로 코드를 써야 한다.
정책이 바뀌면? 예를 들어 로그 포맷을 JSON으로 바꿔야 한다면? 프로젝트 전역을 검색해 수십, 수백 개 파일을 수정해야 한다. 이런 수평적 관심사는 객체의 세로 구조(상속·조합)로는 깔끔히 떨어지지 않는다. AOP는 이 지점을 정면으로 겨냥한다. “가로로 퍼져 있는 관심사”를 세로 계층과 분리해 코드의 균형을 잡는다.
AOP의 핵심 개념
AOP를 이해하려면 다섯 가지 용어를 알아야 한다.
- Aspect: 공통 기능을 묶어둔 모듈. “모든 API 요청 시 로그 남기기 + 권한 체크” 같은 정책을 한 덩어리로 정의한다.
- Join Point: 끼어들 수 있는 지점. 메서드 실행 전·후, 예외가 던져질 때 등 프로그램의 이벤트 지점이 모두 후보가 된다.
- Pointcut: Join Point 중 어떤 것에 적용할지 결정하는 규칙. 예를 들어 “
service.*패키지의 모든 public 메서드”가 될 수 있다. - Advice: 실제로 어떤 시점에 무슨 동작을 할지. Before, After, Around 세 가지 패턴이 대표적이다.
- Weaving: 비즈니스 코드와 Aspect를 합치는 과정. 컴파일 시점, 클래스 로딩 시점, 런타임 중 어떤 단계에서든 가능하다.
이 다섯 가지가 합쳐져 “필요한 곳에만, 원하는 시점에” 부가 기능을 삽입할 수 있다.
구현 방식: 언어별 스펙트럼
AOP는 특정 언어의 전유물이 아니다. 구현 방법은 기술 스택에 따라 달라진다.
1. 미들웨어·필터
HTTP 요청과 응답이 지나는 파이프라인을 가로채는 방식이다. 웹 프레임워크 대부분은 이 레이어를 제공한다. Python의 Django/Flask 미들웨어, Node.js의 Express 미들웨어, Java의 Servlet Filter가 전형적이다. 장점은 전역 정책을 한 번에 적용할 수 있다는 것. 예외 처리나 공통 로깅, 요청/응답 변환 같은 기능에 적합하다.
2. 데코레이터·어노테이션
보다 세밀한 제어가 필요하면 함수 단위로 적용한다. Python은 데코레이터(@login_required), Java와 Kotlin은 어노테이션(@Transactional, @Cacheable)이 대표적이다. 비즈니스 메서드마다 다른 정책을 선택적으로 붙일 수 있어 정밀 타겟팅이 가능하다.
3. 프록시 기반 AOP
객체를 감싸는 프록시를 자동으로 생성해 호출 전후에 코드를 끼워 넣는다. Spring AOP는 JDK 동적 프록시나 CGLIB을 사용해 런타임에 빈을 감싸며, @Aspect와 @Around를 조합해 매우 정교한 시점을 잡는다. “원래 객체를 감싸는 래퍼가 호출 경계마다 부가 기능을 실행한다”라고 이해하면 된다.
Spring Bean과 AOP의 밀접한 관계
스프링에서 AOP가 제대로 동작하려면 모든 대상 객체가 Spring Bean이어야 한다. 이 한 문장이 핵심이다.
Bean 컨테이너가 해주는 일
스프링은 애플리케이션 구동 시 IoC 컨테이너를 띄우고, 설정에 따라 객체를 생성·초기화한 뒤 Bean으로 등록한다. 여기서 AOP가 개입한다. 스프링이 빈을 만들 때, AOP 설정이 걸린 클래스라면 원본 객체 대신 프록시 객체를 컨테이너에 넣는다. 이 프록시가 메서드 호출 전/후/예외 발생 시점에 Aspect의 로직을 끼워 넣는다.
결과적으로 우리가 주입받아 사용하는 것은 “진짜 구현체”가 아니라 “프록시로 감싼 Bean”이다. 그래서 개발자는 아무 것도 의식하지 않고 평소처럼 @Autowired 혹은 생성자 주입으로 의존성을 받아 사용하면 된다.
동작 흐름
- 컨테이너 초기화: 설정된
@Aspect와 Pointcut 정보를 읽어들인다. - 프록시 생성: JDK 동적 프록시(인터페이스 기반)나 CGLIB(클래스 상속 기반)으로 대리 객체를 만든다.
- Bean 주입: 이 프록시 객체를 Bean으로 등록해 애플리케이션 전역에서 사용한다.
- 메서드 호출: 호출이 들어오면 프록시가 먼저 Aspect를 실행하고, 그다음 실제 메서드를 위임 호출한다.
만약 개발자가 new 키워드로 직접 객체를 생성하면 이 프록시 과정을 거치지 않는다. 따라서 AOP는 반드시 스프링 컨테이너가 관리하는 Bean을 대상으로만 동작한다는 점을 기억해야 한다.
실전에서 빛나는 장면들
- 요청 로깅·트레이싱: API 호출마다 IP, 파라미터, 응답 시간 등을 자동 기록
- 권한·인증 검증: 특정 엔드포인트 접근 전, 역할(Role) 기반 체크
- 트랜잭션 관리: 메서드 단위로 DB 트랜잭션을 시작·커밋·롤백
- 캐싱·리트라이: 실패 시 재시도나 캐시 저장을 일관되게 처리
- 성능 모니터링: 메서드 실행 시간을 측정해 메트릭 시스템에 전송
이런 기능을 AOP 없이 직접 작성한다면 프로젝트가 커질수록 코드베이스가 복잡해진다. AOP를 쓰면 한 모듈에서 정책을 수정하면 전체 서비스에 반영된다.
Bean과 함께 완성되는 AOP
AOP는 “프레임워크 기능”이라기보다 설계 철학이다. 핵심은 비즈니스 로직과 공통 부가 기능을 분리해 서로의 변화를 독립적으로 관리하는 것.
언어나 스택이 달라도 아이디어는 동일하다. Python·Node에서는 미들웨어와 데코레이터로, Java·Kotlin에서는 Aspect와 프록시로, Go나 Rust에서도 인터셉터와 래퍼 패턴으로 구현할 수 있다.
하지만 스프링을 사용할 때는 반드시 기억하자. Spring Bean이 곧 AOP의 관문이다. 컨테이너가 관리하지 않는 객체에는 Aspect가 끼어들 수 없다. 이 단순한 사실이 스프링 AOP의 전제를 이루며, Bean과 AOP가 한 몸처럼 움직이는 이유다.