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

출처는 이름이 아니라 자기 설정이야

~13 min · identity, bugs, idempotency, data-modeling

Level 0미분류
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

성공했다고 보고하는 버그

어디에도 오류 메시지가 안 뜨는 실패 하나 보여줄게. 리더가 이미 싣고 있는 발행사의 두 번째 피드를 추가해. 톱 기사 섹션은 이미 있는데 세계 섹션을 넣는 거지. 인터페이스는 출처가 추가됐다고 확인해줘. 선반 연결도 생기고. 수집 라운드도 깨끗하게 돌아. 그리고 새 선반은 옛날 피드 기사로 영원히 채워져. 두 행이 같은 출처로 취급됐거든.

원인은 코드가 이름으로 중복 제거를 했다는 거야. 그리고 이 시스템에서 그 이름은 사람이 친 게 아니었어. 클라이언트가 신디케이션 출처 딱지를 호스트명에서 뽑아내니까, 한 호스트의 서로 다른 경로 둘이 같은 딱지를 만들어내. get-or-create 가 딱지를 맞춰보고, 기존 행을 찾고, 그걸 돌려주고, 성공했다고 보고했어. 어느 코드 경로 시점에서도 잘못된 게 없었지.

신원은 행동을 결정하는 쪽 거야

일반 원칙은 대놓고 적을 값어치가 있어. 피드 훨씬 너머까지 통하거든. 엔티티의 신원은 그게 뭘 하는지를 결정하는 바로 그거야. 출처가 뭘 하는지는 종류랑 설정이 전부 정해. 어느 엔드포인트인지, 어느 질의인지, 어느 로케일인지. 이름은 아무것도 안 정해. 사람용 딱지고, 딱지는 겹쳐도 되는 거야.

행동을 정하는 필드가 아니라 딱지로 중복 제거하면 딱 이 부류의 버그가 나와. 시스템은 둘을 갖고 있다고 믿는데 실은 하나인 조용한 별칭. 반대 실수도 무해하진 않다는 걸 짚어둘게. 아예 중복 제거를 안 하는 것 말이야. 설정이 같으면 진짜로 한 피드고, 선반마다 자기 행을 주면 같은 엔드포인트를 라운드마다 여러 번 가져오게 돼. 앞 강의가 얘기한 바로 그 무례함이지.

직렬화된 걸 말고 파싱한 걸 비교해

구현 세부 하나가 진짜 무게를 져. 설정은 보통 직렬화된 JSON 으로 저장되고, 저장된 문자열을 비교하고 싶어져. 하지 마. 키 순서만 다른 설정 둘은, 공백만 다르든 유니코드 이스케이프만 다르든, 같은 설정이야. 근데 직렬화 결과는 다르지. 양쪽을 파싱해서 나온 구조를 비교해. 그래야 신원이 어쩌다 그렇게 적혔는지가 아니라 설정 자체의 속성이 돼.

그다음에 딱지를 떼어놔

신원이 설정으로 옮겨가면 이름은 그냥 표시용 문자열이어도 돼. 그리고 우아하게 겹칠 수 있어야 하고. 딱지가 이미 쓰인 새 행은 접미사를 받아. 그게 다야. 딱지가 못생긴 건 딱지가 하중을 받는 것보다 훨씬 작은 문제야.

행동을 정하는 걸로 중복을 제거하고, 표시만 하는 건 겹치지 않게 떼어놔. 저 두 책임이 같은 필드에 얹히면 하나는 조용히 져. 그리고 지는 쪽은 행동 쪽이야. 표시 충돌은 눈에 보이고 행동 충돌은 안 보이니까.

Code

신원은 설정에, 구별은 딱지에·python
def _get_or_create_source(con, name, kind, cfg, now):
    """A source IS its (kind, config); the name is only a label.

    Matching on the label instead meant two different feeds from one
    host collided -- the client derives a name from the hostname, so
    adding a publisher's World feed after its Top feed silently bound
    the Top feed and reported success.
    """
    # Identity: parsed comparison, so key order never decides it.
    for row in con.execute(
        "SELECT id, config FROM sources WHERE kind=?", (kind,)
    ):
        if json.loads(row["config"] or "{}") == cfg:
            return int(row["id"])        # genuinely the same feed

    # Display: the label is allowed to collide, so de-collide it.
    label, suffix = name, 2
    while con.execute(
        "SELECT 1 FROM sources WHERE name=?", (label,)
    ).fetchone():
        label = f"{name}-{suffix}"
        suffix += 1

    cur = con.execute(
        "INSERT INTO sources(kind, name, config, created_at) VALUES(?,?,?,?)",
        (kind, label, json.dumps(cfg, ensure_ascii=False), now),
    )
    return int(cur.lastrowid)

External links

Exercise

네가 관리하는 코드에서 get-or-create 를 하나 찾아서, 조회가 어느 컬럼으로 매칭하는지 적어봐. 이제 그 컬럼이 엔티티 행동을 온전히 결정하는지 물어봐. 아니면, 별칭이 될 입력 둘을 만들어봐. 조회 키는 같고 행동은 다른 걸로. 그리고 함수가 뭘 돌려주는지 확인해. 호출한 쪽이 알아챌 수 있을지 판단해보고.
Hint
별칭 쌍은 보통 이미 네 데이터 안에 있어. 안 찾아봤을 뿐이지. 조회 컬럼으로 묶고, 각 묶음 안에서 행동을 정하는 컬럼들의 서로 다른 값이 몇 개인지 세봐. 하나보다 많은 묶음은 가정이 아니라 살아 있는 별칭이야.

Progress

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

댓글 0

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

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