19. 스레드 안전성의 수준 분류와 합성(composition)의 함정
문제 19 — 스레드 안전성의 수준 분류와 합성(composition)의 함정
"이 클래스는 스레드 안전한가요?"라는 질문에 예/아니오로 답할 수 있는 경우는 드물다. 실제로는 여러 수준이 있고, 각 수준마다 호출자가 져야 할 책임이 다르다. 더 나쁜 것은 개별 연산이 모두 스레드 안전해도 그 연산들을 이어 붙인 조합은 안전하지 않다는 점이다. 게임서버에서 "ConcurrentDictionary 로 바꿨으니 이제 안전하다"는 말이 왜 자주 틀리는지 설명할 수 있어야 한다.
다음을 설명하라.
-
스레드 안전성 수준 분류: 불변(immutable), 스레드 안전(thread-safe), 조건부 스레드 안전(conditionally thread-safe), 스레드 호환(thread-compatible), 스레드 적대(thread-hostile) 다섯 수준을 정의하고, 각 수준에서 호출자가 무엇을 책임져야 하는지 설명하라. 각각에 해당하는 .NET/C++ 표준 라이브러리 타입의 예를 하나씩 들어라.
-
합성의 함정: 아래 두 코드는 각 연산이 개별적으로는 원자적인데도 레이스가 있다. 어떤 인터리빙에서 무엇이 깨지는지 구체적으로 서술하라.
// (가) if (!concurrentDict.ContainsKey(id)) concurrentDict.TryAdd(id, CreateSession(id)); // (나) list 는 다른 코드 경로에서 lock (_gate) 로 보호되고 있다고 하자. // 그런데 이 경로는 락 없이 복합 연산을 수행한다. if (!list.Contains(x)) list.Add(x);이런 check-then-act / read-modify-write 패턴을 안전하게 만드는 방법들을 제시하고 각각의 트레이드오프를 논하라.
-
클라이언트 측 락과 락 캡슐화: 호출자가 컬렉션 객체 자체를
lock해서 복합 연산을 보호하는 "클라이언트 측 락(client-side locking)"은 언제 동작하고 언제 조용히 실패하는가? 컬렉션이 내부적으로 다른 락 객체를 쓰거나 래핑(delegation)돼 있으면 왜 깨지는지 설명하라. 이를 대신하는 설계(락 객체 공개, 복합 연산을 클래스 안으로 넣기 등)의 장단점을 비교하라. -
뷰·반복자 반환이 캡슐화를 깨는 문제: 스레드 안전한 클래스가 내부 컬렉션의 반복자, 뷰, 또는 참조를 그대로 반환하면 어떤 문제가 생기는가?
ConcurrentDictionary의 열거,Keys/Values프로퍼티,ToArray()가 각각 어떤 일관성 수준을 주는지 구분하라. 안전한 대안(스냅샷, 불변 컬렉션, 콜백 주입)의 비용을 논하라. -
문서화 의무와 게임서버 적용: 라이브러리/모듈 작성자가 스레드 안전성에 대해 무엇을 반드시 문서화해야 하는가? 그리고 게임서버에서 흔한 "동시성 컬렉션을 쓰면 안전하다"는 착각이 실제로 어떤 버그(중복 세션 생성, 잘못된 인벤토리 수량, 존 이동 중 유령 엔티티 등)로 나타나는지 예를 들어 설명하고, 근본적으로 더 나은 설계 방향을 제시하라.
직접 답을 작성한 뒤, 위 해설 보기에서 모범답안과 대조하세요.