테스트를 위해 테이블 b1 생성
@Transactional 없을 때
TxService.java
package com.jcy.usedhunter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TxService {
@Autowired A1Dao a1Dao;
@Autowired B1Dao b1Dao;
public void insertA1WithoutTx() throws Exception {
a1Dao.insert(1, 100);
a1Dao.insert(1, 200);
}
}
TxServiceTest.java
package com.jcy.usedhunter;
import static org.junit.Assert.*;
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;
@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();
}
}
conn = com.mysql.cj.jdbc.ConnectionImpl@78411116
conn = com.mysql.cj.jdbc.ConnectionImpl@2ca6546f
@Transactional 있을 때
같은 key 값이 주어 졌으니 실행 시 rollback 되서 저장된 값이 없어야 한다.
하지만 같은 Connection 임에도 rollback 이 되지 않았다.
TxService.java
package com.jcy.usedhunter;
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
public void insertA1WithTx() throws Exception {
a1Dao.insert(1, 100);
a1Dao.insert(1, 200);
}
}
TxService.java
package com.jcy.usedhunter;
import static org.junit.Assert.*;
import org.junit.Ignore;
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;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"})
public class TxServiceTest {
@Autowired
TxService txService;
@Ignore
@Test
public void InsertA1WithoutTxTest() throws Exception{
txService.insertA1WithoutTx();;
}
@Test
public void InsertA1WithTxTest() throws Exception{
txService.insertA1WithTx();;
}
}
conn = com.mysql.cj.jdbc.ConnectionImpl@5fb7183b
conn = com.mysql.cj.jdbc.ConnectionImpl@5fb7183b
해결 방법
@Transactional(rollbackFor = Exception.class) 을 어노테이션에 추가해주자
@Transactional 어노테이션은 RuntimeException, Error 만 rollback 한다. 그래서 따로 Exception.class 를 추가 해주어야 한다.
@Transactional 의 속성
속성 | 설명 |
propagation | Tx의 경계(boundary) 를 설정하는 방법을 지정 |
isolation | Tx의 isolation level 을 지정. DEFAULT(DB 설정을 따름), READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE |
readOnly | Tx 가 데이터를 읽기만 하는 경우, True 로 설정하면 성능이 향상 |
rollbackFor | 지정된 예외가 발생하면 , Tx을 rollback 하고 RuntimeException 과 Error 는 자동 rollback |
noRollbackFor | 지정된 예외가 발생해도 , Tx 를 rollback 하지 않음 |
timeout | 지정된 시간(초) 내에 Tx 이 종료되지 않으면, Tx 를 강제종료 |
propagation 속성의 값
값 | 설명 |
REQUIRED | Tx 이 진행 중이면 참여하고, 없으면 새로운 Tx 시작(디폴트) |
REQUIRED_NEW | Tx 이 진행 중이건 아니건, 새로운 Tx 시작 ( Tx 안에 다른 Tx) |
NESTED | Tx 이 진행 중이면, Tx의 내부 Tx 로 실행 (Tx 안에 subTx, save point) |
MANDATORY | 반드시 진행 중인 Tx 내에서만 실행가능. 아니면 예외 발생 |
SUPPORTS | Tx 이 진행 중이건 아니건 상관없이 실행 |
NOT_SUPPORTED | Tx 없이 처리, Tx 이 진행 중이면 잠시 중단(suspend) |
NEVER | Tx 없이 처리. Tx 이 진행 중이면 예외 발생 |
REQUIRED
REQUIRED_NEW
REQUIRED_NEW 테스트
실행하면 B1 의 값은 다 저장되고, A1 은 rollback 되어야 한다.
package com.jcy.usedhunter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TxService {
@Autowired A1Dao a1Dao;
@Autowired B1Dao b1Dao;
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void insertA1WithTx() throws Exception {
a1Dao.insert(1, 100); // A1
insertB1WithTx();
a1Dao.insert(1, 200); // A2
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void insertB1WithTx() throws Exception {
b1Dao.insert(1, 100); // B1
b1Dao.insert(2, 200); // B2
}
}
package com.jcy.usedhunter;
import static org.junit.Assert.*;
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;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"})
public class TxServiceTest {
@Autowired
TxService txService;
@Test
public void InsertA1WithTxTest() throws Exception{
txService.insertA1WithTx();
}
}
하지만 같은 모두 같은 Connection 이며, A1, B1 값도 모두 rollback 되었다.
conn = com.mysql.cj.jdbc.ConnectionImpl@5f80fa43
conn = com.mysql.cj.jdbc.ConnectionImpl@5f80fa43
conn = com.mysql.cj.jdbc.ConnectionImpl@5f80fa43
해결
@Transactional 이 동작하지 않는 이유는 같은 클래스에 속한 메서드 끼리의 호출(내부 호출) 이기 때문이다.
프록시 방식(Default)의 AOP는 내부 호출인 경우, Advice 가 적용되지 않는다. 그래서 Tx가 적용 되지 않는다.
따라서 두 메서드를 별도의 클래스로 분리하면 Tx 가 적용된다.
근본적인 해결 방법은 프록시 방식이 아닌 다른 방식을 사용 해야한다.
TxService2 를 만들어 메서드를 분리 했다. A1은 rollback 되고 B1의 값은 저장되었다.
TxService2.java
package com.jcy.usedhunter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TxService2 {
@Autowired B1Dao b1Dao;
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void insertB1WithTx() throws Exception {
b1Dao.insert(1, 100); // B1
b1Dao.insert(2, 200); // B2
}
}
TxService.java
package com.jcy.usedhunter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TxService {
@Autowired A1Dao a1Dao;
@Autowired TxService2 txService2;
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void insertA1WithTx() throws Exception {
a1Dao.insert(1, 100); // A1
txService2.insertB1WithTx();
a1Dao.insert(1, 200);
}
}
conn = com.mysql.cj.jdbc.ConnectionImpl@6e106680
conn = com.mysql.cj.jdbc.ConnectionImpl@6d4c273c
conn = com.mysql.cj.jdbc.ConnectionImpl@6d4c273c
conn = com.mysql.cj.jdbc.ConnectionImpl@6e106680
'Spring > Ch3. Spring DI, AOP' 카테고리의 다른 글
Spring - 서비스 계층의 분리와 @Transactional (0) | 2022.09.30 |
---|---|
Spring - AOP (0) | 2022.09.30 |
Spring - Transaction (0) | 2022.09.29 |
Spring DI - Spring DI 흉내내기 (0) | 2022.09.25 |
Spring - Spring DI(1) (0) | 2022.09.13 |