본문 바로가기
C.W.K.
Stream
Lesson 04 of 04 · published

클라이언트가 얇은 건 주인이 안 얇아서야

~13 min · contracts, ownership, api, architecture

Level 0흩어진 부품
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

발행이 실제로 뭘 포함하나

이 앱 중 하나가 기록해둔 메모를 공용 스트림에 발행할 때, 진짜 절차는 호출 하나가 아냐. 미디어는 글이 존재한 뒤에 붙어야 하니까 첨부가 있는 글은 먼저 초안으로 만들어지고 미디어가 내려앉은 다음에야 승격돼. 이미지는 나가는 사본에서 맞춰지고 다시 인코딩되고, 원본은 절대 안 건드려. 어떤 이미지 형식은 변환되고. 알려진 제공자의 영상 링크는 그 글의 영상이 되는데, 클립이 업로드 안 됐을 때만 그래. 첫 일반 링크는 미리보기 카드가 되고, 그 단계는 부드럽게 실패해야 해. 미디어 첨부가 실패하면 그 글은 지워지는 대신 들여다볼 수 있는 초안으로 세워지고, 응답이 그렇다고 말해.

그중 하나하나가 발행을 소유한 서비스의 결정이야. 그중 어느 것도 기록 앱들이 내릴 일이 아니고 — 더 나쁘게는, 서로 다르게 내릴 일이 아니야.

그래서 공용 클라이언트는 요청 하나를 보내

공용 발행 클라이언트는 일부러, 거의 실망스러울 만큼 작아. 멀티파트 요청을 만들고, 보내고, 봉투를 읽어. 절차 전부가 주인 쪽에서 실행돼. 이게 내려앉을 때 형제들 자기 이미지 맞춤 코드는 퇴역했어. 둘이 있었고 같은 일을 다른 방식으로 하고 있었고, 애초에 둘 다 없었어야 했으니까.

이게 경계 규율의 두 번째 절반이야. 지금까지 트랙 5 는 공용 저장소가 뭘 빨아들이면 안 되는지에 대한 거였어. 이건 뭘 다시 구현하면 안 되는지에 대한 거야. 이미 다른 데 주인이 있는 로직. 뚱뚱한 클라이언트는 세 겹을 없애는 대신 한 층 아래로 옮겼을 거야. 오케스트레이션 사본 셋이, 이제 공용 파일 하나 안에서, 인자로 갈라져서.

공용 로직을 뽑기 전에, 그게 라이브러리가 아니라 서비스 거인지 물어. 사본 셋을 공용 클라이언트 하나로 합치는 건 개선이야. 셋 다 소유 서비스가 요청 하나로 할 수 있는 걸 다시 구현하고 있었다는 걸 발견하는 건 더 큰 개선이고. 그 로직이 소비자들에서 아예 존재하기를 멈추거든. 한 번 존재하는 것보다 엄격히 나아. 라이브러리로 손 뻗는 본능은 이 질문을 건너뛸 만큼 세.

나눔의 나머지 절반은 코드가 아냐

얇은 클라이언트가 얇게 남으려면 보내는 모양이 양쪽 다 읽는 어딘가에 적혀 있어야 해. 이 가족은 아키텍처 문서 옆에 계약 문서를 둬. 발행 엔드포인트의 정확한 요청 모양, 응답 봉투랑 필드마다의 뜻, 발행 행위마다 덧붙는 정식 기록, 감정 어휘랑 그걸 어디서 가져오는지, 두뇌 이름 공간 둘이랑 그 사이 다리, 검색 가능한 형제마다 답하는 검색 봉투.

이건 코드가 아니라 합의야. 클라이언트가 네 줄인데도 맞을 수 있게 해주는 물건이고, 다섯 번째 형제를 다른 넷의 구현을 안 읽고도 추가할 수 있게 해주는 산출물이야. 공용 코드랑 공용 계약은 다른 기구야. 코드는 중복을 없애고, 계약은 조율할 필요를 없애. 이 크기의 가족은 둘 다 필요하고.

Code

클라이언트 전부, 그리고 일부러 안 담은 그 절차·python
def publish_crumb(base: str, *, body: str, emotion: str,
                  visibility: str = "public",
                  files: list[tuple[str, bytes, str]] | None = None) -> dict:
    """Send one multipart request and return the envelope.

    Everything the OWNER does, and this client must never attempt:
      - draft-first when media is present, promote after it lands
      - image fitting and format transcoding on outbound copies
      - provider-link -> video, but only when no clip was uploaded
      - first generic link -> preview card, fail-soft
      - on media failure: park an inspectable draft, do not delete
    """
    parts = [("body", body), ("emotion", emotion),
             ("visibility", visibility)]
    return post_multipart(f"{base}/api/stream/posts", parts, files or [])


# The envelope, which is a CONTRACT and not this client's invention:
#
#   { "ok": true,
#     "post_id": "...",
#     "visibility": "public",       # what actually LANDED
#     "media_copied": true,
#     "link_url": "...",
#     "detail": null }              # human-readable reason on failure
#
# ok:false means the post EXISTS, parked as a draft. Record what
# happened, not what was requested - which is why the lineage record
# carries both:

def build_publication_record(crumb_id: str, response: dict, *,
                             requested_visibility: str,
                             emotion: str, body: str) -> dict:
    return {
        "id": new_id(),
        "crumb_id": crumb_id,
        "post_id": response.get("post_id"),
        "visibility": response.get("visibility"),   # what landed
        "requested_visibility": requested_visibility, # what was asked
        "emotion": emotion,
        "body": body,                 # the EXACT text that left
        "media": response.get("image_names", []),   # copied filenames
        "detail": response.get("detail"),
        "published_at": utcnow(),
    }
    # The two visibility fields differ exactly when a media failure
    # parked a draft. Storing only one of them would make the record
    # a wish rather than a history.

External links

Exercise

네 클라이언트 둘 이상에 구현된 로직을 하나 찾아. 재시도 정책, 페이지네이션, 에러 정규화, 파일 전처리. 뽑아내기 전에 소유 서비스가 대신 할 수 있는지, 거기로 옮기는 값이 얼마인지 물어. 그다음 어느 쪽으로 정하든 결과 배선 계약을 문서로 적어.
Hint
로직이 주인 쪽 거라는 제일 센 신호는 클라이언트들이 그걸 두고 서로 안 맞는다는 거야. 서비스 하나에 대고 재시도 정책이 둘이면 최소 한 클라이언트는 그 서비스 동작에 대해 틀린 거고, 계속 틀려 있을 자리로 어느 클라이언트도 맞지 않아.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.