ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링(Spring) AOP(Aspect Oriented Programming) 이용한 트랜잭션(Transaction) 처리 패턴
    Spring/컨셉 2018. 9. 7. 10:18

    트랜잭션이란 '하나의 업무 처리단위' 를 뜻한다. 이해를 돕기 위해 대표적으로 사용하는 것이 예금 입금 및 출금이다. ATM기로 예금을 출금하는 경우를 하나의 트랜잭션으로 본다면
    1) 사용자는 ATM기에 원하는 출금액을 입력
    2) ATM기는 출금액 확인 후 DB에서 사용자의 잔고를 갱신 ( 잔고 - 출금액으로 )
    3) ATM기는 출금액만큼 투입구에 반환

    여기서 문제는 ATM기에 출금액만큼의 돈이 없는 경우이다. 출금액만큼의 금액을 줄 수 없는 경우 ATM은 출금액만큼 사용자에게 줄 수 없지만 DB에서는 사용자의 잔고가 출금액만큼 줄어 있다. 따라서 사용자의 경우 클레임을 걸어 올 것이다.

    이를 방지하기 위해 1 ~ 3까지를 하나의 업무 처리단위로 보고 모두 충족시키지 않는 경우 DB를 이전 상황으로 돌려주는 일을 수행해야 한다. 이를 롤백(roll back)이라 한다.

    반대로 1 ~ 3까지의 상황을 정상적으로 수행했다면 DB에 갱신된 결과를 저장해야 한다. 이를 커밋(Commit)이라 한다.

    스프링에서는 이를 효과적으로 처리하기 위해 AOP를 제공한다.
    AOP는 Aspect Oriented Programming의 약어로 공통적으로 적용되는 기능들을 특정 메소드 혹은 클래스를 자동으로 실행될 수 있도록 지원하는 프로그래밍 기법이다. 공통 기능을 처리해주도록 돕는 프로그래밍 기법이라 이해하면 될 것 같다.

    AOP의 핵심은 어드바이저(Advisor)와 포인트컷(Point cut)이다. 정확히 이해하려면 깊은 배경지식이 필요하다. 스프링에 적용시키기 위해서 내가 이해한 바는
     - 포인트컷 : 공통 기능이 적용될 범위를 지정한다.
     - 어드바이저 : 포인트컷으로 선택한 빈에 적용될 부가기능 
    정도이다. 더 깊은 이해를 하고 싶다면 다음 포스팅 참조하면 좋을 것 같다.
    http://egloos.zum.com/springmvc/v/498979


    데이터베이스 관련 설정 저장하는 xml 파일에 다음과 같은 코드를 추가한다


    <!-- 1. transactionManager -->
    <bean id="transactionManager"
    			class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
    		<property name="dataSource" ref="dataSource" /> <!-- 트랜잭션 대상 선정. 바뀔 일 없고 이후 복사해서 사용 -->		
    	</bean>
    	
    
    	<!-- 2. tx:advice 이벤트 발생했을 때 트랜잭션 동작시킬 메소드 지정 -->
    	<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
    		<tx:attributes>
    			<!-- xx로 시작하는 메소드에서 RuntimeException 발생하면 rollback(롤백) 시켜라 -->
    			<!-- 감시할 패키지 범위를 aop로 지정해줌 -->
    			
    			<!-- Create -->
    			<tx:method name="add*" rollback-for="RuntimeException"/>
    			<tx:method name="insert*" rollback-for="RuntimeException"/>
    			<tx:method name="create*" rollback-for="RuntimeException"/>
    			<tx:method name="new*" rollback-for="RuntimeException"/>
    			<tx:method name="plus*" rollback-for="RuntimeException"/>
    			<tx:method name="append*" rollback-for="RuntimeException"/>
    			<tx:method name="make*" rollback-for="RuntimeException"/>
    			
    			<!-- Update -->
    			<tx:method name="update*" rollback-for="RuntimeException"/>
    			<tx:method name="modify*" rollback-for="RuntimeException"/>
    			<tx:method name="edit*" rollback-for="RuntimeException"/>
    			<tx:method name="fix*" rollback-for="RuntimeException"/>
    			<tx:method name="change*" rollback-for="RuntimeException"/>
    			<tx:method name="alter*" rollback-for="RuntimeException"/>
    			
    			<!-- Delete -->
    			<tx:method name="delete*" rollback-for="RuntimeException"/>
    			<tx:method name="remove*" rollback-for="RuntimeException"/>
    			<tx:method name="erase*" rollback-for="RuntimeException"/>
    			<tx:method name="signOut*" rollback-for="RuntimeException"/>
    			
    			<!-- Read -->
    			<!-- 데이터를 단순히 가져오는 거라면 데이터 수정을 허용하지 않겠다. -->
    			<tx:method name="get*" read-only="true"/>
    			<tx:method name="select*" read-only="true"/>
    			<tx:method name="read*" read-only="true"/>
    			<tx:method name="view*" read-only="true"/>
    			
    			<!-- 이름 주기 애매한 경우 (ETC) -->
    			<tx:method name="tx*" rollback-for="RuntimeException"/>
    			<tx:method name="*Action" rollback-for="RuntimeException"/>
    			
    		</tx:attributes>
    	</tx:advice>
    	
    <!-- 3. aop:config -->
    	<aop:config>
    		<!-- 접근지정자, 리턴타입, 패키지 (.* : 하위 모든 패키지) -->
    		<!-- com.blog.naver.하위 패키지.하위패키지의 ServiceImpl로 끝나는 모든 클래스의 모든 메소드 -->
    		<aop:pointcut id="transactionScope" expression="execution(public * com.blog.naver..*.*ServiceImpl.*(..))" />
    		
    		<aop:advisor advice-ref="transactionAdvice" pointcut-ref="transactionScope"/>
    		
    	</aop:config>
    

    1) DataSourceTransactionManager : 트랜잭션 처리를 돕는 객체. 한번 등록 후 계속 사용하면 된다.

    2)<tx:advice> : 트랜잭션에 대한 어드바이저. transaction-manager 속성에 DataSourceTransactionManager 등록해 트랜잭션으로 관리하도록 함.
    <tx:attributes> : 트랜잭션 적용될 속성들
    <tx:method> : 트랜잭션 적용될 메소드. name 속성을 통해 메소드명, rollback-for 속성을 통해 롤백 수행되는 조건(이벤트)를 등록한다.
    Create 부분을 예로 들면 add, insert, create, new, plus, append, make로 시작하는 메소드는 모두 적용시키겠다는 의미이다. 실무 대부분의 메소드명 범주를 다 등록해준 것. 필자는 ServiceImpl 클래스의 addOneBoard 메소드에 트랜잭션이 적용될 것이다. ( 3번 aop:config 참고 )

    3) <aop:config> AOP 구성을 지정한다.
    <aop:pointcut> : AOP 적용될 패키지 expression 통해 지정한다. 작성법은 주석 참조 (필자는 ServiceImpl 클래스들을 지정)
    <aop:advidor> : advire-ref 통해 공통 기능, 포인트컷 지정한다.

    package com.blog.naver.board.service;
    
    import com.blog.naver.board.biz.BoardBiz;
    import com.blog.naver.board.vo.BoardListVO;
    import com.blog.naver.board.vo.BoardSearchVO;
    import com.blog.naver.board.vo.BoardVO;
    
    public class BoardServiceImpl implements BoardService{
    
    	private BoardBiz boardBiz;
    	
    	public void setBoardBiz(BoardBiz boardBiz) {
    		this.boardBiz = boardBiz;
    	}
    
    	@Override
    	public BoardListVO getAllArticles(BoardSearchVO boardSearchVO) {
    		return boardBiz.getAllArticles(boardSearchVO);
    	}
    
    	@Override
    	public BoardVO getOneBoard(int boardId) {
    		return boardBiz.getOneBoard(boardId);
    	}
    
    	@Override
    	public boolean addOneBoard(BoardVO board) {
    		return boardBiz.addOneBoard(board);
    	}
    	
    	@Override
    	public boolean removeOneBoard(int boardId) {
    		return boardBiz.removeOneBoard(boardId);
    	}
    	
    }
    
    

    BoardServiceImpl 클래스. 예제 확인을 위해 addOneBoard만 수정했다. DaoImpl에서 Exception을 발생시켜 트랜잭션 동작 확인해보자.



    package com.blog.naver.board.dao;
    
    import java.util.List;
    
    import org.mybatis.spring.support.SqlSessionDaoSupport;
    
    import com.blog.naver.board.vo.BoardSearchVO;
    import com.blog.naver.board.vo.BoardVO;
    
    public class BoardDaoImpl extends SqlSessionDaoSupport implements BoardDao{
    
    	@Override
    	public int getAllArticlesCount(BoardSearchVO boardSearchVO) {
    		return getSqlSession().selectOne("BoardDao.getAllArticlesCount", boardSearchVO);
    	}
    
    	@Override
    	public List getAllArticles(BoardSearchVO boardSearchVO) {
    		// namespace, id
    		return getSqlSession().selectList("BoardDao.getAllArticles", boardSearchVO);
    	}
    	
    	@Override
    	public int insertOneArticle(BoardVO boardVO) {
    		int count = getSqlSession().insert("BoardDao.insertOneArticle", boardVO);
    		Integer.parseInt("A");
    		return count;
    	}
    	
    	@Override
    	public BoardVO selectOneArticle(int boardId) {
    		return getSqlSession().selectOne("BoardDao.selectOneArticle", boardId);
    	}
    	
    	@Override
    	public int deleteOneArticle(int boardId) {
    		return getSqlSession().delete("BoardDao.deleteOneArticle", boardId);
    	}
    }
    
    

    BoardDaoImpl 클래스. insertOneArticle 메소드만 보면 된다. ServiceImpl의 addOneBoard 메소드를 타고 수행되는 메소드이고, 해당 메소드에서 NumberFormatException을 발생시키도록 하고 DB에 데이터 추가됐는지 확인한다.
    int count = getSqlSession().insert("BoardDao.insertOneArticle", boardVO);
    이 코드에서 DB 추가가 이루어진 이후
     Integer.parseInt("A"); 
    에서 예외가 발생한다.
    트랜잭션이 없다면 예외가 발생해도 DB에는 저장이 되지만, 등록을 해두면 예외 발생하는 순간 롤백을 시켜 DB에 저장되지 않도록 하는 것이다.


Designed by Tistory.