[토비의 스프링] Week8(6.1~6.3)

36 분 소요

토비의 스프링 3.1 Chapter 6.1 ~ 6.3

Chapater 6.1 트랜잭션 코드의 분리

깔끔한 트랜잭션 인터페이스 사용! but..

  • 비즈니스 로직이 주인이어야 할 메소드 안에 이름도 길고 무시무시하게 생긴 트랜잭션 코드가 더 많은 자리를 차지하고 있는 모습

6.1.1 메소드 분리

기존 코드의 특징

  1. 두 가지 종류의 코드가 구분되어 있음
  2. 두 코드 간 주고 받는 정보가 없음
    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을 상속해야함
      1. 트랜잭션 롤백의 확인을 위한 예외 발생 코드가 UserServiceImpl에 있기 때문
      2. 그런데 트랜잭션 기능이 없어짐
    • 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()의 테스트 단계
    1. DB 테스트 데이터 준비
    2. 메일 발송 여부 확인을 위한 목 오브젝트를 DI해줌
    3. 테스트 대상 실행
    4. DB에 저장된 결과 확인
    5. 목 오브젝트를 이용한 결과 확인

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 타입
    1. 레벨 업그레이드 후보 User 오브젝트 목록
    2. 업그레이드 대상 오브젝트를 저장해 둘 목록

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);
      
  • 스텁 기능을 추가
    • ex) getAll() 메소드가 불려올 때 사용자 목록을 리턴하도록
      when(mockUserDao.getAll()).thenReturn(this.users);
      
  • 호출이 있었는지 검증하는 부분
    • ex) update() 메소드가 두번 호출됐는지 확인하도록
      verify(mockUserDao, times(2)).update(any(User.class));
      

Mockito 목 오브젝트 사용 방법

  1. 인터페이스를 이용해 목 오브젝트 생성
  2. 리턴할 값이 있으면 지정
  3. 테스트 대상 오브젝트에 DI
  4. 검증

사용 예시

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 프록시와 프록시 패턴, 데코레이터 패턴

프록시

  • 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 대리자, 대리인

타깃, 실체

  • 최종적으로 요청을 위임받아 처리하는 실제 오브젝트

사용 목적에 따라 두 가지로 구분

  1. 프록시 패턴 : 클라이언트가 타깃에 접근하는 방법을 제어
  2. 데코레이터 패턴 : 타깃에 부가적인 기능을 부여

데코레이터 패턴

  • 타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴
    • 여러 개의 프록시를 단계적으로 위임하는 구조로 사용 가능
    • ex) 클라이언트 -> 라인넘버 데코레이터 -> 컬러 데코레이터 -> 페이징 데코레이터 -> 소스코드 출력기능(타깃)
  • 각 데코레이터의 다음 위임 대상은 인터페이스로 선언하고, 생성자는 수정자 메소드를 통해 위임 대상을 외부에서 런타임 시에 주입
  • 어느 데코레이터에서 타깃으로 연결될지 코드 레벨에서는 미리 알 수 없다.

    프록시 패턴

  • 타깃에 대한 접근 방법을 제어
  • 타깃 오브젝트를 생성하기 복잡하거나 당장 필요하지 않은 경우, 필요한 시점까지 오브젝트를 생성하지 않는 것이 좋다.
    1. 타깃 오브젝트에 대한 레퍼런스가 미리 필요할 수 있다.
    2. 원격 오브젝트를 이용하는 경우
    3. 특별한 상황에서 타깃에 대한 접근권한을 제어하는 경우
  • 구조적으로 데코레이터 패턴과 유사하지만, 프록시는 코드에서 자신이 만들거나 접근할 타깃 클래스 정보를 알고 있는 경우가 많다.

6.3.2 다이내믹 프록시

프록시를 만드는 일은 상당히 번거롭다

  • 6.2에서 목 오브젝트의 불편함을 해소한 것처럼 해결할 수 있을까??

프록시의 구성과 프록시 작성의 문제점

  • 프록시의 두 가지 기능
    1. 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타깃 오브젝트로 위임
    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;
          }
      }
      }
      
  • 위임과 부가기능 수행 두 가지로 구분
  • 프록시를 만들기 번거로운 이유?
    1. 코드를 작성하기가 번거로움
    2. 부가기능 코드의 중복 가능성

리플렉션

  • 다이내믹 프록시는 리플렉션 기능을 이용해 프록시를 만들어 준다
    • 리플렉션은 자바의 코드 자체를 추상화해서 접근하도록 만든 것
  • 리플렉션 API 중 메소드에 대한 정의를 담은 Method 인터페이스로 메소드를 호출하는 방법?
    • String 클래스 정보 : String.class / name.getClass()
    • 길이 정보 : Method lengthMethod = String.class.getMethod(“length”);
  • invoke()
    • 메소드를 실행시킬 대산 오브젝트(obj)와 파라미터 목록(args)을 받아 메소드 호출 후 그 결과를 Object 타입으로 돌려줄 때 사용
      int length = lengthMethod.invoke(name);
      
  • 리플렉션 학습테스트
    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"));
    
  • 문제점?
    1. 인터페이스의 모든 메소드를 구현해 위임하도록 코드를 만들어야 함
    2. 부가기능이 모든 메소드에 중복돼서 나타남

다이내믹 프록시 적용

  • 다이내믹 프록시란 프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어지는 오브젝트를 말함
    • 타깃의 인터페이스와 같은 타입으로 만들어짐
    • 클라이언트는 타깃 인터페이스를 통해 사용 가능
  • 다이내믹 프록시가 인터페이스 구현 클래스의 오브젝트는 만들어 주지만, 프록시로서 필요한 부가기능은 직접 작성해야 함
    • 부가기능은 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 인터페이스를 구현하는 프록시
  • 사용 방법
    1. 첫 번째 파라미터 : 클래스 로더 제공
    2. 두 번째 파라미터 : 다이내믹 프록시가 구현해야 할 인터페이스
    3. 마지막 파라미터 : 부가기능과 위임 관련 코드를 담은 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의 워크플로우
    1. 요청을 위임할 타깃을 DI 받음
    2. 적용할 대상 선별
    3. 일치할 경우 트랜잭션 적용 메소드 호출
    4. 일치하지 않을 경우 타깃 오브젝트 메소드 호출 후 결과 리턴

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

프록시 팩토리 빈 방식의 장점

  • 데코레이터 패턴 적용의 문제점을 해결
    1. 타깃 인터페이스를 구현하는 클래스를 일일이 만드는 번거로움 제거
    2. 부가 기능 코드의 중복 문제 해결

프록시 팩토리 빈의 한계

  • 프록시를 통해 부가기능을 제공하는 것은 메소드 단위로 일어난다
    1. 한 번에 여러 개의 클래스에 공통적인 부가기능 제공이 불가
    2. 하나의 타깃에 여러 개의 부가 기능을 적용할 때 문제
    3. TransactionHandler 오브젝트가 프록시 팩토리 빈 개수만큼 만들어짐

댓글남기기