이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Thread-safe 설계 판단에서 붙잡아야 할 핵심 판단 기준은 무엇인가?
  • 공유 상태를 설계에서 제거한다라는 기준은 어떤 JVM/OS 신호로 확인되는가?
  • Java/Spring 코드에서 Thread-safe 설계 판단 판단 기준은 어떤 장애 신호로 드러나는가?

개요

이 문서의 주제는 Thread-safe 설계 판단이다. Spring bean, singleton service, cache, static field, transaction 경계에서 thread-safe를 판단하는 방법을 정리한다.

먼저 붙잡을 기준은 다음 문장이다. 공유 상태를 설계에서 제거한다. Thread-safe 설계는 동시성 도구를 많이 쓰는 능력이 아니라, 어떤 상태가 어디에 소유되고 어떤 경계에서만 변경되는지 명확히 만드는 일이다.

핵심 모델

  • 판단 기준: 공유 상태를 설계에서 제거한다.
  • Thread-safe 설계는 공유 mutable state를 식별하고 소유권을 정하는 데서 시작한다.
  • Spring singleton bean field, static cache, ThreadLocal, in-memory queue는 설계 리뷰 대상이다.
  • Request별 상태는 method local이나 immutable DTO로 두는 것이 기본이다.
  • 정합성 책임은 JVM lock, DB constraint, optimistic lock, Redis, queue 중 어느 계층인지 명확해야 한다.
  • 다중 인스턴스 배포에서는 단일 JVM 기준의 thread-safe가 시스템 전체 안전을 뜻하지 않는다.

문서별 핵심 구분

  • thread-safe 설계는 공유 mutable state를 줄이는 것에서 시작한다.
  • Spring singleton bean field는 여러 요청 thread가 공유한다.
  • JVM lock은 다중 인스턴스 전체 동시성을 해결하지 못한다.

원리

Thread-safe 판단의 첫 단계는 공유 상태 목록을 만드는 것이다. Singleton bean field, static field, cache, ThreadLocal, in-memory queue, mutable DTO 재사용이 후보가 된다.

두 번째 단계는 상태의 소유자를 정하는 것이다. 요청 local 상태인지, JVM 내부 cache인지, DB가 권위 있는 저장소인지, Redis 같은 외부 store가 책임지는지에 따라 필요한 동시성 전략이 달라진다.

세 번째 단계는 변경 경계를 정하는 것이다. Immutable object, DB constraint, optimistic locking, queue serialization, JVM lock, distributed lock 중 어디가 정합성을 책임질지 선택한다.

따라서 thread-safe 설계는 “synchronized를 붙일까”보다 “공유 mutable state가 꼭 필요한가”와 “단일 JVM을 넘어도 안전한가”를 먼저 묻는다.

Java/Spring 연결

  • Spring singleton bean은 기본적으로 여러 요청 thread가 공유하므로 field는 불변 의존성 위주로 둔다.
  • Request별 상태는 method local, request scope, immutable DTO로 유지하는 것이 안전하다.
  • @Transactional은 DB 상태의 정합성을 다루지만 JVM heap의 shared mutable object를 보호하지 않는다.
  • 다중 인스턴스에서는 JVM lock보다 DB constraint, optimistic lock, Redis lock, message queue가 더 적절할 수 있다.
  • Thread-safe 설계 리뷰는 코드 리뷰와 운영 지표, 장애 재현 결과를 함께 사용한다.
  • Spring 코드 리뷰에서는 Thread-safe 설계 판단 판단 기준이 어떤 thread, pool, timeout, resource 설정으로 드러나는지 확인한다.

코드와 설정 예시

@Service
class OrderService {
    private final OrderRepository orders;
 
    OrderResult place(OrderCommand command) {
        return orders.save(command.toEntity()).toResult();
    }
}

예시는 service가 요청별 상태를 field에 저장하지 않고 method local 객체와 DB 경계로 넘기는 형태다. Field에는 불변 의존성만 두고, 정합성은 repository/DB constraint/transaction 경계에서 다루는 쪽이 thread-safe 설계에 가깝다.

관찰 명령

grep -R "static .*Map\|static .*List\|ThreadLocal" -n src 2>/dev/null | head
jcmd <pid> Thread.print

Thread-safe 설계 판단 관찰 명령은 결론이 아니라 가설 확인 도구다. 운영 중에는 반복 간격, 대상 PID, 수집 시각을 함께 남기고 출력이 큰 명령은 범위를 제한한다.

지표 해석

신호먼저 의심할 것다음 확인
Singleton field 변경요청 간 공유 상태field 목록, scope
ThreadLocal 값 누수thread pool 재사용remove 경로
JVM lock으로만 보호다중 인스턴스 불일치DB/Redis/queue 경계
Cache 값이 권위 상태처럼 사용됨정합성 책임 불명확owner, TTL, invalidation

Thread-safe 설계 판단에서는 핵심 신호와 보조 지표를 같은 시간축에서 맞춘다. 먼저 변한 신호와 뒤따라 포화된 신호를 분리해야 원인과 결과를 구분할 수 있다.

장애 상황에서 보는 순서

  1. 공유 mutable state 목록을 만든다.
  2. 각 상태가 request, JVM, DB, Redis, queue 중 어디에 소유되는지 정한다.
  3. 상태 변경 경계가 method local, transaction, lock, message 처리 중 어디인지 확인한다.
  4. 다중 인스턴스 배포에서 같은 보장이 유지되는지 검토한다.
  5. Lock 추가보다 상태 제거, immutable, DB constraint로 단순화할 수 있는지 본다.

이 순서는 Thread-safe 설계 판단 후보를 빠르게 좁히기 위한 기본 루틴이다. 결론을 내리기 전에는 애플리케이션 증거와 JVM/OS 증거를 최소 한 번 이상 맞춘다.

실전 팁

  • Field에는 불변 의존성과 thread-safe 객체만 두고 요청별 상태는 method local로 둔다.
  • In-memory cache가 권위 있는 상태인지 단순 최적화인지 먼저 정한다.
  • JVM lock으로 해결한 설계가 여러 인스턴스에서도 맞는지 배포 구조를 기준으로 다시 본다.
  • DB unique constraint나 optimistic lock으로 더 단순하게 해결되는 문제인지 확인한다.
  • 장애 회고에는 어떤 상태를 어디로 옮겨 thread-safe를 확보했는지 남긴다.

주니어 팁

  • Singleton bean field에 요청별 값이 들어가면 설계 냄새로 본다.
  • Local variable은 thread별 stack에 놓이므로 field와 다르게 판단한다.
  • “thread-safe collection”이라는 말이 business invariant까지 지켜 준다는 뜻은 아니다.

시니어 팁

  • 상태 소유권 표를 작성해 request, JVM, DB, Redis, queue 중 어느 계층이 책임지는지 명확히 한다.
  • 다중 인스턴스 환경의 정합성은 배포 토폴로지와 함께 리뷰한다.
  • Lock 기반 설계는 contention, timeout, 장애 시 fallback을 설계 문서에 포함한다.

Guru급 팁

  • Thread-safe 설계 판단 주제는 단일 snapshot보다 변화 방향이 중요하다. 같은 명령을 간격을 두고 실행해 상태가 증가, 유지, 회복 중 어디인지 본다.
  • 고급 도구는 Thread-safe 설계 판단 가설을 좁힐 때만 사용하고 기본 지표와 모순되는지 먼저 확인한다.
  • Thread-safe 설계 판단 조치 후에는 CPU, memory, socket, disk, pool 중 다음 포화 지점이 어디로 이동했는지 확인한다.

위험 신호!

  • Singleton bean에 요청별 mutable field를 둔다.
  • JVM 내부 lock으로 여러 서버 인스턴스의 동시성을 해결했다고 생각한다.
  • Cache를 권위 있는 저장소처럼 쓰면서 정합성 정책을 정의하지 않는다.
  • ThreadLocal을 쓰고 remove 경로를 두지 않아 thread pool 재사용에서 값이 새어 나간다.
  • Lock 추가로 증상을 줄이고 상태 소유권 문제를 남겨 둔다.

확인 질문

확인 질문

  • Thread-safe 설계의 첫 질문은 무엇인가?
    • 공유 mutable state가 있는지, 있다면 누가 소유하고 어느 경계에서 변경되는지 묻는 것이다.
  • Spring singleton bean에서 안전한 field와 위험한 field는 어떻게 나뉘는가?
    • 불변 의존성은 안전한 편이지만 요청별로 값이 바뀌는 mutable field는 여러 thread가 공유하므로 위험하다.
  • 다중 인스턴스 배포에서 JVM lock만으로 부족한 이유는 무엇인가?
    • Lock이 한 JVM 안에서만 동작하므로 다른 인스턴스가 같은 DB row나 외부 자원을 동시에 변경할 수 있기 때문이다.

참고 문서