1. 메소드 추출
현재 upgradeLevels() 메소드는 트랜잭션으로 감싸져있다.
- 트랜잭션은 전, 후만 수행되면 되므로 로직 부분은 메소드 추출을 통해 바꿔보자
public void upgradeLevels() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
List<User> users = userDao.getAll();
for (User user : users) {
if(canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
transactionManager.commit(status);
} catch (RuntimeException e) {
transactionManager.rollback(status);
throw e;
}
}
public void upgradeLevels() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
upgradeLevelsInternal();
transactionManager.commit(status);
} catch (RuntimeException e) {
transactionManager.rollback(status);
throw e;
}
}
2. 인터페이스 도입 및 트랜잭션 전용 구현체 생성
기존
변화 1 (인터페이스 적용)
- 약한 결합을 갖도록 하기
변화 2 (트랜잭션 전용 클래스 적용)
- 로직을 감싸는 트랜잭션 전용 클래스를 적용하고, 그 내부에서 impl 클래스를 호출하는 방식으로 구현
3. 테스트 개념
단위 테스트 : 테스트 대상 클래스를 Mock Object 등으로 의존성을 주입하여 테스트하므로써, 외부의 리소스를 사용하지 않는 테스트
통합 테스트 : 외부의 DB / 서비스 리소스가 참여하는 테스트
4. Mockito 프레임워크
Mock 객체 생성
- mock(Class<?>)
UserDao mockUserDao = mock(UserDao.class);
Mock 스터빙
- when(...).thenReturn(...)
when(mockUserDao.getAll()).thenReturn(this.users);
Mock 을 통한 메소드 호출 확인
// mockUserDao 의 update 함수가 User.class 타입으로 2회 수행된 적이 있는지 검증
verify(mockUserDao, times(2)).update(any(User.class));
// mockUserDao 의 update 함수에 해당 파라미터를 통해 수행된 적이 있는지 검증
verify(mockUserDao).update(users.get(1));
5. 프록시
현재 구조
프록시 구조
프록시란?
- 클라이언트가 사용하는 대상을 실제대상인것처럼 위장해서 클라이언트의 요청을 받아주는 대리자같은 역할을 한다.
사용목적
- 클라이언트가 타깃에 접근하는 방법을 제어하기 위해서
- 타깃에 부가적인 기능을 제공하기 위해서
6. 데코레이터 패턴
런타임 시에 다이나믹하게 기능을 부여해주기 위해서 프록시를 사용하는 패턴을 의미한다.
- 생성자 혹은 Setter를 이용하여 다이나믹하게 대상을 주입한다.
- 타깃의 코드는 손대지않고 클라이언트가 호출하는 방법도 변경하지 않고 기능을 추가할 때 사용한다.
가장 많이 사용되는 예) InputStream, OutputStream
InputStream is = new BufferedInputStream(new FileInputStream("a.txt"));
UserServiceTx와 UserServiceImpl의 관계도 데코레이터 패턴이 적용된 예로 볼 수 있다.
@Bean
public UserService userService() {
UserServiceTx userServiceTx = new UserServiceTx();
userServiceTx.setUserService(userServiceImpl());
userServiceTx.setTransactionManager(transactionManager());
return userServiceTx;
}
7. 프록시 패턴
타깃에 대한 접근방법을 제어하기 위해 사용한다. (기능추가, 확장은 하지 않는다.)
JPA의 Lazy 로딩처럼
- 클라이언트가 타깃에 대한 Reference 를 요청
- 실제로 생성하는 것이 아닌 프록시를 반환
- 클라이언트가 프록시 메소드를 통해 사용하려하면 프록시가 타깃 오브젝트를 생성하고 요청을 위임한다.
요약
데코레이터 패턴 | 옵저버 패턴 | |
클라이언트와 타깃사이에 추가되는가 | Yes | Yes |
기능을 변경하는가 | Yes | No |
접근방법을 제어하는가 | No | Yes |
프록시는 두가지 기능으로 구성된다.
- 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타깃 오브젝트로 위임한다.
- 지정된 요청에 대해서는 부가기능을 수행한다.
UserServiceTx
- 위임과 부가작업을 수행한다.
public class UserServiceTx implements UserService {
// 타깃 오브젝트
UserService userService;
PlatformTransactionManager transactionManager;
public void setUserService(UserService userService) {
this.userService = userService;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
// 타깃과 동일한 메소드 구현
@Override
public void add(User user) {
// 위임
userService.add(user);
}
@Override
public void upgradeLevels() {
// 부가기능
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 위임
userService.upgradeLevels();
// 부가기능
this.transactionManager.commit(status);
} catch (RuntimeException e) {
transactionManager.rollback(status);
throw e;
}
}
}
8. 다이나믹 프록시 적용
현재 구조
- Hello를 구현한 Target 클래스 HelloTarget.class
- Hello를 구현한 부가기능을 주는 프록시 클래스 HelloUpperCase.class
문제점
- 인터페이스의 모든 메소드를 구현하여 override 해야하는 귀찮음
- 대문자로 변환시키는 기능이 계속 중복되어서 나타남 (중복)
다이나믹 프록시
- 런타임시에 프록시 팩토리에 의해서 다이나믹하게 만들어지는 오브젝트이다.
- Target이 구현한 인터페이스와 동일한 타입으로 만들어진다.
- 부가기능은 InvocationHandler를 구현한 오브젝트에 담는다.
이후 구조
UpperCaseHandler
public class UppercaseHandler implements InvocationHandler {
Hello target;
public UppercaseHandler(Hello target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String ret = (String) method.invoke(target, args);
return ret.toUpperCase();
}
}
ProxyFactory method
- ClassLoader를 사용해서 클래스를 로딩한다.
- Hello Interface를 구현한 형태로
- 메소드가 수행되면 UppercaseHandler 의 invoke() 를 거쳐서 가도록
Hello proxiedHello = (Hello) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] {Hello.class},
new UppercaseHandler(new HelloTarget()));
문제점
- 빈으로 등록할 방법이 없다.
- Proxy 클래스의 newProxyInstance() 를 통해서만 빈으로 등록되며, 클래스이름을 알 수 없다.
해결
- 팩토리빈을 사용하자.
- 스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈.
Message 클래스와 Message Factory Bean
- 아래의 MessageFactoryBean을 등록하면 Message 타입으로 사용할 수 있다. [getObjectType() 의 빈]
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);
}
}
@Component(value = "message")
public class MessageFactoryBean implements FactoryBean<Message> {
String text;
// DI 받기
public void setText(String text) {
this.text = text;
}
// 실제 빈으로 사용될 오브젝트를 직접 생성한다.
@Override
public Message getObject() throws Exception {
return Message.newMessage(this.text);
}
@Override
public Class<?> getObjectType() {
return Message.class;
}
// 요청마다 다른 빈을 리턴하므로 false
@Override
public boolean isSingleton() {
return false;
}
}
@SpringBootTest
@ContextConfiguration(classes = MessageFactoryBean.class)
public class FactoryBeanTest {
@Autowired
Message message;
@Test
void test() {
assertTrue(message instanceof Message);
}
}
9. 스프링의 프록시 팩토리 빈
ProxyFactoryBean
- 프록시를 생성해서 빈 오브젝트로 등록하게 해주는 팩토리 빈
- Advice (위에서 사용했던 Handler) 를 빈으로 등록할 수 있다.
- Advice 는 MethodInterceptor 를 구현한다.
- MethodInterceptor은 타깃정보에 상관없이 독립적으로 만들 수 있다. <-> InvocationHandler은 타깃정보를 필드에 명시해줌
- 콜백오브젝트로서, 템플릿으로 동작하기 때문이다. [proceed()]
- Advice 는 MethodInterceptor 를 구현한다.
@Test
void proxyFactoryBean() {
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget());
pfBean.addAdvice(new UppercaseAdvice());
Hello proxiedHello = (Hello) pfBean.getObject();
assertEquals("HELLO TOBY", proxiedHello.sayHello("Toby"));
assertEquals("HI TOBY", proxiedHello.sayHi("Toby"));
assertEquals("THANK YOU TOBY", proxiedHello.sayThankYou("Toby"));
}
10. 어드바이스와 포인트컷
Advice
- 타깃 오브젝트에 종속되지 않는, 순수한 부가기능을 담은 오브젝트
PointCut
- Advice를 적용할 대상 메소드를 선정하는 방법
기존의 다이나믹 프록시는 InvocationHandler 에서
- 부가기능 추가
- Pattern을 기반으로 어떤 함수에서는 수행하고 어떤 함수에서는 수행하지 않을 것인지에 대한것에 대한 2가지 기능을 수행했다.
ProxyFactoryBean 은
- 부가기능 추가 -> Advice(MethodInterceptor)
- 메소드 선정 -> PointCut 으로 수행한다.
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget());
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("sayH*");
pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));
Hello proxiedHello = (Hello) pfBean.getObject();
addAdvisor을 통해서 Pointcut과 Advice를 추가한다.
- 어떤 Pointcut에 대해서 어느 Advice를 적용할 지를 명시한다.
Advisior = PointCut + Advice
'책 정리 > 토비의 스프링' 카테고리의 다른 글
7. 스프링 핵심 기술의 응용 (0) | 2021.02.14 |
---|---|
6.2. 스프링 AOP (0) | 2021.02.10 |
5. 서비스 추상화 (0) | 2021.02.01 |
4. 예외 (0) | 2021.01.30 |
3. 템플릿 (0) | 2021.01.23 |