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 및 기타 정보를 저장한다
    }
}

 

 

cs

[의도]

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 키워드 및 블록으로 감싸 준다.

 

 

반응형

+ Recent posts