AOP(관점 지향 프로그래밍, aspect-oriented programming)

기존 기능 모듈(객체)의 변경없이 추가기능(advice)을 결합하여 배포하는 프로그래밍 기법입니다.

추가되는 기능을 어드바이스(advice)라고 하고 어드바이스가 처리해야할 문제를 관심사(aspect) 또는 횡단 관심사(cross-cutting concern)라고 합니다.

AOP 없이 기존 로직(객체)에 추가 기능을 추가한다면

  1. 기존 코드에 추가 기능(관심사) 코드가 섞이게 됩니다. 이때문에 소프트웨어 공학원칙을 많은 부분 벗어나게 됩니다. (단일 책임 원칙..)
  2. 여러 객체에 같은 추가 기능(관심사) 코드를 추가 할때 객체 마다 코드가 반복되게 됩니다. (Don’t repeat yourself)
  3. 1, 2와 같은 이유로 기능 테스트가 매우 어려워 집니다.

AOP를 활용한다면 기존 코드를 손대지 않고 기능을 추가 할 수 있습니다!!

AOP라이브러리에는 어드바이스보다 기존로직을 먼저 실행하느냐 나중에 실행하느냐에 따라 before, after의 메서드 형태와 복수개의 모듈에 동일하게 적용할 수 있는 방법도 제공합니다.

Aop.js

자바스크립트로 구성된 davedx aop.js 라이브러리를 분석해보기로 합니다. (Reliable JavaScript)

// Created by Fredrik Appelberg: http://fredrik.appelberg.me/2010/05/07/aop-js.html
// Modified to support prototypes by Dave Clayton
Aop = {
    // Apply around advice to all matching functions in the given namespaces
    around: function(pointcut, advice, namespaces) {
        // if no namespaces are supplied, use a trick to determine the global ns
        if (namespaces == undefined || namespaces.length == 0)
            namespaces = [ (function(){return this;}).call() ];
        // loop over all namespaces 
        for(var i in namespaces) {
            var ns = namespaces[i];
            for(var member in ns) {
                if(typeof ns[member] == 'function' && member.match(pointcut)) {
                    (function(fn, fnName, ns) {
                        // replace the member fn slot with a wrapper which calls
                        // the 'advice' Function
                        ns[fnName] = function() {
                            return advice.call(this, { fn: fn,
                                                        fnName: fnName,
                                                        arguments: arguments });
                        };
                    })(ns[member], member, ns);
                }
            }
        }
    },
    next: function(f) {
        return f.fn.apply(this, f.arguments);
    }
};

Aop.before = function(pointcut, advice, namespaces) {
    Aop.around(pointcut,
            function(f) {
                advice.apply(this, f.arguments);
                return Aop.next.call(this, f);
            },
            namespaces);
};

Aop.after = function(pointcut, advice, namespaces) {
    Aop.around(pointcut,
            function(f) {
                var ret = Aop.next.call(this, f);
                advice.apply(this, f.arguments);
                return ret;
            },
            namespaces);
};

module.exports = Aop;

Aop 라이브러리 개체는 around, next, before, after 메서드로 구성됩니다.

around는 advice를 객체에 묶어주는 역할을 합니다.

next 는 전달된 기존 객체(타겟)를 실행합니다.

before 는 advice실행 후 기존로직을 실행합니다.

after는 advice실행 전 기존로직을 실행합니다.

around

// 예제
var travelService = { // 기존 객체 코드 1
    getSupportTicket: function() {
        console.log("travelService 의 기존 로직");
    }
};

var infoService = { // 기존 객체 코드 2
    getSupportTicket: function() {
        console.log("infoService 의 기존 로직");
    }
};

function cacheAspectFactory() { // advice
    console.log("cache aspect logic (advice)!!");
}

기존 코드인 travelService객체가 있습니다. 여기에 캐시기능을 넣으려고 한다면

Aop.around('getSupportTicket', cacheAspectFactory, [travelService]);

형태로 Aop라이브러리를 호출합니다.

이 코드의 의미는 travelService의 getSupportTicket 메서드에 cacheAspectFactory라는 어드바이스를 등록한다는 의미입니다.

around 메서드는 다음 형태로 되어있는데 등록지점인 pointcut과 처리해야할 관심사인 advice 그리고 처리 해야할 객체 namespaces가 있습니다.

around: function(pointcut, advice, namespaces)

for(var i in namespaces) 복수의 기존 코드에 어드바이스 등록 가능하도록 되어있습니다.

기존 코드 호출 지점에 어드바이스를 등록합니다.

여기서 ns[fnName] 은 travelService[getSupportTicket] 입니다.

(function(fn, fnName, ns) {
	ns[fnName] = function() {
		return advice.call(this, { fn: fn,
					fnName: fnName,
					arguments: arguments });
	};
})(ns[member], member, ns);

travelService[getSupportTicket] 에 fn, fnName, arguments이 등록됩니다.

ns[fnName] = function() {
    return advice.call(this, { fn: fn,
                fnName: fnName,
                arguments: arguments });
};

현재상태에서travelService.getSupportTicket();를 실행하면 기존로직은 실행되지 않고 advice(cacheAspectFactory)만 실행됩니다.

cache aspect logic (advice)!!

before

Aop.before('getSupportTicket', cacheAspectFactory, [travelService]);

를 실행해 보겠습니다.

라이브러리 내의 before 메서드는 function(pointcut, advice, namespaces) 로 around와 같은 형태로 인자를 받도록 되어있습니다.

전달된 인자로 around 메서드를 실행하면서 새로운 어드바이스를 전달합니다. function(f)...

    Aop.around(pointcut,
            function(f) {
                advice.apply(this, f.arguments);
                return Aop.next.call(this, f);
            },
            namespaces);

around 메서드에서 advice.call 이 실행된면서 travelService[getSupportTicket] 에 before 메서드에 전달된 파라미터가 fn, fnName, arguments 프로퍼티에 설정됩니다. 이 파라미터들은 어드바이스 function(f) 의 f 에 arguments 형태로 설정됩니다.

advice를 실행(advice.apply..) 합니다. before 메서드 호출시 인자로 넘어온 인자로 전달된 advice인 cacheAspectFactory 파라미터를 Aop.next.call(this, f);로 등록됩니다. (before 내에서 생성한 advice 아닙니다.)

function(f) {
	advice.apply(this, f.arguments);
	return Aop.next.call(this, f);
}

결과적으론 travelService 의 getSupportTicket 메서드에 새로운 어드바이스가 등록 됩니다.

travelService.getSupportTicket(); 를 실행해보면 다음처럼 출력됩니다.

cache aspect (advice) logic!!
travelService 의 기존 로직

after

after는 before와 동일하고 기존 로직을 먼저 실행하고 어드바이스를 실행하게 됩니다.

travelService.getSupportTicket(); 를 실행해보면 다음처럼 출력됩니다.

travelService 의 기존 로직
cache aspect (advice) logic!!

배열인자

예제에서는 travelService 하나에만 적용하게 되어있었는데 배열로 넘기게 되면

Aop.around('getSupportTicket', cacheAspectFactory, [travelService, infoService]);

동시에 복수의 객체(기존코드)에 어드바이스 적용이 가능합니다.

travelService.getSupportTicket();
infoService.getSupportTicket();
travelService 의 기존 로직
cache aspect logic (advice) !!
infoService 의 기존 로직
cache aspect logic (advice) !!

Reference

  1. Reliable JavaScript
  2. davedx aop.js