본문 바로가기

WEB/SPRING

토비의 스프링 5.2 트랜잭션 서비스 추상화

레벨을 변경하는 비즈니스 로직을 실행시킬 때 네트워크나 어떤 문제가 생겨서 중간에 예외가 터진다면 어떻게 될까?

일부 유저의 정보는 변경되고 예외가 터진후에 유저의 정보는 변경되지 않을 것이다.

 

이 때 두 가지 방법이 있다.

 

1. 이미 처리된 정보는 유지하고 유저들의 요청이나 오류를 수정해서 반영하는 방법이 있다.

2. 하나의 묶음처리(트랜잭션)로 올바르게 처리된 정보를 되돌린다.(롤백)

 

여기서 하나의 트랜잭션으로 묶어서 롤백하는 방법을 알아보자.

 

먼저 1초도 안되는 테스트 코드에서 네트워크 예외를 처리하지 말고 가짜 객체를 만들어서

id가 중복되게 만들고 예외가 터지게 만들어보자.

 

static class TestUserService extends UserService {
        private String id;

        private TestUserService(String id){
            this.id = id;
        }

        protected void upgradeLevel(User user){
            if(user.getId().equals(this.id)) throw new TestUserServiceException();
            super.upgradeLevel(user);
        }
    }

    static class TestUserServiceException extends RuntimeException{

    }

 

 

// user의 level을 변경할 때 id가 중복되면 예외를 발생시키는 테스트

@Test
    public void upgradeAllOrNothing(){

        UserService testUserService = new TestUserService(users.get(3).getId());
        testUserService.setUserDao(this.userDao);

        userDao.deleteAll();
        for(User user : users) userDao.add(user);

        try{
            testUserService.canUpgradeLevels();
            fail("TestUserServiceException expected");
        }catch(TestUserServiceException e){

        }

        checkLevelUpgraded(users.get(1), false);


    }

 

 

// UserService(class)

public void canUpgradeLevels(){
        List<User> users = userDao.getAll();

        for(User user : users){
            if(canUpgradeLevel(user))
            {
                upgradeLevel(user);
            }
        }
    }

 

test코드에서 upgradeAllOrNothing메서드에서 문제가 생긴다.

checkLevelUpgraded(users.get(1), false); 코드에서 예외가 터졌지만 롤백되지 않아서 일부 유저의 정보에

변경이 일어났다.

 

트랜잭션으로 묶어서 롤백시키자는 정책이 있기 때문에 잘못된 코드라고 할 수 있다.

 


트랜잭션 동기화

 

스프링에서 제공해주는 트랜잭션동기화를 사용하면 connection을 저장소에 저장해두고 재사용할 수 있다.

 

1. UserService에서 connection을 가져온다.

2. Transaction동기화 저장소에 connection을 저장하고 autoCommit(false)설정한다.

3. UserService.update() 메서드를 jdbcTemplate을 사용해서 수행한다.

4. 하나의 트랜잭션에서 connection을 공유해서 사용하고 트랜잭션이 끝나면 자원을 닫아준다.

 

 

// 트랜잭션동기화매니저 사용하여 예외가 발생하면 롤백한다.

public void canUpgradeLevels() throws SQLException {

        TransactionSynchronizationManager.initSynchronization();
        Connection c = DataSourceUtils.getConnection((javax.sql.DataSource) dataSource);
        c.setAutoCommit(false);

        try{
            List<User> users = userDao.getAll();

            for(User user : users){
                if(canUpgradeLevel(user))
                {
                    upgradeLevel(user);
                }
            }

            c.commit();
        }catch(Exception e){
            c.rollback();
            throw e;
        }finally{
            DataSourceUtils.releaseConnection(c, (javax.sql.DataSource) dataSource);
            TransactionSynchronizationManager.unbindResource(this.dataSource);
            TransactionSynchronizationManager.clearSynchronization();
        }




    }

 

 

// 20라인 1번째 유저가 원래의 상태로 돌아와서 롤백성공

@Test
    public void upgradeAllOrNothing(){

        UserService testUserService = new TestUserService(users.get(3).getId());
        testUserService.setUserDao(this.userDao);
        testUserService.setDataSource(this.dataSource);
        userDao.deleteAll();

        for(User user : users) userDao.add(user);

        try{
            testUserService.canUpgradeLevels();
            fail("TestUserServiceException expected");
        }catch(TestUserServiceException | SQLException e){

        }

        checkLevelUpgraded(users.get(1), false);


    }

 

 

 


트랜잭션에 대한 추상화 적용

 

여기까지 트랜잭션을 적용해보았다. 하지만 데이터접근 기술이 변경되었을 때는 어떻게 될까?

트랜잭션을 사용하는 클래스에 있는 모든 메서드를 변경해야 한다.

 

만약 xml이나 애노테이션을 이용한 빈 주입을 사용한다면?

xml이나 애노테이션 관련 팩토리만 변경해주면 다른 기능을 이용할 수 있게 된다.

 

public class UserService {

    UserDao userDao;

	// Spring이 제공하는 트랜잭션 매니저
    private PlatformTransactionManager transactionManager;

	// setter
    public void setTransactionManager(PlatformTransactionManager transactionManager){
        this.transactionManager = transactionManager;
    }

 

 

xml로 트랜잭션 매니저 빈을 생성하고 userService의 setter를 이용하여 주입해준다.

	<bean id="userService" class="spring.service.UserService">
        <property name="userDao" ref="userDao"/>
        <property name="transactionManager" ref="transactionManager"/>
    </bean>

    <bean id="userDao" class="spring.dao.UserDaoJdbc">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

 

 

트랜잭션 매니저를 이용하여 트랜잭션을 적용한다.

 

public void canUpgradeLevels() throws SQLException {



    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(Exception e){
        this.transactionManager.rollback(status);
        throw e;
    }




}