Keycloak
Keycloak은 OAuth2.0 / OIDC 기반의 오픈소스 인증 서버로
SSO, MFA, Social Login 등을 쉽게 구현할 수 있다.
Keycloak을 선택한 이유는 우선 Java라는 언어로 쓰여진 오픈소스기 때문에 Java Backend 개발자인 나에게 조금 더 익숙한
언어로 쓰여 있고 무엇보다 인증/인가라는 시스템을 외부로 분리하고 각 서비스에서는 로그인, MFA, Social Login, 세션 관리와 같은 문제를 개별 서비스가 구현하지 않아도 되기 때문이다.
프로젝트 목표
이 프로젝트의 목표는 단순히 Keycloak을 통한 인증, 인가, SSO, MFA, Social Login 적용이 아니라
더 나아가 Keycloak이라는 인증/인가 시스템과 여러 서비스가 어떻게 분리되고 협력할 수 있는지를
고민해보고 구현해보는것이 목적이다.
이미 인증/인가라는 하나의 시스템이 독립적으로 존재한다면, 그에 맞춰 서비스들도 그 역할과 책임의 범위를 갖고 분리하는 과정에서
많은 고민과 학습을 하며 전체적인 프로젝트의 구조를 잡아갈 수 있을거라고 생각된다.
아키텍처 소개 및 컴포넌트 역할 정의
이 프로젝트는 Keycloak을 인증/인가 서버로 분리하고, 그 위에 Gateway / 서비스(User, Chat) 구조로 기능을 나누어 구성했다.
각 컴포넌트는 책임과 역할을 명확히 하고, 서비스는 가능한 독립적으로 설계하는것을 목표로 했다.

Keycloak
Keycloak은 이 시스템의 인증과 인가를 전담하는 외부 시스템이다. 사용자는 서비스에 가입하고 로그인하는 과정에서 Keycloak을 통해
인증을 수행하며, 서비스는 Keycloak이 발급한 OIDC(JWT) 토큰을 기반으로 사용자를 식별한다.
또한 Keycloak은 기본적으로 SSO를 제공하기 때문에, 사용자가 한 번 로그인하면 동일한 인증 상태를 여러 서비스에서 재사용할 수 있다.
이 프로젝트에서는 여기에 더해 MFA 설정 및 Google Social Login과 같은 소셜 로그인을 Keycloak에 붙여 인증 기능을 서비스에서 직접 구현하지 않고 외부로 분리했을 때 어떤 장점이 생기는지 체감할 수 있도록 했다.
정리하면 Keycloak의 핵심 역할은 다음과 같다.
- 사용자의 회원가입, 로그인/로그아웃, 세션 관리
- 토큰 발급 및 갱신(리프레시 토큰 포함)
- Role 기반 권한 부여
- MFA / Social Login과 같은 인증 확장 기능 제공
API Gateway
API Gateway는 클라이언트 요청이 들어오는 첫 번째 관문이다. 프론트엔드는 여러 서비스(User/Chat)를 직접 호출하지 않고,
Gateway 하나만 호출한다. Gateway는 요청을 받아 토큰(JWT)을 검증하고, URI 규칙에 따라 적절한 백엔드 서비스로 라우팅 한 뒤,
그 응답을 다시 클라이언트에게 반환한다.
이렇게 하면 프론트엔드 입장에서 서비스가 몇개로 분리되어 있든 호출 방식이 단순해지고, 각 서비스의 내부 주소/포트/구조를
알아야 하는 복잡함을 덜 수 있다. 이는 백엔드 서비스 입장에서도 Gateway를 통해 토큰 검증이라는 인증/보안의 1차 방어선을 만들뿐만 아니라 서비스의 구조를 외부로부터 완전히 숨길 수 있다.
즉, 백엔드 서비스의 포트 변경, 분리 통합 등과 같은 구조 변경을 프론트엔드의 수정 없이 수정할 수 있게 된다.
Gateway의 역할을 요약하면 다음과 같다.
- 클라이언트 요청의 토큰 검증
- 규칙화된 경로 기반 라우팅
- 응답을 프록시 형태로 반환하여 프론트엔드의 구조 단순화
User 서비스
User 서비스는 서비스 도메인 관점에서 "사용자"와 "사용자 관계"를 담당한다.
Keycloak이 인증 사용자를 관리 한다면, User 서비스는 실제 서비스에서 필요한 도메인 사용자 정보를 관리한다.
예를 들어, 이 프로젝트에서는 사용자가 처음 서비스를 이용할 때, Keycloak 토큰을 기반으로 서비스 사용자 엔티티를 생성하고,
이후 부터는 닉네임, 표시 이름, 친구관계 같은 서비스 기능에 특화된 사용자 정보를 User 서비스가 책임진다.
User 서비스는 Keyclaok 토큰을 검증하는 Resource Server로 동작하며, 요청자의 sub를 기반으로 현재 로그인한 사용자를 식별한다.
그 위에서 사용자 프로필 관리뿐 아니라, 친구 추가, 차단, 삭제, 목록과 같은 관계 기능을 제공한다.
User 서비스의 역할을 정리하면 다음과 같다.
- JWT 검증 및 현재 사용자 식별(서비스 단의 인증 처리)
- 최초 접근시 서비스 사용자 생성
- 사용자 프로필/계정 정보 관리
- 친구 관계 (추가, 차단, 삭제, 목록) 관리
Chat 서비스
Chat 서비스는 채팅 도메인만을 담당한다. 구체적으로는 채팅방과 메시지를 관리하고,
1:1 채팅과 그룹 채팅을 모두 지원하는것을 목표로한다.
Chat 서비스 역시 Keycloak 토큰을 검증하여 요청자를 식별하고, 해당 사용자가 채팅방에 참여할 권한이 있는지 확인한다.
이후 메시지를 저장하고 채팅방 목록, 채팅 내역 같은 데이터를 제공한다.
그리고 1:1 실시간 채팅 및 그룹 채팅은 WebSocket(STOMP)를 통해 실시간 메시지 중계를 하도록 했다.
Chat 서비스의 역할을 요약하면 다음과 같다.
- JWT 검증 및 채팅 요청자 식별
- 채팅방 생성, 조회, 참여자 관리
- 실시간 메시지 전송, 저장 및 조회(1:1, 그룹)
Keycloak UI를 최종 사용자에게 노출해야 할까?
Keycloak을 도입하면서 든 처음 질문은 다음과 같다.
"Keycloak이 제공하는 로그인, 회원가입, MFA 설정 UI를 최종 사용자에게 그대로 노출하는 것이 맞을까?"
Keycloak은 기본적으로 완성된 인증 UI를 제공한다.
이 UI들은 보안적으로 검증되어 있고, 설정만으로 빠르게 인증 시스템을 구축할 수 있다는 장점이 있지만
이는 분명히 서비스를 이용하는 최종 사용자에게 이질감을 준다.
"나는 분명히 XX서비스에 회원가입을 하려고 하는데 왜 전혀 다른 화면이 나타나지?"
Keycloak UI를 그대로 사용하는 방식의 장단점을 정리해보면 다음과 같다.
장점
- 인증 UI를 직접 개발하지 않아도 된다.
- OAuth2.0, OIDC 표준 흐름을 자연스럽게 따르게 된다
- 보안 취약점을 직접 다룰일이 줄어든다.
즉, 개발 속도와 안정성이라는 측면에서는 매우 매력적인 선택이다.
단점
- 사용자 경험의 단점
- 디자인 톤이 다르다
- URL이 바뀌며 외부에 노출된다
- 모바일 앱에서는 WebView 전환이 필요하다.
- 서비스와 인증 시스템의 경계가 사용자에게 그대로 드러난다.
Keycloak UI를 최종 사용자에게 노출하는 것은 분명히 사용자에게 이질감을 주고
이는 사용자가 서비스에게 갖는 신뢰를 매우 저하시킬수 있다고 생각한다.
나는 사용자 경험 저하로 인한 신뢰도 하락을 예방하는 것이 시스템의 보안적 결함을 보완하는 것만큼
서비스와 비즈니스에 있어 중요한 문제라고 생각한다.
하지만 이 프로젝트의 목표는 백엔드 개발자로서 Keycloak을 전제로한 마이크로 서비스의 설계, 구현이기 때문에
우선순위를 떨어뜨리고 Keycloak이 제공하는 기본 UI를 그대로 사용하되 Keycloak에서 제공하는 커스터마이징을 통해
사용자가 느낄 이질감을 최대한 없애보려고 한다.
추후 이 프로젝트의 1차 완성을 완료하면 Keycloak을 외부로부터 숨기는 구조를 시도해볼 계획이다.
왜 Gateway와 서비스에서 모두 JWT를 검증할까?
Gateway는 클라이언트의 요청이 처음 도달하는 관문이다.
따라서 JWT 검증을 Gateway에서 수행하는 것은 자연스러워 보인다.
하지만, Gateway에서 이미 토큰을 검증하는데, 각 서비스에서 다시 검증하는 것은 중복이 아닌가? 라는 의문이 생길 수 있다.
하지만, 아래와 같은 이유로 각 서비스에서도 JWT 검증을 수행하는 구조를 선택했다.
1. Gateway를 우회한 요청에 대한 대비
인프라 구성시에 네트워크 레벨에서 제어를 통해 각 service로의 직접적인 요청을 차단할 수 있지만, 네트워크 설정만으로 모든 우회 가능성을 완전히 제거하기는 어렵고, 설정 변경이나 운영 실수로 인해 의도치 않은 접근 경로가 열릴 가능성도 존재한다.
따라서 어플리케이션 레벨에서의 토큰 검증을 통해 한번 더 우회 또는 의도된 요청을
차단할 수 있는안전 장치를 두는것이 안전하다고 판단했다.
마지막으로 전체 서비스의 입장에서 단 한번의 인증 우회는 치명적인 보안적 결함으로 이어질 수 있고 이중 검증을 감수하더라도 방어 계층을 추가하는 것이 더 안전하다고 판단했다.
2. 보안에 대한 책임
모든 요청의 유효성 검증을 Gateway 하나에만 의존하는 구조는 Gateway에 과도한 책임을 부여하게 된다고 생각했다.
Gateway는 당연히 인증 검증의 첫 번째 관문 역할을 수행 하지만, 각 서비스가 자신의 리소스를 보호하는 것은 Gateway의 책임 영역 밖의 문제라고 생각했다.
따라서 각 서비스는 독립적으로 배포되고 운영되는 하나의 시스템이기 때문에, 최소한의 보안 설정과 요청 검증은 서비스 스스로 가져가는 것으로 결정했다.
인증 사용자와 서비스 도메인 사용자의 분리
Keycloak은 인증과 인가를 책임지는 시스템으로 다음과 같은 정보를 관리한다.
- keycloak_sub(OIDC subject)
- 이메일
- 소셜 로그인 계정 정보
- MFA 설정
- 세션, 토큰, 인증상태
그리고 서비스 도메인에서 사용하는 사용자 정보는 다음과 같다.
- 이메일
- 닉네임
- 이름
Keycloak과 서비스를 포함한 전체 시스템의 입장에서 봤을때, 사용자는 “회원 가입”이라는 행위를 통해 생성되고
이 과정은 전적으로 Keycloak에 의해 관리된다.
즉, Keycloak은 시스템에서 유일한 “인증 사용자”의 생성 주체이다.
하지만 서비스의 관점에서는 이야기가 조금 달라진다.
서비스 입장에서 Keycloak의 사용자에 직접 의존하는것은 다음과 같은 문제를 가진다.
- 인증 서버의 내부 구조에 서비스가 강하게 결합됨.
- 서비스에서 필요한 사용자의 정보와 Keycloak에서 관리되는 사용자 정보의 책임이 다름.
- 서비스에 종속적인 속성 및 상태(닉네임, 친구, 차단 등)을 표현하기가 어려움.
그래서 이러한 이유로 이 프로젝트에서는 사용자를 회원가입시 Keycloak에서 생성되는 “인증 사용자”와
서비스 이용시 생성되는 “서비스 사용자”로 나누고 Keycloak에서 부여하는 고유 식별자인 keycloak_sub라는
문자열을 인증 사용자와 서비스 사용자의 유일한 연결 키로 사용했다.
이러한 분리를 통해 인증 시스템과 서비스 도메인의 책임을 명확히 구분하고 Keycloak이라는 인증 서버를 전제로 하되, 서비스 자체는 독립적인 구조를 만드려고 했다.
서비스간 데이터 교환에 대한 고민
서비스를 분리한 후 부딪힌 문제 중 하나는 서비스 간 데이터 의존성을 어떻게 다룰것인가에 대한 문제이다.
문제 상황
채팅방 목록을 조회하는 화면을 떠올려보면 다음과 같은 정보가 필요하다.
- 채팅방 ID
- 채팅방 타입(1:1 or 그룹)
- 마지막 메시지
- 마지막 메시지 시간
- 채팅방 제목
여기서 문제가 되는 부분이 채팅방의 제목이다.
일반적인 1:1 채팅의 경우 나의 화면에는 상대방의 이름이, 상대방의 화면에는 나의 이름이 채팅방의 제목으로 표시된다.
하지만 Chat 서비스는 구조적으로 다음의 정보만을 알고 있다.
- 채팅방 ID
- 채팅방 참여자의 keycloak_sub
- 메시지 데이터
즉, Chat 서비스는 사용자(상대방)의 keycloak_sub는 알지만 사용자의 이름은 알지 못한다.
사용자의 이름, 닉네임과 같은 정보는 User 서비스의 책임 영역에 속한다.
따라서 다음과 같은 고민이 나타난다.
채팅방 제목은 누가, 언제, 어떻게 결정해야 할까?
가장 직관적인 해결 방식은 다음 2가지가 있었다.
방법 1
- Chat 서비스가 채팅방 목록을 그대로 프론트엔드에게 반환
- 프론트엔드가 각 채팅방의 상대방 keycloak_sub를 통해 User 서비스에 사용자 이름 조회
하지만 위의 방식은 프론트엔드의 복잡성을 크게 증가시키고 서비스 도메인의 책임을 프론트엔드에 전가하는 형태 이기 때문에 적절한 방식은 아니라고 판단 했다.
방법2
- Chat 서비스의 비즈니스 로직에 채팅방 목록을 쿼리한 후 User 서비스의 API를 직접 호출하는것을 추가한다.
- 최종적으로 클라이언트에게 사용자의 이름이 제목으로 표시된 채팅방 목록을 응답으로 반환한다.
하지만 2번째 방식 또한 문제가 있어보였다.
위와 같은 흐름은 Chat 서비스가 User 서비스에 높은 의존도를 갖고, 서비스간의 결합도와 복잡성도 크게 증가할 것으로 예상됬다.
또한 User 서비스의 장애가 Chat 서비스의 장애로 전파될 확률이 굉장히 커보인다.
즉, User 서비스에서 장애가 발생하여 사용자 이름을 조회하는 API가 실패했을 때, 채팅 서비스 또한 응답에 실패하게 된다.
이는 각각의 독립적인 서비스라는 정체성을 잃고 마이크로 서비스라는 의미를 흐린다고 생각했다.
현재 마주한 문제는 외부 인증 시스템과 각각의 서비스의 역할과 영역을 정의했지만,
아직 전체 서비스의 관점에서 서비스와 서비스간의 회색 지대에서의 데이터 교환 및 처리에 대한 문제는 해결하지 못했다.
채팅방 제목과 같이 여러 서비스의 데이터를 조합해야 하는 조회 모델은 어느 한 서비스의 책임으로 보기 어렵기 때문이다.
현재 위의 2가지 방법외에 API Gateway에서의 채팅과 사용자 서비스 API 조합을 고려할 수 있지만 이 또한 서비스간의 장애 전파를 막을 수는 없기 때문에 조금 더 나은 방식을 찾아보고 있다.
다음글에서는 각 서비스의 독립성을 지키면서 서비스와 서비스간의 데이터 교환 방법을 다루는 방법에 대해 다뤄보려고 한다.
'개발' 카테고리의 다른 글
| WireShark로 Application간 네트워킹 분석하기 (0) | 2025.10.24 |
|---|---|
| [Kubernetes] Ingress, Ingress Controller, Load Balancer 흐름 정리 (0) | 2025.09.17 |
| mutualTLS and java (4) | 2025.08.17 |
| 도메인 기반 디렉토리 구조로 변경하기 (0) | 2024.10.02 |
| WebBrowser의 실행 과정 (0) | 2023.11.15 |