ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 6장 AOP (2)
    개발 스터디/책장 속 먼지 털기 - 토비의 스프링 2021. 1. 25. 08:22
    반응형

    책장속 먼지털기 스터디 11차
    스터디 날짜 : 2021.01.25
    작성 날짜 : 2021.01.24 - 2021.01.25
    페이지 : 475 - 512

    개요

    이전 장을 통해서 우리는 ProxyFactoryBeanAdvise, Pointcut을 적용하여, 핵심 기능에서 분리하여 부가 기능을 따로 깔끔하게 적용할 수 있도록 만들었다. 이번 장에서는 우리가 여태까지 해온 작업들을 돌아보고 Spring AOP를 이용해서 조금 더 "투명한 부가기능 형태"로 제공해보자. 여기서 투명하다라는 뜻은, 부가 기능을 적용한 후에도 기존, 설계와 코드에 영향을 주지 않는다는 뜻이다.

    프록시 팩토리 빈의 한계와 극복 방법

    지난 장에서 언급했던 프록시 팩토리 빈의 두 가지 한계를 다시 한 번 톺아보자. (여기서 프록시 팩토리 빈은 스프링의 ProxyFactoryBean을 말하는 것이 아니다.)

     

    1. 부가 기능이 다깃 오브젝트마다 새로 만들어진다.
    2. 한 번에 여러 개의 클래스에 공통 부가 기능을 제공할 수 없다.

    첫 번째 문제는 스프링에서 제공하는 ProxyFactoryBean의 어드바이스를 해결할 수 있었다. 하지만 여전히, 공통적으로 부가 기능이 필요한 객체마다 ProxyFactoryBean의 설정을 추가해주어야 한다는 문제는 여전히 남아 있다. 이를 어떻게 극복할 수 있을까?

     

    이렇게 코드가 중복되었을 때 우리는 어떤 방식으로 해결했었는지 잠깐 살펴보자.

     

    1. 전략 패턴과 DI를 이용하여 템플릿과 콜백, 클라이언트를 분리 (ex) UserDao )
    2. Dynamic ProxyDynamic Proxy 생성 팩토리를 DI (ex) 이전 장에서 한 작업들)

    위 방법들 중 한 번에 여러 개의 빈에 프록시를 적용할 수 때 설정 코드의 중복을 제거할 수 있는 방법은 아쉽게도 없다. 어떻게 해결할 수 있을까?

     

    정답부터 말하자면, 스프링 컨테이너는 다양하게 확장할 수 있는 방법이 존재한다. 이 중 BeanPostProcessor를 이용해서 빈 객체가 만들어진 후에 그 객체를 개발자 입맛에 맞게 다시 가공할 수 있다. 이 BeanPostProcessor와 더불어서, DefaultAdvisorAutoProxyCreator를 이용해서, 빈 객체 생성 후에, 어드바이저 적용 여부에 따라 프록시를 자동으로 설정해주면 위의 문제를 해결할 수 있다.

    Pointcut 더, 자세히!

    여기서 한 가지 짚고 가자. 사실 Pointcut은 크게 2가지 기능이 있다. 다음은 실제 Pointcut 인터페이스의 코드이다.

    public interface Pointcut {
        ClassFilter getClassFilter();
        MethodMatcher getMethodMatcher();
        // ...
    }

     

    이전 장에서 우리가 사용했던 것은 getMethodMatcher이다. 메소드 이름 별로 어드바이스를 적용할 지 여부를 판단하는 메소드이다. getClassFilter는 클래스 이름으로 어드바이스를 적용할 지 여부를 판단한다. DefaultAdvisorAutoProxyCreator와 결합되어 사용될 때, 클래스와 메소드 선정 알고리즘을 모두 갖게끔 만들어서 이전 절에서 얘기했던 문제를 해결할 수 있는 것이다.

    이제 Pointcut에 대하여 학습 테스트를 작성해보자.

     

    PointCutStudyTest.java

    public class PointcutStudyTest {
        @Test
        @DisplayName("포인트 컷 확장 테스트")
        public void test_point_cut() {
            NameMatchMethodPointcut classMethodPointcut = new NameMatchMethodPointcut(){
                @Override
                public ClassFilter getClassFilter() {
                    return clazz -> isStartedWithHelloT(clazz);
                }
            };
            classMethodPointcut.setMappedName("sayH*");
    
            checkAdvised(new HelloTarget(), classMethodPointcut, HelloTarget.class);
    
            class HelloWorld extends HelloTarget {}
            checkAdvised(new HelloWorld(), classMethodPointcut, HelloWorld.class);
    
            class HelloToby extends HelloTarget {}
            checkAdvised(new HelloToby(), classMethodPointcut, HelloToby.class);
        }
    
        private boolean isStartedWithHelloT (Class<?> clazz) {
            return clazz.getSimpleName().startsWith("HelloT");
        }
    
        private void checkAdvised(Object target, Pointcut pointcut, Class<?> clazz) {
            ProxyFactoryBean pfBean = new ProxyFactoryBean();
            pfBean.setTarget(target);
            pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UpperCaseAdvise()));
            Hello proxy = (Hello) pfBean.getObject();
    
            if (isStartedWithHelloT(clazz)) {
                assertEquals(("Hello Toby").toUpperCase(), proxy.sayHello("Toby"));
                assertEquals(("Hi Toby").toUpperCase(), proxy.sayHi("Toby"));
                assertEquals("Thank you Toby", proxy.sayThankU("Toby"));
            } else {
                assertEquals("Hello Toby", proxy.sayHello("Toby"));
                assertEquals("Hi Toby", proxy.sayHi("Toby"));
                assertEquals("Thank you Toby", proxy.sayThankU("Toby"));
            }
    
        }
    }

     

    테스트를 돌려보면 무사히 통과한다. 어떤 테스트냐면, NameMatchMethodPointcut을 생성하는데, getClassFilter를 커스터마이징해서 만든다. 클래스 이름이 "HelloT"로 시작해야 어드바이스를 적용한다. 그 후 적용 여부에 따라 적정 결과 값을 내놓는지 checkAdvised를 호출하여 확인한다. 이제 Pointcut을 이용해서 클래스, 메소드 이름 별로 어드바이스를 적용할 수 있다는 확신이 생겼다. 다음 절에서 프록시 팩토리 빈의 한계를 완벽하게 극복해보자.

    DefaultAdvisorAutoProxyCreator 적용과 테스트

    먼저 DefaultAdvisorAutoProxyCreator를 빈으로 등록한다.

     

    TestBeanFactory.java(BeanFactory.java도 같은 요령으로 업데이트)

    @TestConfiguration
    public class TestBeanFactory {
        @Bean
        public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
            return new DefaultAdvisorAutoProxyCreator();
        }
    
        // ...
    }

     

    그 후, DefaultAdvisorAutoProxyCreator에서 생성한 빈에 대해서 프록시 적용 여부를 결정하는 Pointcut을 만들어야 한다. 다음과 같이 작성한다.

     

    NameMatchClassMethodPointcut.java

    public class NameMatchClassMethodPointcut extends NameMatchMethodPointcut {
        public void setMappedClassName(String className) {
            this.setClassFilter(new SimpleClassFilter(className));
        }
    
        @RequiredArgsConstructor
        static class SimpleClassFilter implements ClassFilter {
            private final String className;
    
            @Override
            public boolean matches(Class<?> clazz) {
                return PatternMatchUtils.simpleMatch(className, clazz.getSimpleName());
            }
        }
    }

     

    이제 이 Pointcut을 빈으로 등록해야 한다. 이전 testTransactionPointCut을 앞서 만든 포인트 컷 기반으로 빈을 만들 수 있도록 바꿔준다.

     

    TestBeanFactory.java(BeanFactory.java도 같은 요령으로 업데이트)

    @TestConfiguration
    public class TestBeanFactory {
        // ...
    
        @Bean
        public Pointcut testTransactionPointCut() {
            NameMatchClassMethodPointcut pointcut = new NameMatchClassMethodPointcut();
            pointcut.setMappedClassName("*Service");
            pointcut.setMappedName("upgrade*");
            return pointcut;
        }
    
        // ...
    }

     

    뒤에 "Service"가 붙은 클래스에 앞에 "upgrade"가 붙은 메소드가 호출되면, 빈마다 프록시를 적용하게 된다. 기존 어드바이스와 어드바이저는 변경할 필요는 없다. 그 후, 기존 ProxyFactoryBean 타입으로 만들었던 testUserService빈을 다시 UserService 타입으로 변환시킨다.

     

    TestBeanFactory.java(BeanFactory.java도 같은 요령으로 업데이트)

    @TestConfiguration
    public class TestBeanFactory {
        // ...
        @Bean
        public UserService testUserService() {
            UserService userService = new UserServiceImpl(testMailSender(), testUserDao());
            return userService;
        }
    
        // ...
    }

     

    이제 이에 대한 테스트를 만들어보자. UserServiceTest를 다음과 같이 변경한다.

     

    UserServiceTest.java

    @SpringBootTest
    @DirtiesContext
    @Import(TestBeanFactory.class)
    class UserServiceTest {
        // ...
    
        @Autowired
        private UserService testUserService;
    
        // ... 
    
        @Test
        @DisplayName("컨테스트 로드 테스트")
        public void bean() {
            assertNotNull(testUserService);
        }
    
        private void checkLevel(User user, boolean isUpgrade) {
            User update = testUserDao.get(user.getId());
            if (isUpgrade) {
                assertEquals(user.getLevel().getNext(), update.getLevel());
            } else {
                assertEquals(user.getLevel(), update.getLevel());
            }
        }
    
        @Test
        @DisplayName("레벨 업 테스트")
        public void test_level_upgrade() {
            DummyMailSender dummyMailSender = (DummyMailSender) testMailSender;
            List<String> requests = dummyMailSender.getRequests();
            requests.clear();
    
            testUserService.upgradeLevels();
    
            checkLevel(users.get(0), false);
            checkLevel(users.get(1), true);
            checkLevel(users.get(2), false);
            checkLevel(users.get(3), true);
            checkLevel(users.get(4), false);
    
            assertEquals(2, requests.size());
            assertEquals(users.get(1).getEmail(), requests.get(0));
            assertEquals(users.get(3).getEmail(), requests.get(1));
        }
    
        @Test
        @DisplayName("생성 테스트")
        public void test_add() {
            User existLevelUser = User.builder()
                    .id("test6")
                    .name("test6")
                    .password("test6")
                    .email("test6@test.com")
                    .level(Level.GOLD)
                    .login(60)
                    .recommend(31)
                    .build();
            testUserService.add(existLevelUser);
            User saved = testUserDao.get(existLevelUser.getId());
            assertEquals(existLevelUser.getLevel(), saved.getLevel());
    
            User notExistLevelUser = User.builder()
                    .id("test7")
                    .name("test7")
                    .password("test7")
                    .email("test7@test.com")
                    .build();
            testUserService.add(notExistLevelUser);
            saved = testUserDao.get(notExistLevelUser.getId());
            assertEquals(Level.BASIC, saved.getLevel());
        }
    
        // test_cancel_when_exception은 주석 처리한다.
    }

     

    정상 동작한다. 그러나 아직 해결할게 남았다. 강제로 예외를 발생시키는 테스트를 사용했을 때를 테스트하는 것이다. 조금 바꿔야할 것이 많다. 이제 예외 발생 시, 트랜잭션 롤백이 일어나는 것에 대해서 테스트하기 위해서는 TestUserService를 빈으로 등록해야만 한다. 다음과 같이 만든다.

     

    TestUserService.java

    @Service
    public class TestUserService extends UserServiceImpl implements UserService {
        private String id;
    
        public TestUserService(MailSender mailSender, UserDao userDao) {
            super(mailSender, userDao);
        }
    
        public void setId(String id) {
            this.id = id;
        }
    
        public void upgradeLevel(User user) {
            if (id.equals(user.getId())) {
                throw new TestUserServiceException();
            }
    
            super.upgradeLevel(user);
        }
    }

     

    그리고 빈으로 설정해준다.

     

    TestBeanFactory.java(BeanFactory.java도 같은 요령으로 업데이트)

    @TestConfiguration
    public class TestBeanFactory {
        // ...
    
        @Bean
        public UserService testTestUserService() {
            TestUserService userService = new TestUserService(testMailSender(), testUserDao());
            userService.setId("test2");
            return userService;
        }
    
        // ...
    }

     

    그리고 UserServiceTest를 다음과 같이 변경한다.

     

    UserServiceTest.java

    @SpringBootTest
    @DirtiesContext
    @Import(TestBeanFactory.class)
    class UserServiceTest {
        // ...
    
        @Autowired
        private UserService testTestUserService;
    
        // ...
    
        @Test
        @DisplayName("예외 발생 시 작업 취소 여부 테스트")
        public void test_cancel_when_exception() {
            assertThrows(Exception.class, () -> testTestUserService.upgradeLevels());
            checkLevel(users.get(0), false);
            checkLevel(users.get(1), false);
            checkLevel(users.get(2), false);
            checkLevel(users.get(3), false);
            checkLevel(users.get(4), false);
        }
    }

     

    테스트를 돌려보면 아쉽게도 실패한다. 이는 스프링 5.1 이상 버전부터 빈 설정에 대해서 제한 기준(재정의에 대한)이 엄격하게 바뀌었기 때문이다. 이를 해결하기 위해서는 일단 application.properties에 다음과 같이 스프링 설정을 해주어야 한다.

     

    application.properties

    spring.main.allow-bean-definition-overriding=true

     

    그 후 빈 설정 파일을 다음과 같이 변경한다.

     

    TestBeanFactory.java(BeanFactory.java도 같은 요령으로 업데이트)

    @TestConfiguration
    public class TestBeanFactory {
        // ...
    
        @Bean("testUserService")
        public UserService testUserService() {
            UserService userService = new UserServiceImpl(testMailSender(), testUserDao());
            return userService;
        }
    
        @Bean("testTestUserService")
        public UserService testTestUserService() {
            TestUserService userService = new TestUserService(testMailSender(), testUserDao());
            userService.setId("test2");
            return userService;
        }
    
        // ...
    }

     

    이제 테스트를 돌려보면 무사히 통과하는 것을 확인할 수 있다.

    참고!

    책에서는 자동 프록시에 대한 테스트가 있었으나 현재는 트랜잭션이 걸릴지라도, 이에 대한 테스트를 작성할 수 없기 때문에 일단은 스킵합니다. "com.sun.proxy.$Proxy"가 나오는데 이유를 알 수 없군요..

    Pointcut 표현식과 테스트

    이번에는 조금 더 편리한 포인트컷 작성 방법에 대해 알아본다. 포인트컷은 정규식이나, 스프링 표현식 같이 포인트컷만의 "표현식"이 따로 존재한다. 이 기능을 사용하려면 AspectJExpressionPointcut을 사용하면 된다. 먼저 이를 학습하기 위해 학습 테스트를 만들어보자. 먼저 다음 클래스들을 생성한다.

     

    TargetInterface.java

    public interface TargetInterface {
        void hello();
        void hello(String a);
        int minus(int a, int b) throws RuntimeException;
        int plus(int a, int b);
        void method();
    }

    Target.java

    public class Target implements TargetInterface {
        @Override
        public void hello() {
    
        }
    
        @Override
        public void hello(String a) {
    
        }
    
        @Override
        public int minus(int a, int b) throws RuntimeException {
            return 0;
        }
    
        @Override
        public int plus(int a, int b) {
            return 0;
        }
    
        @Override
        public void method() {
    
        }
    }

    Bean.java

    public class Bean {
        public void method() throws RuntimeException {
    
        }
    }

     

    그 후 테스트 클래스를 생성한 후 다음을 테스트를 작성한다.

     

    TargetTest.java

    public class TargetTest {
        @Test
        @DisplayName("Pointcut Expression Test")
        public void test_pointcut_expression() throws Exception {
            isMatchPointcut(Target.class, "minus", int.class, int.class);
            isMatchPointcut(Target.class, "plus", int.class, int.class);
            isMatchPointcut(Target.class, "hello");
            isMatchPointcut(Target.class, "hello", String.class);
            isMatchPointcut(Target.class, "method");
            isMatchPointcut(Bean.class, "method");
        }
    
        private boolean isMatchPointcut(Class<?> clazz, String methodName, Class<?>... args) throws NoSuchMethodException {
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
            pointcut.setExpression("execution(* *(..))");
            return pointcut.getClassFilter().matches(clazz) && pointcut.getMethodMatcher().matches(clazz.getMethod(methodName, args), null);
        }
    }

     

    이 때 이 테스트는 빌드되지 않는다. 다른 의존성이 필요하기 때문이다. build.gradle에 다음을 추가한다.

     

    build.gradle

    // ...
    
    dependencies {
        // ..
        implementation 'org.springframework.boot:spring-boot-starter-aop'
    
        // ..
    }
    
    // ...

     

    그 후 테스트를 돌려보면 무사히 통과하는 것을 알 수 있다. 간단히 설명하자면, AspectJExpressionPointcutsetExpression 메소드를 통해서 표현식을 전달할 수 있다. 이때 표현식은 다음과 같은 형태로 전달한다.

     

    execution([접근 제한자 패턴] 리턴 타입 패턴 [패키지 패턴.].메소드 이름 패턴 (파라미터 타입 패턴 | ..) [예외 이름 패턴])

     

    조금 어려울 수 있다. 한 번 리플렉션 API를 이용해서 Target.minus의 정보를 출력해보자.

    System.out.println(Target.class.getMethod("minus", int.class, int.class));

     

    그럼 결과는 다음과 같다.

    public int com.gurumee.chonangam.study.Target.minus(int,int) throws java.lang.RuntimeException

     

    하나하나 살펴보자. 첫 번째는 접근 제한자이다. 두 번째는 리턴 타입, 세 번째는 클래스의 패키지(com.gurumee.chonangam.study.Target.)를 나타낸다. 그리고 네 번째는 메소드 이름 패턴이다. 다섯 번째는 파라미터 타입 목록을 나타낸다. 그리고 여섯 번째는 예외 이름에 대한 타입 패턴이다.

     

    AspectJExpressionPointcut.setExpression에 전달할 표현식과 일치하는 것을 확인할 수 있다. 이제 이를 이용해서, 빈으로 작성했던 포인트컷을 조금 더 업그레이드 한다.

     

    TestBeanFactory.java(BeanFactory.java도 같은 요령으로 업데이트)

    @TestConfiguration
    public class TestBeanFactory {
        // ...
        @Bean
        public Pointcut testTransactionPointCut() {
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
            pointcut.setExpression("execution(* *..*Service*.upgrade*(..))");
            return pointcut;
        }
        //...
    }

    포인트컷 표현식으로, 클래스 이름에 Service가 있고 메소드 명이 upgrade로 시작하는 메소드가 호출되면 프록시가 생성된다. 그 외 코드는 건드리지 않는다. 역시 테스트를 돌려보면 통과하는 것을 확인할 수 있다. 조금 더 깔끔한 포인트 컷이 되었다.

    AOP 재 정리

    이제 복습 차원에서 6장에서 했던 내용들을 재 정리해보자. 현재 우리는 트랜잭션이란 부가 기능을 추가하기 위해서 다음과 같은 작업을 진행하였다.

     

    1. 트랜잭션 서비스 추상화
    2. 프록시와 데코레이터 패턴
    3. 다이나믹 프록시와 프록시 팩토리 빈
    4. 자동 프록시 생성 방법과 포인트 컷

    이러한 일련의 과정을 거친 덕분에 TransactionAdvice라는 이름으로 트랜잭션 경계를 만드는 부가 기능을 핵심 기능과 분리해낼 수 있었다. 이를 "부가 기능의 모듈화"라고 한다. 이러한 부가적인 기능을 모듈로 만들어서 설계하고 개발하는 방법을 AOP(Aspect Oriented Programming)이라고 한다.

     

    AOP는 크게 2가지 방식으로 나눌 수 있다.

     

    1. 프록시를 이용한 AOP
    2. 바이트코드 생성과 조작을 통한 AOP

    우리가 여지껏 해왔던 방법이 1번 AOP이다. 스프링 기준에서 보면 IoC 컨테이너와 다이나믹 프록시, 데코레이터 패턴, 프록시 패턴, 자동 프록시 생성 기법, 빈 오브젝트의 후처리 조작 기법 등 다양한 기술을 조합해서 AOP를 구현한다. 2번의 경우 대표적인 라이브러리는 AspectJ이다. 바이트 코드를 이용하는 것은 DI 같은 기법 없이도 AOP를 적용할 수 있으며 강력하고 유연하며 깔끔한 코드 작업을 할 수 있게 도와준다. 다만 설정하는데 꽤 난이도가 있으므로 여태까지 정도의 부가 기능 모듈화라면, 1번으로도 충분하다. 다음은 용어 정리이다.

     

    • Target : 부가 기능을 부여할 대상이다.
    • Advice : 부가 기능이다.
    • Join Point : 어드바이스가 적용될 위치를 나타낸다.
    • Pointcut : 어드바아스를 적용할 조인 포인트를 선별하는 작업 혹은 그 기능을 정의한 모듈을 뜻한다.
    • Proxy : 클라이언트와 타깃 사이에 투명하게 존재하면서 부가 기능을 제공하는 오브젝트를 뜻한다.
    • Advisor : 포인트컷과 어드바이스를 하나씩 갖고 있는 오브젝트이다. 어떤 부가 기능(어드바이스)을 어떻게 설정(포인트 컷)할지 알고 있다.
    • Aspect : OOP의 클래스처럼 에스펙트는 AOP의 기본 모듈이다. 한 개 이상의 포인트컷과 어드바이스의 조합으로 만들어지며 보통 실글톤 형태의 오브젝트로 존재한다.

    AOP 네임스페이스

    이제 조금 더 깔끔하게 AOP를 설정해보자. xml 기반으론 AOP 네임스페이스라는 것이 존재하는데, 자바 기반 설정으로 한 번 AOP 설정을 분리해보자. 먼저 AppConfig를 다음과 같이 생성한다.

     

    AppConfig.java

    @Configuration
    @EnableAspectJAutoProxy
    public class AppConfig {
    }

    눈에 띄는 설정이 있다. @EnableAspectJAutoProxy라는 애노테이션이 붙어있는 것을 볼 수 있는데 이렇게 설정해두면 자동으로 빈 생성 후 DefaultAdvisorAutoProxyCreator를 이용하 듯 애플리케이션에 설정된 어드바이스에 따라 프록시를 설정하게 된다. 이제 따로 에스펙트를 만들어주어야 한다. TransactionAspect를 다음과 같이 생성한다.

     

    TransactionAspect.java

    @Aspect
    @Component
    @RequiredArgsConstructor
    public class TransactionAspect {
        private final PlatformTransactionManager transactionManager;
    
        @Around("execution(* *..*Service*.upgrade*(..))")
        public Object invoke(ProceedingJoinPoint jp) throws Throwable {
            TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    
            try {
                Object ret = jp.proceed();
                transactionManager.commit(status);
                return ret;
            } catch (InvocationTargetException e) {
                transactionManager.rollback(status);
                throw e;
            }
        }
    }

    여기서 중요한 것은 @Aspect이다. 이는 이 빈이(@Component가 붙었으니까..), 에스펙트임을 알려준다. 따라서 애플리케이션은 빈 생성 후, 이 에스펙트에 정의된 포인트컷에 맞는 빈이 있으면 프록시를 만들어준다.

     

    @Around("execution(* *..*Service*.upgrade*(..))")는 이 에스펙트에서 정의된 어드바이스이다. 이 때 인수로 포인트컷 표현식을 받는데 해당 표현식은 위에 설명했으니 넘어간다. @Around 애노테이션은 메소드 전반적으로 에스펙트가 가지고 있는 어드바이스를 적용한다. 따라서, 트랜잭션에 걸맞는 어드바이스라고 볼 수 있다. 이외에도, After, Before 등 여러 어드바이스 애노테이션이 존재한다.

     

    이제 빈 설정 파일에서 여지껏 작성했던 어드바이스, 어드바이저, 포인트컷 등을 제거한다.

     

    TestBeanFactory.java(BeanFactory.java도 같은 요령으로 업데이트)

    @TestConfiguration
    public class TestBeanFactory {
    //    @Bean
    //    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    //        return new DefaultAdvisorAutoProxyCreator();
    //    }
    
    //    @Bean
    //    public TransactionAdvice testTransactionAdvice() {
    //        return new TransactionAdvice(testTransactionManager());
    //    }
    //
    //    @Bean
    //    public Pointcut testTransactionPointCut() {
    //        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    //        pointcut.setExpression("execution(* *..*Service*.upgrade*(..))");
    //        return pointcut;
    //    }
    //
    //    @Bean
    //    public DefaultPointcutAdvisor testTransactionAdvisor() {
    //        return new DefaultPointcutAdvisor(testTransactionPointCut(), testTransactionAdvice());
    //    }
    
        //...
    }

    그 후 테스트를 돌려보면 무사히 이전처럼 트랜잭션까지 적용되는 것을 확인할 수 있다.

Designed by Tistory.