멀티테넌트 전환으로 라이선스별 팀 데이터 격리 구현
목차
멀티테넌트 아키텍처로 팀 서버를 분리하며 라이선스 기반 데이터 격리를 구현했다. 초반엔 단순해 보이던 작업이 팀 리딩 관점에서 꽤 복잡한 설계 결정들을 담고 있었다.
왜 데이터 격리가 필요했나
처음엔 모든 고객사가 하나의 팀 서버 인스턴스를 공유하는 구조였다. 이렇게 되면 A 고객의 팀 데이터가 실수로 B 고객에게 노출되거나, 한 고객의 대용량 요청이 다른 고객의 성능을 좌우하는 문제가 생긴다. 특히 규정 준수(compliance) 관점에서 "고객별 데이터를 물리적/논리적으로 완전히 분리하라"는 요구사항이 커지고 있었고, 라이선스 레벨마다 접근 권한과 할당량도 달라져야 했다. 그래서 멀티테넌트 구조로의 전환이 피할 수 없는 결정이었다.
라이선스 기반 격리의 의미
"라이선스 고객" 개념을 primary key처럼 도입한 것이 핵심이다. 시스템 내 어떤 팀이나 리소스든 항상 "어느 라이선스에 속해 있는가"를 명시해야 하고, 모든 쿼리와 권한 체크가 그 정보를 기준으로 동작하게 했다. 단순히 "다른 테이블에 저장한다"는 의미가 아니라, 서버 인스턴스 자체가 특정 라이선스 컨텍스트 내에서만 움직이도록 구조화한 것.
| 개념 | 이전 (단일 테넌트) | 이후 (멀티 테넌트) |
|---|---|---|
| 팀 데이터 위치 | 하나의 DB에 모두 | 라이선스별 namespace/schema |
| 요청 처리 | 사용자 인증만 | 사용자 + 라이선스 컨텍스트 검증 |
| 성능 영향도 | 다른 고객과 경쟁 | 격리된 리소스 풀 |
| 규정 준수 | 논리적 분리만 | 물리적/논리적 완전 분리 |
파일 변경의 역할
polar-client.ts 는 아마 외부 인프라(결제/라이선스 관리 시스템 같은)와 통신하는 레이어일 텐데, 요청할 때마다 라이선스 ID를 헤더나 파라미터로 포함시키도록 수정했을 것 같다. 멀티테넌트 환경에선 클라이언트가 "나는 어느 고객인가"를 명확히 밝혀야 하기 때문이다.
server.ts 에선 팀 서버의 진입점에서 라이선스 컨텍스트를 먼저 검증하고 설정하는 미들웨어 같은 부분을 추가했을 거다. 들어오는 요청마다 "이 사용자가 속한 라이선스가 이 팀 서버에 접근할 권리가 있는가?"를 확인하는 로직.
team-usage.test.ts 의 테스트 추가/수정은 더 흥미롭다. 팀별 사용량 집계가 라이선스 경계를 넘지 않는지, 한 라이선스의 쿼터 초과가 다른 라이선스에 영향을 안 주는지 같은 격리 조건을 검증했을 것이다. 멀티테넌트 작업에서 테스트 케이스가 늘어나는 건 자연스러운 일인데, 격리가 제대로 되지 않은 edge case를 찾으려면 꽤 세밀한 시나리오 설계가 필요하다.
팀 관점: 설계 검증과 리뷰
이런 규모의 변경은 코드 리뷰만으로 부족하다.
- 데이터 격리 모델: 어느 테이블/필드에서 라이선스 ID를 unique key로 가져야 하는지, 인덱스는 어떻게 탈 것인지
- 마이그레이션 전략: 기존 고객 데이터를 어떻게 새 구조로 옮길 건지 (무중단 배포는?)
- 성능 테스트: 라이선스별 리소스 격리가 실제로 noisy neighbor 문제를 해결하는지
- 권한 모델: 고객이 여러 라이선스를 가지면 어떻게 switch하나? 동시 접근은?
이런 항목들을 설계 리뷰 단계에서 미리 체크리스트로 만들어 팀과 공유했다. 누군가 "그 부분 어떻게 했어?" 물어올 때 "여기 ADR(Architectural Decision Record) 참고"라고 지을 수 있게.
흔한 함정과 배운 점
멀티테넌트 작업에서 자주 발생하는 버그:
1. 권한 체크 빠뜨림: 특정 엔드포인트에서 라이선스 컨텍스트를 검증하지 않은 채 DB에 직접 접근
2. 쿼리 필터 누락: SELECT * FROM teams WHERE ... 할 때 AND license_id = ? 를 빼먹는 일
3. 테스트 데이터 오염: 테스트A가 라이선스X에 팀을 만들고, 테스트B가 같은 라이선스를 쓰면서 간섭
그래서 이번 작업에선 라이선스 컨텍스트를 request scope로 고정하는 방식을 택했다. 매 쿼리마다 라이선스 ID를 넘기는 대신, 요청 진입 시점에 한 번만 검증하고 내부적으로는 context object에서 자동으로 읽게 한 것. 이렇게 하면 실수로 라이선스 필터를 빠뜨릴 확률이 훨씬 낮다.
team-usage.test.ts 테스트들이 특히 중요했던 이유도 이것이다. 사용량 집계 같은 로직은 여러 테넌트 데이터를 합산할 위험이 있거든. 리포트 쿼리에서 GROUP BY를 제대로 그룹핑하지 않으면 의도 없이 데이터가 섞인다.
이 작업을 거쳐서 팀원들과 "격리 설계를 할 땐 쿼리 패턴부터 생각하기", "테스트에서 멀티 테넌트 시나리오를 먼저 쓰기" 같은 워크샵을 진행했다. 다음 비슷한 작업이 들어올 때 좀 더 빠르게 움직일 수 있게.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.