Study/Spring

[Spring 5 Recipes] Spring 5 Recipes 2장 정리 #7

꼽냥이 2021. 9. 27. 18:07
반응형

POJO 구성 방식

17. AspectJ 포인트컷 표현식 작성

AspectJ 는 다양한 종류의 조인포인트를 매치할 수 있는 강력한 표현식 언어를 제공한다. 하지만 스프링 AOP 가 지원하는 조인포인트 대상은 IoC 컨테이너 안에 선언된 빈에 국한된다. 스프링 AOP 에서는 AspectJ 포인트컷 언어를 활용해 포인트컷을 정의하며 런타임에 AspectJ 라이브러리를 이용해 포인트컷 표현식을 해석한다.

더보기

AspectJ 포인트컷 언어에 대한 자세한 내용은 공식 웹 사이트 (https://www.eclipse.org/aspectj/) 에서 확인할 수 있다.

 

포인트컷 표현식의 가장 일반적인 형태는 시그니처를 기준으로 여러 메소드를 매치하는 것이다. 예를 들어 다음 포인트컷 표현식은 ArithmeticCalculator 인터페이스에 선언한 메소드를 모두 매치한다. 

execution(* com.apress.springrecipes.calculator.ArithmeticCalculator.*(..))

// execution(* ArithmeticCalculator.*(..))

대상 클래스나 인터페이스가 애스펙트와 같은 패키지에 있는 경우 위의 주석으로 작성한 바와 같이 패키지명은 명시하지 않아도 된다.

 

AspectJ 에 탑재된 포인트컷 언어는 다양한 조인포인트를 매치할 수 있지만, 간혹 매치하고자 하는 메소드 사이에 이렇다 할 공통 특성이 없는 경우, 다음과 같이 커스텀 어노테이션을 사용할 수 있다.

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoggingRequired {
}

위와 같이 커스텀 어노테이션을 만든 후, 이를 적용하고자 하는 클래스에 어노테이션을 붙이면 사용이 가능하다. 단, 어노테이션은 상속되지 않으므로 인터페이스가 아니라 구현 클래스에만 적용해야 한다.

@LoggingRequired
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
	
    public double add(double a, double b) {
    	...
    }
    
    ...
}

그 후, @LoggingRequired 를 붙인 클래스/메소드를 스캐닝하도록 @Pointcut 의 annotation 안에 포인트컷 표현식을 넣는다.

@Aspect
public class CalculatorPointcuts {
	
    @Pointcut("annotation(com.apress.springrecipes.calculator.LoggingRequired")
    public void loggingOperation() {}
}
더보기

위와 같이 적용을 한 뒤, Aspect 클래스에서 위의 pointcut 을 적용했더니 적용이 되지 않는 문제가 있다. 위의 annotation 만으로는 우선 오류가 발생하고 @annotation 으로 변경하면 실행은 되지만, Aspect 클래스에서 Pointcut 을 따로 만들어놓지 않으면 실행이 되지 않는다.

 

내가 이 부분에 대해 이해를 한 방식은, 위와 같이 적용을 했을 때 Aspect 클래스에서 Pointcut 을 따로 정의하지 않더라도 @LoggingRequired 가 붙은 클래스에 대해서 자동으로 어드바이스가 동작을 하는 것이었는데 그런 게 아닌 것 같다.

 

어찌어찌 동작이 하도록 코드를 수정하면, @LoggingRequired 가 붙어있지 않아도 애스펙트가 매칭되고 붙어있어도 Aspect 클래스 안의 Pointcut 과 매칭이 안되면 애스펙트가 매칭이 안 되는.. 기이한 현상이 발생한다.

 

아무래도 내가 책의 내용을 잘못 이해하거나, 책의 내용과 현재 구현된 내용이 변경되었거나, 혹은 그 어떤 모종의 이유로 문제가 발생하는 것 같은데 이 부분에 대해서는 추후 따로 공부를 해서 방법을 찾게 되면 정리를 하도록 하겠다.

 

특정한 타입 내부의 모든 조인포인트를 매치하는 포인트컷 표현식도 존재한다. 스프링 AOP 에 적용하면 그 타입 안에 구현된 메소드를 실행할 때만 어드바이스가 적용되도록 포인트컷 적용 범위를 좁힐 수 있다. 이를테면 다음 포인트컷은 com.apress.springrecipes.calculator 패키지의 전체 메소드 실행 조인포인트를 매치한다.

within(com.apress.springrecipes.calculator.*)
더보기

위의 예시도 within 과 @within 이 존재하는데, 어떤 차이가 있는 지는 따로 공부를 해야할 것 같다.

다음과 같이 어떤 인터페이스를 구현한 모든 클래스의 메소드 실행 조인포인트를 매치하려면 맨 뒤에 + 기호를 붙인다.

within(Interface+)

AspectJ 포인트컷 표현식은 &&, || ! 등의 연산자로 조합이 가능하다. 예를 들어 다음 포인트컷은 ArithmeticCalculator 또는 UnitCalculator 인터페이스를 구현한 클래스의 조인포인트를 매치한다.

within(ArithmeticCalculator+ || within(UnitCalculator+))

 

18. Introduction 을 이용해 POJO 에 기능 추가

어떤 공통 로직을 공유하는 클래스가 여러 개 존재할 경우, OOP 에서는 보통 같은 베이스 클래스를 상속하거나 같은 인터페이스를 구현하는 형태로 애플리케이션을 개발한다. AOP 관점에서는 충분히 모듈화가 가능한 공통 관심사이지만, 자바는 언어 구조상 오직 한 개의 클래스만 상속할 수 있으므로 동시에 여러 구현 클래스로부터 기능을 물려받아 사용하는 것이 불가능하다.

 

Introduction 은 AOP 어드바이스의 특별한 타입으로 객체가 어떤 인터페이스의 구현 클래스를 공급받아 동적으로 인터페이스를 구현하는 기술이다. 마치 객체가 런타임에 구현 클래스를 상속하는 것처럼 보여지고, 여러 구현 클래스를 지닌 여러 인터페이스를 동시에 인트로듀스해 사실상 다중 상속이 가능해진다.

 

다음 MaxCalculator 와 MinCalculator 인터페이스에 각각 max(), min() 메소드를 정의하고, 이를 각각 구현한 MaxCalculatorImpl, MinCalculatorImpl 클래스를 정의한다.

public interface MaxCalculator {
    public double max(double a, double b);
}

public interface MinCalculator {
    public double min(double a, double b);
}

// 구현 클래스
public class MaxCalculatorImpl implements MaxCalculator {
    ...
}

public class MinCalculatorImpl implements MinCalculator {
    ...
}

이 때, 만약 ArithmeticCalculatorImpl 클래스에서 max() 와 min() 을 모두 호출하려면 어떻게 해야 할까. 자바는 단일 상속이 가능하므로 MaxCalculatorImpl, MinCalculatorImpl 클래스를 동시에 상속하는 것이 불가능하다. 구현 코드를 복사하든지, 아니면 실제 구현 클래스에 처리를 맡기든 해서 두 클래스 중 한쪽은 상속하고 한 쪽은 인터페이스를 구현하는 방법뿐이다.

 

이럴 때 인트로덕션을 사용하면 ArithmeticCalculatorImpl 에서 MaxCalculator 와 MinCalculator 인터페이스를 둘 다 동적으로 구현한 것처럼 구현 클래스를 이용할 수 있다. 인트로덕션은 어드바이스와 같이 애스펙트 안에서 필드에 @DeclareParents 를 붙여 선언한다. 애스펙트를 새로 만들거나 용도가 비슷한 기존 애스펙트의 재사용도 가능하다.

@Aspect
@Component
public class CalculatorIntroduction {
	
    @DeclareParents(
    	value = "com.apress.springrecipes.calculator.ArithmeticCalculatorImpl",
        defaultImpl = MaxCalculatorImpl.class)
    public MaxCalculator maxCalculator;
    
    @DeclareParents(
    	value = "com.apress.springrecipes.calculator.ArithmeticCalculatorImpl",
        defaultImpl = MinCalculatorImpl.class)
    public MinCalculator minCalculator;
}

인트로덕션 대상 클래스는 @DeclareParents 의 value 속성으로 지정하며 이 어노테이션을 붙인 필드형에 따라 들여올 인터페이스가 결정된다. 새 인터페이스에서 사용할 구현 클래스는 defaultImpl 속성에 명시한다. @DeclareParents 의 value 속성값에 AspectJ 의 타입 매치 표현식을 넣으면 여러 클래스로 인터페이스를 들여올 수도 있다. 이와 같이 두 인터페이스를 ArithmeticCalculatorImpl 에 인트로덕션했으면, 해당 인터페이스로 캐스팅 후 max(), min() 메소드를 호출할 수 있다.

public static void main(String [] args) {
	
    ArithmeticCalculator arithmeticCalculator;
    
    ...
    
    MaxCalculator maxCalculator = (MaxCalculator) arithmeticCalculator;
    maxCalculator.max(2, 1);
    
    MinCalculator minCalculator = (MinCalculator) arithmeticCalculator;
    minCalculator.min(1, 2);
}

 

19. AOP 를 이용해 POJO 에 상태 추가

기존 객체에 새로운 상태를 추가해 메소드 호출 횟수, 최종 수정 일자 등 사용 내역을 파악하고 싶은 경우가 있다. 모든 객체가 동일한 베이스 클래스를 상속하는 건 해결책이 될 수 없고, 레이어 구조가 다른 여러 클래스에 동일한 상태를 추가하기란 더더욱 어렵다. 이 때, 인트로덕션과 어드바이스를 이용하면 서로 다른 클래스가 동일한 상태를 저장하고 갱신할 수 있도록 할 수 있다.

 

각 객체의 메소드 호출 횟수를 기록하려고 할 때, 원본 클래스에 호출 횟수를 담을 필드가 없기 때문에 스프링 AOP 인트로덕션을 적용할 수 있다. 우선, 호출 횟수를 기록하기 위한 Counter 인터페이스를 작성한다.

public interface Counter {
    public void increase();
    public void getCount();
}

그리고 이를 구현한 구현 클래스를 만든다. 호출 횟수는 count 필드에 저장한다.

public class CounterImpl implements Counter {
	
    private int count;
    
    @Override
    public void increase() {
    	count++;
    }
    
    @Override
    public int getCount() {
    	return count;
    }
}

모든 Calculator 객체에 Counter 인터페이스를 적용하기 위해 다음과 같이 타입 매치 표현식을 이용해 인트로덕션을 적용한다.

@Aspect
@Component
public class CalculatorIntroduction {
	
    @DeclareParents(
    	value = "com.apress.springrecipes.calculator.*CalculatorImpl",
        defaultImpl = CounterImpl.class)
    public Counter counter;
}

 

이로써 CounterImpl 을 모든 Calculator 객체에 적용했지만, 아직 이 상태로는 호출 횟수를 기록할 수 없다. 객체의 메소드가 호출될 때마다 counter 값을 증가시키려면 After 어드바이스를 적용해야 한다. 그리고 Counter 인터페이스를 구현한 객체는 프록시가 유일하므로 반드시 target 이 아니라 this 객체를 가져와 사용해야 한다.

@Aspect
@Component
public class CalculatorIntroduction {
	
    @After("execution(* com.apress.springrecipes.calculator.*Calculator.*(..))"
		+ " && this(counter)")
    public void increaseCount(Counter counter) {
    	counter.increase();
    }
}

 

이제 다음과 같이 각 Calculator 객체를 Counter 타입으로 캐스팅해 메소드 호출 횟수를 출력할 수 있다.

public static void main(String [] args) {

    ArithmeticCalculator arithmeticCalculator;
    UnitCalculator unitCalculator;
    
    ...
    
    Counter arithmeticCounter = (Counter) arithmeticCalculator;
    System.out.println(arithmeticCounter.getCount());
    
    Counter unitCounter = (Counter) unitCalculator;
    System.out.println(unitCounter.getCount());
}

 

#Reference.

 

스프링 5 레시피(4판)

이 책은 스프링 5에 새로 탑재된 기능 및 다양한 구성 옵션 등 업데이트된 프레임워크 전반을 실무에 유용한 해법을 제시하는 형식으로 다룹니다. IoC 컨테이너 같은 스프링 기초부터 스프링 AOP/A

www.hanbit.co.kr