openlang

판별 유니온 전에 JSON 검증하기

타입 단언은 파서가 아니에요. 외부 응답은 바이트로 시작하고, 누군가 확인하기 전까지는 그게 어떤 변형인지 알려 주는 게 하나도 없어요.

텍스트 디코딩과 JSON 파싱이 먼저 오고, 둘 다 각자 실패할 수 있어요. 파서에서 나온 값은 unknown으로 검증 단계에 들어가야 해요.

지원하는 응답은 문자열 배열 items를 가진 ready 태그, 아니면 문자열 message를 가진 failed 태그예요. 런타임 검사는 세 가지를 확인해야 해요. 값이 객체인지, 태그가 아는 태그인지, 그 태그에 맞는 페이로드 모양을 갖췄는지요. ready인데 items가 없으면 불완전한 응답이에요. waiting이 오면 우리가 선언한 적 없는 태그고요. 유니언에 as를 붙여 봐야 둘 다 해결되지 않아요.

검사를 통과하면 디코더가 판별 유니언을 돌려줘요. ready를 확인하는 분기에서 값이 좁혀지고 항목을 쓸 수 있게 돼요. never로 하는 전수 검사는 선언된 모델에서 처리하지 않은 경우를 드러내 줘요. 유지보수에는 좋은 정보지만 응답 바이트를 들여다보지는 않아요.

거부된 입력은 어떻게 할지 정해야 해요. 디코딩 실패로 보고할지, 알 수 없는 응답을 일부러 변형 하나로 모델링할지요. 받아들인 유니언으로 슬쩍 캐스팅하면 그 결정을 건너뛰게 돼요.

런타임 근거는 타입이 붙은 값을 만드는 자리 바로 옆에 두세요. 그리고 핸들러마다 계산은 따로 리뷰해요. 분기를 다 덮어도 그 안의 계산은 틀릴 수 있으니까요.

이어지는 대화 5

서로의 근거를 따라
  1. 고유 링크

    Either는 결과가 가질 수 있는 형태를 기술합니다. 평가가 그중 하나를 실제로 내놓는지는 말하지 않습니다.

    Right _에 매칭해 True를 돌려주는 함수는 Right undefined를 받아도 페이로드를 건드리지 않고 통과시킵니다. 그 페이로드의 length를 물으면 그때는 값이 필요하고, 평가는 바로 거기서 실패합니다. let loop = loop in loop는 같은 타입을 가질 수 있지만 어느 생성자에도 도달하지 않습니다.

    그러니 입력 검사와 대안 표현을 나눈 구분 아래에, 하스켈에서는 층이 하나 더 있습니다. 디코더가 옳고 타입이 정확해도, 호출하는 쪽은 존재하지 않는 값을 요구할 수 있습니다. 평가가 거기까지 도달한다면 결과의 모양은 알 수 있습니다.

  2. 고유 링크

    Deferred(ticket)을 세 번째 경우로 넣고 변경이 어디까지 번지는지 따라가 볼게요. 닫힌 Scala 3 enum은 이 경우에 고유한 데이터를 줄 수 있어서 좋아요. 대신 디코더가 바뀌고, 렌더링이나 저장처럼 값을 쓰는 쪽도 전부 다시 봐야 해요. 모델 하나가 모든 경우를 쥐고 있을 때는 이런 조율이 값어치를 해요.

    독립된 생산자들이 그 중심 정의를 건드리지 않고 변형을 추가해야 한다면 얘기가 달라져요. 닫힌 집합이 비용이 되죠. 그때는 열린 인터페이스나, 명시적으로 검증하는 unknown 봉투가 더 맞을 수 있어요. 그만큼 중앙에서 얻는 전수 검사는 줄어들고요.

  3. 고유 링크

    ticket이 없다는 건 여기서 어떤 뜻인가요? 빠졌거나 null인 티켓은 형식이 깨진 응답일 수 있어요. 추적 번호 없이 받아들인 보류일 수도 있고요. Kotlin의 String?을 쓰면 값이 없을 수 있다는 건 드러나지만, 둘 중 어느 쪽인지는 여전히 말해 주지 않아요. 필드를 널 허용으로만 두면 Deferred(ticket)을 쓰는 곳마다 따로 판단하게 돼요. 이 케이스를 넣기 전에 경계는 명시해 두겠어요.

  4. 고유 링크

    이번 확장에서는 형식 오류로 봐요. deferred에는 ticket이 반드시 있어야 하고, 비어 있지 않은 문자열이어야 해요. 속성이 빠져도, null이어도, ""여도 디코딩에 실패해요. 이게 수락 규칙이고, 태그를 확인하는 바로 그 디코더 안에 있어야 해요.

    사람들이 흔히 원하는 빈 경우는 이미 자리가 있어요. items가 []인 ready는 요청이 끝났는데 항목이 없다는 뜻이에요. 그건 정상 결과예요. 티켓 없는 보류와는 다른 이야기라서 그 모양을 빌려 쓰면 안 돼요.

    나중에 서비스가 추적 없는 보류를 허용하게 되면, 그건 이름과 호출 쪽 안내를 따로 가진 새 대안이에요. 필드가 우연히 선택 사항이었다는 이유로 값이 없는 데서 Deferred를 슬쩍 만드는 건 피하고 싶어요. 프로토콜이 아직 바뀌는 중이라면 별도 변형은 넣기도 쉽고 빼기도 쉬워요.

  5. 고유 링크

    거절된 티켓은 아예 유니언까지 오지 않아도 될 것 같아요. 디코더가 Result<Response, ParseError>를 돌려주면 그런 값은 오류 쪽에 머물고, 호출하는 쪽은 Deferred나 빈 Ready를 만들지 않고 디코딩 오류를 보고할 수 있어요. 검증은 여전히 디코더가 다 해야 해요. 결과 타입은 실패가 갈 자리를 마련해 줄 뿐이에요.

같은 질문의 다른 갈래

  1. 읽기 전용 List<Int>는 스냅샷인가요?

    List<Int>는 읽기 전용 뷰예요. 스냅숏이 아니에요. 그 참조로 어떤 연산을 호출할 수 있는지 알려 줄 뿐, 뒤에 있는 컬렉션이 그대로 남는다고 약속하지는 않아요. val owned = mutableListOf(2, 4…

    답글 2kotlin