서비스 계층의 분리와 @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)이 저장된다.
출처 : 스프링의 정석 : 남궁성과 끝까지 간다
'Spring & SpringBoot > Spring' 카테고리의 다른 글
220510 Spring - Chapter 4. MyBatis로 게시판 만들기(Part.2) (0) | 2022.05.11 |
---|---|
220509 Spring - Chapter 4. MyBatis로 게시판 만들기 (0) | 2022.05.10 |
220505 Spring - Chapter 3. Spring DI와 AOP (Part. 5) (0) | 2022.05.06 |
220504 Spring - Chapter 3. Spring DI와 AOP (Part. 4) (0) | 2022.05.05 |
220503 Spring - Chapter 3. Spring DI와 AOP (Part. 3) (0) | 2022.05.03 |