바버라 리스코프의 데이터 추상화 원칙을 배워 안정적인 인터페이스를 설계하고 파괴적 변경을 줄이며 명확하고 유지보수 가능한 신뢰성 있는 시스템과 API를 구축하는 방법을 알아보세요.

pop()은 가장 최근의 push()를 반환합니다. 스택이 배열을 쓰든 연결 리스트를 쓰든 그건 비공개입니다.\n\n### 실전 API에의 적용\n\n같은 분리가 어디든 적용됩니다:\n\n- REST 엔드포인트: POST /payments는 인터페이스이고, 사기 검사나 재시도, 데이터베이스 쓰기는 구현입니다.\n- SDK 메서드: client.upload(file)은 인터페이스이고, 청크 분할, 압축, 병렬 요청은 구현입니다.\n- UI 컴포넌트: “DatePicker”는 props/이벤트를 노출하고, DOM 구조와 접근성 처리 로직은 구현입니다.\n\n추상화를 염두에 두고 설계하면 호출자가 의존하는 계약에 집중하게 되고, 커튼 뒤의 모든 것을 바꿀 자유를 얻습니다.\n\n## 시스템을 올바르게 유지하는 숨은 규칙: 불변식(Invariants)\n\n불변식은 추상화 내부에서 항상 참이어야 하는 규칙입니다. API를 설계할 때 불변식은 데이터가 불가능한 상태로 흐르지 않도록 가드레일 역할을 합니다—예: 서로 다른 통화를 가진 은행 계좌가 동시에 존재하거나, 항목이 없는 상태에서 "완료"된 주문 같은 것들입니다.\n\n### 수학 없이 보는 불변식의 모습\n\n불변식은 해당 타입의 "현실의 형태"라고 생각하세요:\n\n- Cart는 음수 수량을 가질 수 없다.\n- UserEmail은 항상 유효한 이메일 주소다(나중에 "검증"하지 않음).\n- Reservation의 start < end이고 두 시간은 같은 타임존이다.\n\n이 조건들이 깨지면 시스템은 예측 불가능해집니다. 모든 기능이 이제 "깨진" 데이터가 무엇을 의미하는지 추측해야 하기 때문입니다.\n\n### 불변식이 검증과 오류 처리에 주는 방향\n\n좋은 API는 경계에서 불변식을 강제합니다:\n\n- 생성 시: 잘못된 입력은 조기에 거부(명확한 오류 반환).\n- 갱신 시: 불변식을 유지하는 변경만 허용.\n- 파싱/IO 시: 외부 데이터를 신뢰하지 말고 저장 전에 검증.\n\n이렇게 하면 모호한 실패("문제가 발생했다") 대신 API가 어떤 규칙이 위반됐는지 설명할 수 있습니다("종료 시간은 시작 시간보다 이후여야 합니다" 등).\n\n### 불변식이 인터페이스에 유출되게 하지 마라\n\n호출자가 "먼저 normalize()를 호출해야만 이 메서드가 동작한다" 같은 내부 규칙을 외워야 해서는 안 됩니다. 불변식이 특별한 의식에 의존한다면, 그건 불변식이 아니라 함정입니다.\n\n인터페이스를 이렇게 설계하세요:\n\n- 잘못된 상태는 표현불가(또는 표현하기 어렵게) 만들기\n- 메서드가 자동으로 불변식을 보존하게 하기\n\n### 실용적인 문서화 체크리스트\n\nAPI 타입을 문서화할 때 적어두세요:\n\n1. 불변식 문장(평어, 테스트 가능)\n2. 어디에서 강제되는가(생성자, 세터, 엔드포인트)\n3. 위반 시 처리 방식(오류 타입/메시지, 상태 코드)\n4. 어떤 메서드가 불변식을 보존하는가(예외가 있다면 명시)\n5. 유효/무효 입력 예시(짧고 구체적으로)\n\n## 계약(Contracts): 호출자와 유지보수자가 행동을 명확히 알게 하라\n\n좋은 API는 단지 함수들의 모음이 아니라 약속입니다. 계약은 그 약속을 명시화해 호출자가 동작에 의존할 수 있게 하고, 유지보수자는 내부를 변경해도 사람들을 놀라게 하지 않게 합니다.\n\n### 계약에 명시해야 할 것\n\n최소한 다음을 문서화하세요:\n\n- 사전조건: 호출 전 참이어야 할 것(유효 범위, 권한, 스레드 안전성 기대).\n- 사후조건: 성공 호출 후 어떤 것이 참인지(반환값 의미, 상태 변경).\n- 부작용: 다른 무엇이 변경되는지(디스크 쓰기, 네트워크 요청, 전달된 객체 변경 등).\n\n이 명확성은 동작을 예측 가능하게 합니다: 호출자는 어떤 입력이 안전한지, 어떤 결과를 처리해야 할지 알게 되고, 테스트는 의도를 확인할 수 있습니다.\n\n### 계약은 "부족한 지식(tribal knowledge)"을 줄인다\n\n계약이 없으면 팀은 기억과 비공식 규범에 의존합니다: "거기에 null을 넘기지 마라", "그 호출은 가끔 재시도한다", "오류 시 빈 값을 반환한다" 등. 이런 규칙은 온보딩, 리팩터링, 사건 대응 중에 사라집니다.\n\n문서화된 계약은 숨겨진 규칙을 공유 지식으로 바꾸고, 코드 리뷰의 기준을 "이 변경이 계약을 만족하는가?"로 바꿉니다.\n\n### 좋은 문구 vs 모호한 문구(예시)\n\n모호: "사용자를 생성한다."\n\n더 나음: "고유한 이메일로 사용자를 생성한다.\n\n- 사전조건: email은 유효한 주소여야 하며, 호출자는 users:create 권한을 가져야 함.\n- 사후조건: 새로운 userId를 반환; 유저는 영속화되어 즉시 조회 가능함.\n- 실패 모드: 이메일이 이미 존재하면 409 반환; 필드가 잘못되면 400 반환; 부분적으로 생성된 유저는 없음."\n\n모호: "아이템을 빠르게 가져온다."\n\n더 나음: "createdAt 내림차순으로 정렬된 최대 limit개의 아이템을 반환함.\n\n- 부작용: 없음.\n- 일관성: 최대 60초까지 오래된 데이터일 수 있음.\n- 페이징: 다음 페이지는 nextCursor 사용; 커서는 15분 후 만료."\n\n## 정보 은닉: 내부를 비공개로 유지하고 API를 안정적으로 지켜라\n\n정보 은닉은 데이터 추상화의 실용적 측면입니다: 호출자는 API가 무엇을 하는지에 의존해야지 내부가 어떻게 구현되었는지에 의존하면 안 됩니다. 사용자가 내부를 볼 수 없으면, 당신은 그것을 변경해도 모든 릴리스가 파괴적 변경이 되지 않습니다.\n\n### 표현이 아니라 연산을 노출하라\n\n좋은 인터페이스는 소수의 연산(create, fetch, update, list, validate)만 공개하고 표현(테이블, 캐시, 큐, 파일 레이아웃, 서비스 경계)은 비공개로 유지합니다.\n\n예를 들어 "카트에 항목 추가"는 연산입니다. 데이터베이스의 "CartRowId"는 구현 세부사항입니다. 세부사항을 공개하면 사용자가 거기에 의존해 자체 로직을 만들고, 이는 당신이 변경할 여지를 고정시킵니다.\n\n### 내부를 숨기면 리팩터링이 안전해지는 이유\n\n클라이언트가 안정적 동작에만 의존하면 다음을 할 수 있습니다:\n\n- 데이터베이스나 스토리지 포맷을 변경\n- 모놀리스를 서비스로 분리\n- 캐싱 추가나 인덱싱 변경\n- 내부 모델 재구성\n\n…그리고 API는 호환성을 유지합니다. 이게 진짜 얻는 이익입니다: 사용자에게는 안정성, 유지보수자에게는 자유입니다.\n\n### 흔한 유출 패턴\n\n내부가 실수로 유출되는 몇 가지 방식:\n\n- 내부 ID를 반환하는 것(자동 증가 정수, 샤드 키 등) — 저장 계층에서만 의미 있는 값.\n- 변경 가능한 구조를 노출(클라이언트가 원시 객체를 받아 패치하고 다시 보내는 경우) — 클라이언트를 정확한 필드에 결속시킴.\n- 클라이언트가 내부 상태를 구성하도록 허용: 예를 들어 status=3 같은 형식 허용.\n\n### 안정적으로 유지되는 응답 형태 설계하기\n\n의미를 설명하는 응답을 선호하세요, 메커니즘이 아니라:\n\n- 데이터베이스 행 번호 대신 안정적이고 불투명한 공개 식별자 사용(예: "userId": "usr_…").\n- 컬렉션의 복사본이나 읽기 전용 뷰를 반환해 클라이언트가 순서나 내부 필드에 우연히 의존하지 않게 함.\n- 필드를 호환성 있게 추가하고 기존 필드의 의미를 변경하지 않음.\n\n어떤 세부사항이 바뀔 수 있다면 공개하지 마세요. 사용자가 필요하다면 그것을 의도적으로 문서화된 인터페이스의 일부로 승격시키세요.\n\n## 리스코프 치환 원칙: 인터페이스에 대한 약속으로서\n\n리스코프 치환 원칙(LSP)을 한 문장으로: 어떤 코드가 인터페이스로 동작하면, 그 인터페이스의 어떤 유효한 구현으로 바꿔도 호출자가 특별한 처리를 할 필요 없이 계속 동작해야 한다.\n\nLSP는 상속보다 신뢰에 관한 것입니다. 인터페이스를 공개하면 행동에 대한 약속을 하는 것입니다. LSP는 모든 구현이 그 약속을 지켜야 한다고 말합니다, 비록 내부가 매우 다르더라도.\n\n### LSP를 ‘호출자를 놀라게 하지 마라’로 보기\n\n호출자는 API가 말한 것에 의존합니다—오늘 구현이 우연히 하는 일에 의존하지 않습니다. 인터페이스가 "유효한 레코드를 save()에 전달할 수 있다"고 말하면, 모든 구현은 그 유효한 레코드를 받아들여야 합니다. 인터페이스가 "get()은 값이나 명확한 '찾을 수 없음' 결과를 반환한다"고 말하면, 구현들은 동일한 상황에서 임의로 새로운 오류를 던지거나 부분 데이터를 반환할 수 없습니다.\n\n안전한 확장은 사용자가 공급자를 교체하거나 새 구현을 추가할 때 코드를 다시 쓰지 않도록 합니다. 이것이 LSP의 실용적 이점입니다: 인터페이스를 대체 가능하게 유지합니다.\n\n### API에서 흔한 LSP 위반 사례\n\nAPI가 약속을 깨는 두 가지 흔한 방법은:\n\n- 입력 범위 축소(더 엄격한 사전조건): 새 구현이 인터페이스 정의에서 허용한 입력을 거부함. 예: 기본 인터페이스는 UTF‑8 문자열이라면 허용했지만 한 구현은 숫자 ID만 허용하거나 빈–유효 필드를 거부함.\n\n- 출력 약화(더 느슨한 사후조건): 새 구현이 약속한 것보다 덜 반환함. 예: 인터페이스가 결과가 정렬되고 유일하며 완전하다고 했지만 한 구현은 정렬되지 않은 데이터, 중복, 항목 누락을 반환함.\n\n미묘한 위반으로는 실패 동작 변경이 있습니다: 한 구현은 "찾을 수 없음"을 반환하는데 다른 구현은 동일 상황에서 예외를 던지면 호출자는 안전하게 대체할 수 없습니다.\n\n### 플러그인 동작을 놀라움 없이 설계하기\n\n여러 구현(플러그인)을 지원하려면 인터페이스를 계약처럼 작성하세요:\n\n- 어떤 입력이 유효한지 명시하고 구현 간에 그 집합을 일관되게 유지하세요.\n- 출력의 의미(정렬, 기본값, 엣지 케이스)를 명시하세요.\n- 실패 모드를 표준화하세요: 어떤 오류가 발생할 수 있고 그것이 무엇을 의미하는지 정의하세요.\n\n구현이 정말로 더 엄격한 규칙을 필요로 한다면 같은 인터페이스에 숨기지 마세요. (1) 별도의 인터페이스를 정의하거나, (2) supportsNumericIds() 같은 기능 표시 또는 문서화된 설정 요구사항으로 명시하세요. 이렇게 하면 호출자는 알면서 선택적으로 참여합니다—대체 가능한 것처럼 보이지만 사실 대체 불가한 구현에 의해 놀라지 않게 됩니다.\n\n## 좋은 인터페이스는 작고, 응집적이며 읽기 쉽다\n\n잘 설계된 인터페이스는 호출자가 필요로 하는 것만, 그리고 그 이상은 노출하지 않기 때문에 "직관적"입니다. 리스코프의 데이터 추상화 관점은 인터페이스를 좁고, 안정적이며, 읽기 쉽게 만드는데 도움을 줍니다. 그래서 사용자는 내부를 배우지 않고도 의존할 수 있습니다.\n\n### 다재다능(범용)보다 응집을 우선하라\n\n큰 API는 종종 서로 관련 없는 책임을 섞습니다: 설정, 상태 변경, 보고, 문제 해결 등이 한곳에 모이면 언제 호출해도 안전한지 이해하기 어려워집니다.\n\n응집된 인터페이스는 동일한 추상화에 속하는 동작만 그룹화합니다. 예를 들어 큐를 나타내는 API라면 큐 동작(enqueue/dequeue/peek/size)에 집중하세요, 범용 유틸리티는 빼세요. 개념이 적을수록 오용 경로도 줄어듭니다.\n\n### 모호함을 만드는 과도한 유연한 파라미터를 피하라\n\n"유연함"은 종종 "불분명함"을 뜻합니다. options: any, mode: string, 여러 개의 불리언(force, skipCache, silent) 같은 파라미터는 정의되지 않은 조합을 만듭니다.\n\n대신:\n- 서로 다른 동작에 대해 구체적 메서드를 제공( vs ), 또는\n- 문서화된 기본값과 유효하지 않은 조합을 가진 작고 타입화된 옵션 객체를 제공하세요.\n\n파라미터 때문에 호출자가 소스를 읽어야만 동작을 알 수 있다면, 그것은 좋은 추상화의 일부가 아닙니다.\n\n### 이름 짓기도 인터페이스의 일부다\n\n이름은 계약을 전달합니다. 관찰 가능한 동작을 설명하는 동사(, , , , )를 선택하세요. 기발한 은유나 중복 의미의 용어는 피하세요. 두 메서드가 비슷하게 들리면 호출자는 비슷하게 동작할 거라고 가정하므로, 그 가정을 맞게 만드세요.\n\n### 언제 여러 모듈/리소스로 분리할까\n\n다음 중 하나를 발견하면 분리하세요:\n\n- 서로 다른 사용자 역할(예: "admin" vs "consumer")이 다른 권한을 필요로 할 때\n- 변화 속도가 다른 부분이 함께 섞여 있을 때(한 부분은 자주 진화하고 다른 부분은 안정적이어야 할 때)\n\n모듈을 분리하면 핵심 약속은 유지하면서 내부를 진화시킬 수 있습니다. 성장 계획이 있다면 슬림한 "핵심" 패키지와 애드온을 고려하세요; /blog/evolving-apis-without-breaking-users 도 참고하세요.\n\n## 사용자를 깨뜨리지 않고 API 진화시키기\n\nAPI는 좀처럼 정지해 있지 않습니다. 새로운 기능이 오고, 엣지 케이스가 발견되고, "작은 개선"이 실제 애플리케이션을 조용히 깨뜨릴 수 있습니다. 목표는 인터페이스를 고정시키는 것이 아니라, 사용자가 이미 의존하는 약속을 위반하지 않고 진화시키는 것입니다.\n\n### 실용적 관점의 시맨틱 버저닝\n\n시맨틱 버저닝은 소통 도구입니다:\n\n- 파괴적 변경을 했음.\n- 하위호환성 있게 기능 추가.\n- 의도된 동작을 변경하지 않는 버그 수정.\n\n한계: 여전히 판단이 필요합니다. "버그 수정"이 호출자가 의존하던 행동을 바꾼다면, 그건 실무에서는 파괴적 변경입니다—비록 그 전 동작이 우연히 생긴 것이라도요.\n\n### 파괴적 변경은 타입뿐 아니라 계약에 관한 것이다\n\n많은 파괴적 변경이 컴파일러로 잡히지 않습니다:\n\n- 입력 규칙을 더 엄격하게 함(이전에 허용하던 값들을 거부).\n- 의미 변경(같은 필드지만 다른 해석).\n- 타이밍 변경(빠르던 호출이 느려지거나 블로킹됨).\n- 오류 동작 변경(새 오류 코드, 다른 재시도, 부분 결과 변경).\n\n사전조건과 사후조건 관점에서 생각하세요: 호출자가 무엇을 제공해야 하고, 무엇을 받을 수 있는가.\n\n### 사용자가 실제로 따라올 수 있는 사용중단(deprecation) 경로\n\n사용중단은 명시적이고 기한이 있어야 작동합니다:\n\n- 문서와 응답(경고, 헤더, 로그)에 오래된 동작을 사용중단으로 표시.\n- 을 제공(옛것과 새것을 함께 지원).\n- 명확한 일정 게시(예: "60일 후 기본값 변경, 180일 후 제거").\n\n### 추상화가 진화를 쉽게 만드는 방법\n\n리스코프 스타일의 데이터 추상화는 사용자가 의존할 수 있는 범위를 좁혀주기 때문에 진화를 쉽게 만듭니다. 호출자가 인터페이스 계약에만 의존하면 스토리지 포맷이나 알고리즘, 최적화를 자유롭게 바꿀 수 있습니다.\n\n실무에서는 강력한 도구가 도움이 됩니다. 예를 들어 내부 API를 빠르게 반복하면서 React 웹 앱이나 Go + PostgreSQL 백엔드를 구축할 때, 같은 워크플로우는 구현 속도를 높여주지만 핵심 규율은 변하지 않습니다: 여전히 명확한 계약, 안정적 식별자, 하위호환성 있는 진화가 필요합니다. 속도는 곱셈 인자이므로 올바른 인터페이스 습관에 곱해질 가치가 있습니다.\n\n## 오류 처리와 실패 모드: 예측 가능하도록 설계하라\n\n신뢰할 수 있는 API는 실패하지 않는 API가 아니라, 호출자가 이해하고 처리하고 테스트할 수 있는 방식으로 실패하는 API입니다. 오류 처리는 추상화의 일부입니다: "올바른 사용"의 의미와 세상(네트워크, 디스크, 권한, 시간)이 다를 때 무슨 일이 나는지를 정의합니다.\n\n### 프로그래머 오류 vs 런타임 실패\n\n먼저 두 범주를 구분하세요:\n\n- 호출자가 계약을 위반한 경우(잘못된 ID 형식, 순서가 맞지 않는 메서드 호출, 필수 필드 누락). 이는 조기에 그리고 크게 잡아내야 합니다—종종 직접적인 검증 오류로.\n- 호출자가 계약을 지켰지만 외부 요인이 실패한 경우(타임아웃, 종속 서비스 비가용, 할당량 초과, 동시성 충돌). 이는 표현 가능하고 복구 가능해야 합니다.\n\n이 구분은 인터페이스를 정직하게 만듭니다: 호출자는 코드 수준에서 고칠 수 있는 것과 런타임에서 처리해야 할 것을 배우게 됩니다.\n\n### 계약을 사용해 적절한 실패 형태 선택하기\n\n계약은 실패 표현 방식을 암시해야 합니다:\n\n- **검증 오류(Validation responses)**는 계약 위반에 사용.\n- **예외(Exceptions)**는 라이브러리의 비지역적 예외 상황이나 모든 호출 지점에서 분기 처리를 강제하기 어려울 때 사용.\n- **결과 타입(예: )**은 실패가 기대되고 호출자가 명시적으로 처리하길 원할 때 사용.\n\n무엇을 선택하든 API 전반에 일관되게 적용해 호출자가 추측하지 않게 하세요.\n\n### 실패 모드를 명시적이고 테스트 가능하게 만들기\n\n운영 의미로 가능한 실패를 나열하세요: "버전이 오래되어 충돌 발생", "찾을 수 없음", "권한 없음", "레이트 리미트" 등. 안정적 오류 코드와 구조화된 필드를 제공해 테스트가 문자열 매칭에 의존하지 않게 하세요.\n\n### 재시도, 멱등성, 부분 성공\n\n연산이 , 어떤 조건에서 그런지, 을 어떻게 보장하는지(멱등성 키, 자연 요청 ID)를 문서화하세요. 부분 성공이 가능한 배치 연산의 경우 성공/실패가 어떻게 보고되는지, 타임아웃 후 호출자가 어떤 상태를 가정해야 하는지 정의하세요.\n\n## 추상화 테스트: 인터페이스가 약속을 지키는지 증명하라\n\n추상화는 약속입니다: "유효한 입력으로 이 연산들을 호출하면 이런 결과가 나오고 이 규칙들이 항상 유지된다." 테스트는 코드가 변할 때 그 약속을 지키게 하는 방법입니다.\n\n### 계약을 단위 및 통합 테스트로 전환하기\n\n계약을 자동으로 실행할 수 있는 검사로 바꾸세요.\n\n단위 테스트는 각 연산의 사후조건과 엣지 케이스를 검증해야 합니다: 반환값, 상태 변경, 오류 동작. 인터페이스가 "존재하지 않는 항목 삭제는 를 반환하고 아무 것도 변경하지 않는다"고 말한다면 그걸 그대로 테스트하세요.\n\n통합 테스트는 실제 경계(데이터베이스, 네트워크, 직렬화, 인증)를 통한 계약을 확인해야 합니다. 많은 계약 위반은 타입이 인코딩/디코딩되거나 재시도/타임아웃이 발생할 때만 드러납니다.\n\n### 불변식을 위한 프로퍼티 기반 테스트\n\n불변식은 유효한 연산 순서에서도 항상 참이어야 하는 규칙입니다(예: "잔액은 절대 음수가 되지 않는다", "ID는 유일하다", "로 반환된 항목은 로 조회 가능하다").\n\n프로퍼티 기반 테스트는 많은 무작위지만 유효한 입력과 연산 시퀀스를 생성해 반례를 찾습니다. 개념적으로 당신은 "사용자가 어떤 순서로 이 메서드를 호출하든 불변식은 유지된다"고 주장하는 것입니다. 사람이 생각하지 못한 이상한 코너 케이스를 찾는 데 특히 유용합니다.\n\n### 퍼블릭 API를 위한 소비자 주도 계약 테스트\n\n퍼블릭 또는 공유 API의 경우 소비자가 사용하는 요청/응답 예시를 게시하게 하세요. 제공자는 CI에서 이 계약을 실행해 변경이 실제 사용을 깨뜨리지 않음을 확인할 수 있습니다—제공자 팀이 그 사용을 예상하지 못했더라도요.\n\n### 운영에서 계약 이탈(contract drift) 모니터링\n\n테스트로 모든 것을 커버할 수 없으니 계약이 바뀌고 있다는 신호를 모니터링하세요: 응답 형태 변화, 4xx/5xx 비율 증가, 새로운 오류 코드, 지연 증가, "알 수 없는 필드"나 역직렬화 실패 등. 엔드포인트와 버전별로 추적해 이탈을 조기에 감지하고 안전하게 롤백하세요.\n\n스냅샷이나 롤백을 지원하는 배포 파이프라인이 있다면 이 사고 방식과 잘 맞습니다: 이탈을 조기에 감지하고, 클라이언트가 중간에 적응하도록 강요하지 않고 되돌리세요. (예: Koder.ai는 스냅샷과 롤백을 워크플로우의 일부로 포함해 "먼저 계약, 그 다음 변경" 접근과 잘 맞습니다.)\n\n## 흔한 안티패턴과 회피 방법\n\n추상화를 중시하는 팀도 순간적 실용성 때문에 다음과 같은 패턴에 빠지기 쉽습니다. 당장은 편해 보여도 시간이 지나면 API를 특수 사례의 다발로 만듭니다. 반복되는 함정과 대처법을 소개합니다.\n\n### 영구적인 기능 플래그를 API 조작자로 사용하기\n\n기능 플래그는 롤아웃에 유용하지만, 플래그가 공개적이고 장기화된 파라미터가 되면 문제가 시작됩니다: , , 같은 것들. 시간이 지나면 호출자들이 조합을 만들어 예상치 못한 방식으로 사용하고, 결국 여러 동작을 영구적으로 지원하게 됩니다.\n\n더 안전한 접근법:\n\n- 가능하면 롤아웃 플래그는 내부에만 두세요.\n- 동작이 달라져야 한다면 명확한 이름과 수명주기(그리고 오래된 것 제거 계획)를 가진 새로운 기능으로 표현하세요.\n- 어떤 조합이 유효한지 문서화하고, 나머지는 명시적으로 거부하세요.\n\n### 데이터베이스 개념을 인터페이스로 유출하기\n\n테이블 ID, 조인 키, "SQL형" 필터()를 노출하면 클라이언트가 저장 모델을 배워야 합니다. 그러면 리팩터링 비용이 커집니다.\n\n대신 도메인 개념과 안정적 식별자를 모델링하세요. 클라이언트는 "어떻게 저장되었는가"가 아니라 "무엇을 얻고 싶은가"를 묻게 하세요(예: "특정 고객의 기간 내 주문").\n\n### 그냥 필드를 추가하는 습관\n\n필드 하나를 추가하는 것은 무해해 보입니다. 하지만 반복된 "한 필드만 더" 변경은 책임을 흐려지고 불변식을 약화시킵니다. 클라이언트는 우연한 세부사항에 의존하기 시작합니다.\n\n장기 비용을 피하려면:\n\n- 새 개념에 대해 새로운 집중된 타입을 도입하세요.\n- 관련 필드를 명확한 의미의 중첩 객체로 그룹화하세요.\n- 모든 추가를 계약 변경으로 취급하세요: 이것이 무엇을 의미하고 무엇이 항상 참이어야 하는지 고민하세요.\n\n### 추상화가 너무 엄격해지는 경우\n\n과도한 추상화는 실제 요구를 막을 수 있습니다—예: "이전 커서 이후로 시작"을 표현할 수 없는 페이지네이션, "정확히 일치"를 지정할 수 없는 검색 엔드포인트. 클라이언트는 우회 방법을 쓰게 되고(여러 호출, 로컬 필터링), 성능과 오류는 더 악화됩니다.\n\n해결책은 제어된 유연성입니다: 열린 방주(hatch)식 탈출구가 아니라(예: 지원되는 필터 연산자 소수 제공), 잘 정의된 확장 지점을 제공하세요.\n\n### 기능은 유지하면서 단순화하기\n\n단순화가 곧 기능 포기를 의미할 필요는 없습니다. 혼란스러운 옵션을 사용중단으로 바꾸되, 기본 기능은 더 명확한 형태로 유지하세요: 중복되는 여러 파라미터를 하나의 구조화된 요청 객체로 바꾸거나, "모두 처리" 엔드포인트를 두 개의 응집된 엔드포인트로 분리하세요. 그런 다음 버전된 문서와 명확한 사용중단 일정을 통해 마이그레이션을 안내하세요(참고: /blog/evolving-apis-without-breaking-users).\n\n## 신뢰할 수 있는 API 설계를 위한 실용 체크리스트\n\n리스코프의 데이터 추상화 아이디어를 간단하고 반복 가능한 체크리스트로 적용할 수 있습니다. 목표는 완벽이 아니라 API의 약속을 명시적이고, 테스트 가능하며, 안전하게 진화시킬 수 있게 만드는 것입니다.\n\n### 짧은 체크리스트\n\n- 데이터나 리소스에 대해 항상 참이어야 할 것은 무엇인가?(예: "잔액은 음수가 되지 않음", "ID는 유일함", "항목은 안정적 순서로 반환").\n- 각 연산에 대해 , , (무엇이 포함)을 작성하세요.\n- 의도적으로 비공개인 세부사항(스토리지 포맷, 캐싱, 내부 ID)을 나열하고 호출자가 그것들에 의존하지 못하게 하세요.\n- 기능을 어떻게 추가할지 결정하세요: 버전 전략, 사용중단 정책, 오래된 동작 지원 기간 등.\n\n### 빠른 API 리뷰 워크플로우(반복 가능)\n\n1. 새 팀원이 동작을 예측할 수 있는가?\n2. 정상 케이스, 빈 결과 케이스, 경계 케이스, 잘못된 입력 케이스, 실패 케이스.\n3. 여러 구현이 있다면 하나를 다른 것으로 교체해도 호출자가 놀라지 않을까?\n4. 클라이언트가 내부 상태, 타이밍, 스토리지 세부사항을 알도록 강제하는가?\n5. 목록을 없애거나(또는 의식적으로 수용될 때까지) 다시 설계하세요.\n\n### 문서 템플릿(복사/붙여넣기 가능)\n\n짧고 일관된 블록을 사용하세요:\n\n- \n- 및 계정 존재\n- 잔액은 원자적으로 업데이트; 총합 보존\n- , , \n- 멱등성, 정렬, 성능 기대치\n\n### 참고 읽기\n\n더 깊이 들어가고 싶다면 , , 그리고 를 찾아보세요.\n\n팀 내부 노트를 유지한다면 /docs/api-guidelines 같은 페이지에 링크해 리뷰 워크플로우를 쉽게 재사용할 수 있게 하세요—수동으로든 채팅 기반 빌더(예: Koder.ai)를 사용하든 새로운 서비스를 빠르게 만들 때 이러한 가이드라인을 비협상적 규율로 삼으세요. 신뢰할 수 있는 인터페이스가 속도가 복리로 작용하도록 만듭니다.
그녀는 데이터 추상화와 정보 은닉을 대중화했으며, 이는 현대 API 설계에 직접적으로 적용됩니다: 작은 안정된 계약을 공개하고 구현은 유연하게 유지하세요. 실용적 효과는 명확합니다: 파괴적 변경 감소, 안전한 리팩터링, 예측 가능한 통합입니다.
호출자가 시간 경과에 따라 의존할 수 있는 API입니다:
신뢰성은 "절대 실패하지 않음"이 아니라 예측 가능한 방식으로 실패하고 계약을 지키는 것에 가깝습니다.
동작을 계약으로 작성하세요:
빈 결과, 중복, 정렬 등 엣지 케이스도 포함해 호출자가 약속을 구현하고 테스트할 수 있게 하세요.
불변식(invariant)은 추상화 내부에서 항상 참이어야 하는 규칙입니다(예: “수량은 음수가 될 수 없다”). API는 경계에서 불변식을 강제해야 합니다:
normalize() 같은 특별한 의식을 호출해야만 동작하는 요구는 피하세요.이렇게 하면 시스템 전반에서 "불가능한 상태"를 처리하느라 발생하는 버그가 줄어듭니다.
정보 은닉은 연산과 의미만 노출하고 내부 표현은 숨기라는 뜻입니다. 나중에 바꿀 가능성이 있는 것들(테이블, 캐시, 샤드 키, 내부 상태)을 소비자가 알면 리팩터링이 어려워집니다.
실용적인 방법:
usr_...) 사용. 데이터베이스 행 번호 사용 금지.status=3 금지).내부 개념에 클라이언트가 의존하면 구현이 얼어붙습니다. 클라이언트가 테이블 ID, 조인 키, SQL형 필터에 의존하면 스키마 변경이 곧 API 깨짐으로 이어집니다.
대신 도메인 질의를 모델링하세요(예: "특정 고객의 기간 내 주문")—저장 방식은 계약 뒤에 숨기세요.
LSP는 한 문장으로: 어떤 코드가 인터페이스로 동작한다면, 그 인터페이스의 어떤 유효한 구현으로 바꿔도 특별한 처리가 필요 없어야 한다는 뜻입니다. API 관점에서는 호출자를 놀라게 하지 말라는 규칙입니다.
대체 가능한 구현을 지원하려면 다음을 표준화하세요:
다음에 주의하세요:
구현이 더 엄격한 제약을 필요로 하면 같은 인터페이스 뒤에 숨기지 말고 별도의 인터페이스나 명시적 기능 플래그로 표현하세요.
인터페이스는 작고 응집적이어야 합니다:
options: any나 많은 불리언으로 조합 가능한 파라미터를 피하세요.reserve, release, list, )을 사용하세요.오류를 계약의 일부로 설계하세요:
publish()publishDraft()reservereleasevalidatelistgetOk | Errorfalselist()get(id)?useNewPricing=truemode=legacyv2=truewhere=...transfer(from, to, amount)amount > 0InsufficientFundsAccountNotFoundTimeoutvalidate다른 역할이나 변화 속도가 다르면 모듈/리소스를 분리하세요 (진화 관련 글: /blog/evolving-apis-without-breaking-users).