잊지 않겠습니다.

'proxy'에 해당되는 글 1건

  1. 2014.07.28 Spring Transactional에 대하여. 1

Spring @Transactional에 대하여.

DB에 대한 Transaction의 경계를 잡는데에 유용하게 쓰이는 Spring의 @Transaction은 Proxy mode와 AspectJ mode로 동작합니다. 일반적으로 많이 사용하는 것은 Proxy mode를 주로 사용하는데, Proxy mode를 사용하게 되는 경우, 다음과 같은 주의점이 필요합니다.

private method의 @Transaction 미적용 문제

이는 어찌보면 당연한 결과입니다. 우리는 Service 객체를 Proxy를 통해서 얻어오고, 얻어온 객체를 접근할 때 Proxy에 의하여 Transaction이 시작되기 때문에 당연히 이런 상황에서는 Transaction이 적용되지 못합니다.
이 문제는 큰 문제가 될 수 없습니다. 다만, 다음 문제는 조금 생각해봐야지 되는 문제가 발생하게 됩니다.

Transaction이 적용되지 않은 method에서 Transaction이 적용된 method를 호출할때.

소위 말하는 self call의 경우 입니다. 이 경우는 어떻게 될까요? 다음 code를 통해서 확인해보도록 하겠습니다.

먼저, 2개의 public method를 준비합니다.

public interface BookService {
    void doNotTransactionAction();
    void doTransactionAction();
}

그리고, 이에 대한 서비스 구현 코드를 다음과 같이 구성합니다.

    @Override
    public void doNotTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isNull(TransactionSynchronizationManager.getCurrentTransactionName());
    }

    @Transactional(readOnly = false)
    @Override
    public void doTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.notNull(TransactionSynchronizationManager.getCurrentTransactionName());
    }

코드 내용은 매우 단순합니다. method가 호출되면 TransactionSynchronizationManager에서 현 Transaction의 이름을 확인하는 코드입니다. 첫번째 doNotTrancationAction에서는 당연히 Transaction이 없어야지 되고, 두번째 doTransactionAction에서는 당연히 Transaction이 있어야지 될 것으로 생각할 수 있습니다. 이제 이 두 코드를 호출하는 테스트 코드를 작성해서 시험해보면 다음과 같은 결과를 보입니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DomainConfiguration.class)
public class BookServiceImplTest {
    @Autowired
    private BookService bookService;

    @Before
    public void setUp() {
        assertThat(bookService, is(not(nullValue())));
    }

    @Test
    public void doNotTransactionAction() {
        bookService.doNotTransactionAction();
    }

    @Test
    public void doTransactionAction() {
        bookService.doTransactionAction();
    }
}

매우 당연한 결과이지만, 이 결과를 이제 수정해서 다음 코드로 바꾸어보도록 하겠습니다. Transaction이 적용되지 않은 code에서 Transaction이 적용된 method를 호출하는 경우에 어떻게 진행되는지 보도록 하겠습니다.

    @Override
    public void doNotTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isNull(TransactionSynchronizationManager.getCurrentTransactionName());
        doTransactionAction();
    }

결과는 다음과 같이 Assert Error가 나타나게 됩니다.

java.lang.IllegalArgumentException: [Assertion failed] - this argument is required; it must not be null
    at org.springframework.util.Assert.notNull(Assert.java:112)
    at org.springframework.util.Assert.notNull(Assert.java:123)
    at me.xyzlast.bookstore.services.BookServiceImpl.doTransactionAction(BookServiceImpl.java:54)
    at me.xyzlast.bookstore.services.BookServiceImpl.doNotTransactionAction(BookServiceImpl.java:47)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

왜 이런 결과를 나타내게 될까요? 이는 Spring Transaction이 Proxy로 동작하기 때문입니다. Public으로 Service interface에 접근할 때, Transaction이 동작하기 때문에 발생하는 에러입니다. 이 문제는 보다 더 복잡한 문제를 야기할 수 있습니다. 다음과 같은 코드를 확인해보도록 하겠습니다.

    @Transactional(readOnly = false)
    @Override
    public void doTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.notNull(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isTrue(!TransactionSynchronizationManager.isCurrentTransactionReadOnly());
    }

    @Transactional(readOnly = true)
    @Override
    public void doTransactionReadonlyAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.notNull(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isTrue(TransactionSynchronizationManager.isCurrentTransactionReadOnly());
        doTransactionAction();
    }

2개의 Transaction action이 있습니다. 하나는 ReadOnly로 동작하고, 나머지 하나는 Not ReadOnly입니다. 그런데 ReadOnly method에서 Not ReadOnly method를 내부에서 호출하게 되면 어떻게 될까요. 결과는 Not ReadOnly method 내부에서도 Transaction은 ReadOnly로 동작하게 됩니다. 이는 매우 심각한 Transaction문제를 가지고 오게 됩니다.

Summary

Spring의 @Transactional annotation은 매우 훌륭합니다. 다만 이를 PROXY mode에서 사용할 때는 다음과 같은 원칙을 알고 사용해야지 됩니다.

  1. private method에서는 동작하지 않음
  2. transaction이 적용되지 않은 public method 내부에서 transaction이 적용된 public method를 호출하는 경우, transaction이 동작하지 않는다.
  3. readonly transaction이 적용된 public method 내부에서 not-readonly transaction이 적용된 public method를 호출하는 경우, 모든 method는 readonly transaction으로 동작하게 된다.
  4. (3)의 경우는 반대로도 적용된다.


Posted by Y2K
,