우당탕탕 동시성에 대해 파악하고 thread-safe하게 해보자

2024. 3. 11. 00:41· 프로젝트/씈크럼 프로젝트
목차
  1. synchronized
  2. Lock(락)
  3. pessimisticLock(비관적 락)
  4. optimisticLock(낙관적 락)
반응형

여태 프로젝트 진행하면서 동시성 문제에 대해 생각하지 않았다.

사실 그럴 힘이 없었다. 근데 자바에서는 멀티 스레드를 지원하는데 문제는 여기서 발생한다.

 

멀티 스레드 -> 즉 하나의 프로세스에서 여러 개의 스레드가 동시에 작업을 진행한다.

씈크럼 프로젝트에서 스크럼의 참여 인원을 +1 시키는 메서드가 존재한다.

public void plusCurrentMember(){                               
    if(this.maxMember <= this.currentMember){                  
        throw new RuntimeException("더 이상 참여가 불가능합니다.");        
    }                                                          
    this.currentMember = currentMember + 1;                    
}

현재 멤버랑 최대 멤버 카운트 비교해서 자리가 있으면 +1로 참여를 진행시킨다.

 

싱글 스레드에서는 문제가 없다. 스레드 하나만 요청이 들어오기에 단순 조건을 판단하고 +1을 진행하면 된다.

그러나 멀티 스레드인 경우 문제가 이제 발생한다.

(그림은 양해 부탁드립니다. 그림판으로 그렸습니다)

위 그림을 설명하면 2명의 유저가 하나의 스크럼에 접근했다.
스크럼 -> 최대 멤버 : 20명, 현재 멤버 : 19명

만약에 두 유저가 해당 스크럼 정보를 통해 참여를 진행한다면?

둘 다 현재 참여가 가능하다고 판단해서 Update Query를 날리게 되고, 결국 데이터 정합성에 문제가 발생한다.

 

나는 여태 이런 경우를 생각하지 않고 진행했었는데 이제는 멀티 스레드에 대해 생각할 시간이 됐다.

간단하게 테스트 코드를 만들어서 진행해보았다.

@Test                                                                      
public void 스레드_15개_스크럼_참여() throws InterruptedException {                 
    Long scrumId = 7L;                                                     
    int threadCount = 15;                                                  
                                                                           
    ExecutorService executorService = Executors.newFixedThreadPool(threadCo
    CountDownLatch latch = new CountDownLatch(threadCount);                
                                                                           
                                                                           
    for(int i=0; i<threadCount; i++){                                      
        executorService.submit(() -> {                                     
            try{                                                           
                scrumService.enterTestScrum(scrumId);                      
            }catch (Exception e){                                          
                e.printStackTrace();                                       
            }finally {                                                     
                latch.countDown();                                         
            }                                                              
        });                                                                
    }                                                                      
                                                                           
    latch.await();                                                         
    executorService.shutdown();                                            
                                                                           
    Scrum scrum = scrumRepository.findById(scrumId).orElseThrow();         
    Assertions.assertEquals(15, scrum.getCurrentMember());                 
}

15명의 유저를 가상으로 만들어서 동시 요청을 보내게 했고, 현재 스크럼의 유저가 15가 돼야 정상적으로 전부 수행이 가능해진다. 

그러나 멀티 스레드로 동작했더니 race condition이 발생해서 2명만 참가한 것으로 나타났다.(실제 서비스에서 이런 동기화 문제를 꼭 생각해야 한다)

 

이런 데이터 정합성을 맞추기 위해 해결하는 방법은 크게 4가지가 존재한다.

synchronized

자바에서 지원하는 동기화 기법 중 하나이다.
synchronized 키워드를 통해 현재 데이터를 점유하는 스레드 말고는 접근을 못하게 해주는 키워드이다.

@Override                                                             
@Transactional                                                        
public synchronized void enterTestScrum(Long scrumId) {               
    Scrum scrum = scrumRepository.findById(scrumId).orElseThrow();    
                                                                      
    scrum.plusCurrentMember();                                        
}

그래서 Service 메서드에 synchronized 키워드를 달아주어서 다시 실행했는데 동기화를 보장하지 않았다. 왜지?

분명 synchronized 키워드를 통해 해당 service에 동시 접근을 막았으니, 정상적으로 15개의 스레드가 순차적으로 실행된다고 생각했는데 아니었다.

그 이유는 Spring AOP에서 찾을 수 있었다.

@Transactional을 사용하게 되면 AOP로 인해서 enterTestScrum의 프록시 객체가 만들어지게 된다.

public void enterTestScrum(Long scrumId){
	1. 트랜잭션 시작
    	2. 실제 enterTestScrum 시작
    	3. 커밋
}

그러면 실제 메서드를 호출하기 전후로 트랜잭션을 걸고 커밋을 진행한다.

우리가 실제 Service에는 synchronized를 걸어두었지만, 프록시 객체에는 synchronized가 전달이 안된다고 한다.
결국 enterTestScrum은 우리가 예상했던 대로 thread-safe 하지 않고, 여러 스레드에서 동시에 사용하게 되는 것이다

그래서 @Transactional을 제거하고 동작시켜 보았다.

이번엔 아예 돌지를 않더라. 분명 더티체킹이 동작하지 않나? 싶어서 쿼리문을 보니 전부 select만 나가고 update가 나가지 않았다. 

더티 체킹은 트랜잭션 내에서 변화를 감지하고 종료 시점에 자동으로 UPDATE 쿼리를 날려준다.
현재 나는 트랜잭션을 제거했으니 당연히 더티체킹이 돌지 않았던 것. 그래서 수동으로 save를 진행해 주었다.

@Override                                                                   
// @Transactional                                                            
public synchronized void enterTestScrum(Long scrumId) {                     
    Scrum scrum = scrumRepository.findById(scrumId).orElseThrow();          
                                                                            
    scrum.plusCurrentMember();                                              
    scrumRepository.save(scrum);                                            
}

이렇게 동작하니 제대로 성공하는 것을 확인할 수 있다.

 

그러나 해당 방법의 단점이 존재한다.

-> 하나의 프로세스만 데이터 점유를 판단하고 막기에, 서버가 여러 대인 경우 문제가 발생한다.

위 그림처럼 서버 1, 서버 2에서는 각각 싱크로나이즈를 걸기에 여러 서버에서 요청을 보내는 경우 동기화 문제가 발생한다.

우리는 데이터베이스 레벨에서 데이터 동기화를 진행해주어야 한다.

 

Lock(락)

  • 데이터에 접근하기 위해 얻어야 하는 것
  • 데이터당 Lock을 하나 가지고 있어서 하나의 트랜잭션만 접근이 가능. 나머지는 대기 상태

pessimisticLock(비관적 락)

  • 실제 데이터에 Lock을 걸어서 정합성을 맞추는 방법.
  • exclusive lock을 걸면 다른 트랜잭션에서는 lock이 해제되기 전까지 데이터를 못 가져간다.
  • 데드락 발생 가능성 있음

 

Repository 쿼리 위에 @Lock 어노테이션을 통해 비관적 락을 걸어주었다.

@Lock(LockModeType.PESSIMISTIC_WRITE)                                     
@Query("select s from Scrum s where s.id =:scrum_id")                     
Scrum findByIdPessimisticLock(@Param("scrum_id") Long scrumId);

그리고 기존 enterTestScrum은 트랜잭션을 활성화시켜 주었다.

@Override                                                            
@Transactional                                                       
public void enterTestScrum(Long scrumId) {                           
    Scrum scrum = scrumRepository.findByIdPessimisticLock(scrumId);  
                                                                     
    scrum.plusCurrentMember();                                       
}

정상적으로 동기화 작업을 성공한 것을 볼 수 있다.

 

optimisticLock(낙관적 락)

  • Lock을 이용하지 않고 버전을 이용해서 관리한다.
  • 데이터 읽은 후 update를 수행할 때 내가 읽은 버전과 일치하는지 확인 후 진행.
  • 버전이 일치하지 않으면 다시 읽어서 작업 수행한다.

낙관적 락을 프로젝트에 적용은 추후에 진행하겠습니다.

반응형
저작자표시 (새창열림)

'프로젝트 > 씈크럼 프로젝트' 카테고리의 다른 글

for문을 stream으로 변경하자.(근데 왜 더 느리지?)  (0) 2024.02.24
내가 원하는 컬럼만 업데이트 쿼리가 나가고싶은데..(feat, DynamicUpdate)  (0) 2024.02.22
공통으로 가지는 생성시간, 수정시간을 상속으로 이용해보자.  (0) 2024.02.19
QueryDsl로 페이지네이션 도입(검색 API 구현)  (2) 2024.02.17
  1. synchronized
  2. Lock(락)
  3. pessimisticLock(비관적 락)
  4. optimisticLock(낙관적 락)
'프로젝트/씈크럼 프로젝트' 카테고리의 다른 글
  • for문을 stream으로 변경하자.(근데 왜 더 느리지?)
  • 내가 원하는 컬럼만 업데이트 쿼리가 나가고싶은데..(feat, DynamicUpdate)
  • 공통으로 가지는 생성시간, 수정시간을 상속으로 이용해보자.
  • QueryDsl로 페이지네이션 도입(검색 API 구현)
indeep
indeep
백준 - https://www.acmicpc.net/user/esu08259 깃허브 - https://github.com/qkrrlgus114
indeep
불편한 게 싫어
indeep
글쓰기방명록관리자
전체
오늘
어제
  • 분류 전체보기 (183)
    • 알고리즘문제 (11)
      • 백준 (10)
      • swea (1)
    • CS지식 (24)
      • HTTP 웹 지식 (4)
    • 일상 (2)
    • 스프링 강의(인프런) (4)
    • JAVA 강의(인프런) (11)
    • JAVA (8)
    • 오류해결 (31)
    • Vue (5)
    • 싸피 (4)
    • 스프링 개념 (2)
    • git 관련 (1)
    • 면접 (4)
    • 기타 (15)
    • 프로젝트 (45)
      • RESTAPI 추천 서비스 (34)
      • 씈크럼 프로젝트 (11)
    • 독서 (3)
    • 행사 || 컨퍼런스 (4)
    • 회고 (3)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

최근 댓글

hELLO · Designed By 정상우.v4.2.2
indeep
우당탕탕 동시성에 대해 파악하고 thread-safe하게 해보자
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.