측정하지 않은 것은 안다고 할 수 없습니다 (회고)
제가 오래 설명해온 내용이 측정 한 번에 뒤집혔습니다. 코드는 맞았고 설명이 틀렸어요.
[그때 내린 판단]
비동기 구간에 MDC를 전파하는 TaskDecorator 를 만들었습니다. 코드는 이랬어요.
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
MDC.setContextMap(contextMap != null ? contextMap : Collections.emptyMap());
runnable.run();
} finally {
MDC.clear();
}
};
}
이 코드를 설명할 일이 몇 번 있었습니다. 저는 늘 이렇게 말했어요.
스레드 풀은 스레드를 재사용하니까 작업이 끝나면
MDC.clear()로 비워야 합니다. 안 그러면 다음 작업이 이전 요청의 traceId를 물려받아요.
논리적으로 말이 됩니다. ThreadLocal 은 스레드에 남고, 스레드는 재사용되고, 그러니 치우지 않으면 오염된다는 이야기니까요. 저 자신도 의심한 적이 없었습니다.
[지금 다시 보면]
블로그 글을 쓰면서 이 주장을 실측해보기로 했어요. 풀의 스레드를 traceId가 있는 작업으로 먼저 더럽힌 뒤, traceId 없는 작업 20건을 넣고 이전 값이 보이는지 셌습니다.
finally 의 clear() 를 뺀 버전을 돌렸어요. 제 설명대로면 20건 전부가 오염돼야 합니다.
0건이었습니다.
처음에는 테스트를 잘못 짰다고 생각했어요. 코드를 다시 읽고 나서야 이유를 알았습니다. clear() 를 빼도 작업 시작 시점에 setContextMap(빈 맵) 이 무조건 실행돼요. 이전 값이 그 순간 덮어써집니다. 뒤에서 치우든 말든 앞에서 이미 갈아엎고 있었던 거죠.
그럼 오염은 언제 생기는가. 조건을 바꿔가며 확인해봤습니다.
| 구현 | 남의 traceId를 물려받은 작업 |
|---|---|
if (contextMap != null) 로 감싸서 빈 값이면 설정 생략 | 20/20건 |
finally 의 clear() 제거 | 0/20건 |
| 실제 구현 | 0/20건 |
오염을 막는 건 뒤에서 비우는 동작이 아니라 앞에서 무조건 덮어쓰는 동작이었어요. 제가 중요하다고 믿었던 clear() 는 이 시나리오에서 아무 차이를 만들지 않았고, 반대로 습관적인 null 방어라고 여겼던 삼항 연산자가 진짜 장치였습니다.
[무엇이 달랐어야 했나]
결과적으로 코드는 옳았습니다. 틀린 건 코드가 아니라 제 설명이었어요. 이 구분이 중요하다고 생각합니다.
동작하는 코드를 짜는 것과 그 코드가 왜 동작하는지 아는 것은 다른 일이었어요. 저는 전자를 해내고 후자를 했다고 착각하고 있었습니다. 결과가 맞았기 때문에 검증할 이유를 못 느꼈고요.
돌이켜보면 위험 신호가 있었습니다. 제 설명은 한 번도 반증을 거치지 않았어요. clear() 가 오염을 막는다고 믿었지만, clear() 를 뺀 버전을 돌려본 적이 없었습니다. 그럴듯한 인과를 만들어놓고 그게 검증이라고 여긴 셈이에요.
만약 이걸 면접에서 설명했다면 어땠을까 싶습니다. 면접관이 “그럼 clear() 를 빼면 어떻게 되나요”라고 물었을 때 저는 자신 있게 틀린 답을 했을 거예요. 코드는 제대로 짰는데 이해는 안 하고 있었다는 게 드러났겠죠.
[남은 질문]
이번에 잡은 건 하나뿐입니다. 같은 종류의 착각이 몇 개나 더 있을지는 모르겠어요.
지금 제가 “이건 이래서 이렇습니다”라고 설명할 수 있는 코드들을 떠올려보면, 대부분 실측으로 확인한 게 아니라 그럴듯한 인과로 채워둔 것입니다. 예를 들면 이런 것들이에요.
- 배치 크기를 400으로 잡은 게 정말 의미 있는 차이를 만드는가
- 스레드 풀 core 4와 max 16이 지금 트래픽에 맞는 값인가
- Circuit Breaker 임계값 40%와 55%의 구분이 실제로 다른 동작을 만드는가
세 번째는 이번에 같이 측정했습니다. 나머지는 아직이에요.
당장 전부 검증할 생각은 없습니다. 그럴 시간도 없고, 모든 판단에 실험을 붙이는 게 합리적이지도 않아요. 다만 기준 하나는 생겼습니다.
남에게 설명할 때 “그래서 확인해봤나”를 스스로 물어보기로 했어요. 확인 안 했으면 “그렇게 알고 있습니다”라고 말하려고 합니다. “그렇습니다”와 “그렇게 알고 있습니다”는 다른 문장이니까요.
측정 한 번이 제 설명을 뒤집는 데 20분 걸렸습니다. 그 20분을 안 썼으면 저는 틀린 내용을 블로그에 발행하고, 면접에서도 같은 말을 했을 거예요.