18장: 테이블 활용 기법
18장: 테이블 활용 기법
- 테이블 활용 기법(table-driven method):
if/case논리 대신 테이블에서 정보를 찾아 선택하는 방식 - 단순한 선택은 조건문이 더 직접적이지만, 논리 연쇄가 복잡해질수록 테이블이 단순하고 수정하기 쉬움
18.1 테이블 활용 기법의 일반 고려 사항
- 프로그램 지식을 논리문이 아니라 데이터에 둠
- 문자 분류를 긴 조건문 대신
charTypeTable[inputChar]로 처리하는 식
- 문자 분류를 긴 조건문 대신
// 여러 조건문 대신 문자 코드로 바로 분류를 조회
charType = charTypeTable[inputChar];
- 먼저 테이블 항목에 접근할 방법을 정함
- 직접 접근, 인덱스 접근, 계단식 접근
- 다음으로 테이블에 넣을 내용을 정함
- 결과 데이터 자체를 저장
- 수행할 동작을 나타내는 코드나, 가능하다면 루틴 참조를 저장
테이블 설계에서 먼저 확인할 것
- 테이블은 단순히
if를 짧게 만드는 기법이 아니라, 변하기 쉬운 업무 규칙을 코드에서 데이터로 옮기는 설계다. - 조회 결과가 데이터인지 동작인지 구분한다. 동작을 저장한다면 명령 코드 뒤의
switch가 다시 비대해지지 않는지, 함수 참조·전략 객체 같은 디스패치가 가능한지 함께 검토한다. - 키의 유효 범위와 “없는 키”의 정책을 명시한다. 배열 범위를 벗어난 키, 인덱스의 빈 항목, 정의되지 않은 조합을 정상값처럼 처리하면 테이블은 짧아도 위험하다.
- 외부 파일에서 읽는 테이블은 배포 없이 규칙을 바꿀 수 있지만, 스키마·값 범위·누락 항목 검증과 버전 호환이 필요하다.
// 명령별 동작을 테이블에 둔다. switch를 늘리지 않고 새 명령을 등록할 수 있다.
Map<String, Consumer<Order>> handlers = Map.of(
"PAY", this::pay,
"CANCEL", this::cancel
);
Consumer<Order> handler = handlers.get(command.type());
if (handler == null) throw new IllegalArgumentException("Unknown command");
handler.accept(order);
18.2 직접 접근 테이블
- 키를 별도 계산 없이 배열 인덱스로 바로 쓸 수 있는 테이블
- 월 번호로 월별 일수를 찾거나, 나이·성별·혼인·흡연 여부의 조합으로 보험료를 찾는 경우에 적합
- 여러 조건문을 다차원 테이블 한 번의 조회로 바꾸면 코드가 짧고 변경도 쉬움
다차원 테이블의 크기는 각 차원의 가능한 값 수를 곱한 만큼 늘어난다. 조합이 촘촘하고 키가 작은 열거형일 때는 좋은 선택이지만, 대부분의 조합이 비어 있거나 키 공간이 크면 인덱스 접근·맵·규칙 분리가 더 적합할 수 있다.
Dim daysPerMonth() As Integer = _
{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
days = daysPerMonth(month - 1)
- 테이블 데이터는 코드에 넣어도 되지만 파일에서 읽으면 요율·형식 변경을 코드 수정 없이 반영 가능
유연한 메시지 형식 예제
- 메시지 ID마다 별도 출력 루틴이나 하위 클래스를 만드는 대신, 메시지의 필드 형식을 테이블로 기술
- 공통 해석기가 테이블에서 필드 타입과 이름을 읽어 같은 방식으로 처리
- 새 메시지 타입은 테이블 항목만 추가하면 됨
- 새 필드 타입일 때만 처리 코드 추가가 필요
- 객체지향·다형성 자체가 복잡도를 낮추지는 않음 — 핵심 설계 통찰은 잘 구성된 조회 테이블
즉, 메시지 종류의 변화와 필드 처리 방식의 변화를 분리한다. 형식 정의는 데이터로, 정해진 수의 필드 타입을 읽고 출력하는 해석기는 코드로 둔다. 메시지 형식이 자주 바뀌는 환경에서 특히 강한 경계다.
먼저 해석기를 case(또는 switch)로 짜면 20개의 메시지 루틴은 하나로 줄지만, 필드 타입마다 분기하는 case가 남는다.
// case 버전: 메시지 루틴은 하나지만 필드 타입 분기가 case에 남는다.
void printField(FieldType type, String label, InputStream input) throws IOException {
switch (type) {
case INTEGER -> { printLabel(label); printInt(readInt(input)); }
case TEXT -> { printLabel(label); printText(readText(input)); }
case TIME -> { printLabel(label); printTime(readTime(input)); }
}
}
이 case를 필드 타입 → 처리 객체를 담은 조회 테이블로 대체하면 분기 자체가 사라진다. 필드 타입(enum)을 배열 인덱스로 써서 알맞은 객체의 메서드를 바로 호출한다.
enum FieldType { INTEGER, TEXT, TIME }
interface FieldReader {
void readAndPrint(InputStream input, String label) throws IOException;
}
// 필드 타입(enum)을 배열 인덱스로 직접 써서, 처리 객체를 바로 찾는다.
FieldReader[] readers = {
new IntegerFieldReader(), // INTEGER.ordinal()
new TextFieldReader(), // TEXT.ordinal()
new TimeFieldReader() // TIME.ordinal()
};
void printField(FieldType type, String label, InputStream input) throws IOException {
readers[type.ordinal()].readAndPrint(input, label);
}
예를 들어 온도 메시지의 형식이 INTEGER, TEXT, TIME에서 INTEGER, INTEGER, TEXT, TIME으로 바뀌어도, 메시지 형식 테이블만 바꾸면 된다. IntegerFieldReader·TextFieldReader처럼 새로운 필드 처리 방식이 생길 때만 클래스를 추가한다.
조회 키 보정하기
- 직접 키가 어색하면 정보를 중복해 키 공간을 채우거나, 키를 변환해 직접 접근으로 만듦
- 예: 나이를
min/max로 범위에 고정
- 예: 나이를
- 중복은 공간과 테이블 오류 가능성을 늘림
- 키 변환은
KeyFromAge()같은 별도 루틴에 격리- 변환 규칙이 한곳에 모이고, 규칙 변경과 의도 파악이 쉬움
int rateKey = Math.max(Math.min(66, age), 17);
rate = rateTable[smokingStatus][gender][maritalStatus][rateKey];
- 환경이 제공하는
HashMap같은 키 변환 도구도 활용
int keyFromAge(int age) {
if (age < 17) return 17;
if (age > 66) return 66;
return age;
}
double rateFor(Customer customer) {
return rateTable[customer.smokingStatus()][customer.gender()]
[customer.maritalStatus()][keyFromAge(customer.age())];
}
18.3 인덱스 접근 테이블
- 원본 키로 인덱스 테이블을 먼저 조회하고, 얻은 인덱스로 실제 데이터 테이블을 조회
- 가능한 키 공간은 넓지만 실제 항목 수가 적을 때 적합
- 4자리 부품 번호 10,000개 중 실제 부품 100개처럼, 큰 본문 테이블의 빈 공간을 피함
- 본문 데이터가 클수록 공간 절약 효과가 큼
- 이름·입사일·급여처럼 같은 데이터를 여러 기준으로 정렬·검색하는 여러 인덱스를 만들 수도 있음
- 인덱스 접근 코드는 별도 루틴에 넣어, 테이블 구조 교체가 프로그램 전체에 퍼지지 않게 함
예를 들어 가능한 키가 10,000개, 실제 항목이 100개이고 본문이 항목당 100바이트라면 본문을 직접 채우는 방식은 약 1MB가 든다. 2바이트 인덱스 10,000개와 실제 본문 100개를 분리하면 약 30KB다. 인덱스는 공간 절약뿐 아니라 본문을 복사하지 않고 여러 정렬 순서를 제공하는 수단이기도 하다.
// 10,000개 부품 번호 중 실제로 등록된 번호만 실제 데이터의 위치를 가리킨다.
// partNumberToIndex[1023] = 0, partNumberToIndex[9007] = 1
// parts[0] = Part(1023, "Bolt"), parts[1] = Part(9007, "Nut")
// 나머지 partNumberToIndex 값은 NOT_FOUND
Part findPart(int partNumber) {
int dataIndex = partNumberToIndex[partNumber];
if (dataIndex == NOT_FOUND) {
throw new NoSuchElementException("Unknown part: " + partNumber);
}
return parts[dataIndex];
}
18.4 계단식 접근 테이블
- 개별 값이 아니라 값의 범위에 테이블 항목을 대응하는 방식
- 성적 구간, 확률 분포처럼 불규칙한 경계값에 적합
- 각 구간의 상한을 테이블에 두고, 값이 어느 계단에 닿는지 순차 검색
Dim rangeLimit() As Double = { 50.0, 65.0, 75.0, 90.0, 100.0 }
Dim grade() As String = { "F", "D", "C", "B", "A" }
gradeLevel = 0
studentGrade = "A"
While (studentGrade = "A") And (gradeLevel < grade.Length - 1)
If studentScore < rangeLimit(gradeLevel) Then
studentGrade = grade(gradeLevel)
End If
gradeLevel = gradeLevel + 1
End While
계단식 접근 시 주의점
- 각 구간의 끝점을 빠뜨리지 말 것 — 최상위 구간에는 인위적인 상한이 필요할 수 있음
<와<=를 혼동하지 말고 경계값을 테스트할 것- 테이블이 크면 순차 검색 대신 범주 위치를 찾는 이진 탐색을 고려
- 속도가 더 중요하면 공간을 더 쓰는 인덱스 접근과 비교
- 계단식 조회와 키 변환도 별도 루틴에 넣음
범위 테이블은 경계값 테스트가 본체다. 각 경계 바로 아래·정확히 경계·바로 위, 최소·최대·범위 밖 값을 테스트 케이스로 고정해 </<= 변경이 등급이나 요율을 바꾸지 않는지 확인한다.
// 상한은 오름차순이며, score == 90은 A가 되도록 <=를 선택했다.
String gradeFor(double score) {
double[] upperBounds = {50, 65, 75, 90, 100};
String[] grades = {"F", "D", "C", "B", "A"};
for (int i = 0; i < upperBounds.length; i++) {
if (score <= upperBounds[i]) return grades[i];
}
throw new IllegalArgumentException("score out of range: " + score);
}
// 경계 테스트: 89.999 -> B, 90.0 -> A, 100.0 -> A
18.5 테이블 조회의 다른 예
- 보험 요율 조회, 복잡한 조건을 대체하는 의사결정 테이블, 복잡한 식의 사전 계산값 등에 적용 가능
참고 자료
- 16.3절: 루프를 안에서 밖으로 작성하며 보험 요율 테이블을 조회하는 예
- 19.1절: 복잡한 조건문을 의사결정 테이블로 대체하는 방법
- 26.1절·26.4절: 복잡한 식의 조회 테이블과 사전 계산값 활용
체크리스트: 테이블 활용 기법
- 복잡한 논리의 대안으로 테이블 활용 기법을 검토했는가?
- 복잡한 상속 구조의 대안으로도 검토했는가?
- 코드 변경 없이 데이터를 바꾸도록 테이블을 외부에서 읽는 방법을 검토했는가?
- 직접 인덱싱이 어렵다면 키 계산을 중복하지 않고 별도 루틴에 넣었는가?
요점 정리
- 테이블은 복잡한 논리와 상속 구조를 대체할 수 있다. 코드의 논리나 상속 트리가 복잡하다면 조회 테이블로 단순화할 수 있는지 검토한다.
- 테이블 접근 방법은 직접 접근, 인덱스 접근, 계단식 접근 중 데이터 특성에 맞게 선택한다.
- 테이블에는 무엇을 저장할지와 어떻게 접근할지를 함께 설계해야 한다.