서버와 동시 실행
클라이언트 요청마다 스레드를 할당해서 처리
클라이언트 요청을 처리할 때 사용할 동시 요청 개수만큼 스레드가 동시에 실행
비동기 IO (또는 논블로킹 IO)를 사용해서 처리
단일 스레드가 아닐경우 실행
잘못된 데이터 공유로 인한 문제 예시
필드 주입을 이용한 데이터 값 삽입 시 생기는 문제
- genPayId()로 생성한 값을 payId 필드에 할당
- payId 필드를 이용해서 임시 저장
- sendPayDate()를 호출하고 리턴 결과를 resp 변수에 저장
- applyResponse() 호출
- 파라미터로 받은 resp를 이용해서 PayDate를 생성한다.
- updatePayDate() 메소드에 payId 필드를 전달한다.
- payId : 1, payId : 2 스레드가 동시에 작동 → 값이 변경되는 오류가 발생할 수 있습니다.
프로세스 수준에서의 동시 접근 제어
잠금(lock)을 이용한 접근 제어
잠금은 공유 자원에 접근하는 스레드를 한 번에 하나로 제한할 수 있다
- 잠금을 획득함
- 공유 자원에 접근(임계 영역)
- 잠금을 해제함
임계 영역 : 동시에 둘 이상의 스레드나 프로세스가 접그한면 안되는 공유 장원에 접근하는 코드 영역을 말함
synchronized : 간단하게 잠금 설정 가능
ReentrantLock : 잠금 획득 대기 시간 지정, 가상 스레드
세마포어
동시에 실행할 수 있는 스레드 수를 제한
- 자원에 대한 접근을 일정 수준으로 제한하고 싶을 때 사용
- 퍼밋 : 자바 세마포어 구현체
- 세마포어에서 퍼밋 획득(허용 가능 숫자 1 감소)
- 코드 실행
- 세마포어에 퍼밋 반환(허용 가능 숫자 1 증가)
읽기 쓰기 잠금
읽기 빈도가 높을 때 좋음
- 쓰기 잠금은 한 번에 한 스레드만 구할 수 있음
- 읽기 잠금은 한 번에 여러 스레드가 구할 수 있다
- 한 스레드가 쓰기 잠금을 획득했다면 쓰기 잠금이 해제될 때까지 읽기 잠금을 구할 수 없다
- 읽기 잠금을 획득한 모든 스레드가 읽기 잠금을 해제할 때까지 쓰기 잠금을 구할 수 없다.
→ 읽기 성능 문제 완화
원자적 타입
잠금을 사용하면 간단하게 동시성 문제 해결은 가능하나 CPU 효율이 떨어진다.
- AtomicLong, AtomicBoolean 사용
- 내부적으로 CAS 연산 사용
- 스레드를 멈추지 않고 다중 스레드 환경에서 안전하게 값 변경
동시성 지원 컬렉션
스레드에 안전하지 않은 컬렉션을 여러 스레드가 공유하면 동시성 문제 발생했을 때 동기화된 컬렉션을 사용
- 자바의 Collections 클래스 : 동기화된 컬렉션을 생성하는 메소드를 제공
- 기존 컬렉션 객체를 쉽게 동기화된 컬렉션 객체로 변환 가능
- 모두 동기화된 블록에서 실행되어 동시성 문제 해결
컬렉션 타입을 사용하는 방법
- HashMap → ConcurrentHashMap 사용
- 데이터 변경할 때 잠금 범위를 최소화
- 키의 해시 분포가 고르고 동시 수정이 많으면, 동기화된 맵을 사용하는 것보다 더 나은 성능이다.
DB와 동시성
DB 트랜잭션이 동시성 문제 해결 불가 → DB에 맞는 잠금 기능 활용
- 선점 잠금 : 동일한 레코드에 대해 한 번에 하나의 트랜잭션만 접근할 수 있도록 제어
- 비선점 잠금 : 쿼리 실행 자체는 막지 않으면서도 데이터가 잘못 변경되는 것을 막을 수 있다.
선점 잠금
데이터에 먼저 접근한 트랜잭션이 잠금을 획득하는 방식
비선점 잠금
명시적으로 잠금을 사용하지 안ㅎ고 데이터를 조회한 시점의 값과 수정하려는 시점의 값이 같은지 비교하는 방식
- select 쿼리를 실행할 때 version 칼럼을 함께 조회
- 로직 수행
- update 쿼리를 실행할 때 version 칼럼을 1 증가, 이때 version 칼럼 값이 1에서 조회한 값과 같은지 비교하는 조건을 where 절에 추가
- update 결과로 변경된 행 개수가 0이면, 이미 다른 트랜잭션이 version 값을 증가시킨 것이므로 데이터 변경에 실패한 것이다. 이 경우 트랜잭션을 롤백
- update 결과로 변경된 행 개수가 0보다 크면, 다른 트랜잭션보다 먼저 데이터 변경에 성공한 것이므로 트랜잭션을 커밋
외부 연동과 잠금
트랜잭션 범귀 내에서 외부 시스템과 연동해야 한다면 → 선점 잠금을 고려해야 한다.
- 외부 연동이 취소됐는데, 데이터 변경에 실패해서 트랜잭션이 롤백되는 문제 발생
- 비선점 잠금을 사용하고 싶으면 트랜잭션 아웃박스 패턴 적용
증분 쿼리
동시에 쿼리를 실행시 2가 아닌 1만 증가하는 문제 발생할 시
- 선점 잠금 사용 시 잠금 대기 시간만큼 응답 시간이 길어진다.
- 비선점 잠금을 사용하면 대기 시간은 없지만 변경 실패 에러가 자주 발생할 수 있다.
- 원자적 연산으로 처리하면 문제점을 없앨 수 있습니다.
잠금 사용 시 주의 사항
잠금 해제하기
잠금을 획득한 뒤에는 반드시 잠금을 해제해야 한다.
- 그렇지 않을 경우 잠금을 시도하는 스레드가 무한정 대기하게 됨, 세마포어도 함께
대기 시간 지정하기
동시 접근이 많아지면 대기 시간이 길어지는 문제 발생 → 대기 시간을 지정해주어 문제 해결
- 긴 대기가 초래하는 불안감을 줄여주어 사용자를 안심시킴
교착 상태 피하기
한 작업에서 2개 이상의 자원의 잠금을 획득하는 코드 구조
- 교착 상태 : 2개 이상의 스레드가 서로가 획득한 잠금을 대기하면서 무한히 기다리는 상황
- 잠금 대기 시간을 제한하는 방법으로 교착상태를 풀 수 있음
- 지정한 순서대로 잠금을 획득하게 하는 방법으로 문제 해결
자료 출처
https://product.kyobobook.co.kr/detail/S000216376461