[Spring] 12. 서비스 계층의 분리와 @Transactional

Hyeongmin Jung·2023년 8월 19일
0

Spring

목록 보기
11/17

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

✅ Controller(@Controller) | Presentation Layer
✔️ Spring MVC에서 사용자 요청을 받아 Service 호출
✅ DAO(@Service) | Business Layer(서비스 계층)
✔️ Controller에 의해 호출되어 실제 비지니스 로직 처리, 재사용 가능한 서비스를 제공
✔️ 프레젠테이션 계층과 데이터 액세스 계층 간 중재자 역할
✔️ 트렌잭션 적용에 적합(Contoller에서 tx적용하면 복잡)
✔️ DAO를 호출하여 DB CRUD 처리 후 Controller로 반환
✔️ 관심사 분리에 용이
✅ DAO(@Repository) | Persistance Layer(Data Access Layer, 영속계층)
✔️ Service에 의해 호출되어 DB CRUD 담당

📌 Transaction Manager

: 같은 Tx내에서 같은 Connection을 사용할 수 있도록 관리

Tx은 1개의 Connection에서 이루어짐
DAO의 각 메서드는 개별 Connecion을 사용
개별 Connection의 경우 각각 commit되어 rollback 불가능
↪ DAO 메서드들을 하나의 Tx로 사용하기 위해 같은 Connection으로 묶어주는 Transaction Manager

✔️ DAO에서 Connection을 얻거나 반환할 때 DataSourceUtils 사용

⚫ TransationManager으로 Transactin 적용

⚫ @Transactinal:AOP를 이용한 핵심기능/부가기능 분리

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

⚫ 실습

📃 TxManager 직접생성

A1Dao.java

@Repository
public class A1Dao {
    @Autowired
    DataSource ds;

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

        String sql = "insert into a1 value(?, ?)";

        try {
//            conn = ds.getConnection();
            conn = DataSourceUtils.getConnection(ds);
            System.out.println("conn = " + conn);
            pstmt = conn.prepareStatement(sql);

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

            return pstmt.executeUpdate(); //  insert, delete, update
        } catch (SQLException e) {
            e.printStackTrace();
            throw e;
        } finally {
//            close(pstmt, conn);
            close(pstmt);
            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(); // deletAll은 tx와 별개로 작동해야 하므로 getConnecion으로 개별 connection
        String sql = "delete from a1";
        PreparedStatement pstmt = conn.prepareStatement(sql);
        pstmt.executeUpdate();
        close(pstmt);
    }
}

A1DaoTest.java

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"})
public class A1DaoTest extends TestCase  {
    @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(2, 200);
            a1Dao.insert(1, 200);
            tm.commit(status); //성공
        } catch (Exception e) {
            e.printStackTrace();
            tm.rollback(status); //실패
        } finally {
        }
    }
}

📃 TxManager bean으로 등록

root-context.xml

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

PlatformTransactionManager tm = new DataSourceTransactionManager(ds);
와 같이 TxManager 역할 실행

✔️ A1DaoTest.java에 추가 후 실행, 위 코드 참고

@Autowired
DataSourceTransactionManager tm;

📃 rollbackFor

✔️ @Transactional을 쓰지 않으면 실패해도 전체 롤백X, 성공 지점까지 롤백되지않고 실행
✔️ @Transactional을 써주면 TxManager이 적용되어 전체가 Tx로 묶임

✔️ @Transactional은 RuntimeException, Error만 rollback이 가능
↪ 롤백⭕ throw new RuntimeException();
↪ 롤백❌ throw new Exception();
✔️ rollbackFor을 사용하여 실패 시 rollback가능하도록 설정
ex. @Transactional(rollbackFor = Exception.class)

TxService.java

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

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

    // @Transactional은 RuntimeException, Error만 rollback
    // rollbackFor을 써주어야 실패시 rollback 가능
    @Transactional(rollbackFor = Exception.class)
    public void insertA1WithTxFail() throws Exception{
        a1Dao.insert(1,100);
//        throw new RuntimeException();
        a1Dao.insert(1,200);
    }

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

TxServiceTest.java

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"})
public class TxServiceTest {
    @Autowired
    TxService txService;

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

📌 Transactinal 속성

propagation: Tx의 경계를 설정하는 방법 지정
isolation: Tx의 isolation level을 지정
DEFAULT | READ_UNCOMMITTED | READ_COMMITTED | REPEATABLE_READ | SERIALIZABLE
readOnly: Tx이 데이터를 읽기만 하는 경우, true로 지정하면 성능 향상
rollbackFor: 지정된 예외가 생기면, Tx 롤백
✔️ RuntimeException과 Error는 자동 rollback
norollbackFor(↔rollbackFor): 지정된 예외가 발생해도, Tx를 롤백하지 않음
timeout: 지정된 시간(sec) 내에 Tx가 종료되지 않으면, Tx 강제종료

⚫ Propagation 속성의 값

REQUIRED: Tx이 진행 중이면 참여, 없으면 새로운 Tx 시작 | default
REQUIRES_NEW: Tx이 진행 중이건 아니건, 새로운 Tx 시작 | Tx안에 다른 Tx
NESTED: Tx이 진행 중이면, Tx의 내부 Tx로 실행 | Tx안에 subTx_savepoint, 같은 Tx
MANDATORY: 반드시 진행 중인 Tx내에서만 실행 가능, 아니면 예외 발생
SUPPORTS: Tx이 진행 중이건 아니건 상관없이 실행
NOT_SUPPORTED: Tx 없이 처리, Tx이 진행 중이면 잠시 중단(suspend)
NEVER: Tx 없이 처리, Tx이 진행 중이면 예외 발생

⚫ REQUIRED & REQUIRES_NEW

cf. REQUIRES_NEW에서 B2에서 실패한다고 해도 Tx2만 롤백되고, A1, A2가 성공하면 Tx1은 정상 실행됨

⚫ 실습

📃 A1(성공)-|-B1(성공)-B2(성공)-|-A2(실패)
↪ Tx2 롤백(b1 empty), Tx1 최종성공
📃 A1(성공)-|-B1(성공)-B2(실패)-|-A2(성공)
↪ Tx1 롤백(a1 empty), Tx2 최종성공

conn = com.mysql.cj.jdbc.ConnectionImpl@7cf162bc
conn = com.mysql.cj.jdbc.ConnectionImpl@159a48a6
conn = com.mysql.cj.jdbc.ConnectionImpl@159a48a6
conn = com.mysql.cj.jdbc.ConnectionImpl@7cf162bc

@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{
        PlatformTransactionManager tm = new DataSourceTransactionManager(ds);
        DefaultTransactionDefinition txd = new DefaultTransactionDefinition();
        txd.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus status = tm.getTransaction(txd);
        try {
            a1Dao.insert(1,100); // 성공
            insertB1WihtTx();
            a1Dao.insert(1,200); // 실패
            tm.commit(status);
        } catch (Exception e) {
            e.printStackTrace();
            tm.rollback(status);
        } finally {
        }

    }
//    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void insertB1WihtTx() throws Exception{
        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(2,200); // 성공
            tm.commit(status);
        } catch (Exception e) {
            e.printStackTrace();
            tm.rollback(status);
        } finally {
        }
    }
}

참고) 자바의 정석 | 남궁성과 끝까지 간다

0개의 댓글