서두에
때때로 다양한 곳에서 AOP 라는 단어를 보게 됩니다. wiki 도 여러 번 조회했지만, 매번 그 낯선 용어들의 산에 겁을 먹었습니다. 이번에는 드디어 AOP 에 도전하기로 결심했습니다
마지막으로 알게 된 것은, AOP 는 디자인 패턴과 비슷하지만, [전략 패턴](/articles/디자인 패턴 之 전략 패턴(strategy-pattern)/), [템플릿 메서드 패턴](/articles/디자인 패턴 之 템플릿 메서드 패턴(template-method-pattern)/), [데코레이터 패턴](/articles/디자인 패턴 之 데코레이터 패턴(decorator-pattern)/) 과는 다르다는 것입니다. AOP 의 가까운 친척은 프록시 패턴 으로, 마찬가지로 로직을 분리할 수 있으며, 핵심도 인터셉트와 세부 사항 은닉입니다
P.S. 이렇게 많은 용어를 꺼낸 것은 고의가 아닙니다. AOP 를 이해하는 과정에서 실제로 이 몇 가지 패턴을 비교 고려했기 때문입니다. 그리고, 디자인 패턴이라는 것은, 음,怎么说呢, 유용한가? 유용하지 않은가? 음
일.용어 (Glossary)
- 애스펙트 (Aspect)
AOP 에서 "어디서 무엇을 하는지의 집합"을 나타냅니다
횡단 관심사의 모듈화. 예를 들어 위에서 언급한 로그 컴포넌트. 인핸스먼트, 인트로덕션, 포인트컷의 조합으로 생각할 수 있습니다. 예를 들어 로그, 캐시, 트랜잭션 관리
- 조인 포인트 (Join point)
AOP 에서 "어디서"를 나타냅니다
프로그램 내에 횡단 관심사를 삽입하는 확장 포인트를 나타냅니다. 조인 포인트는 클래스 초기화, 메서드 실행, 메서드 호출, 필드 호출 또는 예외 처리 등이 될 수 있습니다. 실행 시의 한 점을 나타내며, 예를 들어 메서드 실행 또는 속성 액세스
- 어드바이스 (Advice)
AOP 에서 "무엇을 하는지"를 나타냅니다
또는 인핸스먼트라고도 하며, 조인 포인트에서 실행되는 동작. 어드바이스는 AOP 에서 포인트컷으로 선택된 조인 포인트에서 기존 동작을 확장하는 수단을 제공합니다. 사전 어드바이스 (before advice), 사후 어드바이스 (after advice), 라운드 어드바이스 (around advice) 를 포함합니다. 특정 조인 포인트에서의 애스펙트 동작을 나타냅니다
- 포인트컷 (Pointcut)
AOP 에서 "어디서 하는지의 집합"을 나타냅니다
일련의 관련 조인 포인트 패턴을 선택합니다. 즉 조인 포인트의 집합으로 생각할 수 있습니다. Spring 은 perl5 정규 표현식과 AspectJ 포인트컷 패턴을 지원하며, Spring 은 기본적으로 AspectJ 구문을 사용합니다. 조인 포인트에 매칭하는 정규 표현식. 어드바이스에는 관련 포인트컷 식이 있으며,それに 매칭하는 임의의 조인 포인트에서 실행됩니다. 예를 들어, 특정 이름의 메서드 실행
- 인트로덕션 (Introduction)
AOP 에서 "무엇을 하는지 (무엇을 추가하는지)"를 나타냅니다
내부 타입 선언 (inter-type declaration) 이라고도 하며, 기존 클래스에 추가의 새로운 필드 또는 메서드를 추가합니다
- 위빙 (Weaving)
애스펙트를 다른 애플리케이션 타입 또는 객체에 링크하여, 인핸스먼트 객체를 생성합니다
위빙은 프로세스이며, 애스펙트를 타겟 객체에 적용하여 AOP 프록시 객체를 생성하는 프로세스입니다. 위빙은 컴파일 시, 클래스 로드 시, 실행 시에 수행할 수 있습니다
- 타겟 객체 (Target Object)
AOP 에서 "누구에게 하는지"를 나타냅니다
횡단 관심사를 위빙할 필요가 있는 객체. 즉 그 객체는 포인트컷에서 선택되는 객체이며, 인핸스먼트될 필요가 있는 객체이며, 따라서 "인핸스먼트 대상 객체"라고도 불립니다
- AOP 프록시 (AOP Proxy)
AOP 프레임워크가 프록시 패턴을 사용하여 생성하는 객체. 조인 포인트에서 어드바이스를 삽입 (즉 애스펙트를 적용) 하는 것을 실현합니다. 즉 프록시를 통해 타겟 객체에 애스펙트를 적용합니다
용어가 비교적 많지만, 간단히 분류합니다:
추상 개념: 애스펙트, 인트로덕션, 위빙, 타겟 객체, AOP 프록시
구체 개념: 조인 포인트, 어드바이스, 포인트컷
관계: 포인트컷은 조인 포인트가 형성하는 집합. 둘 다 로직을 삽입할 필요가 있는 타겟 위치를 나타내며, 어드바이스는 삽입할 필요가 있는 구체적인 동작을 나타냅니다
AOP 를 사용할 때 주목하는 것은조인 포인트와 포인트컷입니다. 전자는 "어느 위치에 로직을 삽입하고 싶은지", 후자는 "어느 영역에 로직을 삽입하고 싶은지 (영역은 위치로 구성됨)", 그리고 개입하여 어드바이스를 등록하고, 사전 사후 로직을 추가합니다
###어드바이스 타입
- 사전 어드바이스 (Before advice)
어떤 조인 포인트 전에 실행되는 어드바이스. 그러나 이 어드바이스는 조인 포인트 전의 실행을阻止할 수 없습니다 (예외를 스로우하지 않는 한)
- 사후 반환 어드바이스 (After returning advice)
어떤 조인 포인트가 정상적으로 완료된 후에 실행되는 어드바이스: 예를 들어, 메서드가 예외를 스로우하지 않고 정상적으로 반환
- 사후 예외 어드바이스 (After throwing advice)
메서드가 예외를 스로우하여 종료할 때 실행되는 어드바이스
- 사후 최종 어드바이스 (After (finally) advice)
어떤 조인 포인트가 종료할 때 실행되는 어드바이스 (정상 반환 또는 예외 종료 모두)
- 라운드 어드바이스 (Around Advice)
조인 포인트를 둘러싸는 어드바이스. 메서드 호출 등. 이는 가장 강력한 어드바이스 타입입니다. 라운드 어드바이스는 메서드 호출 전후에 커스텀 동작을 완료할 수 있습니다. 또한 조인 포인트의 실행을 계속할지, 아니면 직접 자신의 반환 값을 반환할지, 또는 예외를 스로우하여 실행을 종료할지 선택하는 책임도 있습니다
주의가 필요한 것은 AfterThrowing 과 AroundAdvice 의 차이입니다. 비즈니스 로직이 예외를 발생한 후, 전자는 트리거되지만, 예외 객체를 얻을 수 없고, 주목하고 있는 메서드가 예외를 발생했다는 것만 알 수 있어, 의미는 크지 않습니다. 후자는 비즈니스 로직을 완전히 래핑하므로, 예외 정보를 캡처할 수 있습니다 (비동기 콜백 예외는暂く 논의하지 않습니다).其它几种 어드바이스는 글자 그대로의 의미로, 매우 이해하기 쉽습니다
이.역할
AOP 는 비즈니스와는 무관하지만, 비즈니스 모듈이 공동으로 호출하는 로직 또는 책임 (예를 들어 트랜잭션 처리, 로그 관리, 권한 제어 등) 을 캡슐화할 수 있어, 시스템의 중복 코드를 감소시키고, 모듈 간의 결합도를 낮추며, 미래의 조작성과 유지보수성을 향상시키는 데 유리합니다
예를 들어感受一下. 객체 지향 코드는 쉽게 이렇게 됩니다:
/**
* OO style
*/
class OOUser {
public function add($fields) {
// check auth
if (!$this->isGranted('ADD_USER')) {
throw new Exception("Access Denied");
}
// log
$this->log('creating user');
// create user
try {
$user = array('id' => '003');
$user['name'] = 'user';
//...
// save
$this->insertUser($user);
} catch(Exception $e) {
$this->log('user create error: ' + $e);
// handleError($e);
}
// log
$this->log('user created');
}
}
캐시, 로그, 예외 처리, 권한 체크 등의 로직이 프로젝트 코드의 도처 (User 클래스뿐만 아님) 에 분산되어 산재해 있으며, 분리할 수 없습니다. 많은 문제가 존재합니다:
-
재사용할 수 없음
-
클래스의 본래 직능을 이해하기 어려움. 로직이 어지러움
-
매우 오류가 발생하기 쉬움. 이러한 보일러플레이트 (boilerplate) 코드를 작성하는 것을 잊은 경우
-
DRY 원칙에 위배. 각 로직 블록에 이러한 익숙한 코드가 산재해 있음
특히 오래된 프로젝트를 유지보수할 때, 한 덩어리 한 덩어리의 익숙한 코드를 보는 것은 매우 괴롭습니다. 수정하고 싶지만 끌어낼 수 없음. 또는 매우 고생해서 마지막에 표면 증상만 조금 완화했을 뿐 (예를 들어,其它의 캡슐화 방식을 고려하여, 몇 줄의 비즈니스 코드를 간소화함)
AOP 는 바로 이 문제를 해결합니다.横向로 객체 내부에 개입하여 내과 수술을 하고, 코어 비즈니스 로직을 박리할 수 있습니다. 우리는진정으로 유용한몇 줄의 코드에 집중할 수 있습니다
삼.PHP AOP 예제
매우 사용하기 좋은 PHP AOP 프레임워크를 발견했습니다: Go! AOP
P.S. 우수한 AOP 프레임워크는 리플렉션과 어노테이션을 포함하므로, 필자의 현재 PHP 능력으로는 부족하여, 수동으로 AOP 메커니즘을 구현하는 생각을 포기했습니다
이전의 OO 코드를 고려하여, 로직 블록을 분류합니다:
public function add() {
//=before advice
// check auth
if (!isGranted('ADD_USER')) {
throw new Exception("Access Denied");
}
//=before advice
// log
log('creating user');
//=business logic
// create user
try {
$user = array('id' => '003');
$user['name'] = 'user';
//...
// save
insertUser($user);
} catch(Exception $e) {
//=after throwing advice
log('user create error: ' + $e);
// handleError($e);
}
//=after advice
// log
log('user created');
}
비즈니스 로직은 몇 줄뿐이지만,其它그리 중요하지 않은코드에 깊게 싸여 있습니다. 업데이트 유지보수 시에는 "一大片의 코드 중에서 某一小塊을 수정하는" 문제에 직면합니다.关键 부분에 위치시켜 조심스럽게 수정하고, 그래도 매우 오류가 발생하기 쉽습니다 (특히 예외 처리)
다음으로 비즈니스 로직을 추출하고, 새로운 User 클래스는 이렇게 됩니다:
/**
* AOP style
*/
class AOPUser {
public function add($fields) {
//=business logic
// create user
$user = array('id' => '003');
$user['name'] = 'user';
//...
// save
$this->insertUser($user);
// throw error
$this->badMethod();
throw new Exception("A Stange Error");
}
/**
* Insert user to database
*
* @Loggable
*
* @param Array $info Info
*/
public function insertUser($user) {
//...
$this->log('user inserted');
}
//...其它 공유할 수 없는 의존 메서드
}
비즈니스 로직을 분리했습니다. 공유할 수 있는 의존 메서드 (예를 들어, log(), isGranted() 등) 는 모두 추출되어 공유lib 이 되고,其它 공유할 수 없는 의존 메서드는 여전히 클래스 멤버로 존재합니다.此时 User 클래스의 책임은 비교적 단일하고, 불협화음인 코드는 모두 굴러나갔으며, 로직은 매우 명확합니다
다음으로 조립이 필요합니다 (데코레이터 패턴과 비슷하지만, 구현 방식에서는 큰 차이가 있습니다). 굴러나간 관련 코드를 다시 장착합니다. AOP 가 동적으로 조립해 줍니다. 우리는 관련을 선언하고, AOP 에어디서 무엇을장착할지 (즉 용어 "애스펙트"의 의미) 를 알리기만 하면 됩니다
/**
* User aspect
*/
class UserAspect implements Aspect {
/**
* Pointcut for add method
*
* @Pointcut("execution(public App\App\AOPUser->add(*))")
*/
protected function UserAdd() {}
// $aopuser->add() 실행 시 개입
/**
* Check auth before add user
*
* @param MethodInvocation $invocation Invocation
* @Before("$this->UserAdd")
*/
protected function checkAuthBeforeAdd(MethodInvocation $invocation) {
/** @var $user \App\App\AOPUser */
$user = $invocation->getThis();
// check auth
if (!$isGranted('ADD_USER', $user->caller)) {
throw new Exception("Access Denied");
}
}
/**
* Handle Error after throwing
*
* @param MethodInvocation $invocation Invocation
* @AfterThrowing("$this->UserAdd")
*/
protected function handleErrorAfterThrowing(MethodInvocation $invocation) {
/** @var $user \App\App\AOPUser */
$user = $invocation->getThis();
// =after throwing advice
$log('user create error, handle error here');
// handleError();
//!!! avoid reporting error
set_exception_handler(function($e) {
echo "!!!Global Exception Handler: " . $e->getMessage();
});
}
/**
* Log after add user
*
* @param MethodInvocation $invocation Invocation
* @After("$this->UserAdd")
*/
protected function logAfterAdd(MethodInvocation $invocation) {
/** @var $user \App\App\AOPUser */
$user = $invocation->getThis();
// log
$log('user created');
}
//...其它 어드바이스
}
어노테이션을 통해 어드바이스와 타겟 객체의 관련을 선언하고, AOP 에 어디에 어떤 로직을 삽입할지 알려, 로직의 들러붙음을 제거합니다
주의handleErrorAfterThrowing() 메서드. 글로벌 예외 에러를 피하기 위해, set_exception_handler() 를 사용했습니다. 이렇게 하는 이유는 AfterThrowing 어드바이스가切点에서 예외가 발생할 때 트리거되지만, 예외 객체를 얻을 수 없고, 먹을 수도 없기 때문에, 글로벌 예외 인터셉트를 통해 이 예외를 먹기 때문입니다
어떤 프로세스 중의 예외를 정확하게 조작할 필요가 있다면, Around 어드바이스를 사용하여, 타겟 프로세스를 완전히 래핑하고, try-catch 하면 됩니다. 다음과 같이:
/**
* Around advice to catch exception
* @param MethodInvocation $invocation Invocation
* @Around("execution(public App\App\AOPUser->badMethod(*))")
*/
protected function aroundBadMethod(MethodInvocation $invocation) {
try {
$invocation->proceed();
} catch (Exception $e) {
echo '!!!Around Advice Error Handler: ' . $e->getMessage() . "<br>\n";
}
}
P.S. 여기서는 비동기 콜백 중의 예외에 대해서는 논의하지 않습니다. PHP 는 일반적으로 이 상황을关注하지 않습니다. JS 의 경우도, 어노테이션 방식으로 AOP 를 구현하는 것은 고려하지 않습니다 (고계 함수, binding 등의 방식으로 로직 주입을 수행해야 합니다). 나중에再说
P.S. GO! AOP 은 매우 강력하며, 시스템 메서드 및 도구 함수에 개입하는 방식도 제공합니다. 파라미터 캡처, 속성 액세스 인터셉트 등을 포함합니다
사.Demo
온라인 Demo: http://www.ayqy.net/temp/aop/src/
소스 코드 주소: https://github.com/ayqy/aop
주의: PHP5.5+ 가 필요합니다. GO! AOP 프레임워크 내에서class 를 식별자로 사용하기 때문
오.정리
AOP 는 OOP 를 보완하며,横向로 객체에 개입하여 로직을 주입하여 클래스의 책임을 단일하게 유지합니다
더 적절히 말하면, AOP 는 일종의 디자인 패턴입니다. 비교적 격렬한 견해도 있습니다:
AOP 는 OOP 의 패치.縦方向의 OOP 는 객체 체계를 확립. 상속, 캡슐화, 다형.横向의 AOP 개입.縦橫合璧。천하무적...
틀리지 않았습니다. 다만 침입 정도의 논쟁이 존재합니다. 예를 들어, AOP 로 OO 체계 전체에 개입하고 싶다면, 반드시 침입 정도는 매우 커집니다 (상속을 고려). 개인은 침입 정도가 작은 방안이 더 좋습니다. 유연하지만 불편
怎么说, AOP 를 학습한 것은 일종의 설계思路를 획득한 것과 같습니다. 설계 원칙과 비슷합니다 (복습):
-
변화를 캡슐화 (변화하기 쉬운 부분을 추출하여, 그 변화가其它 부분에 미치는 영향을 감소)
-
상속보다 조합을多用 (조합은 상속보다 탄력성이 있음)
-
인터페이스를 향해 프로그래밍. 구현을 향해 프로그래밍하지 않음 (인터페이스를 사용하면 직접 구체적인 클래스에 의존하는 것을 피할 수 있음)
-
상호작용 객체 간의 소결합 설계를 향해 노력 (더 소결합은 더 많은 탄력성을 의미)
-
클래스는 확장에 대해 열리고, 수정에 대해 닫혀야 함 (open-close 원칙)
-
추상에 의존. 구체적인 클래스에 의존하지 않음 (구체적인 클래스에 대한 직접 의존 감소)
-
친구와만 대화 (친밀한 친구 원칙)
-
나를 부르지 마. 내가 너를 부를게 (Don't call me, I will call you back. 안드로이드 개발의 대원칙)
-
클래스는 변경할 이유를 하나만 가져야 함 (단일 책임 원칙)
-
横向 로직 주입 (AOP)
문제를 고려할 때 또 하나의 선택을 가짐.仅此而已. 대형 시스템을 구축할 때 AOP 는 필요한 내장 기능이어야 함. 그러나 응용 장면而言, AOP 는 만능 키는 아님. 그러나 AOP 의 사상 (横向 로직 주입) 은 임의의 장면에 적용 가능
참고 자료
-
Weaving aspects in PHP with the help of Go! AOP library: GO! AOP 저자의 PPT. 입문에 적합 (감성 인지)
-
Introduction to aspect-oriented programming: AOP 용어에 대한 설명
-
我对 AOP 의 이해: 후반 부분은 매우 좋음
아직 댓글이 없습니다