소프트웨어 공학 100 주제 시리즈의 6번째 글이다. (카테고리: 기초와 역사)

한 줄 요약

Meir M. Lehman 은 1970년대 IBM OS/360 의 릴리스 데이터를 분석해, 현실 세계에서 쓰이는 소프트웨어(E-type)가 계속 바뀌지 않으면 점점 덜 만족스러워지고, 바뀔수록 복잡해지며, 그 진화 과정은 피드백 시스템처럼 움직인다는 규칙성을 “소프트웨어 진화의 법칙” 으로 정리했다. 1996년까지 여덟 개로 늘어났다.

왜 필요한가

많은 조직이 소프트웨어를 “완성되면 끝나는 프로젝트” 로 예산을 잡는다. 출시 후에는 소수의 유지보수 인력만 남기고, 나머지는 다음 프로젝트로 옮긴다. 그런데 몇 년 뒤 그 시스템은 대개 둘 중 하나가 된다.

  • 아무도 손대지 않아서 업무와 점점 어긋나고, 사용자가 엑셀로 우회하기 시작한다.
  • 계속 기능이 덧붙어 아무도 전체를 이해하지 못하고, 작은 변경에도 장애가 난다.

Lehman 의 법칙은 이 두 결과가 우연이나 개인의 실력 문제가 아니라 진화하는 소프트웨어의 구조적 성질이라고 말한다. 그렇다면 대응도 일회성 “정리 프로젝트” 가 아니라 상시 활동이어야 한다.

핵심 개념

출처

1997년 논문은 법칙의 출발점을 이렇게 설명한다. 1968년 IBM 프로그래밍 프로세스 연구가 OS/360 의 측정 기반 연구로 이어졌고, 릴리스 순서 번호(rsn)로 정렬한 약 26개 릴리스·하위 릴리스의 데이터에서 사람의 결정이 간접적으로 만든 결과인데도 예상치 못한 규칙성이 보였다. 이런 규칙성은 소프트웨어 기술 바깥(조직 동역학, 관리, 사회학)까지 걸쳐 있어 소프트웨어 공학 입장에서는 외부에서 작용하는 제약으로 받아들여야 했고, 그래서 “법칙” 이라 불렸다.

E-type 소프트웨어

법칙이 적용되는 대상은 E-type 시스템이다. 1997년 논문의 정의는 “현실 세계의 문제를 풀거나 응용을 다루는 소프트웨어” 다. 현실 세계에 놓인 소프트웨어는 설치·운영되는 순간 그 세계를 바꾸고, 외부 변화가 소프트웨어에 박힌 가정을 무효로 만든다. 그래서 계속 갱신되어야 한다.

반대편에는 명세로 완전히 정의되고 명세를 만족하는지로 정답이 갈리는 프로그램(Lehman 의 분류에서 S-type)이 있다. 정렬 함수는 정렬이 맞으면 끝이지만, 급여 시스템은 세법이 바뀌면 “맞던” 것이 틀리게 된다.

여덟 개의 법칙

1997년 논문 표 1 을 옮긴다. 괄호 안은 처음 발표된 해다.

번호 이름 내용
I (1974) 지속적 변화 Continuing Change E-type 시스템은 계속 적응되어야 한다. 그렇지 않으면 점점 덜 만족스러워진다
II (1974) 복잡성 증가 Increasing Complexity E-type 시스템이 진화하면 복잡성이 증가한다. 그것을 유지하거나 줄이는 작업을 하지 않는 한
III (1974) 자기 조절 Self Regulation 진화 과정은 자기 조절적이며 제품·프로세스 측정값의 분포가 정규분포에 가깝다
IV (1980) 조직 안정성 보존 (불변 작업률) 진화하는 시스템의 평균 유효 전역 활동률은 제품 수명 동안 불변이다
V (1980) 친숙성 보존 Conservation of Familiarity 개발자·영업·사용자 등 관련된 모두가 시스템의 내용과 동작을 계속 숙지해야 한다. 과도한 성장은 그 숙지를 떨어뜨린다. 그래서 평균 증분 성장은 불변으로 유지된다
VI (1980) 지속적 성장 Continuing Growth 사용자 만족을 유지하려면 기능 내용이 계속 늘어나야 한다
VII (1996) 품질 저하 Declining Quality 엄격히 유지되고 운영 환경 변화에 적응되지 않으면 품질이 떨어지는 것으로 보인다
VIII (1996) 피드백 시스템 Feedback System E-type 진화 프로세스는 다층·다중 루프·다중 행위자 피드백 시스템이며, 의미 있는 개선을 얻으려면 그렇게 다뤄야 한다(1974년 처음 언급, 1996년 법칙으로 정식화)

1990년대의 재검증: 모두 같은 강도가 아니다

1997년 논문은 FEAST/1 프로젝트에서 금융 거래 시스템(FW)의 데이터를 분석해 법칙을 다시 점검했다(표 3). 결과는 법칙마다 다르다.

법칙 판정 근거 요지
I 지속적 변화, VI 지속적 성장 지지 지속적 성장이 뚜렷이 보이고, 일부는 적응·변경, 일부는 기능 추가 때문임이 확인됨
II 복잡성 증가 지지 성장 모형의 예측력이 복잡성을 제약 요인으로 보는 해석을 뒷받침
IV 불변 작업률 지지 단일 상수 매개변수로 좋은 적합과 예측력
VIII 피드백 시스템 지지 여러 그림의 조절 패턴이 뒷받침. 피드백 메커니즘 식별은 과제
III 자기 조절 불확실 매끄러운 추세 주위의 물결은 조절을 시사하지만 메커니즘 규명이 필요
V 친숙성 보존 불확실 평균 증분 성장에 뚜렷한 추세가 있어 원래의 “불변” 주장이 의문시됨
VII 품질 저하 판정 불가 찬반 근거 데이터 없음

저자들도 이 분석이 법칙을 “지지한다, 더 정확히는 반박하지 않는다” 고 조심스럽게 쓴다. 법칙이라는 이름에도 불구하고 물리 법칙이 아니라 경험적 관찰의 정리라는 점을 기억하자.

피드백 관점

8번 법칙이 전체를 묶는다. 사용자 요구 → 변경 → 릴리스 → 사용 경험 → 새 요구로 이어지는 루프가 여러 겹 겹쳐 있다. 그래서 한 지점(예: 코딩 속도)만 개선해도 전체 진화 속도는 다른 루프(리뷰 대기, 사용자 피드백 주기, 승인 절차)에 묶여 크게 달라지지 않을 수 있다. 1997년 논문은 이 피드백 성격 때문에 E-type 개발·진화 프로세스가 개선하기 가장 어려운 대상이었을 수 있다고 본다.

        ┌──────────── 외부 세계 변화 (법·시장·플랫폼) ───────────┐
        ▼                                                       │
   [요구·불만] → [변경 계획] → [구현] → [릴리스] → [운영·사용] ──┘
        ▲            │                       │
        │            └─ 복잡성 증가 (II) ────┤
        └──────── 숙지 한계로 증분 제한 (V) ─┘

실무 적용

법칙을 팀의 운영 원칙으로 번역하면 이렇다.

법칙 실무 원칙
I, VI 출시 후에도 지속적인 용량을 예산에 넣는다. “유지보수 = 잔업” 이 아니다
II 복잡성 감소 작업(리팩터링, 의존성 정리)을 기능 작업과 별도 항목으로 계획한다
IV 인력을 늘려도 진화 속도가 비례해 늘지 않을 수 있다. 병목 루프를 먼저 찾는다
V 한 릴리스에 너무 많은 변화를 넣지 않는다. 큰 변경은 쪼개서 사용자와 개발자가 따라올 수 있게 한다
VII 운영 환경(OS, 런타임, 브라우저, 클라우드 API)의 변화를 추적하는 정례 작업을 둔다
VIII 개선은 루프 단위로 본다 — 리드 타임, 피드백 주기, 승인 대기

5번 법칙을 우리 저장소에서 대략 확인해 볼 수 있다. 릴리스 태그별 코드 크기 증분을 보는 스크립트다.

# 릴리스 태그 사이의 증분 성장(추가-삭제 줄 수)을 본다.
# 증분이 평균보다 크게 튄 릴리스 뒤에 장애·핫픽스가 몰렸는지 함께 보면 좋다.
import subprocess, statistics

def tags():
    out = subprocess.run(["git", "tag", "--sort=creatordate"],
                         capture_output=True, text=True, check=True).stdout
    return [t for t in out.split() if t]

def net_lines(a, b):
    out = subprocess.run(["git", "diff", "--numstat", a, b],
                         capture_output=True, text=True, check=True).stdout
    added = deleted = 0
    for line in out.splitlines():
        x, y, _ = line.split("\t", 2)
        if x != "-":                      # 바이너리 파일 제외
            added, deleted = added + int(x), deleted + int(y)
    return added - deleted

ts = tags()
growth = [(b, net_lines(a, b)) for a, b in zip(ts, ts[1:])]
mean = statistics.mean(g for _, g in growth) if growth else 0
for tag, g in growth:
    flag = "  <-- 평균의 2배 초과" if mean > 0 and g > 2 * mean else ""
    print(f"{tag:>12} {g:>8}{flag}")

줄 수는 거친 대리 지표다. 그래도 “이번 릴리스는 평소의 몇 배를 바꿨다” 는 사실이 보이면, 릴리스를 쪼갤지 판단하는 근거가 된다.

흔한 오해와 함정

  • “법칙이니까 반드시 성립한다.” 경험적 관찰에서 나온 일반화이고, 저자들 스스로 1990년대 재검증에서 일부 법칙을 “불확실” 로 판정했다.
  • “모든 소프트웨어에 적용된다.” 대상은 현실 세계에 놓인 E-type 시스템이다. 명세로 완결되는 라이브러리 함수에는 그대로 적용되지 않는다.
  • “복잡성 증가는 피할 수 없다.” 2번 법칙에는 조건이 붙어 있다 — “그것을 유지하거나 줄이는 작업을 하지 않는 한.” 부패는 기본값이지 운명이 아니다(기술 부채 참고).
  • “유지보수는 버그 수정이다.” 1번과 6번 법칙이 보여 주듯, 진화의 상당 부분은 환경 적응과 기능 성장이다. 유지보수 예산을 결함 건수로만 잡으면 늘 모자란다.

확인 문제

  1. E-type 소프트웨어란 무엇이며, 왜 계속 바뀌어야 하는가?
  2. 2번 법칙(복잡성 증가)의 조건절은 무엇이고, 실무에서 어떤 의미인가?
  3. 5번 법칙이 릴리스 크기에 대해 시사하는 바는?
  4. 1997년 재검증에서 “판정 불가” 였던 법칙과 그 이유는?
  5. 8번 법칙의 관점에서 “코딩 속도를 2배로” 같은 개선이 전체 진화 속도를 2배로 만들지 못할 수 있는 이유는?

풀이

  1. 현실 세계의 문제를 풀거나 응용을 다루는 소프트웨어. 설치·운영이 세계를 바꾸고 외부 변화가 소프트웨어에 박힌 가정을 무효화하므로 계속 갱신해야 한다.
  2. “복잡성을 유지하거나 줄이는 작업을 하지 않는 한.” 복잡성 감소 작업을 명시적으로 계획하고 용량을 배정해야 한다는 뜻이다.
  3. 관련된 사람들이 시스템을 계속 숙지할 수 있어야 하므로, 한 번에 지나치게 큰 증분은 위험하다. 변경을 쪼개 릴리스하는 근거가 된다.
  4. 7번 품질 저하. FW 데이터에 찬반 근거가 될 자료가 없었다.
  5. 진화는 요구·리뷰·승인·릴리스·사용자 피드백이 얽힌 다중 루프 피드백 시스템이라, 한 단계만 빨라져도 다른 루프가 병목으로 남기 때문이다.

더 읽을거리 (References)