LockMin

2023. 5. 24. 14:29Spring Framework/Spring AOP

다음은 Spring 테스트 스위트의 예시로, 한 개 이상의 객체에 다음 인터페이스를 도입한다고 가정합니다:

public interface Lockable {  
  void lock();  
  void unlock();  
  boolean locked();  
}

이는 믹스인(mixin)을 나타냅니다. 어드바이스된 객체를 Lockable로 캐스팅하고, 객체의 타입에 관계없이 lock 및 unlock 메서드를 호출할 수 있기를 원합니다. lock() 메서드를 호출하면 모든 setter 메서드가 LockedException을 던지도록 하고 싶습니다. 따라서 객체가 이를 전혀 인지하지 못한 상태에서 객체를 불변으로 만드는 기능을 추가할 수 있습니다. 이것은 AOP의 좋은 예입니다.

먼저, 무거운 작업을 수행할 IntroductionInterceptor가 필요합니다. 이 경우 org.springframework.aop.support.DelegatingIntroductionInterceptor 클래스를 확장합니다. IntroductionInterceptor를 직접 구현할 수 있지만, 대부분의 경우 DelegatingIntroductionInterceptor를 사용하는 것이 가장 좋습니다.

DelegatingIntroductionInterceptor는 도입을 실제 도입된 인터페이스의 구현에 위임하도록 설계되어 있으며, 이를 위해 인터셉션(interception)을 사용하는 것을 숨깁니다. 생성자 아규먼트를 사용하여 delegate를 임의의 객체로 설정할 수 있습니다. 기본 delegate(아규먼트가 없는 생성자를 사용할 경우)는 this입니다. 따라서 다음 예시에서 delegate는 DelegatingIntroductionInterceptor의 LockMixin 하위 클래스입니다. 기본적으로 delegate가 this-LockMixin 인스턴스-일 때, DelegatingIntroductionInterceptor 인스턴스는 delegate가 구현한 모든 인터페이스(IntroductionInterceptor 제외)를 찾아 이를 도입할 수 있습니다. LockMixin과 같은 하위 클래스는 suppressInterface(Class intf) 메서드를 호출하여 노출되지 말아야 할 인터페이스를 억제할 수 있습니다. 그러나 IntroductionInterceptor가 지원할 수 있는 인터페이스가 얼마나 많든지 간에, 사용된 IntroductionAdvisor가 실제로 노출되는 인터페이스를 제어합니다. 도입된 인터페이스는 대상이 동일한 인터페이스를 구현하고 있는 경우 이를 숨깁니다.

따라서 LockMixin은 DelegatingIntroductionInterceptor를 확장하고 Lockable을 자체적으로 구현합니다. 슈퍼 클래스는 자동으로 Lockable이 도입될 수 있음을 감지하므로 이를 명시할 필요가 없습니다. 이 방법을 통해 여러 인터페이스를 도입할 수 있습니다.

locked 인스턴스 변수를 사용하는 것을 주목하세요. 이는 대상 객체에 저장된 상태에 추가적인 상태를 효과적으로 추가합니다.

다음은 LockMixin 클래스의 예시입니다:

public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {

  private boolean locked;

  public void lock() {
      this.locked = true;
  }

  public void unlock() {
      this.locked = false;
  }

  public boolean locked() {
      return this.locked;
  }

  public Object invoke(MethodInvocation invocation) throws Throwable {
      if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
          throw new LockedException();
      }
      return super.invoke(invocation);
  }

}

위 설명은 Spring AOP를 사용하여 객체에 새로운 기능을 도입하는 방법을 보여줍니다. 특히, Lockable 인터페이스를 객체에 추가하고 이를 통해 객체를 잠금(lock) 기능을 가지도록 하는 예제입니다. 이 예제를 통해 객체가 외부에서 쉽게 수정되지 않도록 방지할 수 있습니다. 이 과정을 단계별로 자세히 설명하겠습니다.

1. 도입할 인터페이스: Lockable

  • Lockable 인터페이스는 객체에 잠금 기능을 제공하는 인터페이스입니다. 이 인터페이스는 세 가지 메서드를 정의합니다:
    • void lock(): 객체를 잠그는 메서드입니다. 객체가 잠기면, 이 객체의 상태를 변경하는 메서드들이 호출되지 않도록 합니다.
    • void unlock(): 객체의 잠금을 해제하는 메서드입니다.
    • boolean locked(): 객체가 현재 잠겨있는지 여부를 반환하는 메서드입니다.

이 인터페이스를 사용하면, 어떤 객체든지 Lockable로 캐스팅하여 lock()unlock() 메서드를 호출할 수 있습니다.

2. IntroductionInterceptor의 필요성

  • IntroductionInterceptor는 객체에 새로운 인터페이스를 도입하고, 그 인터페이스의 메서드 호출을 처리할 수 있게 해줍니다.
  • 여기서는 Lockable 인터페이스를 도입하고, lock()unlock() 메서드를 처리하기 위해 IntroductionInterceptor가 필요합니다.

3. DelegatingIntroductionInterceptor 사용

  • DelegatingIntroductionInterceptor는 IntroductionInterceptor의 편리한 구현체로, 도입된 인터페이스의 구현을 간단하게 처리할 수 있도록 도와줍니다.
  • 이 클래스는 도입된 인터페이스의 메서드 호출을 delegate 객체(위임 객체)에 위임하는 방식으로 작동합니다.

4. LockMixin 클래스의 구현

LockMixin 클래스는 DelegatingIntroductionInterceptor를 확장하고 Lockable 인터페이스를 구현합니다. 이 클래스는 실제로 도입된 인터페이스의 기능을 어떻게 처리할지를 정의합니다.

public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {

  private boolean locked; // 객체가 잠겨있는지 여부를 나타내는 변수

  public void lock() {
      this.locked = true; // lock() 호출 시 locked 변수를 true로 설정
  }

  public void unlock() {
      this.locked = false; // unlock() 호출 시 locked 변수를 false로 설정
  }

  public boolean locked() {
      return this.locked; // 객체가 잠겨있는지 여부를 반환
  }

  public Object invoke(MethodInvocation invocation) throws Throwable {
      // 객체가 잠겨있고, 호출된 메서드가 "set"으로 시작하는 경우
      if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
          throw new LockedException(); // LockedException을 던져 메서드 호출을 막음
      }
      return super.invoke(invocation); // 그 외의 경우는 원래의 메서드 호출을 진행
  }

}

5. LockMixin의 작동 방식

  • 잠금 상태 관리:
    • LockMixin 클래스는 locked라는 boolean 변수를 사용해 객체의 잠금 상태를 관리합니다.
    • lock() 메서드를 호출하면 lockedtrue로 설정되고, unlock() 메서드를 호출하면 lockedfalse로 설정됩니다.
  • 메서드 호출 가로채기:
    • invoke() 메서드는 도입된 인터페이스의 메서드 호출을 처리합니다. 이 메서드는 MethodInvocation 객체를 매개변수로 받아, 어떤 메서드가 호출되었는지 확인할 수 있습니다.
    • 만약 객체가 잠겨있고(locked() 메서드가 true를 반환), 호출된 메서드가 set으로 시작하는 setter 메서드라면, LockedException을 던져 메서드 호출을 차단합니다.
    • 그렇지 않은 경우, super.invoke(invocation)을 호출하여 원래 메서드 실행을 진행합니다.

6. delegate와 this

  • delegate 란 도입된(또는 "인터페이스된") 기능의 메서드를 실제로 구현하는 객체를 의미.
  • DelegatingIntroductionInterceptor는 도입된 인터페이스의 메서드 호출을 delegate에 위임합니다. 기본적으로 delegate는 이 클래스의 인스턴스(this)입니다.
  • LockMixin 클래스의 경우, thisLockMixin 인스턴스 자체를 가리킵니다. 이 인스턴스가 Lockable 인터페이스를 구현하고 있으므로, LockMixin은 도입된 인터페이스의 메서드 호출을 스스로 처리하게 됩니다.

7. IntroductionAdvisor와 LockMixin의 관계

  • IntroductionAdvisor는 Lockable 인터페이스를 도입할 클래스들을 관리하고, IntroductionInterceptor가 올바르게 설정되었는지 확인합니다.
  • LockMixinLockable 인터페이스를 구현하는 IntroductionInterceptor 역할을 하며, 이 클래스의 인스턴스를 사용해 객체에 Lockable 인터페이스를 도입합니다.

8. 결론

이 전체 과정은 Spring AOP의 Introduction 기능을 사용하여, 기존 객체에 새로운 인터페이스(Lockable)를 도입하고, 객체의 상태를 잠금으로 관리하는 기능을 추가하는 예입니다. 이를 통해 객체는 외부에서 쉽게 수정되지 않도록 보호되며, 잠금과 관련된 메서드들은 객체의 타입에 관계없이 사용할 수 있게 됩니다.