[OAuth] 4개월 간의 통합회원 로그인 연동 및 프로젝트 회고
- -
* 상세 구현 내용은 모두 예시 값으로 변경하여 포스팅 작성하였습니다.
📝 프로젝트 배경
2024년 9월, 새로운 프로젝트가 시작되었다.
우리 회사 커머스에는 고**과 샵**라는 두 가지 주요 솔루션이 있다. (나는 고**개발팀에 속해있다)
기존에는 각 솔루션의 상점이 독립적으로 최고 운영자 데이터를 관리해왔지만, 시간이 지나면서 구조적인 한계가 드러났다. 운영 데이터를 종합적으로 관리하기 어려워지는 등 비효율적인 운영 문제가 발생하면서, 이를 해결하기 위해 회원 시스템을 통합 구조로 전환하는 장기 프로젝트가 시작되었다. 그 첫 단계로, 고**에서는 최고 운영자 통합회원 로그인 연동 작업이 시작되었다.🔥
이번 프로젝트는 단순히 기존 인증 서버를 가져와 활용하는 것이 아니라, 커머스만의 독자적인 인증/인가 시스템을 설계하고 구현해야 했다. 이 과정에서 OAuth 2.0 표준을 기반으로 인증 프로토콜이 도입되었다. 소셜 로그인을 구현한 사람들이라면 OAuth 2.0에 대해 한번쯤 들어보았을텐데, OAuth 2.0은 인증(Authentication, 사용자가 누구인지 확인)과 인가(Authorization, 사용자가 특정 자원에 접근할 수 있는 권한을 부여)를 명확히 구분하여 안전하고 확장성 있는 인증 구조를 구축할 수 있는 인증 프로토콜이다. 유관 부서와 긴밀한 협업을 지속하며 인증 및 재인증 정책, 토큰 검사과 갱신 처리, 보안 기준 등을 끊임없이 논의하고 검토하여 단순한 기술적인 구현에 그치지 않도록 하기 위해 노력했고, 덕분에 현재는 커머스의 운영 환경에 적합한 안정적인 인증/인가 시스템이 구축되었다. (사실 고**은 클라이언트의 역할을 수행했기 때문에, 설계를 주도하지는 않았지만 안정적인 개발을 위해 유관 부서와의 소통 및 Flow 설계 회의에 적극적으로 참여했다.)
이번 신규 프로젝트는 2024.09.~2024.12. 약 4개월 동안 진행되었다. (최초 이슈업은 7월이었으나, 내가 담당자로 배정 받은 시점은 9월말이었다.) 로그인이라는 중요한 기능의 업데이트인 만큼, 안정성을 목표로 신규 프로젝트에 온전히 집중하고 싶었으나, 아쉽게도 라이브 환경에서 발생하는 긴급한 이슈들을 병행으로 대응하며 개발해야 했다. 매일 출근할 때마다 처음 보는 새로운 이슈가 터져 있었고, 여러 채팅방에서 멘션이 걸리며 급박하게 Asap 건들을 대응하다보니 하루 대부분이 이슈 대응으로 지나가곤 했다. 오후 6~7시가 되어서야 프로젝트 개발을 할 수 있는 여유가 생기는 상황 속에서, 3개월 동안에는 저녁/밤/새벽/주말 동안 잠을 쪼개가며 개발을 진행해왔다. 프로젝트 중간에 정책 추가 검토 및 변경에 대한 회의도 많아지면서, 개발 마감 일정 준수에 대한 압박이 적지는 않았다..ㅎㅎ
농담이 아니구.. 10월~12월 다 합해서 평일에 6시간 이상 잔 날이 손에 꼽는다 🫠
특히 고** 서비스의 특수성(ㅌㄴ)까지 고려해야 했기 때문에 더욱 까다로운 과제였다. 또한, 2개의 기획팀과 5개의 개발팀이 협업하는 대형 프로젝트였기에 정책 수립, Flow 설계, 배포 후 고객 대응 방안 마련까지 철저히 검토하며 진행했다.
그리고 마침내, 12월 배포까지 성공적으로 마무리되었다!
이번 포스팅에서는 4개월 동안의 여정 속에서 마주했던 고민과 배운 점들에 대하여 적어보려고 한다.
그럼, 지금부터 시작해볼까!
💭 시퀀스 다이어그램 진짜 열심히 그렸는데..
내부 직원 확인 용으로 상세 내용까지 포함된 거라서 포스팅에 첨부는 안 했다.
🔍 로그인 및 소유권 검증
고** 관리자의 통합회원 로그인의 구현 목표는 간단했다.
SSO(Single Sign-On)를 통해 단일 로그인으로 고**과 샵** 관리자 계정에 모두 접근할 수 있는 환경을 만드는 것이었다.
이를 위해 OAuth 기반의 인증 방식을 도입하여, 사용자가 한 번의 인증으로 여러 서비스에 안전하게 접근할 수 있도록 했다.
아래는 통합회원 로그인 요청에 사용한 URL 예시이다.
curl --request GET \
--url https://example-auth.com/login \
?continue=https://example-api.com/oauth2/authorize?shop_no={shop_no}&response_type=code&client_id={client_id}&scope=profile&state=xyz&redirect_uri={admin_uri}
OAuth 2.0 프로토콜에서 로그인 페이지를 담당하는 엔드포인트와 로그인에 성공한 다음 리다이렉트 될 URL로 구성하였다.
사용자가 인가 코드(Authorization Code)를 발급받는 작업이 진행되는 곳이 https://example-api.com/oauth2/authorize 인데, Authorization Endpoint의 파라미터는 다음과 같이 구성하였다.
📌 Authorization Endpoint 구성
- shop_no={shop_no}
- 로그인한 사용자의 상점 소유권을 검증하기 위한 조건으로 활용
- response_type=code
- Authorization Code Grant 방식을 사용하기 위해 필수적으로 설정하는 값
- 인증 서버는 요청값에 따라 인가 코드(Authorization Code)를 발급하고, 클라이언트는 이 코드로 토큰 발급을 요청함
- client_id={client_id}
- 인증 서버에 사전에 등록된 클라이언트를 식별하기 위한 고유 값
- 보안성 강화를 위해, 인증 서버는 등록된 클라이언트만 요청할 수 있도록 제한함
- scope=profile
- 클라이언트가 요청하는 접근 권한의 범위를 profile로 지정 (최소한의 권한만 요청하도록 함)
- state=xyz
- CSRF(Cross-Site Request Forgery) 공격 방지를 위해 사용되는 값
- 클라이언트가 임의로 생성하며, 요청 시 함께 전달된 이 값은 응답 시 그대로 반환되어 요청의 신뢰성을 확인
- 이번 프로젝트에서는 상점 고유 키로 암호화 하여 전송했으며, 인가 코드를 받는 구간에서 복호화에 성공하면 신뢰 가능한 값으로 간주함
- redirect_uri={admin_uri}
- 사용자가 인증에 성공한 후, 인증 서버가 리다이렉션할 대상 URL
- 이 URI는 사전에 인증 서버에 등록되어 있어야 하며, 불일치할 경우 요청이 거부됨
📌 통합회원 로그인 흐름
1. 사용자가 통합회원 로그인 버튼을 클릭
→ https://example-auth.com/login 로 이동하여 로그인 화면이 표시됨
2. 로그인이 성공하면, 사용자는 Authorization Endpoint(https://example-api.com/oauth2/authorize)로 리다이렉션(304)됨
→ 이 단계에서 Authorization Code가 발급
위 과정을 통해 사용자가 성공적으로 인가 코드를 획득했다면, 이제 다음 단계로 '토큰 신규 발급'을 진행할 차례이다.
🔍 토큰 신규 발급
curl -X POST "https://example-api.com/oauth2/token" \
-H "Accept: application/json" \
-H "Content-type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id={clientId}" \
-d "client_secret={clientSecret}" \
-d "redirect_uri={redirectUri}" \
-d "code={code}"
토큰 발급 과정은 클라이언트 입장에서 크게 복잡하지는 않았다. 획득한 인가 코드를 사용해 1분 이내에 OAuth 로그인을 위한 토큰 발급 요청을 보내고, 성공적으로 JWT를 받아오면 끝이었다. 다만 요청을 전송할 때 가장 큰 문제는 redirect_uri 설정이었다. 인가 서버는 클라이언트 요청의 유효성을 검증하기 위해, 요청에 포함된 redirect_uri가 인가 서버 DB에 미리 등록된 값인지 확인한다. 그러나 개별 상점의 도메인을 redirect_uri로 설정할 경우, 인가 서버에 n만 개의 상점 도메인을 등록해야 하는 부담이 발생했다. 여기에 더해 서비스 종료 시 인가 서버와의 동기화 문제까지 고려한다면..? "배보다 배꼽이 더 큰" 상황이었다.😥
이 문제를 해결하기 위해 redirect_uri를 중앙화하는 방법으로 bypass 용도의 중개 API를 추가하기로 했다.
📌 중개 API 추가하기: 첫 Kotlin 개발
클라이언트는 PHP로 개발되었지만, 중개 API는 Kotlin으로 서비스되고 있었다. 덕분에 이번 기회에 처음으로 Kotlin, 그것도 코루틴을 처음으로 경험해볼 수 있었다. 하지만 갑작스럽게 Kotlin으로 개발을 시작하다 보니, 언어 문법부터 기존 API의 아키텍처 구조와 개발 규칙에 익숙하지 않아서 어려움을 겪었다. 중개 API 관리를 메인으로 담당하는 연동 파트의 도움을 많이 받았는데, 첫 PR에서 받은 코멘트가 무려 36개였다🤣 익숙하지 않은 환경에서 개발하다 보니 많은 고민을 했음에도 자신감이 조금 부족했는데, 하나하나의 코멘트가 정말 큰 도움이 되었다..🍀
중개 API를 개발하면서 배운 점은, 중개 API는 클라이언트 개발과는 확실히 다른 관점이 필요하다는 것이었다. 미들웨어로서 요청과 응답의 흐름을 설계하고 구현할 때 더욱 신중한 접근해야 했다. 예를 들면,, 요청 객체를 정의하고 네이밍 작업을 진행하면서 클라이언트가 아닌 중개 API 관점을 반영해야 했기에, 연동 파트와 여러 차례 논의 끝에 기준을 세웠던 기억이 난다. 또한, 비교적 최근에 만들어진 레포지토리인 만큼 API 요청과 응답이 철저히 규격화되어 있었고, 확장성과 유지보수를 고려한 설계가 인상적이었다. 중개의 체계적인 코드를 보며 우리 (20년 된 레거시 투성인..) 클라이언트 코드와 자연스럽게 비교하게 되었고, 모듈화와 코드 일관성의 중요성을 다시금 실감했다.
새로운 언어와 생소한 환경에서의 개발이 쉽지는 않았지만, 끝나고 돌아보니 꽤나 유익했다! 한번더 중개 API를 개발해야 하는 기회가 찾아온다면, 더 잘해내야지 ㅎㅎ 무엇보다 더 나은 설계와 구현을 위해 끊임없이 고민하고, 코드 리뷰와 같은 소통을 통해 배우고 성장하는 과정이 중요하다는 것을 다시 한번 느낄 수 있었다. 🚀
📌 토큰 받았다! 이제 세션에 저장하자
통합회원 로그인 세션을 어디서 관리할지도 원래는 고민해야 할 부분 중 하나였다. 다행히(?) 우리 개발팀은 이미 세션 데이터를 모두 Redis에서 관리하고 있었고, 관리자 일반 로그인 세션 역시 Redis에 저장 중이었다. 따라서 통합회원 로그인 세션 역시 동일하게 Redis를 활용하기로 결정했다. 익숙한 방식이기에 구현 과정에서도 큰 문제 없이 진행할 수 있었다.
위 과정을 통해 사용자가 성공적으로 토큰을 신규 발급받고 로그인까지 성공했다면, 이제 다음 단계로 '토큰 검증 및 갱신'을 진행할 차례이다.
🔍 토큰 검증 및 갱신
로그인까지 무사히 마쳤다면, 이제 발급받은 토큰으로 사용자가 로그인을 유지한 상태로 서비스를 정상적으로 이용할 수 있도록 환경을 보장해주어야 한다. 서비스 보안과 사용자 경험(UX)을 고려하여, 토큰 상태를 지속적으로 검증하고 자연스럽게 갱신할 수 있는 환경을 만들기 위해 인가서버 담당자와 많은 고민을 했었다.
📌 Introspect (토큰 검증)
curl -X POST "https://example-api.com/oauth2/introspect" \
-H "Accept: application/json" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Bearer {accessToken}" \
-d "token={accessToken}" \
-d "token_type_hint=access_token" \
-d "client_id={clientId}"
서비스의 기획 정책에 맞춰, 우리 서비스는 토큰 검증을 매우 엄격하게 처리하기로 했다. 예를 들어, A 커머스 회원이 소유한 여러 상점(A', B', C') 중 하나라도 로그아웃되면, 해당 회원이 소유한 모든 상점의 로그인 세션도 준실시간으로 종료되도록 했다. (참고, A 커머스 회원이 로그아웃한 후 B 계정으로 다시 로그인했을 때, C' 상점에서 여전히 A 회원으로 로그인된 상태가 발생하는 문제를 방지하기 위한 조치였다)
이 기능을 구현하기 위해, 클라이언트는 페이지 이동 시마다 AccessToken의 revoke 여부를 확인하여 토큰 상태를 검증하도록 했다. 이 방안을 채택하기까지 많은 회의가 있었는데, 내가 계속 우려했던 부분은 이거였다.
페이지 이동 시마다 revoke 요청을 보내면, 저희 클라이언트에서는 revoke 요청 수를 컨트롤할 수 없어요.
n만개의 상점에서 페이지 이동할 때마다 revoke를 찌를 텐데, 인가 서버에서 감당 가능한가요?
실제로 회의 이후에 간단하게 알파 스테이지에 테스트했을 때, 인가 서버 부하가 치솟았고 바로 죽어버려서..ㅋㅋ 인가 서버 담당자와 내가 같이 벙쪘던 웃픈 경험도 있었다. 다행히 인가 서버에서 비즈니스 로직을 수정하고 캐싱 처리 및 추가적인 부하 테스트를 거친 뒤, 라이브 환경에서도 문제 없이 동작하도록 개선했다. 그러나.. 정말 이게 최선일까? 라는 아쉬움은 남아있어 25년도 상반기에 예정되어 있는 2차 스펙에서 한번더 검토하기로 했다.
현재 클라이언트에서 유효한 토큰의 조건은, AccessToken의 payload 내 expired 시간이 지나지 않았고, introspect 결과가 true인 경우로 설정했다.
AccessToken이 만료되지 않았다면, 다음으로 RefreshToken 의 expired 여부를 확인한다. 페이로드 내 RefreshToken의 만료시간이 지나지 않았다면, 토큰 재발급 요청을 진행한다.
curl -X POST "https://example-api.com/oauth2/token" \
-H "Accept: application/json" \
-H "Content-type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token={refreshToken}" \
-d "client_id={clientId}"
토큰 재발급 요청을 보내고 응답값을 처리하는 로직 구현 자체는 그리 어렵지 않았으나, 예상하지 못했던 예외 케이스가 있었다. 바로 Refresh 요청이 동시에 발생하는 경우였다. Refresh Token의 동시성 이슈로 인해 간헐적으로 토큰 갱신이 실패하고 사용자가 로그아웃되는 문제가 발생했다. 이 이슈 해결을 위해 했던 자세한 고민은 아래 포스팅에 따로 정리해두었다📝
https://ddingji-dev.tistory.com/22
[OAuth] RTR 방식에서 Refresh Token 동시성 이슈 해결하기
📌 Intro고객 계층 관리를 위해 통합 로그인 기능 개발이라는 새로운 프로젝트를 담당하게 되었다. 이번 프로젝트에서는 보안 강화를 위해 Oauth 2.0 프로토콜에서 RTR(Refresh Token Rotation) 방식을
ddingji-dev.tistory.com
🔍 토큰 만료 처리 (revoke)
curl -X POST "https://example-api.com/oauth2/revoke" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic {basic}" \
-d "token={accessToken}" \
-d "token_type_hint=access_token"
Revoke 요청은 고객이 ‘로그아웃’ 버튼을 클릭하는 순간에만 처리하도록 제한했기 때문에, 구현이 복잡하지는 않았다. 클라이언트 단에서는 로그아웃 시 revoke 요청을 보내고, 이후 로그인 세션을 삭제하는 방식으로 처리했다.
😯 API가 너무 많다
이번 프로젝트에서 추가된 API는 총 4개였다. 하지만 응답 코드와 메시지 종류까지 고려하면 클라이언트에서 처리해야 할 케이스가 엄청 증가했다. 이를 해결하기 위해 기획팀과 인가 서버 담당자와 함께 처리 방안을 논의했는데, 구두로 설명하거나 줄글로 풀어서 이야기하는 경우가 잦아지다 보니 각자 이해하고 기억하는 내용에 미묘한 차이가 생기는 문제가 있었다..! (1번 이야기할거 3번 이야기하고..ㅜㅜ)
효율적인 커뮤니케이션 방법을 고민하던 끝에, 응답값별 처리 방안을 구글 시트를 활용해 시각화해보았다. 정리한 결과, 이후 소통은 훨씬 간결하고 빠르게 진행될 수 있었다. 🎉
☕️ 마무리, 회고
열심히 개발한 덕분에, 이제는 고**에서도 통합회원 로그인을 제공할 수 있게 되었다. 🥳
정말 다행히도 크리티컬한 이슈가 없었고, 롤백 없이 프로젝트를 무사히 마무리할 수 있었다. 이번 프로젝트는 신기하게도 기획자 2명과 개발자 2명이 모두 주니어였고, 그래서 더 활발하게 토론하며 진행할 수 있었다. 편한 분위기에서 서로의 의견을 주고받으며 프로젝트가 원활하게 진행될 수 있었던 점이 좋은 기억으로 남았다.
총 6개월 간 함께 고생한 만큼, 반드시 회고를 해보고 싶었던 나는 주니어들을 모아 리뷰를 제안했다.
편하게 이야기할 수 있는 분위기를 조성하기 위한 아이스브레이킹을 시작으로..ㅋㅋㅋ 😄
직접 정리한 WBS를 토대로 우리가 걸어온 길을 한번 다같이 훑어보았고,
4L 형식에 맞춰서 각자 코멘트로 내용을 작성하고, 공유하는 시간을 가졌다. 혼자 회고했을 때는 생각하지 못했던 부분들에 대해서 들을 수 있어서 좋기도 했고, 나는 개발팀의 입장이다보니까 기획팀의 자세한 고충은 알지 못했었는데, 편하게 서로 일했던 스토리를 공유하는 과정에서 다음엔 원활한 협업을 위해 어떤 점을 더 신경써야할 지도 명확해져서 좋았던 시간이었다.
각자 생각한 부분을 4L(Liked, Learnd, Lacked, Longed for) 형식으로 코멘트를 남긴 다음, 공유하는 시간을 가졌다. 혼자 회고했을 때는 생각하지 못했던 포인트들도 들을 수 있었고, 나는 개발팀의 입장이다 보니 기획팀의 고충을 잘 몰랐었는데, 서로의 이야기를 공유하는 과정에서 앞으로 더 원활한 협업을 위해 무엇에 신경 써야 할지 명확히 알게 되었다. 회고 덕분에 팀워크가 한층 더 강화된 느낌도 있었다.
마지막으로 회고에 대한 회고까지 진행하며 재미있고 알차게 마무리했다 ㅎㅎ (원래 60분 예상했는데 끝나고보니 90분이었다ㅋㅋㅋ)
회고를 하지 않았을 때는 배포가 끝나면 그저 다음 프로젝트로 정신없이 넘어갔기 때문에, 그 과정에서 만족감을 느끼거나 리프레시할 시간이 없었는데, 이번에 회고를 진행하면서 팀원들 모두 ‘프로젝트가 정말 끝났다는 게 실감 난다. 우리 잘 마무리한 것 같고, 뿌듯하다, 고생했다!’는 감정을 공유할 수 있었다. 나 역시 회고를 좋아하는 성향이라 그런지, 정말 힐링되는 시간이었다 ❤️
그리고 회고한 글은 대장님들 포함 모든 유관 담당자가 있었던 단체방에 공유하였다. 공유하고 나서, 회고 게시글에 유관부서 대장님들이 전부 고생했다고 코멘트 남겨주셔서.. 주니어들끼리 엄청 좋아했다 ㅎㅎ🥰 다같이 고생하며 마무리한 프로젝트인 만큼, 회고를 통해 다 함께 성과를 돌아보고 공유하는 시간이 나에겐 꽤나 소중한 경험이었다.
💭 이 프로젝트를 통해 OAuth 2.0에 대해 몸소 경험하며 공부해볼 수 있었던 점도 좋았고, 여러 파트의 사람들과 파이팅 넘치는 분위기에서 함께 성장할 수 있었던 점도 좋았다. 짧다면 짧고, 길다면 긴 기간 동안 프로젝트를 잘 이끌어가고 마무리할 수 있어서 다행이었다. 고생했다!! 끝!
'Development > Review' 카테고리의 다른 글
DB 접속 정보 관리 개선 프로젝트 회고 (Memcached) (0) | 2025.03.24 |
---|---|
주니어 백엔드 개발자의 2주년 회고: 일잘러가 되고 싶었던 (7) | 2024.09.30 |
2023년, 주니어 개발자로서 내가 걸어온 길 (5) | 2023.12.31 |
1년 차 주니어 개발자가 느낀, 성장하기 좋은 근무 환경 (3) | 2023.08.06 |
소중한 공감 감사합니다