오늘 안에 유저 CRUD 만들기를 모두 끝내야한다. 진짜 거짓말 같다... 만우절인데 거짓말이었으면 좋겠다.
SSO 지원, 세션 관리
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:
인증, 권한 부여가 적용되는 범위를 나타내는 단위이다.
SSO를 적용한다고 했을 때 해당 SSO가 적용되는 범위는 Realm 단위이다.
인증, 권한 부여 행위를 대행하도록 맡길 애플리케이션을 나타내는 단위이다.
그 단위는 웹사이트 혹은 REST API를 제공하는 서비스도 될 수 있다. 하나의 Realm에 n개의 Client를 생성, 관리할 수 있다.
User에게 부여할 권한 내용을 나타낸다.
여기에는 Keycloak의 REST API를 사용할 권한을 부여할 수 있고 사용자가 정의한 권한을 부여할 수도 있다.

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

-> 근데 이건 사용하지 않고 키클록 서버가 아닌 User 서비스 서버에서 관리하기로 했다.
- 루트 애그리거트 (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; }
- 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; } }
- 도메인 서비스: User 관점에서 다른 서비스의 개념을 객체로 생성한다.
다른 서비스의 정보가 필요하거나, 특정 엔티티에 귀속시키기 모호한 비즈니스 로직을 정의한다. (정보를 가져오는 방식은 FeignClient나 Event 처리 등 마음대로 한다.)public interface HubProvider { HubData get(UUID hubId); // 허브 아이디를 통해 허브 정보 가져옴 }
- 도메인 이벤트: 상태 변화를 외부에 알릴 이벤트 객체를 정의한다.(예: OrderCreated)
public interface UserEvents { void approved(User user); void deleted(User user); }
도메인 계층이 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
);
}
}
- 데이터 영속화 및 외부 연동 : 도메인 객체를 DB에 저장하는 Repository를 구현하고, Keycloak과 같은 외부 IDP(Identity Provider) 연동 로직을 완성
| 구분 | KeycloakIdentityProvider | KeycloakAuthService |
|---|---|---|
| 핵심 관심사 | 계정(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패키지에 모여 있는 이유는, "상위 계층이 시키는 일을 실제로 수행하는 도구"가 같기 때문이다.
| 구분 | 설명 |
|---|---|
| 왜 둘다 인프라? | IdentityProvider와 AuthService는 상위 인터페이스의 계층은 다르지만, 실제 구현체(KeycloakIdentityProvider, KeycloakAuthService)는 둘 다 Keycloak이라는 외부 기술을 사용 |
| 패키지 위치 | 구현체들은 외부 시스템 연동을 담당하는 infrastructure 계층에 배치 |
| Domain이 시킨 일 | IdentityProvider → “우리 서비스 유저가 가입/탈퇴할 때, Keycloak의 사용자 정보도 동일하게 동기화해라.” |
| Application이 시킨 일 | AuthService → “사용자가 로그인하려고 하니까 Keycloak에게 가서 유효한 토큰을 발급받아 와라.” |
- 응용 서비스(Application Service) 작성:
도메인 객체들에게 일을 시키고, 트랜잭션 관리와 보안 등 인프라와 도메인을 연결하는 흐름을 작성한다.
- 이벤트 발행 및 소비 로직:
도메인 계층에서 정의한 이벤트를 실제로 던지거나(발행 Publish) 받는 (구독(Subscribe)) 흐름을 작성한다.
- DTO 설계 및 Controller 작성:
외부 API 스펙에 맞춘 DTO를 만들고, 요청을 받아 Application 계층으로 넘겨주는 컨트롤러를 구현한다.
5+. CQRS 및 Query 패키지 구성: 명령(CUD)과 조회(R) 책임을 분리한다.

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

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

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