테이블 기반 프로그래밍으로 복잡한 조건문 정복하기

개요

소프트웨어 개발에서 복잡한 조건문은 유지보수를 어렵게 만드는 주요 원인 중 하나입니다. 이 문제를 해결하기 위한 강력한 도구가 바로 테이블 기반 메서드(table-driven method)입니다. 이 방법은 로직 대신 데이터를 활용하여 코드의 복잡성을 획기적으로 줄일 수 있습니다.

테이블 기반 메서드의 핵심 원리

테이블 기반 메서드는 다음과 같은 기본 철학을 가집니다:

프로그램의 지식을 논리가 아닌 데이터에 저장한다 - 테이블에 저장하고 if 검사가 아닌 곳에 저장한다.

실제 개발에서 논리문으로 구현할 수 있는 기능은 대부분 테이블 조회로도 구현 가능합니다. 간단한 경우에는 논리문이 더 직관적일 수 있지만, 로직이 복잡해질수록 테이블 기반 접근의 장점이 명확해집니다.

예를 들어, 문자를 알파벳, 구두점, 숫자로 분류하는 코드를 생각해봅시다:

// 전통적인 조건문 방식
CharacterType classifyCharacter(char input) {
    if ((input >= 'a' && input <= 'z') || 
        (input >= 'A' && input <= 'Z')) {
        return CharacterType.LETTER;
    } else if (" ,.!?():;?-".indexOf(input) >= 0) {
        return CharacterType.PUNCTUATION;
    } else if (input >= '0' && input <= '9') {
        return CharacterType.DIGIT;
    }
    return CharacterType.UNKNOWN;
}

테이블 기반 방식을 사용하면:

// 테이블 기반 방식
CharacterType[] charCategory = new CharacterType[256];
// 테이블 초기화 (별도 초기화 루틴에서 수행)
initializeCharTable(charCategory);

CharacterType classifyCharacter(char input) {
    return charCategory[(int) input];
}

테이블 기반 메서드를 적용할 때 고려해야 할 두 가지 핵심 질문이 있습니다:

  1. 테이블에서 데이터를 어떻게 조회할 것인가?
  • 직접 접근(Direct access)
  • 인덱스 접근(Indexed access)
  • 계단식 접근(Stair-step access)
  1. 테이블에 무엇을 저장할 것인가?
  • 데이터 자체 저장
  • 동작(action) 저장 (람다 표현식이나 메서드 참조 활용)

사례 1: 월별 일수 계산

윤년을 고려하지 않은 간단한 월별 일수 계산을 먼저 살펴봅시다.

전통적인 조건문 방식:

// C# - 조건문 사용
int getDaysInMonth(int month) {
    int days;
    if (month == 1) days = 31;
    else if (month == 2) days = 28;
    else if (month == 3) days = 31;
    else if (month == 4) days = 30;
    else if (month == 5) days = 31;
    else if (month == 6) days = 30;
    else if (month == 7) days = 31;
    else if (month == 8) days = 31;
    else if (month == 9) days = 30;
    else if (month == 10) days = 31;
    else if (month == 11) days = 30;
    else if (month == 12) days = 31;
    return days;
}

테이블 기반 리팩토링:

// C# - 테이블 기반
int[] daysInMonth = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

int getDaysInMonth(int month) {
    return daysInMonth[month - 1];
}

윤년을 고려한 확장도 간단합니다:

// 윤년 고려 확장
int[,] daysInMonthTable = {
    {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, // 평년
    {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}  // 윤년
};

int getDaysInMonth(int month, bool isLeapYear) {
    int yearIndex = isLeapYear ? 1 : 0;
    return daysInMonthTable[yearIndex, month - 1];
}

사례 2: 보험료율 계산 시스템

나이, 성별, 결혼 상태, 흡연 여부에 따라 보험료율이 달라지는 복잡한 시스템을 고려해봅시다.

조건문 기반 구현의 일부:

// Java - 복잡한 중첩 조건문
public double calculatePremium(boolean isFemale, boolean isMarried, 
                              boolean isSmoker, int age) {
    double premium = 0;
    
    if (isFemale) {
        if (!isMarried) {
            if (!isSmoker) {
                if (age < 18) premium = 200.00;
                else if (age < 25) premium = 250.00;
                else if (age < 35) premium = 300.00;
                // ... 더 많은 조건
                else premium = 450.00;
            } else {
                if (age < 18) premium = 250.00;
                else if (age < 25) premium = 300.00;
                // ... 더 많은 조건
                else premium = 575.00;
            }
        }
        // 결혼한 여성의 경우...
    } else {
        // 남성의 경우...
    }
    return premium;
}

테이블 기반 설계:

// Java - 테이블 기반 설계
public enum SmokingStatus { NON_SMOKER, SMOKER }
public enum Gender { MALE, FEMALE }
public enum MaritalStatus { SINGLE, MARRIED }

public class PremiumCalculator {
    private static final int MAX_AGE = 100;
    private double[][][][] premiumTable = new double[2][2][2][MAX_AGE + 1];
    
    public PremiumCalculator() {
        initializeTable();
    }
    
    private void initializeTable() {
        // 보험료 테이블 초기화 로직
        // 실제로는 파일이나 데이터베이스에서 로드할 수 있음
    }
    
    public double getPremium(Gender gender, MaritalStatus marital, 
                           SmokingStatus smoking, int age) {
        int g = gender.ordinal();
        int m = marital.ordinal();
        int s = smoking.ordinal();
        return premiumTable[g][m][s][Math.min(age, MAX_AGE)];
    }
}

사례 3: 동적 메시지 포맷팅 시스템

해양 부표에서 수집된 데이터 메시지를 처리하는 시스템을 예로 들어보겠습니다. 각 메시지는 고유 ID를 가지며, 메시지 타입마다 다른 필드 구조를 가집니다.

전통적인 접근 방식:

# Python - 메시지 타입별 조건 분기
def processMessage(messageId, rawData):
    if messageId == 1:
        processTemperatureMessage(rawData)
    elif messageId == 2:
        processDriftMessage(rawData)
    # ... 20개 이상의 메시지 타입
    elif messageId == 20:
        processLocationMessage(rawData)
    else:
        raise UnknownMessageError(messageId)

테이블 기반 설계:

먼저, 메시지의 기본 데이터 타입을 정의합니다:

# Python - 데이터 타입 정의
from enum import Enum

class DataType(Enum):
    FLOAT = 1
    INTEGER = 2
    STRING = 3
    TIMESTAMP = 4
    BOOLEAN = 5
    BINARY = 6

메시지 구조를 테이블로 정의:

# 메시지 구조 정의
MESSAGE_DEFINITIONS = {
    1: {  # 온도 메시지
        'name': 'Buoy Temperature Report',
        'fields': [
            {'type': DataType.FLOAT, 'label': 'Average Temperature'},
            {'type': DataType.FLOAT, 'label': 'Temperature Range'},
            {'type': DataType.INTEGER, 'label': 'Sample Count'},
            {'type': DataType.STRING, 'label': 'Location'},
            {'type': DataType.TIMESTAMP, 'label': 'Measurement Time'}
        ]
    },
    2: {  # 부표 이동 메시지
        'name': 'Buoy Drift Data',
        'fields': [
            {'type': DataType.FLOAT, 'label': 'Drift Distance'},
            {'type': DataType.FLOAT, 'label': 'Direction'},
            {'type': DataType.TIMESTAMP, 'label': 'Start Time'},
            {'type': DataType.TIMESTAMP, 'label': 'End Time'}
        ]
    }
    # ... 다른 메시지 타입들
}

메시지 처리 로직:

# 데이터 타입별 처리기
class DataFieldProcessor:
    @staticmethod
    def processFloat(label, dataStream):
        value = dataStream.readFloat()
        print(f"{label}: {value}")
    
    @staticmethod
    def processInteger(label, dataStream):
        value = dataStream.readInt()
        print(f"{label}: {value}")
    
    @staticmethod
    def processString(label, dataStream):
        value = dataStream.readString()
        print(f"{label}: {value}")
    
    @staticmethod
    def processTimestamp(label, dataStream):
        value = dataStream.readTimestamp()
        print(f"{label}: {value}")
    
    # 다른 데이터 타입 처리기들...

# 메인 처리 로직
def handleMessage(messageId, dataStream):
    messageDef = MESSAGE_DEFINITIONS.get(messageId)
    if not messageDef:
        raise UnknownMessageError(messageId)
    
    print(f"\n{messageDef['name']}")
    
    for field in messageDef['fields']:
        processor = {
            DataType.FLOAT: DataFieldProcessor.processFloat,
            DataType.INTEGER: DataFieldProcessor.processInteger,
            DataType.STRING: DataFieldProcessor.processString,
            DataType.TIMESTAMP: DataFieldProcessor.processTimestamp,
            # 다른 타입들...
        }.get(field['type'])
        
        if processor:
            processor(field['label'], dataStream)

이 설계를 객체지향 패턴으로 발전시킬 수도 있습니다:

# 추상 기반 클래스
class FieldHandler:
    def process(self, label, stream):
        raise NotImplementedError

# 구체적인 핸들러들
class FloatFieldHandler(FieldHandler):
    def process(self, label, stream):
        value = stream.readFloat()
        print(f"{label}: {value:.2f}")

class StringFieldHandler(FieldHandler):
    def process(self, label, stream):
        value = stream.readString()
        print(f"{label}: {value}")

# 핸들러 팩토리
class HandlerFactory:
    def __init__(self):
        self._handlers = {
            DataType.FLOAT: FloatFieldHandler(),
            DataType.INTEGER: IntegerFieldHandler(),
            DataType.STRING: StringFieldHandler(),
            # ... 다른 핸들러들
        }
    
    def getHandler(self, dataType):
        return self._handlers.get(dataType)

# 메인 처리 루프
def processMessageWithOOP(messageId, dataStream):
    messageDef = MESSAGE_DEFINITIONS[messageId]
    factory = HandlerFactory()
    
    print(f"\n--- {messageDef['name']} ---")
    
    for field in messageDef['fields']:
        handler = factory.getHandler(field['type'])
        if handler:
            handler.process(field['label'], dataStream)

결론

테이블 기반 프로그래밍은 복잡한 조건문을 다루는 강력한 기법입니다. 핵심은 프로그램의 지식을 제어 흐름이 아닌 데이터 구조에 담는 것입니다. 이 접근 방식은 코드를 더 읽기 쉽고, 유지보수하기 좋으며, 확장성 있게 만들어줍니다.

개발자로서 성장하기 위해서는 "알지 못한다는 것을 모르는" 영역을 줄여나가는 과정이 중요합니다. 테이블 기반 메서드와 같은 강력한 도구들을 알고 있다는 것 자체가 더 나은 코드를 작성하는 첫걸음입니다.

태그: table-driven-method Refactoring clean-code software-design patterns

9월 12일 06:12에 게시됨