입력값은 사용자가 입력한 문자열이 아니다

vx_developer·2026년 9월 9일

개발하다가

목록 보기
18/30
post-thumbnail

프로그래밍을 처음 배울 때 입력값은 보통 사용자가 키보드로 입력한 값이라고 배운다.

const userName = prompt(
  "이름을 입력하세요."
);

console.log(userName);

사용자가 문자열을 입력하면 프로그램이 그 값을 받아 처리한다.

입력과 출력이라는 기본 개념을 이해하기에는 충분한 설명이다.

하지만 실제 서비스를 개발하기 시작하면 입력값을 텍스트 필드의 문자열로만 생각해서는 부족하다.

서비스 외부에서 내부로 들어오는 모든 데이터가 입력값이 될 수 있기 때문이다.

  • 폼에 입력한 쿠폰 제목
  • URL에 포함된 쿠폰 ID
  • HTTP 헤더의 인증 토큰
  • 업로드한 이미지 파일
  • 모바일 앱이 보낸 위치 정보
  • 결제 서비스가 전송한 웹훅
  • 다른 API에서 받은 사용자 데이터
  • 운영자가 설정한 환경변수
  • 메시지 큐에서 읽은 이벤트
  • 데이터베이스에서 불러온 과거 데이터

이 값들은 모두 코드가 실행되기 전에 서비스 바깥에 존재했다.

따라서 실제 서비스에서 중요한 질문은 “어떻게 입력을 받는가?”에 그치지 않는다.

  • 이 값은 어디에서 왔는가?
  • 누가 이 값을 결정했는가?
  • 이 값을 얼마나 신뢰할 수 있는가?
  • 형태가 올바르다는 사실과 내용이 허용된다는 사실은 같은가?
  • 사용자가 직접 결정하면 안 되는 값은 무엇인가?
  • 이 값을 처리하기 전에 크기와 형식을 제한해야 하는가?
  • 실패했을 때 어떤 정보를 사용자에게 보여줘야 하는가?
  • 같은 입력이 다시 도착하면 어떤 일이 발생하는가?

실제 서비스에서 입력값은 사용자가 입력한 문자열이 아니라, 시스템의 신뢰 경계를 넘어 들어와 해석·검증·권한 확인이 필요한 모든 데이터다.


입력창 밖에서도 데이터는 계속 들어온다

쿠폰 생성 화면에 다음과 같은 폼이 있다고 생각해 보자.

<input name="title" />
<input name="remainingUses" />
<button type="submit">
  쿠폰 만들기
</button>

사용자가 입력한 제목과 사용 횟수는 명확한 입력값이다.

그러나 쿠폰 생성 요청 전체를 살펴보면 더 많은 입력이 존재한다.

POST /coupons?notify=true
Authorization: Bearer eyJ...
Content-Type: application/json

{
  "title": "Coffee Date",
  "recipientId": "user_123",
  "remainingUses": "3"
}

서버가 받아들이는 입력은 요청 본문만이 아니다.

입력 위치예시주요 판단
URL 경로/coupons/123ID 형식과 접근 권한
쿼리 문자열notify=true타입 변환과 허용 옵션
HTTP 헤더인증 토큰인증과 위조 가능성
요청 본문쿠폰 정보구조와 비즈니스 규칙
파일쿠폰 이미지크기, 형식, 실제 내용
네트워크 정보IP 주소프록시 환경과 신뢰 범위

인증된 사용자를 나타내는 값도 처음에는 요청 헤더에서 들어온 외부 데이터다.

const token =
  request.headers.authorization;

토큰을 단순히 읽었다고 사용자의 신원이 확인된 것은 아니다.

서명, 만료 시각, 발급자, 대상 서비스 등을 확인한 뒤에야 인증 정보로 사용할 수 있다.

const currentUser =
  await authenticate(token);

같은 데이터라도 처리 단계에 따라 의미와 신뢰 수준이 달라진다.


타입을 선언했다고 외부 데이터의 형태가 보장되지는 않는다

TypeScript에서는 요청 본문에 타입을 지정할 수 있다.

interface CreateCouponRequest {
  title: string;
  recipientId: string;
  remainingUses: number;
}

다음과 같이 요청을 사용하고 싶을 수 있다.

const input =
  request.body as CreateCouponRequest;

하지만 as는 실제 데이터를 검사하지 않는다.

클라이언트는 다음 요청을 보낼 수도 있다.

{
  "title": ["Coffee Date"],
  "recipientId": null,
  "remainingUses": "three"
}

TypeScript 타입은 개발 중 코드가 기대하는 형태를 설명한다.

네트워크를 통해 도착한 데이터가 실제로 그 형태인지 보장하지는 않는다.

따라서 신뢰 경계에서 들어오는 값은 먼저 unknown으로 보는 편이 현실적이다.

const requestBody: unknown =
  request.body;

그다음 검사와 변환을 통과한 값만 서비스 입력으로 만든다.

const input =
  parseCreateCouponRequest(
    requestBody
  );

이 코드는 단순한 타입 변환이 아니다.

“외부에서 도착한 값을 아직 신뢰하지 않는다”는 설계 판단을 표현한다.


입력값은 읽는 순간보다 신뢰할 수 있는 값으로 바뀌는 과정이 중요하다

쿠폰 생성 요청은 한 번에 비즈니스 로직으로 들어가지 않는다.

flowchart LR
    A[외부 입력]
    --> B[크기와 형식 제한]
    --> C[구조 해석]
    --> D[값 정규화]
    --> E[인증과 권한 확인]
    --> F[비즈니스 규칙 적용]
    --> G[데이터베이스 저장]

각 단계는 서로 다른 질문에 답한다.

구조를 해석한다

if (
  typeof body !== "object" ||
  body === null
) {
  throw new InvalidRequestError();
}

요청 본문이 객체로 처리할 수 있는 값인지 확인한다.

값을 정규화한다

const title =
  body.title.trim();

const remainingUses =
  Number(body.remainingUses);

외부 표현을 서비스가 일관되게 사용하는 형태로 바꾼다.

인증과 권한을 확인한다

const issuerId =
  request.currentUser.id;

쿠폰 발행자를 요청 본문에서 믿지 않고, 검증된 인증 정보에서 가져온다.

비즈니스 규칙을 적용한다

if (
  remainingUses < 1 ||
  remainingUses > 10
) {
  throw new CouponPolicyError(
    "쿠폰은 1회에서 10회까지 사용할 수 있다."
  );
}

숫자로 변환할 수 있다는 사실과 서비스에서 허용되는 숫자라는 사실을 구분한다.

입력을 안전하게 다룬다는 것은 하나의 if문을 추가하는 일이 아니다.

외부 표현이 내부에서 신뢰할 수 있는 서비스 데이터로 바뀌는 경로를 설계하는 일이다.


데이터의 출처는 값 자체만큼 중요하다

쿠폰 생성 요청에 issuerId가 포함되어 있다고 생각해 보자.

{
  "title": "Coffee Date",
  "issuerId": "admin_001",
  "recipientId": "user_123",
  "remainingUses": 3
}

서버가 이 값을 그대로 사용하면 사용자가 다른 사람의 ID로 쿠폰을 발행할 수 있다.

const coupon = {
  title: request.body.title,
  issuerId:
    request.body.issuerId,
  recipientId:
    request.body.recipientId,
};

문자열 형식이 올바르고 실제로 존재하는 사용자 ID여도 안전하지 않다.

문제는 값의 형태가 아니라 그 값을 결정할 권한이다.

발행자 ID는 검증된 로그인 정보에서 가져와야 한다.

const coupon = {
  title: input.title,
  issuerId:
    request.currentUser.id,
  recipientId:
    input.recipientId,
};

쿠폰 생성 시각도 클라이언트가 결정하게 해서는 안 된다.

const createdAt = new Date();

할인 금액 역시 서버가 상품 가격과 쿠폰 정책을 이용해 계산해야 한다.

const discountAmount =
  calculateDiscount(
    product.price,
    coupon.policy
  );

입력값을 설계할 때는 각 필드의 권한을 구분해야 한다.

적절한 출처
쿠폰 제목사용자 요청
수신자 ID사용자 요청 후 권한·존재 확인
발행자 ID인증된 사용자 정보
생성 시각서버 시계
초기 상태비즈니스 규칙
상품 가격데이터베이스
할인 금액서버 계산
관리자 권한인증·권한 시스템

외부 입력을 신뢰할 수 없다는 말은 모든 값이 거짓이라는 뜻이 아니다. 누가 그 값을 결정할 권한이 있는지 확인하기 전에는 서비스의 사실로 받아들일 수 없다는 뜻이다.


클라이언트가 보낸 계산 결과를 원본 데이터로 저장해서는 안 된다

쿠폰을 적용하는 주문 요청이 다음과 같다고 생각해 보자.

{
  "productId": "product_123",
  "couponId": "coupon_456",
  "originalPrice": 100,
  "discountAmount": 80,
  "finalPrice": 20
}

서버가 이 값을 그대로 저장하면 클라이언트가 결제 금액을 결정하게 된다.

await orderRepository.save({
  productId: input.productId,
  finalPrice: input.finalPrice,
});

화면에 표시된 값이 정상적으로 계산되었더라도 요청은 브라우저 개발자 도구나 별도의 프로그램으로 수정할 수 있다.

서버는 사용자가 선택한 대상만 입력으로 받고, 권한 있는 원본 데이터에서 결과를 다시 계산해야 한다.

const product =
  await productRepository.findById(
    input.productId
  );

const coupon =
  await couponRepository.findById(
    input.couponId
  );

const discountAmount =
  calculateDiscount(
    product.price,
    coupon
  );

const finalPrice =
  product.price - discountAmount;

여기서 원본 데이터는 데이터베이스에 저장된 상품 가격과 쿠폰 정책이다.

discountAmountfinalPrice는 그 원본으로부터 계산되는 값이다.

클라이언트가 계산된 값을 보내더라도 화면 확인이나 비교 목적으로만 사용할 수 있다.

서비스의 최종 판단을 맡겨서는 안 된다.


허용할 필드를 명시하지 않으면 내부 상태까지 입력이 된다

객체 전체를 데이터베이스 업데이트 함수에 전달하는 코드는 간단해 보인다.

await database.user.update({
  where: {
    id: currentUser.id,
  },
  data: request.body,
});

하지만 요청 본문에 예상하지 않은 필드가 포함될 수 있다.

{
  "displayName": "Chris",
  "role": "admin",
  "emailVerified": true
}

사용자가 변경할 수 있어야 하는 값은 displayName뿐인데 객체 전체를 전달하면 내부 관리 필드도 수정될 수 있다.

허용된 값만 명시적으로 선택해야 한다.

const profileUpdate = {
  displayName:
    parseDisplayName(
      request.body.displayName
    ),
};
await database.user.update({
  where: {
    id: currentUser.id,
  },
  data: profileUpdate,
});

이 방식은 코드가 조금 더 길다.

대신 외부 요청이 서비스 내부 모델의 모든 속성을 수정할 수 있는 통로가 되는 것을 막는다.

입력 객체와 데이터베이스 객체의 모양이 비슷하더라도 두 객체의 책임은 다르다.

  • 요청 객체는 외부에서 변경을 요청한 값이다.
  • 서비스 입력은 허용되고 해석된 값이다.
  • 데이터베이스 객체는 서비스가 저장하고 관리하는 전체 상태다.

이들을 하나의 타입으로 공유하면 외부 입력 범위와 내부 상태 범위가 섞일 수 있다.


데이터베이스에서 읽은 값도 항상 현재 규칙에 맞는 것은 아니다

데이터베이스는 서비스 내부에 있으므로 모든 값을 신뢰할 수 있다고 생각하기 쉽다.

그러나 저장된 데이터도 현재 코드가 기대하는 조건과 다를 수 있다.

  • 이전 버전의 코드가 다른 형식으로 저장했다.
  • 마이그레이션이 일부 데이터에 적용되지 않았다.
  • 운영자가 직접 데이터를 수정했다.
  • 다른 서비스가 같은 데이터베이스에 기록했다.
  • 과거 버그로 잘못된 값이 저장되었다.
  • 백업에서 오래된 데이터를 복원했다.
const coupon =
  await database.coupon.findUnique({
    where: {
      id: couponId,
    },
  });

조회에 성공했다는 사실은 쿠폰이 현재 비즈니스 규칙을 만족한다는 뜻이 아니다.

예를 들어 과거에는 remainingUsesnull일 수 있었지만 현재 코드는 숫자를 기대할 수 있다.

저장 경계에서 서비스 모델로 복원할 때 오래된 표현을 처리할 수 있다.

function toCoupon(
  row: CouponRow
): Coupon {
  return {
    id: row.id,
    title: row.title,
    remainingUses:
      row.remainingUses ?? 0,
  };
}

이 변환이 항상 기본값으로 오류를 숨겨야 한다는 뜻은 아니다.

복구할 수 없는 데이터라면 명확한 오류로 분류하고 운영자가 발견할 수 있도록 기록해야 한다.

핵심은 데이터베이스라는 위치만으로 신뢰 수준을 결정하지 않는 것이다.

데이터를 작성한 주체, 적용된 규칙의 버전, 변경 경로까지 함께 고려해야 한다.


다른 서비스의 응답도 외부 입력이다

결제 API나 회원 API가 반환한 값은 개발자가 직접 작성한 화면 입력보다 믿을 만해 보일 수 있다.

const recipient =
  await userApi.getUser(
    input.recipientId
  );

하지만 외부 서비스의 응답도 현재 서비스의 신뢰 경계를 넘어 들어온다.

  • 응답 형식이 변경될 수 있다.
  • 일부 필드가 누락될 수 있다.
  • 시간 초과 후 불완전한 응답이 도착할 수 있다.
  • 서로 다른 버전의 서버가 다른 형식을 반환할 수 있다.
  • 테스트 환경과 운영 환경의 데이터가 다를 수 있다.

따라서 외부 API 응답을 내부 모델로 바로 단정해서는 안 된다.

const response: unknown =
  await userApi.getUser(
    input.recipientId
  );

const recipient =
  parseRecipient(response);

parseRecipient는 외부 서비스의 표현을 현재 서비스가 사용하는 Recipient 개념으로 바꾼다.

interface Recipient {
  id: string;
  notificationAddress:
    string | null;
}

외부 서비스가 반환한 수십 개의 필드를 그대로 전달할 필요도 없다.

현재 책임에 필요한 값만 경계를 통과시키면 외부 API의 변경이 서비스 전체로 퍼지는 범위를 줄일 수 있다.


파일 입력은 이름과 확장자만으로 판단할 수 없다

쿠폰에 이미지를 첨부할 수 있다고 생각해 보자.

const image =
  request.files.couponImage;

파일 이름이 coupon.png라고 해서 실제 PNG 이미지라는 보장은 없다.

파일 입력에는 문자열과 다른 판단이 필요하다.

  • 파일 크기가 제한 안에 있는가?
  • 허용된 미디어 형식인가?
  • 실제 내용이 선언된 형식과 일치하는가?
  • 파일 이름을 저장 경로로 사용해도 되는가?
  • 압축을 풀었을 때 크기가 지나치게 커지지 않는가?
  • 처리 라이브러리가 안전하게 다룰 수 있는가?
  • 누구에게 파일 조회 권한이 있는가?

원본 파일 이름을 서버 경로로 직접 사용해서는 안 된다.

const storageKey =
  crypto.randomUUID();

서비스가 생성한 식별자를 저장 키로 사용하고, 원래 이름은 필요할 때 별도의 메타데이터로 보관한다.

또한 파일 검사는 업로드 후 부가적으로 실행하는 장식이 아니다.

저장 비용, 이미지 처리 비용, 악성 콘텐츠, 경로 조작 같은 문제를 막는 입력 경계의 일부다.


입력 크기도 계약의 일부다

문자열인지 확인하는 것만으로는 충분하지 않을 수 있다.

if (
  typeof input.title !== "string"
) {
  throw new InvalidTitleError();
}

수백 메가바이트의 문자열도 여전히 문자열이다.

배열에 들어 있는 모든 항목이 올바르더라도 항목이 수백만 개라면 서버 자원을 과도하게 사용할 수 있다.

if (
  input.title.length > 50
) {
  throw new InvalidTitleError();
}
if (
  input.recipientIds.length > 100
) {
  throw new TooManyRecipientsError();
}

입력 제한은 화면 디자인을 위한 규칙만이 아니다.

메모리 사용량, 데이터베이스 쿼리 수, 외부 API 호출 수, 처리 시간을 통제하는 운영 규칙이기도 하다.

대량 입력이 실제 요구사항이라면 제한을 없애는 대신 처리 방식을 바꿔야 한다.

  • 한 번에 처리할 크기를 나눈다.
  • 백그라운드 작업으로 전환한다.
  • 진행 상태를 별도로 관리한다.
  • 부분 실패와 재시도를 설계한다.
  • 사용자별 호출 빈도를 제한한다.

웹훅은 외부 시스템이 보내는 명령처럼 보여도 먼저 사실 여부를 확인해야 한다

결제 서비스가 다음과 같은 웹훅을 보낸다고 생각해 보자.

{
  "event": "payment.completed",
  "orderId": "order_123"
}

본문만 확인하고 주문을 결제 완료로 변경하면 공격자가 같은 형태의 요청을 직접 보낼 수 있다.

await orderRepository.markPaid(
  request.body.orderId
);

웹훅은 외부 시스템에서 온 입력이다.

처리 전에 발신자를 확인해야 한다.

verifyWebhookSignature({
  body: request.rawBody,
  signature:
    request.headers[
      "x-webhook-signature"
    ],
});

서명을 확인한 뒤에도 고려할 문제가 남는다.

같은 이벤트가 네트워크 재시도로 여러 번 도착할 수 있다.

if (
  await eventRepository.exists(
    event.id
  )
) {
  return;
}

웹훅 입력에서는 다음 내용을 함께 설계해야 한다.

  • 실제 발신자를 어떻게 확인하는가?
  • 서명 검증에 원본 요청 본문이 필요한가?
  • 이벤트가 중복 도착하면 안전한가?
  • 이벤트 순서가 바뀌어도 처리할 수 있는가?
  • 오래된 이벤트를 언제 거부하는가?
  • 처리 실패 시 외부 시스템이 재시도하는가?

입력값은 값의 형식뿐 아니라 전달 방식과 재전송 가능성까지 포함한다.


오류 응답도 입력을 다시 노출하지 않도록 설계해야 한다

입력 오류를 설명하기 위해 받은 값을 그대로 반환할 수 있다.

throw new Error(
  `잘못된 토큰: ${token}`
);

하지만 인증 토큰, 비밀번호, 결제 정보 같은 값이 오류 메시지나 로그에 남을 수 있다.

사용자에게는 수정에 필요한 정보만 제공해야 한다.

throw new AuthenticationError(
  "인증 정보가 올바르지 않다."
);

운영 로그에도 전체 입력을 무조건 기록해서는 안 된다.

logger.warn(
  "coupon_request_rejected",
  {
    userId:
      request.currentUser?.id,
    reason:
      "INVALID_REMAINING_USES",
  }
);

이 로그는 문제를 추적할 정보를 남기지만 요청 본문 전체를 복사하지 않는다.

입력 데이터를 기록할 때는 다음을 확인해야 한다.

  • 비밀번호나 토큰이 포함되어 있는가?
  • 개인 정보가 포함되어 있는가?
  • 결제 정보가 포함되어 있는가?
  • 값 전체 대신 길이, 타입, ID만 기록해도 되는가?
  • 보관 기간과 접근 권한이 적절한가?

입력을 안전하게 처리하는 책임은 값을 사용하는 순간에 끝나지 않는다.

오류, 로그, 분석 시스템으로 다시 전달되는 과정까지 이어진다.


입력을 받을 때 물어봐야 할 질문

출처와 신뢰

  1. 이 값은 사용자, 다른 서버, 데이터베이스 중 어디에서 왔는가?
  2. 누가 이 값을 결정할 권한을 가지는가?
  3. 인증이나 서명 검증을 통과한 값인가?
  4. 다른 시스템이 형식을 변경할 가능성이 있는가?
  5. 과거 버전의 데이터가 들어올 수 있는가?

형태와 크기

  1. 런타임에서 실제 타입을 확인했는가?
  2. 문자열의 길이와 배열의 개수를 제한했는가?
  3. 허용된 형식과 값의 범위를 정의했는가?
  4. 파일의 확장자가 아니라 실제 내용을 확인하는가?
  5. 지나치게 큰 입력이 시스템 자원을 고갈시킬 수 있는가?

권한과 소유권

  1. 사용자가 직접 결정해도 되는 필드인가?
  2. 서버가 인증 정보에서 가져와야 하는 값인가?
  3. 데이터베이스에서 조회해야 하는 원본 데이터인가?
  4. 서버가 다시 계산해야 하는 값인가?
  5. 현재 사용자가 대상 데이터에 행동할 권한이 있는가?

경계와 변환

  1. 외부 데이터를 unknown에서 시작하는가?
  2. 요청 객체와 내부 서비스 모델을 구분하는가?
  3. 외부 API나 데이터베이스 타입이 서비스 전체로 퍼지는가?
  4. 정규화가 어느 경계에서 한 번만 수행되는가?
  5. 처리된 값과 원본 입력을 혼동하고 있지는 않은가?

실패와 운영

  1. 잘못된 입력에 어떤 오류를 반환하는가?
  2. 오류 메시지가 민감한 입력을 노출하지 않는가?
  3. 로그에 요청 본문 전체를 기록하고 있지는 않은가?
  4. 같은 입력이 반복되면 안전하게 처리되는가?
  5. 부분 처리 후 실패했을 때 복구할 수 있는가?

흔히 하는 실수

TypeScript 타입 단언을 검증으로 생각한다

const input =
  request.body as CreateCouponRequest;

이 코드는 런타임 데이터를 검사하지 않는다.

const input =
  parseCreateCouponRequest(
    request.body
  );

신뢰 경계에서 실제 값의 구조를 확인해야 한다.


클라이언트가 보낸 사용자 ID를 신뢰한다

const issuerId =
  request.body.issuerId;

다른 사용자의 ID를 보낼 수 있다.

const issuerId =
  request.currentUser.id;

발행자 정보는 검증된 인증 결과에서 가져온다.


요청 객체를 그대로 데이터베이스에 전달한다

await database.user.update({
  data: request.body,
});

사용자가 수정하면 안 되는 내부 필드까지 입력이 될 수 있다.

await database.user.update({
  data: {
    displayName:
      parsed.displayName,
  },
});

허용된 필드를 명시적으로 선택한다.


클라이언트가 계산한 금액을 저장한다

const finalPrice =
  request.body.finalPrice;

클라이언트가 값을 변경할 수 있다.

const finalPrice =
  calculateFinalPrice(
    product,
    coupon
  );

권한 있는 원본 데이터에서 서버가 다시 계산한다.


파일 확장자만 확인한다

if (
  file.name.endsWith(".png")
) {
  save(file);
}

파일 이름은 실제 내용을 보장하지 않는다.

크기, 미디어 형식, 실제 콘텐츠, 저장 경로를 함께 검사해야 한다.


오류 로그에 입력 전체를 남긴다

logger.error({
  body: request.body,
  headers: request.headers,
});

토큰과 개인 정보가 로그에 저장될 수 있다.

logger.error({
  requestId,
  userId,
  errorCode,
});

문제 추적에 필요한 최소 정보만 기록한다.


데이터베이스 값은 항상 안전하다고 가정한다

const coupon =
  row as Coupon;

저장된 데이터가 이전 규칙이나 잘못된 수정의 영향을 받았을 수 있다.

const coupon =
  restoreCoupon(row);

저장 표현을 현재 서비스 모델로 복원하는 경계를 둔다.


핵심 정리

입력값은 프로그램 밖에서 안으로 들어오는 값이다.

처음 배울 때는 주로 키보드나 입력창에서 받은 문자열로 설명한다.

const couponTitle =
  prompt("쿠폰 제목");

그러나 실제 서비스에는 훨씬 다양한 입력 경계가 존재한다.

  • HTTP 요청 본문
  • URL과 쿼리 문자열
  • 인증 헤더
  • 파일 업로드
  • 외부 API 응답
  • 웹훅과 메시지
  • 환경 설정
  • 데이터베이스의 과거 데이터

입력값을 안전하게 처리하려면 단순히 값을 읽는 것보다 그 값이 서비스의 사실이 되는 과정을 설계해야 한다.

flowchart LR
    A[외부 데이터]
    --> B[형식과 크기 확인]
    --> C[해석과 정규화]
    --> D[인증과 권한 확인]
    --> E[비즈니스 규칙]
    --> F[신뢰 가능한 서비스 데이터]

좋은 입력 경계는 다음 내용을 분명하게 만든다.

  • 값이 어디에서 왔는가
  • 누가 값을 결정할 수 있는가
  • 어떤 형식과 크기를 허용하는가
  • 어떤 값은 서버가 다시 조회하거나 계산해야 하는가
  • 외부 표현을 어떤 내부 모델로 변환하는가
  • 실패와 중복 입력을 어떻게 처리하는가
  • 민감한 정보가 오류와 로그에 남지 않는가

외부 입력이 올바른 타입이라고 해서 신뢰할 수 있는 것은 아니다.

존재하는 사용자 ID라고 해서 요청자가 그 ID를 사용할 권한이 있는 것도 아니다.

클라이언트가 계산한 금액이 수학적으로 맞다고 해서 서비스의 결제 금액으로 인정해야 하는 것도 아니다.

입력값을 설계한다는 것은 결국 다음 질문에 답하는 일이다.

이 값은 어디에서 왔으며, 어떤 과정을 통과해야 서비스가 책임질 수 있는 사실이 되는가?

입력값은 사용자가 입력한 문자열이 아니다.

시스템의 신뢰 경계를 넘어 들어오는 데이터를 그 출처와 권한에 따라 해석하고, 서비스가 안전하게 사용할 수 있는 형태로 바꾸는 출발점이다.

profile
Vision eXperience Developer

0개의 댓글