최근에 동시성에 관한 이슈를 검색하고 공부했던 것을 글로 간략하게 정리하고자 한다.
동시성 이슈는 개발을 하면서 빈번하게 발생할 이슈는 아니지만 발생 여지가 있다면 개발자로서 알아야 할 내용인 것 같아서 정리해봤다.
일반 변수에 대한 동시성 이슈
일반 변수가 동시성 이슈가 발생하는 경우
일반 변수에 대한 동시성 이슈가 발생한다는 것은 당연히 일반 변수가 공유될 때만 일어난다.
해당 변수가 Static 변수로 설정되어있다라든가 혹은 쓰레드를 이용해 동시 접근을 하는 경우에만 발생한다.
public class TestClass {
static int = a; //공유 변수 사용
public void test(){
}
}
원칙적으로는 위와 같이 변수를 공유하는 경우는 극히 드물고 사용해서도 안 되는 게 원칙이다.
스프링 구조에서는 이렇게 쓰는 방식을 엄연히 금지하고 있다.(물론 아예 사용이 안 되는 것은 아니다)
이러한 문제는 공유 변수를 위와 같이 변수에 담아 관리하는 것이 아니라 전부 DB에 의존하게 만들면 문제는 없다.
문제가 될만한 상황이라면 위처럼 Spring 진영에서 하지말라는데도 기어코 개발자가 비즈니스 영역에서 값을 관리한다거나 할 때 문제가 발생한다.
가령 DB를 거치면 자원 소모가 많다는 이유에서 변수를 공유한다거나 스프링이라면 Component-Scan 영역에 들어가 있지 않은 공통 함수와 같은 곳에서 사용하는 변수에 대해 공유 차원에서 사용할 수도 있다.
일반 변수에 대한 동시성 이슈 해결 방법
어찌됐든 기어코 변수를 공유해서 사용한다면 해결 방법은 아래와 같다.
일반 변수에 대한 동시성을 제어하기 위한 3가지 방법으로는 Synchronized, Volatile, Concurrent 패키지의 Atomic 클래스 방식이 있다. Synchronized와 Volatile은 성능 문제로 Atomic을 사용한다.
Synchronized는 메소드 영역에 설정해서 메소드 자체를 임계 영역(Critical Section)으로 설정해 동시 진입을 못하게 하는 방식이다.
진입 자체가 막혀버리니 대기 시간으로 인한 속도 이슈가 발생한다.
가장 좋은 방법은 Atomic이다. Compare-And-Swap(CAS) 알고리즘 방식을 사용하는데, 이 방식은 메소드에 진입을 막진 않으면서(속도가 빠름) 위의 알고리즘 덕분에 동시성 문제도 같이 해결해주는 방식이다.
만약에 변수가 HashMap으로 되어있다면 ConcurrentHashMap 클래스를 사용하면 된다.
웬만하면 개발 중에 이런 케이스를 만날 일이 없겠지만 알고 안쓰는 것과 모르고 놓치는 것은 다른 문제다.
데이터베이스의 동시성 이슈
아래 내용을 읽기 위해서는 트랜잭션 격리수준에 대한 선행지식이 필요하다.
해당 글과 관련해 잘 정리해주신 분이 있어 링크를 걸어둔다.
일단 변수에 비해서 데이터베이스의 동시성 이슈는 좀 더 복잡하다.
데이터베이스에서도 동시성 문제를 해결하기 위한 방법은 여러 개가 존재하는데 위에서 얘기했던 Synchronized도 하나의 방법이지만 속도 이슈로 사용을 하지 않는다.
그 다음으로 사용하는 것이 Lock이다.
근데 Lock 메커니즘을 개발자들이 오해하고 있는 부분이 굉장히 많은 것 같다.
이게 특히 JPA를 사용하는 개발자들이 많이 오해한다.
Lock 메커니즘의 오해
JPA 동시성 문제로 검색하면 비관적 락, 낙관적 락에 대한 설명이 많다.
그리고 락을 동시성 문제를 처리하는 것이라고 설명하는 글이 많다.
하지만 결론부터 말하자면 InnoDB 엔진에서는 Lock 메커니즘을 동시성 이슈를 막는 역할로 사용하지 않아도 된다.
왜냐면 이미 동시성 이슈가 다른 기술들로 해결되고 있기 때문이다.
그럼 동시성 이슈는 어떻게 막아지는 걸까?
JPA 1차 캐시
오히려 JPA에서는 1차 캐시가 보장되기 때문에 개발자가 강제로 Flush 하지 않는 이상 Non-Repeatable Read(하나의 트랜잭션에서 같은 쿼리를 2번 참조했을 때 서로 다른 값 검색)가 발생할 이유가 없다.
InnoDB의 Default 격리 수준
InnoDB의 Default 격리 수준이 Repeatable-Read 이기 때문에 Non-Repeatable Read가 발생할 여지가 없다.
MVCC
DBMS가 기본적으로 제공하는 MVCC의 기능으로 인해 Non-Repeatable Read를 막을 수 있다.
MVCC(다중 버전 동시성 제어)
Lock 메커니즘은 기본적으로 동시 작업 수를 제한하는 방식이다.
하나의 쓰레드가 락을 보유하는 동안 다른 쓰레드는 락을 기다리는 상태에 들어간다.
이렇게 하면 당연히 속도가 느려지고 처리량이 낮아질 수밖에 없다.
이러한 방식을 개선시켜 등장한 것이 MVCC다.
MVCC는 DBMS가 기본적으로 제공하는 기능이다. MVCC는 Undo-Log라는 영역을 사용해서 격리 수준이 Read Committed만 되어도 항상 Non-Repeatable Read 현상을 막아준다.
그렇기에 MVCC가 작동해서 Non-Repeatable Read를 막아주고 있는데 동시성 제어를 위해 락을 한번 더 걸어주면 MVCC의 이점을 무효화하는 행위를 하는 것이다.
The last resort is to manually set an exclusive lock on all the necessary rows (SELECT FOR UPDATE) or even on the entire table (LOCK TABLE). This always works, but nullifies the benefits of multiversion concurrency
개발자가 수동으로 모든 행(SELECT FOR UPDATE) 또는 전체 테이블(LOCK TABLE)에 대해 수동으로 단독 잠금을 설정하는 경우 작동은 하지만 다중 버전 동시성(MVCC)의 이점을 무효화합니다.
더불어 InnoDB 엔진에서는 Next-Key Locking 알고리즘을 사용하고 있어 Repeatable Read에서도 Phantom Read를 예방할 수 있다.
InnoDB 엔진에서는 어떻게 됐든 Non-Repeatable Read와 Phantom Read가 다 예방이 된다.
그럼 Lock 메커니즘은 언제 써?
mysql에서는 동시성 이슈를 위해 락을 쓰는 것이 아니라 갱신 분실(Lost Update)이 발생할 때 사용한다.
갱신 분실도 동시성 이슈로 분류할 수 있긴 하지만 속도가 빠른 트랜잭션 내의 처리가 아닌 게시판 같은 곳에서 데이터가 덮어지는 문제를 말한다.
MVCC는 어찌 됐든 Repeatable-Read만 보장할 뿐이지.
결과적으로 저장을 안 하는 것은 아니다. 저장을 하기 때문에 갱신 분실이 일어날 수가 있는 것이다.
그렇기에 갱신 분실을 예방하기 위해 Lock을 사용할 수 있다.
Lock을 사용하면 이전 트랜잭션 내용과 다를 때 저장 자체를 하지 않기 때문에 분실의 위험을 예방할 수 있게 된다.
그 외
"어? 블로그 이곳저곳에서는 동시성 문제가 발생한다고 하던데, 이 블로그는 왜 동시성 문제가 없다고하지?" 라고 생각할 수 있다. 보통 그런 글들의 대부분 공통점은 하나의 쓰레드내에서 동시, 병렬 처리를 할 경우에는 동시성 문제가 발생한다.(cyclicBarrier를 이용해 테스트 가능) 이런 경우에는 분산락, 스핀락을 이용해 처리한다고 한다.
최대한 간단하게 썼지만 내가 봐도 이해하기 어려운 주제인 듯 하다.
'📖 ORM' 카테고리의 다른 글
Kotlin에서 JPA 사용법 (0) | 2023.06.21 |
---|---|
Fetch Join 사용 시 조건문(Condition) 올바르게 사용하기 - 실습으로 배우는 JPA 4편 (0) | 2022.11.03 |
@Transactional 사용 시 자기 호출(Self-Invocation) 이슈 - 실습으로 배우는 JPA 3편 (2) | 2022.07.06 |
findAll()에 관한 N+1 테스트 - 실습으로 배우는 JPA 2편 (0) | 2022.06.30 |
@OnDelete와 CascadeType.ALL, orphanRemoval 속성 (0) | 2022.06.24 |