개요
소프트웨어 개발에서 복잡한 조건문은 유지보수를 어렵게 만드는 주요 원인 중 하나입니다. 이 문제를 해결하기 위한 강력한 도구가 바로 테이블 기반 메서드(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];
}
테이블 기반 메서드를 적용할 때 고려해야 할 두 가지 핵심 질문이 있습니다:
- 테이블에서 데이터를 어떻게 조회할 것인가?
- 직접 접근(Direct access)
- 인덱스 접근(Indexed access)
- 계단식 접근(Stair-step access)
- 테이블에 무엇을 저장할 것인가?
- 데이터 자체 저장
- 동작(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)
결론
테이블 기반 프로그래밍은 복잡한 조건문을 다루는 강력한 기법입니다. 핵심은 프로그램의 지식을 제어 흐름이 아닌 데이터 구조에 담는 것입니다. 이 접근 방식은 코드를 더 읽기 쉽고, 유지보수하기 좋으며, 확장성 있게 만들어줍니다.
개발자로서 성장하기 위해서는 "알지 못한다는 것을 모르는" 영역을 줄여나가는 과정이 중요합니다. 테이블 기반 메서드와 같은 강력한 도구들을 알고 있다는 것 자체가 더 나은 코드를 작성하는 첫걸음입니다.