2020-5-25 토비의 스프링 6장 AOP

6장 AOP

  • AOP를 통해 선언적 트랜잭션을 사용할 수 있다. 이를 통해 트랜잭션 경계설정을 깔끔하게 할 수 있다.
//Before
public void upgradeLevels() {
        TransactionStatus status =
          this.transactionManager.getTransaction(new DefaultTransactionDefinition());
  try {
    List<User> users = userDao.getAll();
    for (User user : users) {
      if (canUpgradeLevel(user)) {
        upgradeLevel(user);
      }
    }
    this.transactionManager.commit(status);
  } catch (RuntimeException e) {
    this.transactionManager.rollback(status);
    throw e;
  }
}

// 메서드분리
public void upgradeLevels() {
  TransactionStatus status =
    this.transactionManager.getTransaction(new DefaultTransactionDefinition());
  try {
    upgradeLevelsInternal();
    this.transactionManager.commit(status);
  } catch (RuntimeException e) {
    this.transactionManager.rollback(status);
    throw e;
  }
}

private void upgradeLevelsInternal() {
  List<User> users = userDao.getAll();
  for (User user : users) {
    if (canUpgradeLevel(user)) {
      upgradeLevel(user);
    }
  }
}
  • 가운데 users를 제외하고 나머지 전후가 트랜잭션경계를 설정하는 부분이다. 트랜잭션 경계를 나누는 부분하고 비즈니스 로직이 함께 있기때문에 가독성이 매우 안좋다.
  • 비즈니스로직 코드와 트랜잭션 설정 코드간의 정보를 주고받는게 없기때문에 메서드 분리를 할 수 있다. 그래도 여전히 트랜잭션 담당이 UserService에 있는것은 깔끔하지 않다.
  • 약한 결함을 위해 UserService인터페이스를 정의했는데, 2가지 구현체를 구현해서. 이용하면?? 구현체를 2개를 만들어서 하나는 UserService 비즈니스로직에 집중. 다른 하나는 외부에서 트랜잭션을 조작하는 역할로 설정할 수 있다.
public class UserServiceTx implements UserService {
  private UserService userService;
  private PlatformTransactionManager transactionManager;

  @Override
  public void add(User user) {
    userService.add(user);
  }

  @Override
  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;
    }
  }

  public UserServiceTx setUserService(UserService userService) {
    this.userService = userService;
    return this;
  }

  public UserServiceTx setTransactionManager(PlatformTransactionManager transactionManager) {
    this.transactionManager = transactionManager;
    return this;
  }
}

  • UserService를 구현한 다른 클래스를 DI를 통해 주입받는다.
  • 비즈니스로직을 구현한 구현체 외부클래스를 통해 트랜잭션 기능을 지원할 수 있다. 이를 통해 UserServiceImpl의 코드를 작성할때는 비즈니스 로직만 집중할 수 있다.
  • applicationContext.xml의 의존관계를 변경해야한다.

테스트 분리

public class UserServiceTest {

  @Autowired
  UserServiceImpl userService;

  @Autowired
  UserDao userDao;

  @Autowired
  MailSender mailSender;

  @Autowired
  PlatformTransactionManager transactionManager;
  ...
}
  • 현재 UserService는 UserDao, TransactionManager, MailSender 의존관계를 주입받고 있다. UserService가 실행될때 마다 이 의존관계들도 함께 실행된다. 테스트 대상 오브젝트를 고립시켜야된다.

단위테스트와 통합테스트

  • 단위테스트 : 테스트 대상 클래스를 목 오브젝트 등의 테스트 대역을 이요해 외부 리소스를 사용하지 않고 고립시켜 테스트
  • 통합테스트 : 다른 오브젝트와 함께 연동하여 테스트하고 DB나 파일, 서비스등의 리스소가 참여하는 테스트
  • 테스트가 있어야 코드의 품질이 높아지고, 리팩토링과 개선을 할 수 있는 여지가 있다.

가이드라인

  • 항상 단위테스트부터 고려해라

  • 성격이나 목적이 같은 클래스를 몇개모아서 의존관계를 차단하고 스텁이나 목 오브젝트 등의 테스트 대역을 이용해라.

  • 외부리소스를 사용하면 통합테스트로 만들어라.

  • DAO는 단위테스트로 만들기 어렵다. DAO는 DB까지 연동하는 테스트로 만드는게 좋다.

  • 여러개의 단위가 의존관계를 맺는 통합테스트가 필요하다.

  • 단위테스트가 복잡하면 통합테스트를 고려해라

  • 스프렝 테스트 컨텍스트 프레임워크가 하는 역할이 통합테스트다.

  • Mock프레임워클르 써라. 대표적으로 Mockito

    • // Ex
      UserDao mockUserDao = mock(UserDao.class);
      when(mockUserDao.getAll()).thenReturn(this.users);
      verify(mockUserDao, time(2)).update(any(User.class));
      

다이나믹 프로식와 팩토리빈

배경 및 문제

  • 핵심기능 코드를 구현하는 부분과 부가기능 위임하는 부분을 분리했다. 핵심기능은 UserServiceImpl, 부가기능은 UserServiceTx로 만들었다.
  • 부가기능 외 모든 핵심기능은 핵심클래스로 위엄해줘야되고. 핵심기능클래스는 부가기능을 가진 클래스의 존재 자체를 말라서 부가기능을 가진 클래스가 핵심기능의 클래스를 사용하는 구조가 나온다.
  • 문제 : 클라리언트가 부가기능 클래스가 아닌 핵심기능을 클래스를 사용하면 문제..
    • 그래서 부가기능을 핵심기능을 가진 클래스처럼 꾸미고 클라이언트가 이를 통해 핵심기능을 사용하도록 해야한다.
    • 클라이언트는 인터페이스만을 통해 핵심기능을 사용해야한다. 부가기능도 핵심기능과 같은 인터페이스를 구현한 뒤 감싸야한다. 이를 프록시.
    • 클라이언트는 프록시를 통해 타깃(실제 핵심기능 객체)에 접근하게 된다. 프록시가 타깃을 제어할 수 있다.

프로시의 구분

  1. 클라이언트가 타깃에 접근하는 방법을 제어하기 위해
  2. 타깃에 부가적인 기능을 제공하기 위해
  • 프록시는 데코레이터 패턴과 프록시패턴으로 구현할 수 있다.

데코레이터패턴

  • 타깃의 부가기능을 런타임시 다이내믹하게 부여하기 위해 사용. 코드상에는 어떤 방법과 순서로 연결되어있는지 정해지지 않았다.
  • 데코레이터에서는 프록시를 여러개 둘 수 있고, 같은 인터페이스를 구현한 여러개의 프록시를 사용할 수 있다.
    • ex) 소스코드 출력하는 기능 : 라인넘버 프록시, 칼라변경 프록시, 특정폭변경 프록시 .. 등등등
  • 데코레이터에서는 위임하는 대상도 인터페이스로 접근하기 때문에 자신이 최종 타깃으로 위임하는지 다음 데코레이터 프록시로 위임하는지 알지 못한다. 이를 위해 런타임시 인터페이스로 외부주입을 받아야한다.
    • ex) Java IO package InputStream, OutputStream
    • InputStream is = new BufferedInputStream(New FileInputStream(“a.txt”));
  • UserServiceImpl에 UserServiceTx의 역할이 데코레이터. 여기서도 UserService란 동일한 인터페이스를 구현했지만, UserServiceTx에 UserServiceImpl라는 구체적인 클래스를 주입했다.

프록시패턴

  • 프록시패턴은 프록시구분에서 타깃에 대한 접근 방법을 제어하기 위한 목적이다. 타깃의 기능을 확장하거나 추가하지 않고 접근하는 방식을 변경해준다.

다이나믹 프록시

  • 프록시도 매번 일일이 인터페이슬르 구현해서 만들기는 어렵다. 편리하게 만들기 위한 방법은?

  • 프록시구성과 프록시 작성의 문제
    • 타깃과 같은 메서드를 구현하고 있다가 메서드가 호출되면 타깃오브젝트로 위임한다.
    • 지정된 요청에 대해 부가기능을 수행. 트랜잭션 부가기능을 하기 위해 만든게 UserTx였다.
    • 프록시 코드를 보면 요청을 위임하는 부분과 부가기능을 수행하는 부분으로 나눌 수 있다.
  • 프록시가 번거로운이유
    • 타깃의 인터페이스를 구현하고 위임코드를 작성하기가 번겨러보다. 부가기능이 필요없는 메서드도 일일이 다 만들어야한다.
    • 부가기능 코드가 중복될 가능성이 많다.
    • 두번째 문제는 메서드 분리를 통해 해결할 수 있지만, 첫번째 문제는 해결하기 어렵다. 이때 JDK의 다이나믹 프록시를 쓴다.
리플렉션
  • 다이나믹 프록시는 리플렉션 기능을 이용해서 프록시를 만든다. 리플렉션은 자바 코드자체를 추상화해서 접근한다.

    • String name = "string"
        // 길이를 알고싶으면 length()를 호출하면 된다. 일반적으로는 name.length();
      
      // reflection
      Method leng = String.class.getMethod("length");
      
      public class ReflectionTest {
        @Test
        public void intvokeMethod() throws Exception {
          String name = "Spring";
      
          // length();
          assertThat(name.length(), is(6));
      
          Method lengMethod = String.class.getMethod("length");
          assertThat((Integer)lengMethod.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"));
        }
      }
      
    • 자바의 모든 클래스는 클래스 자체의 구성정보를 담는 Class타입의 오브젝트를 갖고 있다. “className.class” or getClass()를 호출한다.

    • **클래스 오브젝트를 통해 클래스 코드에 대한 메타정보를 가져오거나 오브젝트를 조작하고 수정할 수 있다. ex) 클래스 이름, 어떤 클래스 상속, 인터페이스 구현여부, 필드들. 각각의 타입, 정의된 메서드들. 메서드의 파라미터와 리턴값 등등.

    • Java.lang.reflect패키지 참조

    • Method의 invoke()를 실행시키면 호출할 수 도 있다.

    • 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 {
          private Hello hello;
          public HelloUppercase(Hello hello) {
              this.hello = hello;
          }
          @Override
          public String sayHello(String name) {
              return this.hello.sayHello(name).toUpperCase();
          }
          @Override
          public String sayHi(String name) {
              return this.hello.sayHi(name).toUpperCase();
          }
          @Override
          public String sayThankYou(String name) {
              return this.hello.sayThankYou(name).toUpperCase();
          }
      }
      
      public class simpleProxy {
          @Test
          public void target() {
              Hello hello = new HelloTarget();
              assertThat(hello.sayHello("toby"), is("hello toby"));
          }
          @Test
          public void proxy() {
              Hello proxiedHello = new HelloUppercase(new HelloTarget());
              assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
          }
      }
      
    • 위의 프록시는 인터페이스의 모든 메서드를 구현하고 위임하는 문제와 부가기능인 리턴값을 대문자로 바꾸는 기능이 모든 메서드에 중복되서 나타난다.
다이나믹 프록시 적용
  • 다이나믹 프록시는 프록시 팩토리에 의해 런타임 시 다이나믹하게 만들어지는 오브젝트 프록시팩토리에 인터페이스 정보만 제공해주면 해당 인터페이스를 구현한 오브젝트를 자동으로 만들어준다. 부가기능은 직접 작성해야한다.

  • 프록시 오브젝트는 인터페이스와 같은 타입으로 만들어지고, 클라이언트는 타깃 인터페이스를 통해 접근할 수 있다.

    • public Object invoke(Object proxy, Method method, Object [] args)
      
      // 다이나믹 프록시 오브젝트는 클라이언트의 모든 요청을 리플렉션 정보로 변환해서 InvocationHandler구현 오브젝트의 invoke()로 넘긴다.
      
다이나믹 프록시 만들기
public class UppercaseHandler implements InvocationHandler {
    private Object 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(); // 부가기능
    }
}

...
@Test
public void dynamicProxy() {
  Hello proxyHello = (Hello) Proxy.newProxyInstance(
  getClass().getClassLoader(), // 동적으로 생성되는 다이나믹 프록시 클래스 로딩에 사용할 클래스로더
  new Class[] {Hello.class},  // 구현할 인터페이스
  new UppercaseHandler(new HelloTarget())); // 부가기능과 위임코드를 담은 InvocationHandler
}
  • InvocationHandler : 다이나믹 프록시로부터 메서드 호출정보를 받아 처리한다.
  • 클래스로더를 제공. 다이나막 프록시가 정의되는 클래스 로더를 지정. 두번째 파라미터 구현해야할 인터페이스. 다이나믹 프록시는 한번에 하나 이상의 인터페이스를 구현할 수 있어서 배열을 사용. 마지막 파라미터는 부가기능과 위임에 대한 InvocationHandler구현오브젝트.
  • newProxyInstance()에 의해 만들어진 다이나믹 프록시 오브젝트는 파라미터로 제공한 Hello 인터페이스의 구현체이기 때문에 Hello타입으로 타입 캐스팅이 가능하다.
  • UppercaseHandler의 모든 메서드의 리턴타입이 String인데, 다른 타입으로 리턴하면??
  • target을 Object로 받으면 재사용할 수 있다.. 하지만 문제가..??..

트랜잭션 부가기능. UserServiceTx변경.

public class TransactionHandler implements InvocationHandler {
  private Object target; // 타깃오브젝트. 어떤 타입도 된다.
  private PlatformTransactionManager transactionManager; // 트랜잭션 기능을 제공하는 트랜잭션 매니저
  private String 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();
    }
  }
  ... setter
}
// UserServiceTest.class
@Test
public void upgradeAllOrNothing() {
  UserServiceImpl testUserService = new TestUserServiceImpl(users.get(3).getId());
  testUserService.setUserDao(this.userDao);
  testUserService.setMailSender(this.mailSender);

  TransactionHandler txHandler = new TransactionHandler();
  txHandler.setTarget(testUserService);
  txHandler.setTransactionManager(transactionManager);
  txHandler.setPattern("upgradeLevels");
  UserService txUserService = (UserService) Proxy.newProxyInstance(
    getClass().getClassLoader(), new Class[] {UserService.class}, txHandler
  );

  userDao.deleteAll();
  for(User user : users) userDao.add(user);

  try {
    txUserService.upgradeLevels();
    fail("TestUserServiceException expected");
  }
  catch(TestUserServiceException e) {
  }
}
  • 타깃을 DI로 받는다. 타깃은 Object로 선언. 어떤 타깃에도 적용가능하다.

다이나믹 프록시를 위한 팩토리빈

  • 앞선 방법에서 다이나믹 프록시 오브젝트는 스프링빈으로 등록할 방법이없다.

  • 스프링은 내부적으로 리플렉션을 이용해서 빈오브젝트를 생성한다. 다이나믹 프록시의 경우 이렇게 생성하지 못한다.

  • 스프링빈에서는 팩토리빈을 이용해서 기본생성자말고 다른 방식으로 빈을 생성할 수 있다.

    • public class Message {
        String text;
      
        private Message(String text) {
          this.text = text;
        }
      
        public static newMessage(String text) {
          return new Message(text); // 정적 팩토리메서드
        }
      }
      
      public class MessageFactoryBean implements FactoryBean<Message> {
        private String text;
      
        public void setText(String text) {
          this.text = text;
        }
        @Override
        public Message getObject() throws Exception {
          return Message.newMessage(text);
        }
      
        @Override
        public Class<?> getObjectType() {
          return Message.class;
        }
      
        @Override
        public boolean isSingleton() {
          return false;
        }
      }
      
      // applicationContext.xml
      <bean id="message" class="toby.factory.MessageFactoryBean">
        <property name="text" value="Factory Bean" />
        </bean>
      
    • 이렇게 선언하면 정적팩토리 메서드를 통해 생성해야되지 빈으로 못만든다. 리플렉션은 private접근 규약을 위반할 수 있지만 private생성자로 만든데는 이유가 있다. 따라서 쓰지 말자.

    • 스프링은 FactoryBean 인터페이스를 구현한 클래스가 빈의 클래스로 지정되면 빈 클래스의 오브젝트의 getObject()를 이용해 오브젝트를 가져오고 이를 빈오프젝트로 사용한다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:test-applicationContext.xml")
public class FactoryBeanTest {
  @Autowired
  private ApplicationContext context;

  @Test
  public void getMessageFromFactoryBean() {
    Object message = context.getBean("message");
    assertThat(message, is(instanceOf(Message.class))); // check type
    assertThat(((Message)message).getText(), is("Factory Bean"));
  }
}
  • xml에는 message가 MessageFactoryBean.class로 정의되었는데, 실제로는 Message타입이다.
  • Message빈타입은 MessageFactoryBean의 getObjectType()가 돌려주는 타입으로 결정된고 getObject()으로 생성되는 오브젝트가 message빈의 오브젝트가 된다.!!!!!
    • 드물게 Message가 아닌 FactoryBean자체를 가져오고 싶을때가 있다. context.getBean(“&message”)처럼 &만 붙여주면 팩토리빈 자체를 돌려준다.
트랜잭션 프록시빈
public class TxProxyFactoryBean implements FactoryBean<Object> {
    private Object target;
    private PlatformTransactionManager transactionManager;
    private String pattern;
    private Class<?> serviceInterface;

    @Override
    public Object getObject() throws Exception {
        TransactionHandler txHandler = new TransactionHandler();
        txHandler.setTransactionManager(transactionManager);
        txHandler.setPattern(pattern);
        txHandler.setTarget(target);
        return Proxy.newProxyInstance(
                getClass().getClassLoader(), new Class[] {serviceInterface}, txHandler
        );
    }

    @Override
    public Class<?> getObjectType() {
        return serviceInterface;
    }

    @Override
    public boolean isSingleton() {
        return false;
    }
    ...setter...
}

//applicationContext.xml
<bean id="userService" class="toby.service.TxProxyFactoryBean">
  <property name="transactionManager" ref="transactionManager" />
  <property name="target" ref="userServiceImpl" />
  <property name="pattern" value="upgradeLevels" />
  <property name="serviceInterface" value="toby.service.UserService" />
</bean>
  • userServiceBean이라는 이름으로 TxProxyFactoryBean을 등록했다. target, tarnasactionManager프로퍼티는 다른빈을 가리키기 때문에 ref설정. pattern은 문자열이라 value로 매핑
  • serviceInterface는 Class타입이다. Class타입은 value를 이용해서 인터페이스이름 또는 클래스를 넣어준다. 프로퍼티의 타입이 Class인 경우는 value로 설정한 이름을 가진 Class오브젝트로 자동 변환
프록시팩토리 빈 방식의 장점과 한계
  • 팩토리빈을 사용하면 타깃의 상관없이 재사용할 수 있는점이 좋다.
  • TransactionHandler를 이용하는 다이나믹 프록시를 생성하는 TxProxyFactoryBean은 코드의 수정 없이도 다양한 클래스에 적용할 수 있다. 타깃 오브젝트에 맞는 프로퍼티만 설정하면 된다.
  • 장점
    • 인터페이스를 구현하는 클래스를 일일이 만들지 않아도 된다.
    • 핸들러 메서드를 구현하면 부가기능을 부여해주고 중복문제도 사라진다.
    • DI를 이용하면 프록시 생성코드도 제거할 수 있다.
  • 한계
    • 부가기능은 메서드단위로 일어나나지만 여러개의 메서드에 부가기능을 제공할 수는 있지만 여러개의 클래스에 공통적인 부가기능을 제공하는건 불가능하다.
    • TransactionHandler 오브젝트가 프록시 팩토리 빈 갯수만큼 만들어진다. 이게 문제다….
Written on May 25, 2020