주요글: 도커 시작하기
반응형
Spring 2.0에 새롭게 추가된 AOP 관련 태그와 새로운 트랜잭션 설정 관련 태그에 대해서 살펴본다.

사용하기 쉬워진 AOP 적용

Spring 2.0은 많은 면에서 발전을 이루었는데 그 중 한가지가 AOP를 좀 더 명확하게 적용할 수 있게 되었다는 점이다. Spring 2.0은 기존의 <bean> 태그 이외에 콘텍스트에 알맞은 태그를 확장할 수 있도록 설계되었으며, AOP를 위한 태그도 새롭게 추가되었다. 본 글에서는 Spring 2.0에 새롭게 추가된 태그를 사용하여 AOP를 적용하는 방법에 대해서 살펴보고, 추가적으로 새로운 트랜잭션 설정 방법에 대해서도 살펴볼 것이다.

본 글을 읽기 전에, 아직까지 AOP와 관련된 용어에 익숙하지 않은 개발자들을 위해 AOP와 관련된 몇 가지 용어를 간단하게 정리해보았다.

  • Pointcut : pointcut은 매칭 규칙으로서, 프로그램이 실행될 때 aspect를 적용할 지점(point)의 집합을 정의한다. aspect가 적용되는 지점을 joinpoint 라고 부른다. 예를 들어, 메소드 실행, 처리해야 할 예외와 같은 것이 joinpoint가 될 수 있다. Spring AOP의 경우 public 메소드에 대해서만 joinpoint를 지정할 수 있다.
  • Advice : joinpoint에 aspect를 언제 적용할지를 나타낸다. Spring AOP의 경우 Before advice, After returning advice, After throwing advice, After advice, Around advice의 5가지의 advice가 존재한다.
  • Aspect : 다양한 객체에서 요구하는 소프트웨어의 기능을 구현한 모듈. 로깅, 트랜잭션 처리 등이 aspect의 좋은 예이다. Spring AOP는 Aspect를 일반적인 자바 클래스로 구현한다.
Spring 2.0은 하위 호환을 위해 기존 Spring 1.x 버전이 제공하던 AOP 기능을 유지하면서, "aop" 네임스페이스 태그를 사용한 AOP 설정 방식을 추가하였다. 새롭게 추가된 태그를 사용하여 보다 쉽게 Aspect를 적용할 수 있게 되었는데 본 절에서는 기존 1.x 방식의 단점에 대해서 살펴본 뒤, 새로운 AOP 설정 방식을 살펴볼 것이다.

Spring 1.x의 AOP 설정

Spring 1.x 버전의 경우 Aspect를 적용하기 위해서 다음과 같은 형태의 코드를 사용했었다. 이 코드를 보면 AOP를 적용하기 위해 프록시 빈을 정의하고(ProxyFactoryBean), Advice와 Aspect가 혼용된 WelcomeAdvice를 정의한 것을 알 수 있다.

    <bean id="kwikEMartTarget"
        class="com.springinaction.chapter03.store.AquKwikEMart" />
    
    <bean id="welcomeAdvice"
        class="com.springinaction.chapter03.store.WelcomeAdvice" />
    
    <bean id="kwikEMart"
        class="com.springframework.aop.framework.ProxyFactoryBean">
        <property name="proxyInterfaces">
            <value>com.springinaction.chapter03.store.KwikEMart</value>
        </property>
        <property name="interceptorNames">
            <list>
                <value>welcomeAdvice</value>
            </list>
        </property>
        <property name="target">
            <ref bean="kwikEMartTarget" />
        </property>
    </bean>

외부에서 Aspect가 적용될 객체에 접근할 때 사용되는 빈의 이름은 프록시 객체의 이름이어야 한다. (위 코드의 경우 AquKwikEMart 객체에 Aspect를 적용하기 위해서는 "kwikEMartTarget"이 아닌 "kwikEMart" 이름으로 접근해야만 AOP가 적용된다.)

Aspect를 적용하고 싶지 않다면 프록시 빈의 설정을 변경하거나 Advice를 목록에서 제거하는 등의 방법을 사용해야 하는 불편함이 있다. 위와 같은 코드의 가장 큰 문제점은 AOP를 위한 설정 코드가 꽤나 복잡하다는 점이다. 또 한, 설정 파일만으로는 WelcomeAdvice의 타입을 알 수 없다. WelcomeAdvice가 Before 타입인지 After 타입인지 또는 Around 타입인지의 여부는 WelcomeAdvice의 소스 코드나 API 문서를 봐야만 확인이 가능하다.

Spring 2.0의 AOP 설정

Spring 2.0에는 AOP 설정하기 위한 "aop" 네임스페이스가 추가되었으며, 이 네임스페이스와 관련된 XML 스키마도 함께 추가하였다. "aop" 네임스페이스와 관련 XML 스키마는 다음과 같이 <beans> 태그에 명시할 수 있다.

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans   
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">
    ...
</beans>

위와 같이 "aop" 네임스페이스와 관련된 XML 스키마를 지정한 뒤에, 다음과 같이 <aop:config> 태그를 사용하여 AOP 관련 정보를 설정할 수 있다.

    <bean id="logging"
        class="net.daum.cto.ts.techReport.report0.LoggingAspect">
    </bean>

    <aop:config>
        <aop:aspect id="loggingAspect" ref="logging">
            <aop:pointcut id="loggingPointCut"
                expression="execution(* net.daum.cto.ts.techReport.*.*Service*.*(..))" />
            <aop:around pointcut-ref="loggingPointCut" method="log" />
        </aop:aspect>
    </aop:config>
    <bean id="reservationService"
        class="net.daum.cto.ts.techReport.report0.ReservationServiceImpl">
    </bean>

위 코드에서 <aop:*>에 해당하는 태그는 다음과 같은 정보를 설정한다.

  • <aop:config> - AOP 설정 정보임을 나타낸다.
  • <aop:aspect> - Aspect를 설정한다.
  • <aop:pointcut> - Pointcut을 설정한다.
  • <aop:around> - Around Advice를 설정한다. 이 외에도 다양한 Advice를 설정할 수 있다.
위 코드에서 <aop:aspect> 태그의 ref 속성은 Aspect로서 기능을 제공할 빈을 설정할 때 사용되며, 위 코드의 경우 "logging" 빈이 Aspect 기능을 제공한다고 명시하고 있다.

위 코드에서 주목할 점은 Aspect를 적용하기 위해 더이상 프록시 객체를 사용할 필요가 없다는 점이다. 앞서 Spring 1.x 버전의 경우 Aspect를 적용하기 위해 프록시 객체를 사용했던 반면에, Spring 2.0에서는 <aop:pointcut>의 expression 속성에 Aspect가 적용될 클래스 또는 메소드에 대한 정보를 지정하기만 하면 자동으로 Aspect가 해당 메소드에 적용된다.

위 코드를 보면 "loggingAspect"가 ReservationServiceImpl의 모든 메소드에 Around Advice로 적용되며, 이때 Aspect의 구현 클래스인 LoggingAspect의 log() 메소드가 호출된다는 것을 쉽게 유추해낼 수 있는데, Spring 2.0의 AOP 설정은 이렇게 설정 파일만 보더라도 어렵지 않게 어떤 코드에 어떤 Aspect가 어떤 타입의 Advice로 적용되는지를 파악해낼 수 있다.

Spring 2.0의 AOP 설정하기

Aspect 설정

Srping 2.0에서 Aspect를 선언하려면 다음 코드처럼 <aop:aspect> 태그를 사용하면 된다.

<aop:config>
    <aop:aspect id="someConcernAspect" ref="someConcern">
        ...
    </aop:aspect>
</aop:config>

<bean id="someConcern" class="...SomeConcernImpl">
   ...
</bean>

<aop:aspect> 태그의 ref 속성은 Aspect의 구현을 제공하는 빈을 지정한다. 위 코드의 경우 "someConcern" 빈이 "someConcernAspect"의 구현을 제공하게 된다.

Spring 2.0에서 <aop:> 태그는 내부적으로 AspectJ에서 제공하는 기능을 사용한다. 따라서, <aop:> 태그를 사용하려면 AspectJ와 관련된 jar 파일을 클래스패스에 추가해주어야 한다. Maven을 사용할 경우 다음과 같은 <dependency> 태그를 추가해주면 된다.

    <dependency>
        <groupId>aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.5.3</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.5.3</version>
        <scope>runtime</scope>
    </dependency>

Spring 2.0의 Aspect 구현하기

Spring 1.x 버전의 경우 Aspect로서 동작하기 위해서는 MethodBeforeAdvice 인터페이스와 같은 AOP를 위한 인터페이스를 구현했어야 하는 반면에, Spring 2.0에서 Aspect로서 사용될 클래스는 특정한 인터페이스를 구현하지 않아도 된다. 즉, 일반 자바 클래스를 Aspect로 사용할 수 있는 것이다. 예를 들어, 아래 클래스는 Spring 2.0에서 Around Advice로 적용 가능한 Aspect 클래스의 예이다.

package net.daum.cto.ts.techReport.report0;

import org.aspectj.lang.ProceedingJoinPoint;

public class LoggingAspect {
    public Object log(ProceedingJoinPoint call) throws Throwable {
        System.out.println("from LoggingAspect : entering method : "
                + call.toShortString());
        Object point = call.proceed();
        System.out.println("from LoggingAspect : exiting method : "
                + call.toShortString());
        return point;
    }
}

위 코드를 보면 Aspect로 동작하기 위해 특정한 인터페이스를 필요로 하지 않는 것을 알 수 있다. 단지, Advice 타입에 따라 Aspect가 적용될 때 호출될 메소드의 파라미터를 알맞게 설정해주면 된다.

Pointcut 설정하기

ointcut을 설정할 때에는 다음과 같이 <aop:pointcut> 태그를 사용하면 된다.

<aop:aspect id="loggingAspect" ref="logging">
    <aop:pointcut id="loggingAspect"
        expression="execution(* net.daum.cto.ts.techReport.*.*Service*.*(..))" />
    ...
</aop:aspect>

<aop:pointcut>의 expression 속성은 pointcut에 포함될 joinpoint를 표현한다. expression 속성은 AspectJ의 문법을 따르고 있다.

<aop:pointcut>의 expression

<aop:pointcut> 태그의 expression은 pointcut을 정의할 때 사용되며 execution과 같은 designator를 사용하여 pointcut을 명시하게 된다. execution 외에도 within, this, target 등의 designator가 존재한다.

  • execution : 메소드 실행 joinpoint와 매칭할 때 사용된다.
  • within : 특정 패키지에 있는 모든 타입의 joinpoint와 매칭할 때 사용된다.
  • this : 특정 타입을 구현하고 있는 프록시의 joinpoint와 매칭할 때 사용된다.
  • target : 특정 타입의 인스턴스의 joinpoint와 매칭할 때 사용된다.
  • args : 메소드의 인자 타입이 일치하는 joinpoint와 매칭할 때 사용된다.
이 외에도 @target, @args 등의 designator가 존재하는데, 이들 designator는 Annotation을 사용할 때 적용할 수 있는 것으로서, 본 글에서는 이에 대해서 설명하지 않겠다. 이에 대한 자세한 내용이 알고 싶다면 Spring 레퍼런스 문서를 참고하기 바란다.

위에서 설명한 designator 중에서 가장 많이 사용되는 것은 execution 이다. execution 은 다음과 같은 형식을 사용하여 pointcut을 정의할 수 있다.

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

execution의 몇 가지 예를 들면 다음과 같다.

  • 모든 public 메소드의 실행: execution(public * *(..))
  • set으로 시작하는 모든 메소드의 실행: execution(* set*(..))
  • AccountService 인터페이스에 정의된 모든 메소드의 실행: execution(* com.xyz.service.AccountService.*(..))
execution 외에 나머지 designator의 설정 예는 다음과 같다.

  • com.xyz.service 패키지에 있는 모든 joinpoint: within(com.xyz.service.*)
  • com.xyz.service 패키지 및 그 하위 패키지에 있는 모든 joinpoint: within(com.xyz.service..*)
  • AccountService 인터페이스를 구현한 프록시의 모든 joinpoint: this(com.xyz.service.AccountService)
  • AccountService 인터페이스를 구현한 객체의 joinpoint: target(com.xyz.service.AccountService)
  • args(java.io.Serializable): 한 개의 파라미터를 갖고 그 파라미터의 타입이 Serializable인 메소드 호출
Advice 선언하기

Aspect와 Pointcut을 설정했다면 다음으로 할 작업은 Advice를 설정하는 것이다. Spring 2.0은 다음과 같이 5개의 Advice를 제공하고 있다.

  • Before Advice - 메소드가 호출되기 이전에 실행된다.
  • After Returning Advice - 메소드가 정상적으로 리턴된 다음에 실행된다.
  • After Throwing Advice - 메소드가 예외를 발생할 경우 실행된다.
  • After Advice - 메소드가 정상적으로 실행되었는지의 여부에 상관없이 메소드의 실행이 종료된 다음에 실행된다. finally와 동일한 기능을 제공한다.
  • Around Advice - 메소드의 호출 이전과 이후에 Aspect를 적용할 수 있다.
Before Advice

Before Advice를 설정할 때에는 <aop:before> 태그를 사용하면 된다.

<bean id="securityChecker"
    class="net.daum.cto.ts.techReport.report0.SecurityChecker">
</bean>

<aop:aspect id="securityCheckAspect" ref="securityChecker">
    <aop:pointcut id="needAuthMethod" ... />
    <aop:before 
         pointcut-ref="needAuthMethod"
         method="checkAuth" />
</aop:aspect>

<aop:before>에 적용될 메소드는 다음과 같이 두 가지 형태로 구현할 수 있다.

public void checkAuth(ProceedingJoinPoint call) {
    ...
}

public void checkAuth() {
    ...
}

첫번째 메소드는 ProceedingJoinPoint 클래스를 파라미터로 전달받는데, 이 객체에는 호출되는 메소드의 정보가 저장되어 있다. 따라서 호출되는 메소드에 대한 정보가 필요한 경우 첫번째 형태를 사용하면 된다. 호출되는 메소드에 대한 정보가 필요없다면 두번째와 같이 파라미터가 없는 메소드를 사용한다.

After Returning Advice

After Returning Advice는 다음과 같이 <aop:after-returning> 태그를 사용하여 정의한다.

<bean id="returnValueChecker"
    class="net.daum.cto.ts.techReport.report0.ReturnValueChecker">
</bean>
<aop:aspect id="returnValueCheckAspect" ref="returnValueChecker">
    <aop:pointcut id="needCheckRetValMethod" 
        expression="execution(* net.daum.cto.ts.techReport.*.*Service*.check*(..))" />
    <aop:after-returning pointcut-ref="needCheckRetValMethod"
        returning="returnVal"
        method="checkReturnValue" />
</aop:aspect>

returning 속성은 리턴값을 저장할 파라미터의 이름을 나타낸다. 예를 들어, 위 설정에서 ReturnValueChecker.checkReturnValue() 메소드는 다음과 같이 returnValue 파라미터를 값으로 갖게 된다.

public class ReturnValueChecker {
    public void checkReturnValue(Object returnVal) throws Exception {
        ...
    }
}

만약 리턴값을 사용할 필요가 없다면 returning 속성을 지정하지 않아도 되며, 이 경우 Aspect로 사용될 메소드는 다음과 같이 파라미터를 가져서는 안 된다.

public class ReturnValueChecker {
    public void checkReturnValue() throws Exception {
        ...
    }
}

Aspect를 구현한 클래스의 메소드에서 리턴 타입을 전달받기 위해서는 asm 라이브러리가 필요하다. Maven을 사용할 경우 다음과 같이 asm 라이브러리에 대한 의존관계를 추가해주어야 한다.

    <dependency>
        <groupId>asm</groupId>
        <artifactId>asm-all</artifactId>
        <version>2.2</version>
        <scope>runtime</scope>
    </dependency>

After Throwing Advice

After Throwing Advice는 다음과 같이 <aop:after-throwing> 태그를 사용하여 지정할 수 있다.

<bean id="loggingException"
    class="net.daum.cto.ts.techReport.report0.LoggingException">
</bean>
<aop:aspect id="loggingExceptionAspect" ref="loggingException">
    <aop:pointcut ... />
    <aop:after-throwing
        pointcut-ref="servicePointcut2"
        throwing="ex"
        method="doLogException" />
</aop:aspect>

위 코드에서 throwing 속성은 doLogException 메소드의 파라미터 이름을 의미한다. 이때 파라미터의 타입은 발생한 예외 클래스 또는 그 상위 타입이어야 한다. 아래 코드는, doLogException() 메소드의 작성 예를 보여주고 있다.

public void doLogException(Throwable ex) {
    System.out.println("---- 예외 발생했음!!!!:"+ex.getClass().getName());
}

After Returning Advice와 마찬가지로 발생한 예외에 대한 정보가 필요없다면 throwing 속성을 지정할 필요가 없으며, 이 경우 메소드는 파라미터를 가져서는 안 된다.

앞서 리턴값을 전달받는 경우와 마찬가지로, Aspect를 구현한 클래스의 메소드에서 예외 객체를 전달받기 위해서는 asm 라이브러리가 필요하다.

After Advice

After Advice는 finally의 성격을 갖는 Advice로서 <aop:after> 태그를 사용하여 지정한다.

<aop:aspect id="releaseResourceAspect" ref="releaseResource">
    <aop:pointcut ... />
    <aop:after pointcut-ref="usingResourceMethod"
        method="release" />
</aop:aspect>

Around Advice

Around Advice는 메소드의 호출을 완벽하게 제어할 수 있는 Advice로서 메소드 호출 이전 및 이후, 그리고 심지어 메소드 호출가 예외 처리까지도 제어할 수 있다. Around Advice는 다음과 같이 <aop:around> 태그를 사용하여 설정할 수 있다.

<aop:aspect id="loggingAspect" ref="logging">
    <aop:pointcut id="servicePointcut1"
        expression="execution(* net.daum.cto.ts.techReport.report0.*.*(..))" />
    <aop:around pointcut-ref="servicePointcut1" method="log" />
</aop:aspect>

Around Advice가 적용될 때 호출될 메소드는 다음과 같이 첫번째 파라미터로 ProceedingJoinPoint 타입을 지정해주어야 한다.

public Object log(ProceedingJoinPoint call) throws Throwable {
    // 메소드가 호출되기 전에 전처리
    
    Object retVal = call.proceed();
    
    // 메소드가 호출된 이후 후처리
    
    return retVal;
}

메소드 몸체에서는 ProceedingJoinPoint.proceed() 메소드를 호출하여 원래 호출하려던 메소드를 실행할 수 있다. ProceedingJoinPoint.proceed() 메소드는 Object 타입을 리턴하는데, 이때 리턴받은 값을 리턴하거나 또는 새로운 값을 리턴할 수도 있다.

AOP를 이용한 트랜잭션 처리

Spring 2.0에는 트랜잭션을 위한 태그도 새롭게 추가되었다. 트랜잭션을 위한 태그는 "tx" 네임스페이스를 가지며, 다음과 같이 트랜잭션 관련 태그와 AOP 관련 태그를 사용하여 트랜잭션을 원하는 pointcut에 적용할 수 있다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx 
        http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
    
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" ...>
        ...
    </bean>
    
    <bean id="txManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
        
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="get*" read-only="true" />
            <tx:method name="somethingSpecial*" propagation="REQUIRES_NEW"/>
            <tx:method name="*" />
        </tx:attributes>
    </tx:advice>
    
    <aop:config>
        <aop:pointcut id="fooServiceOperation"
            expression="execution(* x.y.service.FooService.*(..))" />
        <aop:advisor advice-ref="txAdvice"
            pointcut-ref="fooServiceOperation" />
    </aop:config>
    
    <bean id="fooService" class="x.y.service.DefaultFooService" />
</beans>

기존에 프록시 객체를 사용하여 트랜잭션을 적용했던 반면에, 위 코드의 경우 서비스를 정의한 빈은 변경하지 않고 <tx:advice> 태그와 <aop:advisor> 태그를 사용하여 서비스 빈에 트랜잭션을 적용한 것을 알 수 있다.

관련링크:

+ Recent posts