대니
danny's blog@dannywon_dev
발행 145 · 대기 173

1인 개발자. 클로드 코드를 팀처럼 굴린다. 매일 겪은 것만 쓴다.

← 클로드 코드

함수 40개가 보던 테이블 이름을 distinct on 뷰로 남겼다

한 사람은 한 갤러리에만 — 유니크 제약을 개막 엿새 전에 풀어야 했다. 실제로 고친 함수는 6개였다.

아트페어 플랫폼을 운영한다. 갤러리가 로그인해서 부스와 작품을 등록하는 서비스다. 큰 페어 개막 엿새 전에 연락이 왔다. 운영팀 계정을 팀원으로 추가할 수 없다는 것이었다. 화면에는 이렇게 떴다.

다른 갤러리에 이미 등록된 이메일입니다. 그 갤러리에서 먼저 나와야 합니다

원인은 금방 나왔다. 소속 테이블에 한 사람은 한 갤러리에만 이라는 유니크 제약이 걸려 있었다. 이메일도 전체 유니크였다. 둘이 같이 막고 있었다.

제약이 실수가 아니었다

보통 이런 제약은 초기에 대충 잡아 놓고 잊은 것이기 마련이다. 그런데 테이블 주석에 이유가 적혀 있었다.

member_id 는 유니크라 한 사람은 한 갤러리에만 속한다 — 쓰기 함수들이 "내 갤러리"를 하나로 가정하기 때문이다.

세어 보니 DB 함수 40개가 이 테이블을 보고 있었고, 대부분이 같은 패턴이었다.

select exhibitor_id into v_gallery_id
  from exhibitor_accounts where member_id = auth.uid();

유니크만 풀면 이 문장들이 여러 행 중 아무거나 집는다. 에러도 안 난다. 엉뚱한 갤러리에 작품이 등록되고, 아무도 모른다. 개막 엿새 전에 가장 곤란한 종류의 고장이다.

첫 안을 로컬까지 적용한 뒤 버렸다

처음엔 40개를 다 고치는 그림을 그렸다. 엿새 전에 할 일이 아니었다.

그다음엔 소속 테이블에 is_active 칸을 두고 "활성인 것만 하나"로 유니크를 다시 거는 안을 만들었다. 마이그레이션까지 써서 로컬에 적용했다.

그런데 예외를 훑다가 걸렸다. 한 사람이 여러 갤러리에 미리 초대돼 있는 상태에서 가입하면, 가입 트리거가 이메일로 자리를 한꺼번에 채운다. 전부 활성으로 들어가면서 그 유니크가 터진다. 아직 가입하지 않은 사람을 초대해 두는 건 이 서비스에서 흔한 일이라, 언젠가가 아니라 곧 터질 문제였다.

설계를 갈아엎었다. 활성 표시를 소속 쪽이 아니라 사람 쪽으로 옮겼다.

alter table members add column active_exhibitor_id uuid;

사람당 하나뿐이니 충돌할 자리가 없다.

이름을 바꾸고 옛 이름을 뷰로 되살렸다

alter table exhibitor_accounts rename to exhibitor_memberships;

create view exhibitor_accounts with (security_invoker = true) as
  select distinct on (m.member_id) m.*
  from exhibitor_memberships m
  left join members mb on mb.id = m.member_id
  where m.member_id is not null
  order by m.member_id,
           (mb.active_exhibitor_id = m.exhibitor_id) desc nulls last,
           m.granted_at;

distinct on 이 한 사람당 반드시 한 행을 보장한다. 활성 지정이 있으면 그 갤러리를, 없으면 가장 먼저 들어간 곳을 준다.

그래서 옛 이름을 보고 있던 함수 40개와 앱 코드 6곳이 한 줄도 안 바뀌었다. 그것들 입장에서는 여전히 "내 갤러리는 하나"다. 실제로 고친 건 자리를 만들고·지우고·세는 함수 6개뿐이었다. 초대, 내보내기, 팀원 목록, 가입 승인 둘, 가입 트리거.

가르는 기준이 명확했다.

  • "지금 내 갤러리가 어디냐"를 묻는 줄 → 그대로 뷰를 본다. 한 행이 와야 맞다
  • "이 갤러리에 누가 있냐"를 묻거나 자리를 만드는 줄 → 실제 테이블을 본다

로컬을 되돌린 뒤 다시 돌렸다

되돌릴 수 없는 변경이라, 로컬을 원래대로 되돌린 뒤 마이그레이션을 통째로 다시 돌렸다. 손으로 적용한 상태 위에서 테스트하면 "마이그레이션이 실제로 도는가"를 확인한 게 아니라 "내가 손으로 만든 상태가 맞는가"를 확인한 것이 된다.

확인한 것들:

  • 다른 갤러리에 이미 있는 이메일 초대 → 뚫렸다
  • 같은 갤러리에 두 번 초대 → 여전히 막힌다
  • 갤러리 전환 → 뷰가 따라온다
  • 내가 속하지 않은 갤러리로 전환 시도 → 막힌다
  • 뷰에서 한 사람이 두 행을 갖는 경우 → 0건
  • 기존 사용자 10명 → 뷰 행수 = 사람 수, 영향 없음

남는 것

이름을 바꾸고 옛 이름을 뷰로 남기는 건 새로운 기법이 아니다. 다만 이번엔 어떤 가정을 살릴지가 먼저였다. "내 갤러리는 하나"라는 가정은 틀린 게 아니라 화면 한 번에 하나만 보면 되니까 여전히 참이었다. 틀린 건 그 가정을 저장 구조에 박아 둔 것이었다.

가정을 코드에서 걷어내는 대신, 그 가정이 계속 참이 되는 바닥을 깔아 주는 쪽이 쌌다. 40 대 6이었다.

← 목록으로