처리중입니다. 잠시만 기다려주세요.
TTJ 코딩클래스
정규반 단과 자료실 테크 뉴스 코딩 퀴즈
테크 뉴스
Hacker News 2026.03.22 42

"실패하기 어렵게 만들어라" — 방어적 설계가 코드 품질을 바꾸는 법

Hacker News 원문 보기
"실패하기 어렵게 만들어라" — 방어적 설계가 코드 품질을 바꾸는 법

좋은 코드는 '잘 동작하는 코드'가 아니라 '잘못 쓰기 어려운 코드'다

우리는 흔히 코드 품질을 이야기할 때 "잘 동작하는 코드"에 초점을 맞춥니다. 테스트를 통과하고, 스펙대로 결과를 내놓으며, 성능도 괜찮은 코드. 하지만 실무에서 버그가 발생하는 지점은 대개 "코드가 잘못 동작할 때"가 아니라 "코드를 잘못 사용할 때"입니다. API를 호출하는 순서를 실수하거나, 필수 파라미터를 빠뜨리거나, null이 올 수 있는 곳에 null 체크를 잊거나. 이 글은 바로 그 관점의 전환을 이야기합니다. 잘 동작하게 만드는 것보다, 실패하기 어렵게 만드는 것이 더 중요하다는 철학입니다.

"Pit of Success" — 성공의 구덩이에 빠뜨려라

이 개념은 사실 마이크로소프트의 Rico Mariani가 .NET 프레임워크 설계 시절에 대중화한 용어인 "Pit of Success(성공의 구덩이)"와 맞닿아 있습니다. 일반적으로 프로그래밍에서 "pit"이라고 하면 함정, 즉 빠지면 안 되는 곳을 떠올립니다. 하지만 Pit of Success는 정반대입니다. 개발자가 아무 생각 없이 자연스럽게 코드를 작성하면 자동으로 올바른 방향으로 굴러떨어지는 구조를 만들자는 것이죠.

예를 들어볼까요. 어떤 함수가 반드시 초기화 후에 호출되어야 한다면, 두 가지 설계가 가능합니다. 첫 번째는 문서에 "이 함수를 호출하기 전에 반드시 init()을 먼저 호출하세요"라고 적어두는 것. 두 번째는 애초에 init()의 반환값으로만 해당 함수를 호출할 수 있는 타입을 돌려주는 것입니다. 전자는 사람의 주의력에 의존하고, 후자는 컴파일러가 강제합니다. 어느 쪽이 더 실패하기 어려운 설계인지는 명확합니다.

구체적인 패턴들: 타입 시스템을 방패로 쓰기

실패하기 어려운 코드를 만드는 가장 강력한 도구는 타입 시스템입니다. TypeScript, Kotlin, Rust 같은 언어에서 타입을 잘 설계하면 잘못된 상태 자체가 표현 불가능해집니다.

가장 대표적인 예가 "불가능한 상태를 불가능하게 만들기(Make Impossible States Impossible)"입니다. 로딩 상태를 관리할 때 { isLoading: boolean, data: T | null, error: Error | null } 같은 구조를 흔히 씁니다. 하지만 이 구조에서는 isLoading: true이면서 동시에 dataerror가 모두 존재하는 비정상적 상태가 타입상 가능합니다. 반면 유니온 타입으로 { state: 'loading' } | { state: 'success', data: T } | { state: 'error', error: Error }처럼 설계하면, 로딩 중에 데이터가 존재하는 상태 자체가 타입 레벨에서 불가능해집니다.

Rust의 소유권 시스템도 같은 철학입니다. "메모리를 해제한 뒤에 접근하지 마세요"라고 문서에 적는 대신, 소유권이 이동하면 컴파일러가 접근 자체를 차단합니다. C/C++에서 수십 년간 개발자의 실수에 의존하던 메모리 안전성을, Rust는 실수 자체가 불가능한 구조로 바꿔놓았습니다.

Builder 패턴과 Fluent API: 올바른 사용법을 강제하는 인터페이스

함수의 파라미터가 많아질 때도 비슷한 문제가 생깁니다. createUser(name, email, age, role, department, isActive) 같은 함수를 보면, 호출하는 쪽에서 파라미터 순서를 헷갈릴 확률이 높습니다. 특히 agerole처럼 타입이 같은(둘 다 number일 수 있는) 파라미터가 연속되면 바꿔 넣어도 컴파일러가 잡아주지 못합니다.

Builder 패턴은 이 문제를 우아하게 해결합니다. UserBuilder().name("홍길동").email("hong@test.com").age(30).build() 형태로 작성하면, 각 필드가 명시적이라 순서 실수가 원천 차단됩니다. 더 나아가 Rust의 typestate 패턴을 활용하면 필수 필드를 넣지 않으면 .build() 자체가 호출 불가능하게 만들 수도 있습니다.

기존 방식과 무엇이 다른가: 방어적 프로그래밍 vs. 실패 불가능 설계

흔히 아는 "방어적 프로그래밍(Defensive Programming)"과는 결이 다릅니다. 방어적 프로그래밍은 잘못된 입력이 들어올 것을 예상하고 런타임에 체크하는 것입니다. if (param == null) throw new Error(...) 같은 코드가 전형적이죠. 이것도 물론 필요하지만, 문제가 발생한 후에 잡아내는 방식입니다.

"실패하기 어렵게 만들기"는 한 단계 더 나아갑니다. 잘못된 입력이 애초에 만들어질 수 없는 구조를 설계하는 것입니다. 런타임 에러 대신 컴파일 에러로, 문서 대신 타입으로, 컨벤션 대신 구조로 안전성을 보장합니다. 물론 모든 것을 타입으로 강제할 수는 없지만, 가능한 한 많은 제약을 컴파일 타임으로 끌어올리는 것이 핵심입니다.

한국 개발자에게 주는 시사점

이 철학은 특히 팀 규모가 커질수록 빛을 발합니다. 혼자 개발할 때는 "이거 조심해서 써야 해"라고 머릿속에 기억해두면 됩니다. 하지만 팀원이 10명, 50명이 되면 모든 사람이 모든 주의사항을 기억하기란 불가능합니다. 코드 리뷰에서 매번 같은 실수를 잡아내고 있다면, 그건 리뷰어가 부족한 게 아니라 API 설계가 잘못 쓰기 너무 쉬운 것일 수 있습니다.

실무에서 바로 적용할 수 있는 점검 목록을 정리하면 이렇습니다. 첫째, 함수 시그니처를 보고 잘못 호출할 수 있는 방법이 있는지 확인하세요. 같은 타입의 파라미터가 연속되면 객체 파라미터나 Builder로 바꾸세요. 둘째, boolean 파라미터를 enum으로 바꾸세요. doSomething(true, false) 보다 doSomething(Mode.STRICT, Logging.OFF)가 실수할 여지가 적습니다. 셋째, 상태 관리에서 불가능한 조합이 타입상 가능한지 점검하세요.

마무리

핵심은 단순합니다. 실수를 줄이는 가장 좋은 방법은 조심하는 것이 아니라, 실수할 수 없는 구조를 만드는 것입니다. "Make it hard to fail"은 단순한 슬로건이 아니라 API 설계, 타입 설계, 시스템 아키텍처 전반에 걸쳐 적용되는 엔지니어링 원칙입니다.

여러분의 코드베이스에서 가장 자주 반복되는 실수는 무엇인가요? 그리고 그 실수를 "조심해서 쓰세요"가 아니라 구조적으로 불가능하게 만들 수 있는 방법이 있을까요?


🔗 출처: Hacker News

이 뉴스가 유용했나요?

이 기술을 직접 배워보세요

AI 도구, 직접 활용해보세요

AI 시대, 코딩으로 수익을 만드는 방법을 배울 수 있습니다.

AI 활용 강의 보기

"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"

실제 수강생 후기
  • 비전공자도 6개월이면 첫 수익
  • 20년 경력 개발자 직강
  • 자동화 프로그램 + 소스코드 제공

매일 AI·개발 뉴스를 받아보세요

주요 테크 뉴스를 매일 아침 이메일로 전해드립니다.

스팸 없이, 언제든 구독 취소 가능합니다.