SaaS에서 서비스별 도메인 & API Key 기반 인증 시스템 도입하기(1) - 목표와 문제점들

장성호·2023년 12월 24일

[Server]

목록 보기
2/6
post-thumbnail

어제 부로 캡스톤디자인 최종보고서를 제출하면서 마지막 대학교 강의까지 모두 끝났다. 팀원들과 이런저런 이야기를 하며 보고서를 작성해보니까, 시도해보고 싶은 것과 아쉬운 점이 많이 생기었다. 그래서 조금씩 고도화 해보려고 한다. 일단 나 혼자지만 그래도 명확한 목적과 세세한 로드맵이 있으면, 누구라도 동참해주지 않을까 하는 마음이다.

"라이브 스트리밍 SaaS 플랫폼"이 주제였고, Webflux & Project Reactor, RTMP, Redis, OpenStack 등 처음 마주하는 기술이 너무 많았다. 그렇다보니 어떻게든 구현하는데 급급하긴 했지만, 그 과정에서 큰 폭으로 성장해가고 있다는게 정말 많이 느껴져서 좋았다. 그래서인지 이걸 약 4개월짜리 단기 프로젝트로 끝내기에는 너무 아깝다는 생각이 들었다.

재설계 목표

최종 보고서에 시스템 구성도 넣느라 이 그림을 준비한 팀원이, 뭐 이렇게 복잡한 프로젝트를 했냐면서 투덜거렸었다 ㅋㅋ 우리도 이 그림 보면서 화살표가 참 어지럽다고 생각했다. 이런저런 일이 많아서 어쩌다보니 서버들이 기능별로 분리되었는데 이런 결과로 이어졌다.

가장 먼저 하고 싶은 건 기왕 서버가 나뉘어진거 확실하게 서비스를 나누고, 각 서비스별 도메인 인증 시스템을 도입하는 것이다.

  1. 도메인 & API Key 기반 인증 시스템을 구축하기
  2. 사용자 / 라이브 스트리밍 / 채팅으로 각각 서비스를 나누기
  3. 나뉘어진 각 서비스는 독립적으로 도메인 & API key 기반 인증 시스템을 구축하기

이런 식으로 서비스 별 API 키를 분리시키려고 한다. 통일된 API Key를 사용하면 각 서비스가 인증 서비스에 강하게 의존해야만 하는 문제가 발생하기 때문이다.

현재 문제가 되는 부분

API Key 기반 인증 시스템

현재 "instream-sdk"라는 React 기반 UI Kit를 제공하는데, 해당 UI Kit는 API Key가 웹에 노출되는 문제가 있다. 따라서 API Key만으로는 보안이 불가능하다는 생각이 들었다.

아이디어를 얻은 곳은 카카오맵 SDK이다. 이 SDK를 사용하기 위해서는 Web 플랫폼에 사이트 도메인을 등록해야한다. 이렇게 도메인을 따로 등록받아 권한을 검증하면, API Key가 노출되더라도 보안을 유지할 수 있다고 생각했다.

복잡한 API Key 사용성

지금 서비스로는 기업이 수 많은 API Key를 관리해야하는 문제가 있다. 위 사진에서 2번 버튼을 누르면 어플리케이션을 추가로 생성할 수 있다. 근데 어플리케이션 별로 고유한 API Key가 있어서, 100개 어플리케이션이 있으면 API Key도 100개이다... 이렇게 설계했던 이유는 OBS Studio와의 연동 때문이었다.

OBS Studio로 방송해본 경험이 있는 팀원의 이야기로는 방송 서비스가 스트림 키로만 인증한다고 한다. 이 이야기를 듣고 만약 기업이 아프리카 TV나 트위치처럼, 개별 사용자한테 스트림 키를 지급해야한다면 스트림키 중복 문제가 발생할 수 있다고 생각했다. 스트림 키가 똑같으면 서비스 입장에서는 사용자를 구분할 수 없기 때문이다.

이 시나리오를 실제 시연해보니까, 기업 입장에서 수많은 API Key를 어플리케이션 별로 딱 맞게 관리하는게 너무 어렵다는 걸 깨달았다. OBS를 어떻게 튜닝 못하나... 라는 생각을 하던 도중, Nginx RTMP 공식 문서에서 답을 얻었다. tcurl이라는 속성에는 쿼리 파라미터를 포함한 전체 URL 문자열이 담기는 것이었다.

# nginx rtmp
application stream {
	live on;

	# publish 이벤트가 발생하면 tcurl과 name을 넘겨줌
	# tcurl은 rtmp://localhost:1935/live?id=application-id
	# name은 api-key
	exec_publish /bin/sh /app/exec_publish.sh $tcurl $name;
}
#!/bin/sh

tcurl=$1
name=$2

# '?' 문자를 기준으로 쿼리 파라미터 추출
query_params=$(echo $tcurl | awk -F'?' '{print $2}' | cut -d'/' -f1)

# UUID 정규 표현식
uuid_regex="[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"

# UUID 추출
application_id=$(echo "$query_params" | \
awk -F'&' '{
    for(i=1; i<=NF; i++) {
        split($i, kv, "=");
        if (kv[1] == "id") {
            print kv[2];
            exit;
        }
    }
}')

application_id=$(echo $application_id | grep -oE $uuid_regex)

이렇게 쿼리 파라미터를 통해 API Key 없이도 개별 어플리케이션을 파악할 수 있게 되었다. 여기까지는 성공했지만 캡스톤디자인 강의가 끝나서, API Key 단일화는 진행하지 못 했다.

DB 재설계

Monolithic 환경에서의 DB

도메인 기반 인증을 추가로 도입하기 위해서, DOMAINS라는 테이블을 추가로 만들었다.

  1. 라이브 스트리밍, 채팅 등 각 서비스 별 도메인 인증은 DOMAINS 테이블의 type 컬럼을 보고 인증한다.
  2. 전체 서비스 도메인 인증은 DOMAINS 테이블의 type 컬럼이 ALL인 데이터를 보고 인증한다.

이렇게 하고 보니까 전체 서비스, 개별 서비스 도메인 인증을 지원할 수는 있게 됐다.

MSA 환경에서의 DB

여기서 MSA 환경에서 개별 서비스가 자체적인 DB를 구축한다고 고려해봤다.

Monolithic 환경에서의 1번 기능을 똑같이 지원할 수 있다. 2번도 개별 서비스 DB에 똑같은 도메인 값을 넣으면, 전체 서비스 도메인 인증처럼 사용할 수 있다. (개인적으론 테이블 명이 더 직관적이어서 좋다.)

근데 이러면 "전체 서비스 인증을 위한 도메인 등록"이라는 요구사항을 처리하기가 정말 어렵다는 생각이 들었다. 해당 API는 누가 받을거고, 개별 서비스에서 트랜잭션을 하나로 어떻게 묶을지 문제가 되었다.

트랜잭션 통합 시나리오 검토

서비스끼리 HTTP API 통신으로 해결하기

제일 먼저 간단하게 생각할 수 있는 건, 서비스 끼리 HTTP API 통신이다. 장애가 발생한 서비스에서 등록된 도메인 데이터를 삭제해달라고 다른 서비스에 요청하는 것이다. 근데 이건 다음과 같은 문제점이 있었다.

서버 내에서 장애가 발생한 경우

이 경우에는 서버 내부에서 장애가 발생한 걸 알기 때문에, Exception이 발생했을 때 다른 서비스에 전파할 수 있다. 하지만 만약 채팅 서비스에서 도메인 등록 요청 API가 라이브 스트리밍의 도메인 삭제 API 요청보다 늦게 처리된다면, 결국 트랜잭션은 완벽하게 복구되지 않는 문제가 발생한다.

클라이언트의 API를 받지 못한 경우

이 경우에는 클라이언트 단에서 트랜잭션 복구가 처리되어야 하는데, 트랜잭션 복구 작업을 클라이언트한테 맡기는 건... 전적으로 클라이언트한테 의존하게 되므로, 서버 개발자의 책임 회피인 것 같았다.

결론

이 시나리오에서는 "서버 내에서 스스로 트랜잭션 복구가 처리될 것"이라는 요구사항을 만족하지 못 했다.

BFF 서버 (Backend For Frontend)

이번 캡스톤디자인에서 다른 팀의 소개로 알게 된 BFF 서버를 고려해봤다. 클라이언트의 API를 받지 못한 경우를 대비하기 위해서, BFF 서버를 앞단에 두고 해당 서버가 트랜잭션 관리를 하는 방법을 생각했다.

  1. 프론트가 BFF 서버에 요청을 보낸다.
  2. BFF 서버는 각 서비스에 도메인 등록 요청 API를 호출한다.
  3. 실패한 서비스가 있을 시, 각 서비스에 도메인 삭제 요청 API를 요청한다.
  4. API를 수신한 서비스는 해당 도메인 데이터가 등록되어 있다면 삭제한다.

이러면 서버 내에서 스스로 트랜잭션 복구 작업을 처리할 수 있었다. 프론트엔드에서도 개별 서비스에 요청을 보내는게 아니라, 단일 엔드 포인트로 추상화된 API를 사용할 수 있어서 좋아보였다.

다만 개별 서비스가 BFF 서버에 강하게 의존하는 상태가 됐고, BFF 서버는 SPOF가 되었다. 그리고 서비스가 늘어날 때마다 BFF 서버 내의 통합 트랜잭션 관리 로직을 유지보수해야하는 문제가 생겼다. 각 서비스는 독립적으로 도메인 & API key 기반 인증 시스템을 구축하려는 목표에 부합하지 않아보인다.

결론

이 시나리오에서는 다음과 같은 결론을 얻을 수 있었다.

  • 해결
    - 서버 내에서 스스로 트랜잭션 복구가 처리
  • 미해결
    - SPOF (BFF 서버)
    - 강한 의존성
  • 이점
    - 각 서비스를 추상화한 레이어 추가
    - 단일 엔드 포인트

Message Queue

우아콘 2023에서 보았던 Event-Driven Architecture랑 Message Queue 기반으로 하면 해결이 될까 고민해봤다.

  1. "HTTP API 통신으로 해결하기" 때처럼 프론트가 개별 서비스에 API 요청을 보낸다.
  2. 각 서비스는 Domain 등록 관련 이벤트를 발행한다.
  3. Domain 등록에 실패한 서비스는 롤백 이벤트를 발행한다.
  4. 롤백 이벤트를 수신한 다른 서비스는 트랜잭션을 롤백한다.

이렇게 하면 BFF 때처럼 트랜잭션을 통합할 수 있으면서, 이벤트만 잘 발행되면 각 서비스가 독립적으로 트랜잭션을 롤백할 수 있다. 의존성은 낮추었지만 BFF나 Kafka가 SPOF라는 문제는 아직 똑같다.

결론

이 시나리오에서는 다음과 같은 결론을 얻을 수 있었다.

  • 해결
    - 각 서비스 내에서 스스로 트랜잭션 복구 가능
    - 강한 의존성
  • 미해결
    - SPOF (Kafka)
    - 여러 엔드 포인트

분산 트랜잭션 관리

시나리오별 도식도를 그리면서 각 서비스별 API를 개별로 호출한다고 생각했다. 계속 생각해보니 서비스별로 트랜잭션이 독립적으로 진행되면 롤백 정책을 정하기 어렵다는 판단이 들었다. 만약 롤백 데이터 이벤트를 먼저 처리하고 도메인 등록 요청 API를 처리하면, 결국 트랜잭션 롤백에 실패한 것이었다.

Message Queue를 사용하는 분산 서비스에서 트랜잭션을 순차적으로 처리하는 방법을 찾다보니, Saga 패턴이라는 것을 찾아볼 수 있었다.

Saga 패턴을 이해한대로 적용해보았다.

  1. 전체 서비스 도메인 등록 API는 유저 서비스에서 받는다.
  2. 유저 서비스는 도메인 데이터 생성 이벤트를 발행하고, 이벤트 속성에서 생성 타입을 전체로 지정한다.
  3. 라이브 스트리밍 서비스는 유저 도메인 데이터 생성 이벤트를 받아, 도메인 데이터를 생성한 뒤 마찬가지로 이벤트를 발행한다.
  4. 채팅 서비스도 라이브 스트리밍 도메인 데이터 생성 이벤트를 받아, 도메인 데이터를 생성한다.

이렇게 서비스별로 나뉘어져있는 트랜잭션이지만, 논리적으로는 통합되어야할 때 트랜잭션을 순차적으로 적용시킬 수 있는 패턴이었다. 만약 실패했을 때는 다음과 같이 보상 트랜잭션을 적용한다.

  1. 전체 서비스 도메인 등록 API는 유저 서비스에서 받는다.
  2. 유저 서비스는 도메인 데이터 생성 이벤트를 발행하고, 이벤트 속성에서 생성 타입을 전체로 지정한다.
  3. 라이브 스트리밍 서비스는 유저 도메인 데이터 생성 이벤트를 받아, 도메인 데이터를 생성 도중 실패한다.
  4. 라이브 스트리밍 서비스는 유저 도메인 데이터 롤백 이벤트를 발행한다.
  5. 유저 서비스는 해당 이벤트를 받아 보상 트랜잭션을 적용한다.

분산 트랜잭션이어도 이렇게 순차적으로 트랜잭션을 처리할 수 있다보니, 채팅 서비스는 아예 트랜잭션을 진행하지 않은 모습이다.

이제 바꾸러 출발!

최종적으로 Message Queue + Saga 패턴을 적용해보려고 한다. 검토과정에서 다음과 같은 결론을 얻을 수 있었기 때문이다.

  • 해결
    - 각 서비스 내에서 스스로 트랜잭션 복구 가능
    - 강한 의존성
    - 단일 엔드 포인트 (전체 서비스 도메인 등록 한정)
  • 미해결
    - SPOF (Kafka)

Kafka SPOF에 대해선 자료를 찾아보니, Kafka Cluster를 사용하면 충분히 대응이 가능하다고 판단했다. 이건 BFF 서버도 Cluster를 사용하면 마찬가지이지만, Kafka는 상대적으로 구축 난이도가 더 쉬울 것이라고 생각했다. Cluster 상황을 미리 고려해서 구현되어 있기 때문이다.

일단은 처음부터 판을 너무 크게 벌리면 안 되니까, 목표를 다음처럼 작은 걸 하나 하나 이뤄보려고 한다.

  1. Monolithic 환경에서 Spring Security 활용해서 도메인 & API Key 인증 시스템 도입
  2. 유저 / 라이브 스트리밍 / 채팅 서비스를 분리
  3. React 같은 클라이언트 쪽에서 보상 트랜잭션 처리해보기
  4. Kafka 도입해서 Saga 패턴 적용

검증을 위해서 보상 트랜잭션을 유발할 수 있는 상황을 어떻게 만들 수 있을지 고민해봐야겠다.

profile
일벌리기 좋아하는 사람

1개의 댓글