순간을 성실히, 화려함보단 꾸준함을

동시성 문제에 관하여... 본문

나의 개발 메모장

동시성 문제에 관하여...

폭발토끼 2023. 12. 10. 22:48

글또 9기 첫번째 글입니다.

 

동시성 문제란 무엇일까요?

하나의 api 에 동시에 요청이 들어올때 데이터의 정합성이 보장받지 못하는 문제를 뜻합니다.

그럼 꼭 완벽하게 동시에 요청이 들어와야 하는 걸까요???그렇지는 않습니다.

첫번째 요청을 처리하기 전 두번째 요청이 시작되면 이런 현상도 동시성 문제라고 간주합니다.

결국 데이터의 정합성이 보장받지 못하게 되는 거니까요.

 

데이터의 정합성이라는 게 뭘까요?

보통 트랜잭션에 관하여 공부하게 되면 나오는 개념인데 정의는 이와 같습니다.

'데이터베이스는 항상 일관된 상태를 유지해야 한다'

무슨 뜻일까요?

 

예를 들어 2개의 계좌가 있다고 가정해보겠습니다.

계좌 A : 10000원

계좌 B : 5000원

 

이때 계좌 A->계좌B 로 2000원을 이체합니다.

단계1 : 계좌A 에서 2000원 차감(계좌A : 8000원)

단계2 : 계좌B 에서 2000원 추가(계좌B : 7000원)

 

최종

계좌 A : 8000원

계좌 B : 7000원

 

위와 같이 되어야겠죠.

 

다른 예시로 2000원을 이체하려고 하고 난 뒤 9000원을 이체하려고 합니다. 이때 2000원이 이체된 후에는 8000원이 남으니 9000원 차감하려는 요청을 수행하면 안되겠죠.

그러나 첫번째 요청과 두번째 요청 간극이 굉장히 작아 동시에 요청이 들어가서 2000원을 차감하기 직전 10000원을 기준으로 9000원을 차감하려고 하면??? 결국 계좌 A 에서는 음수(-1000원) 이 되어버립니다...

이런 상황은 발생하면 안되겠죠?

 

락이란?

정의 : 데이터베이스에서 동시성 관리를 지원해주는 기능으로서, 여러 트랜잭션이 동일한 데이터에 동시에 접근할 때 발생할 수 있는 문제를 방지하는 기능

 

즉, 간단히 말해서 하나의 요청(트랜잭션)이 커밋 되기 전까지 다른 요청은 수행하지 않게끔 막아주는 역할입니다.

위 예시를 가지고 설명하면 2000원을 차감하려는 트랜잭션이 발생될때 해당 요청이 완료되기까지 어떤 요청도 접근하지 못하게 되는 것이죠. 결국 9000원을 차감하려는 요청은 2000원이 차감된 8000원을 기준으로 실행되니 차감되지 못하게 되는 것 입니다.

 

데이터에 동시에 접근하고 수정할때 발생할 수 있는 충돌을 구분하여 비관적 락과 낙관적 락으로 나눌 수 있습니다.

해당 내용은 다른 블로그나 책에서 훨씬 잘 정리되어 있으므로 어떻게 사용하는지만 소개하도록 하겠습니다.

 

낙관적 락

- JPA 에서는 @Version 어노테이션을 사용하여 락을 걸 수 있습니다.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private int reward;

@Version
private Long version;

private String gender;

private String country;
UPDATE USER
SET REWARD = ?,
    VERSION = VERSION+1
WHERE USER_ID = ?
   AND VERSION = ?

Entity 에 @Version 어노테이션을 정의해주면 Entity 의 변화가 발생할 때마다 version 을 +1 시킵니다.

따라서 동시에 요청이 발생한다고 한들 첫번째 요청이 먼저 완료되면 version 을 +1 시킬 것이니 후에 들어온 요청에 대해서는 해당하는 version 이 없으므로 실행되지 않고 exception 을 터트립니다.

 

비관적 락

- select for update 구문을 이용하는 방식입니다.

//Repository layer
public interface UserRepository extends JpaRepository<User,Long> {
     @Lock(LockModeType.PESSIMISTIC_WRITE)
     @Query("select u from User u where u.id = :id")
     User findWithIdForUpdate(@Param("id")Long id);
}

위와 같이 @Lock 어노테이션을 사용하여 정의해 줄 수 있습니다.

//결과 query
  select
      user0_.id as id1_3_,
      user0_.create_date as create_d2_3_,
      user0_.modified_date as modified3_3_,
      user0_.country as country4_3_,
      user0_.gender as gender5_3_,
      user0_.reward as reward6_3_ 
  from
      users user0_ 
  where
      user0_.id=? for update

실제로 실행된 쿼리내역입니다.

 

이와 같이 오늘은 간단하게 동시성 문제를 어떻게 해결할 것인지에 대해 적어보았는데요.

아마 좀 더 깊은 학습이 필요해보이며 부족한 부분은 채워나아가야겠습니다.

 

감사합니다