4/1(수) keycloak 연동하기

dev_joo·2026년 4월 1일

오늘 안에 유저 CRUD 만들기를 모두 끝내야한다. 진짜 거짓말 같다... 만우절인데 거짓말이었으면 좋겠다.

keycloak 연동하기

SSO 지원, 세션 관리

keycloak 서버

services:
  postgres:
    image: postgres
    container_name: postgres-keycloak
    restart: unless-stopped
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: _aA123456
      PGDATA: /var/lib/postgresql/data
    volumes:
      - postgres-keycloak-data:/var/lib/postgresql
    ports:
      - '3998:5432'
    networks:
      - keycloak-net

  keycloak:
    image: quay.io/keycloak/keycloak
    container_name: keycloak
    restart: unless-stopped
    environment:
      # 개발용 관리자 계정 (dev 모드에서 편리)
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      # DB 설정
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres-keycloak:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: _aA123456
      # Optional: 호스트명/포워딩 설정 (리버스 프록시 뒤에 둘 경우)
      KC_HOSTNAME_STRICT: "false"
      KC_PROXY_HEADERS: xforwarded
      PROXY_ADDRESS_FORWARDING: "true"
      KEYCLOAK_FRONTEND_URL: https://keycloak.code-factory.co.kr
    command:
      - start-dev
    ports:
      - '3300:8080'
    depends_on:
      - postgres
    volumes:
      - keycloak-data:/opt/keycloak/data
      # realm 자동 임포트용 (선택)
      # - ./import/realms:/opt/keycloak/data/import
    networks:
      - keycloak-net

volumes:
  postgres-keycloak-data:
  keycloak-data:

networks:
  keycloak-net:

Releam

인증, 권한 부여가 적용되는 범위를 나타내는 단위이다.
SSO를 적용한다고 했을 때 해당 SSO가 적용되는 범위는 Realm 단위이다.

Client

인증, 권한 부여 행위를 대행하도록 맡길 애플리케이션을 나타내는 단위이다.
그 단위는 웹사이트 혹은 REST API를 제공하는 서비스도 될 수 있다. 하나의 Realm에 n개의 Client를 생성, 관리할 수 있다.

Role

User에게 부여할 권한 내용을 나타낸다.
여기에는 Keycloak의 REST API를 사용할 권한을 부여할 수 있고 사용자가 정의한 권한을 부여할 수도 있다.

서비스 도메인에 맞는 role을 추가해줬다.

-> 근데 이건 사용하지 않고 키클록 서버가 아닌 User 서비스 서버에서 관리하기로 했다.


4계층 구조에 맞춰 개발하기

애그리거트 정의

  1. 루트 애그리거트 (Entity)정의: 해당 도메인에서 보호해야 할 비즈니스 규칙을 정리한다. 도메인 계층의 도메인 로직은 할 일만 알려주는 '레시피' 역할만 하며, 실제 실행은 서비스 계층이 담당한다.
@Entity
@Table(name = "p_user")
public class User extends BaseEntity {
    @Id
    @Column(name = "id")
    private UUID id;

    @Column(length = 50, name = "username")
    private String username;

    @Column(length = 100, name = "email")
    private String email;

    @Column(length = 100, name = "slack_id")
    private String slackId;

    @Column(length = 20, nullable = false, name = "role")
    @Enumerated(EnumType.STRING)
    private UserRole userRole;

    @Column(name = "status")
    @Enumerated(EnumType.STRING)
    private UserStatus userStatus;
   // Delivery Manager
	@Column(name = "hub_id")
    private UUID hubId;
    @Column(name = "delivery_sequence")
    private int deliverySequence;
   
}

VO로 애그리거트 필드 묶어서 의미있게 관리하기

  1. Aggregate에 사용되는 VO 구성: VO로 각자의 데이터와 로직을 캡슐화한다. 엔티티의 Id도 VO가 될수도 있다. VO 안에도 VO가 포함될 수 있다.
    정적 팩토리 메서드(of, from)를 통해 유효한 객체 생성을 강제한다.
@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class DeliveryManager {
    @Column(name = "hub_id")
    private UUID hubId;
    @Column(name = "delivery_sequence")
    private int deliverySequence;

    public DeliveryManager(UserRole role,
                           UUID hubId,
                           int deliverySequence) {

        if (!isDeliveryManager(role)) {
            // TODO: Exception
            throw new IllegalArgumentException("배송 담당자만 DeliveryManager를 가질 수 있습니다.");
        }

        if (role == UserRole.HUB_DELIVERY_MANAGER) {
            if (hubId == null) {
                // TODO: Exception
                throw new IllegalArgumentException("허브 배송 담당자는 hubId가 필수입니다.");
            }
            this.hubId = hubId;
        }

        this.deliverySequence = deliverySequence;

    }

    private boolean isDeliveryManager(UserRole role) {
        return role == UserRole.HUB_DELIVERY_MANAGER
                || role == UserRole.COMPANY_DELIVERY_MANAGER;
    }

}

다른 서비스가 할 일 추상화

  1. 도메인 서비스: User 관점에서 다른 서비스의 개념을 객체로 생성한다.
    다른 서비스의 정보가 필요하거나, 특정 엔티티에 귀속시키기 모호한 비즈니스 로직을 정의한다. (정보를 가져오는 방식은 FeignClient나 Event 처리 등 마음대로 한다.)
public interface HubProvider {
    HubData get(UUID hubId); // 허브 아이디를 통해 허브 정보 가져옴
}

내 서비스가 해줄 일 추상화

  1. 도메인 이벤트: 상태 변화를 외부에 알릴 이벤트 객체를 정의한다.(예: OrderCreated)
public interface UserEvents {
    void approved(User user);
    void deleted(User user);
}

Audit로 ApprovedBy를 사용할 수 있을까?

도메인 계층이 JPA에 의존하게 되어 좋지 않다.
게다가 이벤트 소비 시점이...


public record UserApprovedPayload(
        UUID userId,
        String name,
        String role,
        int deliveryRotationOrder,
        String email,
        String slackId,
        String approvedBy
) {
    public static UserApprovedPayload from(User user, String approvedBy) {
        return new UserApprovedPayload(
                user.getId(),
                user.getUsername(),
                user.getUserRole().toString(),
                user.getDeliveryManager().getDeliverySequence(),
                user.getEmail(),
                user.getSlackId(),
                approvedBy
        );
    }
}

일을 실제로 수행하는 도구 결정하기

  1. 데이터 영속화 및 외부 연동 : 도메인 객체를 DB에 저장하는 Repository를 구현하고, Keycloak과 같은 외부 IDP(Identity Provider) 연동 로직을 완성

🔐 Keycloak 관련 클래스 역할 비교

구분KeycloakIdentityProviderKeycloakAuthService
핵심 관심사계정(Account)의 생명주기 관리인증(Authentication) 및 세션 유지
주요 기능회원가입(register), 회원탈퇴(withdraw), 비밀번호 변경(changePassword)로그인(getToken), 토큰 갱신(refreshToken), 로그아웃(logout)
상위 인터페이스IdentityProvider (도메인 서비스 계층)AuthService (응용 서비스 계층)
종류'사용자라는 데이터'의 상태를 결정하는 도메인 규칙사용자가 서비스를 이용하는 '과정'의 응용 기능
사용 도구Keycloak Admin Client (Keycloak 객체)Feign Client (KeycloakClient 인터페이스)
권한 수준관리자(Admin) 권한 (전체 유저 제어 가능)사용자(User) 권한 (개인 인증 정보 기반)
데이터 타입UUID, UserRepresentation 등 라이브러리 객체AuthTokenResult, Map 등 커스텀 DTO

정리하고 보니 상위 인터페이스가 다른 두 구현체가 같은 infrastructure 계층에 있어도 되는지 의문이 들었다. 의존성 주입이 전체 계층에서 한 방향으로 이루어지도록 4계층으로 분리한다고 생각하고있었다.
그게 아니라 의존성 주입이 각 계층끼리 한 방향으로 이루어지도록 하는거였다.
두 클래스 모두 infrastructure패키지에 모여 있는 이유는, "상위 계층이 시키는 일을 실제로 수행하는 도구"가 같기 때문이다.

구분설명
왜 둘다 인프라?IdentityProviderAuthService상위 인터페이스의 계층은 다르지만, 실제 구현체(KeycloakIdentityProvider, KeycloakAuthService)는 둘 다 Keycloak이라는 외부 기술을 사용
패키지 위치구현체들은 외부 시스템 연동을 담당하는 infrastructure 계층에 배치
Domain이 시킨 일IdentityProvider → “우리 서비스 유저가 가입/탈퇴할 때, Keycloak의 사용자 정보도 동일하게 동기화해라.”
Application이 시킨 일AuthService → “사용자가 로그인하려고 하니까 Keycloak에게 가서 유효한 토큰을 발급받아 와라.
  1. 응용 서비스(Application Service) 작성:
    도메인 객체들에게 일을 시키고, 트랜잭션 관리와 보안 등 인프라와 도메인을 연결하는 흐름을 작성한다.
  1. 이벤트 발행 및 소비 로직:
    도메인 계층에서 정의한 이벤트를 실제로 던지거나(발행 Publish) 받는 (구독(Subscribe)) 흐름을 작성한다.
  1. DTO 설계 및 Controller 작성:
    외부 API 스펙에 맞춘 DTO를 만들고, 요청을 받아 Application 계층으로 넘겨주는 컨트롤러를 구현한다.

5+. CQRS 및 Query 패키지 구성: 명령(CUD)과 조회(R) 책임을 분리한다.


게이트웨이 인증


반드시 헤더 내용은 게이트웨이가 가공한 내용으로만 작성되어야 한다.
그 외 서비스에서 작성된 내용은 무시한다.


아예 헤더를 새로 만들지 않고 다른 서비스로 보내줄 헤더키들을 비워주는 이유는 게이트웨이로 들어온 원래의 헤더 정보를 유지하기 위함이다.

헤더에는 현재 로그인한 사용자가 활성된 사용자인지 여부도 포함해 전달해야한다.

profile
풀스택 연습생. 끈기있는 삽질로 무대에서 화려하게 데뷔할 예정 ❤️🔥

0개의 댓글