재사용은커녕 단 한 번 호출... 그런데도 합당했던 함수
얼마 전, POCU 아카데미 코드베이스에서 사소하지만 치명적인 버그가 하나 발생했습니다. 단순히 TempData에 숫자 하나를 저장했을 뿐인데, 프로덕션에서 500 Internal Server Error가 발생했습니다.
문제를 되짚어보면, 우리가 당연하게 믿었던 컴파일러가 전혀 도와주지 못했던 상황이었고, 결국 런타임에서만 드러나는 예외를 막지 못한 것이었습니다.
TempData란 무엇인가?
ASP.NET Core MVC의 TempData는 Controller와 View 사이에서 데이터를 잠시 보관하는 저장소입니다.
특징은 다음과 같습니다.
- 내부적으로 세션(Session)이나 쿠키(Cookie)를 사용합니다.
- 한 번 읽으면 사라지고, 다음 요청까지만 값이 유지됩니다.
- 주로 리디렉트 후에도 유지해야 하는 데이터(예: 사용자 이름, 알림 메시지 등)에 쓰입니다.
예를 들어, Controller에서:
public IActionResult Save()
{
TempData["Message"] = "저장이 완료되었습니다!";
return RedirectToAction("Complete");
}
그리고 View에서는:
<p>@TempData["Message"]</p>
이렇게 간단히 Controller → View로 값을 넘길 수 있습니다. 문제는, 단순해 보이는 TempData가 사실 직렬화/역직렬화 과정을 거친다는 점입니다.
사건의 발단
문제의 코드는 이랬습니다.
TempData["something"] = longValue;
수정 히스토리를 봤더니, 원래 코드는 이렇게 돼 있었습니다.
TempData["something"] = longValue.ToString();
최근에 리팩토링이 크게 있었고, 그 과정에서 개발자 한 명이 "불필요해 보이는 코드"라며 .ToString()을 제거한 것이었습니다.
이 판단은 충분히 이해할 만합니다. 당연히 불필요하게 string으로 변환하는 것보다는, 실제 데이터를 넘기는 게 더 좋은 방법처럼 보이니까요. "굳이 문자열로 바꿀 필요 없이 long을 그대로 넘기면 더 깔끔하지 않나?" 하는 생각을 하는 건 자연스러운 일이었습니다.
하지만… 여기서 문제가 발생했습니다. ASP.NET Core의 TempData는 내부적으로 문자열 기반 직렬화를 하기 때문에 실제 데이터를 그대로 넘기는 게 오히려 런타임 예외를 불러오는 길이었던 겁니다.
왜 문제가 되었을까?
앞서 설명했듯이 TempData는 내부적으로 문자열 기반 직렬화를 합니다.
string이나int같은 타입은 지원되지만,long은 기본 지원하지 않습니다.
따라서 long 그대로 넣으면 예외가 발생하고, 문자열로 변환해서 넣으면 정상적으로 동작합니다.
즉, 처음 코드를 작성했던 사람은 이미 이 사실을 알고 .ToString()을 넣었던 겁니다. 하지만 주석조차 남기지 않았기 때문에, 나중에 코드를 본 사람은 이유를 몰라서 그대로 삭제해버린 것이죠.
컴파일러는 왜 잡지 못할까?
더 답답한 점은 이런 실수를 컴파일러가 잡아주지 못한다는 겁니다.
TempData는 사실상 IDictionary<string, object?>처럼 동작합니다.
TempData["something"] = longValue; // 합법
TempData["something"] = longValue.ToString(); // 이것도 합법
object에 뭐든 넣을 수 있기 때문에 컴파일러는 전혀 경고하지 않습니다.
문제는 오직 런타임에서만 드러나죠.
그래서 함수를 만들었다
이 문제를 근본적으로 해결하기 위해 헬퍼 함수를 만들었습니다. 다행히도 저희 코드 베이스의 모든 API 컨트롤러는 ApiControllerBase를 상속받고 있습니다. 그래서 이곳에 헬퍼 함수를 추가했습니다.
[ApiController]
public abstract class ApiControllerBase : Controller
{
...
protected void SetTempData(string key, long value)
{
// .NET doesn't support serializer for long value. so we save it as string
TempData[key] = value.ToString(CultureInfo.InvariantCulture);
}
...
}
덕분에 앞으로는 팀원 누구도 실수로 long을 그대로 넣지 않을 가능성이 높습니다. 명확한 시그니처(컨트랙트)를 제공했기 때문이죠.
재사용은커녕 단 한 번 호출… 그런데도 합당했다
보통 우리는 "함수는 여러 번 쓰일 때만 뽑는다"는 규칙을 따릅니다. 그런데 이번 경우엔 상황이 달랐습니다.
- 지금 당장은 POCU 아카데미 코드베이스 전체에서 딱 한 군데에서만 호출됩니다.
- 제 규칙대로라면 함수로 만들지 않는 게 맞습니다.
하지만 이 코드는 실수하기 너무 쉬웠고, 컴파일러조차 잡아주지 못했습니다. 따라서 재사용을 위한 리팩토링이 아니라, "올바른 시그니처(컨트랙트)로 실수를 예방"하기 위해 함수를 만든 겁니다.
즉, 이 함수는 단순히 코드 중복을 줄이기 위한 것이 아니라, "앞으로 같은 사고가 반복되지 않도록 막는 안전장치"라는 점에서 합당했습니다. 그래서 저는 이 함수를 만들고도 당당하게, 그리고 조금은 자랑스럽게 글을 남깁니다.
교훈
이 사건에서 얻은 교훈은 단순합니다.
- 작은 버그라도 실서비스에서 큰 사고로 이어질 수 있다.
- 컴파일러가 잡아주지 못하는 실수는 추상화와 시그니처로 강제해야 한다.
- 함수는 재사용뿐 아니라, 실수를 방지하기 위한 컨트랙트로서의 가치도 있다.
마무리
버그는 늘 사소한 데서 시작합니다. .ToString() 하나가 빠졌을 뿐인데, 프로덕션이 멈췄습니다.
하지만 이번 경험 덕분에 재사용성만이 아니라 안정성을 위해 함수를 만든다는 중요한 교훈을 얻었습니다. 앞으로는 같은 문제로 밤잠 설치는 일은 없을 겁니다.
제대로 대우받는 개발자 | 부족한 컴공지식 배우기 | MIT급 컴공인강
최저임금으로 고통받는 일회성 프로그래머는 그만! POCU 아카데미가 올해 연봉협상을 책임지겠습니다!