서비스 계층의 분리와 @Transactional

 

1. 서비스 계층(Layer)의 분리 - 비지니스 로직의 분리

 

PRESENTATION LAYER

@Controller

 

BUSINESS LAYER(서비스 계층)

@Service

 

PERSISTENCE LAYER (영속계층)

@Repository

 

세 애너테이션 모두 @Component를 포함하고 있어서 <component-scan>에 스캔이 됨.

 

2. TransactionManage란?

 

DAO의 각 메소드는 개별 Connection을 사용함

그런데 하나의 Tx로 묶으려면 같은 Connection을 사용해야 함

 

DAO에서 Connection을 얻거나 반환할 때 DataSourceUtils를 사용하게 해야 함.

(개별 Connection을 얻는 코드가 수정되어야 함) 

 

//          conn = ds.getConnection();
            conn = DataSourceUtils.getConnection(ds);

 

3. TransactionManager로 Transaction 적용하기

 

public void insertWithTx() throws Exception {
        PlatformTransactionManager tm = new DataSourceTransactionManager(ds);
        TransactionStatus status = tm.getTransaction(new DefaultTransactionDefinition());
        // Tx 시작
        try {
            a1Dao.insert(1, 100);
            a1Dao.insert(1, 200);
            tm.commit(status); // Tx 끝 - 성공(커밋)
        } catch (Exception ex) {
            tm.rollback(status); // Tx 끝 - 실패(롤백)
        }
    }

[빈]

 

 

4. @Transactional로 Transaction적용하기

 

AOP를 이용한 핵심 기능과 부가 기능의 분리

@Transactional은 클래스나 인터페이스에도 붙일 수 있음(클래스와 인터페이스 내의 모든 메소드에 적용)

 

@Transactional
public void insertWithTx() throws Exception {
	a1Dao.insert(1,100);
	a1Dao.insert(1,200);
}

이렇게 핵심 기능만 따로 관리할 수 있게 된다.

 

 

 

실습)

package com.fastcampus.ch3;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

@Repository
public class A1Dao {
    @Autowired
    DataSource ds;

    public int insert(int key, int value) throws Exception {
        Connection conn = null;
        PreparedStatement pstat = null;

        try {
//            conn = ds.getConnection();
            conn = DataSourceUtils.getConnection(ds);
            System.out.println("conn = " + conn);
            pstat = conn.prepareStatement("insert into a1 values(?,?)");

            pstat.setInt(1, key);
            pstat.setInt(2, value);

            return pstat.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
            throw e;
        } finally {
//            close(conn, pstat);
            close(pstat);
            DataSourceUtils.releaseConnection(conn, ds);
        }

    }
    private void close(AutoCloseable... acs) {
        for(AutoCloseable ac :acs)
            try { if(ac!=null) ac.close(); } catch(Exception e) { e.printStackTrace(); }
    }
    public void deleteAll() throws Exception {
        Connection conn = ds.getConnection();
        String sql = "delete from a1";
        PreparedStatement pstat = conn.prepareStatement(sql);
        pstat.executeUpdate();
        close(pstat);
    }
}
package com.fastcampus.ch3;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import javax.sql.DataSource;

import static org.junit.Assert.*;

@RunWith(SpringJUnit4ClassRunner.class) // ac를 자동으로 만들어줌
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"}) // 이 설정파일(root-context.xml)을 이용함
public class A1DaoTest {
    @Autowired
    A1Dao a1Dao;

    @Autowired
    DataSource ds;

    @Test
    public void insertTest() throws Exception {
        // TxManager를 생성
        PlatformTransactionManager tm = new DataSourceTransactionManager(ds);
        TransactionStatus status = tm.getTransaction(new DefaultTransactionDefinition());
        // Tx 시작
        try {
            a1Dao.deleteAll();
            a1Dao.insert(1, 100);
            a1Dao.insert(1, 200);
            tm.commit(status);
        } catch (Exception e) {
            e.printStackTrace();
            tm.rollback(status);
        } finally {
        }
    }
}

TransactionManager를 이용해서 Connection을 하나로 사용할 수 있게 되었다.

Connection이 하나이기 때문에 commit이나 rollback이 한 번에 적용된다.

 

 

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"/>
	</bean>
	<tx:annotation-driven/>

DataSourceTransactionManager 객체를 @Autowired로 생성하기 위해 

root-context.xml에 다음과 같이 bean을 만들어준다.

 

package com.fastcampus.ch3;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import javax.sql.DataSource;

import static org.junit.Assert.*;

@RunWith(SpringJUnit4ClassRunner.class) // ac를 자동으로 만들어줌
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"}) // 이 설정파일(root-context.xml)을 이용함
public class A1DaoTest {
    @Autowired
    A1Dao a1Dao;

    @Autowired
    DataSource ds;

    @Autowired
    DataSourceTransactionManager tm;

    @Test
    public void insertTest() throws Exception {
        // TxManager를 생성
//        PlatformTransactionManager tm = new DataSourceTransactionManager(ds);
        TransactionStatus status = tm.getTransaction(new DefaultTransactionDefinition());
        // Tx 시작
        try {
            a1Dao.deleteAll();
            a1Dao.insert(1, 100);
            a1Dao.insert(2, 200);
            tm.commit(status);
        } catch (Exception e) {
            e.printStackTrace();
            tm.rollback(status);
        } finally {
        }
    }
}

tm을 @Autowired로 주입받기 때문에 따로 생성하지 않아도 된다.

 

 

package com.fastcampus.ch3;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TxService {
    @Autowired A1Dao a1Dao;
    @Autowired B1Dao b1Dao;

    public void insertA1WithoutTx() throws Exception {
        a1Dao.insert(1, 100);
        a1Dao.insert(1, 200);
    }

    @Transactional(rollbackFor = Exception.class) // Exception을 rollback
    // @Transactional // RuntimeException, Error만 rollback을 함
    public void insertA1WithTxFail() throws Exception {
        a1Dao.insert(1, 100);
//        throw new RuntimeException();
//        throw new Exception();
        a1Dao.insert(1, 200);
    }

    @Transactional
    public void insertA1WithTxSuccess() throws Exception {
        a1Dao.insert(1, 100);
        a1Dao.insert(2, 200);
    }
}

Transaction을 적용하지 않은 메소드와 적용한 메소드(중복 유무에 따라 2가지 메소드)를 만들고 테스트를 해본다.

 

package com.fastcampus.ch3;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.Assert.*;

@RunWith(SpringJUnit4ClassRunner.class) // ac를 자동으로 만들어줌
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"}) // 이 설정파일(root-context.xml)을 이용함
public class TxServiceTest {
    @Autowired
    TxService txService;

    @Test
    public void insertA1WithoutTxTest() throws Exception{
        txService.insertA1WithoutTx();
    }
    
    @Test
    public void insertA1WithTxTest() throws Exception{
        txService.insertA1WithTxFail();
    }

    @Test
    public void insertA1WithTxTest2() throws Exception{
        txService.insertA1WithTxSuccess();
    }

}

 

결과를 보면 

일단 Transaction을 적용하지 않으면 서로 다른 Connection을 사용하게 되고 한 줄의 데이터는 저장이 된다.

 

Transaction을 적용한 경우,

1) 중복값을 저장할 때

같은 Connection을 사용한 것을 확인할 수 있으며 rollback이 발생하므로 어떤 데이터도 저장이 되지 않는다.

다만 여기서 @Transactional 애너테이션에 따로 설정을 하지 않으면 RuntimeException과 Error가 발생했을때만 rollback을 하고 다른 Exception이 발생하면 rollback이 되지 않는다.

따라서 @Transactional(rollbackFor = Exception.class) 과 같이 예외 클래스를 따로 지정해줘야 한다.

 

2) 중복값이 없을 때

같은 Connection이 사용되고 두 줄의 데이터가 저장이 된다.

 

 

 

 

5. @Transactional의 속성

(isolation의 DEFAULT는 DB 설정을 따른다)

 

 

6. propagation속성의 값

REQUIRES_NEW : Tx 안에 다른 Tx (서로 다른 두 Tx)

NESTED : Tx안에 subTx (save point를 둬서 Tx를 나누는것... 같은 Tx)

 

 

7. REQUIRED와 REQUIRES_NEW

 

(1) - REQUIRED

REQUIRED : 다른 Tx에 끼어들게 됨. 예외가 발생해서 rollback이 발생하면 먼저 존재했던 Tx의 처음으로 rollback이 됨.

 

(2) - REQUIRES_NEW

REQUIRES_NEW : 다른 Tx에 상관없이 새로운 Tx를 만들게 됨. 

 

 

실습) REQUIRES_NEW 사용 예 ( @Transactinal 애너테이션 없이 설정 )

package com.fastcampus.ch3;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import javax.sql.DataSource;
import javax.xml.crypto.Data;

@Service
public class TxService {
    @Autowired A1Dao a1Dao;
    @Autowired B1Dao b1Dao;

    @Autowired
    DataSource ds;


//    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void insertA1WithTx() throws Exception {
//        a1Dao.insert(1, 100); // 성공
//        insertB1WithTx();
//        a1Dao.insert(2, 200); // 성공
        PlatformTransactionManager tm = new DataSourceTransactionManager(ds);
        DefaultTransactionDefinition txd = new DefaultTransactionDefinition();
        txd.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus status = tm.getTransaction(txd);

        try {
            a1Dao.insert(1, 100); // 성공
            insertB1WithTx();
            a1Dao.insert(2, 200); // 성공
            tm.commit(status);
        } catch (Exception e) {
            e.printStackTrace();
            tm.rollback(status);
        } finally {

        }
   }

//    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void insertB1WithTx() throws Exception {
//            b1Dao.insert(1, 100); // 성공
//            b1Dao.insert(1, 200); // 실패
        PlatformTransactionManager tm = new DataSourceTransactionManager(ds);
        DefaultTransactionDefinition txd = new DefaultTransactionDefinition();
        txd.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        TransactionStatus status = tm.getTransaction(txd);

        try {
            b1Dao.insert(1, 100); // 성공
            b1Dao.insert(1, 200); // 실패
            tm.commit(status);
        } catch (Exception e) {
            e.printStackTrace();
            tm.rollback(status);
        } finally {

        }
    }

    public void insertA1WithoutTx() throws Exception {
        a1Dao.insert(1, 100);
        a1Dao.insert(1, 200);
    }

    @Transactional(rollbackFor = Exception.class) // Exception을 rollback
//    @Transactional // RuntimeException, Error만 rollback을 함
    public void insertA1WithTxFail() throws Exception {
        a1Dao.insert(1, 100);
//        throw new RuntimeException();
//        throw new Exception();
        a1Dao.insert(1, 200);
    }

    @Transactional
    public void insertA1WithTxSuccess() throws Exception {
        a1Dao.insert(1, 100);
        a1Dao.insert(2, 200);
    }
}

 

insertB1WithTx 메소드의 Propagation을 REQUIRES_NEW로 설정하였다.

 

이렇게 하면 첫 번째 트랜잭션에서 insertA1WithTx 메소드가 진행중에 있을 때,

insertB1WithTx 메소드가 가 시작 되면서 새로운 Tx를 만들어 메소드를 실행하게 된다.

새로운 트랜잭션은 첫 번째 트랜잭션에 영향을 주지 않기 때문에

두 번째 트랜잭션 안의 메소드에서 예외가 발생하면 rollback이 되지만

첫 번째 트랜잭션의 메소드는 정상적으로 실행되고 commit이 된다.

 

package com.fastcampus.ch3;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.Assert.*;

@RunWith(SpringJUnit4ClassRunner.class) // ac를 자동으로 만들어줌
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"}) // 이 설정파일(root-context.xml)을 이용함
public class TxServiceTest {
    @Autowired
    TxService txService;

    @Test
    public void insertA1WithTxTest() throws Exception {
        txService.insertA1WithTx();
    }

}

 

따라서 위의 테스트 코드의 결과로 

a1Dao.insert(1, 100);

a1Dao.insert(2, 200);  의 결과가 반영되어

 

a1 테이블에 (1, 100)과 (2, 200)이 저장된다.

 


출처 : 스프링의 정석 : 남궁성과 끝까지 간다

 

+ Recent posts