티스토리 뷰

프로젝트를 수행함에 있어 오픈시 가장 크리티컬한 문제이기도 하고 자주 문제가 발생하는 부분인 Transaction 오류에 관련된 내용을 정리해본다.


Transaction이 잘 수행되다가 사용자가 몰리면 DB가 죽는경우

Spring Framework를 사용하는 경우라면 Transaction을 Spring Container 안에서 설정을 해야 한다. (선언적 트랜잭션)
선언적 트랜잭션 중 XML 선언방식을 사용할 때 잘못된 예와 잘 설정된 예를 보자면

 

* 잘못된 예

<tx:advice id="txAdvice" transaction-manager="txManager">
  <tx:attributes>  
    <tx:method name="*" propagation="REQUIRED" rollback-for="Exception"/>
  </tx:attributes>
</tx:advice>

 

* 잘 설정된 예

<tx:advice id="txAdvice" transaction-manager="txManager">
  <tx:attributes>
    <tx:method name="get*" read-only="true" />
    <tx:method name="select*" read-only="true" />
    <tx:method name="insert*" propagation="REQUIRED" rollback-for="Exception"/>
    <tx:method name="update*" propagation="REQUIRED" rollback-for="Exception"/>
    <tx:method name="delete*" propagation="REQUIRED" rollback-for="Exception"/>
  </tx:attributes>
</tx:advice>

 

위의 안좋은 예는 현재 모든 서비스 구현체 클래스 안의 모든 메소드에 대해서 트랜잭션을 수행하도록 설정이 되어 있다. 이는 매우 비효율적인 방법으로 성능에 안좋은 영향을 미친다. 조회문이 트랜잭션을 생성한다면 사람이 많아지면 트랜잭션 IO가 엄청나게 발생하여 DB가 죽을수도 있다.

따라서 조회성 업무(R)와 삽입(C)/변경(U)/삭제(D) 업무가 메소드 이름 단위로 구분이 되어 CUD 업무에 대해서만 트랜잭션을 처리하도록 변경하여야 한다. 이런 설정을 하기 위해서는 Service 클래스의 method를 정의할 때 네이밍 룰을 명확하게 하여 정의하였다면 설정을 바꾸는 것만으로도 간단하게 적용할 수 있을것이다. 프로젝트 초기에 CRUD의 네이밍 룰을 명확하게 잡는것이 중요하다.

 


Controller에 Serivce 로직 혼재하는 경우

Controller는 화면단에서 넘긴 URL과 data를 받아주고 그 정보를 Service 단으로 전달해주고, Service 단으로부터 처리된 결과 데이터를 다시 화면으로 보내주는 역할을 하는 공간이다. 이 Controller에서 여러개의 Service를 호출하고 심지어는 그것을 Transaction으로 묶으려는 시도들도 있다.

Transaction은 기본적으로 Service 단위로 동작하기 때문에 Controller에서 이렇게 하는 것은 전혀 Transaction 처리가 되지 않을 뿐만 아니라 Transaction을 사용하지 않는 부분이라 해도 잘못 작성된 코드이다.

2개의 Service 로직이 하나의 Transaction으로 묶여 동작해야 된다면 2개의 서비스를 묶는 상위 Service를 하나 만들어서 호출하는 식으로 해야 한다.

 


for문을 이용하여 하나의 쿼리에 대해 여러번 CUD 작업을 하는 경우

Service에서 위와 같은 처리를 해야하는 경우에는 Transaction에 유의해야 한다. MyBatis에서는 기본적으로 간단한 설정만으로 위와 같은 Batch 성격의 서비스에 대해 처리할 수 있는 옵션을 주고 있다.

 

<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate" destroy-method="clearCache">
  <constructor-arg index="0" ref="sqlSessionFactory" />  
  <constructor-arg index="1" value="BATCH" />
</bean>
public void updateBatch1(List<TestBook> list) {
  for (TestBook testBook : list) {
    sqlSession.update("TestBook.updateBatch1", testBook);
  }
}

위처럼 이 sqlSession은 BATCH를 수행하기 위한 sqlSession이다 라고 정의를 할 수 있는 부분이 있다. Batch를 사용할 때 반드시 이런 설정값을 추가시켜줘야 하며 그렇지 않다면 다건의 Data가 CUD되는 과정에서 Transaction이 보장되지 않는다.


Application Context와 Web Application Context에서 Service Component-Scan 중복 설정

보통 Spring을 사용하면 Web 영역에 대한 설정인 Web Application Context(이하 wac), Domain 영역에 대한 설정인 Application Context(이하 ac) 로 구분한다. Web과 Domain의 분리를 쉽게 하기 위해 이런식으로 Spring Container를 분리해서 설정하는데 만약 ac에서도 Service를 include 하고 wac에서도 Service를 include한다면 Transaction 처리에 문제가 생기게 된다. 이를테면

 

* ac 설정

 <context:component-scan base-package="com">
     <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
 </context:component-scan>

 

* wac 설정

<context:component-scan base-package="com"/>

 

이렇게 설정한 경우 component-scan 의 default 값은 include 이기 때문에 wac 측에서는 @Controller, @Service, @Repository 모두를 스캔하게 된다. 하지만 부모의 성격을 가지는 ac 쪽에서 이미 @Service에 대해 스캔하면서 Transaction 으로 사용할 놈들에 대해 flag를 걸어 놓았는데 자식의 성격을 가지는 wac쪽 scan을 하며 @Service를 재정의 한다면? Transaction으로 설정해 놓은 놈들이 모두 설정초기화가 되어 버린다. 따라서 Transaction이 동작하지 않는다. 반드시 @Service쪽 scan은 ac 영역에서만 해야 한다.

 


interface 코드 유무 확인

@Transactional 어노테이션 같은경우 Spring AOP를 이용하게 되는데 이 AOP는 기본적으로 Dynamic Proxy를 이용한다. Dynamic Proxy는 인터페이스 기반으로 동작하기 때문에 인터페이스가 없을경우 트랜잭션이 동작하지 않는다. interface가 없을 때 트랜잭션 동작하게 하려면 CGLib(Code Generation Library) Proxy를 이용하면 된다. CGLib Proxy는 클래스에 대한 Proxy가 가능하기 때문에 인터페이스가 없어도 된다.

설정하는 방법은 CGLib library를 추가해 준 후에 Context에 <tx:annotation-driven proxy-target-class=“true”/> 를 설정해 주면 된다.

 

끝!

 

댓글
최근에 올라온 글
최근에 달린 댓글
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31