C# 조건부 리팩토링: Derek Comartin과 함께 조건부 혼잡 피하기
C#에서 if 문, if-else 문, switch 문과 같은 조건문은 필수적인 도구입니다. 하지만 이러한 구조가 과도하게 사용될 때, 특히 열거형과 함께 사용될 때는 어떤 일이 발생할까요? 데릭 코마틴은 자신의 영상 "[열거형은 악이 아니다. "Conditionals Everywhere Are" (https://www.youtube.com/watch?v=QoK7jSZ-viw) 영상에서는 널리 사용되는 조건부 로직을 더 깔끔하고 유지보수하기 쉬운 패턴으로 대체하는 자세한 리팩토링 과정을 보여줍니다.
이 글에서는 데릭이 제시한 타임스탬프를 기준으로 그의 추론 과정을 단계별로 살펴보겠습니다. 또한 그의 아이디어가 삼항 연산자, else 문, switch-case 구조와 같은 C#의 일반적인 조건문 패턴에 어떻게 적용되는지 살펴보고, 이러한 패턴이 대규모 코드베이스에서 어떤 문제를 일으킬 수 있는지, 그리고 더 나은 설계를 위해 어떻게 리팩토링할 수 있는지 강조할 것입니다.
조건문 폭발: 진짜 문제점
데릭은 먼저 제품 유형을 확인하는 if 문을 보여줍니다.
if (productType == ProductType.Template || productType == ProductType.Ebook)if (productType == ProductType.Template || productType == ProductType.Ebook)언뜻 보면 위의 조건문은 간단해 보입니다. 하지만 데릭은 이러한 방식의 구문은 주어진 조건을 평가한 다음 조건이 참일 경우에만 코드 블록을 실행하므로, 이러한 논리가 모든 곳에서 반복될 경우 문제가 발생할 수 있다고 경고합니다.
이 코드 블록은 다른 메서드나 클래스에서도 다시 나타날 수 있습니다.
if (offeringType == ProductType.Template || offeringType == ProductType.Ebook)if (offeringType == ProductType.Template || offeringType == ProductType.Ebook)데릭은 이러한 패턴이 대규모 시스템 전체에 빠르게 확산된다고 설명합니다. 동일한 if-else 논리가 여러 서비스에서 나타나므로 새 열거형 값을 추가할 때 불일치 및 버그가 발생합니다. 예를 들어, 비디오와 같은 새로운 제품 유형을 도입하면 어떻게 될까요? 이 조건식이 포함된 모든 블록을 업데이트해야 한다는 점을 잊지 마세요.
반복은 복잡성을 증폭시킨다
다음 예시에서 데릭은 중첩 조건문을 자세히 살펴봅니다. 한 메서드 내부에서 if-else 문이 동일한 열거형을 확인하고, 그 결과가 또 다른 메서드로 전달되는데, 이 메서드 역시 유사한 검사를 포함하고 있습니다.
해당 구문은 템플릿 또는 전자책인지 확인하고, 그렇지 않으면 null을 반환합니다. 데릭은 이러한 중복이 코드 길이를 늘릴 뿐만 아니라 유지보수상의 위험을 초래한다고 지적합니다. 동일한 로직이 여러 파일에 복사되어 제어 흐름에 혼란을 초래합니다.
시스템에서 열거형을 수정할 때마다 기본 케이스를 추가해야 한다면 뭔가 잘못된 것입니다.
조건문에 대한 우리의 생각을 바꾸다
데릭은 if-else 문을 사용하여 타입을 끊임없이 확인하는 대신, 더 나은 질문을 던질 것을 제안합니다.
해당 제품에 다운로드 가능한 기능이 있습니까?
이는 의도를 더 잘 표현한 것입니다. 이렇게 하면 코드 가독성이 향상되고 열거형에 대한 의존도가 완전히 줄어듭니다. 다음과 같이 두 가지 조건을 가진 if 문을 작성하는 대신:
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)해당 로직을 모델로 캡슐화하고 다음과 같이 작성하면 됩니다.
if (product.HasDownloadableResource())if (product.HasDownloadableResource())이 함수는 다운로드 가능한 리소스가 있는 경우에만 true를 반환하므로 복잡한 조건식을 사용할 필요성을 줄여줍니다.
if 문에서 캡슐화된 동작까지
핵심 문제를 해결하기 위해 데릭은 DownloadableResource 유형을 도입했습니다. 이 유형에는 다운로드 URL과 기본 파일 이름이 포함됩니다. if 문을 통해 도출하는 대신, 해당 기능이 도메인의 핵심적인 부분으로 자리 잡게 됩니다.
이제, 이 내용을 반복하는 대신:
if (product.Type == ProductType.Template)
{
// Generate file name
}
else if (product.Type == ProductType.Ebook)
{
// Generate file name
}if (product.Type == ProductType.Template)
{
// Generate file name
}
else if (product.Type == ProductType.Ebook)
{
// Generate file name
}당신은 이렇게 씁니다:
var downloadable = product.GetDownloadableResource();
if (downloadable != null)
{
Console.WriteLine(downloadable.FileName);
}var downloadable = product.GetDownloadableResource();
if (downloadable != null)
{
Console.WriteLine(downloadable.FileName);
}이렇게 하면 논리가 dramatically 단순화되고 else 문 분기나 switch 문이 필요 없어집니다.
컴파일 시간보다 런타임이 우선: 전략적 변화
데릭은 여기서 한 걸음 더 나아가 중요한 설계 선택, 즉 컴파일 타임 로직을 런타임으로 옮긴 것을 설명합니다. 이는 런타임에 시스템에 쿼리를 보내 제품에 대한 DownloadableResource가 존재하는지 확인하는 것을 의미합니다. 그렇다면 즉시 조치를 취하십시오. 그렇지 않다면 건너뛰세요.
그는 이러한 변화가 정적인 if-else 논리를 런타임 쿼리로 전환시킨다고 지적합니다. 데이터베이스 호출이 추가될 수 있지만, 중첩된 if-else 논리를 줄이고 동작을 중앙 집중화합니다. 이는 대규모 환경에서의 유지보수성을 향상시킵니다.
다운로드 가능한 제품에 상속 사용하기
데릭이 고려하는 또 다른 경로는 상속입니다. 추상 기본 클래스인 Product를 만든 다음 Ebook, Template 또는 OfflineCourse와 같은 파생 유형을 정의할 수 있습니다.
각각은 다음과 같은 메서드를 재정의합니다.
public virtual string GetDownloadUrl() { ... }public virtual string GetDownloadUrl() { ... }이러한 접근 방식을 통해 각 제품은 자체 로직을 처리할 수 있습니다. 이 방법은 switch 문이나 여러 개의 조건문을 피할 수 있지만, 데릭은 주의하지 않으면 여전히 내부적으로 조건식을 작성하게 될 수 있다고 지적합니다.
상속 없이 더 나은 캡슐화
상속이 부담스럽게 느껴진다면, 데릭은 계층 구조에 얽매이지 않고 자체 속성과 메서드를 포함하는 DownloadableProduct와 같은 명시적 형식을 사용하는 것을 제안합니다.
프로그램에서는 다음과 같이 나타날 수 있습니다.
var downloader = new DownloadableProduct(product);
Console.WriteLine(downloader.GetDefaultFileName());var downloader = new DownloadableProduct(product);
Console.WriteLine(downloader.GetDefaultFileName());동작을 결정하기 위해 if-else 문이나 switch 문을 사용할 필요가 없습니다. 각 객체는 무엇을 해야 할지 알고 있기 때문입니다.
간단한 수정: 열거형에 대한 확장 메서드
열거형을 포기할 준비가 되지 않았다면, 데릭은 간편한 해결책으로 확장 메서드를 만드는 방법을 제안합니다.
public static bool IsDownloadable(this ProductType type)
{
return type == ProductType.Template || type == ProductType.Ebook;
}public static bool IsDownloadable(this ProductType type)
{
return type == ProductType.Template || type == ProductType.Ebook;
}이제 다음과 같이 쓰는 대신에:
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)이를 간단히 표현하면 다음과 같습니다.
if (product.Type.IsDownloadable())if (product.Type.IsDownloadable())이렇게 하면 논리가 중앙 집중화되고 중괄호와 코드 블록을 반복해서 사용하는 것을 방지할 수 있습니다.
삼항 연산자의 과다 사용을 피하고 스위치를 사용하십시오.
데릭은 또한 삼항 연산자와 같은 약어를 과도하게 사용하는 것에 대해 경고합니다.
string filename = product.Type == ProductType.Template ? "template.pdf" : "default.pdf";string filename = product.Type == ProductType.Template ? "template.pdf" : "default.pdf";문법적으로는 유효하지만, 논리가 복잡해지면 오류가 발생하기 쉽고 가독성이 떨어질 수 있습니다. 특히 조건이 거짓으로 평가될 경우, 미묘한 방식으로 잘못된 값이 할당될 수 있습니다.
마찬가지로, break 문과 default case를 사용하는 switch 문도 이러한 함정에 빠집니다. switch-case 논리를 사용하는 것보다 객체에게 동작을 직접 묻는 것이 더 좋습니다.
결론: 조건부 복잡성을 줄이고 더욱 스마트한 제어
결론적으로, 데릭의 영상은 열거형 자체에 대한 공격이 아니라, 열거형을 둘러싼 조건문(if) 사용 방식에 대한 비판입니다. if-else 문과 switch 문을 코드베이스 전체에 퍼뜨리면 시스템을 테스트하고 유지 관리하고 발전시키기가 더 어려워집니다.
캡슐화, 런타임 조회, 상속 또는 간단한 확장 메서드 중 어떤 방식을 선택하든 목표는 동일합니다. 조건문을 줄이고 로직을 적절한 위치로 옮기는 것입니다.
기억하다:
조건문은 나쁜 게 아닙니다.
- 조건부 잡동사니는 다음과 같습니다.
깔끔한 코드는 클래스 곳곳에 흩어져 있는 여러 개의 if-else 문에 의존하지 않습니다.
- 상황을 평가하고 그에 따라 리팩토링하세요.
데릭의 말처럼 "상황에 따라 다릅니다." 하지만 한 가지는 분명합니다. 제품은 단순히 제품일 뿐만 아니라, 때로는 디자인을 재고해야 할 신호가 될 수 있습니다.

