1. Transaction 설정방법
트랜잭션 설정 방법은 이곳을 참고.
2. Isolation
SQL 의 Isolation level 과 동일하게 동작 (SQL 격리수준 속성에 대한 자세한 내용은 이곳을 참고)
- READ_UNCOMMITED : commit 되지 않은 데이터를 읽는다
- READ_COMMITED : commit 된 데이터만 읽는다
- REPEATABLE_READ : 자신의 트랜잭션이 생성되기 이전의 트랜잭션(낮은 번호의 트랜잭션)의 커밋된 데이터만 읽는다
- SERIALIZABLE : LOCK 을 걸고 사용
- DEFAULT : 사용하는 DB 기본 설정을 따른다. (Oracle 은 READ_COMMITED, Mysql InnoDB 는 REPEATABLE_READ 가 Default)
3. Propagation
- REQUIRE : 부모 트랜잭션(자신을 호출한 메소드의 Transaction)이 존재한다면 그에 포함되어 동작. 부모 트랜잭션이 존재하지 않을 경우 새로 트랜잭션 생성(default 속성)
- SUPPORTS : 부모 트랜잭션이 존재하면 그에 포함되어 동작. 부모 트랜잭션이 없다면 트랜잭션 없이 동작.
- MANDATORY : 부모 트랜잭션이 존재하면 그에 포함되어 동작. 부모 트랜잭션이 없다면 예외 발생시킨다.
- REQUIRES_NEW : 부모 트랜잭션 존재 여부와 상관없이 트랜잭션을 새로 생성
- NOT_SUPPORTED : 트랜잭션을 사용하지 않는다. 부모 트랜잭션이 존재하면 보류시킨다.
- NEVER : 트랜잭션을 사용하지 않도록 강제한다. 부모 트랜잭션이 존재할 경우 예외를 발생시킨다.
- NESTED : 부모 트랜잭션이 존재하면 부모 트랜잭션 안에 트랜잭션을 만든다. 부모트랜잭션의 커밋과 롤백에 영향을 받지만 자신의 커밋과 롤백은 부모 트랜잭션에 영향을 주지 않는다.
※ NESTED 사용의 예 :
메인 작업을 진행하며 이와 관련된 로그를 DB에 저장해야 한다.
로그를 저장하는 작업이 실패하더라도 메인 작업의 트랜잭션은 롤백하지 않는다.
하지만 메인 작업이 실패할 경우 로그 또한 저장하지 않아야 한다(롤백 되어야 한다).
Propagation 과 관련된 부분은 이곳을 참고
4. 전파속성(Propagation)과 격리레벨(Isolation Level) 사용 예제
[에러가 발생하는 코드]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
@Component
class SeqUtil {
@Resource
private DAO dao;
public long getNextSeq(){
long currentSeq = dao.getCurrentSeq(); //현재 seq를 가져온다
long nextSeq = currentSeq+1L; // seq + 1
dao.updateSeq(currentSeq+1L); //DB seq 를 +1 한 값으로 update 한다
return nextSeq;
}
}
@Component
class InfoUtil {
@Resource
private DAO dao;
public void insertInfo(long nextSeq){
dao.insertInfo(nextSeq); //+1 한 seq 및 기타 정보를 저장한다
}
}
@Service
class Service {
@Autowired
private SeqUtil seqUtil;
@Autowired
private InfoUtil infoUtil;
@Transactional
public void service(){
long nextSeq = seqUtil.getNextSeq(); //+1 한 seq 를 가져온다
infoUtil.insertInfo(nextSeq); //+1 한 seq 및 기타 정보를 저장한다
}
}
|
|
[의도]
autoincrement 속성을 사용하지 않고 위의 getNextSeq() 와 같이 직접 구현한 상황.
getNextSeq() : 현재 seq 값을 가져온 후 +1 증가 시킨다. 증가시킨 값으로 seq 값을 update 하고 증가시킨 seq 값을 return 한다.
insertInfo() : seq 값을 받아 기타 정보들과 함께 데이터를 저장한다.
Service() : getNextSeq(), insertInfo() 메소드를 사용하는 메소드 (getNextSeq(), insertInfo() 메소드가 service() 메소드 하위에 속해 있으므로 상위 메소드인 service()의 Transaction에 묶이게 된다)
[문제]
배치와 같이 동시각에 N개의 서비스가 처리된다면 문제가 발생한다.
insert 한 레코드의 seq 값이 1, 2, 3, 4 .... 와 같이 1씩 순차적으로 증가하길 기대했지만 증가하길 기대했지만 1, 1, 2, 2 ... 와 같이 중복이 발생되며 저장된다.
[해결 방안 1. 격리수준을 SERIALIZABLE 로 올린다]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
@Component
class SeqUtil {
@Resource
private DAO dao;
@Transactional(isolation=Isolation.SERIALIZABLE, propagation=Propagation.REQUIRES_NEW)
public long getNextSeq(){
long currentSeq = dao.getCurrentSeq(); //현재 seq를 가져온다
long nextSeq = currentSeq+1L; // seq + 1
dao.updateSeq(currentSeq+1L); //DB seq 를 +1 한 값으로 update 한다
return nextSeq;
}
}
@Component
class InfoUtil {
@Resource
private DAO dao;
public void insertInfo(long nextSeq){
dao.insertInfo(nextSeq); //+1 한 seq 및 기타 정보를 저장한다
}
}
@Service
class Service {
@Autowired
private SeqUtil seqUtil;
@Autowired
private InfoUtil infoUtil;
@Transactional
public void service(){
long nextSeq = seqUtil.getNextSeq(); //+1 한 seq 를 가져온다
infoUtil.insertInfo(nextSeq); //+1 한 seq 및 기타 정보를 저장한다
}
}
|
cs |
1) service() 메소드 에서 getNextSeq() 메소드를 분리하여 별도의 Transaction 으로 동작하도록 REQUIRES_NEW 전파속성을 적용시킨다. (혹은 service() 메소드의 @Tranactional 어노테이션을 제거하고 해당 메소드가 사용하는 getNextSeq() 메소드와 insertInfo(..) 메소드에 각각 @Transactional 어노테이션을 붙인다)
2) SERIALIZABLE 격리 레벨을 적용시킨다.
[해결 방안 2. 동기화 synchronized 키워드를 사용하여 임계영역 문제 해결]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
@Component
class SeqUtil {
@Resource
private DAO dao;
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long getNextSeq(){
long currentSeq = dao.getCurrentSeq(); //현재 seq를 가져온다
long nextSeq = currentSeq+1L; // seq + 1
dao.updateSeq(currentSeq+1L); //DB seq 를 +1 한 값으로 update 한다
return nextSeq;
}
}
@Component
class InfoUtil {
@Resource
private DAO dao;
public void insertInfo(long nextSeq){
dao.insertInfo(nextSeq); //+1 한 seq 및 기타 정보를 저장한다
}
}
@Service
class Service {
@Autowired
private SeqUtil seqUtil;
@Autowired
private InfoUtil infoUtil;
private final Object semaphore = new Object();
@Transactional
public void service(){
synchronized(semaphore){
long nextSeq = seqUtil.getNextSeq(); //+1 한 seq 를 가져온다
}
infoUtil.insertInfo(nextSeq); //+1 한 seq 및 기타 정보를 저장한다
}
}
|
cs |
1) service() 메소드 에서 getNextSeq() 메소드를 분리하여 별도의 Transaction 으로 동작하도록 REQUIRES_NEW 전파속성을 적용시킨다. (혹은 service() 메소드의 @Tranactional 어노테이션을 제거하고 해당 메소드가 사용하는 getNextSeq() 메소드와 insertInfo(..) 메소드에 각각 @Transactional 어노테이션을 붙인다)
2) 임계영역을 synchronized 키워드 및 블록으로 감싸 준다.
'back > Spring Framework' 카테고리의 다른 글
[Spring Batch+Quartz] 스프링 배치, 쿼츠 메타테이블 들여다보기 (1) | 2020.04.21 |
---|---|
[Spring Batch+Quartz] 스프링 배치+쿼츠 설정 및 구현 Clustering 모드 사용 (3) | 2020.04.20 |
[Spring Quartz] Batch Job 중지 시키기 : JobOperator, Stop, StoppableTasklet (0) | 2020.04.14 |
[Spring Fw] Bean 생명주기 : Init/Destroy 사용법 및 호출 순서 (0) | 2020.03.19 |
@Autowired @Resource @Inject Bean 탐색 기준 (0) | 2020.02.27 |