<토비의스프링> 6.1~6.3 AOP
by BFine6.1 트랜잭션 코드 분리
메소드 분리
비즈니스 로직과 얽혀 있는게 보기 좋지 않다
public void upgradeLevels() throws Exception { TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { List<User> users = userDao.getAll(); // 1 for (User user : users) { if (canUpgradeLevel(user)) { upgradeLevel(user); } } this.transactionManager.commit(status); } catch (Exception e) { this.transactionManager.rollback(status); throw e; } }
로직 1번부분은 메서드로 분리 가능하다
but 트랜잭션 코드가 없는 것 처럼 깔끔하게 하고싶다
트랜잭션 경계설정을 담당 하는 코드를 외부로 빼내자
DI 적용을 이용한 트랜잭션 분리
p.406의 구조처럼 UserService 인터페이스를 만들고 관심사를 분리하여 구현
public class UserServiceTx implements UserService { UserService userService; PlatformTransactionManager transactionManager; public void setTransactionManager(PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } //DI public void setUserService(UserService userService) { this.userService = userService; } public void upgradeLevels() { TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { // 기능을 위임한다. userService.upgradeLevels(); this.transactionManager.commit(status); } catch (RuntimeException e) { this.transactionManager.rollback(status); throw e; } } }
로직을 이용하려 할때
- 먼저 트랜잭션을 담당하는 객체가 사용돼서 트랜잭션 작업을 진행
- 이후 비즈니스 로직이 관련된 작업을 수행
장점
- UserServiceImpl 코드를 작성할때 트랜잭션을 신경쓰지않아도됨
- 비즈니스 로직에 대한 테스트를 손쉽게 만들어냄
6.2 고립된 단위테스트
복잡한 의존관계 속 테스트
- p.414 그림 처럼 복잡하게 의존이 얽혀있음
- 테스트 대상을 다른클래스, 서버에 종속되지 않도록 고립시켜야함
- Mock 객체 이외에 필요하지 않은 의존 객체와 서비스를 모두 제거
단위테스트와 통합테스트
- 테스트 클래스를 의존 객체나 외부 사용하지 않도록 고립하여 테스트 하는 것을 단위테스트
- 두개 이상의 다른 객체가 연동하여 테스트, 외부 DB 등 참여하는 테스트는 통합테스트
Mock 프레임워크
목 클래스를 일일이 준비 할 필요 X
// 스테틱 메소드 호출 UserDao mockUserDao = mock(UserDao.class) //스텁 기능 추가, getAll이 호출됐을때 users가 리턴 when(mockUserDao.getAll()).thenReturn(this.users); // 몇번 호출됐는지 검증 verify(mockUserDao, time(2)).update(any(User.class));
6.3 다이나믹 프록시 와 팩토리빈
프록시
위의 문제는 핵심기능을 가진 클래스를 직접 사용하면 부가기능 적용기회가 없어짐
부가기능은 마치 핵심기능을 가진 클래스인 것 처럼 꾸며야함
클라이언트 --> 핵심기능 인터페이스 부가기능 ---> 핵심기능 인터페이스 핵심기능
사용하는 대상인 것 처럼 위장해서 요청을 받아주는 것을 프록시 (대리자) , 실제 객체는 타깃
프록시 사용목적
- 클라이언트가 타깃에 접근하는 방법 제어
- 타깃에 부가기능 부여
데코레이터 패턴
타깃에 부가적인 기능을 런타임시에 동적으로 부여해주기 위해 프록시를 사용
프록시가 한개로 제한되지 않고 고정시킬 필요도 없음
⇒ 같은 인터페이스를 구현한 타깃과 여러 개의 프록시 사용 가능
다음 위임 대상은 인터페이스로 선언, 생성자나 수정자를 통해 외부에서 런타임시 주입
프록시패턴
프록시는 클라이언트와 사용 대상사이에 대리역활을 맡은 객체를 두는 방법
프록시패턴은 프록시 사용 방법 중에 타깃에 대한 접근 방법을 제어
타깃의 기능을 확장하거나 추가하지 않는다
⇒ 대신 클라이언트가 타깃에 접근하는 방식을 변경
Collections.unmodifiableCollection()// 접근권한 제어 // 특정레이어로 넘어가는 경우 읽기 전용 등
클라이언트에게 타깃의 레퍼런스 대신 프록시를 넘겨주고 사용할때 프록시가 타깃 생성
프록시는 만들거나 접근해야하는 타깃 클래스 정보를 알아야함
다이나믹 프록시
reflect 패키지에 프록시를 손쉽게 만들어주는 클래스가 있음
프록시가 만들기 번거러운 이유
타깃 인터페이스를 만들고 위임하는 코드를 만들기 번거로움
⇒ 부가기능 필요없는 메소드도 만들어야하고, 타깃 메소드 변경시 바꿔줘야함
- 부가 코드 중복 가능성 발생
리플랙션
자바의 코드 자체를 추상화 해서 접근하도록 만든 것
자바의 모든 클래스는 자체 구성정보를 담은 Class타입 객체를 가지고 있음
⇒ 메타정보 확인, 객체 조작 가능
프록시 클래스
예시
public class HelloUppercase implements Hello { Hello hello; // 프록시 추가될수 있으니 인터페이스로 접근 public HelloUppercase(Hello hello) { this.hello = hello } public String sayHello(String name) { return hello.sayHello(name).toUpperCase(); // 부가기능 } public String sayHelloHi(String name) { return hello.sayHelloHi(name).toUpperCase(); // 부가기능 } } Hello proxHello = new HelloUppercase(new HelloTarget());
문제점
- 인터페이스의 모든 메소드를 구현해 위임
- 부가기능이 모든 메소드에 중복
다이나믹 프록시 적용(p.441 그림)
프록시 팩토리에 의해 런타임시 다이나믹 하게 만들어짐
클라이언트는 다이나믹 프록시 객체를 타깃 인터페이스에 사용
invokeHandler
public class UppercaseHandler implements InvocationHandler { Hello target; public UppercaseHandler(Hello target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String ret = (String) method.invoke(target, args); //타깃 호출 return ret.toUpperCase(); //부가기능 } }
프록시생성
Hello proxiedHello = (Hello) Proxy.newProxyInstance( getClass().getClassLoader(), // 다이나믹 프록시가 정의되는 클래스 로더 지정 new Class[] { Hello.class }, //구현할 인터페이스, 하나이상 new UppercaseHandler(new HelloTarget())); //부가기능 위임 기능 }
트랜잭션 InvocationHandler
public class TransactionHandler implements InvocationHandler { private Object target; private PlatformTransactionManager transactionManager; private String pattern; public void setXXX(Object target) { . . . } 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); return e.getTargetException(); } }
다이나믹 프록시를 위한 팩토리 빈
다이나믹 프록시 객체는 일반적인 스프링 빈으로는 등록 안됨
스프링은 지정된 클래스 이름을 가지고 리플렉션 하여 클래스와 객체를 만듬
위의 방법 이외에 빈을 만들 수 있는 여러가지 방법 제공
팩토리 빈을 이용하여 인터페이스를 구현
public interface FactoryBean<T> { T getObject() throws Exception; //빈 오브젝트를 생성 Class<? extends T> getObjectType(); //생성되는 오브젝트 타입 boolean isSingleton(); //항상 싱글톤 오브젝트인지 알려줌 }
TEST.
public class Message { String text; private Message(String text) { this.text = text; } public String getText() { return text; } public static Message newMessage(String text) { // 반드시 스태틱 메소드 사용 return new Message(text); } } // 팩토리빈 public class MessageFactoryBean implements FactoryBean<Message> { String text; public void setText(String text) { this.text = text; } public Message getObject() throws Exception { return Message.newMessage(this.next); // 스태틱 } public Class<? extends Message> getObjectType() { return Message.class; } public boolean isSingleton() { return false; } }
팩토리빈 설정방법
- message 빈의 오브젝트타입이 Message 타입
- 팩토리 빈 자체를 가져오고 싶은 경우 getBean("&message") &를 붙이면 된다.
다이나믹 프록시를 만들어주는 팩토리빈
- 팩토리빈의 getObject 메소드에 다이나믹 프록시 만들어주는 코드 설정
- 스프링 빈에는 팩토리빈과 UserServiceImpl 만 반으로 등록
- 팩토리빈은 타깃에 대한 레퍼런스를 DI로 받아야함
트랜잭션 프록시 팩토리빈
public class TxProxyFactoryBean implements FactoryBean<Object> { Object target; PlatformTransactionManager transactionManager; String pattern; Class<?> serviceInterface;//다이나믹 프록시 생성시 필요 public void setServiceInterface(Class<?> serviceInterface) { this.serviceInterface = serviceInterface; } // 팩토리빈 인터페이스 구현 메서드 public Object getObject() throws Exception { TransactionHandler txHandler = new TransactionHandler(); txHandler.setTarget(targer); txHandler.setTransactionManager(transactionManager); txHandler.setPattern(pattern); return Proxy.newProxyInstance( getClass().getCalssLoader(), new Class[] { serviceInterface }, txHandler); } } TEST TxProxyFactoryBean txbean = context.getBean("&userService",TxProxyFactoryBean.class) // txbean.setTarget(testUserService); UserService txUserService = (UserService) txbean.getObject(); // 트랜잭션 다이내픽 프록시 객체를 다시 생성한다.
프록시 팩토리빈 방식의 장점과 한계
재사용
- TxProxyFactoryBean은 코드 수정없이 다양한 클래스에 적용
장점
- 타깃 인터페이스를 구현하는 클래스를 일일히 만드는 번거로움 제거
- 모든 메서드에 부가기능 중복제거
한계
한번에 여러개의 클래스에 부가기능을 제공 불가
하나의 타깃에 여러 부가기능 적용시 문제
프록시 팩토리빈 설정이 부가기능 개수만큼 붙어야함
⇒ XML 설정이 길어짐
TransactionHandler 오브젝트가 팩토리빈 개수만큼 만들어짐 (타깃이 달라질 경우)
'개발서적 > 토비의스프링' 카테고리의 다른 글
<토비의스프링> 6.7~6.8 트랜잭션 어노테이션 (0) | 2021.02.20 |
---|---|
<토비의스프링> 6.4~6.5 AOP(2) (0) | 2021.02.20 |
<토비의스프링> 5.1~5.2 서비스추상화 (0) | 2021.02.17 |
<토비의스프링> 4.1~4.2 예외 (0) | 2021.02.17 |
<토비의스프링> 3.1~3.5 템플릿 (0) | 2021.02.16 |
블로그의 정보
57개월 BackEnd
BFine