728x90
반응형
동시성과 공유 자원 🗂️ | 꼬이지 않게 관리하는 법
서버가 동시에 여러 요청을 처리하다 보면, 하나의 데이터를 여러 스레드가 동시에 건드리는 상황이 생긴다.
이때 제대로 제어하지 않으면 데이터가 꼬이고, 예기치 못한 결과가 발생한다.
이 글에서는 그런 문제를 동시성과 공유 자원 관점에서 정리한다.
1️⃣ 동시성과 공유 자원의 문제
여러 스레드가 동시에 하나의 자원(변수, 객체, DB 레코드 등)에 접근할 때 문제가 생긴다.
이 현상을 Race Condition(경쟁 상태)이라고 한다.
예시
- 두 스레드가 같은 카운터 값을 읽고 +1을 수행했지만, 실제로는 한 번만 증가되는 경우
- 동시에 재고를 차감해 재고가 음수가 되는 경우
2️⃣ 왜 중요한가
웹 서버나 이벤트 시스템처럼 동시에 요청이 쏟아지는 환경에서는, 이런 문제가 데이터 무결성(integrity)을 직접적으로 깨뜨린다.
예를 들어 재고가 1인데 두 주문이 동시에 들어오면,
둘 다 성공하여 재고가 -1이 되거나 한쪽이 누락되는 문제가 생길 수 있다.
3️⃣ 원자성 / 가시성 / 배타성
동시성 제어는 세 가지 핵심 개념으로 구성된다.
- 원자성(Atomicity)
하나의 연산이 중간 상태 없이 완전히 수행되는 성질이다.
`count++` 같은 단순 연산도 내부적으로 여러 단계이므로, 섞이면 오류가 생긴다. - 가시성(Visibility)
한 스레드의 변경이 다른 스레드에서 즉시 보이도록 하는 성질이다.
CPU 캐시나 메모리 동기화 지연으로 가시성이 깨질 수 있다. - 배타성(Mutual Exclusion)
여러 스레드가 동시에 동일 자원에 접근하지 못하도록 막는 성질이다.
4️⃣ 해결 방법
- synchronized / Lock
특정 코드 블록에 한 스레드만 진입하게 하여 배타성을 확보한다. - volatile
캐시를 무시하고 메인 메모리에서 직접 읽어 가시성을 보장한다.
단, 복합 연산(읽기 → 수정 → 쓰기)은 보장하지 않는다. - Atomic 클래스 사용
AtomicInteger, AtomicLong 등을 사용하면 CAS 기반으로 원자성을 보장한다. - DB 수준의 락 활용
비관적 락, 낙관적 락 등 DB 차원의 제어를 함께 고려한다.
5️⃣ Java에서 자주 쓰는 동시성 제어 기법
| 기법 | 특징 |
| `synchronized` | 간단하지만 JVM 내부에서만 유효하다. |
| `Lock / ReentrantLock` | 세밀한 제어가 가능하다. |
| `volatile` | 가시성만 보장하며 복합 연산은 불가능하다. |
| `AtomicXxx` (다양한 시리즈) | 락 없이 원자적 연산을 수행한다. |
| `DB 락 / 분산 락` | 여러 서버 간 자원 제어를 위해 필요하다. |
6️⃣ 애플리케이션 vs DB vs 분산 환경
- 애플리케이션 락은 간단하지만 단일 서버에서만 유효하다.
- DB 락은 트랜잭션 격리 수준을 활용해 레코드 단위의 제어가 가능하다.
- 분산 락은 Redis나 Zookeeper를 통해 여러 서버 간 동시성을 제어한다.
환경이 단일인지, 다중 서버인지에 따라 접근 방식이 달라야 한다.
⚠️ 주의할 점
- `@Transactional`과 `synchronized`를 함께 사용하면 예상치 못한 동작이 발생할 수 있다.
- 락을 과도하게 사용하면 병목이 생긴다.
- 가능한 공유 상태를 줄이고 불변 객체를 활용하는 것이 좋다.
7️⃣ 정리
동시성 제어는 코드 한 줄로 끝나는 문제가 아니다.
원자성, 가시성, 배타성을 이해하고, 환경(단일/분산)과 데이터 특성을 고려해야 안정적인 시스템이 만들어진다.
728x90
반응형
