Post

토비의 스프링 6장

6. AOP

6.1 사용자 레벨 관리 기능 추가

  • 6.1.1 메소드 분리

    • 트랜잭션 경계설정의 코드와 비즈니스 로직 코드 간에 서로 주고받는 정보가 없기 때문에 분리 가능
  • 6.1.2 DI를 이용한 클래스의 분리

    • DI 적용을 이용한 트랜잭션 분리
      • 한 번에 두 개의 UserService 인터페이스 구현 클래스 이용(UserServiceImpl, UserServiceTx)
    • UserService 인터페이스 도입
      1
      2
      3
      4
      
        public interface UserService {
        void add(User user);
        void upgradeLevels();
        }
      
    • UserService 인터페이스의 구현 클래스인 UserServiceImpl은 기존 UserService 클래스에서 트랜잭션과 관련된 코드만 제거하고 나머지는 유지한다.
    • 분리된 트랜잭션 기능
      • UserServiceTx는 UserService를 구현한 다른 오브젝트를 DI 받는다.
      • DI 받은 UserService 오브젝트에 모든 기능을 위임한다.
      • 그 후 트랜잭션을 적용한다.
    • 트랜잭션 적용을 위한 DI 설정
    • 트랜잭션 분리에 따른 테스트 수정
      • 같은 타입의 빈이 두 개라면 필드 이름(아이디)를 이용해 빈을 찾는다.
      • 목 오브젝트를 이용해 수동 DI를 적용하는 테스트라면 어떤 클래스의 오브젝트인지 분명하게 선언
    • 트랜잭션 경계설정 코드 분리의 장점
      • 비즈니스 로직을 담당하고 있는 UserServiceImpl의 코드를 작성할 때는 트랜잭션과 같은 기술적인 내용에는 전혀 신경쓰지 않아도 된다.
      • 비즈니스 로직에 대한 테스트를 손쉽게 만들어낼 수 있다.

6.2 고립된 단위 테스트

  • 6.2.1 복잡한 의존관계 속의 테스트

    • UserService를 테스트하는 것처럼 보이지만 사실은 그 뒤에 존재하는 훨씬 더 많은 오브젝트와 환경, 서비스, 서버, 네트워크까지 함께 테스트함
  • 6.2.2 테스트 대상 오브젝트 고립시키기

    테스트의 대상이 환경이나, 외부 서버, 다른 클래스의 코드에 종속되고 영향을 받지 않도록 고립시킬 필요가 있다.

    • 테스트를 위한 UserServiceImpl 고립
    • 고립된 단위 테스트 활용
      1. 테스트 실행 중에 UserDao를 통해 가져올 테스트용 정보를 DB에 넣는다.
      2. 메일 발송 여부를 확인하기 위해 MailSender 목 오브젝트를 DI 해준다.
      3. 실제 테스트 대상인 userService의 메소드를 실행한다.
      4. 결과각 DB에 반영됐는지 확인하기 위해서 UserDao를 이용해 DB에서 데이터를 가져와 결과를 확인한다.
      5. 목 오브젝트를 통해 UserService에 의한 메일 발송이 있었는지를 확인하면 된다.
    • UserDao 목 오브젝트
      • UserDao와 DB까지 직접 의존하고 있는 첫 번째와 네 번째의 테스트 방식에 목 오브젝트 적용
      • getAll()에 대해서는 스텁으로서, update()에 대해서는 목 오브젝트로서 동작하는 UserDao의 테스트 대역 필요 -> MockUserDao
      • 인터페이스를 구현하므로 사용하지 않는 메소드가 있다. 이 메소드에서는 아무 것도 안 하는 것 보다 UnsupportedOperationException을 던지게 하여 지원하지 않는 기능이라는 예외가 발생하도록 만드는 것이 좋다.
      • 기존에는 @AutoWired를 통해 UserService 타입의 빈을 가져왔지만, MockUserDao를 도입하여 고립됐기 때문에 빈을 가져올 필요가 없다.
    • 테스트 수행 성능의 향상
      • 고립된 테스트를 하면 테스트가 다른 의존 대상에 영향을 받을 경우를 대비해 복잡하게 준비할 필요가 없을 뿐만 아니라, 테스트 수행 성능도 향상된다.
  • 6.2.3 단위 테스트와 통합 테스트
    • 단위 테스트: 테스트 대상 클래스를 목 오브젝트 등의 테스트 대역을 이용해 의존 오브젝트나 외부의 리소스를 사용하지 않도록 고립시켜서 테스트하는 것
    • 통합 테스트: 두 개 이상의, 성격이나 계층이 다른 오브젝트가 연동하도록 만들어 테스트하거나, 또는 외부의 DB나 파일, 서비스 등의 리소스가 참여하는 테스트
    • 단위 테스트와 통합 테스트 중에서 어떤 방법을 쓸지에 대한 가이드라인
      • 항상 단위 테스트를 먼저 고려한다.
      • 하나의 클래스나 성격과 목적이 같은 긴밀한 클래스 몇 개를 모아서 외부와의 의존관계를 모두 차단하고 필요에 따라 스텁이나 목 오브젝트 등의 테스트 대역을 이용하도록 테스트를 만든다.
      • 외부 리소스를 사용해야만 가능한 테스트는 통합 테스트로 만든다.
      • DAO처럼 DB를 통해 로직을 수행하는 코드는 단위 테스트로 만들기 어렵다.
      • DAO를 테스트를 통해 충분히 검증해두면, DAO를 이용하는 코드는 DAO 역할을 스텁이나 목 오브젝트로 대체해서 테스트할 수 있다.
      • 여러 개의 단위가 의존관계를 가지고 동작할 때를 위한 통합 테스트는 필요하다. 다만, 단위 테스트를 충분히 거쳤다면 통합 테스트의 부담은 상대적으로 줄어든다.
      • 단위 테스트를 만들기가 너무 복잡하다고 판단되는 코드는 처음부터 통합 테스트를 고려해본다.
      • 스프링 테스트 컨텍스트 프레임워크를 이용하는 테스트는 통합 테스트다. 가능하면 스프링의 지원 없이 직접 코드 레벨의 DI를 사용하면서 단위 테스트를 하는 게 좋다.
    • 테스트는 코드가 작성되고 빠르게 진행되는 편이 좋다.
    • 코드르 작성하면서 테스트는 어떻게 만들 수 있을까를 생각해보는 것은 좋은 습관이다.
  • 6.2.4 목 프레임워크
    • Mockito 프레임워크
      • 목 프레임워크의 특징은 목 클래스를 일일이 준비할 필요가 없다는 점이다.
      • Mockito 목 오브젝트 사용 단계
        1. 인터페이스를 이용해 목 오브젝트를 만든다.
        2. 목 오브젝트가 리턴할 값이 있으면 이를 지정해준다. 예외를 강제로 던지게 만들 수도 있다.
        3. 테스트 대상 오브젝트에 DI 해서 목 오브젝트가 테스트 중에 사용되도록 만든다.
        4. 테스트 대상 오브젝트를 사용한 후에 목 오브젝트의 특정 메소드가 호출됐는지, 어떤 값을 가지고 몇 번 호출됐는지를 검증한다.

6.3 다이내믹 프록시와 팩토리 빈

  • 6.3.1 프록시와 프록시 패턴, 데코레이터 패턴
    • 부가기능과 핵심기능을 분리했더라도 클라이언트가 자신을 거쳐서 핵심기능을 가진 클래스를 직접 사용해버리면 부가기능이 적용될 기회가 없다는 문제가 있다.
    • 그래서 부가기능은 마치 자신이 핵심 기능을 가진 클래스인 것처럼 꾸며서, 클라이언트가 자신을 거쳐서 핵심기능을 사용하도록 만들어야 한다.
    • 프록시: 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것
    • 타깃(실체): 프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트
    • 프록시의 특징은 타깃과 같은 인터페이스를 구현했다는 것과 프록시가 타깃을 제어할 수 있는 위치에 있다는 것
    • 프록시는 사용 목적에 따라 두 가지로 구분
      1. 클라이언트가 타깃에 접근하는 방법 제어
      2. 타깃에 부가적인 기능을 부여하기 위해서
    • 데코레이터 패턴
      • 데코레이터 패턴: 타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴
      • 다이내믹하게 기능을 부가한다는 의미는 컴파일 시점, 즉 코드상에서는 어떤 방법과 순서로 프록시와 타깃이 연결되어 사용되는지 정해져 있지 않다는 점
      • 인터페이스를 구현한 타겟과 여러 개의 프록시를 단계적으로 위임하여 사용할 수 있다.
      • 다음 위임 대상은 인터페이스로 선언하고 생성자나 수정자 메소드를 통해 위임 대상을 외부에서 런타임 시에 주입받을 수 있도록 만들어야 한다.
    • 프록시 패턴
      • 프록시라는 용어와 프록시 패턴은 다름
      • 프록시 패턴: 프록시를 사용하는 방법 중에서 타깃에 대한 접근 방법을 제어하려는 목적을 가진 경우
      • 타깃 오브젝트에 대한 레퍼런스가 미리 필요할 때 사용
      • 원격 오브젝트를 이용하는 경우에 사용
      • 특별한 상황에서 타깃에 대한 접근권한을 제어하기 위해 사용
  • 6.3.2 다이내믹 프록시
    • 프록시 만드는 일 번거로움
    • java.lang.reflect 사용
    • 프록시의 구성과 프록시 작성의 문제점
      • 프록시의 구성
        • 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타깃 오브젝트로 위임
        • 지정된 요청에 대해서는 부가기능 수행
      • 프록시를 만들기 번거로운 이유
        1. 타깃의 인터페이스를 구현하고 위임하는 코드를 작성하기 번거롭다.
        2. 부가기능 코드가 중복될 가능성이 많다.
      • 다이내믹 프록시로 해결 가능!
    • 리플렉션
      • 다이내믹 프록시는 리플렉션 기능을 이용해서 프록시를 만든다.
      • 리플렉션은 자바의 코드 자체를 추상화해서 접근하도록 만든 것
    • 프록시 클래스

      • HelloUpperCase 데코레이터 패턴 추가
    • 다이내믹 프록시 적용
      • 다이내믹 프록시는 프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어지는 오브젝트
      • 다이내믹 프록시 오브젝트는 타깃의 인터페이스와 같은 타입
      • 부가기능은 프록시 오브젝트와 독립적으로 InvocationHandler를 구현한 오브젝트에 담는다.
      • public Object invoke(Object proxy, Method method, Object[] args)
      • 다이내믹 프록시 오브젝트는 클라이언트의 모든 요청을 리플렉션 정보로 변환해서 InvocationHandler 구현 오브젝트의 invoke() 메소드로 넘긴다.
        1
        2
        3
        4
        5
        
          // 프록시 생성
          Hello proxiedHello = (Hello)Proxy.newProxyInterface( // 생성된 다이내믹 프록시 오브젝트는 Hello 인터페이스를 구현하고 있으므로 캐스팅 안전
          getClass().getClassLoader(), // 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용할 클래스 로더
          new Class[] {Hello.class}, // 구현할 인터페이스
          new UppercaseHandler(new HelloTarget())); // 부가기능과 위임 코드를 담은 InvocationHandler
        
    • 다이내믹 프록시의 확장
      • InvocationHandler는 타깃의 종류에 상관없이 적용이 가능하다는 장점이 있다.
      • 호출하는 메소드의 이름, 파라미터의 개수와 타입, 리턴 타입 등의 정보를 가지고 부가적인 기능을 적용할 메소드를 선택 가능
  • 6.3.3 다이내믹 프록시를 이용한 트랜잭션 부가기능
    • UserServiceTx는 서비스 인터페이스의 메소드를 모두 구현해야 하고 트랜잭션이 필요한 메소드마다 중복되는 문제
    • 트랜잭션 InvocationHandler
      • 타깃 오브젝트의 모든 메소드에 무조건 트랜잭션이 적용되지 않도록 트랜잭션을 적용할 메소드 이름의 패턴을 DI 받음
      • RuntimeException이 아닌 InvocationTargetException 사용
  • 6.3.4 다이내믹 프록시를 위한 팩토리 빈
    • 다이내믹 프록시 오브젝트는 리플렉션 API를 이용해서 빈 정의에 나오는 클래스 이름을 가지고 오브젝트를 생성할 수 없다.
    • 팩토리 빈
      • 팩토리 빈: 스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈
        1
        2
        3
        4
        5
        
          public interface FactoryBean<T> {
          T getObject() throws Exception; // 빈 오브젝트를 생성해서 돌려준다.
          Class<? extends T> getObjectType(); // 생성되는 오브젝트의 타입을 알려준다.
          boolean isSingleton(); // getObject()가 돌려주는 오브젝트가 하상 같은 싱글톤 오브젝트인지 알려준다.
          }
        
      • 스프링은 private 생성자를 가진 클래스도 빈으로 등록해주면 리플렉션을 이용해 오브젝트를 만들어준다.
    • 팩토리 빈의 설정 방법

      • 여타 빈 설정과 다른 점은 message 빈 오브젝트의 타입이 class 애트리뷰트에 정의된 MessageFactoryBean이 아니라 Message 타입이라는 것
    • 다이내믹 프록시를 만들어주는 팩토리 빈
      • 스프링 빈에는 팩토리 빈과 UserServiceImpl만 빈으로 등록
      • 팩토리 빈은 다이내믹 프록시가 위임할 타깃 오브젝트인 UserServiceImpl에 대한 레퍼런스를 프로퍼티를 통해 DI받음
    • 트랜잭션 프록시 팩토리 빈
    • 트랜잭션 프록시 빈 테스트
      • TransactionHandler는 TxProxyFactoryBean 내부에서 만들어져 다이내믹 프록시 생성에 사용될 뿐 별도로 참조할 방법이 없다는 문제가 있다.
      • 그래서 빈으로 등록된 TxProxyFactoryBean을 직접 가져와서 프록시를 만든다.
  • 6.3.5 프록시 팩토리 빈 방식의 장점과 한계
    • 프록시 팩토리 빈의 재사용
      • 타깃 오브젝트에 맞는 프로퍼티 정보를 설정해서 빈으로 등록하면 재사용 가능
    • 프록시 팩토리 빈 방식의 장점
      • 타깃 인터페이스를 구현하는 클래스를 일일이 만드는 번거로움 제거
      • 하나의 핸들러 메소드를 구현하는 것만으로도 수많은 메소드에 부가기능 부여할 수 있기 때문에 중복 문제 제거
    • 프록시 팩토리 빈의 한계
      • 한 번에 여러 개의 클래스에 공통적인 부가기능을 제공하는 것 불가능
      • 하나의 타깃에 여러 개의 부가기능을 적용할 때, 설정 파일이 너무 복잡하고 길어지는 문제
      • TransactionHandler 오브젝트가 팩토리 빈 개수만큼 만들어지는 문제(동일한 트랙잭션 부가기능임에도 불구하고 타깃 오브젝트가 달라지면 TransactionHandler를 생성해야 함)

6.4 스프링의 프록시 팩토리 빈

  • 6.4.1 ProxyFactoryBean
    • ProxyFactoryBean은 순수하게 프록시를 생성하는 작업만을 담당하고 프록시를 통해 제공해줄 부가기능은 별도의 빈에 둘 수 있다.
    • ProxyFactoryBean이 생성하는 프록시에서 사용할 부가기능은 MethodInterceptor 인터페이스를 구현해서 만든다.
    • MethodInterceptor의 invoke() 메소드는 ProxyFactoryBean으로부터 타깃 오브젝트에 대한 정보까지도 함께 제공받는다.
    • 그래서 MethodInterceptor는 타깃 오브젝트에 상관없이 독립적으로 만들어질 수 있다.
      1
      2
      3
      4
      5
      6
      7
      
        @Test
        public void proxyFactoryPattern() {
        ProxyFactoryBean pfBean = new ProxyFactoryBean();
        pfBean.setTarget(new HelloTarget()); // 타깃 설정
        pfBean.addAdvice(new UppercaseAdvice()); // 부가기능을 담은 어드바이스를 추가한다. 여러 개를 추가할 수도 있다.
        Hello proxiedHello = (Hello)pfBean.getObject(); // FactoryBean이므로 getObject()로 생성된 프록시를 가져온다.
        }
      
      1
      2
      3
      4
      5
      6
      
        static class UppercaseAdvice implements MethodInterceptor {
        public Object invoke(MethodInvocation invocation) throws Throwable {
            String ret = (String)invocation.proceed(); // 리플렉션의 Method와 달리 메소드 실행 시 타깃 오브젝트를 전달한 필요가 없다.
            return ret.toUpperCase();
        }
        }
      
    • 어드바이스: 타깃이 필요 없는 순수한 부가기능
      • MethodInvocation은 일종의 콜백 오브젝트로, proceed() 메소드를 실행하면 타깃 오브젝트의 메소드를 내부적으로 실행해주는 기능이 있다.
      • ProxyFactoryBean은 작은 단위의 템플릿/콜백 구조를 응용해서 적용했기 때문에 템플릿 역할을 하는 MethodInvocation을 싱글톤으로 두고 공유할 수 있다.
      • addAdvice()를 통해 여러 개의 MethodInterceptor를 추가할 수 있다(ProxyFactoryBean 하나만으로 여러 개의 부가기능 제공 가능).
      • 어드바이스(advice): 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트
      • ProxyFactoryBean에 있는 인터페이스 자동검출 기능을 사용해 타깃 오브젝트가 구현하고 있는 인터페이스 정보를 알아낸 후, 알아낸 인터페이스르 모두 구현하는 프록시를 생성
      • 일부만 프록시에 적용하기를 원한다면 인터페이스 정보를 직접 제공해줘도 된다.
    • 포인트컷: 부가기능 적용 대상 메소드 선정 방법
      • 기존의 TxProxyFactoryBean
        • 부가기능을 가진 InvocationHandler가 타깃과 메소드 선정 알고리즘 코드에 의존하는 문제가 있다.
        • 타깃과 메소드 선정 알고리즘은 DI를 통해 분리할 수는 있지만 한번 빈으로 구성된 InvocationHandler 오브젝트는, 특정 타깃을 위한 프록시에 제한된다.
        • 결국 확장에는 유연하게 열려 있지 못하고 관련 없는 코드의 변경이 필요할 수 있는, OCP 원칙을 깔끔하게 잘 지키지 못하는 어정쩡한 구조이다.
      • ProxyFactoryBean 적용
        • 포인트컷(PointCut): 메소드 선정 알고리즘을 담은 오브젝트
        • 어드바이스와 포인트컷은 모두 싱글톤 빈으로 등록 가능
        • 스프링 proxyFactoryPattern을 이용한 방식
          1. 포인트컷에게 부가기능을 부여할 메소드인지 확인해달라고 요청
          2. 포인트컷에게 확인받으면, MethodInterceptor 타입의 어드바이스 호출
          3. Invocation 콜백
          4. 타깃 오브젝트에게 위임
      • 포인트컷을 함께 등록할 때는 어드바이스와 포인트컷을 Advisor 타입으로 묶어서 addAdvisor() 메소드 호출
      • 어드바이저: 어드바이스와 포인트컷을 묶은 오브젝트
  • 6.4.2 ProxyFactoryBean 적용
    • TransactionAdvice
      • 다이내믹 프록시가 제공하는 Method와는 달리 스프링의 MethodInvocation을 통한 타깃 호출은 예외가 포장되지 않고 타깃에서 보낸 그대로 전달
    • 스프링 XML 설정파일
    • 테스트
    • 어드바이스와 포인트컷의 재사용
      • ProxyFactoryBean은 스프링의 DI, 템플릿/콜백 패턴, 서비스 추상화 등의 기법이 모두 적용된 것이다.
      • 그래서 독립적이며, 여러 프록시가 공유할 수 있는 어드바이스와 포인트컷으로 분리 가능 -> 재사용 가능

6.5 스프링 AOP

  • 6.5.1 자동 프록시 생성
    • 부가기능의 적용이 필요한 티깃 오브젝트마다 거의 비슷한 ProxyFactoryBean 설정정보를 추가하는 문제가 있다.
    • 빈 후처리기를 이용한 자동 프록시 생성기
      • 빈 후처리기가 빈으로 등록되어 있으면 빈 오브젝트가 생성될 때마다 빈후처리기에 보내서 후처리 작업을 요청
      • 자동 프록시 생성 빈 후처리기: 스프링이 생성하는 빈 오브젝트의 일부를 프록시로 포장, 프록시를 빈으로 대신 등록 가능
      • 빈 후처리기는 빈으로 등록된 모든 어드바이저 내의 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지 확인
      • 프록시 적용 대상이면 내장된 프록시 생성기에게 현재 빈에 대한 프록시를 만들게 하고, 만들어진 프록시에 어드바이저 연결
    • 확장된 포인트컷
      1
      2
      3
      4
      
        public interface PointCut {
            ClassFilter getClassFilter(); // 프록시를 적용할 클래스인지 확인해준다.
            MethodMatcher getMethodMatcher(); // 어드바이스를 적용할 메소드인지 확인해준다.
        }
      
    • 포인트컷 테스트
      • 포인트컷이 클래스 필터까지 동작해서 클래스를 걸러벼리면 프록시를 적용했다고 해도 부가기능은 전혀 제공되지 않는다.
  • 6.5.2 DefaultAdvisorAutoProxyCreator의 적용
    • 클래스 필터를 적용한 포인트컷 작성
    • 어드바이저를 이용하는 자동 프록시 생성기 등록
    • 포인트컷 등록
    • 어드바이스와 어드바이저

      • 어드바이저를 이용하는 자동 프록시 생성기인 DefaultAdvisorAutoProxyCreator에 의해 자동수집되고, 프록시 대상 선정 과정에 참여하며, 자동생성된 프록시에 다이내믹하게 DI돼서 동작하는 어드바이저가 된다.
    • ProxyFactoryBean 제거와 서비스 빈의 원상복구

      • 명시적인 프록시 팩토리 빈을 등록하지 않기 때문에 원상복구한다.
    • 자동 프록시 생성기를 사용하는 테스트
      • 자동 프록시 생성기라는 스프링 컨테이너에 종속적인 기법을 사용했기 때문에 예외상황을 위한 테스트 대상도 빈으로 등록해야 한다.
      • TestUserService가 UserServiceTest 클래스 내부에 정의된 스태틱 클래스라는 문제가 있다.
      • 또한, ServiceImpl이라고 되어 있어 TestUserService는 프록시 적용 대상으로 선정되지 않는 문제가 있다.
        1
        2
        3
        4
        5
        6
        7
        8
        
          // 수정한 테스트용 UserService 구현 클래스
          static class TestUserServiceImpl extends UserServiceImpl { // 클래스 필터에 선정되도록 Impl로 이름 변경
          private String id = "madnite1";
          protected void upgradeLevel(User user) {
              if(user.getId().equal(this.id)) throw new TestUserServiceException();
              super.upgradeLevel(user);
          }
          }
        
    • 자동생성 프록시 확인
      • 트랜잭션이 필요한 빈에 트랜잭션 부가기능이 적용됐는가 확인
      • 아무 빈에나 트랜잭션 부가기능이 적용된 것은 아닌지 확인
  • 6.5.3 포인트컷 표현식을 이용한 포인트컷
    • 포인트컷 표현식

      • 포인트컷 표현식: 정규식이나 JSP의 EL과 비슷한 일종의 표현식 언어를 사용해서 포인트컷을 작성할 수 있도록 하는 방법
    • 포인트컷 표현식 문법

      • execution([접근제한자 패턴] 타입패턴 [타입패턴.]이름패턴 (타입패턴 | “..”, …)) [throws 예외 패턴]
      • ex) public int springbook.learningtest.spring.pointcut.Target.minus(int,int) throws java.lang.RuntimeException
        • public: 접근제한자, 생략 가능
        • int: 리턴 값의 타입을 나타내는 패턴, 생략 불가
        • springbook.learningtest.spring.pointcut.Target: 패키지와 타입 이름을 포함한 클래스의 타입 패턴, 생략 가능
        • minus: 메소드 이름 패턴, 필수
        • (int, int): 메소드 파라미터 패턴, 필수
        • throws java.lang.RuntimeException: 예외 타입 패턴, 생략 가능
      • 메소드 시그니처를 이용한 포인트컷 표현식 테스트
        1
        2
        3
        
          pointcut.setExpression("execution(public int " +
              springbook.learningtest.spring.pointcut.Target.minus(int,int) " +
              throws java.lang.RuntimeException");
        
      • 포인트컷 표현식 테스트

        • 필수가 아닌 항목 제외 -> execution(int minus(int, int))
      • 포인트컷 표현식을 이용하는 포인트컷 적용
        • 빈의 이름, 애노테이션 타입, 메소드, 파라미터에 적용되어 있는 것을 보고 메소드를 선정하게 하는 포인트컷 생성 가능
        • 포인트컷 표현식을 사용하면 로직이 짧은 문장에 담기기 떄문에 클래스나 코드를 추가할 필요가 없어서 코드와 설정이 모두 단순해진다.
        • 문자열로 된 표현식이므로 런타임 시점까지 문법의 검증이나 기능 확인이 되지 않는다는 단점이 있다.
      • 타입 패턴과 클래스 이름 패턴

        • 포인트컷 표현식에서 타입 패턴이라고 명시된 부분은 모두 동일한 원리가 적용된다.
  • 6.5.4 AOP랑 무엇인가?
    • 트랜잭션 서비스 추상화
    • 프록시와 데코레이션 패턴
    • 다이내믹 프록시와 프록시 팩토리 빈
    • 자동 프록시 생성 방법과 포인트 컷
    • 부가기능의 모듈화
      • 부가기능은 여타 핵심기능과 같은 레벨에서는 독립적으로 존재하는 것 자체가 불가능
    • AOP: 애스펙트 지향 프로그래밍
      • 애스펙트(aspect): 애플리케이션의 핵심기능을 담고 있지는 않지만, 애플리케이션을 구성하는 중요한 한 가지 요소이고, 핵심기능에 부가되어 의미를 갖는 특별한 모듈
      • 핵심기능을 담은 코드는 부가기능인 트랜잭션 코드와 함께 섞여 있어서 핵심기능인 사용자 관리 로직을 파악하고, 수정하고, 테스트하기가 불편
      • 그래서 애스펙트로 분리
      • 애스펙트 지향 프로그래밍(Aspect Oriented Programming): 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 독특한 모듈로 만들어서 설계하고 개발하는 방법
  • 6.5.5 AOP 적용기술
    • 프록시를 이용한 AOP
      • 독립적으로 개발한 부가기능 모듈을 다양한 타깃 오브젝트의 메소드에 다이내믹하게 적용해주는 것인 프록시이기 때문에 스프링 AOP는 프록시 방식의 AOP이다.
    • 바이트코드 생성과 조작을 통한 AOP
      • AspectJ는 타깃 오브젝트를 뜯어고쳐서 부가기능을 직접 넣어주는 방식의 AOP 사용
      • AspectJ에서 컴파일된 클래스 파일 수정이나 바이트코드 조작과 같은 방법을 사용하는 이유
        1. DI 컨테이너의 도움 없이 AOP를 적용할 수 있기 때문에
        2. 프록시 방식보다 훨씬 강력하고 유연한 AOP 가능
          • AOP의 핵심 메커니즘으로 사용하면 부가기능을 부여할 대상은 클라이언트가 호출할 때 사용하는 메소드로 제한됨
          • 하지만, 바이트코드를 직접 조작해서 AOP를 적용하면 오브젝트의 생성, 필드 값의 조회와 조작, 스태틱 초기화 등의 다양한 작업에 부가기능 부여 가능
  • 6.5.6 AOP 용어
    • 타깃: 부가기능을 부여할 대상
    • 어드바이스: 타깃에게 제공할 부가기능을 담은 모듈
    • 조인 포인트: 어드바이스가 적용될 수 있는 위치
    • 포인트컷: 어드바이스를 적용할 조인 포인트를 선별하는 작업 또는 그 기능을 정의한 모듈
    • 프록시: 클라이언트와 타깃 사이에 투명하게 존재하면서 부가기능을 제공하는 오브젝트
    • 어드바이저: 포인트컷과 어드바이스를 하나씩 갖고 있는 오브젝트
    • 애스펙트: 한 개 또는 그 이상의 포인트컷과 어드바이스의 조합으로 만들어지며 보통 싱글톤 형태의 오브젝트로 존재
  • 6.5.7 AOP 네임스페이스
    • 스프링의 프록시 방식 AOP를 적용하려면 최소한 4 가지 빈을 등록해야 한다.
      1. 자동 프록시 생성기
      2. 어드바이스
      3. 포인트컷
      4. 어드바이저
    • AOP 네임스페이스
      • <aop:config>, <aop:pointcut>, <aop:advisor> 를 정의해두면 빈이 자동 등록됨
      • 전용 태그를 사용해 정의하면 사용하기 편리하다.
    • 어드바이저 내장 포인트컷

6.6 트랜잭션 속성

  • 6.6.1 트랜잭션 정의
    • 트랜잭션 전파
      • 트랜잭션 전파: 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식
        • PROPAGATION_REQUIRED: 진행 중인 트랜잭션이 없으면 새로 시작, 이미 시작된 트랜잭션이 있으면 이에 참여
        • PROPAGATION_REQUIRES_NEW: 항상 새로운 트랜잭션 시작
        • PROPAGATION_NOT_SUPPORTED: 트랜잭션 없이 동작하도록 만든다. 진행 중인 트랜잭션이 있어도 무시
    • 격리수준
      • 서버환경에서 여러 개의 트랜잭션이 동시에 진행될 때, 가능한 한 많은 트랜잭션을 동시에 진행시키면서도 문제가 발생하지 않게 제어하기 위해 설정
      • ISOLATION_DEFAULT: DataSource에 설정되어 있는 디폴트 격리수준을 그대로 따른다.
    • 제한시간
      • 트랜잭션을 수행하는 제한시간 설정
    • 읽기 전용
      • 읽기전용으로 설정해두면 트랜잭션 내에서 데이터를 조작하는 시도를 막을 수 있다.
  • 6.6.2 트랜잭션 인터셉터와 트랜잭션 속성
    • TransactionInterceptor
      • 트랜잭션 정의를 메소드 이름 패턴을 이용해서 다르게 지정할 수 있는 방법 추가 제공
      • Properties 타입은 transactionAttributes로, 트랜잭션 속성을 정의한 프로퍼티
      • TransactionInterceptor가 예외처리 기본 원칙을 따르지 않는 경우가 있을 수 있기 때문에 rollbackOn()이라는 속성으로 기본 원칙과 다른 예외처리를 할 수 있게 해준다.
    • 메소드 이름 패턴을 이용한 트랜잭션 속성 지정
      • PROPAGATION_NAME, ISOLATION_NAME, readOnly, timeout_NNNN, -Exception1, +Exception2
      • 트랜잭션 전파 항목만 필수, 나머지는 생략 가능
    • tx 네임스페이스를 이용한 설정 방법
  • 6.6.3 포인트컷과 트랜잭션 속성의 적용 전략
    • 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용한다.
    • 공통된 메소드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의한다.
    • 프록시 방식 AOP는 같은 타깃 오브젝트 내의 메소드를 호출할 떄는 적용되지 않는다.
  • 6.6.4 트랜잭션 속성 적용
    • 트랜잭션 경계설정의 일원화
      • 비즈니스 로직을 담고 있는 서비스 계층 오브젝트의 메소드가 트랜잭션 경계를 부여하기에 적절한 대상
      • 테스트와 같은 특별한 이유가 아니고는 다른 계층이나 모듈에서 DAO에 직접 접근하는 것은 차단해야 한다.
    • 서비스 빈에 적용되는 포인트컷 표현식 등록
    • 트랜잭션 속성을 가진 트랜잭션 어드바이스 등록
    • 트랜잭션 속성 테스트

6.7 애노테이션 트랜잭션 속성과 포인트컷

  • 6.7.1 트랜잭션 애노테이션
    • @Transactional
      • 메소드, 클래스, 인터페이스에 사용 가능
    • 트랜잭션 속성을 이용하는 포인트컷
      • 트랜잭션 부가기능 적용 단위는 메소드
      • 메소드마다 @Transactional을 부여하고 속성을 지정할 수 있지만 지저분해짐
    • 대체 정책
      • 메소드의 속성을 확인할 때 타깃 메소드, 타깃 클래스, 선언 메소드, 선언 타입의 순서에 따라서 @Transactional이 적용됐는지 확인하고, 가장 먼저 발견되는 속성정보를 사용하게 하는 방법
    • 트랜잭션 애노테이션 사용을 위한 설정
      • <tx:annotation-driven/>
  • 6.7.2 트랜잭션 애노테이션 적용

6.8 트랜잭션 지원 테스트

  • 6.8.1 선언적 트랜잭션과 트랜잭션과 전파 속성
    • 선언적 트랜잭션과(declarative transaction): AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 하는 방법
    • 프로그램에 의한 트랜잭션(programmatic transaction): TransactionTemplate이나 개별 데이터 기술의 트랜잭션 API를 사용해 적접 코드 안에서 사용하는 방법
  • 6.8.2 트랜잭션 동기화와 테스트
    • 트랜잭션 매니저와 트랜잭션 동기화
      • PlatformTransactionManager 인터페이스를 구현한 트랜잭션 메니저를 통해 구체적인 트랜잭션 기술의 종류에 상관없이 일관된 트랜잭션 제어가 가능
      • 트랜잭션 동기화 기술이 있었기에 시작된 트랜잭션 정보를 저장소에 보관해뒀다가 DAO에서 공유할 수 있었다.
      • 클래스에 @AutoWired를 이용해 애플리케이션 컨텍스트에 등록된 빈을 가져와 테스트
      • 3개의 메소드가 실행되면 트랜잭션이 3개 생성됨
    • 트랜잭션 매니저를 이용한 테스트용 트랜잭션 제어
      • 세 개의 트랜잭션을 하나로 통합할 수 없을까?
      • 세 개의 메소드 모두 트랜잭션 전파 속성이 REQUIRED이니 이 메소드들이 호출되기 전에 트랜잭션이 시작되게만 한다면 가능
    • 트랜잭션 동기화 검증
      • 트랜잭션을 읽기전용으로 설정한 뒤 테스트
      • deleteAll()이 실행되지 않아 예외 발생 -> deleteAll() 메소드가 참여하고 있다는 확신을 얻음
      • JdbcTemplate과 같이 스프링이 제공하는 데이터 액세스 추상화를 적용한 DAO에도 동일한 영향
    • 롤백 테스트
      • 테스트 내의 모든 DB 작업을 하나의 트랜잭션 안에서 동작하게 하고 테스트가 끝나면 무조건 롤백해버리는 테스트
      • 롤백 테스트는 DB 작업이 포함된 테스트가 수행돼도 DB에 영향을 주지 않기 때문에 장점이 많다.
      • 롤백 테스트는 테스트를 진행하는 동안에 조작한 데이터를 모두 롤백하고 테스트를 시작하기 전 상태로 만들어주기 때문에 유용
  • 6.8.3 테스트를 위한 트랜잭션 애노테이션
    • @Transactional
    • @Rollback
      • 테스트용 @Transactional은 기본적으로 트랜잭션을 강제 롤백시키도록 설정되어 있다.
      • 트랜잭션을 커밋시켜서 테스트에서 진행한 작업을 그대로 DB에 반영하고 싶을 때 @Rollback(false) 사용
    • @TransactionConfiguraiton
      • @TransactionConfiguraiton을 사용하면 롤백에 대한 공통 속성을 지정할 수 있다.
    • NotTransactional과 Propagation.NEVER
      • @NotTransactional을 테스트 메소드에 부여하면 클래스 레벨의 @Transactional을 무시하고 트랜잭션을 시작하지 않은 채로 테스트를 진행
      • @NotTransactional 대신 전파 속성을 Propagation.NEVER로 설정 가능
    • 효과적인 DB 테스트
      • 단위 테스트와 통합 테스트는 클래스를 구분하는 것이 좋음
      • DB가 사용되는 통합 테스트를 별도의 클래스로 만들어둔다면 기본적으로 클래스 레벨에 @Transactional을 부여
This post is licensed under CC BY 4.0 by the author.