[SE100 #004] Parnas 의 정보 은닉 — 모듈을 나누는 기준
소프트웨어 공학 100 주제 시리즈의 4번째 글이다. (카테고리: 기초와 역사)
한 줄 요약
David Parnas 는 1972년 논문에서 모듈을 처리 단계(순서도) 로 나누지 말고, 어렵거나 바뀔 가능성이 높은 설계 결정 목록에서 출발해 각 모듈이 그 결정 하나를 나머지 시스템으로부터 숨기도록 나누라고 제안했다. 이것이 정보 은닉(information hiding)이다.
왜 필요한가
“모듈화하자” 는 말에는 모두 동의하지만, 무엇을 기준으로 나누느냐는 잘 묻지 않는다. 기준 없이 나누면 흔히 이런 일이 생긴다.
- 서비스를 “주문 접수 → 결제 → 배송” 처리 순서대로 쪼갰는데, 주문 데이터 형식이 바뀔 때마다 세 서비스를 동시에 배포해야 한다.
- 레이어를 나눴는데, 데이터베이스 컬럼 하나를 추가하면 컨트롤러·서비스·리포지터리·DTO 를 모두 고쳐야 한다.
모듈이 여러 개인데 변경은 항상 여러 모듈에 걸친다면, 모듈화의 이득(독립 개발, 독립 변경, 부분 이해)을 하나도 얻지 못한 것이다. Parnas 의 논문은 정확히 이 문제를 50년 전에 예제로 보여 줬다.
핵심 개념
원문
D. L. Parnas, On the Criteria To Be Used in Decomposing Systems into Modules, Communications of the ACM 15(12), 1053–1058, 1972년 12월. 당시 소속은 카네기멜런 대학이다. 논문은 모듈식 프로그래밍에 기대하는 이득을 세 가지로 정리하고 시작한다.
| 기대 이득 | 원문 요지 |
|---|---|
| 관리 측면 | 각 모듈을 별도 그룹이 거의 소통 없이 개발할 수 있어 개발 기간이 줄어든다 |
| 제품 유연성 | 한 모듈을 크게 바꿔도 다른 모듈을 바꿀 필요가 없어야 한다 |
| 이해 용이성 | 시스템을 한 번에 한 모듈씩 공부할 수 있어야 한다 |
그리고 중요한 정의를 하나 둔다. 이 논문에서 모듈은 서브프로그램이 아니라 “책임의 할당(responsibility assignment)” 이다.
KWIC 예제: 두 가지 분해
예제는 KWIC(Key Word In Context) 색인 시스템이다. 줄들을 입력받아 각 줄의 모든 순환 이동(circular shift)을 만들고, 알파벳 순으로 정렬해 출력한다.
첫 번째 분해(관례적) — 처리 단계마다 모듈 하나.
[1 입력] → [2 순환 이동] → [3 알파벳 정렬] → [4 출력]
↑ 모두가 같은 메모리 내 자료 형식(문자 4개씩 압축, 인덱스 배열)을 공유
[5 주 제어]
두 번째 분해(정보 은닉) — 각 모듈이 설계 결정 하나를 숨긴다.
[줄 저장소] 줄·단어·문자를 꺼내는 함수만 공개. 저장 방식은 숨김
[입력] 입력 형식을 숨김. 줄 저장소에 넣는다
[순환 이동기] 순환 이동을 "미리 만들지, 요청 때 계산할지" 를 숨김
[정렬기] 정렬을 "한 번에 할지, 필요할 때 할지" 를 숨김. ITH(i) 로 i번째만 알려 줌
[출력] 출력 형식을 숨김
[주 제어]
두 분해는 실행 가능한 형태로는 똑같은 프로그램일 수 있다. 차이는 변경할 때 드러난다.
바뀔 가능성이 있는 결정들
Parnas 는 KWIC 에서 “의심스럽고 여러 상황에서 바뀔 가능성이 큰” 설계 결정을 나열한다.
- 입력 형식
- 모든 줄을 메모리(core)에 두는 결정
- 문자를 한 워드에 4개씩 압축하는 결정
- 순환 이동을 실제로 저장하지 않고 인덱스로 두는 결정
- 목록을 한 번에 정렬하는 결정(대신 필요할 때 검색하거나 부분 정렬할 수도 있다)
그리고 비교한다. 1번 변경은 두 분해 모두 한 모듈에 갇힌다. 그러나 2번과 3번 변경은 첫 번째 분해에서는 모든 모듈을 바꿔야 한다. 메모리 내 줄 저장 형식이 모든 모듈의 인터페이스였기 때문이다. 두 번째 분해에서는 줄 저장소 모듈 하나만 바뀐다.
기준 그 자체
논문의 “The Criteria” 절과 결론이 핵심이다.
- 첫 번째 분해의 기준은 “처리의 주요 단계마다 모듈 하나” 다. 순서도를 그리면 나오는 분해다. Parnas 는 순서도가 5,000~10,000 명령 규모 시스템에서는 유용한 추상이었지만 그 이상에서는 충분하지 않다고 본다.
- 두 번째 분해의 기준은 정보 은닉이다. “각 모듈은 자신이 아는 설계 결정 하나로 특징지어지며, 그것을 다른 모든 모듈로부터 숨긴다. 인터페이스는 내부 동작을 가능한 한 적게 드러내도록 고른다.”
결론의 처방은 이렇다.
순서도를 기준으로 시스템을 모듈로 나누기 시작하는 것은 거의 언제나 틀리다. 대신 어려운 설계 결정이나 바뀔 가능성이 높은 설계 결정의 목록에서 시작하라. 그리고 각 모듈이 그런 결정을 다른 모듈로부터 숨기도록 설계하라.
설계 결정은 실행 시점을 초월하므로, 이렇게 나눈 모듈은 처리 단계와 일치하지 않는다.
원문의 구체적 지침
논문은 숨겨야 할 것의 예도 든다.
| 원문 지침 | 오늘의 번역 |
|---|---|
| 자료구조와 그 접근·변경 절차는 한 모듈에 둔다. 여러 모듈이 공유하지 않는다 | 테이블을 여러 서비스가 직접 읽고 쓰지 않는다 |
| 운영체제 큐의 제어 블록 형식은 “제어 블록 모듈” 안에 숨긴다 | 메시지 스키마를 직접 노출하지 말고 계약을 둔다 |
| 문자 코드, 알파벳 순서 같은 데이터는 숨긴다 | 로캘·정렬 규칙·인코딩을 한 곳에 |
| 항목 처리 순서는 가능한 한 한 모듈 안에 숨긴다 | 워크플로 순서를 호출자 전체에 퍼뜨리지 않는다 |
효율에 대한 정직한 경고
Parnas 는 두 번째 분해를 단순히 “함수 호출” 로 구현하면 호출이 많아져 효율이 떨어질 수 있다고 인정한다. 그래서 모듈을 곧 서브루틴으로 보는 가정을 버리고, 여러 모듈의 코드를 조립해 구현하는 방식을 제안한다. 설계 단위와 실행 단위를 분리해서 생각하라는 이야기다. 오늘날의 인라이닝 컴파일러와 같은 발상이다.
예제
정보 은닉이 깨진 코드와 지킨 코드를 KWIC 의 줄 저장소로 비교한다.
# (1) 처리 단계 분해: 모든 단계가 '줄 = 단어 리스트의 리스트' 라는 형식을 안다
lines = [l.split() for l in open("input.txt")] # 입력
shifts = [(i, k) for i, w in enumerate(lines) for k in range(len(w))] # 순환 이동
shifts.sort(key=lambda s: lines[s[0]][s[1]:] + lines[s[0]][:s[1]]) # 정렬
for i, k in shifts: # 출력
print(" ".join(lines[i][k:] + lines[i][:k]))
# (2) 정보 은닉: 저장 방식은 LineStore 만 안다
class LineStore:
def __init__(self):
self._buf = [] # 나중에 파일·mmap·압축으로 바꿔도 밖은 모른다
def add(self, text): self._buf.append(text.split())
def count(self): return len(self._buf)
def words(self, i): return len(self._buf[i])
def word(self, i, w): return self._buf[i][w]
class CircularShifter:
"""순환 이동을 미리 저장할지, 요청 때 계산할지 숨긴다."""
def __init__(self, store): self.s = store
def shifts(self):
return [(i, k) for i in range(self.s.count()) for k in range(self.s.words(i))]
def word(self, shift, w):
i, k = shift
n = self.s.words(i)
return self.s.word(i, (k + w) % n)
(1) 에서 “줄을 메모리에 다 두지 않는다” 로 바꾸면 네 단계를 모두 고쳐야 한다. (2) 에서는 LineStore 만 바뀐다. Parnas 가 말한 변경 2·3번의 재현이다.
흔한 오해와 함정
- “정보 은닉 = private 키워드.” 접근 제한자는 도구일 뿐이다. getter 로 내부 자료구조를 그대로 꺼내 주면 private 이어도 결정은 새어 나간다. 기준은 “어떤 결정을 숨기는가” 다.
- “캡슐화 = 데이터와 메서드를 묶기.” 묶는 것만으로는 부족하다. Parnas 의 기준은 변경 가능성이다. 바뀌지 않을 결정까지 감추느라 추상화를 겹겹이 쌓는 것도 비용이다.
- “레이어링이면 충분하다.” 처리 단계별 레이어(표현·업무·데이터)는 첫 번째 분해와 닮을 수 있다. 필드 하나에 모든 레이어가 흔들린다면 레이어가 결정을 숨기지 못한 것이다(레이어드 아키텍처 참고).
- “마이크로서비스면 자동으로 모듈화된다.” 서비스 경계를 처리 순서로 그으면 1972년의 첫 번째 분해를 네트워크 너머로 옮긴 셈이다(마이크로서비스 vs 모놀리스).
확인 문제
- Parnas 논문에서 “모듈” 은 무엇으로 정의되는가?
- KWIC 의 첫 번째 분해에서 “문자를 워드에 4개씩 압축” 결정을 바꾸면 왜 모든 모듈이 바뀌는가?
- Parnas 가 제안한 분해의 출발점은 무엇인가?
- 정보 은닉 분해의 단점으로 Parnas 가 인정한 것과 그 해법은?
- 모든 필드에 getter/setter 를 둔 클래스가 정보 은닉을 지키지 못하는 이유는?
풀이
- 서브프로그램이 아니라 책임의 할당. 독립 작업 전에 내려야 할 설계 결정을 포함한다.
- 메모리 내 줄 저장 형식이 모든 모듈이 공유하는 인터페이스였기 때문이다.
- 어렵거나 바뀔 가능성이 높은 설계 결정의 목록. 각 모듈이 그중 하나를 숨기도록 한다.
- 각 기능을 정교한 호출 절차를 가진 프로시저로 구현하면 호출이 많아져 비효율적일 수 있다. 모듈을 서브루틴으로 보는 가정을 버리고 여러 모듈의 코드를 조립해 구현하는 방식을 제안했다.
- 내부 표현(필드 구성과 타입)이라는 설계 결정이 인터페이스로 그대로 드러나, 표현을 바꾸면 호출자도 바뀌어야 하기 때문이다.
더 읽을거리 (References)
- D. L. Parnas, On the Criteria To Be Used in Decomposing Systems into Modules, Communications of the ACM 15(12), 1053–1058, 1972
- D. L. Parnas, Designing Software for Ease of Extension and Contraction, IEEE Transactions on Software Engineering SE-5(2), 128–138, 1979
- D. L. Parnas, P. C. Clements, D. M. Weiss, The Modular Structure of Complex Systems, IEEE Transactions on Software Engineering SE-11(3), 259–266, 1985
- 이 시리즈의 맥락: SOLID 원칙