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)은 임계구역 밖으로 이동.


필요하시면 현재 서비스 패턴(읽기/쓰기 비율, 경합 지점, 단일/다중 인스턴스)을 알려주시면, 위 기준으로 최소 변경 비용의 설계를 골라 구체 코드까지 붙여 드릴게요.


LIST

+ Recent posts