티스토리 뷰
이전 코드 개선
이전 코드
public void upgradeLevels() throws Exception{
// 트랜잭션 시작
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 (Exception e) {
//예외 발생시 롤백
transactionManager.rollback(status);
throw e;
}
}
- 문제점
- 트랜잭션 코드 - 서비스 로직 혼재
개선된 코드
public interface UserService {
void add(User user);
void upgradeLevels();
}
public class UserServiceImpl implements UserService {
public void upgradeLevels() {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
}
}
public class UserServiceTx implements UserService {
public void upgradeLevels() {
// 트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
userService.upgradeLevels();
//정상적으로 작업을 마치면 트랜잭션 커밋
transactionManager.commit(status);
} catch (Exception e) {
//예외 발생시 롤백
transactionManager.rollback(status);
throw e;
}
}
}
@Bean
public UserService userService() {
UserService userService = new UserServiceTx(userServiceImpl() ,transactionManager());
return userService;
}
@Bean
public UserServiceImpl userServiceImpl() {
UserServiceImpl userServiceImpl = new UserServiceImpl(userDao(), mailSender());
return userServiceImpl;
}
- UserService 인터페이스를 만들고 구현 클래스를 두 개 생성
- 비즈니스 로직은
UserServiceImpl
에 위임- UserServiceImpl 코드를 작성할 때는 트랜잭션과 같은 기술적인 내용에 대해 전혀 신경쓰지 않아도 됨
- 트랜잭션이 가미된 로직은
UserServiceTx
에만 존재
고립된 단위 테스트
기존 upgradeLevels() 테스트 코드
public void upgradeLevels() throws Exception {
//DB test data 준비
userDao.deleteAll();
for (User user : users) {
userDao.add(user);
}
//테스트 대상 실행
userService.upgradeLevels();
//DB 에 저장된 결과 확인
checkLevelUpgraded(users.get(0), false);
checkLevelUpgraded(users.get(1), true);
checkLevelUpgraded(users.get(2), false);
checkLevelUpgraded(users.get(3), true);
checkLevelUpgraded(users.get(4), false);
//mock 오브젝트를 이용한 결과 확인
List<String> requests = mockMailSender.getRequests();
assertThat(requests.size()).isEqualTo(2);
assertThat(requests.get(0)).isEqualTo(users.get(1).getEmail());
}
- 위 코드는 테스트용 DB (userDao) 에 의존하고 있다. 이러한 의존 관계를 없애보자.
UserDao Mock 오브젝트
public class MockUserDao implements UserDao {
//레벨 업그레이드 후보 User 오브젝트 목록
private List<User> users;
//업그레이드 대상 오브젝트를 저장해둘 목록
private List<User> updated = new ArrayList<>();
public MockUserDao(List<User> users) {
this.users = users;
}
public List<User> getUpdated() {
return updated;
}
//목 오브젝트 기능 제공
@Override
public void update(User user1) {
updated.add(user1);
}
//스텁 기능 제공
@Override
public List<User> getAll() {
return this.users;
}
@Override
public void add(User user) {
throw new UnsupportedOperationException();
}
//...
}
@Test
void upgradeLevels() throws Exception {
MockUserDao mockUserDao = new MockUserDao(users);
//고립된 테스트에서는 테스트 대상 오브젝트를 직접 생성, Mock UserDao DI
UserServiceImpl userServiceImpl = new UserServiceImpl(mockUserDao, mockMailSender);
userServiceImpl.upgradeLevels();
List<User> updated = mockUserDao.getUpdated();
assertThat(updated.size()).isEqualTo(2); //업데이트 횟수 확인
checkUserAndLevel(updated.get(0), "id2", SILVER); //업데이트 정보 확인
checkUserAndLevel(updated.get(1), "id4", GOLD);
List<String> requests = mockMailSender.getRequests();
assertThat(requests.size()).isEqualTo(2);
assertThat(requests.get(0)).isEqualTo(users.get(1).getEmail());
assertThat(requests.get(1)).isEqualTo(users.get(3).getEmail());
}
private void checkUserAndLevel(User updated, String expectedId, Level expectedLevel) {
assertThat(updated.getId()).isEqualTo(expectedId);
assertThat(updated.getLevel()).isEqualTo(expectedLevel);
}
- 기존에는 test DB 에 실제로 데이터를 넣고 업데이트 했다면 이제는 in-memory mock 오브젝트를 이용해서 업데이트한 대상이 누구인지, 업데이트가 몇 번 일어났는지 확인 가능
- DB data 전처리 작업(DB 데이터 전체 삭제, DB 데이터 등록) 작업이 필요 없게 됨
- Spring Container (SpringExtension) 또한 불필요
- 테스트 수행 시간 감축
Mock 프레임워크 : Mockito
@Test
void upgradeLevels() throws Exception {
UserDao mockUserDao = mock(UserDao.class);
when(mockUserDao.getAll()).thenReturn(users);
//고립된 테스트에서는 테스트 대상 오브젝트를 직접 생성, Mock UserDao DI
UserServiceImpl userServiceImpl = new UserServiceImpl(mockUserDao, mockMailSender);
userServiceImpl.upgradeLevels();
verify(mockUserDao, times(2)).update(any(User.class));
verify(mockUserDao, times(2)).update(any(User.class));
//users.get(1) 을 파라미터로 Update()가 호출된 적이 있는지를 확인
verify(mockUserDao).update(users.get(1));
assertThat(users.get(1).getLevel()).isEqualTo(SILVER);
verify(mockUserDao).update(users.get(3));
assertThat(users.get(3).getLevel()).isEqualTo(GOLD);
// MockMailSender 에 전달된 파라미터를 가져와 내용을 검증
ArgumentCaptor<SimpleMailMessage> mailMessageArg = ArgumentCaptor.forClass(SimpleMailMessage.class);
verify(mockMailSender, times(2)).send(mailMessageArg.capture());
List<SimpleMailMessage> mailMessages = mailMessageArg.getAllValues();
assertThat(mailMessages.get(0).getTo()[0]).isEqualTo(users.get(1).getEmail());
assertThat(mailMessages.get(1).getTo()[0]).isEqualTo(users.get(3).getEmail());
}
프록시
프록시
- 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아줌. 대리자, 대리인과 같은 역할
- 타깃과 같은 인터페이스를 구현
- 프록시가 타깃을 제어할 수 있는 위치에 있음
타깃 (target, real subject)
- 프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트
데코레이터 패턴
- 타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴
- 코드상에서는 어떤 방법과 순서로 프록시와 타깃이 연결되어 사용되는지 정해져 있지 않음
- e.g. UserServiceTx
- UserService 타입 오브젝트를 DI 받아서 기능을 위임
- 동시에 트랜잭션 경계설정 기능을 부여
@Bean
public UserService userService() {
UserService userService = new UserServiceTx(userServiceImpl() ,transactionManager());
return userService;
}
프록시 패턴
- 프록시를 사용하는 방법 중에서 타깃에 대한 접근 방법을 제어하려는 목적을 가진 경우
- 타깃의 기능 자체에는 관여하지 않음
- 데코레이터 패턴과 유사하지만 프록시는 코드에서 자신이 만들거나 접근할 타깃 클래스 정보를 알고 있는 경우가 많음
이 책에서 프록시 정의
- 타깃과 동일한 인터페이스를 구현
- 기능 부가 혹은 접근 제어를 담당하는 오브젝트
다이내믹 프록시
프록시 기능
- 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타깃 오브젝트로 위임
- 지정된 요청에 대해서는 부가기능을 수행
프록시 만드는 것이 번거로운 이유
- 인터페이스를 구현하고 위임하는 코드 작성 번거로움
- 해결 : JDK 다이내믹 프록시 -> 리플렉션 기능 활용
- 부가기능 코드가 중복될 가능성이 많음
- e.g. 트랜잭션 경계 설정 코드 -> 메소드가 추가될 때 마다 try-catch 반복 …
리플렉션
- 자바의 모든 클래스는 그 클래스의 구성정보를 담은 Class 타입의 오브젝트를 하나씩 가지고 있음
- 클래스 오브젝트를 이용하면 클래스 코드에 대한 메타정보를 가져오거나 오브젝트를 조작할 수 있음
프록시 예제 코드
public interface Hello {
String sayHello(String name);
String sayHi(String name);
String sayThankYou(String name);
}
public class HelloTarget implements Hello{
@Override
public String sayHello(String name) {
return "Hello " + name;
}
@Override
public String sayHi(String name) {
return "Hi " + name;
}
@Override
public String sayThankYou(String name) {
return "Thank you " + name;
}
}
//프록시 클래스
public class HelloUppercase implements Hello{
//위임할 타깃 오브젝트. 다른 프록시를 추가할 수도 있으므로 인터페이스로 접근.
Hello hello;
public HelloUppercase(Hello hello) {
this.hello = hello;
}
@Override
public String sayHello(String name) {
return hello.sayHello(name).toUpperCase();
}
@Override
public String sayHi(String name) {
return hello.sayHi(name).toUpperCase();
}
@Override
public String sayThankYou(String name) {
return hello.sayThankYou(name).toUpperCase();
}
}
HelloUppercase (프록시 클래스) 문제점
- 인터페이스의 모든 메소드를 구현해 위임해야 함 -> 번거로움
- 부가 기능 코드가 모든 메소드에 중복되어 나타남
문제 해결 : 다이내믹 프록시
- 프록시 팩토리에 의해 런타임 시 동적으로 만들어지는 오브젝트
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 {
//e.g. target.sayHi(args)
String ret = (String)method.invoke(target, args);
return ret.toUpperCase();
}
}
@Test
void simpleProxy() {
//프록시 팩토리에게 다이내믹 프록시를 만들어달라고 요청
Hello proxiedHello = (Hello)Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] {Hello.class},
new UppercaseHandler(new HelloTarget())
);
assertThat(proxiedHello.sayHello("Toby")).isEqualTo("Hello Toby");
assertThat(proxiedHello.sayHi("Toby")).isEqualTo("Hi Toby");
assertThat(proxiedHello.sayThankYou("Toby")).isEqualTo("Thank you Toby");
}
다이내믹 프록시 장점
- 인터페이스의 메소드가 늘어나더라도 코드를 추가하지 않아도 됨
다이내믹 프록시 확장
public class UppercaseHandler implements InvocationHandler {
Object target;
public UppercaseHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//e.g. target.sayHi(args)
Object ret = method.invoke(target, args);
if (ret instanceof String && method.getName().startsWith("say")) {
return ((String)ret).toUpperCase();
} else {
return ret;
}
}
}
- 스트링 이외의 리턴 타입을 갖는 메소드 추가 가능
- 타깃의 종류와 관계 없이(어떤 종류의 인터페이스를 구현한 타깃이든 상관 없이) 적용 가능
- 어떤 메소드에 어떤 기능을 적용할지 설정 가능.
- e.g. 메소드 이름으로 분기 처리
다이내믹 프록시를 이용한 트랜잭션 부가기능
public class TransactionHandler implements InvocationHandler {
private Object target;
private PlatformTransactionManager transactionManager;
private String pattern;
public TransactionHandler(Object target, PlatformTransactionManager transactionManager, String pattern) {
this.target = target;
this.transactionManager = transactionManager;
this.pattern = pattern;
}
@Override
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();
}
}
}
@Test
void upgradeAllOrNothing() throws Exception{
TestUserService testUserService = new TestUserService(userDao,mockMailSender, users.get(3).getId());
//UserServiceTx userServiceTx = new UserServiceTx(testUserService, transactionManager);
TransactionHandler txHandler = new TransactionHandler(testUserService, transactionManager, "upgradeLevels");
UserService userServiceTx = (UserService)Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] {UserService.class},
txHandler
);
userDao.deleteAll();
for (User user : users) {
userDao.add(user);
}
try {
userServiceTx.upgradeLevels();
Assertions.fail("TestUserServiceException expected");
} catch (TestUserServiceException e) {
}
//예외 발생 이전에 이미 레벨이 변경된 사용자 레벨이 처음으로 롤백 되었는지 확인
checkLevelUpgraded(users.get(1), false);
}
- UserServiceTx 를 다이내믹 프록시 방식으로 변경
- 인터페이스 메소드를 모두 구현할 필요가 없어짐
- 트랜잭션 처리 코드가 중복되지 않음
다이내믹 프록시를 위한 팩토리 빈
다이내믹 프록시 문제점
- 다이내믹 프록시 오브젝트를 스프링 빈으로 등록할 방법이 없다.
- 다이내믹 프록시 오브젝트의 클래스는 Object 로 만들어져서 다이내믹하게 새로 정의한 후 사용하기 때문
문제 해결 : 팩토리 빈
- 팩토리 빈
- 스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈
- 팩토리 빈을 만드는 간단한 방법으로,
FactoryBean
인터페이스를 구현하는 방법이 있다.
- 스프링은
FactoryBean
인터페이스를 구현한 클래스가 빈의 클래스로 지정되면, 팩토리 빈 클래스 오브젝트의getObject()
메소드를 이용해 오브젝트를 가져오고, 이를 빈 오브젝트로 사용한다. - 팩토리 빈을 사용하면 다이내믹 프록시 오브젝트를 스프링의 빈으로 만들어줄 수 있다.
getObject()
메소드에 다이내믹 프록시 오브젝트를 만들어주는 코드를 넣으면 되기 때문.
public class TxProxyFactoryBean implements FactoryBean<Object> {
//TransactionHandler 생성시 필요.
Object target;
PlatformTransactionManager transactionManager;
String pattern;
//다이나믹 프록시 생성시 필요.
Class<?> serviceInterface;
public TxProxyFactoryBean(Object target, PlatformTransactionManager transactionManager, String pattern, Class<?> serviceInterface) {
this.target = target;
this.transactionManager = transactionManager;
this.pattern = pattern;
this.serviceInterface = serviceInterface;
}
@Override
public Object getObject() throws Exception {
TransactionHandler txHandler = new TransactionHandler(target, transactionManager, pattern);
return Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[] {serviceInterface}, txHandler
);
}
//팩토리 빈이 생성하는 오브젝트의 타입은 DI 받은 인터페이스 타입에 따라 달라진다.
//따라서 다양한 타입의 프록시 오브젝트 생성에 재사용할 수 있다.
@Override
public Class<?> getObjectType() {
return serviceInterface;
}
//싱글톤 빈이 아니라는 뜻이 아니라 getObject()가 매번 같은 오브젝트를 리턴하지 않는다는 의미.
@Override
public boolean isSingleton() {
return false;
}
}
// @Bean
// public UserService userService() {
// UserService userService = new UserServiceTx(userServiceImpl() ,transactionManager());
// return userService;
// }
@Bean(name = "userService")
public TxProxyFactoryBean txProxyFactoryBean() {
TxProxyFactoryBean txProxyFactoryBean = new TxProxyFactoryBean(userServiceImpl(), transactionManager(), "upgradeLevels", UserService.class);
return txProxyFactoryBean;
}
- 팩토리 빈이 만드는 다이내믹 프록시는 구현 인터페이스나 타깃 종류에 제한이 없다. 따라서 UserService 외에도 트랜잭션 부가기능이 필요한 오브젝트를 위한 프록시를 만들 때 얼마든지 재사용이 가능하다. -> 설정이 다른 여러 개의
TxProxyFactoryBean
을 등록하면 됨- e.g. 트랜잭션 부가기능이 필요한 빈이 추가되면 빈 설정만 추가해주면 된다. 즉 매번
UserServiceTx
와 같은 프록시 클래스를 작성할 필요가 없다.
- e.g. 트랜잭션 부가기능이 필요한 빈이 추가되면 빈 설정만 추가해주면 된다. 즉 매번
프록시 팩토리 빈 장점
프록시 팩토리 빈 재사용 가능
@Bean(name = "coreService")
public TxProxyFactoryBean txProxyFactoryBean() {
TxProxyFactoryBean txProxyFactoryBean = new TxProxyFactoryBean(coreServiceTarget(), transactionManager(), "", CoreService.class);
return txProxyFactoryBean;
}
- 가령, CoreService라는 클래스에 트랜잭션 경계설정 기능을 부여해야 한다면 핵심 비즈니스 로직을 구현한 후 위와 같이 프록시 팩토리 빈 설정을 추가하는 것만으로 충분하다.
(데코레이터 패턴이 적용된 프록시) 단점 극복
- 인터페이스를 구현하는 프록시 클래스 작성 번거로움
- 다이내믹 프록시로 해결. 인터페이스 메소드를 일일이 오버라이드할 필요 없음
- 부가기능 구현 코드 반복
- 다이내믹 프록시로 해결. 핸들러 메소드만 구현하면 된다.
번거로운 다이내믹 프록시 생성 코드 제거
프록시 팩토리 빈 한계 (?)
- 한 번에 여러 개의 클래스에 공통적인 부가기능을 제공 불가능
- 하나의 타깃에 여러 개의 부가기능을 적용시 설정 복잡해짐
- 타깃과 인터페이스만 다른, 비슷한 설정이 반복됨
- TransactionHandler 오브젝트가 프록시 팩토리 빈 개수만큼 만들어짐
스프링의 프록시 팩토리 빈
ProxyFactoryBean
- 프록시를 생성해서 빈 오브젝트로 등록하게 해주는 팩토리 빈
- 프록시를 생성하는 작업만 담당
- 프록시로 제공하는 부가기능은 별도 빈으로 둔다.
MethodInterceptor
InvocationHandler
와 유사하게 부가기능 구현할 때 implement하는 인터페이스- ProxyFactoryBean 으로부터 타깃 오브젝트에 대한 정보도 함께 제공 받음
- 타깃 오브젝트와 독립적으로 만들어질 수 있다. 즉 싱글톤 빈으로 등록 가능
어드바이스 (Advice)
- 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트
- 스프링 싱글톤 빈으로 등록해서 여러 프록시에서 공유 가능
포인트컷 (Pointcut)
- 부가기능 적용 대상 메소드 선정 방법을 담은 오브젝트
- 메소드 선별 기능을 프록시로부터 분리
- 스프링 싱글톤 빈으로 등록해서 여러 프록시에서 공유 가능
어드바이저
- 어드바이스와 포인트컷을 묶은 오브젝트
ProxyFactoryBean -> 트랜잭션 경계설정 기능에 적용
public class TransactionAdvice implements MethodInterceptor {
PlatformTransactionManager transactionManager;
public TransactionAdvice(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
Object ret = invocation.proceed();
this.transactionManager.commit(status);
return ret;
} catch (RuntimeException e) {
this.transactionManager.rollback(status);
throw e;
}
}
}
- 타깃 메소드 호출 코드가 간결해짐
@Bean
public TransactionAdvice transactionAdvice() {
TransactionAdvice transactionAdvice = new TransactionAdvice(transactionManager());
return transactionAdvice;
}
@Bean(name = "transactionPointcut")
public NameMatchMethodPointcut transactionPointcut() {
NameMatchMethodPointcut transactionPointcut = new NameMatchMethodPointcut();
transactionPointcut.setMappedName("upgrade*");
return transactionPointcut;
}
@Bean(name = "transactionAdvisor")
public PointcutAdvisor transactionAdvisor() {
PointcutAdvisor transactionAdvisor = new DefaultPointcutAdvisor( transactionPointcut(), transactionAdvice());
return transactionAdvisor;
}
@Bean(name = "userService")
public ProxyFactoryBean proxyFactoryBean() {
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(userServiceImpl());
proxyFactoryBean.addAdvisor(transactionAdvisor());
return proxyFactoryBean;
}
- UserService 외에 새로운 비즈니스 로직을 담은 서비스 클래스가 만들어져도 이미 만들어둔 TransactionAdvice 를 재사용할 수 있음
'SPRING' 카테고리의 다른 글
토비의 스프링 8. 스프링 핵심 기술의 응용 (1) (0) | 2022.09.18 |
---|---|
토비의 스프링 7. AOP (2) (0) | 2022.09.18 |
토비의 스프링 5. 서비스 추상화 (0) | 2022.05.27 |
토비의 스프링 4. 예외 (0) | 2022.05.27 |
토비의 스프링 3. 템플릿 (0) | 2022.05.27 |
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- 메서드레퍼런스
- 디자인패턴
- 프로그래머스
- 백기선
- 프록시
- 토비
- gracefulshutdown
- provider
- 템플릿콜백
- 프록시패턴
- 자바스터디
- 예외처리
- 객체지향
- OOP
- 카카오
- java
- ec2
- 토비의봄TV
- 자바
- c++
- 스프링
- AOP
- BOJ
- 데코레이터패턴
- 서비스추상화
- 코딩테스트
- 김영한
- SOLID
- 코테
- 토비의스프링
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함