이번 UMC 데모데이를 하면서 로그인 구현 파트를 담당했다.
이번에 인증/인가에 대해 제대로 알고 완벽하게 구현을 해보자! 라는 생각으로 구현을 시작했다.
| 항목 | 설명 | 상태 |
|------|------|------|
| OAuth CSRF 보호 | state 파라미터로 요청 검증 (Redis 저장, 일회성 사용)
| 일회성 코드 교환 | 토큰이 URL에 노출되지 않음, getAndDelete()로 atomic 처리
| AT 블랙리스트 | 로그아웃 시 AT 즉시 무효화
| RT 재사용 공격 방지 | Redis 저장된 RT와 불일치 시 즉시 무효화
| RT 유효성 검증 | JWT 서명, 만료시간, Redis 존재 여부 3단계 검증
| SecureRandom 코드 생성 | 예측 불가능한 인증 코드
| 짧은 TTL | 인증 코드 5분, state 10분
| 에러 메시지 보호 | 내부 정보 대신 에러 코드만 노출
| 로그아웃 인증 필수 | 로그아웃 엔드포인트는 인증된 사용자만 접근
| JWT 예외 안전 처리 | validateToken()이 예외 대신 false 반환
| JWT 시크릿 키 검증 | @PostConstruct에서 최소 32바이트(256비트) 검증
| URL 인코딩 | redirect_uri 특수문자 처리
| Provider 응답 검증 | RestTemplate 응답 null 체크, access_token 유효성 확인
나름 이런 것들을 다 구현을 해 놓은 상태다.
물론 구현과정에서 문제가 많았다.
일단 우리는 구글로그인과 깃허브로그인을 지원을 한다.
벌써 우리 서비스 자체 jwt와 구글, 깃허브에서 제공하는 AT, RT 관리해야 할 것이 6개였다.
우리 서비스야 내가 컨트롤해서 TTL 설정 및 정책을 만들고 제어할 수 있지만 구글이나 깃허브 같이 소셜에서 넘겨주는 토큰들은
모든 것을 개발자가 제어할 수 없다.
각각의 서비스 마다 TTL도 다르고 깃허브는 Refresh Token Rotation 를 지원하여 새로운 AT를 받을 때마다 RT도 다시 발급한다.
물론 OAuth 2.0 표준에서 Refresh Token Rotation를 권장하고 있지만 이렇게 제공자마다 다르다는 것은 개발하는 입장에서 큰 산이다.
과연 우리가 로그인을 정확히 보안적인 문제 없이 완벽하게 할 수 있을까?
솔직히 난 자신 없다. 자신있다고 말하는 사람들은 아마 보안 전문가 수준의 높은 지식 및 구현 능력을 가졌을거다.
그게 아니라면 인증인가에 대해 구현하면서 깊은 고민을 해보지 않았을 확률이 더 높다고 본다.
그래서 난 굳이 우리가 직접 인증인가 시스템을 구축해야하나 라는 근본적인 의문을 가지게 되었다.
그리고 좋은 Saas 서비스를 하나 찾았다.
Clerk | Authentication and User Management
The easiest way to add authentication and user management to your application. Purpose-built for React, Next.js, Remix, and “The Modern Web”.
clerk.com
인증인가에 대한 99%를 외부 서비스에 맡기는 거다.
누군가는 이렇게 말할 수 있다. -> 너 인증인가 구현하기 싫어서 그런거 아니야?
물론 100% 틀렸다라고 할 수 없다.
하지만 나는 이미 인증인가를 대부분 구현을 해두고 나서도 과감하게 버리고 SaaS 서비스로 바꿔야겠다는 판단을 한 것에는 더 큰 이유들이
존재한다.
지금부터 내가 왜 이러한 판단을 했는지 설득을 해보겠다.
현재 내가 개발하고 있는 서비스를 기준으로 보겠다. 우리 서비스는 깃허브와 구글 로그인을 지원한다.
1. 이종 OAuth 제공자 간의 토큰 생명주기 파편화 (AT/RT 관리)
구글과 깃허브라는 두 개의 OAuth 제공자를 동시에 관리하는 것은 단순히 API 호출의 문제가 아니라, 각 제공자별로 상이한 토큰 생명주기 정책을 모두 개별적으로 대응해야 함을 의미한다.
구글(Google): 일반적으로 긴 유효기간을 가진 Refresh Token(RT)을 제공하며, 이를 통해 Access Token(AT)을 갱신하는 비교적 정형화된 흐름을 따른다.
깃허브(GitHub): 깃허브의 보안 정책은 훨씬 까다롭다. 특히 'Refresh Token Rotation' 정책으로 인해, 새로운 AT를 발급받을 때마다 기존 RT가 무효화되고 새로운 RT가 발급된다.
리스크: 만약 백엔드 서버에서 새로운 RT를 DB에 저장하는 과정에서 네트워크 오류나 트랜잭션 실패가 발생하면, 해당 유저의 깃허브 연동은 즉시 끊어지게 된다. 유저는 다시 로그인을 해야 하며, 비동기로 돌아가던 AI 분석 작업은 토큰 권한 오류로 인해 즉시 중단되는 치명적인 결함이 발생한다.
구현 부담: 두 매커니즘을 모두 지원하면서, 예외 상황 발생 시의 롤백 로직과 토큰 갱신 시점의 동시성 제어(Concurrency Control)를 백엔드 팀이 직접 구현하는 것은 리스크가 크다.
토큰 관리 책임의 외부 위임 (Delegation of Token Lifecycle)
GitHub RT 로테이션 대응: Clerk은 깃허브의 까다로운 토큰 로테이션 정책을 자체 보안 서버에서 관리한다. 우리 백엔드는 복잡한 갱신 로직 없이, Clerk Backend API 호출 한 번으로 "지금 당장 사용 가능한 최신 AT"를 즉시 획득할 수 있다.
통합 추상화: 구글과 깃허브의 서로 다른 토큰 만료 시간과 갱신 메커니즘을 고려할 필요가 없다. 백엔드 코드는 제공자에 관계없이 동일한 인터페이스로 인증 정보를 다룰 수 있어 코드 가독성과 안정성이 비약적으로 상승한다.
2. 단일 Redis 인스턴스 운용의 병목 현상
우리는 단일 redis 인스턴스를 사용한다. (정확히는 valkey) 엔터프라이즈 급에서는 세션용과 캐시용 Redis를 물리적으로 분리하지만, 현재의 리소스로는 단일 인스턴스에서 두 성격의 데이터를 모두 처리해야 한다.
물론 2개를 띄우면 좋지만 그만큼의 비용도 고려해야 하기에 이 정도의 서비스에서는 단일 인스턴스가 맞는 판단이라 생각한다.
데이터 만료 정책(Eviction Policy)의 모순:
세션 데이터: 유저의 로그인 상태를 유지해야 하므로 메모리가 부족하더라도 절대 삭제되어서는 안 된다.
(noeviction 또는 특정 키 보호 필요)
캐시 데이터: AI 분석 결과나 매칭 메타데이터처럼 다시 계산 가능한 데이터는 메모리 확보를 위해 오래된 것부터 삭제되어야 한다.(allkeys-lru)
운영 및 모니터링의 불명확성: 세션과 캐시가 섞여 있으면 트래픽 급증 시 부하의 원인을 파악하기 어렵다. "로그인 요청이 많아서 메모리가 부족한 것인지, AI 분석 요청이 많아서인지"에 대한 모니터링이 불분명해지며, 이는 장애 대응 시간(MTTR)을 늦추는 결정적인 원인이 된다.
Redis의 순수 캐시 최적화
Clerk 도입으로 세션 관리 의무가 사라지면서, 단일 Redis 인스턴스를 다음과 같이 최적화할 수 있다.
공격적인 캐싱 전략: 메모리 부족 시 가장 오래된 데이터를 즉시 삭제하는 allkeys-lru 정책을 확정적으로 사용할 수 있다. 세션 유실에 대한 공포 없이 AI 리포트 데이터를 적극적으로 캐싱하여 서비스 응답 속도를 극대화한다.
장애 격리: Redis의 지표 상승은 곧 비즈니스 로직(AI 분석, 매칭 쿼리)의 부하를 의미하게 된다. 모니터링이 단순해지고, 장애 발생 시 원인 파악과 인프라 증설 여부 판단이 명확해진다.
임시 데이터의 안전한 처리: AI 분석 과정에서 발생하는 대규모 JSON 데이터를 Redis에 임시 저장하더라도, 세션 데이터와의 경합이 없으므로 데이터 오염 리스크가 0에 수렴한다.
3. PostgreSQL의 순도 유지 및 보안 (Password-less)
민감 정보 제로화: 우리 DB에는 패스워드 해시나 OAuth 토큰 자체가 저장되지 않는다. DB 설계 단계에서 인증 관련 테이블을 완전히 제거함으로써, 오로지 PM-개발자 매칭 도메인에만 집중된 깨끗한 ERD를 유지한다.
인가(Authorization)의 유연성: 유저의 역할(PM/DEV)은 우리 서비스 로직에 따라 언제든 변경될 수 있다. 인증(Identity)은 Clerk에 맡기되, 인가 정보는 우리 DB에서 관리함으로써 비즈니스 유연성과 보안성이라는 두 마리 토끼를 잡는다.
4. 코드 유지보수 부채의 최소화 (Maintenance & Payload)
직접 구현한 코드는 모두 '부채'이다.
자체 구현 시 인증 관련 보일러플레이트(Boilerplate) 코드가 백엔드 코드의 상당 부분을 차지하게 된다.
이는 추후 리팩토링이나 기능 변경 시 관리해야 할 코드 양을 늘리고 결합도를 높인다.
Clerk을 도입하면 인증 로직이 추상화되어 백엔드 코드가 훨씬 선언적(Declarative)이고 간결해진다.
'무엇을 검증하는가'만 남고 '어떻게 검증하는가'는 외부로 밀어냄으로써, 코드 가독성과 유지보수성을 극대화할 수 있다.
5. 유연한 피벗(Pivot)과 확장성
"만약 중도에 소셜 로그인 수단을 추가하거나(예: 카카오, 애플), 유저에게 MFA(2단계 인증) 기능을 제공해야 하는 상황이 온다면 자체 구현 시에는 아키텍처를 뒤흔들어야 할 수도 있다.
하지만 Clerk과 같은 SaaS를 쓰면 설정 딸깍 한 번으로 대응이 가능하다.
이러한 유연성은 불확실성이 높은 프로젝트 환경에서 백엔드 개발자가 가질 수 있는 가장 강력한 무기이다.
6. "비용이나 서비스 중단 리스크는요?"
Clerk이 유료화되거나 서비스가 중단되면 우리 프로젝트는 어떻게 되나요? 외부 서비스에 너무 의존적인 것 같습니다."
Clerk은 현재 **10,000 MAU(월간 활성 사용자)까지 무료이며, 소셜 로그인 기능도 포함되어 있어 우리 프로젝트 규모에는 차고 넘칩니다.
만약 서비스가 중단되더라도 Clerk은 표준 JWT 기반의 인증을 제공하므로, 유저 데이터 내보내기 기능을 통해 다른 OIDC(OpenID Connect) 제공자나 자체 DB로 이전할 수 있는 'Exit Plan'을 세울 수 있습니다.
초기에 보안 사고로 프로젝트가 무너지는 리스크보다 훨씬 통제 가능한 리스크입니다.
이외에도 자잘한 이유들이 더 많지만 큰 이유는 이정도로 정리할 수 있을거 같다.
7. 태클
누군가는 내 판단에 태클을 걸 수도 있다.
물론 태클은 좋은거다. 새로운 논쟁거리를 해결하면서 더 좋은 대안이 나올 수 있으니까
그 중에서 나올만한 공격들에 대해 내 나름 고민을 해서 답을 미리 만들어봤다.
"인증 구현 역량을 쌓지 못하는 것 아닌가?"
백엔드 엔지니어의 핵심 역량은 단순히 API를 연동하는 것이 아니라, 전체 아키텍처의 트레이드오프를 계산하여 최적의 솔루션을 선택하는 설계 능력에 있다고 생각한다.
2개월이라는 극한의 일정 속에서 리스크가 큰 인증 구현을 피하고, 대신 Redis 최적화와 비동기 보안 설계에 집중하는 것이 훨씬 더 실무적인 엔지니어링 경험이라 생각한다.
그리고 오히려 기본적인 로그인을 어느정도 해봤다면 오히려 잘 만든 서비스를 직접 이용해보고 플로우를 이해하는게 더 많은 공부가 되지 않을까? 라는 생각이다.
"외부 서비스 장애 시 대응은?"
Clerk은 글로벌 보안 표준을 준수하며 매우 높은 가용성을 보장한다.
자체 구현 시 발생할 수 있는 보안 취약점이나 토큰 갱신 버그로 인한 서비스 중단 확률보다, 전문 서비스의 장애 확률이 훨씬 낮다.
또한 JWKS 기반 로컬 검증을 통해 매 요청마다 Clerk 서버를 거치지 않으므로 성능 손실은 발생하지 않는다.
그리고 이건 모든 서비스가 마찬가지다.
얼마전 AWS 장애로 인해 리그오브레전드라는 게임 및 많은 AI 서비스들도 장애를 겪었다.
이런 큰 서비스들 조차 직접 구현하는 것보다 외부 서비스에 의존하는 것을 보면 외부 서비스가 자체 구현보다 더 안정적이다 라고 판단한 것이 아닐까 생각한다.
그리고 역으로 묻고 싶다.
우리가 구현하면 외부 서비스보다 장애 발생 확률이 더 적나요?
답은 되었을거라 생각한다.
"우리 DB에 유저 정보가 없으면 통계나 조인(Join)은 어떻게 하지??"
"유저 이메일이나 프로필 정보가 우리 DB에 없으면, 유저별 활동 통계를 내거나 조인 쿼리를 날릴 때 성능 문제가 생기지 않을까?"
그래서 우리는 '최소한의 동기화 전략'을 사용한다.
Clerk의 Webhook을 통해 유저 ID와 닉네임 정도의 최소 정보만 우리 DB(Member 테이블)에 저장한다.
민감한 인증 데이터(비밀번호, OAuth 토큰)는 Clerk에 맡겨 보안을 챙기고, 서비스 운영에 필요한 메타데이터만 우리 DB에 유지함으로써 보안과 조인 성능 두 마리 토끼를 다 잡을 수 있다.
"네트워크 호출 때문에 더 느려지지 않을까?"
"로그인 확인이나 유저 정보를 가져올 때마다 외부 API를 호출하면 레이턴시(Latency)가 발생하지 않나?"
인증 확인(Authentication)은 우리 서버가 가진 Clerk의 공개키로 로컬에서 즉시 검증하기 때문에 추가적인 네트워크 호출이 발생하지 않는다.
깃허브 토큰처럼 특별한 정보가 필요할 때만 API를 호출하며, 이마저도 필요한 시점에만 수행하도록 최적화할 계획이다. 직접 DB를 조회하는 것과 비교해도 유의미한 성능 차이는 없으면서 아키텍처는 훨씬 깔끔해진다.
8. 결론
인증인가는 서비스에서 굉장히 중요하다.
하지만 완벽하게 구현하는 것은 실제로 굉장히 어렵다.
아무리 물론 많은 시간을 투자하면 상당 수는 구현이 가능하겠지만 그마저도 과연 완벽할까 의문이다.
차라리 나는 내가 만들고자 하는 서비스에 좀 더 집중하는 것이 맞다고 생각한다.
항상 모든 개발은 비용과 시간을 고려해야 하고 기술적인 모험도 좋지만 더 큰 그림을 볼 줄도 알아야 한다고 생각한다.
그러한 관점에서 SaaS 도입은 훌륭한 대안이 될 수 있다고생각한다.
'tech > project' 카테고리의 다른 글
| [Auth] 로그인 화면 주도권을 누구에게 줄 것인가 (0) | 2026.01.19 |
|---|---|
| [Test] redis(valkey)도 Testcontainers 도입하기 (0) | 2026.01.17 |
| [Test] H2 대신 Testcontainers 도입하기 (0) | 2026.01.13 |
| [postgreSQL] varchar VS text 무엇이 더 좋을까 (0) | 2026.01.07 |
| [token] 토큰을 어떻게 저장하는 것이 좋을까? (1) | 2025.12.22 |