Java ‐ 과도한 동기화는 피하라[Effective Java Item 79] - thought-corner/Backend-PlayGround GitHub Wiki
외계인 메서드(Alien Method)로 인한 교착 상태(Deadlock) 위험
과도한 동기화의 가장 큰 무서움은 단순히 느려지는 것이 아니라 프로그램이 그대로 멈춰버리는 버그를 유발한다는 점이다.
특히 동기화 블록 안에서 제어할 수 없는 외부 메서드를 호출할 때 문제가 터진다.
상황 : 동기화 메서드 안에서 클라이언트가 넘겨준 콜백 함수나 재정의 가능한 메서드를 호출했다고 가정하자.
문제 : 외부 메서드가 이 안에서 무슨 행동을 할지 모른다. 외계인 메서드가 교묘하게 다른 쓰레드를 만들어서 클래스의 자원을 다시 요구하거나 다른 락을 획득하려고 하면 순식간에 락이 꼬이면서 시스템이 영원히 대기하는 교착 상태에 빠진다.
해결 : 동기화 블록 안에서는 가시적인 최소한의 작업만을 하고 빨리 빠져나와야 한다. 정체를 알 수 없는 외부 메서드를 동기화 영역 안으로 끌어들이면 안 된다.
컨텍스트 스위칭으로 인한 대규모 성능 저하
락을 쥐고 있는 시간이 길어지거나 너무 자잘하게 동기화가 걸려 있다면 멀티 쓰레드의 가장 큰 장점인 병렬 처리 능력이 상실된다.
암달의 법칙 : 코어를 늘려도 프로그램 구조상 synchronized 키워드로 묶여서 쓰레드 딱 하나만 순차적으로 통과해야 하는 직렬 영역이 길어진다면 전체 성능은 그 직렬 영역의 속도에 갇히게 된다는 원칙이다.
컨텍스트 스위칭 비용 : CPU 입장에서 쓰레드가 락을 얻지 못하고 대기 상태(BLOCKED)로 갈 때, 그리고 다시 락을 획득해 꺠어날 때 컨텍스트 스위칭이라는 거대한 비용이 발생한다. CPU가 비즈니스 로직 연산을 하는 시간보다 쓰레드를 재우고 깨우는 절차에 더 많은 에너지를 쓰게 되면서 서버 처리량(Throughput)이 저하된다.
현대적 아키텍처와의 상극
Thread-per-request의 한계 : 요청당 쓰레드를 하나씩 할당하는 전통적인 서블릿 구조에서 락 대기가 길어지면 Tomcat 쓰레드 풀이 순식간에 고갈된다.
리액티브/가상 쓰레드 환경에서 부적합 : 논블로킹(Non-Blocking) 기반의 reactive 스택이나 자바 21의 가상 쓰레드를 도입한 환경이라면 문제가 심각해진다. 가상 쓰레드가 synchronized 블록을 만나면 캐리어 쓰레드에 계속 마운트되는 핀 고정 현상이 발생한다. 이로 인해 대규모 트래픽을 처리하려고 만든 것이 부메랑이 되어 돌아오게 된다.
이 Pinning이란, 언마운트되어야 할 타이밍에 내려오지 못해서 큐에 있는 다른 가상 쓰레드들의 Starvation 현상을 말하는 것이다.