✅ 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 담당
: 같은 Tx내에서 같은 Connection을 사용할 수 있도록 관리
∵ Tx은 1개의 Connection에서 이루어짐
∵ DAO의 각 메서드는 개별 Connecion을 사용
∵ 개별 Connection의 경우 각각 commit되어 rollback 불가능
↪ DAO 메서드들을 하나의 Tx로 사용하기 위해 같은 Connection으로 묶어주는 Transaction Manager
✔️ DAO에서 Connection을 얻거나 반환할 때 DataSourceUtils 사용
cf) @Transactional은 클래스와 인터페이스에도 붙일 수 있음(클래스/인터페이스 내의 모든 메서드에 적용)
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 {
}
}
}
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;
✔️ @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();
}
}
✅ 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 강제종료
✅ 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이 진행 중이면 예외 발생
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 {
}
}
}
참고) 자바의 정석 | 남궁성과 끝까지 간다