@Inherited
@InterceptorBinding
//어노테이션을 사용할 대상
//메서드와 타입처럼 한 개 이상의 대상을 지정한다.
@Target({ElementType.TYPE, ElementType.METHOD})
//어노테이션 정보가 언제까지 유지되는지를 결정한다.
//런타임 때 어노테이션 정보를 리플렉션을 통해서 얻는다.
@Retention(RetentionPolicy.RUNTIME)
public @interface Transactional {
TxType value() default Transactional.TxType.REQUIRED;
@Nonbinding
Class[] rollbackOn() default {};
@Nonbinding
Class[] dontRollbackOn() default {};
public static enum TxType {
REQUIRED,
REQUIRES_NEW,
MANDATORY,
SUPPORTS,
NOT_SUPPORTED,
NEVER;
private TxType() {
}
}
}
@Transactional의 대상은 메서드와 타입이다.
@Transactional을 사용하면 적용된 모든 오브젝트를 자동으로 타깃 오브젝트로 인식한다.
우선 AnnotationTransactionAttributeSource가 @Transactional 어노테이션에서 트랜잭션 속성을 가져온다.
또한 포인트컷도 @Transactional을 통해 포인트컷의 선정 대상이 되도록 한다.
이렇게 AnnotationTransactionAttributeSource를 통해 포인트컷의 선정대상이 되도록 하며, 트랜잭션의 속성을 가져올 수 있다.
스프링은 @Transactional을 다음과 같은 방식으로 적용한다.
우선 메서드에 적용된 @Transactional을 찾아본다.
메서드에 적용되어 있지 않으면, 타깃 클래스에 부여된 @Transactional을 찾는다. 클래스 레벨에 @Transactional이 적용되어 있다면, 해당 클래스 레벨의 @Transactional을 적용한다.
클래스 레벨에도 없다면 선언 메서드(인터페이스의 메서드)의 @Transactional을 적용한다.
이렇게 1~3에도 없다면 선언 타입(인터페이스)의 @Transactional을 적용하며, 여기에도 없다면 적용하지 않는다.
그렇기에 다음과 같이 정의가 되어 있다면 해당 순서대로 찾고 @Transactional을 적용하는 것이다.
//4
public interface Seungkyu{
//3
void method1();
}
//2
public class SeungkyuImpl implements Seungkyu{
//1
void method1();
}
그렇기에 클래스에 공통적으로 트랜잭션을 적용하고, 커스텀해야 하는 메서드가 있다면 해당 메서드에만 개별적으로 트랜잭션을 적용하는 방법도 있다.
인터페이스에 적용하면 클래스가 변경되어도 트랜잭션이 적용될 것 같지만, 스프링을 사용하면 인터페이스에 정의된 @Transactional은 무시될 수 있기 때문이다.
트랜잭션 어노테이션 적용
어노테이션으로 트랜잭션을 적용하게 된다면, 클래스에 공통 트랜잭션을 설정하고 달라지는 트랜잭션에 대해서는 하나하나 어노테이션을 추가해줘야 한다는 단점이 있다.
하지만 기존의 트랜잭션보다 설정이 쉽고 직관적이기 때문에 이 방법을 선호하는 사람이 더 많다.
package seungkyu;
import lombok.AllArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@AllArgsConstructor
@Transactional
public class UserServiceImpl implements UserService{
private final UserDao userDao;
protected MailSender mailSender;
public void upgradeLevels() {
List<User> users = userDao.getAll();
for (User user : users) {
if (user.isUpgradeLevel()) upgradeLevel(user);
}
}
public void add(User user) {
if(user.getLevel() == null)
user.setLevel(Level.BRONZE);
userDao.add(user);
}
@Transactional(readOnly = true)
public User get(String id) {
return userDao.get(id);
}
@Transactional(readOnly = true)
public List<User> getAll() {
return userDao.getAll();
}
public void deleteAll() {
userDao.deleteAll();
}
public void update(User user) {
userDao.update(user);
}
}
이렇게 클래스 전체에 @Transactional을 설정하고, 개별 메서드에 다른 설정을 거는 방식으로도 간편하게 트랜잭션을 적용할 수 있다.
public class HelloImpl implements Hello {
public String sayHello(String name) {
return "hello " + name;
}
public String sayHi(String name) {
return "hi " + name;
}
public String sayBye(String name) {
return "bye " + name;
}
}
이런 추상 인터페이스와 구현체가 존재한다.
HelloImpl을 건드리지 않고 반환되는 값을 모두 대문자로 변경하고 싶다면 이렇게 HelloImpl을 멤버로 가져와서 사용하는 다음과 같은 클래스를 만들어야 한다.
@RequiredArgsConstructor
public class HelloUpperCase implements Hello {
private final 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 sayBye(String name) {
return this.hello.sayBye(name).toUpperCase();
}
}
이거를 InvocationHandler를 사용해 바꿔보자.
@RequiredArgsConstructor
public class HelloUpperCase implements InvocationHandler {
private final Hello target;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getReturnType().equals(String.class)) {
String returnValue = (String)method.invoke(target, args);
return returnValue.toUpperCase();
}
else return method.invoke(target, args);
}
}
이렇게 호출받은 메서드를 실행하면서 전후로 무언가 추가적인 작업을 할 수 있다.
나는 여기서 리턴의 타입이 String이면 대문자로 넘겨줄 수 있도록 만들었다.
@Test
public void simpleProxy2(){
Hello proxyHello = (Hello) Proxy.newProxyInstance(
getClass().getClassLoader(),
//구현할 인터페이스
new Class[]{Hello.class},
//부가기능을 담은 InvocationHandler 구현체
new HelloUpperCase(new HelloImpl())
);
Assertions.assertEquals("HELLO SEUNGKYU", proxyHello.sayHello("seungkyu"));
Assertions.assertEquals("HI SEUNGKYU", proxyHello.sayHi("seungkyu"));
Assertions.assertEquals("BYE SEUNGKYU", proxyHello.sayBye("seungkyu"));
}
Proxy.newProxyInstance를 통해 프록시가 설정된 클래스를 생성한다.
파라미터를 하나씩 살펴보자.
첫번째 파라미터
그냥 클래스로더이다. 다이나믹 프록시가 적용되는 클래스 로더를 지정하는 것이라고 한다.
두번째 파라미터
다이나믹 프록시가 구현할 인터페이스이다. 여기서 배열을 사용하는 이유는 하나 이상의 인터페이스를 구현하기 때문이다.
세번째 파라미터
부가기능을 가지고 있는 InvocationHandler 구현 오브젝트이다.
여기서 invoke 메서드는 요청받은 클래스의 메서드만 사용 가능하다.
만약 이렇게 FakeHello가 존재하고, FakeHello에도 동일한 메서드들이 존재한다면
@RequiredArgsConstructor
public class HelloUpperCase implements InvocationHandler {
private final Hello target;
private final FakeHello fakeTarget;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getReturnType().equals(String.class)) {
String returnValue = (String)method.invoke(target, args);
return returnValue.toUpperCase();
}
else return method.invoke(target, args);
}
}
FakeHello를 통해서 invoke 하려고 해도, 에러가 발생한다는 것이다.
이렇게 해서 simpleProxy2를 테스트해보니 다음과 같이 성공하는 것을 볼 수 있었다.
그리고 또 궁금한 점이 생겼다.
만약 타입에 맞지 않는 값을 반환한다면?
UserService에 정수를 반환하는 메서드를 추가하고, invocationHandler를 아래와 같이 바꾸고
@RequiredArgsConstructor
public class HelloUpperCase implements InvocationHandler {
private final Hello target;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String returnValue = (String)method.invoke(target, args);
return returnValue.toUpperCase();
}
}
다음의 테스트를 실행해보았다.
@Test
public void return1Test(){
Hello proxyHello = (Hello) Proxy.newProxyInstance(
getClass().getClassLoader(),
//구현할 인터페이스
new Class[]{Hello.class},
//부가기능을 담은 InvocationHandler 구현체
new HelloUpperCase(new HelloImpl())
);
proxyHello.return1();
}