34. 스레드풀 워커의 ThreadLocal 컨텍스트 잔존과 요청 간 오염 전파
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// [시나리오] 요청 처리 스레드풀의 "현재 처리 중인 플레이어" 컨텍스트 (서버 내부)
// 요청 핸들러 곳곳(로깅, 권한 검사, 감사 기록)에서 "지금 이 스레드가 어느 플레이어의
// 요청을 처리하고 있는가"를 알아야 한다. 매 호출마다 playerId 를 파라미터로 넘기는
// 대신, 스레드로컬(ThreadLocal<T>)에 "현재 처리 중인 컨텍스트"를 저장해두고 핸들러
// 내부 아무 곳에서나 꺼내 쓰는 방식을 택했다. 스레드풀 워커는 요청을 하나 처리한 뒤
// 즉시 다음 요청을 처리하도록 재사용된다.
//
// [요구사항]
// - 한 요청을 처리하는 동안 조회되는 "현재 컨텍스트"는 반드시 그 요청의 플레이어여야
// 한다.
// - 한 요청 처리가 끝나면, 다음에 그 워커 스레드가 다른 요청을 처리할 때 이전 요청의
// 컨텍스트가 남아 있어서는 안 된다.
//
// [과제] RequestProcessor.HandleRequest 와 CurrentContext 사용부를 검토하고, 스레드풀
// 워커가 재사용될 때 위 요구사항이 어떻게 어긋날 수 있는지 원인과 수정안을
// 제시하라. 표시된 (A)(B) 지점에 주목하라.
using System;
using System.Threading;
using System.Threading.Tasks;
class PlayerContext
{
public int PlayerId;
public string PermissionLevel;
}
static class CurrentContext
{
// 워커 스레드마다 하나씩 유지되는 "현재 처리 중인 컨텍스트"
public static readonly ThreadLocal<PlayerContext> Value = new ThreadLocal<PlayerContext>();
}
class RequestProcessor
{
private void AuthorizeAction(string action)
{
var ctx = CurrentContext.Value.Value;
if (ctx == null) throw new InvalidOperationException("컨텍스트 없음");
// ctx.PermissionLevel 로 권한 검사...
}
private void LogAudit(string message)
{
var ctx = CurrentContext.Value.Value;
// ctx.PlayerId 를 로그에 기록...
}
private void DoWork(int playerId, string action)
{
AuthorizeAction(action);
// ... 실제 처리 ...
LogAudit($"{action} 처리됨");
}
// 스레드풀 워커에서 요청마다 호출된다 (같은 워커 스레드가 계속 재사용됨).
public void HandleRequest(int playerId, string permissionLevel, string action)
{
// (A)
CurrentContext.Value.Value = new PlayerContext
{
PlayerId = playerId,
PermissionLevel = permissionLevel
};
try
{
DoWork(playerId, action);
}
catch (Exception)
{
// 예외 발생 시 로깅 후 요청 실패 응답
throw;
}
// (B)
}
} 결함 코드 · C++
// [시나리오] 요청 처리 스레드풀의 "현재 처리 중인 플레이어" 컨텍스트 (서버 내부)
// 요청 핸들러 곳곳(로깅, 권한 검사, 감사 기록)에서 "지금 이 스레드가 어느 플레이어의
// 요청을 처리하고 있는가"를 알아야 한다. 매 호출마다 playerId 를 파라미터로 넘기는
// 대신, thread_local 포인터에 "현재 처리 중인 컨텍스트"를 저장해두고 핸들러 내부
// 아무 곳에서나 꺼내 쓰는 방식을 택했다. 스레드풀 워커는 요청을 하나 처리한 뒤
// 즉시 다음 요청을 처리하도록 재사용된다.
//
// [요구사항]
// - 한 요청을 처리하는 동안 조회되는 "현재 컨텍스트"는 반드시 그 요청의 플레이어여야
// 한다.
// - 한 요청 처리가 끝나면, 다음에 그 워커 스레드가 다른 요청을 처리할 때 이전 요청의
// 컨텍스트가 남아 있어서는 안 된다.
//
// [과제] RequestProcessor::handleRequest 와 CurrentContext 사용부를 검토하고, 스레드풀
// 워커가 재사용될 때 위 요구사항이 어떻게 어긋날 수 있는지 원인과 수정안을
// 제시하라. 표시된 (A)(B) 지점에 주목하라.
#include <string>
#include <memory>
#include <stdexcept>
struct PlayerContext {
int playerId;
std::string permissionLevel;
};
// 워커 스레드마다 하나씩 유지되는 "현재 처리 중인 컨텍스트" (raw 포인터)
struct CurrentContext {
static thread_local PlayerContext* value;
};
thread_local PlayerContext* CurrentContext::value = nullptr;
class RequestProcessor {
void authorizeAction(const std::string& action) {
PlayerContext* ctx = CurrentContext::value;
if (!ctx) throw std::runtime_error("컨텍스트 없음");
// ctx->permissionLevel 로 권한 검사...
(void)action;
}
void logAudit(const std::string& message) {
PlayerContext* ctx = CurrentContext::value;
// ctx->playerId 를 로그에 기록...
(void)message;
(void)ctx;
}
void doWork(int playerId, const std::string& action) {
authorizeAction(action);
// ... 실제 처리 ...
(void)playerId;
logAudit(action + " 처리됨");
}
public:
// 스레드풀 워커에서 요청마다 호출된다 (같은 워커 스레드가 계속 재사용됨).
void handleRequest(int playerId, const std::string& permissionLevel, const std::string& action) {
// (A)
PlayerContext* ctx = new PlayerContext{playerId, permissionLevel};
CurrentContext::value = ctx;
try {
doWork(playerId, action);
} catch (...) {
// 예외 발생 시 상위로 재던짐
throw;
}
// (B)
}
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.