소
프트웨어 기술은 문제를 해결하는 방향으로 발전하고 있다. 객체지향 프로그래밍은 데이터와 메쏘드를 하나의 데이터 구조로
결합함으로써 기존 구조적 프로그래밍(structured program ming)의 문제를 해결하고자 했으며, 컴포넌트 기반
프로그래밍은 객체지향 프로그래밍이 갖는 바이너리 표준 문제와 버전 관리, 개발 언어의 비호환성 등을 해결하고자 했다. 그러나
그러한 컴포넌트가 모든 문제를 해결해 주는 키는 아니다. 컴포넌트를 사용할 때 곧잘 발생하는 문제 중 하나는 컴포넌트가 다양한
영역에서 사용될 때 추가적인 코드를 요구할 때가 발생하곤 한다는 점이다.
예를 들어 보자. 어떤 회사에서 특정
기업군에서 사용될 수 있는 데이터 액세스 컴포넌트를 개발했다. 이 컴포넌트를 배포할 때는 온갖 문제들이 발생하곤 한다. 한
기업에서는 이 컴포넌트가 다른 데이터 액세스와 함께 단일 트랜잭션으로 묶이기를 원하고, 또 다른 기업에서는 그렇게 되지 않기를
원하기도 한다. 트랜잭션뿐인가? 어떤 회사는 보안 규칙이 매우 엄격해 컴포넌트의 모든 메쏘드에 대해 누가 언제 어떤 매개변수로
호출을 했는지 로그를 남기기를 원할 수도 있다. 컴포넌트가 사용되는 다양한 영역에 대해 컴포넌트 개발자가 모두 고려하기란 매우
어려운 것이다.
AOP 개념 이렇게 컴포넌트의 다양성에 대한 해결책을 고민한 것이 Aspect
Oriented Programming(AOP, 영역 지향 프로그래밍)이라 할 수 있다. 앞서 예를 든 것처럼 컴포넌트가
트랜잭션이나 로깅(logging), 보안 검사 등을 처리해야 할 때 이러한 처리를 직접 컴포넌트 내에 코드로서 처리한다면 이들
코드들은 각 컴포넌트에서 반복적으로 나타날 것이며, 매 프로젝트마다 비슷한 코드가 다시 사용돼야 할 것이다. <리스트
1>을 살펴보자.
<리스트 1> 전통적인 트랜잭션 처리 코드 void TP_SERVICE(tpsvcinfo* info) { int tx = 0;
if (tpgetlev() == 0) { // 트랜잭션이 시작되었는지 검사 tpbegin(30, 0); // 트랜잭션이 시작되지 않았다면 시작한다. tx = 1; }
// 데이터 액세스 처리 루틴 (생략)
if (tx == 1) { // 트랜잭션을 시작했다면 커밋/롤백을 수행 tpcommit(); } }
이
코드는 전통적인 미들웨어에서 트랜잭션을 처리하는 코드를 예로 든 것이다. 이 서비스의 도입부와 종료부에서 트랜잭션이 시작 여부에
따라 트랜잭션을 시작하고 종료하고 있다. 그다지 복잡하게 보이지 않지만, 이 코드들이 많은 함수들에서 반복적으로 사용될 것임은
의심할 나위가 없다. 또한 <리스트 1>에서는 나타나지 않았지만, 데이터 액세스 루틴에서 오류가 발생한 경우에는
트랜잭션을 시작했는가 혹은 그렇지 않은가에 따라 오류 처리 루틴이 달라져야 할 것은 너무도 자명하다. 더욱 더 문제를 심각하게
하는 것은 이 코드가 유연성이 현저히 떨어지기 때문에 약간의 환경 변화에도 수정될 가능성이 높아지며, 동일한 코드가 많은
함수에서 반복적으로 사용되었기 때문에 우리의 불쌍한 개발자들은 또 다시 삽을 들고 ‘천삽 푸고 허리 펴기 운동’의 ‘삽질’을
해야 할 것이다. 가끔씩 ‘어? 원래 코드가 맞네!’라는 소리가 들리면 다시 삽을 들고 파놓았던 구덩이를 메워야 할 수도 있다.
이
제 앞 문제를 AOP의 시각에서 해결하고자 한다면 트랜잭션의 처리와 같은 영역을 모듈화해 간단한 특성으로 코드에 ‘표시’한다.
이 글은 ‘닷넷 컬럼’이다. 자바 코드나 언어 중립적인 의사 코드를 기대했다면 실망할 것이다. <리스트 2>는
<리스트 1>과 동등한 C# 코드이다. 트랜잭션의 시작 유무에 따라 트랜잭션을 시작하는 것은 트랜잭션
특성(attribute)이 처리해 준다. 또한 트랜잭션의 commit/ abort 결정은 메쏘드가 예외를 발생했는가에 따라
AutoComplete 특성이 알아서 처리해 준다. <리스트 1>과 같이 트랜잭션에 관련된 구체적인 코드는 존재하지
않으며 특성을 통해 영역 지향적이고, 재사용 가능하고, 반복적이며, 버그 잠재성이 높은 코드들을 분리해 낸 것이다.
<리스트 2> 특성을 이용한 트랜잭션 처리 코드 using System.EnterpriseServices;
[Transaction(TransactionOption.Required)] // 트랜잭션이 시작되지 않았으면 시작한다. class MyDataService : ServicedComponent { [AutoComplete] // 메쏘드 처리에서 예외가 발생하지 않으면 커밋을 수행한다. public void Method1() { // 데이터 액세스 처리 루틴 (생략) } }
이
런 관점에서 볼 때 AOP의 선구자는 COM+의 전신인 MTS라고 볼 수 있다. 트랜잭션과 동기화 특성을 카탈로그에 기록해 두고
이 특성을 변경함에 따라 컴포넌트가 다양한 영역에 적응할 수 있도록 해주기 때문이다. MTS가 EJB는 물론이요, AOP가
거론되기 몇 년 전에 릴리즈됐다는 점을 상기하자. 하지만 MTS/COM+가 제공하는 영역 지향성은 확장성이 떨어진다. 즉,
사용자 정의 특성을 허용하지 않고 트랜잭션 처리와 컴포넌트 서비스에 국한된 특성만을 제공한다는 점이다.
AOP in .NET AOP
를 위한 닷넷 인프라의 핵심은 특성이라고 할 수 있다. <리스트 2>에서 볼 수 있듯이 간단한 트랜잭션 특성과
AutoComplete 특성을 통해 반복적인 코드를 제거할 수 있는 것이다. 닷넷 특성은 광범위한 분야에서 사용되고 있으며,
닷넷 프레임워크의 뛰어난 유연성을 등에 업고 사용자 정의 특성을 손쉽게 정의하고 사용할 수 있다. 예를 들어, 개발자가 작성한
컴포넌트에 로깅 기능을 추가하기 위해 MyLogging 특성을 정의할 수 있고 이 특성을 임의의 클래스에 설정함으로써 메쏘드
호출에 대한 로그를 생성할 수도 있다.
지금까지 AOP를 특성 관점에서만 살펴봤지만 AOP를 제공하는 또 하나의
중요한 기법은 가로채기(interception)이다. 가로채기는 프로퍼티 액세스나 메쏘드 호출을 가로채어 필요한
전처리(pre-processing)와 후처리(post-processing)를 수행하는 것을 말한다. 지금까지 가로채기 기법의
가장 큰 수혜자는 역시 COM+로 볼 수 있다. COM+가 제공하는 트랜잭션, 동기화, JIT 활성화 등은 모두 이 가로채기를
통해 이뤄진다. 어떤 컴포넌트가 JIT 활성화 특성이 설정되어 있을 때, 메쏘드 호출이 발생하면 가로채기 인프라가 필요에 따라
객체를 활성화(activate)라는 전처리를 수행한 후에 메쏘드 호출이 이뤄지도록 한다. 그리고 메쏘드 호출이 종료되면 가로채기
인프라는 객체를 비활성화(deactivate)라는 후처리를 수행한다. 따지고 보면 특성은 가로채기와 뗄래야 뗄 수 없는 관계를 갖는다고 볼 수 있겠다.
이
글은 학술지의 논문이 아니다. AOP에 대한 이론적인 면은 더 이상 기대하지 않는 것이 좋다. 이번 컬럼에서는 이 가로채기에
관련된 닷넷의 다양한 인프라를 살펴보는 것에 중점을 둘 것이다. 특히 가로채기의 핵심적인 기법인 메쏘드 호출을 메시지화하는 것과
이 메쏘드 호출 메시지를 처리하는 프록시, ContextBound Object 클래스의 정체 등에 대해 상세히 알아보도록 하자.