KISS: 문제를 해결하는 가장 단순한 설계를 선택하는 원칙
KISS(Keep It Simple, Stupid)는 요구사항을 만족하는 범위에서 가장 단순한 설계와 구현을 선택하라는 소프트웨어 공학 원칙이다. 단순함은 코드 줄 수가 아니라 이해해야 할 개념과 상태, 분기의 수로 측정한다. 이 글에서는 과설계를 단순한 구현으로 바꾸는 예제와 함께, 단순함을 판단하는 기준과 오용 사례를 다룬다.
By 송민성
1. 개념
KISS(Keep It Simple, Stupid)는 시스템을 필요 이상으로 복잡하게 만들지 말고, 요구사항을 충족하는 가장 단순한 해법을 택하라는 설계 원칙이다. 1960년대 미 해군 항공 분야의 엔지니어 켈리 존슨(Kelly Johnson)이 처음 쓴 표현으로 알려져 있다.
여기서 단순함(simplicity)은 "짧다"가 아니라 "이해하기 쉽다"에 가깝다. 한 줄에 로직을 압축한 코드는 짧지만 단순하지 않을 수 있다. 단순함을 판단하는 기준은 다음과 같다.
- 읽는 사람이 한 번에 머릿속에 올려야 하는 개념의 수
- 코드가 가질 수 있는 상태(state)와 분기(branch)의 수
- 변경 시 함께 수정해야 하는 파일과 모듈의 수
복잡도는 두 종류로 나뉜다. 문제 자체에서 오는 본질적 복잡도(essential complexity)와, 해법을 고르는 과정에서 생기는 우발적 복잡도(accidental complexity)다. KISS가 겨냥하는 대상은 후자다. 본질적 복잡도는 줄일 수 없고, 잘 드러내어 관리해야 한다.
2. 왜 사용하는가
- 유지보수 비용 감소: 코드는 작성하는 시간보다 읽는 시간이 훨씬 길다. 이해하기 쉬운 코드는 수정 비용이 낮다.
- 결함 감소: 상태와 분기가 적을수록 테스트해야 할 경우의 수가 줄어든다. 예를 들어 독립적인 불리언 플래그가 n개면 조합은 2ⁿ가지다.
- 온보딩 속도: 새 팀원이 구조를 파악하는 시간이 짧아진다.
- 변경 용이성: 단순한 구조는 요구사항이 바뀔 때 고치기 쉽다. 미래를 예측해 만든 추상화는 예측이 빗나가면 오히려 걸림돌이 된다.
3. 동작 원리
KISS는 알고리즘이 아니라 의사결정 규칙이다. 설계 선택지가 여러 개일 때 다음 순서로 적용한다.
- 현재 요구사항을 문장으로 적는다. 요구사항에 없는 "나중에 필요할지 모르는 것"은 제외한다.
- 가장 직관적인 구현을 먼저 만든다. 언어와 표준 라이브러리가 제공하는 기능으로 해결 가능한지 본다.
- 복잡도를 늘리는 장치는 근거가 있을 때만 도입한다. 추상화 계층, 디자인 패턴, 외부 의존성, 캐시, 비동기 처리가 여기에 해당한다. 측정된 문제나 실제 요구가 근거가 된다.
- 리팩터링으로 단순하게 되돌린다. 이미 복잡해진 부분은 조건문을 평탄화하고, 중복된 개념을 합치고, 쓰지 않는 코드를 삭제한다.
비슷한 원칙과의 관계도 구분해 두면 좋다.
| 원칙 | 핵심 질문 |
|---|---|
| KISS | 더 단순한 해법이 있는가? |
| YAGNI(You Aren't Gonna Need It) | 지금 필요한 기능인가? |
| DRY(Don't Repeat Yourself) | 같은 지식이 여러 곳에 중복되어 있는가? |
DRY를 지나치게 적용해 서로 다른 개념을 하나의 추상화로 묶으면 KISS를 해칠 수 있다. 원칙끼리 충돌하면 "읽는 사람이 이해하기 쉬운가"를 기준으로 판단한다.
4. 코드 예제
요구사항: 주문 금액에 따라 할인율을 계산한다. 10만 원 이상은 10%, 5만 원 이상은 5%, 그 외는 0%다.
먼저 과설계된 버전이다. 요구사항은 규칙 3개뿐인데 전략 패턴과 레지스트리까지 도입했다.
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def applies(self, amount: int) -> bool: ...
@abstractmethod
def rate(self) -> float: ...
class TenPercent(DiscountStrategy):
def applies(self, amount: int) -> bool:
return amount >= 100_000
def rate(self) -> float:
return 0.10
class FivePercent(DiscountStrategy):
def applies(self, amount: int) -> bool:
return 50_000 <= amount < 100_000
def rate(self) -> float:
return 0.05
class DiscountRegistry:
def __init__(self) -> None:
self._strategies: list[DiscountStrategy] = []
def register(self, strategy: DiscountStrategy) -> None:
self._strategies.append(strategy)
def calculate(self, amount: int) -> float:
for s in self._strategies:
if s.applies(amount):
return s.rate()
return 0.0
registry = DiscountRegistry()
registry.register(TenPercent())
registry.register(FivePercent())
print(registry.calculate(120_000)) # 0.1같은 요구사항을 KISS에 맞게 작성한 버전이다.
def discount_rate(amount: int) -> float:
if amount >= 100_000:
return 0.10
if amount >= 50_000:
return 0.05
return 0.0
if __name__ == "__main__":
for amount in (30_000, 50_000, 120_000):
print(amount, discount_rate(amount))실행 결과는 다음과 같다.
30000 0.0
50000 0.05
120000 0.1두 버전의 동작은 같지만, 두 번째는 클래스 4개와 등록 절차가 함수 하나로 줄었다. 경계 조건도 위에서 아래로 읽으면 바로 드러난다. 할인 규칙이 수십 개로 늘고 운영 중에 동적으로 바뀌어야 한다면 그때 데이터 기반 구조로 바꾸면 된다.
단순함은 로직을 압축하는 것과 다르다. 아래 두 코드를 비교해 보자.
# 짧지만 읽기 어려운 코드
def f(xs):
return sorted({x for x in xs if x % 2 == 0 and x > 0}, reverse=True)[:3]
# 의도가 드러나는 코드
def top_three_positive_evens(numbers: list[int]) -> list[int]:
positive_evens = {n for n in numbers if n > 0 and n % 2 == 0}
return sorted(positive_evens, reverse=True)[:3]
print(top_three_positive_evens([4, -2, 8, 8, 6, 10, 3])) # [10, 8, 6]5. 시간 복잡도 또는 성능 특성
KISS는 특정 시간 복잡도를 갖는 기법이 아니다. 다만 성능과의 관계에서 알아둘 점이 있다.
- 단순한 구현은 대체로 병목을 찾고 최적화하기 쉽다. 프로파일링 결과를 해석하기 쉽기 때문이다.
- 측정 없이 하는 선제적 최적화(premature optimization)는 복잡도를 늘리는 주된 원인이다. 캐시, 병렬 처리, 비트 연산 트릭은 측정된 병목에 대해서만 도입한다.
- 단순한 코드가 항상 빠른 것은 아니다. 중첩 반복문으로 작성한 O(n²) 탐색이 데이터 규모 때문에 실제 병목이 된다면 딕셔너리나 집합(set)을 쓰는 것이 옳다. 이 경우 평균 O(1) 조회를 제공하는 해시 테이블 기반 자료구조는 오히려 표준적이고 단순한 선택이다.
즉 성능 요구가 실제로 존재하면 그것도 요구사항의 일부이고, KISS는 그 요구를 만족하는 가장 단순한 해법을 고르라는 뜻이다.
6. 실무 사용 사례
- 모놀리스 우선: 팀 규모와 트래픽이 작은 초기 서비스에서 마이크로서비스 대신 하나의 배포 단위로 시작한다. 네트워크 호출, 분산 트랜잭션, 배포 파이프라인 복잡도를 피할 수 있다.
- 데이터베이스 선택: 관계형 데이터가 명확하다면 PostgreSQL 하나로 시작한다. 검색, 큐, 캐시를 위해 저장소를 여러 개 두기 전에 PostgreSQL의 기능으로 충족되는지 먼저 확인한다.
- 상태 관리: 프런트엔드에서 컴포넌트 로컬 상태로 충분한 화면에 전역 상태 라이브러리를 넣지 않는다.
- 설정과 분기: 기능 플래그(feature flag)는 수명이 정해진 임시 장치로 쓰고, 목적을 달성하면 제거한다.
- 코드 리뷰 기준: "이 추상화가 지금 해결하는 문제가 무엇인가?"를 질문 항목으로 둔다.
PostgreSQL에서의 예를 하나 들면, 단순한 작업 큐가 필요할 때 별도 메시지 브로커 없이 테이블과 FOR UPDATE SKIP LOCKED로 시작할 수 있다.
CREATE TABLE IF NOT EXISTS jobs (
id BIGSERIAL PRIMARY KEY,
payload JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 워커가 작업 하나를 가져가는 쿼리
WITH next_job AS (
SELECT id
FROM jobs
WHERE status = 'pending'
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED
)
UPDATE jobs
SET status = 'running'
FROM next_job
WHERE jobs.id = next_job.id
RETURNING jobs.id, jobs.payload;처리량이 늘어 한계가 측정되면 그때 전용 메시지 큐로 옮기면 된다.
7. 주의할 점
- 단순함과 쉬움은 다르다. 익숙한 방식이 쉬워 보여도 구조가 얽혀 있으면 단순하지 않다. 단순함은 개념이 서로 분리되어 있는 정도로 판단한다.
- 필요한 복잡도까지 깎지 않는다. 보안 검증, 트랜잭션 경계, 오류 처리, 멱등성(idempotency)처럼 문제 자체가 요구하는 부분은 본질적 복잡도다. 이를 생략하면 단순해지는 것이 아니라 틀린 코드가 된다.
- 복사·붙여넣기는 단순함이 아니다. 같은 규칙이 여러 곳에 흩어져 있으면 변경 시 누락이 생긴다. 중복 제거가 이해를 돕는 경우에는 DRY를 적용한다.
- YAGNI와 혼동하지 않는다. KISS는 "지금 필요한 것을 어떻게 단순하게 만들까"이고, YAGNI는 "지금 필요한가"다.
- 단순함은 팀 맥락에 따라 달라진다. 팀이 이미 익숙한 도구와 관용구를 쓰는 편이 새로운 패러다임보다 단순할 수 있다.
- 한 번에 완성하려 하지 않는다. 처음부터 완벽히 단순한 설계는 드물다. 동작하는 구현을 만든 뒤 테스트를 안전망으로 삼아 반복적으로 단순화한다.
8. 핵심 정리
- KISS는 요구사항을 만족하는 가장 단순한 해법을 선택하라는 설계 원칙이다.
- 단순함의 기준은 코드 길이가 아니라 이해해야 할 개념, 상태, 분기의 수다.
- 줄여야 할 대상은 우발적 복잡도(accidental complexity)이며, 본질적 복잡도(essential complexity)는 숨기지 말고 명확히 다룬다.
- 추상화, 패턴, 외부 의존성, 최적화는 실제 요구나 측정 결과가 있을 때 도입한다.
- 단순한 구현으로 시작하고, 필요가 확인되면 그때 복잡도를 추가한다.
- 단순함을 이유로 보안, 오류 처리, 정합성 같은 필수 요건을 생략해서는 안 된다.