synchronized는 간단‧안전하지만, 성능·유연성에서 트레이드오프가 분명합니다. 한 번에 훑어보실 수 있게 정리했어요.
synchronized의 장단점(Trade-off)
장점
간단함/안정성: 진입·탈출 시 자동으로 happens-before 보장(가시성/메모리 장벽 포함), 재진입 가능(reentrant), 예외 발생해도 자동 해제.
JIT 최적화: 경합이 낮을 땐 경량화/편향 락 등으로 오버헤드가 작음(현대 JVM).
단점
대기 제어 부재: tryLock, 타임아웃, 인터럽트 가능 락이 없음 → 오래 기다릴 수밖에.
공정성/우선순위 없음: priority inversion·기아(starvation) 완화 수단 부족.
과도한 직렬화: 락 범위가 넓으면(메서드 전체 등) 멀티코어 확장성 급감.
데드락 위험: 여러 모니터를 교차 획득 시 순서 불일치로 교착.
관측/디버깅 한계: 대기 큐 상태, 보유 여부를 세밀하게 다루기 어려움.
분산 환경 한계: JVM 프로세스 경계를 넘지 못함(멀티 인스턴스/분산 락 불가).
> 언제 적합? 경합이 낮고, 임계구역이 아주 짧고, 단일 JVM에서 간단히 보호하면 끝일 때.
---
대안적 접근(상황별)
1) 같은 JVM 내에서의 동시성 제어
ReentrantLock
lockInterruptibly(), tryLock(timeout), 공정성 옵션 지원, 조건변수(newCondition)로 세밀한 대기/신호 가능.
예:
ReentrantLock lock = new ReentrantLock(true); // 공정 락
if (lock.tryLock(50, TimeUnit.MILLISECONDS)) {
try { /* 임계구역 */ }
finally { lock.unlock(); }
}
ReadWriteLock (예: ReentrantReadWriteLock)
읽기 다수/쓰기 소수인 워크로드에 유리(쓰기 시엔 여전히 단일 진입).
StampedLock
낙관적 읽기로 읽기 경합이 큰 곳에서 성능 우수. (단, 재진입/조건변수 미지원, 인터럽트 불가 주의)
예:
long stamp = lock.tryOptimisticRead();
var snapshot = data; // 읽기
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try { snapshot = data; }
finally { lock.unlockRead(stamp); }
}
락 분할/세분화(lock splitting/sharding, striping)
큰 공유 구조를 키 범위별 여러 락으로 쪼개 동시성↑.
락 없는(비차단) 기법
Atomic*, VarHandle(JDK9+), LongAdder(고경합 카운터) 등 CAS 기반.
예: 다중 스레드 카운터 → LongAdder 권장.
병행 컬렉션 활용
ConcurrentHashMap(버킷·트리화 기반), ConcurrentLinkedQueue, ConcurrentSkipListMap 등.
불변/Copy-On-Write
읽기 훨씬 많고 크기 작을 때 CopyOnWriteArrayList/스냅샷 불변 객체로 락 제거.
스레드 컨파인먼트(Thread confinement)
소유 스레드만 쓰게 설계(예: 이벤트 루프, Actor) → 공유 상태 자체를 최소화.
2) 데이터베이스/도메인 수준에서의 일관성
낙관적 락(Optimistic Locking): JPA @Version 컬럼 사용 → 충돌 시 재시도/사용자 충돌 해결 UX.
비관적 락(Pessimistic): LockModeType.PESSIMISTIC_WRITE/READ 또는 SELECT ... FOR UPDATE 등.
트랜잭션 격리/제약조건: 유니크 키·체크 제약·원자적 업데이트(SQL)로 응용계층 락을 대체.
예: 재고 차감 UPDATE ... SET qty = qty - ? WHERE id = ? AND qty >= ? (반환행 수로 성공 판단).
3) 분산/멀티 인스턴스 환경
분산 락: Redis(Redisson), ZooKeeper, etcd(세션/리스 기반) — 만료/페일오버·재진입/공정성 옵션 검토.
메시지 큐/직렬화: Kafka/RabbitMQ로 단일 파티션 소비자가 순서 보장/직렬 처리.
샤딩: 키 해싱으로 요청을 인스턴스/파티션에 고정 라우팅(락 범위 축소).
---
선택 가이드(현업 감각)
짧고 저경합 + 단일 JVM: synchronized(가독성 최우선).
타임아웃/인터럽트/조건 필요: ReentrantLock.
읽기 압도적: StampedLock(낙관적 읽기) → 복잡도↑를 감수할 가치 있을 때.
카운터/집계: LongAdder. 맵 누적은 compute/merge 등 CHM API.
비즈 규칙 충돌 방지: DB 낙관/비관 락 + 제약조건/원자적 SQL.
멀티 인스턴스/서버리스: Redisson 분산 락 또는 MQ로 직렬화.
성능 튜닝: 임계구역 짧게, 락 범위 최소화/세분화, I/O는 임계구역 밖으로, 불변화 적극 활용.
---
실수 방지 팁
락 대상 노출 금지: synchronized(this)/공개 객체 대신 private final Object lock = new Object();
락 순서 합의: 여러 락 획득 시 전역 순서를 문서화.
double-checked locking엔 volatile 필수.
긴 작업/외부 호출(I/O, 원격 API)은 임계구역 밖으로 이동.
필요하시면 현재 서비스 패턴(읽기/쓰기 비율, 경합 지점, 단일/다중 인스턴스)을 알려주시면, 위 기준으로 최소 변경 비용의 설계를 골라 구체 코드까지 붙여 드릴게요.
'Spring & Backend' 카테고리의 다른 글
| 퍼포먼스를 분석할 때 Chrome DevTools에서 어떤 지표를 체크해야 하나요? (0) | 2025.09.24 |
|---|---|
| Spring과 Spring Boot의 차이를 말해주세요. 백엔드와 관련된 질문이에요. (0) | 2025.09.24 |
| AutoConfiguration 동작 원리를 설명해주세요. (0) | 2025.09.23 |
| 전자정부 프레임워크의 변천사 (2) | 2025.09.22 |
| Redis와 직렬화 (0) | 2025.09.22 |
