일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 페치조인
- jQueryUI
- 스프링데이터흐름
- JPQL
- 제너릭
- springflow
- 자바서블릿
- fetchjoin
- 벌크연산
- calendar
- 제네릭
- joinfetch
- jscalendar
- jQuery값전송
- values()
- paging
- fullcalendar
- 페이징
- 프로젝트생성
- javascriptcalendar
- namedQuery
- Hibernate
- JPA
- javaservlet
- Generic
- 엔티티직접사용
- JQuery
- LIST
- 대량쿼리
- jQuery값전달
- Today
- Total
가자공부하러!
토비의스프링(6) - AOP 본문
github : https://github.com/HyeongJunMin/tobyspring/blob/6-AOP/src/main/resources/md-files/Ch6AOP.md
#6장. AOP
요약 및 결론
책 내용
AOP는 스프링 3대 기반기술 중 하나이다.(IoC/DI, 서비스 추상화)
OOP를 대체하려는 것 처럼 보이는 AOP의 등장배경과 스프링이 이것을 도입한 이유, 장점을 이해해야 한다.
1. 등장배경 : 여러 핵심기능들에 중복되는 부가기능들이 있었음 2. 이유 : 부가기능들을 깔끔하게 뽑아내고 싶었음 3. 장점 : 중복 없이 독립적인 모듈로 구분해냈음. 핵심기능은 순수하게 그 기능을 담은 코드로만 존재하고 독립적으로 살펴볼 수 있도록 구분된 면에 존재하게 됨
어드바이스 : 스프링에서 타깃 객체에 적용할 부가기능을 담은 객체
포인트컷 : 메서드 선정 알고리즘을 담은 객체
서비스 계층을 트랜잭션이 시작되고 종료되는 경계로 정했다면, 테스트와 같은 특별한 이유가 아니고는 다른 계층이나 모듈에서 DAO에 직접 접근하는 것은 차단해야 한다.
핵심 : 504쪽 AOP: 애스펙트 지향 프로그래밍
Aspect : 그 자체로 애플리케이션의 핵심 기능을 담고 있지는 않지만, 애플리케이션을 구성하는 중요한 한 가지 요소이고 핵심기능에 부가되어 의미를 갖는 특별한 모듈을 가리킨다.
1. 트랜잭션 코드의 분리
- 코드를 정리해본다 :
- 한 메서드가 하나의 책임만 갖도록 : UserService.upgradeLevelsInternal()
- 한 클래스가 하나의 책임만 갖도록 : UserService에서 트랜잭션 경계설정 코드
- 메서드 분리
- upgradeLevels()에서 트랜잭션 경계설정 코드와 비즈니스 로직 코드를 분리한다.
코드 접기/펼치기
// 트랜잭션 경계설정 코드만 남은 메서드 public void upgradeLevels() { TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); try { upgradeLevelsInternal(); transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw e; } } // upgradeLevels에서 비즈니스로직만 분리 private void upgradeLevelsInternal() { List
userList = userDao.getAll(); userList.forEach(user -> { if (canUpgradeLevel(user)) { upgradeLevel(user); } }); }
- DI를 이용한 클래스의 분리
- 트랜잭션을 담당하는 코드는 UserService에 있을 필요가 없으니 클래스 밖으로 뽑아낸다.
- UserService에 인터페이스를 도입하고 구현클래스를 2개 둔다(UserServiceImpl, UserServiceTx)
- UserServiceTx는 트랜잭션 경계설정에 대한 책임만 갖게된다.
- UserServiceTx는 비즈니스 로직을 전혀 갖지 않고 다른 UserService 구현 오브젝트에 위임만 한다.
- 트랜잭션이 적용된 UserServiceTx
코드 접기/펼치기
@RequiredArgsConstructor public class UserServiceTx implements UserService { private final UserService userService; private final PlatformTransactionManager transactionManager; ... public void upgradeLevels() { TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); try { userService.upgradeLevels(); transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw e; } } ... }
- 트랜잭션 경계설정 코드 분리의 장점
- UserServiceImpl은 트랜잭션 내용을 신경쓰지 않아도 된다.
- 비즈니스 로직에 대한 테스트를 손쉽게 만들어낼 수 있다.(다음절에서 확인)
- @Transactional이 더 편리해 보인다.
2. 고립된 단위 테스트
- 가장 편하고 좋은 테스트 방법은 가능한 한 작은 단위로 쪼개서 테스트하는 것이다.
- 테스트가 실패했을 때 원인을 찾기 쉽다.
- 복잡한 의존관계 속의 테스트
- UserService는 꽤 복잡하다
- UserService 뒤에 존재하는 훨씬 더 많은 객체, 환경, 서비스, 서버, 네트워크까지 테스트 하는 셈이다.
- 테스트 대상 객체 고립시키기
- 그래서
- 외부 요소들에 영향을 받지 않도록 고립시킬 필요가 있다.
- 장점
- 다른 요소를 배제한 UserService의 기능만을 테스트할 수 있다.
- 다른 요소들에 필요한 자원들을 소모하지 않으므로 수행 성능이 좋다.(속도 향상)
- 테스트를 위한 UserServiceImpl 고립
- MockUserDao, MockMailSender 사용하면 됨
- 고립된 단위테스트 활용
- upgradeLevels()에 적용
- UserDao 목 객체
- UserServiceTest전용이므로 스태틱 내부 클래스로 선언
코드 접기/펼치기
@Test public void upgradeLevels() throws Exception { // 고립된 테스트이므로 대상 객체를 직접 생성한다. UserServiceImpl userService = new UserServiceImpl(); // 목 객체를 set해준다. MockUserDao mockUserDao = new MockUserDao(this.userList); userService.setUserDao(mockUserDao); MockMailSender mockMailSender = new MockMailSender(); userService.setMailSender(mockMailSender); // 목 객체를 가진 UserService의 메서드를 수행한다. userService.upgradeLevels(); // UserService 결과가 담긴 목 객체의 값들을 확인한다. List
updated = mockUserDao.getUpdated(); assertThat(updated.size()).isEqualTo(3); checkUserAndLevel(updated.get(0), userList.get(1).getId(), Level.SILVER); checkUserAndLevel(updated.get(1), userList.get(3).getId(), Level.GOLD); checkUserAndLevel(updated.get(2), userList.get(4).getId(), Level.GOLD); List requests = mockMailSender.getRequests(); assertThat(requests.size()).isEqualTo(3); assertThat(requests.get(0)).isEqualTo(userList.get(1).getEmail()); assertThat(requests.get(1)).isEqualTo(userList.get(3).getEmail()); assertThat(requests.get(2)).isEqualTo(userList.get(4).getEmail()); }
- 그래서
- 단위 테스트와 통합 테스트
- 이 책에서 단위 테스트 : 의존 오브젝트나 외부 리소스를 사용하지 않도록 고립시킨 테스트
- 이 책에서 통합 테스트 : 외부의 DB, 파일 등의 리소스가 참여하는 테스트
- 선택 가이드라인
- 항상 단위테스트 먼저 고려
- 외부리소스를 사용해야만 하는 경우에는 통합 테스트
- 단위 테스트를 만들기가 너무 복잡하면 처음부터 통합테스트를 고려해본다.
- 목 프레임워크
- Mockito 프레임워크
- 특징 : 목 클래스를 준비할 필요 없음
- 오... 좋은데
- 생성자 의존성 주입으로 바꾸면 더 좋겠다.
- 좋은데 앞으로 안쓴단다. 왜안쓰지;;
코드 접기/펼치기
@Test public void upgradeLevelsWithMockito() { UserServiceImpl userService = new UserServiceImpl(); UserDao mockUserDao = mock(UserDao.class); when(mockUserDao.getAll()).thenReturn(this.userList); userService.setUserDao(mockUserDao); MailSender mockMailSender = mock(MailSender.class); userService.setMailSender(mockMailSender); userService.upgradeLevels(); // 목 객체가 제공하는 검증 기능 활용 // 어떤 메서드가 몇 번 호출됐는지, 파라미터는 무엇인지 확인할 수 있다. verify(mockUserDao, times(3)).update(any(User.class)); verify(mockUserDao).update(userList.get(1)); assertThat(userList.get(1).getLevel()).isEqualTo(Level.SILVER); verify(mockUserDao).update(userList.get(3)); assertThat(userList.get(3).getLevel()).isEqualTo(Level.GOLD); verify(mockUserDao).update(userList.get(4)); assertThat(userList.get(4).getLevel()).isEqualTo(Level.GOLD); // 파라미터를 정밀하게 검사하기 위해 캡처 ArgumentCaptor
mailMessageArgs = ArgumentCaptor.forClass(SimpleMailMessage.class); verify(mockMailSender, times(3)).send(mailMessageArgs.capture()); List mailMessages = mailMessageArgs.getAllValues(); assertThat(mailMessages.get(0).getTo()[0]).isEqualTo(userList.get(1).getEmail()); assertThat(mailMessages.get(1).getTo()[0]).isEqualTo(userList.get(3).getEmail()); assertThat(mailMessages.get(2).getTo()[0]).isEqualTo(userList.get(4).getEmail()); }
- Mockito 프레임워크
3. 다이내믹 프록시와 팩토리 빈
프록시와 프록시 패턴, 데코레이터 패턴
- 프록시
- 클라이언트는 인터페이스만 보고 사용하기 때문에 핵심기능을 갖는 클래스(타깃)를 사용할 것으로 기대하지만 실제적으로는 부가기능(프록시)을 통해 핵심기능을 사용하는 구성
- 클라이언트 -> 프록시 -> 타깃
- 프록시 : 마치 자신이 실제 대상인 것처럼 위장해서 클라이언트의 요청을 대리인처럼 대신 받아줌
- 타깃 또는 실체 : 프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 객체
- 프록시의 목적
- 클라이언트가 타깃에 접근하는 방법을 제어하기 위함
- 타깃에 부가적인 기능을 부여해주기 위함
- 데코레이터 패턴
- 타깃에 부가적인 기능을 런타임 시점에 동적으로 부여해주기 위해 프록시를 사용하는 패턴
- 동적인 이유 : 컴파일 시점에는 프록시와 타깃의 관계가 정해져있지 않기 때문
- 데코레이터인 이유 : 여러 겹의 포장지와 장식처럼 실제 내용물에는 변함 없지만 부가적인 효과를 부여하기 때문
- 대표적인 예 : InputStream(내용물) - FileInputStream(부가기능 + 내용물)
- 예제에서 UserService와 UserServiceTx도 데코레이터 패턴
- 프록시 패턴
- 일반적으로 사용하는 프록시와 디자인 패턴 프록시 패턴에서의 프록시는 구분이 필요하다.
- 전자 : 클라이언트와 사용 대상 사이에 대리 역할을 맡은 객체를 두는 방법을 총칭
- 후자 : 프록시를 사용하는 방법 중 타깃에 대한 접근 방법을 제어하려는 목적을 가진 경우
- 프록시 패턴에서 프록시는
- 타깃의 기능을 확장하거나 추가하지 않는다.
- 클라이언트가 타깃에 접근하는 방식을 변경해준다.
타깃 객체를 생성하기가 복잡하거나 당장 필요하지 않은 경우에는 꼭 필요한 시점까지 객체를 생성하지 않는 편이 좋다. 그런데 타깃 객체에 대한 레퍼런스가 미리 필요할 수 있다. 이럴 때 프록시 패턴을 적용한다. 실제 타깃 객체를 만드는 대신 프록시를 넘겨준다. 프록시의 메서드를 통해 타깃을 사용하려고 시도하면? 그때 프록시가 타깃 객체를 생성하고 요청을 위임하는 식이다.
- 일반적으로 사용하는 프록시와 디자인 패턴 프록시 패턴에서의 프록시는 구분이 필요하다.
- 앞으로는...
- 타깃과 동일한 인터페이스를 구현하고
- 클라이언트와 타깃 사이에 존재하면서
- 기능의 부가 또는 접근 제어를 담당하는 객체
- 를 모두 프록시라고 부르겠다.
- 프록시
다이내믹 프록시
- 프록시는 기존 코드에 영향을 주지 않으면서 기능을 확장하거나 접근 방법을 제어하는 유용한 방법이다.
- 그러나 번거롭기 때문에 손이 안간다.
- 프록시도 목 프레임워크 처럼 편리하게 사용할 수 있는 방법이 있을까?
- 프록시의 구성과 프록시 작성의 문제점
- 프록시의 구성
- 타깃과 같은 메서드를 구현하고 있다가 메서드가 호출되면 타깃 객체로 위임한다.
- 지정된 요청에 대해서는 부가기능을 수행할 수 있다.
- UserServiceTx에서 프록시의 구성
코드 접기/펼치기
public class UserServiceTx implements UserService { @Autowired private PlatformTransactionManager transactionManager; @Autowired private UserServiceImpl userService; // 타깃 객체로 위임 public void add(User user) { userService.add(user); } public void upgradeLevels() { // 부가기능 수행 ~ TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); try { userService.upgradeLevels(); // 타깃 객체로 위임 transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw e; } // ~ 부가기능 수행 } }
- 프록시 작성의 문제점
- 타깃의 인터페이스를 구현하고 위임하는 코드를 작성하기 번거롭다. 메서드가 추가되거나 변경될 때 마다 해줘야된다.
- 부가기능 코드 중복이 몹시 심해진다.
- 프록시의 구성
- 리플렉션
- JDK의 다이내믹 프록시는 리플렉션 기능을 이용해서 프록시를 만들어 준다.
코드 접기/펼치기
@Test public void invokeMethod() throws Exception { String name = "Spring"; // length int nameLength = name.length(); Method lengthMethod = String.class.getMethod("length"); assertThat(lengthMethod.invoke(name)).isEqualTo(nameLength); // charAt int index = 0; char charAt = name.charAt(index); Method charAtMethod = String.class.getMethod("charAt", int.class); assertThat(charAtMethod.invoke(name, index)).isEqualTo(charAt); }
- 프록시 클래스
- 다이내믹 프록시를 이용한 프록시를 만들어본다.
- HelloProxyTest.simpleProxy()는 프록시 적용의 일반적인 문제점 두가지를 모두 갖는다.
- 다이내믹 프록시 적용(UppercaseHandler, HelloProxyTest.dynamicProxy())
- HelloUppercaseProxy를 다이내믹 프록시를 이용해 만들어본다.
- Hello 인터페이스에 메서드가 아무리 많아도 InvocationHandler에서 invoke메서드 하나로 처리한다.
- 충분히 재사용할 수 있다.
- 메서드를 선별해서 사용할 수도 있다.
- 프록시는 기존 코드에 영향을 주지 않으면서 기능을 확장하거나 접근 방법을 제어하는 유용한 방법이다.
다이내믹 프록시를 이용한 트랜잭션 부가기능
- UserServiceTx를 다이내믹 프록시 방식으로 변경
- 변경하는 이유?
- 서비스 인터페이스의 메서드를 모두 구현해야 함
- 트랜잭션이 필요한 메서드 마다 트랜잭션 처리코드가 중복
- 트랜잭션 InvocationHandler
- 트랜잭션 부가기능을 갖는 핸들러
코드 접기/펼치기
public class TransactionHandler implements InvocationHandler { // 부가기능을 제공할 타깃 객체. 어떤 타입의 객체든 적용할 수 있다. @Setter private Object target; // 트랜잭션을 제공하기 위한 트랜잭션 매니저 @Autowired private PlatformTransactionManager transactionManager; // 트랜잭션을 적용할 메서드 이름 패턴 @Setter private String pattern; public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().startsWith(pattern)) { return invokeInTransaction(method, args); } else { return method.invoke(target, args); } } private Object invokeInTransaction(Method method, Object[] args) throws Throwable { TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { Object ret = method.invoke(target, args); this.transactionManager.commit(status); return ret; } catch (InvocationTargetException e) { this.transactionManager.rollback(status); throw e.getTargetException(); } } }
- TransactionHandler와 다이내믹 프록시를 이용하는 테스트
- UserServiceTest에 적용
코드 접기/펼치기
@Test public void upgradeAllOrNothing() { TestUserService testUserService = new TestUserService(userList.get(3).getId()); testUserService.setUserDao(this.userDao); testUserService.setMailSender(mailSender); TransactionHandler txHandler = new TransactionHandler(); txHandler.setTarget(testUserService); txHandler.setTransactionManager(transactionManager); txHandler.setPattern("upgradeLevels"); UserService txUserService = (UserService)Proxy.newProxyInstance( getClass().getClassLoader(), new Class[] { UserService.class }, txHandler ); userDao.deleteAll(); userList.forEach(user -> userDao.add(user)); try { txUserService.upgradeLevels(); fail("TestUserServiceException expected"); } catch (TestUserServiceException e) { } checkLevelUpgraded(userList.get(1), false); }
다이내믹 프록시를 위한 팩토리 빈
이제 TransactionHandler와 다이내믹 프록시를 스프링의 DI를 통해 사용할 수 있도록 만들 차례다.
그런데 일반적인 빈으로 등록할 수 없는게 문제
- 스프링 빈은 클래스 이름과 프로퍼티로 정의되는데?
- 스프링은 지정된 클래스 이름을 가지고 리플렉션을 이용해서 해달 클래스의 객체를 만들어요
- 다이내믹 프록시 객체는 이런 식으로 프록시 객체가 생성되지 않는다는 점이다.
- 다이내믹 프록시는 Proxy클래스의 newProxyInstance()라는 스태틱 메서드를 통해서만 만들 수 있다.
팩토리 빈
- 스프링을 대신해서 객체의 생성로직을 담당하도록 만들어진 특별한 빈
- 팩토리 빈을 등록하는 가장 간단한 방법은 스프링의 FactoryBean인터페이스를 구현하는 방법
- 생성자를 제공하지 않는 Message클래스를 팩토리 빈으로 등록하는 예제
코드 접기/펼치기
// 참고 : https://www.baeldung.com/spring-factorybean @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = FactoryBeanConfig.class) public class MessageFactoryBeanTest { @Autowired private ApplicationContext context; @Qualifier("message") @Autowired private Message message; @Resource(name = "&message") private MessageFactoryBean messageFactoryBean; @Test public void getMessageFromFactoryBean() throws Exception { Object messageFromContext = context.getBean("message"); assertThat(messageFromContext).isInstanceOf(Message.class); assertThat(((Message) messageFromContext).getText()).isEqualTo("Factory Bean"); assertThat((message).getText()).isEqualTo("Factory Bean"); Message messageFromFactoryBean = messageFactoryBean.getObject(); assertThat(messageFromFactoryBean.getText()).isEqualTo("Factory Bean"); } }
다이내믹 프록시를 만들어주는 팩토리 빈
팩토리 빈을 사용하면 스프링의 빈으로 만들어 줄 수 있군(팩토리 빈의 getObject 메서드)
FactoryBean코드 접기/펼치기
@Setter public class TxProxyFactoryBean implements FactoryBean<Object> { private Object target; private PlatformTransactionManager transactionManager; private String pattern; private Class<?> serviceInterface; // FactoryBean 구현 메서드 @Override public Object getObject() throws Exception { TransactionHandler txHandler = new TransactionHandler(); txHandler.setTarget(target); txHandler.setTransactionManager(transactionManager); txHandler.setPattern(pattern); return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{serviceInterface}, txHandler); } public Class<?> getObjectType() { return serviceInterface; } public boolean isSingleton () { // 싱글톤 빈이 아니라는 뜻이 아님 // getObject가 매 번 같은 객체를 리턴하지 않는다는 의미 return false; } }
Config 코드 접기/펼치기
@Configuration public class FactoryBeanConfig { @Autowired private UserServiceImpl userServiceImpl; @Autowired private PlatformTransactionManager transactionManager; @Bean(name = "txProxy") public TxProxyFactoryBean txProxyFactoryBean() { TxProxyFactoryBean factoryBean = new TxProxyFactoryBean(); factoryBean.setTarget(userServiceImpl); factoryBean.setTransactionManager(transactionManager); factoryBean.setPattern("upgradeLevels"); factoryBean.setServiceInterface(UserService.class); return factoryBean; } }
-
테스트 코드 접기/펼치기
@RunWith(SpringRunner.class) @SpringBootTest @Slf4j class UserServiceTest { @Autowired private TxProxyFactoryBean txProxy; @Test public void upgradeAllOrNothing() throws Exception { TestUserService testUserService = new TestUserService(userList.get(3).getId()); testUserService.setUserDao(this.userDao); testUserService.setMailSender(mailSender); txProxy.setTarget(testUserService); UserService txProxyUserService = (UserService)txProxy.getObject();
userDao.deleteAll(); userList.forEach(user -> userDao.add(user)); try { txProxyUserService.upgradeLevels(); fail("TestUserServiceException expected"); } catch (TestUserServiceException e) { } checkLevelUpgraded(userList.get(1), false);
}
static class TestUserService extends UserServiceImpl {private String id; public TestUserService(String id) { this.id = id; } protected void upgradeLevel(User user) { if (user.getId().equals(this.id)) { throw new TestUserServiceException(); } super.upgradeLevel(user); }
}
}
프록시 팩토리 빈 방식의 장점과 한계
- 프록시 팩토리 빈의 재사용
- TransactionHandler를 이용하는 TxProxyFactoryBean은 코드 수정 없이 다양한 클래스에 적용할 수 있다.
- 프록시 팩토리 빈 방식의 장점
- 데코레이터 패턴이 적용된 프록시를 사용할 때 발생하는 문제 두 가지를 해결할 수 있다.
- 프록시 대상 클래스를 일일이 만들어야 하는 번거로움
- 부가기능 코드 중복
- 데코레이터 패턴이 적용된 프록시를 사용할 때 발생하는 문제 두 가지를 해결할 수 있다.
- 프록시 팩토리 빈의 한계
- 여러 클래스에 공통적인 부가기능을 제공하려면 중복이 발생한다.
- 한 클래스 안에 있는 여러 메서드에 부가기능을 제공하는건 가능하다.
- 비슷한 설정이 계속 반복돼서 나타나게 될 것
- TransactionHandler 객체가 프록시 팩토리 빈 개수만큼 만들어질 것이다.
- 타깃 객체를 프로퍼티로 갖고있다(UserServiceImpl)
- 그래서 타깃 객체가 여러개라면 그 개수만큼 빈을 선언하고 그만큼 객체가 생성 된다.
- 싱글톤 빈으로 만드는게 좋겠지?
- 여러 클래스에 공통적인 부가기능을 제공하려면 중복이 발생한다.
- 프록시 팩토리 빈의 재사용
- 여기까지 요약
```
AOP 냄새가 나기 시작했다.
1 트랜잭션 코드의 분리 : AOP들어가기위한 준비
2 고립된 단위테스트- 1에서 분리한 내용의 장점을 테스트코드에 적용
- 장점 : 코드 분리를 통해 비즈니스 로직에 대한 테스트를 더 쉽게 만들 수 있음(비즈니스 로직 UserService 코드와 부가기능 트랜잭션 경계설정 코드 분리)
- mockito는 내용 설명하다 생각나서 끼워넣은거 같은데 참 좋아보였다. 안쓴다니 아쉽다.
- 다이내믹 프록시와 프록시 패턴
- 프록시 패턴을 적용해서 부가기능을 추가했다.
- 기존 코드에 영향 없이 필요한 기능을 추가했다.
- 프록시를 팩토리 빈으로 등록했다.
- 필요한 기능이 중복되는 경우 중복을 어느정도 막을 수 있다.
- 하지만 타깃 객체가 여러개가 되는 경우 설정 중복을 피할 수 없다.
- AOP 냄새가 난다.
AOP? 중복되는 기능을 코드 중복 없이 동작하게 하는 방법?
```4. 스프링의 프록시 팩토리 빈
- ProxyFactoryBean
- 스프링은 일관된 방법으로 프록시를 만들 수 있게 도와주는 추상레이어를 제공한다.
- ProxyFactoryBean : 프록시를 생성해서 빈 객체로 등록하게 해주는 팩토리 빈이다.
- ProxyFactoryBean을 이용한 다이내믹 프록시 테스트(6-41)
- ProxyFactoryBean에 타깃 객체를 설정하고 부가기능을 추가한 다음 ProxyFactoryBean에서 설정한 객체를 빼오는식
테스트 코드 접기/펼치기
public class HelloProxyTest { @Test public void proxyFactoryBean() { ProxyFactoryBean pfBean = new ProxyFactoryBean(); pfBean.setTarget(new HelloTarget()); //타깃 설정 pfBean.addAdvice(new UppercaseAdvice()); //부가기능 추가 Hello proxiedHello = (Hello) pfBean.getObject(); //FacotryBean이므로 생성된 프록시를 가져온다. assertThat(proxiedHello.sayHello("Toby")).isEqualTo("HELLO TOBY"); assertThat(proxiedHello.sayHi("Toby")).isEqualTo("HI TOBY"); assertThat(proxiedHello.sayThankYou("Toby")).isEqualTo("THANKYOU TOBY"); } static class UppercaseAdvice implements MethodInterceptor { public Object invoke(MethodInvocation invocation) throws Throwable { String ret = (String)invocation.proceed(); //타깃을 알고 있기에 타깃 오브젝트를 전달할 필요가 없다. return ret.toUpperCase(); //부가기능 적용 } } }
- 어드바이저 : 어드바이스(부가기능)와 포인트컷(메서드 선정 알고리즘)을 묶은 객체
- 어드바이스 : 타깃이 필요 없는 순수한 부가기능
- UppercaseAdvice에는 타깃 객체가 등장하지 않는다.(setTarget 했기 때문)
- addAdvice : 여러 개의 MethodInterceptor(부가기능)를 추가할 수 있다.
- 어드바이스 : 타깃 객체에 종속되지 않는 순수한 부가기능을 담은 객체
- 포인트컷 : 부가기능 적용 대상 메서드 선정 방법
- TxProxyFactoryBean에서는 대상 메서드를 setPattern으로 설정
- MethodInterceptor 객체는 타깃 정보를 갖고 있지 않으므로 메서드 패턴으로 대상 메서드를 선택하는 방식은 사용할 수 없다.
- 프록시에 부가기능 적용 대상 메서드를 선택하는 기능을 넣으면 된다.
- 어드바이스가 템플릿이 되고 MethodInvocation 객체가 콜백이 되는 식
- 스프링이 제공하는 NameMatchMethodPointcut과 UppercaseAdvice 활용 테스트 코드
테스트 코드 접기/펼치기
@Test public void pointcutAdvisor() { ProxyFactoryBean pfBean = new ProxyFactoryBean(); pfBean.setTarget(new HelloTarget()); // 메서드 이름을 비교해서 대상을 선정하는 알고리즘을 제공하는 포인트컷 객체 NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut(); pointcut.setMappedName("sayH*"); // 포인트컷과 어드바이스를 advisor로 묶어서 한 번에 추가 // 묶는 이유? 어떤 어드바이스에 대해 어떤 포인트컷을 사용할 지 명확하게 구분하기 위해 // 여러 개의 어드바이스와 포인트컷이 추가될 수 있기 때문 pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice())); Hello proxiedHello = (Hello) pfBean.getObject(); assertThat(proxiedHello.sayHello("Toby")).isEqualTo("HELLO TOBY"); assertThat(proxiedHello.sayHi("Toby")).isEqualTo("HI TOBY"); assertThat(proxiedHello.sayThankYou("Toby")).isEqualTo("ThankYou Toby"); }
- ProxyFactoryBean 적용
- JDK 다이내믹 프록시의 구조를 그대로 이용해서 만들었던 TxProxyFactoryBean을 스프링이 제공하는 ProxyFactoryBean을 이용하도록 수정
- 어드바이스와 포인트컷의 재사용
- ProxyFactoryBean은 스프링의 DI, 템플릿/콜백 패턴, 서비스 추상화 등의 기법이 모두 적용된 것이다.
- 아래 예제코드에서는 UserService만 적용했지만 그 외에 다른 클래스도 이름 패턴만 지정해서 ProxyFactoryBean에 등록해주면 된다.
- TransactionAdvice
- 예제코드
테스트 코드 접기/펼치기
@Setter public class TransactionAdvice implements MethodInterceptor { PlatformTransactionManager transactionManager; // 타깃을 호출하는 기능을 가진 콜백 객체를 프록시로부터 받는다. // 덕분에 어드바이스는 특정 타깃에 의존하지 않고 재사용할 수 있다. public Object invoke(MethodInvocation methodInvocation) throws Throwable { TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { // 콜백을 호출해서 타깃의 메서드를 실행 // 타깃 메서드 호출 전후로 필요한 부가기능을 넣을 수 있다. // 경우에 따라 타깃이 아예 호출되지 않게끔 하거나 재시도를 위한 반복적 호출도 가능하다. Object ret = methodInvocation.proceed(); this.transactionManager.commit(status); return ret; // JDK 다이내픽 프록시가 제공하는 Method와 달리 // 스프링의 MethodInvocation을 통한 타깃 호출은 예외가 포장되지 않고 타깃에서 보낸 그대로 전달된다. } catch (RuntimeException e) { this.transactionManager.rollback(status); throw e; } } }
- 스프링 XML 설정파일
- 코드는 손볼곳이 없고 설정만 해주면 된다.
- 어드바이스를 등록한다.
- 설정파일(Java Config)
테스트 코드 접기/펼치기
@Configuration @RequiredArgsConstructor public class AopConfig { private final PlatformTransactionManager transactionManager; private final UserService userService; @Bean public TransactionAdvice transactionAdvice() { return new TransactionAdvice(transactionManager); } @Bean public NameMatchMethodPointcut transactionPointcut() { NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut(); pointcut.setMappedName("upgrade*"); return pointcut; } @Bean public DefaultPointcutAdvisor transactionAdvisor() { DefaultPointcutAdvisor pointcutAdvisor = new DefaultPointcutAdvisor(); pointcutAdvisor.setAdvice(transactionAdvice()); pointcutAdvisor.setPointcut(transactionPointcut()); return pointcutAdvisor; } @Bean public ProxyFactoryBean userServiceProxyFactoryBean() { ProxyFactoryBean factoryBean = new ProxyFactoryBean(); factoryBean.setTarget(userService); factoryBean.setInterceptorNames("transactionAdvisor"); return factoryBean; } }
- 테스트코드
테스트 코드 접기/펼치기
@Test public void upgradeAllOrNothing() { TestUserService testUserService = new TestUserService(userList.get(3).getId()); testUserService.setUserDao(this.userDao); testUserService.setMailSender(mailSender); ProxyFactoryBean proxyFactoryBean = context.getBean("&userServiceProxyFactoryBean", ProxyFactoryBean.class); proxyFactoryBean.setTarget(testUserService); UserService txUserService = (UserService) proxyFactoryBean.getObject(); try { txUserService.upgradeLevels(); fail("TestUserServiceException expected"); } catch (TestUserServiceException e) { } checkLevelUpgraded(userList.get(1), false); }
5. 스프링 AOP
- 지금까지 : 반복적으로 등장하는 트랜잭션 코드를 깔끔하고 효과적으로 분리
- 투명한 부가기능이다 : 기존 설계와 코드에는 영향을 주지 않음(있는 듯 없는 듯)
- 자동 프록시 생성
- 아직 남은 문제 : 설정정보를 계속 추가해주는 번거로움
- 중복 문제의 접근 방법
- 지금까지 살펴본 방법에는 한 번에 여러 빈에 프록시를 적용할 만한 방법은 없었다.
- 빈 후처리기를 이용한 자동 프록시 생성기
- 빈 후처리기 : 빈 객체로 만든 후에 그 빈 객체를 다시 가공할 수 있게 해준다.
- 빈 후처리기를 빈으로 등록하면 사용할 수 있다.
- 빈 객체 일부를 프록시로 포장하고 프록시를 빈으로 대신 등록할 수도 있다.
- 빈 후처리기의 동작
1. 빈 후처리기가 등록돼있으면 스프링은 빈 객체를 만들 때 마다 후처리기에게 빈을 보낸다. 2. 빈 후처리기는 등록된 어드바이저의 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지 확인한다. 3. 적용 대상이면? 내장된 프록시 생성기에게 현재 빈에 대한 프록시를 만들게 하고 어드바이저를 연결한다. 4. 프록시를 만들었으면? 전달받은 빈 객체 대신 만든 프록시 객체를 돌려준다. 5. 스프링은 돌려받은 프록시 객체를 빈으로 등록하고 사용한다.
- 확장된 포인트컷
- 포인트컷은 클래스 필터와 메서드 매처 두 가지를 돌려주는 메서드를 갖고있다.
- 모든 빈에 대해 프록시 적용 대상인지 판단해야 하는 상황에서는 클래스와 메서드 선정 알고리즘을 모두 갖는 포인트컷이 필요하다.
- 포인트컷 테스트
- 확장 포인트컷 학습 테스트 코드
테스트 코드 접기/펼치기
@Test @Description("확장 포인트컷 테스트 480p") public void classNamePointcutAdvisor() { // 포인트컷 준비 NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut() { public ClassFilter getClassFilter() { // 클래스 이름이 HelloT로 시작하는 것만 선택 return aClass -> aClass.getSimpleName().startsWith("HelloT"); } }; // sayH로 시작하는 메서드만 선택 pointcut.setMappedName("sayH*"); checkAdviced(new HelloTarget(), pointcut, true); checkAdviced(new HelloWorld(), pointcut, false); checkAdviced(new HelloToby(), pointcut, true); } class HelloWorld extends HelloTarget { } class HelloToby extends HelloTarget { } private void checkAdviced(Object target, Pointcut pointcut, boolean adviced) { ProxyFactoryBean pfBean = new ProxyFactoryBean(); pfBean.setTarget(target); pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice())); Hello proxiedHello = (Hello) pfBean.getObject(); if (adviced) { assertThat(proxiedHello.sayHello("Toby")).isEqualTo("HELLO TOBY"); assertThat(proxiedHello.sayHi("Toby")).isEqualTo("HI TOBY"); assertThat(proxiedHello.sayThankYou("Toby")).isEqualTo("ThankYou Toby"); } else { assertThat(proxiedHello.sayHello("Toby")).isEqualTo("Hello Toby"); assertThat(proxiedHello.sayHi("Toby")).isEqualTo("Hi Toby"); assertThat(proxiedHello.sayThankYou("Toby")).isEqualTo("ThankYou Toby"); } }
- DefaultAdvisorAutoProxyCreator의 적용
- 클래스 필터를 적용한 포인트컷 작성
- 주어진 이름 패턴을 가지고 클래스를 찾아내는 ClassFilter를 추가하도록 만들 것이다.
코드 접기/펼치기
public class NameMatchClassMethodPointcut extends NameMatchMethodPointcut { public void setMappedClassName(String mappedClassName) { // 매개변수로 받은 클래스 이름을 이용한 필터 this.setClassFilter(new SimpleClassFilter(mappedClassName)); } static class SimpleClassFilter implements ClassFilter { private final String mappedName; public SimpleClassFilter(String mappedName) { this.mappedName = mappedName; } @Override public boolean matches(Class> clazz) { if (mappedName.startsWith("Test") || clazz.getSimpleName().startsWith("Test")) { System.out.println("!!!!!!!!!!!"); } if (mappedName.startsWith("User") || clazz.getSimpleName().startsWith("User")) { System.out.println("!!!!!!!!!!!"); } System.out.println(String.format("mappedName : %s, classSimpleName : %s", mappedName, clazz.getSimpleName())); return PatternMatchUtils.simpleMatch(mappedName, clazz.getSimpleName()); } } }
- 어드바이저를 이용하는 자동 프록시 생성기 등록, 포인트컷 등록
코드 접기/펼치기
@Bean public NameMatchClassMethodPointcut transactionPointcut() { NameMatchClassMethodPointcut pointcut = new NameMatchClassMethodPointcut(); // 클래스 이름 패턴 pointcut.setMappedClassName("*ServiceImpl"); // 메서드 이름 패턴 pointcut.setMappedName("upgrade*"); return pointcut; } @Bean public DefaultPointcutAdvisor transactionAdvisor() { DefaultPointcutAdvisor pointcutAdvisor = new DefaultPointcutAdvisor(); pointcutAdvisor.setAdvice(transactionAdvice()); pointcutAdvisor.setPointcut(transactionPointcut()); return pointcutAdvisor; } @Bean public ProxyFactoryBean userServiceProxyFactoryBean() { transactionAdvisor(); ProxyFactoryBean factoryBean = new ProxyFactoryBean(); factoryBean.setTarget(userService); factoryBean.setInterceptorNames("transactionAdvisor"); return factoryBean; }
- 테스트코드
코드 접기/펼치기
@Test public void upgradeAllOrNothing() { userDao.deleteAll(); userDao.addAll(userList); testUserServiceImpl.setMailSender(mailSender); try { testUserServiceImpl.upgradeLevels(); fail("TestUserServiceException expected"); } catch (TestUserServiceException e) { } checkLevelUpgraded(userList.get(1), false); }
- 자동생성 프록시 확인
- 포인트컷 클래스 이름 필터를 *NotServiceImpl로 바꿔서 확인
- 자동생성된 프록시 확인(testUserService가 프록시로 변경된 객체인지)
- 클래스 필터를 적용한 포인트컷 작성
- 포인트컷 표현식을 이용한 포인트컷
- 더 편리한 포인트컷 작성 방법
- 포인트컷 표현식
- 정규식 같은 일종의 표현식 언어를 사용해서 포인트컷을 작성하는 방시
- AspectJExpressionPointcut 클래스 사용
- 포인트컷 표현식 문법
[] : 옵션(생략가능) | : or조건 execution([접근제한자 패턴] 타입패턴1 [타입패턴2.]이름패턴 (타입패턴3 | "..", ...) [throws 예외 패턴]) 접근제한자 : public, private 등 타입패턴1 : 리턴 값의 타입 패턴 타입패턴2 : 패키지와 클래스 이름에 대한 패턴. 사용할 때는 '.'으로 연결 이름패턴 : 메서드 이름 패턴 타입패턴3 : 파라미터의 타입을 순서대로 넣을 수 있다. 와일드 카드 사용 가능 예외 패턴 : 예외 이름 패턴
- 포인트컷 표현식 테스트
테스트 코드 접기/펼치기
@Test public void methodSignaturePointcut() throws SecurityException, NoSuchMethodException { // aspectjweaver 디펜던시 필요 AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression("execution(public int toby.common.learningtest.Target.minus(int,int)) "); // Target.minus() : 성공 assertThat(pointcut.getClassFilter().matches(Target.class) && pointcut.getMethodMatcher().matches(Target.class.getMethod("minus", int.class, int.class), null)).isTrue(); // Target.plus() : 메서드 매처에서 실패 assertThat(pointcut.getClassFilter().matches(Target.class) && pointcut.getMethodMatcher().matches(Target.class.getMethod("plus", int.class, int.class), null)).isFalse(); // Bean.method() : 클래스 매처에서 실패 assertThat(pointcut.getClassFilter().matches(Bean.class) && pointcut.getMethodMatcher().matches(Target.class.getMethod("method"), null)).isFalse(); }
- 포인트컷 표현식 테스트
// 두 개의 정수형 파라미터를 갖는 minus라는 이름의 모든 메서드를 선정하는 포인트컷 execution(int minus(int, int)) // 리턴타입 상관없는 포인트컷 execution(* minus(int, int)) // 파라미터 개수와 타입을 무시하는 포인트컷 execution(* minus(..)) // 모든 메서드 다 허용 execution(* *(..))
테스트 코드 접기/펼치기
@Test public void pointcut() throws NoSuchMethodException { targetClassPointcutMatches("execution(* *(..))", true, true, true, true, true, true); targetClassPointcutMatches("execution(* *(int, int))", false, false, true, true, false, false); targetClassPointcutMatches("execution(* *..*get.*(..))", true, true, true, true, true, false); targetClassPointcutMatches("execution(void *(..))", true, true, false, false, true, true); } // 타깃 클래스 메서드에 대해 포인트컷 선정여부를 검사하는 헬퍼 메서드 public void targetClassPointcutMatches(String expression, boolean... expected) throws NoSuchMethodException { pointcutMatches(expression, expected[0], Target.class, "hello"); pointcutMatches(expression, expected[1], Target.class, "hello", String.class); pointcutMatches(expression, expected[2], Target.class, "plus", int.class, int.class); pointcutMatches(expression, expected[3], Target.class, "minus", int.class, int.class); pointcutMatches(expression, expected[4], Target.class, "method"); pointcutMatches(expression, expected[5], Bean.class, "method"); } // 포인트컷과 메서드를 비교해주는 테스트 헬퍼 메서드 private void pointcutMatches(String expression, Boolean expected, Class> clazz, String methodName, Class>... args) throws NoSuchMethodException { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression(expression); assertThat(pointcut.getClassFilter().matches(clazz) && pointcut.getMethodMatcher().matches(clazz.getMethod(methodName, args), null)).isEqualTo(expected); }
- 포인트컷 표현식을 이용하는 포인트컷 적용
@Bean public AspectJExpressionPointcut transactionPointcut() { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression("execution(* *..*ServiceImpl.upgrade*(..))"); return pointcut; }
- 코드와 설정이 단순해진다.
- 문자열로 된 표현식이기 때문에 런타임 시점까지 문법의 검증이나 기능 확인이 불가능하다.
- 타입 패턴과 클래스 이름 패턴
- TestUserServiceImpl 클래스 이름을 TestUserService로 바꾸면 테스트는 실패하는가?
- 슈퍼클래스가 UserServiceImpl이고 인터페이스는 UserService이기 때문에 ServiceImpl로 끝나는 타입 패턴의 조건을 충족한다.
- 타입 패턴의 조건을 충족하기 때문에 테스트는 실패하지 않는다.
- 여기까지 요약
```
AOP 냄새가 나기 시작했다.
1 트랜잭션 코드의 분리 : AOP들어가기위한 준비
2 고립된 단위테스트- 1에서 분리한 내용의 장점을 테스트코드에 적용
- 장점 : 코드 분리를 통해 비즈니스 로직에 대한 테스트를 더 쉽게 만들 수 있음(비즈니스 로직 UserService 코드와 부가기능 트랜잭션 경계설정 코드 분리)
- mockito는 내용 설명하다 생각나서 끼워넣은거 같은데 참 좋아보였다. 안쓴다니 아쉽다.
- 다이내믹 프록시와 프록시 패턴
- 프록시 패턴을 적용해서 부가기능을 추가했다.
- 기존 코드에 영향 없이 필요한 기능을 추가했다.
- 프록시를 팩토리 빈으로 등록했다.
- 필요한 기능이 중복되는 경우 중복을 어느정도 막을 수 있다.
- 하지만 타깃 객체가 여러개가 되는 경우 설정 중복을 피할 수 없다.
- AOP 냄새가 난다.
AOP? 중복되는 기능을 코드 중복 없이 동작하게 하는 방법?
- 스프링의 프록시 팩토리 빈
- 프록시 팩토리 빈에 타깃객체를 설정하고 부가기능을 추가한 다음 설정한 객체를 하나씩 빼오는 식
- 어드바이스와 포인트컷을 묶어서 어드바이저를 만들고 팩토리 빈에 등록한다.
- 괜찮긴 한데 설정정보를 계속 추가해주는 번거로움이 있다.
- 스프링 AOP
- 빈 후처리기를 사용해서 포인트컷에 맞는 타깃에 부가기능을 적용하는 방법 사용(NameMatchClassMethodPointcut)
- 더 편한 포인트컷 표현식 활용(aspectjweaver)
- 결국 빈이 생길 때 마다 포인트컷에 맞는 대상에 트랜잭션 기능이 추가됐고 부가기능이 추가된 빈은 프록시로 존재하게됨
```
- AOP란 무엇인가?
- UserService에 트랜잭션을 적용해온 과정을 정리해보면
- 트랜잭션 서비스 추상화
- 트랜잭션 처리에 대한 구체적인 구현 방법을 자유롭게 바꿀 수 있도록 추상화 기법 적용
- 서버환경에서 종속되지 않음
- 프록시와 데코레이터 패턴
- 비즈니스 로직 코드에 부가기능(트랜잭션) 코드가 보이지 않게됐음
- 다이내믹 프록시와 프록시 팩토리 빈
- 부가기능 부여 코드 중복되는 문제 해결(JDK 다이내믹 프록시)
- 포인트 컷과 부가기능을 분리해서 여러 프록시에서 공유하도록 설정(프록시 팩토리 빈)
- 자동 프록시 생성 방법과 포인트컷
- 트랜잭션 대상 빈 마다 프록시 팩토리빈을 설정해주는 문제 해결(빈 후처리기)
- 포인트컷 표현식 활용(aspectjweaver)
- 부가기능의 모듈화
- TransactionAdvice라는 이름으로 모듈화
- 트랜잭션 서비스 추상화
- AOP : Aspect Oriented Programmin
- 왼쪽 : 2차원적인 평면 구조에 중복된 부가기능들 분포
- 오른쪽 : 3차원 구조에서 부가기능을 다른 면에 존재하도록 설계
- AOP
- 부가적인 기능을 분리해서 Aspect라는 독특한 모듈로 만들어서 설계하고 개발하는 방법
- OOP를 돕는 보조적인 기술
- 관점 지향 프로그래밍 : 애플리케잇녀을 특정한 관점을 기준으로 바라볼 수 있게 해준다는 의미
- Aspect
- 그 자체로 애플리케이션의 핵심 기능을 담고 있지는 않지만, 애플리케이션을 구성하는 중요한 요소이다.
- 핵심기능에 부가되어 의미를 갖는 특별한 모듈이다.
- 부가기능이 정의된 어드바이스와 어드바이스의 대상을 결정하는 정보를 담은 포인트컷을 갖는다.
- UserService에 트랜잭션을 적용해온 과정을 정리해보면
- AOP 적용기술
- 프록시를 이용한 AOP
- 프록시로 만들어서 DI로 연결된 빈 사이에 적용해 타깃의 메서드 호출 과정에 참여해서 부가기능을 제공
- 어드바이스가 적용되는 대상은 객체의 메서드이다.
- 스프링의 AOP는 프록시 방식의 AOP라고 할 수 있다. : 프록시는 독립적인 부가기능을 타깃 메서드에 다이내믹하게 적용해주기 위한 가장 중요한 역할을 맡음
- 바이트코드 생성과 조작을 통한 AOP
- 프록시 방식이 아닌 AOP는? AspectJ 프레임워크
- 컴파일된 타깃의 클래스 파일 자체를 수정하거나, JVM에 로딩되는 시점을 가로채서 바이트코드를 조작하는 방법 사용
- 대부분의 AOP는 프록시 방식으로도 충분하다.
- AspectJ는 복잡하지만 더 강력하고 유연함
- 프록시를 이용한 AOP
- AOP의 용어
- 타깃 : 부가기능을 부여할 대상
- 어드바이스 : 부가기능을 담은 모듈
- 조인 포인트 : 어드바이스가 적용될 수 있는 위치. 프록시 AOP에서 조인 포인트는 메서드의 실행단계뿐
- 포인트컷 : 어드바이스를 적용할 조인 포인트를 선별하는 작업 또는 그 기능을 정의한 모듈
- 프록시 : 클라이언트와 타깃 사이에 투명하게 존재하면서 부가기능을 제공하는 객체
- 어드바이저 : 포인트컷과 어드바이스를 하나 씩 갖는 객체
- 애스펙트 : AOP의 기본 모듈. 스프링의 어드바이저는 아주 단순한 애스펙트라고 볼 수도 있다.
- AOP 네임스페이스
- 스프링 AOP를 적용하기 위해 추가했던 어드바이저, 포인트컷, 자동 프록시 생성기 같은 빈들은 스프링 컨테이너에 의해 자동으로 인식돼서 특별한 작업을 위해 사용된다.
- 스프링의 프록시 방식 AOP를 적용하려면 최소한 네 가지 빈을 등록해야 한다.
- 어드바이스 외에는 스프링이 직접 제공하는 클래스를 빈으로 등록하고 프로퍼티 설정만 해준 것
- 자동 프록시 생성기
- 어드바이스
- 포인트컷
- 어드바이저
- AOP 네임스페이스
- 스프링은 AOP와 관련 태그를 정의해준 aop스키마를 제공한다.
transaction-config.xml <?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-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"/> ... </beans>
AOP 설정 빈 <aop:config> <aop:pointcut id="transactionPointcut" expression="execution(* *..*ServiceImpl.upgrade*(..))" /> <aop:advisor advice-ref="transactionAdvice" pointcut-ref="transactionPointcut"/> </aop:config> 이전 설정 @Bean public TransactionAdvice transactionAdvice() { return new TransactionAdvice(transactionManager); } @Bean public AspectJExpressionPointcut transactionPointcut() { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression("execution(* *..*ServiceImpl.upgrade*(..))"); return pointcut; } @Bean public DefaultPointcutAdvisor transactionAdvisor() { DefaultPointcutAdvisor pointcutAdvisor = new DefaultPointcutAdvisor(); pointcutAdvisor.setAdvice(transactionAdvice()); pointcutAdvisor.setPointcut(transactionPointcut()); return pointcutAdvisor; }
- 어드바이저 내장 포인트컷
<aop:config> <aop:advisor advice-ref="transactionAdvice" pointcut="execution(* *..*ServiceImpl.upgrade*(..))"/> </aop:config>
6. 트랜잭션 속성
트랜잭션 속성들이 이런게 있다 정도만 알면 될듯 포인트컷 마다 속성을 다르게 하면 유지보수성이 몹시 떨어질 듯 get~에다가 readOnly붙이는 정도까지는 좋다고 생각함.
- DefaultTransactionDefinition의 용도는?
- 트랜잭션 정의
- 트랜잭션이라고 모두 같은 방식으로 동작하는 것은 아니다.
- 트랜잭션 전파
- 트랜잭션 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식
- B는 A트랜잭션에 종속돼야 하는가, 독립적으로 작동해야 하는가를 결정
- PROPAGATION_REQUIRED
- 가장 많이 사용되는 전파 속성
- 진행 중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있으면 참여한다.
- PROPAGATION_REQUIRES_NEW
- 항상 새로운 트랜잭션을 실행한다.
- PROPAGATION_NOT_SUPPORTED
- 진행 중인 트랜잭션이 있어도 무시한다.
- 포인트컷을 복잡하게 만들지 않고 특정 기능에만 트랜잭션 적용이 안되도록 설정하고자 할 때 사용
- 이러면 더 복잡할거 같은데... config쪽 보는게 아닌 이상 이 메서드에 트랜잭션이 걸려있는지 안걸려있는지 어떻게알아
- 격리 수준(Isolation level)
- 모든 DB 트랜잭션은 격리수준을 갖고 있어야 한다.
- 기본적으로는 DB에 설정되어 있고 DefaultTransactionDefinition도 ISOLATION_DEFAULT이다.
- 특별한 작업을 수행하는 메서드의 경우 독자적인 격리수준을 지정할 필요가 있다.
- 제한시간
- 트랜잭션 수행 제한시간을 설정할 수 있다.
- DefaultTransactionDefinition의 기본 설정은 제한시간이 없는 것이다.
- 읽기전용
- 읽기전용 트랜잭션은 트랜잭션 내에서 데이터조작 시도를 막아줄 수 있다.
- 트랜잭션 인터셉터와 트랜잭션 속성
메서드 별로 다른 트랜잭션 정의를 적용하려면 어드바이스의 기능을 확장해야 한다.
TransactionInterceptor
- 스프링이 트랜잭션 경계설정 어드바이스를 편리하게 사용할 수 있도록 만든 인터셉터.
- PlatformTransactionMnager와 Properties 타입의 두 가지 프로퍼티를 갖는다.
- Properties는 Map타입 객체다. 메서드 패턴에 따라 각기 다른 트랜잭션 속성을 부여하기 위해 Map타입으로 받는다.
메서드 이름 패턴을 통한 트랜잭션 속성 지정
TransactionInterceptor의 Properties의 키
PROPAGATION_NAME // 트랜잭션 전파 방식. 필수값 , ISOLATION_NAME // 격리 수준. 생략 가능(deafult=디폴트격리수준) , readOnly // 읽기전용. 생략 가능(default=읽기전용아님) , timeout_NNNN // 제한시간. NNNN은 초단위 시간. 생략 가능 , -Exception1 // 체크 예외 중 롤백 대상으로 추가할 예외. 한 개 이상 등록가능 , +Exception2 // 런타임 예외지만 롤백시키지 않을 예외. 한 개 이상 등록가능
트랜잭션 속성 정의 예시
@Bean public TransactionInterceptor transactionInterceptor() { TransactionInterceptor interceptor = new TransactionInterceptor(); interceptor.setTransactionManager(transactionManager);
NameMatchTransactionAttributeSource txAttributeSource = new NameMatchTransactionAttributeSource();
Map<String, TransactionAttribute> txMethods = new HashMap();RuleBasedTransactionAttribute txAttributeForAll = new RuleBasedTransactionAttribute();
txAttributeForAll.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
txMethods.put("*", txAttributeForAll);RuleBasedTransactionAttribute txAttributeForGet = new RuleBasedTransactionAttribute();
txAttributeForGet.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
txAttributeForGet.setReadOnly(true);
txAttributeForGet.setTimeout(30);
txMethods.put("get*", txAttributeForGet);RuleBasedTransactionAttribute txAttributeForUpgrade = new RuleBasedTransactionAttribute();
txAttributeForUpgrade.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
txAttributeForUpgrade.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
txMethods.put("upgrade*", txAttributeForGet);txAttributeSource.setNameMap(txMethods);
interceptor.setTransactionAttributeSources(txAttributeSource);
return interceptor;
}
tx 네임스페이스를 이용한 설정 방법
// transaction-config.xml <?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" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"> ... // 오타나면? Enumeration으로 스키마에 값이 정의되어 있기 때문에 XML 유효성검사로 확인 가능하단다. <tx:advice // 이 태그에 의해 TransactionInterceptor 빈이 등록된다. id="transactionAdvice transaction-manger="transactionManager"> // 트랜잭션 매니저와 빈 아이디가 transactionManager라면 생략 가능\ <tx:attributes> <tx:method name="get*" propagation="REQUIRED" read-only="true" timeout="30" /> <tx:method name="upgrade*" propagation="REQUIRES_NEW" isolation="SERIALIZABLE" /> <tx:method name="*" propagation="REQUIRED" /> </tx:attributes> </tx:advice>
- 포인트컷과 트랜잭션 속성의 적용 전략
- 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용한다.
- 트랜잭션 경계로 삼을 클래스들이 모여있는 패키지를 통째로 선택
- 클래스 이름에서 일정한 패턴을 찾아서 표현식 결정 : execution(* *..*Service.*(..))
- 스프링의 빈 이름을 이용 : bean(*Service)
- 공통된 메서드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의한다.
- 실제로 하나의 어플리케이션에서 사용할 트랜잭션 속성의 종류는 그다지 다양하지 않다.
- 가장 간단한 트랜잭션 속성 부여 방법은 모든 메서드에 대해 디폴트 속성을 지정하는 것이다. 개발 진척도에 따라 단계적으로 추가해주면 됨
- get, find등 명명규칙을 설정해서 트랜잭션을 적용하면 좋음
- 프록시 방식 AOP는 같은 타깃 객체 내의 메서드를 호출할 때는 적용되지 않는다.
- 그렇지요? 클라이언트는 프록시를 호출하게끔 설정돼있는데 타깃 객체 내에서는 프록시를 못부르니까
- 주의해야 한다는 내용
- 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용한다.
- 트랜잭션 속성 적용
- UserService에 적용해보자
- 트랜잭션 경계설정의 일원화
- *중요* 서비스 계층을 트랜잭션이 시작되고 종료되는 경계로 정했다면, 테스트와 같은 특별한 이유가 아니고는 다른 계층이나 모듈에서 DAO에 직접 접근하는 것은 차단해야 한다.
- 아래 메서드를 추가하고 구현해서 User관련 데이터 조작은 UserService라는 트랜잭션 경계를 통해 진행할 경우 모두 트랜잭션을 적용할 수 있게 됐다.
UserService에 메서드 추가
public interface UserService { // ... User get(String id); List getAll(); void deleteAll(); void update(User user); }
- 서비스 빈에 적용되는 포인트컷 표현식 등록
- 빈 이름을 사용한 표현식을 갖는 포인트컷과 어드바이저
<aop:config> <aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)"/> </aop:config>
- 트랜잭션 속성을 가진 트랜잭션 어드바이스 등록
- 이전에 설정했던 transaction-config.xml 그대로 두면 끝
- 트랜잭션 속성 테스트
- 포인트컷을 Bean으로 설정했기 때문에 testUserServiceImpl로 설정했던 빈 이름 testUserService로 변경
- 읽기전용 속성 테스트
- 안되는데..? h2는 안된다는데 mariadb로 해봐야겠다
- mariadb로 하니까 된다(TransientDataAccessResourceException)
7. 애노테이션 트랜잭션 속성과 포인트컷
- 트랜잭션 애노테이션
- 자바 5에서 등장
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) // 애노테이션 정보가 언제까지 유지될 지 지정. 이렇게 설정하면 런타임 때도 리플렉션을 통해 애노테이션 정보를 얻을 수 있다. @Inherited // 상속을 통해서도 애노테이션 정보를 얻을 수 있게 한다. @Documented public @interface Transactional { // 디폴트 값이 설정되어 있으므로 모두 생략 가능하다. @AliasFor("transactionManager") String value() default ""; @AliasFor("value") String transactionManager() default ""; Propagation propagation() default Propagation.REQUIRED; Isolation isolation() default Isolation.DEFAULT; int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; boolean readOnly() default false; Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {}; }
- 트랜잭션 속성을 이용하는 포인트컷
- 포인트컷과 트랜잭션 속성을 애노테이션 하나로 지정할 수 있다.
- 메서드 마다 부여하면 코드가 지저분해 질 수 있다.
- 대체 정책
- 그래서 스프링은 @Transactional을 적용할 때 4단계의 대체 정책을 이용하게 해준다.
- 타깃 메서드, 타깃 클래스, 선언 메서드, 선언 타입(클래스, 인터페이스) 순서에 따라 @Transactional이 적용됐는지 확인
- 후보 순서 : [5, 6], [4], [2,3], [1]
- 트랜잭션 애노테이션 사용을 위한 설정
- <tx:annotation-driven/>
- spring boot에서는 본 적이 없는데? 자동설정인가봄
- 트랜잭션 애노테이션 적용
- UserService 인터페이스에 애노테이션 적용
UserService 인터페이스
@Transactional public interface UserService { void add(User user); void upgradeLevels(); void createOrIncreaseRecommend(User user); void setMailSender(MailSender mailsender); @Transactional(readOnly = true) User get(String id); @Transactional(readOnly = true) List
getAll(); void deleteAll(); void update(User user); } - 여기서 구현체 UserServiceImpl에 @Transactional을 붙이면? 대체정책에 따라 readOnly는 무시된다.
8. 트랜잭션 지원 테스트
- 선언적 트랜잭션과 트랜잭션 전파 속성
- 선언적 트랜잭션 : AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 하는 방법
- 프로그램에 의한 트랜잭션 : 위와 반대로 개별 데이터 기술의 트랜잭션 API를 사용하는 방법
- 특별한 경우가 아니라면 선언적 방식의 트랜잭션을 사용하는 것이 바람직하다.
- 트랜잭셩 동기화와 테스트
- 트랜잭션 매니저와 트랜잭션 동기화
3개의 트랜잭션이 만들어진 테스트코드
@Test public void transactionSync() { userService.deleteAll(); userService.add(userList.get(0)); userService.add(userList.get(1)); }
- 트랜잭션 매니저를 이용한 테스트용 트랜잭션 제어
3개의 트랜잭션을 합친 테스트코드
@Test public void transactionSync() { // UserService를 호출하기 전에 트랜잭션을 시작해주면 트랜잭션이 전파되어 통합된다. DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition(); // 트랜잭션 매니저에게 트랜잭션을 요청한다. // 기존에 시작된 트랜잭션이 없으므로 새로운 트랜잭션을 시작시키고 트랜잭션 정보를 돌려준다. // 동시에 만들어진 트랜잭션을 다른 곳에서도 사용할 수 있도록 동기화한다. TransactionStatus txStatus = transactionManager.getTransaction(txDefinition); userService.deleteAll(); userService.add(userList.get(0)); userService.add(userList.get(1)); transactionManager.commit(txStatus); }
- 트랜잭션 동기화 검증용 테스트
read-only 테스트
@Test(expected = TransientDataAccessResourceException.class) public void transactionSyncReadOnly() { DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition(); txDefinition.setReadOnly(true); TransactionStatus txStatus = transactionManager.getTransaction(txDefinition); userService.deleteAll(); }
롤백 테스트
@Test public void transactionSyncRollBack() { userDao.deleteAll(); assertThat(userDao.getAll().size()).isEqualTo(0); DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition(); TransactionStatus txStatus = transactionManager.getTransaction(txDefinition); userService.add(userList.get(0)); assertThat(userDao.getAll().size()).isEqualTo(1); transactionManager.rollback(txStatus); assertThat(userDao.getAll().size()).isEqualTo(0); }
- 롤백 테스트
- 테스트 내의 모든 DB작업을 하나의 트랜잭션 안에서 동작하게 하고, 테스트가 끝나면 무조건 롤백하는 테스트
- 테스트의 DB조작으로 인한 사이드이펙트를 방지할 수 있음
- 근데 데이터 조금만 복잡해져도 만들기 빡센데
- 테스트를 위한 트랜잭션 애노테이션
- @Transactional
- 테스트에도 @Transactional을 적용할 수 있다.
위에서 봤던 read-only 테스트
@Test(expected = TransientDataAccessResourceException.class) @Transactional(readOnly = true) public void transactionSyncReadOnly() { userService.deleteAll(); }
- @Rollback
- @Transactional이 달려있어도 @Rollback(false)해주면 예외가 발생하지 않는 한 트랜잭션은 커밋된다.
- @TransactionConfiguration
- @Rollback은 메서드에만 붙일 수 있다.
- @TransactionalConfiguration(defaultRollback = false) 해주면 죄 @Rollback(false) 되는것
- 특정 테스트메서드만 롤백시키고 싶으면 그 메서드에 @Rollback 붙이면 됨
- @TransactionalConfiguration deprecated 됐다.
- 왜냐? 테스트레벨에서 @Transactional은 항상 rollback=true라서
- 테스트클래스에 @Transactional 붙이고 테스트코드 돌리면 전체 테스트 메서드는 rollback된다.
- NotTransactional과 Propagation.NEVER
- @NotTransactional : @Transactional 설정을 무시하고 트랜잭션을 시작하지 않은 채로 테스트를 시작하게 함
- @Transactional(propagation = Propagation.NEVER) : 트랜잭션이 시작되지 않는다.
- 효과적인 DB 테스트
- 트랜젝션 제어 애노테이션은 통합 테스트를 만들 때 아주 유용하다.
- DB가 사용되는 통합테스트는 별도의 클래스로 만들고 롤백되도록 설정하는게 좋다.
- 토비 아저씨는 트랜잭션을 예시로 AOP를 설명했고, 설명이 다 끝나니 트랜잭션 설정을 쉽고 깔끔하게 할 수 있는 방법을 알려주셨다. 박수짝짝
9. 정리
- @Transactional
- 진짜 길었다 정말
- 6장에서는 트랜잭션 경계설정 기능을 성격이 다른 비즈니스 로직 클래스에서 분리하고 유연하게 적용할 수 있는 방법을 찾아보면서 애플리케이션에 산재해서 나타나는 부가기능을 모듈화할 수 있는 AOP 기술을 알아봤다.
- 트랜잭션처럼 환경과 외부 리소스에 영향을 받는 코드를 분리하면 비즈니스 로직에만 충실한 테스트를 만들 수 있다.
- 목 객체를 이용하면 의존관계 속에 있는 객체도 손쉽게 고립된 테스트로 만들 수 있다.
- 포인트컷은 AspectJ 포인트컷 표현식을 사용해서 작성하면 편리하다.
- AOP는 OOP만으로는 모듈화하기 힘든 부가기능을 효과적으로 모듈화하도록 도와주는 기술이다.
- 스프링은 자주 사용되는 AOP 설정과 트랜잭션 속성을 지정하는 데 사용할 수 있는 전용 태그를 제공한다.
- AOP를 이용해 트랜잭션 속성을 지정하는 방법
- 포인트컷 표현식과 메서드 이름 패턴을 이용하는 방법
- 타깃에 직접 부여하는 @Transactional 애노테이션 활용
- @Transactional을 이용한 트랜잭션 속성을 테스트에 적용하면 손쉽게 DB를 사용하는 코드의 테스트를 만들 수 있다.
'공부 > Spring' 카테고리의 다른 글
토비의 스프링(8) - 스프링이란 무엇인가? (0) | 2021.06.17 |
---|---|
토비의스프링(7) - 스프링 핵심 기술의 응용 (0) | 2021.06.17 |
토비의스프링(5) - 서비스 추상화 (0) | 2021.02.15 |
토비의스프링(4) - 예외 (0) | 2021.01.26 |
토비의스프링(3) - 템플릿 (0) | 2021.01.26 |