Aspect-Oriented Programming의 이해와 적용

객체 지향 프로그래밍(Object-Oriented Programming, a.k.a. OOP)은 코드를 객체라는 독립적인 모듈로 묶어 관리한다. 각 객체는 관심사의 분리 원칙에 따라 만들어져야 하는데, 이는 각 객체가 자신의 특정 책임에 집중해야 한다는 의미다. 그러나 실제 서비스를 개발하다보면 여러 객체나 모듈에 걸쳐 공통적으로 나타나는 기능들을 종종 만나게 된다. 이렇게 여러 모듈에 걸쳐 있는 기능들을 횡단 관심사(Cross-cutting Concerns)라고 부른다.

횡단 관심사가 가지는 문제점

Scattering

이러한 횡단 관심사들은 OOP만으로 다룰 경우 두 가지 주요 문제를 만든다. 첫 번째 문제는 Scattering이라고 부른다. Scattering은 특정 기능의 코드가 복사 붙여넣기를 통해 여러 곳에 흩뿌려지는 현상을 말한다. 예를 들어, 모든 함수에 사용자 권한 확인 로직과 로깅 코드를 추가한다고 해보자. 동일한 로깅 및 권한 확인 코드가 각 메서드 내에 반복적으로 나타날 것이다.

Tangling

다른 문제는 Tangling이다. 직역하면 얽힘이라고 할 수 있는데, 이는 비즈니스 로직과 횡단 관심사의 코드가 하나의 모듈이나 함수 안에서 뒤섞여 복잡해지는 현상을 말한다.

class LibraryService:
def __init__(self):
self.repository = BookRepository()
def get_book_info(self, book_id):
# (Cross-cutting Concern) Method entry logging
log("info", "get_book_info method started")
start_time = time.time() # For performance measurement
# (Cross-cutting Concern) User permission check
if not UserSession.has_permission("READ"):
raise PermissionError("Access Denied: Insufficient permissions")
# (Business Logic) Find book information
book = self.repository.find_by_id(book_id)
if book is None:
raise NoBookError(book_id)
# (Cross-cutting Concern) Performance measurement
end_time = time.time()
log("info", f"get_book_info execution time: {end_time - start_time:.3f}s")
# (Business Logic) Return book information
return book

get_book_info 함수의 주된 목적은 원하는 책의 정보를 찾는 것이다. 하지만 로깅, 권한 확인, 성능 측정 등 다양한 관심사가 그 안에 뒤얽혀 있다. 이 때문에 이 함수의 가독성은 떨어지고 핵심 비즈니스 로직을 파악하기 어려워진다. 이는 마치 지저분한 책상과 같다. 익숙해지면 무엇이 어딨는지 한 번에 알 수 있지만, 익숙하지 않은 사람은 무엇이 어디있는지 찾기 힘들어진다.

Aspect-Oriented Programming의 등장

관점 지향 프로그래밍(Aspect-Oriented Programming, a.k.a. AOP)은 이런 문제를 비즈니스 로직횡단 관심사를 분리하여 해결하려 한다. AOP는 90년대 후반 Xerox PARCGregor Kiczales와 그의 연구팀이 처음 제안했다. 키찰레스는 기존의 패러다임(프로시저, 객체 등)으로 모듈화할 수 있는 코드를 컴포넌트라고 불렀다. 하지만 어떤 속성은 프로그램 전반에 걸쳐 있기 때문에 기존 패러다임으로는 충분히 모듈화할 수 없다고 보았다. 이런 모듈화될 수 없는 속성을 횡단 관심사라고 정의하였으며, 이 횡단 관심사를 Aspect라는 독립적인 구현으로 분리해 깔끔하게 설계할 수 있다고 주장했다.

Aspect-Oriented Programming의 주요 개념

AOP는 횡단 관심사들을 비즈니스 로직으로부터 분리하여 Aspect는 독립적인 모듈로 조직화하여 비즈니스 로직에 삽입함으로써 작동한다. 이렇게 조직된 Aspect들은 필요할 때 삽입되며, 핵심 비즈니스 로직 자체를 변경하지 않는다. 이러한 접근 방식을 비침투적(non-invasive)이라고 하는데, 이는 핵심 비즈니스 로직 코드를 직접 수정하지 않고도 부가 기능을 추가하거나 변경할 수 있음을 뜻한다. 예를 들어, 모든 로깅 관련 로직은 LoggingAspect로 캡슐화할 수 있다.

Join Point

Join Point는 프로그램 실행 흐름에서 Aspect의 기능을 삽입될 수 있는 지점을 의미한다. 이는 메서드 호출, 객체 생성, 필드 접근, 심지어 예외 발생까지 포함할 수 있다. Join Point에서 실제로 적용되어 실행되는 과정을 Weaving이라고 부른다.

Weaving

키찰레스는 Weaving이 일어나는 시점을 컴파일 타임과 런타임 양쪽이 가능하다고 생각했다. 하지만 현대의 AOP 라이브러리들은 대부분 프록시 기반 런타임 Weaving으로 동작한다. Spring AOP가 대표적이다. 이 방식은 실행 중에 대상 객체를 직접 수정하는 대신, 대상 객체를 대신하는 프록시(Proxy) 객체를 생성하여 호출을 가로챈다.

Advice

Advice는 Aspect가 Join Point에서 실제로 수행할 동작을 정의한 코드 블록이다. Advice는 실행되는 시점에 따라 종류를 나눌 수 있다. 대상 이전에 실행되는 before advice, 이후에 실행되는 after advice, 그리고 Join Point를 감싸는 around advice의 세 유형으로 나누는 것이 일반적이고, AspectJ 같은 경우는 after advice를 다시 세분화해서 함수가 정상적으로 종료했을 때만 불리는 after returning advice와 예외가 발생했을 때 실행되는 after throwing advice을 추가적으로 구분하기도 한다.

Pointcut

Advice가 Join Point에서 실행되는 것은 맞지만 Advice가 정의되어 있다고 해서 무조건 모든 Join Point에서 실행되야 하는 것은 아니다. 특정 조건에 맞는 Join Point에서만 Advice를 실행시키고 싶을 때 어떤 Join Point에서 실행할지 정의하는 것이 Pointcut이다. Pointcut은 Join Point의 부분 집합을 지정하는 조건식으로, Aspect가 개입할 위치를 세밀하게 조정할 수 있도록 한다. 예를 들어, execution(* get_*(*)) 같은 식으로 get_으로 시작하는 함수의 실행에서만 실행되도록 선언할 수 있다. 이러한 방식으로 비즈니스 로직과 횡단 관심사의 연결을 느슨하게 유지하면서 횡단 관심사의 적용 범위를 선언적으로 제어할 수 있게 해준다. 이로 인해 시스템의 구조적 복잡성을 줄이고, Aspect의 재사용성과 유지보수성을 높이는 데 기여한다.

예제

Aspect는 보통 Spring 프레임워크와 함께 사용하는 것이 일반적이지만, 이번 글에서는 Python의 aspectlib을 이용해 예제를 작성하였다. 이는 aspectlib이 Spring에 비해 더 단순하기 때문에 AOP의 핵심 아이디어를 더 직관적으로 보여줄 것이라고 생각하기 때문이다.

예제 1: 로깅

@aspectlib.Aspect(bind=True)
def start_log_aspect(cutpoint, *args, **kwargs):
# (Cross-cutting Concern) Method logging
print(f"'{cutpoint.__name__}' method started")
yield
@aspectlib.Aspect(bind=True)
def finish_log_aspect(cutpoint, *args, **kwargs):
yield
# (Cross-cutting Concern) Method logging
print(f"'{cutpoint.__name__}' method finished")
if ENV == 'development':
aspectlib.weave(LibraryService, start_log_aspect)
aspectlib.weave(LibraryService, finish_log_aspect)
view raw log_aspect.py hosted with ❤ by GitHub

AOP가 사용되는 가장 흔한 사례는 로깅이다. 로깅은 개발 환경과 운영 환경에서 다르게 동작해야 하는 경우가 많다. 예를 들어, 개발 환경에서는 디버깅을 위해 모든 요청과 응답을 상세히 기록하고, 운영 환경에서는 성능 저하를 피하기 위해 에러 상황만 기록하는 경우가 일반적이다. 그런데 이러한 로깅 코드를 비즈니스 로직 내부에 직접 삽입하게 되면, 로깅 정책이 바뀔 때마다 비즈니스 로직 코드까지 함께 수정해야 하는 번거로움이 발생한다. 이는 불필요한 코드 변경으로 이어져 개발자가 쉽게 실수를 저지를 여지를 만들고, 결과적으로 유지보수를 어렵게 만든다. 이런 경우 로깅을 별도의 Aspect로 분리하여 구현하면, 핵심 비즈니스 로직과 로깅 로직이 서로 얽히지 않아 유연하게 동작 환경에 맞춰 설정을 변경할 수 있다. 이러한 구조는 코드의 가독성과 재사용성을 높이는 데에도 크게 기여한다.

예제 2: 권한 검사

def request_book_info(library, user, book_id):
@aspectlib.Aspect
def check_read_permission(*args, **kwargs):
# (Cross-cutting Concern) User permission check
if not user.has_permission("READ"):
raise PermissionError("Access Denied: Insufficient permissions")
yield
with aspectlib.weave(library, check_read_permission, methods='get_'):
return library.get_book_info(book_id)

두 번째 예제는 권한 검사다. 현대에 개발되는 대부분의 서비스는 다중 사용자를 기반으로 한다. 따라서 사용자별로 접근할 수 있는 기능이나 데이터에 대한 권한을 관리하는 건 매우 중요한 문제다. 하지만 권한 관리는 비즈니스 로직이라기 보다는 여러 비즈니스 로직에 걸쳐 적용되는 로직. 즉, 횡단 관심사라고 볼 수 있다. 이런 경우에도 AOP는 유용한 해결책이 될 수 있다. 권한 검사를 별도의 Aspect로 구현하면, 핵심 로직을 수정하지 않고도 필요한 검증을 삽입할 수 있을 뿐 아니라, 권한 정책이 바뀌었을 때 시스템 전체에 일관성 있게 적용할 수 있는 구조를 만들 수 있다.

예제 3: 성능 측정

@aspectlib.Aspect(bind=True)
def log_execution_time(cutpoints, *args, **kwargs):
# (Cross-cutting Concern) Performance measurement
start_time = time.time()
yield
end_time = time.time()
log(f"'{cutpoints.__name__}({args}, {kwargs})' execution time: {end_time - start_time:.3f}s")

이번 예제에 사용한 Aspect는 around advice로 타겟 함수 전에 시간을 기록하고, 함수가 종료됐을때의 시간과 비교해서 실행된 시간을 측정한다. 이처럼 성능 측정과 같은 부수적인 기능은 애플리케이션의 비즈니스 로직과는 직접적인 관련이 없는 횡단 관심사다. AOP를 사용하면 이러한 횡단 관심사를 비즈니스 로직 코드와 독립적으로 분리하여 관리할 수 있다. 성능 측정 외에도 캐싱, 트랜잭션 관리 등 다양한 부수적인 동작을 비즈니스 로직에 침투하지 않으면서 구현할 때 around advice는 매우 효과적이다.

예제 4: 예외 처리

@aspectlib.Aspect
def report_error(*args, **kwargs):
# (Cross-cutting Concern) Performance measurement
try:
yield
except ex:
report_to_monitor_server(ex)
view raw report_error.py hosted with ❤ by GitHub

이번 예제는 예외가 발생했을 때 실행되는 일종의 after throwing advice다. 서비스 개발에서 기능을 구현하는 것도 중요하지만 그보다 중요한 것은 서비스의 안정성을 확보하는 것이다. 서비스의 안정성을 확보하기 위해서는 예외 발생 시 이를 감지하고 외부 모니터링 시스템에 보고하는 작업이 필요하다. 하지만 이러한 예외 처리 코드를 각 비즈니스 로직 함수 내부에 일일이 작성하다 보면, 핵심 로직이 예외 처리 코드와 뒤섞여 코드의 가독성이 크게 떨어지게 된다. 특히 이런 작업은 모든 함수에서 동일한 방식으로 반복되기 쉽기 때문에 코드 중복이 많아지고, 정책이 변경될 경우 전체 함수를 일일이 수정해야 하는 번거로움도 생긴다. 이런 경우에도 AOP는 효과적인 해결책이 될 수 있다. 예외 보고 로직을 별도의 Aspect로 분리하면, 핵심 로직에는 전혀 영향을 주지 않으면서, 예외가 발생한 시점에만 원하는 방식으로 동작을 삽입할 수 있다. 이로 인해 예외 처리 정책을 일관성 있게 유지할 수 있고, 예외 감지와 보고 방식의 변경도 중앙에서 손쉽게 관리할 수 있다.

결론

Aspect-Oriented Programming의 장점

이러한 예시들을 통해 보듯이, AOP는 코드의 횡단 관심사를 효과적으로 분리하고 관리할 수 있게 해주는 패러다임이다. AOP를 활용하면 로깅, 권한 검사, 성능 측정, 예외 처리와 같이 여러 모듈에 걸쳐 반복적으로 나타나는 기능들을 비즈니스 로직에서 독립시켜 별도의 Aspect로 캡슐화할 수 있다. 이는 핵심 비즈니스 로직의 순수성을 유지하여 가독성을 높이고, 코드의 중복을 줄여 개발자가 쉽게 실수를 저지를 여지를 줄인다. 또한, 관심사를 분리함으로써 각 기능의 유지보수가 쉬워지며, 필요에 따라 특정 기능을 추가하거나 변경, 제거하는 작업이 코드 전체에 미치는 영향을 최소화할 수 있다.

Aspect-Oriented Programming의 단점

AOP는 많은 장점을 가지고 있지만 다른 패러다임에 비해 주류 패러다임이 되지 못했다. 이는 몇 가지 큰 단점 때문이다. 가장 큰 단점은 코드의 흐름을 파악하기 어렵게 만들 수 있다는 점이다. Aspect가 비즈니스 로직 외부에 정의될 뿐 아니라, 코드의 삽입을 관리하는 것도 비즈니스 로직의 선언부나 호출부와 관련 없는 곳에서 벌어진다. 이는 함수가 호출될 때 어떤 부작용이 있는지 한눈에 파악하기 어렵게 만들어, 디버깅을 복잡하게 만들고, 의도치 않은 동작이나 성능 저하를 유발할 수 있다. 또한, 비즈니스 로직과 횡단 관심사를 구분하는 기준은 주관적 판단에 의존하게 된다. 만약 잘못된 분리가 이루어질 경우 Aspect 간의 복잡한 의존성이 생기거나, Aspect 자체가 비즈니스 로직만큼 복잡해지는 문제가 발생할 수도 있다.

그렇다면 AOP는 이제 알 필요가 없을까?

그렇지는 않다. 비록 AOP가 주류 패러다임으로 널리 쓰이진 않더라도, 비즈니스 로직과 횡단 관심사를 분리하려는 접근 방식 자체는 여전히 유효하고 강력하다. AOP는 도구가 아니라 사고방식이라고 봐야 한다. 직접적으로 AOP 프레임워크를 사용하지 않더라도, 프록시 패턴이나 고차 함수와 같은 디자인 패턴과 프로그래밍 기법을 활용하면 비즈니스 로직을 침해하지 않으면서 횡단 관심사를 분리하는 AOP와 유사한 효과를 얻을 수 있다.

댓글

이 블로그의 인기 게시물

USB 2.0 케이블의 내부 구조

[C++] enum class - 안전하고 쓰기 쉬운 enum

터미널 출력 제어를 위한 termios 구조체 이해하기

[Web] SpeechSynthesis - TTS API

Linux의 clear와 Mac의 clear는 다르다