언어 기초 인터뷰
공유 상태 동시성 면접: 경쟁 상태와 잠금
경쟁 상태, 원자성, 뮤텍스와 원자 연산, 데드락의 네 조건, 스레드·프로세스·코루틴의 차이를 정리한 면접 준비 가이드입니다.
이 글이 다루는 것과 다루지 않는 것
동시성 질문은 크게 두 갈래로 나뉩니다. 하나는 “기다리는 동안 무슨 일이 벌어지는가”를 묻는 갈래입니다. 이벤트 루프, 마이크로태스크, await의 재개 지점, 취소와 타임아웃이 여기 속합니다. 이 갈래는 동시성·비동기 면접에서 따로 다루고 있으니 그쪽을 먼저 보셔도 좋습니다.
이 글은 다른 갈래입니다. 여러 실행 흐름이 같은 데이터를 동시에 만질 때 무엇이 깨지는가를 다룹니다. 실행 순서가 아니라 데이터의 일관성이 주제이고, 답의 핵심 어휘도 큐와 콜백이 아니라 불변식·원자성·잠금 범위입니다. 두 갈래를 섞어 말하면 면접에서 곧바로 티가 납니다. 순서 질문에 잠금 이야기를 꺼내거나, 공유 데이터 질문에 “비동기로 처리하면 됩니다”라고 답하는 식입니다.
구분하는 기준은 간단합니다. 질문에 “두 요청이 동시에 들어오면”, “카운터가 안 맞습니다”, “재고가 마이너스가 됐습니다” 같은 말이 들어 있으면 공유 상태 문제입니다. “왜 이 로그가 먼저 찍히나요”, “왜 응답이 늦나요”면 실행 모델 문제입니다.
경쟁 상태는 “동시에 실행돼서” 생기지 않습니다
많은 지원자가 경쟁 상태를 “두 스레드가 동시에 실행되어 값이 꼬이는 것”이라고 정의합니다. 틀린 말은 아니지만 이 정의로는 원인을 짚지 못합니다. 더 쓸모 있는 정의는 이것입니다. 어떤 불변식이 여러 단계에 걸쳐 유지되어야 하는데, 그 단계 중간에 다른 흐름이 상태를 관찰하거나 변경할 수 있을 때 경쟁 상태가 생깁니다.
여기서 핵심 단어는 “중간”입니다. 동시 실행이 문제가 아니라, 논리적으로 하나여야 하는 연산이 물리적으로 여러 개로 쪼개져 있다는 점이 문제입니다. 그래서 코어가 하나뿐이어도, 진짜 병렬 실행이 없어도 경쟁 상태는 생깁니다. 운영체제가 그 중간 지점에서 스레드를 갈아 끼우기만 하면 충분합니다.
경쟁 상태는 형태로 나누면 두 가지가 거의 전부입니다. 첫째는 검사 후 행동입니다. 조건을 확인하고 그 결과에 따라 행동하는데, 확인과 행동 사이에 조건이 바뀝니다. 재고를 확인하고 차감하기, 파일이 없는지 보고 만들기, 사용자가 없는지 보고 가입시키기가 모두 이 형태입니다. 둘째는 읽고 고쳐 쓰기입니다. 현재 값을 읽어 계산한 뒤 다시 쓰는데, 읽은 뒤 쓰기 전에 다른 흐름이 같은 값을 읽어 갑니다. 카운터 증가, 잔액 갱신, 집계 누적이 여기 속합니다.
count++가 원자적이지 않은 이유
이 질문은 거의 반드시 나옵니다. count++는 소스 코드에서 한 줄이지만 실제로는 세 단계입니다. 메모리에서 현재 값을 읽고, 1을 더하고, 결과를 다시 씁니다. 두 흐름이 값 7을 각각 읽어 갔다면 둘 다 8을 계산해 8을 씁니다. 증가는 두 번 일어났는데 결과는 8입니다. 한 번이 사라진 것입니다.
여기에 한 겹이 더 있습니다. 컴파일러와 CPU는 성능을 위해 읽기와 쓰기의 순서를 바꾸거나 값을 레지스터에 캐시해 둘 수 있습니다. 그래서 한 스레드가 쓴 값을 다른 스레드가 언제 보게 되는지도 보장되지 않습니다. 잠금과 원자 연산은 값이 깨지지 않게 할 뿐 아니라 “이 시점 이후에는 앞선 쓰기가 다른 흐름에 보인다”는 가시성까지 함께 보장합니다. 면접에서 원자성만 말하고 가시성을 빠뜨리는 답이 흔한데, 가시성까지 언급하면 확실히 구분됩니다.
코드로 보는 검사 후 행동
JAVA · 검사 후 행동 경쟁과 수정본
// 깨지는 코드: 재고 확인과 차감이 분리되어 있다
class Inventory {
private final Map<String, Integer> stock = new HashMap<>();
boolean reserve(String sku, int qty) {
Integer cur = stock.get(sku); // (1) 읽기
if (cur == null || cur < qty) { // (2) 검사
return false;
}
stock.put(sku, cur - qty); // (3) 쓰기
return true;
}
}
// 두 스레드가 재고 5에서 각각 4개를 예약하는 경우
// T1: (1) cur=5 T2: (1) cur=5
// T1: (2) 5 >= 4 T2: (2) 5 >= 4 둘 다 통과
// T1: (3) 1 저장 T2: (3) 1 저장 실제로는 8개가 나갔는데 재고는 1
// 수정 1: 불변식 전체를 하나의 잠금으로 묶는다
class LockedInventory {
private final Map<String, Integer> stock = new HashMap<>();
private final Object lock = new Object(); // stock 맵 전체를 보호한다
boolean reserve(String sku, int qty) {
synchronized (lock) { // 읽기·검사·쓰기가 하나의 단위
Integer cur = stock.get(sku);
if (cur == null || cur < qty) return false;
stock.put(sku, cur - qty);
return true;
}
}
}
// 수정 2: 값 하나면 잠금 없이 비교-교환 반복으로도 된다
class AtomicInventory {
private final ConcurrentHashMap<String, AtomicInteger> stock = new ConcurrentHashMap<>();
boolean reserve(String sku, int qty) {
AtomicInteger cell = stock.get(sku);
if (cell == null) return false;
while (true) {
int cur = cell.get();
if (cur < qty) return false;
// 읽은 값이 그대로일 때만 쓴다. 아니면 처음부터 다시.
if (cell.compareAndSet(cur, cur - qty)) return true;
}
}
}수정 1과 수정 2는 서로 대체재가 아닙니다. 보호해야 할 대상이 값 하나라면 수정 2가 잠금 경합 없이 잘 동작합니다. 그러나 재고를 줄이면서 같은 순간에 예약 목록에도 항목을 넣어야 하고 그 둘이 항상 일치해야 한다면, 원자 변수 두 개로는 불가능합니다. 각각은 원자적이어도 둘 사이 간격에서 불일치가 관찰되기 때문입니다. 이때는 두 변경을 함께 감싸는 잠금이나 트랜잭션이 필요합니다.
잠금을 말할 때는 반드시 무엇을 보호하는지를 붙이십시오. “잠금을 걸면 됩니다”가 아니라 “재고 맵과 예약 목록이 항상 합이 맞아야 하므로 이 둘을 하나의 잠금으로 묶겠습니다”라고 말하는 순간, 답의 성격이 암기에서 설계로 바뀝니다.
실전 문제를 끝까지 풀어 보기
질문: “쿠폰 발급 API가 있습니다. 선착순 100장이고, 한 사람당 한 장만 받을 수 있습니다. 운영에서 확인해 보니 102장이 발급됐고 일부 사용자는 두 장을 받았습니다. 원인이 무엇이고 어떻게 고치겠습니까?”
약한 답
“동시 요청 때문에 경쟁 상태가 생긴 것 같습니다. 발급 로직에 락을 걸면 됩니다. 자바라면 synchronized를 붙이고, 서버가 여러 대면 레디스 분산 락을 쓰면 해결됩니다. 그리고 ConcurrentHashMap 같은 동시성 자료구조를 쓰면 더 안전합니다.”
강한 답
“증상이 두 가지인데 원인이 서로 다를 수 있어 나눠 보겠습니다. 102장이 나간 것은 총량 불변식이 깨진 것이고, 한 사람이 두 장을 받은 것은 사용자별 유일성 불변식이 깨진 것입니다. 지켜야 할 불변식이 둘이므로 대책도 둘이어야 합니다.
두 증상 모두 형태는 검사 후 행동입니다. 발급 수를 세고 100 미만인지 확인한 뒤 발급하는데, 확인과 발급 사이에 다른 요청이 끼어들었습니다. 유일성 쪽도 같습니다. 이 사용자에게 발급된 쿠폰이 있는지 조회하고 없으면 넣는데, 두 요청이 동시에 ‘없음’을 봤습니다.
먼저 범위를 확인하겠습니다. 서버가 한 대인지 여러 대인지, 그리고 발급 기록이 어디에 저장되는지입니다. 이게 정해지지 않으면 프로세스 안의 잠금으로 충분한지 판단할 수 없습니다. 서버가 여러 대라면 프로세스 내 잠금은 아무것도 보장하지 못하고, 실제 직렬화 지점은 공유 저장소여야 합니다.
저장소가 관계형 데이터베이스라면 저는 잠금보다 제약 조건을 먼저 쓰겠습니다. (쿠폰종류, 사용자) 조합에 유일 인덱스를 걸면 두 장 발급은 두 번째 삽입이 실패하면서 구조적으로 막힙니다. 애플리케이션 코드가 어떻게 돌든 데이터가 깨지지 않는다는 점이 잠금보다 나은 부분입니다. 총량은 조건부 갱신으로 처리할 수 있습니다. 남은 수량을 갱신할 때 ‘남은 수량이 0보다 클 때만 1 감소’라는 조건을 갱신문 안에 넣고, 영향받은 행 수가 0이면 소진으로 처리합니다. 읽고 판단해서 쓰는 대신 판단을 쓰기 안으로 밀어 넣는 것이 이 형태의 표준 해법입니다.
단일 프로세스에 상태가 있는 경우라면 잠금을 쓰되, 잠금이 보호하는 대상을 발급 카운터와 발급자 집합 두 가지로 명시하고 두 변경을 한 임계 구역 안에 두겠습니다. 임계 구역 안에서는 외부 호출을 하지 않겠습니다. 쿠폰 발급 알림 같은 것을 잠금 안에서 호출하면 외부 지연이 그대로 잠금 유지 시간이 되어 처리량이 무너지고, 그 호출이 다른 잠금을 잡으면 데드락 위험이 생깁니다.
마지막으로 검증 방법을 말씀드리면, 동시 요청을 여러 개 던지는 테스트를 만들되 성공 응답 수와 저장소의 실제 발급 수가 일치하는지를 단언하겠습니다. 응답만 세면 통과하는 잘못된 테스트가 되기 쉽습니다.”
강한 답이 이긴 이유
두 답 모두 “경쟁 상태”라는 단어를 씁니다. 차이는 어휘가 아니라 추론의 순서입니다. 약한 답은 증상을 하나로 뭉친 뒤 곧바로 도구 이름으로 건너뜁니다. 락, 분산 락, 동시성 컬렉션이 나열되지만 무엇을 보호하는지가 없어서, 사실 이 답으로는 코드를 고칠 수 없습니다. 게다가 동시성 컬렉션은 개별 연산만 원자적이므로 검사 후 행동을 전혀 막아 주지 않는데, 그 점을 모르고 대책으로 제시했습니다.
강한 답은 순서가 반대입니다. 증상을 불변식 단위로 쪼개고, 각 불변식이 깨지는 지점을 코드 흐름 위에 짚고, 직렬화가 실제로 어디서 일어나야 하는지를 배치 구조에서 확인한 다음에야 도구를 고릅니다. 그래서 도구가 바뀌어도 답이 무너지지 않습니다. 저장소가 다른 것으로 바뀌면 “조건부 갱신에 해당하는 것이 무엇인가”로 옮겨 가면 그만입니다. 면접관이 “서버를 늘리면 어떻게 되나요”라고 되물었을 때 약한 답은 처음부터 다시 생각해야 하고, 강한 답은 이미 그 축을 검토해 두었습니다.
뮤텍스, 원자 연산, 불변 설계 중 무엇을 고를까
세 선택지는 난이도 순서가 아니라 적용 범위 순서로 이해하는 편이 낫습니다.
불변 설계가 가장 강력합니다. 공유되는 데이터를 아예 바꾸지 않으면 경쟁 상태가 정의상 생기지 않습니다. 변경이 필요하면 새 값을 만들어 교체하고, 교체 자체만 원자적으로 처리합니다. 여러 흐름이 읽기만 하는 설정값, 조회 캐시, 계산 결과 스냅숏에 잘 맞습니다. 비용은 복사입니다. 데이터가 크고 변경이 잦으면 부담이 됩니다.
원자 연산은 보호 대상이 값 하나일 때 적합합니다. 카운터, 플래그, 단일 참조 교체가 대표적입니다. 잠금을 잡지 않으므로 경합이 심해도 대기가 없고, 대신 비교-교환이 실패하면 재시도하므로 경합이 극심하면 재시도 비용이 붙습니다. 여러 원자 변수를 조합해 복합 불변식을 만들려는 시도는 거의 항상 실패한다는 점을 기억하십시오.
뮤텍스는 여러 변수가 함께 일관되어야 할 때 씁니다. 여기서 중요한 것은 잠금의 개수가 아니라 잠금과 데이터의 대응입니다. 어떤 잠금이 어떤 필드를 보호하는지 코드에 주석이나 이름으로 남기고, 그 필드는 반드시 그 잠금 아래에서만 접근한다는 규칙을 지켜야 합니다. 이 대응이 문서화되지 않은 코드베이스는 시간이 지나면 반드시 깨집니다. 누군가 잠금 없이 필드 하나를 읽는 코드를 추가하고, 그것이 몇 달 뒤 재현 안 되는 버그가 됩니다.
실무에서 가장 좋은 답은 종종 네 번째입니다. 공유 자체를 없애는 것입니다. 상태를 하나의 소유자에게 몰아 두고 다른 흐름은 메시지를 보내 요청하게 만들면, 상태 변경은 항상 한 흐름에서만 일어나므로 잠금이 필요 없습니다. 이 접근을 언어 차원에서 밀어붙이는 사례는 고루틴과 채널 가이드에서 볼 수 있습니다.
데드락: 네 조건과 실제로 쓰는 해법
데드락은 네 조건이 동시에 성립할 때만 생깁니다. 상호 배제는 자원을 한 번에 하나만 쓸 수 있다는 것, 점유 대기는 이미 무언가를 쥔 채로 다른 것을 기다린다는 것, 비선점은 남이 쥔 것을 강제로 뺏을 수 없다는 것, 순환 대기는 기다림의 관계가 원을 그린다는 것입니다.
면접에서 네 조건을 외워 말하는 것까지는 누구나 합니다. 점수가 갈리는 지점은 그다음입니다. “그럼 어느 조건을 깨겠습니까”라고 물었을 때, 실무에서 실제로 깰 수 있는 것은 사실상 순환 대기 하나뿐입니다. 상호 배제는 자원의 성질이라 없앨 수 없는 경우가 많고, 점유 대기를 없애려면 필요한 잠금을 전부 한꺼번에 잡아야 하는데 무엇이 필요한지 미리 알기 어렵습니다. 비선점을 깨는 것은 이미 진행한 작업을 되돌린다는 뜻이라 복잡도가 큽니다.
그래서 실전 해법은 잠금 순서 정하기입니다. 모든 잠금에 전역 순서를 부여하고, 어떤 코드든 반드시 그 순서대로만 획득하게 합니다. 계좌 이체에서 두 계좌의 잠금을 계좌 번호가 작은 쪽부터 잡는 것이 교과서적인 예입니다. 순서가 하나로 정해지면 순환이 생길 수 없습니다.
여기에 두 가지를 덧붙이면 답이 완성됩니다. 하나는 잠금 획득에 시간 제한을 두어 데드락이 생기더라도 영원히 멈춰 있지 않고 실패로 드러나게 하는 것입니다. 다른 하나는 잠금을 쥔 채 예측 불가능한 일을 하지 않는 것입니다. 잠금 안에서 네트워크 호출을 하거나, 콜백을 부르거나, 다른 컴포넌트의 메서드를 호출하면 그 안에서 무슨 잠금을 잡을지 알 수 없습니다. 잠금 범위를 짧고 예측 가능하게 유지하는 규칙 하나가 순서 규칙만큼이나 실질적인 효과를 냅니다.
데드락보다 진단이 어려운 것이 잠금 없이 돌아가다가 가끔 틀리는 코드입니다. 데드락은 멈추기라도 하므로 스레드 덤프를 뜨면 보입니다. 반면 보호되지 않은 공유 변수는 대부분의 실행에서 우연히 맞는 답을 내고, 부하가 높을 때만 조용히 틀린 값을 남깁니다. “테스트에서 안 나왔으니 괜찮다”는 이 영역에서 가장 위험한 문장입니다.
스레드, 프로세스, 코루틴은 무엇을 공유하느냐로 갈립니다
이 셋을 비교하라는 질문에 “스레드가 가볍고 프로세스가 무겁습니다”로 답하면 절반만 말한 것입니다. 무게는 결과이고, 원인은 무엇을 공유하는가입니다.
프로세스는 메모리 주소 공간을 공유하지 않습니다. 그래서 한 프로세스가 메모리를 망가뜨려도 다른 프로세스는 멀쩡하고, 애초에 공유 변수가 없으니 이 글에서 다룬 경쟁 상태도 생기지 않습니다. 대신 데이터를 주고받으려면 직렬화해서 파이프나 소켓이나 공유 메모리로 넘겨야 하고, 그 비용이 프로세스를 “무겁게” 만듭니다. 격리가 필요한 곳, 예를 들어 신뢰할 수 없는 코드를 실행하거나 한쪽의 크래시가 전체를 죽이면 안 되는 구조에서 값을 합니다.
스레드는 같은 주소 공간을 공유합니다. 전역 변수와 힙 객체를 그대로 같이 봅니다. 데이터를 넘기는 비용이 거의 없다는 것이 장점이고, 정확히 그 이유로 이 글의 모든 문제가 발생합니다. 스케줄링은 운영체제가 하며, 어느 지점에서든 실행이 중단될 수 있습니다. 그래서 “여기서는 안 끊길 것”이라는 가정을 세울 수 없습니다.
코루틴은 같은 주소 공간을 공유하되 스케줄링을 런타임이 협조적으로 합니다. 중단은 정해진 지점에서만 일어나고 그 지점이 코드에 표시됩니다. 이 성질이 실질적인 차이를 만듭니다. 중단점이 없는 구간은 끼어들기가 없으므로, 단일 스레드에서 도는 코루틴이라면 그 구간의 읽고-고쳐-쓰기는 안전합니다. 반대로 말하면 위험 구간을 특정하기가 쉽습니다. 중단점을 가로지르는 검사 후 행동만 의심하면 됩니다.
다만 코루틴이 여러 스레드에 분산되어 실행되는 런타임에서는 이 안전 가정이 사라집니다. 면접에서 코루틴의 안전성을 말할 때는 “단일 스레드 스케줄러 기준입니다”라는 전제를 반드시 붙이십시오. 전제 없이 안전하다고 단정하는 답은 되묻기 한 번에 무너집니다. 언어별 문법과 실행 모델 차이는 언어별 비동기 비교에서 나란히 볼 수 있습니다.
자주 나오는 실수
동시성 컬렉션을 쓰면 안전하다고 믿는 것입니다. 스레드 안전 맵은 get 하나, put 하나가 각각 원자적이라는 뜻이지, get 후 판단해서 put하는 흐름이 원자적이라는 뜻이 아닙니다. 복합 연산이 필요하면 그 컬렉션이 제공하는 원자적 복합 연산을 쓰거나, 바깥에서 직접 묶어야 합니다.
잠금을 걸면 성능이 나빠지니 최소한만 걸겠다며 범위를 쪼개는 것입니다. 잠금 범위를 줄이는 것 자체는 옳지만, 하나의 불변식을 지키는 코드를 두 개의 잠금 구간으로 나누면 그 사이에서 불변식이 깨진 상태가 관찰됩니다. 성능 최적화가 정확성을 무너뜨린 전형적인 경우입니다. 범위를 줄이려면 불변식 단위 아래로는 내려가지 않아야 합니다.
“읽기만 하니까 잠금이 필요 없다”고 생각하는 것입니다. 읽기끼리는 충돌하지 않지만, 누군가 쓰는 동안 읽으면 절반만 갱신된 상태를 볼 수 있습니다. 읽기가 압도적으로 많다면 읽기-쓰기 잠금이나 불변 스냅숏 교체가 적절한 답이지, 잠금 제거가 답은 아닙니다.
재현되지 않는다고 고쳐졌다고 판단하는 것입니다. 동시성 버그는 타이밍에 의존하므로 로그 한 줄을 추가하는 것만으로도 재현되지 않게 됩니다. 고쳤다고 말하려면 “이 코드 경로에서 이 불변식이 왜 항상 유지되는지”를 논증할 수 있어야 하고, 관찰로는 증명되지 않습니다.
단일 스레드 언어라서 안전하다고 말하는 것입니다. 중단점을 가로지르는 검사 후 행동은 단일 스레드에서도 그대로 깨집니다. 잠금은 필요 없지만 순서에 대한 사고는 여전히 필요합니다.
이어서 나오는 질문들
낙관적 잠금과 비관적 잠금 중 무엇을 쓰시겠습니까?
충돌 빈도로 답합니다. 충돌이 드물면 낙관적 방식이 대기 없이 처리량을 유지하고, 충돌이 잦으면 재시도가 누적되어 비관적 잠금이 유리합니다. “충돌률을 측정해 보고 정하겠지만, 기본값은 낙관적으로 두고 재시도 실패율을 지표로 보겠습니다”처럼 판단 기준과 관측 방법을 함께 말하면 좋습니다.
스레드 풀 크기는 어떻게 정하나요?
작업 성격으로 나눠 답합니다. CPU를 계속 쓰는 작업이면 코어 수 근처가 출발점이고 그 이상 늘리면 전환 비용만 늘어납니다. 대기가 긴 I/O 작업이면 코어 수보다 많이 두어야 하지만 상한은 상대 서비스와 연결 풀이 정합니다. 숫자를 말하기 전에 “대기 비율을 측정하겠다”가 먼저 나와야 합니다.
volatile 같은 키워드는 무엇을 보장하나요?
가시성과 재정렬 제한을 보장하지만 원자성은 보장하지 않는다는 점이 핵심입니다. 그래서 플래그를 켜고 끄는 용도에는 맞고, 증가 연산에는 맞지 않습니다. 이 구분을 정확히 말하면 메모리 모델을 이해하고 있다는 신호가 됩니다.
동시성 버그는 어떻게 테스트하나요?
확률에 기대는 반복 실행은 보조 수단일 뿐이라고 전제하고 시작합니다. 실행 지점을 테스트가 직접 제어할 수 있게 만들어 문제의 순서를 강제로 재현하거나, 정적 분석과 경쟁 탐지 도구로 접근 규칙 위반을 잡거나, 아예 공유를 줄여 검증 대상을 줄이는 쪽이 실질적입니다.
기아 상태와 데드락은 어떻게 다릅니까?
데드락은 아무도 진행하지 못하는 상태이고, 기아는 시스템 전체는 진행하는데 특정 흐름만 계속 밀리는 상태입니다. 원인도 다릅니다. 기아는 잠금 획득이 공정하지 않거나 우선순위가 낮은 작업이 계속 뒤로 밀릴 때 생기며, 공정 잠금이나 대기 시간 기반 우선순위 조정으로 완화합니다.
분산 환경에서 잠금은 어떻게 달라지나요?
프로세스 내 잠금과 달리 잠금 보유자가 죽으면 잠금이 남는다는 점, 만료 시간을 두면 작업이 끝나기 전에 잠금이 풀릴 수 있다는 점을 짚습니다. 그래서 분산 잠금은 정확성 보장이 아니라 중복 실행을 줄이는 최적화로 보고, 정확성은 저장소의 유일 제약이나 조건부 갱신으로 확보하는 편이 안전하다고 답합니다.
준비 점검표
- 경쟁 상태를 “동시 실행” 대신 “불변식이 여러 단계에 걸친다”로 정의해 말할 수 있는가
- 검사 후 행동과 읽고 고쳐 쓰기를 각각 예시로 들 수 있는가
- 증가 연산이 세 단계로 쪼개진다는 점과 가시성 문제를 함께 설명하는가
- 잠금을 말할 때 보호 대상을 항상 함께 지목하는가
- 원자 연산으로 충분한 경우와 부족한 경우의 경계를 말할 수 있는가
- 데드락 네 조건 중 실무에서 깨는 것이 순환 대기임을 근거와 함께 설명하는가
- 잠금 안에서 외부 호출을 하지 않는 이유를 두 가지 말할 수 있는가
- 스레드·프로세스·코루틴을 공유 범위와 스케줄링 주체로 구분하는가
- 단일 스레드에서도 남는 위험 구간을 지목할 수 있는가
- 고쳤다는 주장을 재현 실패가 아니라 불변식 논증으로 뒷받침하는가
연습 방법
가장 효과가 큰 연습은 자기 코드에서 공유 가변 상태를 찾아 목록으로 만드는 것입니다. 전역 변수, 싱글턴 필드, 캐시, 커넥션 풀, 정적 컬렉션이 후보입니다. 각각에 대해 누가 읽고 누가 쓰는지, 지켜야 할 불변식이 무엇인지, 지금 그것을 무엇이 보장하는지 한 줄씩 적어 보십시오. “아무것도 보장하지 않음”이라고 적히는 항목이 나오면 그것이 곧 면접에서 이야기할 수 있는 실전 사례가 됩니다.
그다음에는 위의 재고 코드를 직접 깨뜨려 보십시오. 스레드를 여럿 띄워 같은 물건을 예약하게 하고 최종 재고를 확인하면 됩니다. 수정본으로 바꿔 다시 돌려 보고, 이어서 잠금 범위를 일부러 두 조각으로 나눠 다시 깨지는 것을 확인하면 잠금 범위가 왜 불변식 단위여야 하는지가 몸에 남습니다. 이어서 볼 주제로는 메모리와 참조가 좋습니다. 별칭과 변경 가능성은 공유 상태 문제의 다른 얼굴이기 때문입니다. 실행 순서 쪽을 보강하려면 비동기 면접 가이드로, 전체 목록은 심층 가이드에서 이어 가시면 됩니다.