[토비의 스프링] Week8(6.1~6.3)
토비의 스프링 3.1 Chapter 6.1 ~ 6.3
Chapater 6.1 트랜잭션 코드의 분리
깔끔한 트랜잭션 인터페이스 사용! but..
- 비즈니스 로직이 주인이어야 할 메소드 안에 이름도 길고 무시무시하게 생긴 트랜잭션 코드가 더 많은 자리를 차지하고 있는 모습
6.1.1 메소드 분리
기존 코드의 특징
- 두 가지 종류의 코드가 구분되어 있음
- 두 코드 간 주고 받는 정보가 없음
public class UserService { public void upgradeLvls() throws Exception { TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { upgradeLvlsInternal(); this.transactionManager.commit(status); } catch (Exception e) { this.transactionManager.rollback(status); throw e; } } // 분리된 비즈니스 로직 코드 // 트랜잭션 적용 전과 동일 private void upgradeLvlsInternal() { List<User> users = userDao.getAll(); for (User user : users) { if (canUpgradeLvl(user)) { upgradeLvl(user); } } } }
- 보기에 깔끔, 비즈니스 로직이 독립된 메소드로 분리되어 이해하기 편하고, 수정하기에도 부담이 없다
6.1.2 DI를 이용한 클래스의 분리
여전히 트랜잭션을 담당하는 기술적인 코드가 UserService내에 위치
DI 적용을 이용한 트랜잭션 분리
- DI의 기본 아이디어는 실제 사용할 오브젝트의 클래스 정체는 감춘 채 인터페이스를 통해 간접으로 접근하는 것
- UserService는 클래스이고, 클라이언트는 UserService를 직접 참조하게 된다 => 간접적으로 참조하게 변경!
- UserService를 인터페이스로 만들고, 기존 코드는 UserService를 구현한 클래스에 넣으면 된다.
UserService 인터페이스 도입
public interface UserService { void add(User user); void upgradeLvls(); }
- 인터페이스로 변환하고 Service기능과 Transaction 기능 수행하는 클래스로 구분
public class UserServiceImpl implements UserService { private UserDao userDao; private MailSender mailSender; @Override public void upgradeLvls() { List<User> users = userDao.getAll(); for (User user : users) { if (canUpgradeLvl(user)) { upgradeLvl(user); } } } }
- UserServiceImpl
- UserService 내용 대부분 유지하면서 트랜잭션 코드는 모두 제거
분리된 트랜잭션 기능
public class UserServiceTx implements UserService {
UserService userService;
// UserService를 구현한 다른 오브젝트를 DI 받음
public void setUserService(UserService userService) {
this.userService = userService;
}
// DI 받은 UserService 오브젝트에 모든 기능을 위임
@Override
public void add(User user) {
this.userService.add(user);
}
@Override
public void upgradeLvls() {
this.userService.upgradeLvls();
}
}
- UserServiceTx
- UserService를 구현하면서 같은 인터페이스 구현한 다른 오브젝트에게 작업을 위임
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) {
this.userService.add(user);
}
@Override
public void upgradeLvls() {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
this.userService.upgradeLvls();
this.transactionManager.commit(status);
} catch (RuntimeException e) {
this.transactionManager.rollback(status);
throw e;
}
}
}
- 트랜잭션 경계 설정 부가작업 부여
트랜잭션 적용을 위한 DI 설정
- 트랜잭션 기능의 오브젝트가 적용된 의존관계
- Client(UserServiceTest) -> UserServiceTx -> UserServiceImpl ```java
//userService 빈 아이디는 UserServiceTx 클래스로 정의된 빈에게 부여 //userService 빈은 UserServiceImpl 클래스로 정의되는 userServiceImpl 빈을 DI
- 클라이언트는 UserServiceTx Bean을 호출해서 사용하도록 만들어야 한다.
### 트랜잭션 분리에 따른 테스트 수정
- Autowired 어노테이션으로 타입이 일치하는 빈을 찾음, 타입으로 찾지 못하면 필드 이름으로 빈을 찾아 준다
```java
@Autowired
UserService userService;
- userService 라는 id를 가진 UserServiceTx 빈이 주입 됨
- 목 오브젝트를 이용해 수동 DI를 적용하는 테스트에서는 어떤 클래스의 오브젝트인지 분명하게 알 필요가 있음
@Autowired UserServiceImpl userServiceImpl;
- upgradeLvls() 테스트 메소드
- MailSender의 목 오브젝트 설정은 UserService의 인터페이스를 통해서는 불가능
- 별도로 가져온 userServieImpl 빈에 해주어야 함
public class UserServiceTest { public void upgradeLvls() throws Exception { // ... MockMailSender mockMailSender = new MockMailSender(); userServiceImpl.setMailSender(mockMailSender); // ... } }
- upgradeAllOrNothing() 테스트 메소드
- 트랜잭션 기술이 바르게 적용됐는지를 확인하기 위한 일종의 학습 테스트
- 따라서 직접 테스트용 확장 클래스도 만들고 수동 DI도 적용하고 한 만큼, 바뀐 구조를 모두 반영해주는 작업이 필요
- TestUserService는 트랜잭션 기능이 빠진 UserServiceImpl을 상속해야함
- 트랜잭션 롤백의 확인을 위한 예외 발생 코드가 UserServiceImpl에 있기 때문
- 그런데 트랜잭션 기능이 없어짐
- TestUserService 오브젝트를 UserServiceTx 오브젝트에 수동 DI 시킨 후 UserServiceTx의 메소드를 호출해야함
public class UserServiceTest { // 클래스 선언 변경 static class TestUserService extends UserServiceImpl { // ... } @Test public void upgradeAllOrNothing() throws Exception { TestUserService testUserService = new TestUserService(users.get(3).getId()); testUserService.setUserDao(userDao); testUserService.setMailSender(mailSender); // 트랜잭션 기능을 분리한 UserServiceTx는 예외 발생용으로 수정할 필요가 없으니 그대로 사용한다. UserServiceTx txUserService = new UserServiceTx(); txUserService.setTransactionManager(transactionManager); txUserService.setUserService(testUserService); userDao.deleteAll(); for (User user : users) { userDao.add(user); } try { // 트랜잭션 기능을 분리한 오브젝트를 통해 예외 발생용 TestUserService가 호출되게 해야한다. txUserService.upgradeLvls(); fail("TestUserServiceException expected"); } // ... } }
트랜잭션 경계설정 코드 분리의 장점
1. 비즈니스 로직을 담당하는 UserServiceImpl은 기술적 내용에 전혀 신경을 쓰지 않아도 된다. 2. 비즈니스 로직에 대한 테스트를 손쉽게 만들어 낼 수 있다. (6.2에 계속) #
Chapater 6.2 고립된 단위 테스트
작은 단위의 테스트
- 테스트가 실패했을때 원인을 찾기 쉽다 but, 테스트 대상이 다른 오브젝트와 환경에 의존하고 있다면..
6.2.1 복잡한 의존관계 속의 테스트
UserService
- 아주 간단한 기능만 갖고 있지만 구현 클래스들의 동작을 위해 세 가지 타입의 의존 오브젝트가 필요
- DB 처리를 위해 UserDao, 메일 처리를 위해 MailSender, 트랜잭션 처리를 위한 PlatformTransactionManager UserServiceTest
- 세 가지 의존관계를 갖고 있으므로 테스트가 진행되는 동안 세 가지 오브젝트가 모두 정상이어야 한다.
- 그 환경 뒤의 어느 하나라도 문제가 있다면 테스트가 실패해버림
6.2.2 테스트 대상 오브젝트 고립시키기
테스트 대상이 외부 환겨엥 영향을 받지 않으려면?
- 테스트를 고립시킨다. 그를 위해 테스트를 위한 대역 사용
테스트를 위한 UserServiceImpl 고립
PlatformTransactionManager
- 트랜잭션 코드를 독립시켜 UserServiceImpl은 더 이상 PlatformTransactionManager에 의존하지 않음
UserDao
- 부가적인 검증 기능을 가진 목 오브젝트로 만든다
UserServiceImpl
- 기능이 수행되더라도 결과가 DB 등을 통해 남지 않으니, 기존 방법으로는 작업 결과 검증이 힘들다
- 따라서 UserDao에게 어떤 요청을 했는지 확인하는 작업이 필요
고립된 단위 테스트 활용
- upgradeLevels()의 테스트 단계
- DB 테스트 데이터 준비
- 메일 발송 여부 확인을 위한 목 오브젝트를 DI해줌
- 테스트 대상 실행
- DB에 저장된 결과 확인
- 목 오브젝트를 이용한 결과 확인
UserDao 목 오브젝트
- DB에 직접 의존하는 첫 번째와 네 번째 테스트 방식을 목 오브젝트를 만들어서 적용
-
UserServiceImpl이 UserDao를 사용하는 경우 ```java public void upgradeLvls() { // 업그레이드 후보 사용자 목록을 가져옴 List
users = userDao.getAll(); for (User user : users) { if (canUpgradeLvl(user)) { upgradeLvl(user); } } }
protected void upgradeLvl(User user) { user.upgradeLvl(); // 수정된 사용자를 DB에 반영한다. userDao.update(user); sendUpgradeEMail(user); }
- userDao.getAll()은 레벨 업그레이드 후보가 될 사용자 목록을 가져옴
- userDao.update(user)는 리턴 값이 없기에 따로 준비할 것은 X
MockUserDao
- getAll()은 스텁으로서, update()는 목 오브젝트로서 UserDao 타입 테스트 대역 필요
- UserServiceTest 전용이므로 스태틱 내부 클래스로 정의
```java
static class MockUserDao implements UserDao {
private List<User> users;
private List<User> updated = new ArrayList<User>();
private MockUserDao() {
}
private MockUserDao(List<User> users) {
this.users = users;
}
public List<User> getUpdated() {
return this.updated;
}
// Stub
@Override
public List<User> getAll() {
return this.users;
}
// Mock Object
@Override
public void update(User user) {
updated.add(user);
}
@Override
public void add(User user) {
throw new UnsupportedOperationException();
}
@Override
public User get(String id) {
throw new UnsupportedOperationException();
}
@Override
public void deleteAll() {
throw new UnsupportedOperationException();
}
@Override
public int getCount() {
throw new UnsupportedOperationException();
}
}
- UserDao 인터페이스 구현
- 사용하지 않는 메소드 역시 모두 구현해주어야 하며, UnsupportedOperationException을 던진다.
- 두 개의 User 타입
- 레벨 업그레이드 후보 User 오브젝트 목록
- 업그레이드 대상 오브젝트를 저장해 둘 목록
UserServiceTest
public class UserServiceTest {
@Test
public void upgradeLvls() throws Exception {
// 고립된 테스트에서는 테스트 대상 오브젝트를 직접 생성하면 된다.
UserServiceImpl userServiceImpl = new UserServiceImpl();
// 목 오브젝트로 만든 UserDao를 직접 DI gownsek.
MockUserDao mockUserDao = new MockUserDao(this.users);
userServiceImpl.setUserDao(mockUserDao);
MockMailSender mockMailSender = new MockMailSender();
userServiceImpl.setMailSender(mockMailSender);
userServiceImpl.upgradeLvls();
// MockUserDao로부터 업데이트 결과를 가져온다.
// 업데이트 횟수와 정보를 확인한다.
List<User> updated = mockUserDao.getUpdated();
assertThat(updated.size(), is(2));
checkUserAndLvl(updated.get(0), "joytouch", Lvl.SILVER);
checkUserAndLvl(updated.get(1), "madnite1", Lvl.GOLD);
List<String> request = mockMailSender.getRequests();
assertThat(request.size(), is(2));
assertThat(request.get(0), is(users.get(1).getEmail()));
assertThat(request.get(1), is(users.get(3).getEmail()));
}
/*
** id와 lvl을 확인하는 헬퍼 메서드
*/
private void checkUserAndLvl(User updated, String expectedId, Lvl expectedLvl) {
assertThat(updated.getId(), is(expectedId));
assertThat(updated.getLvl(), is(expectedLvl));
}
}
고립된 테스트
- 스프링 컨테이너에서 빈을 가져올 필요가 없다.
- UserServiceImpl 오브젝트를 직접 생성
- @RunWith를 제거할 수 있음(upgradeLvls() 테스트만 있다면)
- MockUserDao를 사용하도록 수동 DI
- UserDao의 update()를 이용해 몇 명의 사용자 정보를 DB에 수정하려고 했는지, 그 사용자는 누구인지, 어떤 레벨로 변경되었는지를 확인
테스트 수행 성능의 향상
- 직접적으로 필요하지 않은 의존 오브젝트와 서비스를 모두 제거했기 때문에 수행시간이 빨라졌다
6.2.3 단위 테스트와 통합 테스트
단위 테스트와 통합 테스트의 가이드 라인
- 항상 단위 테스트를 먼저 고려
- 스텁이나 목 오브젝트 등의 테스트 대역을 이용하도록 테스트를 만든다.
- 외부 리소스를 사용해야만 가능한 테스트는 통합테스트로 만든다.
- 대표적 통합테스트 : DAO는 DB를 통해 로직을 수행하는 인터페이스와 같은 역할
- DAO 테스트
- 외부 리소스를 사용하는 통합 테스트
- 하나의 기능 단위를 테스트
- 여러 개의 단위가 의존관계를 가지고 동작할 경우 통합 테스트 필요
- 단위 테스트를 만들기가 너무 복잡하다고 생각되는 테스트
- 스프링 테스트 컨텍스트 프레임워크를 이용하는 테스트는 통합테스트
- 통합 테스트를 해야하는 경우에도 가능한 많은 부분을 미리 단위 테스트로 검증해두는 것이 유용하다.
깔끔하고 좋은 코드가 테스트하기도 편하다
6.2.4 목 프레임워크
단위 테스트
- 만들기 위해 스텁이나 목 오브젝트의 사용이 필수적
- 작성이 번거롭다는 단점
Mockito 프레임워크
- 목 클래스를 일일이 준비해둘 필요가 없음
- static 메소드를 한 번 호출해주면 만들어짐
UserDao mockUserDao = mock(UserDao.class);
- static 메소드를 한 번 호출해주면 만들어짐
- 스텁 기능을 추가
- ex) getAll() 메소드가 불려올 때 사용자 목록을 리턴하도록
when(mockUserDao.getAll()).thenReturn(this.users);
- ex) getAll() 메소드가 불려올 때 사용자 목록을 리턴하도록
- 호출이 있었는지 검증하는 부분
- ex) update() 메소드가 두번 호출됐는지 확인하도록
verify(mockUserDao, times(2)).update(any(User.class));
- ex) update() 메소드가 두번 호출됐는지 확인하도록
Mockito 목 오브젝트 사용 방법
- 인터페이스를 이용해 목 오브젝트 생성
- 리턴할 값이 있으면 지정
- 테스트 대상 오브젝트에 DI
- 검증
사용 예시
public class UserServiceTest {
@Test
public void mockUpgradeLvls() throws Exception {
UserServiceImpl userServiceImpl = new UserServiceImpl();
UserDao mockUserDao = mock(UserDao.class);
when(mockUserDao.getAll()).thenReturn(this.users);
userServiceImpl.setUserDao(mockUserDao);
MailSender mockMailSender = mock(MailSender.class);
userServiceImpl.setMailSender(mockMailSender);
userServiceImpl.upgradeLvls();
verify(mockUserDao, times(2)).update(any(User.class));
verify(mockUserDao, times(2)).update(any(User.class));
verify(mockUserDao).update(users.get(1));
assertThat(users.get(1).getLvl(), is(Lvl.SILVER));
verify(mockUserDao).update(users.get(3));
assertThat(users.get(3).getLvl(), is(Lvl.GOLD));
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], is(users.get(1).getEmail()));
assertThat(mailMessages.get(1).getTo()[0], is(users.get(3).getEmail()));
}
}
- times() : 메소드 호출 횟수 검증
- any() : 파라미터의 내용을 무시
- 레벨의 변화는 assertThat으로 직접 확인
#
Chapater 6.3 다이내믹 프록시와 팩토리 빈
6.3.1 프록시와 프록시 패턴, 데코레이터 패턴
프록시
- 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 대리자, 대리인
타깃, 실체
- 최종적으로 요청을 위임받아 처리하는 실제 오브젝트
사용 목적에 따라 두 가지로 구분
- 프록시 패턴 : 클라이언트가 타깃에 접근하는 방법을 제어
- 데코레이터 패턴 : 타깃에 부가적인 기능을 부여
데코레이터 패턴
- 타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴
- 여러 개의 프록시를 단계적으로 위임하는 구조로 사용 가능
- ex) 클라이언트 -> 라인넘버 데코레이터 -> 컬러 데코레이터 -> 페이징 데코레이터 -> 소스코드 출력기능(타깃)
- 각 데코레이터의 다음 위임 대상은 인터페이스로 선언하고, 생성자는 수정자 메소드를 통해 위임 대상을 외부에서 런타임 시에 주입
- 어느 데코레이터에서 타깃으로 연결될지 코드 레벨에서는 미리 알 수 없다.
프록시 패턴
- 타깃에 대한 접근 방법을 제어
- 타깃 오브젝트를 생성하기 복잡하거나 당장 필요하지 않은 경우, 필요한 시점까지 오브젝트를 생성하지 않는 것이 좋다.
- 타깃 오브젝트에 대한 레퍼런스가 미리 필요할 수 있다.
- 원격 오브젝트를 이용하는 경우
- 특별한 상황에서 타깃에 대한 접근권한을 제어하는 경우
- 구조적으로 데코레이터 패턴과 유사하지만, 프록시는 코드에서 자신이 만들거나 접근할 타깃 클래스 정보를 알고 있는 경우가 많다.
6.3.2 다이내믹 프록시
프록시를 만드는 일은 상당히 번거롭다
- 6.2에서 목 오브젝트의 불편함을 해소한 것처럼 해결할 수 있을까??
프록시의 구성과 프록시 작성의 문제점
- 프록시의 두 가지 기능
- 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타깃 오브젝트로 위임
- 지정된 요청에 대해 부가기능을 수행
- UserServiceTx
- 기능 부가를 위한 프록시
public class UserServiceTx implements UserService { // 타깃 오브젝트 UserService userService; // ... // 메소드 구현과 위임 @Override public void add(User user) { this.userService.add(user); } // 메소드 구현 @Override public void upgradeLvls() { // 부가기능 수행 TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { // 위임 this.userService.upgradeLvls(); // 부가기능 수행 this.transactionManager.commit(status); } catch (RuntimeException e) { this.transactionManager.rollback(status); throw e; } } }
- 기능 부가를 위한 프록시
- 위임과 부가기능 수행 두 가지로 구분
- 프록시를 만들기 번거로운 이유?
- 코드를 작성하기가 번거로움
- 부가기능 코드의 중복 가능성
리플렉션
- 다이내믹 프록시는 리플렉션 기능을 이용해 프록시를 만들어 준다
- 리플렉션은 자바의 코드 자체를 추상화해서 접근하도록 만든 것
- 리플렉션 API 중 메소드에 대한 정의를 담은 Method 인터페이스로 메소드를 호출하는 방법?
- String 클래스 정보 : String.class / name.getClass()
- 길이 정보 : Method lengthMethod = String.class.getMethod(“length”);
- invoke()
- 메소드를 실행시킬 대산 오브젝트(obj)와 파라미터 목록(args)을 받아 메소드 호출 후 그 결과를 Object 타입으로 돌려줄 때 사용
int length = lengthMethod.invoke(name);
- 메소드를 실행시킬 대산 오브젝트(obj)와 파라미터 목록(args)을 받아 메소드 호출 후 그 결과를 Object 타입으로 돌려줄 때 사용
- 리플렉션 학습테스트
public class ReflectionTest { @Test public void invokeMethod() throws Exception { String name = "Spring"; // length() assertThat(name.length(), is(6)); Method lengthMethod = String.class.getMethod("length"); assertThat((Integer)lengthMethod.invoke(name), is(6)); // charAt assertThat(name.charAt(0), is('S')); Method charAtMethod = String.class.getMethod("charAt", int.class); assertThat((Character)charAtMethod.invoke(name, 0), is('S')); } }
- String 클래스의 length() 메소드와 charAt() 메소드를 코드에서 직접 호출하는 방법과, Method를 이용해 리플렉션 방식으로 호출하는 방법을 비교
프록시 클래스
- 다이내믹 프록시를 이용한 프록시
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 ReflectionTest { @Test public void simpleProxy() { // 타깃은 인터페이스를 통해 접근 Hello hello = new HelloTarget(); assertThat(hello.sayHello("Toby"), is("Hello Toby")); assertThat(hello.sayHi("Toby"), is("Hi Toby")); assertThat(hello.sayThankYou("Toby"), is("Thank You Toby")); } }
- 위를 토대로 Hello 인터페이스를 구현한 프록시를 만들어보자 (문자를 대문자로 바꿔주는 기능 추가)
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(); } }
- 테스트 코드
Hello proxiedHello = new HelloUppercase(new HelloTarget()); assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY")); assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY")); assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
- 문제점?
- 인터페이스의 모든 메소드를 구현해 위임하도록 코드를 만들어야 함
- 부가기능이 모든 메소드에 중복돼서 나타남
다이내믹 프록시 적용
- 다이내믹 프록시란 프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어지는 오브젝트를 말함
- 타깃의 인터페이스와 같은 타입으로 만들어짐
- 클라이언트는 타깃 인터페이스를 통해 사용 가능
- 다이내믹 프록시가 인터페이스 구현 클래스의 오브젝트는 만들어 주지만, 프록시로서 필요한 부가기능은 직접 작성해야 함
- 부가기능은 InvocationHandler를 구현한 오브젝트에 담음
public Object invoke(Object proxy, Method method, Object[] args)
- invoke()
- 리플렉션의 Method 인터페이스를 파라미터로 받음
- 메소드 호출 시 전달되는 파라미터도 args로 받음
- Hello 인터페이스의 메소드가 아무리 많아도 invoke() 메소드 하나로 처리 가능
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();
}
}
- 리턴 값을 대문자로 바꿔주는 InvocationHandler 구현 클래스
- 다이내믹 프록시로부터 요청을 전달받음
- 메소드 : invoke()
- 타깃 오브젝트의 메소드 호출이 끝나면 부가기능인 리턴 값을 대문자로 변환하고, 결과 리턴
Hello proxiedHello = (Hello)Proxy.newProxyInstance(
// 동적으로 생성되는 다이내믹 프록시 클래스 로딩에 사용할 클래스로더
getClass().getClassLoader(),
// 구현할 인터페이스
new Class[] { Hello.class },
// 부가기능과 위임 코드를 담은 InvocationHandler
new UppercaseHandler(new HelloTarget()));
//.....
- InvocationHandler를 사용하고 Hello 인터페이스를 구현하는 프록시
- 사용 방법
- 첫 번째 파라미터 : 클래스 로더 제공
- 두 번째 파라미터 : 다이내믹 프록시가 구현해야 할 인터페이스
- 마지막 파라미터 : 부가기능과 위임 관련 코드를 담은 InvocationHandler 구현 오브젝트
다이내믹 프록시의 확장
- String 외의 리턴 타입을 갖는 메소드가 추가된다면?
- 현재 강제로 String 타입으로 캐스팅을 하고 있으므로 캐스팅 오류 발생
- 스트링인 경우만 대문자로 변경
- InvocationHandler 방식은 타깃 종류에 상관 없이도 적용이 가능
- 위 두가지 포인트를 생각하며 수정한 UppercaseHandler
public class UppercaseHandler implements InvocationHandler { // 어떤 종류의 인터페이스를 구현한 타깃에도 적용 가능하도록 Object 타입으로 수정 Object target; public UppercaseHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object ret = method.invoke(target, args); // 호출한 메소드의 리턴 타입이 String인 경우에만 대문자 변경 기능을 적용 if (ret instanceof String) { return ((String)ret).toUpperCase(); } else { return ret; } } }
- InvocationHandler는 호출하는 메소드 이름, 파라미터 개수와 타입, 리턴 타입 등으로 부가적인 기능을 적용할 메소드를 선택할 수 있다.
6.3.3 다이내믹 프록시를 이용한 트랜잭션 부가기능
UserServiceTx를 다이내믹 프록시 방식으로 변경
- 현재 UserServiceTx는 인터페이스의 모든 메소드를 구현하여야하며, 트랜잭션이 필요한 메소드마다 트랜잭션 처리코드가 반복됨
- InvocationHandler를 정의하여 다이내믹 프록시를 적용
트랜잭션 InvocationHandler
public class TransactionHandler implements InvocationHandler {
// 부가기능을 제공할 타깃 오브젝트
// 어떤 타입의 오브젝트에도 적용 가능
private Object target;
// 트랜잭션 기능을 제공하는데 필요한 트랜잭션 매니져
private PlatformTransactionManager transactionManager;
// 트랜잭션을 적용할 메소드 이름 패턴
private String pattern;
public void setTarget(Object target) {
this.target = target;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void setPattern(String pattern) {
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();
}
}
}
- 트랜잭션 부가기능을 가진 핸들러
- TransactionHandler의 워크플로우
- 요청을 위임할 타깃을 DI 받음
- 적용할 대상 선별
- 일치할 경우 트랜잭션 적용 메소드 호출
- 일치하지 않을 경우 타깃 오브젝트 메소드 호출 후 결과 리턴
TransactionHandler와 다이내믹 프록시를 이용하는 테스트
public class UserServiceTest {
@Test
public void upgradeAllOrNothing() throws Exception {
// ...
TransactionHandler txHandler = new TransactionHandler();
// 트랜잭션 핸들러가 필요한 정보와 오브젝트를 DI
txHandler.setTarget(testUserService);
txHandler.setTransactionManager(transactionManager);
txHandler.setPattern("upgradeLvls");
// UserService 인터페이스 타입의 다이내믹 프록시 생성
UserService txUserService = (UserService)Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] { UserService.class },
txHandler
);
// ...
}
}
- UserServiceTx 대신 TransactionHandler를 이용하는 다이내믹 프록시를 사용하도록 수정
- UserServiceTx 오브젝트 대신 TransactionHandler를 만들고 타깃 오브젝트와 트랜잭션 매니저, 메소드 패턴을 주입
6.3.4 다이내믹 프록시를 위한 팩토리 빈
앞서 수정한 TransactionHandler와 다이내믹 프록시를 스프링의 DI를 통해 사용할 수 있도록
- 다이내믹 프록시 오브젝트는 일반적인 스프링 빈으로 등록할 방법이 없음
- 스프링의 빈은 기본적으로 클래스 이름과 프로퍼티로 정의됨
- 그러나, 다이내믹 프록시는 Proxy 클래스의 newProxyInstance()라는 스태틱 팩토리 메소드를 통해서만 생성 가능
팩토리 빈
- 스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈
- 만드는 가장 간단한 방법은 FactoryBean이라는 인터페이스를 구현하는 것
public interface FactoryBean<T> { // 빈 오브젝트를 생성해서 돌려줌 T getObject() throws Exception; // 셍성되는 오브젝트의 타입을 알려줌 Class<? extends T> getObjectType(); // getObject()가 돌려줒는 오브젝트가 항상 싱글톤 오브젝트인지 알려줌 boolean isSigleton(); }
- 학습 테스트
// 생성자를 제공하지 않는 클래스 public class Message { String text; // 생성자가 private으로 선언되어 외부에서 생성자를 통해 오브젝트를 만들 수 없다. 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; /* * 오브젝트를 생성할 때 필요한 정보를 팩토리 빈의 프로퍼티로 설정하여 대신 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; } /* * getObject()가 돌려주는 오브젝트가 싱글톤인지 알려준다. * 이 팩토리 빈은 요청할 때마다 새로운 오브젝트를 만들어주므로 false * 이것은 팩토리 빈의 동작방식에 관한 설정이고, * 만들어진 빈 오브젝트는 싱글톤으로 스프링이 관리해줄 수 있다. */ @Override public boolean isSingleton() { return false; } }
- 팩토리 빈은 팩토리 메소드를 가진 오브젝트
- FactoryBean을 구현한 클래스가 빈으로 등록되면, 팩토리 빈 클래스의 오브젝트를 getObject()를 이용해 가져오고, 이를 빈 오브젝트로 사용
팩토리 빈의 설정 방법
- 일반 빈과 비슷 ```
- 다른 점은 message 빈 오브젝트의 타입은 class 애트리뷰트에 정의된 MessageFactoryBean이 아닌 Message 타입이라는 점
- Message 빈의 타입은 getObjectType() 메소드가 돌려주는 타입으로 결정됨
- getObject() 메소드가 생성해주는 오브젝트가 message 빈의 오브젝트가 됨
```java
@RunWith(SpringJUnit4ClassRunner.class)
// 설정파일 이름을 지정하지 않으면 클래스이름 + "-context.xml" 파일을 찾아서 사용함
// 같은 패키지 내에 위치해있어야 함
@ContextConfiguration
public class FactoryBeanTest {
@Autowired
ApplicationContext context;
@Test
public void getMessageFromFactoryBean() {
Object message = context.getBean("message");
// 타입 확인
// is(class) Deprecated -> is(instanceOf(class))
assertThat(message, is(instanceOf(Message.class)));
// 설정과 기능 확인
assertThat(((Message)message).getText(), is("Factory Bean"));
}
}
- message 빈의 타입이 정확히 무엇인지 확실하지 않으므로 ApllicationContext를 이용해 getBean()을 사용 타입을 지정하지 않으면 Object 타입으로 리턴
- getBean()이 리턴한 오브젝트는 Message 타입이어야 함
- MessageFactoryBean을 통해 text 프로퍼티의 값이 바르게 주입되었는지 확인
다이내믹 프록시를 만들어주는 팩토리 빈
- 팩토리 빈 사용법
- getObject() 메소드에 다이내믹 프록시 오브젝트를 만들어주는 코드를 삽입
- 스프링 빈에는 팩토리 빈과 UserServiceImpl만 등록
트랜잭션 프록시 팩토리 빈
// 생성할 오브젝트 타입을 지정할 수도 있지만 범용적으로 사용하기 위해 Object로 함
public class TxProxyFactoryBean implements FactoryBean<Object> {
// TransactionHandler 생성 시 필요한 프로퍼티
Object target;
PlatformTransactionManager transactionManager;
String pattern;
// 다이내믹 프록시를 생성할 때 필요
// UserService 외 인터페이스를 가진 타겟에도 적용 가능
Class<?> serviceInterface;
public void setTarget(Object target) {
this.target = target;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
public void setServiceInterface(Class<?> serviceInterface) {
this.serviceInterface = serviceInterface;
}
// DI 받은 정보를 이용하여 TransactionHandelr를 사용하는 다이내믹 프록시를 생성
@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
);
}
@Override
public Class<?> getObjectType() {
// 팩토리 빈이 생성하는 오브젝트의 타입은 DI 받은 인터페이스 타입에 따라 달라진다.
// 다양한 타입의 프록시 오브젝트 생성에 재사용 가능
return serviceInterface;
}
@Override
public boolean isSingleton() {
// 싱글톤 빈이 아니라는 뜻이 아니라 getObject()가 매번 같은 오브젝트를 리턴하지 않는다는 의미
return false;
}
}
- TH를 이용하는 다이내믹 프록시를 생성하는 팩토리 빈 클래스
- 팩토리 빈이 만드는 다이내믹 프록시는 구현 인터페이스나 타깃의 종류에 제한이 없다.
- 빈 설정 ```
- serviceInterface
- Class 타입
- value를 이용하여 클래스 또는 인터페이스 이름을 넣어주면 됨
- 프로퍼티 타입이 클래스인 경우, value로 설정해 준 이름을 가진 Class 오브젝트로 변환해 줌
### 트랜잭션 프록시 팩토리 빈 테스트
- UserServiceTest의 upgradeAllOrNothing()
- add()는 @AutoWired로 가져온 userService 빈을 사용하기 때문에 TxProxyFactoryBean 팩토리 빈이 생성하는 다이내믹 프록시 이용
- 반면 upgradeLvls(), mockUpgradeLvls()는 목 오브젝트를 이용하므로 트랜잭션과 무관
- upgradeAllOrNothing()는 현재 수동 DI를 하고 있으므로 팩토리 빈이 적용되지 않음
- 타깃 오브젝트에 대한 레퍼런스는 TransactionHandler가 갖고 있음. 그런데 TransactionHandler는 TxProxyFactoryBean 내부에서 만들어지므로 별도로 참조할 방법이 없음
- TxProxyFactoryBean을 직접 가져와서 프록시를 만들어보자
- 스프링 빈으로 등록된 TxProxyFactoryBean을 가져와 target 프로퍼티를 재구성 후 다시 프록시 오브젝트를 생성하도록 요청
- 컨텍스트의 설정이 변경되므로 DirtiesContext 필요
```java
public class UserServiceTest {
// 팩토리 빈을 가져오기 위해서는 애플리케이션 컨텍스트가 필요
@Autowired ApplicationContext context;
@Test
// 다이내믹 프록시 팩토리 빈을 직접 만들어 사용할 때는 없앴다가 다시 등잘
@DirtiesContext
public void upgradeAllOrNothing() throws Exception {
TestUserService testUserService = new TestUserService(users.get(3).getId());
testUserService.setUserDao(userDao);
testUserService.setMailSender(mailSender);
// 팩토리 빈 잧를 가져와야 하므로 '&' 필요
// 테스트용 타깃 주입
TxProxyFactoryBean txProxyFactoryBean = context.getBean("&userService", TxProxyFactoryBean.class);
txProxyFactoryBean.setTarget(testUserService);
// 변경된 타깃 설정을 이용해 트랜잭션 다이내믹 프록시를 다시 생성
UserService txUserService = (UserService)txProxyFactoryBean.getObject();
userDao.deleteAll();
for (User user : users) {
userDao.add(user);
}
try {
txUserService.upgradeLvls();
fail("TestUserServiceException expected");
} catch (TestUserServiceException e) {
}
checkLvlUpgraded(users.get(1), false);
}
}
6.3.5 프록시 팩토리 빈 방식의 장점과 한계
프록시 팩토리 빈의 재사용
- TransactionHandler를 이용하는 다이내믹 프록시를 생성해주는 TxProxyFactoryBean은 코드 수정 없이 다양한 클래스에 적용가능
- 빈에 트랜잭션 기능이 필요하면 UserService에 적용하느라 만들었던 TxProxyFactoryBean을 그대로 적용해주면 됨
- 설정 변경을 통한 트랜잭션 기능 부가
- 클라이언트 ———————–> CoreServiceImpl
- 클라이언트 -> TxProxyFactoryBean -> CoreServiceImpl
프록시 팩토리 빈 방식의 장점
- 데코레이터 패턴 적용의 문제점을 해결
- 타깃 인터페이스를 구현하는 클래스를 일일이 만드는 번거로움 제거
- 부가 기능 코드의 중복 문제 해결
프록시 팩토리 빈의 한계
- 프록시를 통해 부가기능을 제공하는 것은 메소드 단위로 일어난다
- 한 번에 여러 개의 클래스에 공통적인 부가기능 제공이 불가
- 하나의 타깃에 여러 개의 부가 기능을 적용할 때 문제
- TransactionHandler 오브젝트가 프록시 팩토리 빈 개수만큼 만들어짐
댓글남기기