인증 서버가 필요하다고 해서 Keycloak을 쓰게 됐다.
근데 솔직히 처음엔 뭔지 몰랐다. JWT는 알았고, 구글 로그인이나 카카오 로그인 같은 소셜 로그인도 어렴풋이 알았다. 근데 Keycloak은 그것들이랑 뭐가 다른 건지, 왜 쓰는 건지부터가 몰랐다.
그리고 지금도 솔직히 어렵다. 이 글은 "Keycloak 마스터" 글이 아니라, 모르고 시작했다가 하나씩 부딪혀가는 과정을 기록한 글이다.
한 줄로 정리하면 직접 운영할 수 있는 인증 서버다.
로그인, 회원가입, 토큰 발급, 권한 관리 — 이런 걸 직접 짜지 않아도 되게 해준다. 오픈소스고, 서버에 직접 띄워서 쓴다.
JWT는 인증 상태를 담는 토큰 포맷이다. 로그인 자체를 처리해주는 시스템은 아니다.
서명된 데이터 규격에 가깝다. JWT를 직접 구현한다는 건 이런 걸 다 짜야 한다는 거다.
간단한 프로젝트면 직접 짜는 게 낫다. 근데 서비스가 여러 개가 되면 얘기가 달라진다. 매번 각 서비스마다 인증 로직을 따로 구현하거나 공유해야 한다.
Keycloak은 인증 자체를 중앙 서버로 분리한다. 각 서비스는 발급된 토큰을 검증하고 권한만 판단하면 된다.
Okta, Auth0 같은 SaaS 인증 플랫폼이나, Google, 카카오 같은 소셜 로그인 제공자를 사용할 수도 있다. 이것들은 레이어가 다르긴 한데, 공통점은 외부 서비스에 인증을 맡긴다는 거다.
직접 운영할 필요가 없다는 건 장점이다. 근데 단점도 있다.
| Keycloak | Okta / Auth0 | |
|---|---|---|
| 운영 주체 | 직접 | 외부 서비스 |
| 비용 | 서버 비용만 | 유저 수 기반 과금 |
| 데이터 위치 | 내 서버 | 외부 |
| 커스터마이징 | 높음 | 제한적 |
유저 데이터를 외부에 두기 어려운 환경, 유저가 많아질수록 비용이 부담되는 상황, 인증 흐름을 세밀하게 제어해야 할 때 — Keycloak이 선택지가 된다.
Keycloak은 진짜 설정할 수 있는 게 많다.
한 화면에서 로그인 페이지 디자인부터 토큰 만료 시간까지 다 건드릴 수 있다.
설정할 수 있는 게 많다는 건, 설정해줘야 하는 게 많다는 뜻이기도 하다.
간단하게 "유저한테 company_id attribute 저장하고 싶어" 라고 생각하면 이렇게 된다.
이게 다 안 맞으면 동작을 안 한다. 그리고 동작을 안 해도 에러가 안 난다.
승인 처리 시 해당 유저에게 company_id를 붙여줘야 했다. Keycloak Admin Client로 이렇게 짰다.
public void updateUserAttribute(String email, String key, String value) {
try (Keycloak keycloak = buildAdminKeycloak()) {
var usersResource = keycloak.realm("sparta-logistics").users();
var users = usersResource.searchByUsername(email, true);
if (users.isEmpty()) return;
var userResource = usersResource.get(users.get(0).getId());
var userRep = userResource.toRepresentation();
Map<String, List<String>> attrs = userRep.getAttributes();
if (attrs == null) attrs = new HashMap<>();
attrs.put(key, List.of(value));
userRep.setAttributes(attrs);
userResource.update(userRep);
log.info("[Keycloak] 유저 attribute 업데이트 완료 - email={}, {}={}", email, key, value);
}
}public void updateUserAttribute(String email, String key, String value) {
try (Keycloak keycloak = buildAdminKeycloak()) {
var usersResource = keycloak.realm("sparta-logistics").users();
var users = usersResource.searchByUsername(email, true);
if (users.isEmpty()) return;
var userResource = usersResource.get(users.get(0).getId());
var userRep = userResource.toRepresentation();
Map<String, List<String>> attrs = userRep.getAttributes();
if (attrs == null) attrs = new HashMap<>();
attrs.put(key, List.of(value));
userRep.setAttributes(attrs);
userResource.update(userRep);
log.info("[Keycloak] 유저 attribute 업데이트 완료 - email={}, {}={}", email, key, value);
}
}로그엔 "유저 attribute 업데이트 완료"가 찍혔다. 근데 Admin Console에서 확인하면 null이다. JWT에도 company_id가 없다.
원인은 Keycloak 24.0부터 새로 만든 realm에서 User Profile 기능이 기본 활성화됐기 때문이다. 이전 버전에서는 Admin Client로 attribute를 그냥 넣어도 저장되는 경우가 많았는데, 24.0 이후부터는 User Profile 설정과 attribute 정책 영향을 받게 됐다. 내 경우엔 User Profile 스키마에 등록되지 않은 attribute를 update() 했을 때 예외 없이 조용히 무시됐다.
추가로, attribute를 먼저 User Profile에 등록해야 한다는 것 자체를 모르고 있었다.
설정이 안 돼 있으면 에러 대신 침묵이 온다.
특히 더 헷갈렸던 건, 코드상으론 성공처럼 보인다는 점이었다. 로그도 정상이고 예외도 없는데 실제 데이터만 저장되지 않았다. 코드만 보면 절대 못 찾는다.
Realm Settings > User Profile > Create attribute 에서 먼저 등록해야 한다.

| Attribute Name | Display Name |
|---|---|
| company_id | Company ID |
| hub_id | Hub ID |
등록 후 동일한 코드 재실행 → 정상 저장됐다.
attribute가 저장됐다고 JWT에 자동으로 들어오지 않는다.
Keycloak은 User Attribute와 JWT Claim을 자동으로 연결하지 않는다. Mapper를 통해 어떤 attribute를 어떤 claim으로 노출할지 직접 지정해야 한다. attribute 저장과 토큰 노출은 완전히 분리된 개념이다.
user-attributes
company_id, Token Claim Name: company_id

이 세 가지가 다 맞아야 토큰에 들어온다.
Keycloak은 아직도 어렵다.
설정 하나하나가 연결돼 있는데, 그 구조를 모르면 계속 막힌다. 에러가 나면 그나마 낫다. 그냥 아무 일도 안 일어나는 게 더 무섭다.
Keycloak은 코드보다 설정을 먼저 의심해야 하는 툴이었다.