Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 8장 스프링의 관점 지향 프로그래밍 #1

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.



8. 스프링의 관점 지향 프로그래밍

8.1 소개
관점지향 프로그래밍(AOP, Aspect-Oriented Programming)은 프로그램 구조를 다른 방식으로 생각하게 함으로써 객체지향 프로그래밍(OOP, Object-Oriented Programming)을 보완한다. OOP에서 모듈화의 핵심단위는 클래스이지만 AOP에서 모듈화의 핵심단위는 관점(aspect)이다. 관점은 다양한 타입과 객체에 걸친 트랙잭션 관리같은 관심(concern)을 모듈화할 수 있게 한다. (AOP 문서에서는 이러한 관심을 종종 crosscutting 관심이라고 부른다.)

스프링의 핵심 컴포넌트중 하나는 AOP 프레임워크이다. 스프링 IoC 컨테이너가 AOP에 의존하지 않기 때문에(즉 필요하지 않으면 AOP를 사용하지 않아도 된다.) AOP는 아주 기능이 많은 미들웨어 솔루션을 제공해서 스프링 IoC를 보완한다.

스프링 프레임워크에서 사용한 AOP는...

  • ... EJB의 선언적인 서비스의 대체제같은 선언적인 엔터프라이즈 서비스를 제공한다. 이러한 엔터프라이즈 서비스 중에서 가장 중요한 것은 선언적인 트랜잭션 관리이다.
  • ... 사용자가 OOP의 사용을 AOP로 보충하는 커스텀 관점을 구현하도록 해준다.
선언적인 제너릭 서비스나 풀링(pooling)같은 미리 패키징된 다른 선언적인 미들웨어 서비스에만 관심이 있다면 스프링 AOP를 직접 사용할 필요다 없고 이번 장의 대부분을 건너뛰어도 된다.

Spring 2.0 AOP
스프링 2.0에서 스키마 기반의 접근이나 @AspectJ 어노테이션 방식을 사용하서 더 간단하면서도 강력하게 커스텀 관점(aspect)를 작성하는 방법을 도입했다. 두 가지 방법 모두 완전한 타입의 어드바이스(advice)와 AspectJ 포인트컷 언어(pointcut language)를 제공하지만 여전히 위빙(weaving)에는 Spring AOP를 사용했다
.

스프링 2.0의 스키마기반이나 @AspectJ 기반의 AOP는 이번 장에서 얘기하는 것들을 지원한다. 스프링 2.0 AOP는 스프링 1.2 AOP와 완전한 하위호환성을 가지고 있고 다음 장에서 얘기할 스프링 1.2 API가 제공하는 저수준 AOP를 지원한다.


8.1.1 AOP 개념
핵심 AOP의 개념과 용어를 정리하면서 시작해 보자. 이러한 용어들은 스프링에 특화된 것이 아니고 안타깝게도 AOP 용어는 그다지 직관적이지 않다. 하지만 스프링이 스프링만의 용어를 쓴다면 오히려 더 혼란스러울 것이다.

  • 관점(Aspect): 여러 클레스에 걸친 관심사의 모듈화이다. 엔터프라이즈 자바 어플리케이션에서 트랙잭션 관리는 관신사를 보여주는 좋은 예제이다. 스프링 AOP는 정규 클래스(스키마기반의 접근)나 @Aspect 어노테이션이 붙은 정규 클래스(@AspectJ 방식)를 사용해서 관점을 구현했다.
  • 조인 포인트(Join point): 메서드의 실행이나 예외 처리같은 프로그램이 실행되는 중의 어떤 지점이다. 스프링 AOP에서 조인포인트는 항상 메서드 실행을 나타낸다.
  • 어드바이스(Advice): 특정 조인포인트에서 관점이 취하는 행동(action). "around", "before", "after" 어드바이스같은 여러 가지 타입의 어드바이스가 있다.(어드바이스의 타입은 아래에서 나와있다.) 스프링을 포함한 많은 AOP 프레임워크는 조인포인트 주위에 인터셉터 체인을 유지한다.
  • 포인트컷(Pointcut): 조인포인트를 매칭하는 것(predicate)이다. 어드바이스는 포인트컷 표현식과 연결되고 포인트컷이 매치한 조인포인트에서 실행된다. (예를 들어 특정이름의 메서드 실행처럼) 포인트컷 표현식과 일치하는 조인포인트의 개념은 AOP의 핵심이고 스프링은 기본적으로 AspectJ 포인트컷 표현식 언어를 사용한다.
  • 인트로덕션(Introduction): 타입을 대신해서 메서드나 필드를 추가적으로 선언한다. 스프링 AOP로 어떤 어드바이스 객체에도 새로운 인터페이스(와 대응하는 구현체)를 추가할 수 있다. 예를 들어 간단하게 캐싱할 수 있도록 IsModified 인터페이스를 구현하는 빈을 만들 때 인트로덕션을 사용할 수 있다.(AspectJ 커뮤니티에서는 인트로덕션을 인터타입(inter-type) 선언이라고 부른다.)
  • 대상 객체(Target object): 하나 이상의 관점으로 어드바이스된 객체다. 어드바이스된(advised) 객체라고 부르기도 한다. 스프링 AOP가 런타임 프록시를 사용해서 구현되었기 때문에 이 대상 객체는 언제나 프록시된 객체가 될 것이다.
  • AOP 프록시: 관점 계약(aspect contract, 어드바이스 메서드 실행 등등)이 생성한 객체. 스프링 프레임워크의 AOP 프록시는 JDK 다이나믹 프록시나 CGLIB 프록시가 될 것이다.
  • 위빙(Weaving): 다른 어플리케이션 타입이나 어드바이즈된 객체를 생성하는 객체와 관점을 연결한다. 위빙은 컴파일시점(예를 들면 AspectJ 컴파일러를 사용해서), 로딩시점, 런타임시점에서 수행될 수 있다. 다른 순수한 자바 AOP 프레임워크와 같이 스프링 AOP도 런타임시에 위빙을 수행한다.
어드바이스의 타입

  • 어드바이스 이전(Before advice): 조인포인트 이전에 실행되는 어드바이스이지만 조인포인로 진행되는 흐름을 막을 수는 없다.(예외를 던지지 않는 한)
  • 어드바이스 반환 후(After returning advice): 조인 포인트가 정상적으로 완료될 후에 실행되는 어드바이스. 예를 들어 메서드가 예외를 던지지 않고 반환하는 경우이다.
  • 어드바이스를 던진 후(After throwing advice): 예외를 던진 메서드가 있는 경우 실행되는 어드바이스.
  • (최종적인)어드바이스 이후(After (finally) advice): 어떤 조인포인트가 존재하는 지와 상관없이 실행되는 어드바이스(정상적이든 예외를 던지든)
  • 어드바이스 주변(Around advice): 메서드 호출같은 조인포인트를 둘러싼 어드바이스. 이 어드바이스는 어드바이스중에서 가장 강력하다. Around advice는 메서드 실행 이전이나 이후에 임의의 동작을 수행할 수 있다. 반환값을 반환하거나 예외를 던짐으로써 실행되는 어드바이스된 메서드에 대한 단축키나 어떤 조인프인트로 진행할 것인지 선택하는 책임을 지기도 한다.
Around advice가 가장 일반적인 어드바이스다. AspectJ처럼 스프링 AOP가 어드바이스 타입을 모두 제공하기 때문에 필요한 동작을 구현할 수 있는 어드바이스 중에서 가장 덜 강력한 어드바이스를 사용하기를 권장한다. 예를 들어 메서드의 리턴값으로 캐시를 업데이트만 하려고 한다면 around advice로 같은 작업을 할 수 있다로 하더라도 around advice보다는 after returning advice를 구현하는 것이 더 낫다. 가장 구체적인 어드바이스를 사용함으로써 잠재적인 오류를 줄이고 프로그래밍 모델을 더 간단하게 할 수 있다. 예를 들어 around advice에 사용한 JoinPoint에서 proceed() 메서드를 호출할 필요가 없다면 proceed() 메서드 호출하지 않을 수 없다.

스프링 2.0에서 모든 어드바이스 파라미터들은 정적타입이므로 Object 배열대신의 적절한 타입(메서드 실행 결과로 반환된 값의 타입같은)의 어드바이스 파라미터를 사용해야 한다.

포인트컷으로 매치된 조인포인트의 개념은 인터셉트만 제공하는 오래된 다른 기술들과 AOP를 구분짓는 핵심개념이다. 포인트컷은 객체지향 계층에서 어드바이스가 독립적인 대상이 될 수 있게 한다. 예를 들어 선언적인 트랜잭션 관리를 제공하는 around advice를 다양한 객체(서비스 레이어의 모든 비즈니르 작업같은)에 걸친 메서드 세트에 적용할 수 있다.


8.1.2 Spring AOP의 능력과 목표
스프링 AOP는 자바로만 구현되었고 여기에는 특별한 컴파일 과정이 필요없다. 스프링 AOP는 클래스 로더 계층을 제어할 필요가 없으므로 서블릿 컨테이너나 어플리케이션 서버에서 사용하기에 적합하다.

현재 스프링 AOP는 메서드 실행 조인포인트만 지원한다.(스프링 빈의 메서드를 실행하는 어드바이스) 핵심 스프링 AOP API를 깨뜨리지 않고도 필드 인터셉트를 추가할 수 있었지만 필드 인터셉트는 구현되지 않았다. 필드 접근을 어드바이스하고 조인포인트를 갱신해야 한다면 AspectJ같은 언어를 고려해라.

AOP에 대한 스프링 AOP의 접근은 다른 대부분의 AOP 프레임워크와는 다르다. 가장 완전한 AOP 구현체를 제공하고자 하는 것이 아니다.(스프링 AOP가 아주 강력하기는 하지만) 오히려 엔터프라이즈 어플리케이션의 일반전인 문제를 해결할 수 있도록 스프링 IoC와 AOP 구현체의 닫힌 통합을 하려는 것이 목적이다.

그러므로 스프링 프레임워크의 AOP 기능은 보통 스프링 IoC 컨테이너와 겹합해서 사용한다. 일반적인 빈 정의 문법(이 문법이 강력한 "autoproxying" 기능을 지원함에도)을 사용해서 관점을 설정한다. 이 부분이 다른 AOP 구현체와 결정적으로 다른 점이다. 아주 커다란 객체(보통 도메인 객체같은)를 어드바이하는 작업같은 것은 스프링 AOP로는 쉽게하거나 효율적으로 할 수 없다. 이러한 경우에는 AspectJ가 가장 좋은 선택이다. 하지만 AOP를 따라야 하는 엔터프라이즈 자바 어플리케이션의 대부분의 문제에 대해 스프링 AOP는 경험상 뛰어난 해결책을 제공한다.

광범위한 AOP 해결책을 제공하기 위해 스프링 AOP는 결코 AspectJ와 경쟁하려고 하지 않을 것이다. 스프링 AOP같은 프록시기반의 프레임워크와 AspectJ같은 충분히 발달한 프레임워크 둘다 가치가 있다고 생각하고 경쟁관계라기 보다는 서로 보완하는 관계라고 생각한다. 스프링 2.0은 일관된 스프링기반의 어플리케이션 아키텍쳐내에서 AOP의 모든 사용을 제공할 수 있도록 스프링 AOP와 IoC를 어렵지않게 AspectJ와 통합했다. 이 통합은 스프링 AOP API나 AOP Alliance API(하위 호환성을 위해서 남아있다.)에는 영향을 끼치지 않는다. 스프링 AOP API는 다음 장을 봐라.

Note
스프링 프레임워크의 핵심 신조중 하나는 비침투적인 특성이다. 이는 프레임워크에 특화된 클래스나 인터페이스를 비즈니스 모델이나 도메인모델에 추가하는 것을 강제하지 않는다는 것이다. 하지만 어떤 경우에는 스프링 프레임크가 스프링 프레임워크에 특화된 의존성을 기반코드에 추가하는 선택사항을 준다. 특정 시나리오에서는 이러한 방법으로 구현하는 것이 명백하게 코드를 더 읽기 쉽게 만들거나 구현하기 쉽게 만들기 때문에 이러한 선택사항을 주는 것은 합리적이다. 스프링 프레임워크는 (거의) 항상 선택권을 제공하므로 특정 유즈케이스나 시나리오에 가장 적합한 좋은 선택을 할 수 있는 자유를 가진다.

이번 장과 연관된 이러한 선택 중 하나는 어떤 AOP 프레인워크(혹은 어떤 AOP 방식)을 선택할 것인가 하는 것이다. AspectJ와 스프링 AOP 중에서 선택할 수 있고 @AspectJ 어노테이션방식의 접근이나 스프링 XML 설정방식의 접근 중에서도 선택할 수 있다. 이번 장에서 @AspectJ 방식의 접근을 먼저 선택했다는 것을 스프링 팀이 스프링 XML 설정방식보다 @AspectJ 어노테이션 방식의 접근을 더 좋아한다고 받아들여서는 안된다.

각 방식의 이유에 대한 자세한 내용은 Section 8.4, “사용할 AOP 선언방식 선택하기”를 봐라.


8.1.3 AOP 프록시
스프링 AOP는 기본적으노 AOP 프록시에 표준 J2SE 다이나믹 프록시를 사용한다. 이는 어떤 인터페이스(또는 인터페이스의 설정)도 프록시할 수 있게 한다.

스프링 AOP는 CGLIB 프록시를 사용할 수도 있다. CGLIB 프록시는 인터페이스보다는 클래스를 프록시하는데 필수적이다. 비즈니스 객체가 인터페이스를 구현하지 않았다면 기본적으로 CGLIB을 사용한다. 클래스보다는 인터페이스를 작성하는 것이 더 좋은 사용방법이므로 비즈니스 클래스는 보통 하나 이상의 비즈니스 인터페이스를 구현할 것이다. 인터페이스에 선언되지 않은 메서드를 어드바이즈해야 하거나 고정된(concrete) 타입인 것처럼 메서드에 프록시한 객체를 전달해야 하는 경우에(드문 경우이길 바란다) CGLIB을 사용하도록 강제할 수 있다.

스프링 AOP가 프록시 기반이라는 것은 중요하므로 꼭 이해해야 한다. 이 구현체의 세부사항들이 실제로 무엇을 의미하는 지에 대한 철저한 조사는 Section 8.6.1, “AOP 프록시 이해하기”를 봐라.


8.2 @AspectJ 지원
@AspectJ는 자바 5 어노테이션이 붙은 정규 자바 클래스처럼 관점을 선언하는 방식을 사용한다. @AspectJ 방식은 AspectJ 5 릴리즈의 일부인 AspectJ 프로젝트에서 도입되었다. 스프링 2.0은 포인트컷을 파싱하고 매칭하는데 AspectJ가 제공하는 라이브러리를 사용해서 AspectJ 5와 같은 어노테이션을 해석한다. 하지만 AOP 런타임은 여전히 순수 스프링 AOP이고 AspectJ 컴파일러나 위버(weaver)에 대한 의존성은 없다.

AspectJ 컴파일러와 위버(weaver)를 사용하면 전체 AspectJ 언어를 사용할 수 있으며 Section 8.8, “스프링 어플리케이션에서 AspectJ 사용하기”에서 설명한다.


8.2.1 @AspectJ 지원 활성화하기
스프링 설정에서 @AspectJ 관점을 사용하려면 @AspectJ 관점에 기반한 스프링 AOP 설정을 지원하도록 활성화해야 하고 autoproxying 빈들은 이러한 관점이 어드바이즈하는지 안하는 지에 따라 달라진다. autoproxying이 의미하는 바는 빈이 하나 이상의 관점으로 어드바이즈 되는 지를 스프링이 결정한다는 것이고 해당 빈의 메서드 호출을 가로채고 필요할 때 어드바이스를 실행할 수 있도록 자동으로 프록시를 생성할 것이다.

스프링 설정내에 다음 요소를 추가해서 @AspectJ 지원을 활성화한다.

<aop:aspectj-autoproxy/>

여기서는 Appendix C, XML Schema-based configuration에서 설명한 스키마 지원을 사용한다고 가정한다. aop 네임스페이스에서 태그를 어떻게 임포트하는 지에 대해서는 Section C.2.7, “The aop schema”를 봐라.

DTD를 사용하는 경우에도 어플리케이션 컨텍스트에 다음의 정의를 추가해서 @AspectJ 지원을 활성화할 수 있다.

<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" />

그리고 어플리케이션의 클래스패스에 AspectJ의 aspectjrt.jar 라이브러리가 필요하다. 이 라이브러리는 AspectJ 배포판의 'lib' 디렉토리에서나 메이븐 중앙저장소를 통해서 찾을 수 있다.


8.2.2 관점 선언
@AspectJ 지원이 활성화되면 어플리케이션 컨텍스트에서 @AspectJ 관점인 클래스 (@Aspect 어노테이션이 붙어있다.)로 정의된 모든 빈을 스프링이 자동으로 탐지할 것이고 스프링 AOP를 설정하는데 사용한다. 다음 예제는 아주 유용하지는 않은 관점에 대한 최소한의 필수 정의를 보여준다.

@Aspect 어노테이션이 붙은 빈 클래스를 가리키는 어플리케이션 컨텍스트의 정규 빈 정의.

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
  <!-- 평소처럼 관점의 프로퍼티를 여기에 설정한다 -->
</bean>

다음은 org.aspectj.lang.annotation.Aspect 어노테이션이 붙은 NotVeryUsefulAspect 클래스 정의다.

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {
}

관점(@Aspect 어노테이션이 붙은 클래스들)은 다른 클래스와 마찬가지로 메서드와 필드를 가질 것이다. 이 관점들은 포이트컷, 어드바이스, 인트로덕션(inter-type) 선언도 포함할 것이다.

컴포넌트 스캔으로 관점을 자동으로 탐지하기
스프링 XML 설정에서 다른 정규 빈들처럼 관점 클래스들을 등록하거나 클래스패스 스캐닝으로 관점 클래스들을 자동으로 탐지할 것이다. 이는 스프링이 관리하는 다른 빈들과 동일하다. 하지만 클래스패으세어 자동으로 탐지하기에 @Aspect 어노테이션은 충분하지 않다. 자동으로 탐지하기 위해서는 별도의 @Component 어노테이션을 추가해야 한다.(아니면 대신 스프링의 컴포넌트 스캐너의 규칙마다 제한을 하는 커스텀 스테레오타입 어노테이션을 추가해야 한다.)

관점을 다른 관점들과 함께 어드바이징하기?
스프링 AOP에서 관점 자체는 다른 관점의 어드바이스 타겟이 될 수 없다. 클래스에 붙은 @Aspect 어노테이션은 클래스가 관점이라는 것을 표시하므로 오토프록싱에서 제외된다.


8.2.3 포인트컷 선언
포인트컷을 호출하는 것은 관심(interest)의 조인포인터를 결정하므로 어드바이스를 실행할 때 제어할 수 있도록 한다. 스프링 AOP는 스프링 빈에 대한 메서드 실행 조인포인트만 지원하므로 포인트컷을 스프링 빈에서 메서드의 실행을 매칭하는 것으로 생각할 수 있다. 포인트컷 선언은 두 부분으로 나누어져 있다. 이름과 파라미터들로 구성된 시그니와 관심을 가지는 것이 정확히 어던 메서드 실행인지를 결정하는 포인트컷 표현식 두 부분이다. @AspectJ 어노테이션 방식의 AOP에서 포인트컷 시그니쳐는 정규 메서드 정의로 제공하고 포인트컷 표현식은 @Pointcut 어노테이션을 사용해서 나타낸다.(포인트컷 시그니처로 제공되는 메서드는 반드시 void 반환타입이 되어야 한다.)

다음 예제는 포인트컷 시그니처와 포인트컷 표현식을 명확하게 구별하는데 도움을 줄 것이다. 다음 에제는 'transfer'라는 이름의 메서드의 실행과 매칭될 'anyOldTransfer'라는 이름의 포인트컷을 정의한다.

@Pointcut("execution(* transfer(..))")// 포인트컷 표현식
private void anyOldTransfer() {}// 포인트컷 시그니처

@Pointcut 어노테이션의 값을 가진 포인트컷 표현식은 정식 AspectJ 5 포인트컷 표현식이다. AspectJ의 포인트컷 언어에 대한 전체 내용은 AspectJ Programming Guide (그리고 자바 5에 기반한 확장에 대한 AspectJ 5 Developers Notebook)이나 Colyer et. al.가 쓴 “Eclipse AspectJ”와 Ramnivas Laddad가 쓴 “AspectJ in Action” 같은 AspectJ 책들을 봐라.


8.2.3.1 지원되는 포인트컷 지정자(Designator)
스프링 AOP는 포인트컷 표현식에서 사용하는 다음의 AspectJ 포인트컷 지정자(AspectJ pointcut designators, PCD)를 지원한다.

  • execution - 실행 메서드 조인포인트를 위한 것으로 스프링 AOP를 사용할 때 주요 포인트컷 지정자이다.
  • within - 매칭할 조인포인트를 특정 타입으로 제한한다.(스프링 AOP를 사용할 때 매칭한 타입내에서 선언된 메서드의 실행이다.)
  • this - 매칭할 조인포인트(스프링 AOP를 사용하는 경우 메서드의 실행)를 빈 참조(스프링 AOP 프록시)가 전달한 타입의 인스턴스인 경우로 제한한다.
  • target - 매칭할 조인포인트(스프링 AOP를 사용하는 경우 메서드의 실행)를 대상객체(어플리케이션 객체는 프록시된다.)가 전달한 타입의 인스턴스 인 경우로 제한한다.
  • args - 매칭할 조인포인트(스프링 AOP를 사용하는 경우 메서드의 실행)를 아규먼트가 전달한 타입의 인스턴스인 경우로 제한한다.
  • @target - 매칭할 조인포인트(스프링 AOP를 사용하는 경우 메서드의 실행)를 실행하는 객체의 클래스가 전달한 타입의 어노테이션이 붙어있는 경우로 제한한다.
  • @args - 매칭할 조인포인트(스프링 AOP를 사용하는 경우 메서드의 실행)를 전달된 실제 아규먼트의 런타임 타입이 전달한 타입의 어노테이션이 붙어있는 경우로 제한한다.
  • @within - 매칭할 조인포인트를 전달한 어노테이션이 붙은 타입으로 제한한다. (스프링 AOP를 사용할 때 전달한 어노테이션이 붙은 타입에 선언된 메서드의 실행)
  • @annotation - 매칭할 조인포인트를 조인포인트의 주체(스프링 AOP에서 실행되는 메서드)가 전달한 어노테이션이 붙어있는 경우로 제한한다.
그 밖의 포인트컷 타입
완전한 AspectJ 포인트컷 언어는 스프링이 지원하지 않는 추가적인 포인트컷 지정자를 지원한다. 이러한 지정자에는 call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this, @withincode가 있다. 스프링 AOP가 해석하는 포인트컷 표현식에서 이러한 포인트컷 지정자를 사용하면 IllegalArgumentException가 던져질 것이다.

AspectJ 포인트컷 지정자를 더 지원하기 위해 스프링 AOP가 지원하는 포인트컷 지정자의 세트는 차후 버전에서는 늘어날 것이다.

스프링 AOP가 메서드 실행 조인포인트만 매칭하기 때문에 위의 포인트컷 지정자에 대한 설명은 AspectJ 프로그래밍 가이드보다는 더 좁은 정의를 하고 있다. 게다가 AspectJ 자체는 타입기반의 시맨틱이고 조인포인트를 실행할 때 'this'와 'target' 둘 다 같은 객체 즉 메서드를 실행하는 객체를 참조한다. 스프링 AOP는 프록시기반의 시스템이고 프록시 객체 자체와('this'로 바인딩된다) 프록시 뒤의 대상객체('target'로 바인딩된다)를 구별한다.

Note
스프링 AOP 프레임워크의 프록시에 기반한 특성으로 인해 프로텍티드(protected) 메서드는 JDK 프록시(적용가능하지도 않다.)와 CGLIB 프록시(기술적으로는 가능하지만 AOP 목적에 따라 추천하지 않는다.)로 당연히 인터셉트되지 않는다. 그 결과 주어진 포인트컷은 모두 퍼블릭 메서드에만 매칭될 것이다!

protected/private 메서드나 생성자를 인터셉트해야 한다면 스프링의 프록시기반 AOP 프레임워크 대신에 스프링주도(Spring-driven) 네이티브 AspectJ 위빙의 사용을 고려해 봐라. 이는 다른 특성을 가진 AOP 사용방법에 대한 다른 모드로 구성되어 있으므로 선택을 하지 전에 먼저 위빙에 익숙한지 생각해봐라.

스프링 AOP는 'bean'이라는 이름의 추가적인 PCD도 지원한다. 이 PCD로 매칭할 조인포인트를 특정 이름의 스프링 빈이나 빈의 세트(와일드카드 사용시)로 제한할 수 있다. 'bean' PCD는 다음의 형식을 가진다.

bean(idOrNameOfBean)

'idOrNameOfBean' 토큰은 어떤 스프링 빈의 이름도 될 수 있다. '*' 문자로 와일드카드를 사용한 제한도 제공하므로 스프링 빈이 어떤 네이밍 관례를 따르고 있다면 아주 쉽게 필요한 빈을 선택하는 'bean' PCD 표현식을 작성할 수 있다. 다른 포인트컷 지정자를 사용하는 경우 'bean' PCD는 &&, ||, !(부정)도 될 수 있다.

Note
'bean' PCD는 스프링 AOP에서만 지원되고 네이티브 AspectJ 위빙에서는 지원하지 않는다. 이는 AspectJ가 정의한 표준 PCD에 대한 스프링에 한정된 확장이다.

'bean' PCD는 타입 수준에서만이(어떤 위빙기반의 AOP가 제한하는 지) 아니라 인스턴스 수준(스프링 빈 이름 개념을 이루는)에서 수행된다. 인스턴스 기반의 포인트컷 지정자는 스프링 빈 팩토리와 닫힌 통합을 하는 스프링의 프록시기반 AOP 프레임워크의 특별한 기능이고 이름으로 특정 빈을 식별하는 것이 자연스럽고 직관적이다.


8.2.3.2 포인트컷 표현식 결합하기
'&&', '||', '!'를 사용해서 포인트컷 표현식을 결합할 수 있고 이름으로 포인트컷 표현식을 참조하는 것도 가능한다. 다음 예제는 3가지 포인트컷 표현식을 보여준다. anyPublicOperation (메서드 실행 조인포인트가 어느 퍼블릭 메서드의 실행을 나타나는 지 매칭한다.), inTrading (trading 모듈에 있는 메서드 실행을 매칭한다.), tradingOperation (메서드 실행이 trading 모듈의 어느 퍼블릭 메서드를 나타내는지 매칭한다.)

@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
    
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
    
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}

위에서 보여줬듯이 이름있고 작은 컴포넌트들로 더 복잡한 포인트컷 표현식을 작성하는 것이 가장 좋은 사용방법이다. 이름으로 포인트컷을 참조하는 경우 일반적인 자바의 가시성 규칙이 적용된다. (같은 타임에서 프라이빗 포인트컷을, 계층에서는 프로텍티드 포잇트컷을 볼 수 있고 퍼블릭 포인트컷은 어디서나 볼 수 있다.) 가시성은 포잇트컷 매칭에는 영향을 주지 않는다.


8.2.3.3 공통 포잇트컷 정의 공유하기
엔터프라이즈 어플리케이션을 개발할 때는 때로 여러 관점에서 어플리케이션의 모듈을 참조하거나 작업의 특정 세트를 참조할 필요가 있다. 이러한 경우에는 공통 포인트컷 표현식을 가진 "SystemArchitecture" 관점을 정의하는 것을 추천한다. 이러한 관점은 일반적으로 다음과 같이 생겼다.

package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

  /**
   * 메서드가 com.xyz.someapp.web 패키지나 그 하위 패키지에 있는 
   * 타입에서 정의되었다면 조인포인트는 웹계층에 있다.
   */
  @Pointcut("within(com.xyz.someapp.web..*)")
  public void inWebLayer() {}

  /**
   * 메서드가 com.xyz.someapp.service 패키지나
   * 그 하위 패키지에 있는 타입에서 정의되었다면 
   * 조인포인트는 서비스계층에 있다.
   */
  @Pointcut("within(com.xyz.someapp.service..*)")
  public void inServiceLayer() {}

  /**
   * 메서드가 com.xyz.someapp.dao 패키지나
   * 그 하위 패키지에 있는 타입에서 정의되었다면 
   * 조인포인트는 데이터계층에 있다.
   */
  @Pointcut("within(com.xyz.someapp.dao..*)")
  public void inDataAccessLayer() {}

  /**
   * 비즈니스 서비스는 서비스 인터페이스에 정의된 어떤 메서드의 실행이다.
   * 이 정의는 인터페이스가 "service" 패키지에 있고 그 구현체 타입은 하위 패키지에 있다고 
   * 가정한다.
   * 
   * 서비스 인터페이스를 기능적인 영역으로 구분지었다면(예를 들어
   * com.xyz.someapp.abc.service나 com.xyz.def.service패키지)
   * 대신 "execution(* com.xyz.someapp..service.*.*(..))"
   * 포인트컷 표현식을 사용한다.
   *
   * 그 대신 "bean(*Service)"같은 'bean' PCD를
   * 사용한 표현식을 작성할 수 있다.(이는 스프링의 서비스 빈에 일관적인 방법으로 이름을 지었다고
   * 가정한다.)
   */
  @Pointcut("execution(* com.xyz.someapp.service.*.*(..))")
  public void businessService() {}
  
  /**
   * 데이터 접근작업은 dao 인터페이스에 정의된 어떤 메서드의 실행이다.
   * 이 정의는 인터페이스가 "dao" 패키지에 있고 그 구현체 타입은
   * 하위 패키지에 있다고 가정한다.
   */
  @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
  public void dataAccessOperation() {}
}

이러한 a9n 관점에서 정의한 포인트컷은 포인트컷 표현식이 필요한 어디서도 참조할 수 있다. 예를 들어 서비스계층이 트랙잭션이 되도록 하려면 다음과 같이 작성할 수 있다.

<aop:config>
  <aop:advisor 
      pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
      advice-ref="tx-advice"/>
</aop:config>

<tx:advice id="tx-advice">
  <tx:attributes>
    <tx:method name="*" propagation="REQUIRED"/>
  </tx:attributes>
</tx:advice>

<aop:config>요소와 <aop:advisor>요소는 Section 8.3, “스키마 기반의 AOP 지원”에서 설명한다. 트랜잭션 요소는 Chapter 11, Transaction Management에서 설명한다.


8.2.3.4 예제
스프링 AOP 사용자들은 execution 포인트컷 지정자를 가장 많이 사용할 것이다. execution 표현식의 형식은 다음과 같다.

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)
          throws-pattern?)

반환하는 타입 패턴(위의 예제에서 ret-type-pattern), 이름 패턴, 파라미터 패턴을 제외한 모든 부분은 선택사항이다. 반환하는 타입 패턴은 일치하는 조인포인트를 찾기 위해 메서드가 어떤 타입을 반환해야 하는지를 결정한다. 반환하는 타입 패턴에 어떤 반환 타입도 매칭할 수 있도록 *를 가장 자주 사용할 것이다. 정규화된 타입 이름은 메서드가 주어진 타입을 반환할 때만 매칭될 것이다. 이름 패턴은 메서드의 이름을 매칭한다. 이름 패턴에에서 전체나 일부분에 * 와일드카드를 사용할 수 있다. 파라미터 패턴은 약간 더 복잡하다. ()는 파라미터가 없는 메서드와 매칭되지만 (..)는 파라미터의 갯수와 상관없이(0 또는 그 이상) 매칭한다. (*) 패턴은 어떤 타입이든 하나의 파라미터를 받는 메서드와 매칭되고 (*,String)는 2개의 파라미터를 받지만 첫 파라미터는 타입이 상관없고 두번째 파라미터는 String이어야 하는 메서드와 매칭된다. 더 자세한 내용은 AspectJ Programming Guide의 Language Semantics 섹션을 참고해라.

다음은 일반적인 포인트컷 표현식의 몇가지 예제이다.

  • 모든 퍼블릭 메서드의 실행
    
    execution(public * *(..))
    
  • 이름이 "set"로 시작하는 모든 메서드의 실행
    
    execution(* set*(..))
    
  • AccountService 인터페이스가 정의한 모든 메서드의 실행
    
    execution(* com.xyz.service.AccountService.*(..))
    
  • service 패키지에서 정의된 모든 메서드의 실행
    
    execution(* com.xyz.service.*.*(..))
    
  • service 패키지나 그 하위 패키지에서 정의한 모든 메서드의 실행
    
    execution(* com.xyz.service..*.*(..))
    
  • service 패키지내의 모든 조인포인트(스프링 AOP에서는 메서드 실행만 가능)
    
    within(com.xyz.service.*)
    
  • service 패키지나 그 하위 패키지내의 모든 조인포인트(스프링 AOP에서는 메서드 실행만 가능)
    
    within(com.xyz.service..*)
    
  • 프록시가 AccountService 인터페이스를 구현한 모든 조인포인트(스프링 AOP에서는 메서드 실행만 가능)
    
    this(com.xyz.service.AccountService)
    

    바인딩 형식에서는 'this' 를 더 일반적으로 사용한다. 어드바이스 바디에서 어떻게 프록시 객체를 사용가능하도록 만드는지는 어드바이스 부분에 나오는 섹션을 봐라.
  • 대상 객체가 AccountService 인터페이스를 구현한 모든 조인포인트(스프링 AOP에서는 메서드 실행만 가능)
    
    target(com.xyz.service.AccountService)
    

    바인딩 형식에서는 'target'을 더 일반적으로 사용한다. 어드바이스 바디에서 어떻게 대상 객체를 사용가능하도록 만드는 지는 어드바이스 부분에 나오는 섹션을 봐라.
  • 하나의 파라미터를 받고 런타임시에 전달된 아규먼트가 Serializable인 모든 조인포인트(스프링 AOP에서는 메서드 실행만 가능)
    
    args(java.io.Serializable)
    
    바인딩 형식에서는 'args'을 더 일반적으로 사용한다. 어드바이스 바디에서 어떻게 메서드 아규먼트를 사용가능하도록 만드는 지는 어드바이스 부분에 나오는 섹션을 봐라.

    이 예제에서 주어진 포인트컷은 execution(* *(java.io.Serializable))와는 다르다. args 방식은 런타임시에 전달된 아규먼트가 Serializable하는 경우 매칭되고 execution 방식은 메서드 시그니처가 Serializable 타입의 단일 파라미터를 선언한 경우에 매칭된다.
  • 대상 객체에 @Transactional 어노테이션이 붙어있는 모든 조인포인트(스프링 AOP에서는 메서드 실행만 가능)
    
    @target(org.springframework.transaction.annotation.Transactional)
    

    바인딩 형식에서는 '@target'도 사용할 수 있다. 어드바이스 바디에서 어떻게 어노테이션 객체를 사용할 수 있게 만드는지는 어드바이스 부분에 나오는 섹션을 봐라.
  • 대상 객체에 선언된 타입에 @Transactional 어노테이션이 붙어있는 모든 조인포인트(스프링 AOP에서는 메서드 실행만 가능)
    
    @within(org.springframework.transaction.annotation.Transactional)
    

    바인딩 형식에서는 '@within'도 사용할 수 있다. 어드바이스 바디에서 어떻게 어노테이션 객체를 사용할 수 있게 만드는지는 어드바이스 부분에 나오는 섹션을 봐라.
  • 실행하는 메서드에 @Transactional 어노테이션이 붙은 모든 조인포인트(스프링 AOP에서는 메서드 실행만 가능)
    
    @annotation(org.springframework.transaction.annotation.Transactional)
    

    바인딩 형식에서는 '@annotation'도 사용할 수 있다. 어드바이스 바디에서 어떻게 어노테이션 객체를 사용할 수 있게 만드는지는 어드바이스 부분에 나오는 섹션을 봐라.
  • 하나의 파라미터만 받고 전달된 아규먼트의 런타임 타입에 @Classified 어노테이션이 붙어있는 모든 조인포인트(스프링 AOP에서는 메서드 실행만 가능)
    
    @args(com.xyz.security.Classified)
    

    바인딩 형식에서는 '@args'도 사용할 수 있다.어드바이스 바디에서 어떻게 어노테이션 객체를 사용할 수 있게 만드는지는 어드바이스 부분에 나오는 섹션을 봐라.
  • 'tradeService'이라는 이름의 스프링 빈에 대한 모든 조인포인트(스프링 AOP에서는 메서드 실행만 가능)
    
    bean(tradeService)
    
  • 와일드카드 표현식 '*Service'와 일치하는 이름의 스프링 빈에 대한 모든 조인포인트(스프링 AOP에서는 메서드 실행만 가능)
    
    bean(*Service)
    


8.2.3.5 좋은 포인트컷 작성하기
AspectJ는 매칭 성능을 최적화하기 위해 컴파일 중에 포인트컷을 처리한다. 코드를 검사하고 각 조인포인트가 주어진 포인트컷과 일치하는지(정적으로나 동적으로나) 결정하는 것은 비용이 많이 드는 작업이다.(동적 매칭은 정적 분석으로는 매칭을 완전히 결정할 수 없어서 코드가 실행될 때 실제로 매칭되는지 결정하는 테스트를 코드에 둔다.) 첫 포인트컷 선언을 만났을 때 AspectJ는 매칭 프로세스에 최적인 형식으로 포인트컷을 재작성 할 것이다. 이것이 무엇을 의미하는가? 기본적으로 포인트컷은 DNF (Disjunctive Normal Form)에서 재작성되고 포인트컷의 컴포넌트들이 정렬되어 있기 때문에 평가하는데 비용이 적게 드는 컴포넌트를 먼저 확인할 것이다. 이는 다양한 포인트컷 지정자의 성능을 이해해야 하는지 걱정하지 않아도 되고 포인트컷 선언에서 아무 순서로나 컴포넌트들을 제공할 것이다.

하지만 AspectJ는 AspectJ가 지정해 준 것과만 동작할 수 있으므로 최적의 매칭 성능을 위해서는 정의할 때 가능한한 매치에 대한 검색 영역을 획득하고 한정하도록 고민해 봐야한다. 존재하는 지정자들은 자연스럽게 세가지 그룹으로 나누어진다. 종류(kinded), 범위(scoping), 컨텍스트(context) 세가지 그룹니다.

  • 종류 지정자(Kinded designator)는 조인포인트의 특수한 종류를 선택한다. 예를 들면 execution, get, set, call, handler 이다.
  • 범위 지정자(Scoping designator)는 관심있는 조인포인트의 그룹을(아마도 많은 종류의) 선택한다. 예를 들면 within, withincode 이다.
  • 컨텍스트 지정자(Contextual designator)는 컨텍스트에 기반해서 매칭한다.(그리고 선택적으로 바인딩한다.) 예를 들어 this, target, @annotation 이다.
잘 작성된 포인트컷은 최소한 앞의 두 타입(종류와 범위)에 속해야하고 컨텍스트 지정자는 조인포인트 컨텍스트에 기반해서 매칭해야 하거나 어드바에스에서 사용할 목적으로 컨텍스트를 바인딩하는 경우에 사용한다. 그냥 종류 지정자를 제공하거나 컨텍스트 지정자를 제공하는 것 모두 잘 동작할 것이지만 여분의 모든 처리와 분석때문에 위빙 성능(사용되는 시간과 메모리)에 영향을 줄 것이다. 범위 지정자는 매칭이 아주 빠르고 범위 지정자를 사용한다는 것은 AspectJ가 더 처리되지 않아도 되는 조인포인트의 그룹을 아주 빠르게 제거할 수 있다는 의미이다. 이것이 좋은 포인트컷은 가능한한 항상 하나만 포함하는가 하는 이유이다.


8.2.4 어드바이스 선언
어드바이스는 포인트컷 표현식과 연결되고 포인트컷과 채킹된 before, after, around 메서드 실행을 수행한다. 포인트컷 표현식은 이름있는 포인트컷에 대한 간단한 참조나 적절한 위치에 선언된 포인트컷 표현식이 모두 될 수 있다.


8.2.4.1 Before advice
Before advice는 @Before 어노테이션을 사용해서 관점에서 선언한다.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

  @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
  public void doAccessCheck() {
    // ...
  }
}

인플레이스(in-place) 포인트컷 표현식을 사용해서 위의 예제를 다음과 같이 재작성할 수 있다.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

  @Before("execution(* com.xyz.myapp.dao.*.*(..))")
  public void doAccessCheck() {
    // ...
  }
}


8.2.4.2 After returning advice
매칭된 메서드 실행이 정상적으로 반환했을 때 After returning advice가 실행된다. 이는 @AfterReturning 어노테이션을 사용해서 정의한다.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

  @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
  public void doAccessCheck() {
    // ...
  }
}

Note: 다른 멤버들와 마찬가지로 여러 어드바이스를 같은 관점내에 선언할 수 있다. 예제에서는 말하고자 하는 내용에 집중하기 위해서 하나의 어드바이스 선언만 보여준다.

때로는 어드바이스 바디에서 반환되는 실제 값에 접근해야 할 필요가 있다. 이러한 경우 @AfterReturning의 형식을 사용해서 반환 값에 바인딩할 수 있다.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

  @AfterReturning(
    pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
    returning="retVal")
  public void doAccessCheck(Object retVal) {
    // ...
  } 
}

returning 속성에서 사용한 이름은 반드시 어드바이스 메서드의 파라미터 이름과 일치해야 한다. 메서드 실행이 반환되었을 때 반환값은 어드바이스 메서드의 대응되는 아규먼트의 값으로 전달될 것이다. returning 절도 지정한 타입의 값을 반환하는 메서드 실행만으로 매칭을 제한한다.(이 경우에 Object는 모든 반환값과 매치될 것이다.)

after-returning advice를 사용하는 경우 완전히 다른 참조를 반환할 가능성은 없다.


8.2.4.3 After throwing advice
매치된 메서드 실행이 예외를 던지고 종료되었을 때 After throwing advice가 실행된다. 이는 @AfterThrowing 어노테이션을 사용해서 선언한다.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

  @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
  public void doRecoveryActions() {
    // ...
  }
}

때로는 주어진 타입의 예외가 던져졌을 때만 어드바이스를 실행하거나 어드바이스 바이에서 던져진 예외에 접근해야할 필요가 있다. 매칭을 제한하고(원한다면 대신 예외타입으로 Throwable를 사용해라.) 어드바이스 파라미터로 던져진 예외를 바인딩 하는데 모두 throwing 속성을 사용해라.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

  @AfterThrowing(
    pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
    throwing="ex")
  public void doRecoveryActions(DataAccessException ex) {
    // ...
  }
}

throwing 속성에서 사용한 이름은 반드시 어드바이스 메서드의 파라미터 이름과 같아야 한다. 메서드 실행이 예외를 던지고 종료되었을 때 예외는 어드바이스 메서드의 대응되는 아규먼트의 값으로 전달될 것이다. throwing 절도 지정한 타입의 예외를 던지는 메서드 실행만으로 매칭을 제한한다.(이 경우에는 DataAccessException)


8.2.4.4 After (finally) advice
매치된 메서드실행이 종료되었을 때 After (finally) advice가 실행된다. 이는 @After 어노테이션을 사용해서 선언한다. After advice는 반드시 정상적인 상황과 예외가 발생한 상황을 모두 처리할수 있도록 준비해야 한다. 보통 리소스를 해제하는 등에 사용한다.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

  @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
  public void doReleaseLock() {
    // ...
  }
}


8.2.4.5 Around advice
마지막으로 얘기할 어드바이스는 around advice다. 매치된 메서드 실행 "주위에서" Around advice가 실행된다. Around advice는 메서드 실행의 전과 후에 모두 작업을 할 수 있고 언제, 어떻게, 상황에 따라 결정하기 위해서 메서드는 실제로 실행할 모든 것을 획득한다. 쓰레드 세이프한 방법으로 메서드를 실행하는 전후로 상태를 공유해야할 때(예를 들면 타이머를 시작하고 종료하는 작업) Around advice를 사용하기도 한다. 항상 요구사항을 충족하는 어드바이스 중에 가장 덜 강력한 것을 사용해라.(예를 들면 before advice가 간단히 할 수 있는 경우에 around advice를 사용하지 마라.)

@Around 어노테이션을 사용해서 Around advice를 선언한다. 어드바이스 메서드의 첫 파라미터는 반드시 ProceedingJoinPoint 타입이어야 한다. 어드바이스 바디내에서 ProceedingJoinPoint의 proceed()를 호출하면 기반하는 메서드가 실행된다. proceed 메서드에는 Object[]가 전달되면서 실행될 것이다. 배열내의 값들은 진행되면서 메서드 실행의 아규먼트로 사용될 것이다.

proceed가 Object[]로 호출되었을 때의 동작은 AspectJ가 컴파일러가 컴파일한 around advice에 대한 proceed의 동작과는 약간 다르다. 전통적인 AspectJ 언어를 사용해서 작성한 around advice에서 proceed에 전달된 아규먼트의 수는 around advice에 전달된 아규먼트의 수와 일치해야 하고(기반하는 조인포인트가 취한 아규먼트의 수가 아니다.) 주어진 아규먼트위 위치에서 proceed에 전달된 값들은 값이 바인딩되는 엔티티를 위해서 조인포인트의 원래 값을 대체한다.(지금은 잘 이해되지 않아도 걱정하지 마라.) 스프링이 취한 접근은 의미론적으로만 실행하는 프록시기반의 특성에 맞게 더 간단하고 좋다. 스프링에 작성된 @AspectJ 관점을 컴파일때와 AspectJ 컴파일러와 위버를 가진 아규먼트로 proceed를 사용할 때의 이러한 차이점이 알아야할 전부이다. 스프링 AOP와 AspectJ에서 100% 호환성이 있는 관점을 작성하는 방법이 있는데 이는 어드바이스 파리미터에 나온 섹션에서 이야기한다.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

  @Around("com.xyz.myapp.SystemArchitecture.businessService()")
  public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // 스톱워치를 시작한다
    Object retVal = pjp.proceed();
    // 스톱워치를 멈춘다
    return retVal;
  }
}

around advice가 반환한 값은 메서드 호출자(caller)가 받는 반환값이 될 것이다. 예를 들어 관점을 캐싱하면 캐싱된 값이 있는 경우 캐시로부터 값을 돌려받고 캐싱된 값이 없을 경우 proceed()를 호출한다. around advice의 바디내에서 proceed를 한번 호출하거나 여러번 호출할 수 있고 전혀 호출하지 않을수도 있다.
2012/09/30 04:42 2012/09/30 04:42