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로 만들었다.
- 부가기능 외 모든 핵심기능은 핵심클래스로 위엄해줘야되고. 핵심기능클래스는 부가기능을 가진 클래스의 존재 자체를 말라서 부가기능을 가진 클래스가 핵심기능의 클래스를 사용하는 구조가 나온다.
- 문제 : 클라리언트가 부가기능 클래스가 아닌 핵심기능을 클래스를 사용하면 문제..
- 그래서 부가기능을 핵심기능을 가진 클래스처럼 꾸미고 클라이언트가 이를 통해 핵심기능을 사용하도록 해야한다.
- 클라이언트는 인터페이스만을 통해 핵심기능을 사용해야한다. 부가기능도 핵심기능과 같은 인터페이스를 구현한 뒤 감싸야한다. 이를 프록시.
- 클라이언트는 프록시를 통해 타깃(실제 핵심기능 객체)에 접근하게 된다. 프록시가 타깃을 제어할 수 있다.
프로시의 구분
- 클라이언트가 타깃에 접근하는 방법을 제어하기 위해
- 타깃에 부가적인 기능을 제공하기 위해
- 프록시는 데코레이터 패턴과 프록시패턴으로 구현할 수 있다.
데코레이터패턴
- 타깃의 부가기능을 런타임시 다이내믹하게 부여하기 위해 사용. 코드상에는 어떤 방법과 순서로 연결되어있는지 정해지지 않았다.
- 데코레이터에서는 프록시를 여러개 둘 수 있고, 같은 인터페이스를 구현한 여러개의 프록시를 사용할 수 있다.
- 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 오브젝트가 프록시 팩토리 빈 갯수만큼 만들어진다. 이게 문제다….