← Projects
프로젝트 상태: 완료

BARO FARM

생산자와 소비자를 직접 잇는 농수산물 커머스를 Spring Cloud 기반 MSA로 만든 5인 팀 프로젝트(프로그래머스 데브코스)입니다. 이 중 인증·인가 경계 — 회원·인증 서비스, 게이트웨이 인증·인가 필터, OPA 정책과 정책 번들 서비스, 사용자·판매자 상태 전파 — 를 맡았습니다. 실서비스 운영 없이 발표 시연(2026-01-27)까지 진행했고, 설계는 그 시점 기준으로 설명합니다. 발표 시점의 정책은 기본 허용이었고, 지금 main에서는 라우트의 OPA 인가 필터가 빠져 있습니다.

Role

백엔드(5인 팀) — 회원·인증 서비스, Gateway 인증·인가 필터, OPA 정책·번들 서비스, 사용자·판매자 상태 전파

Period

2025.11.29–2026.01.27 팀 프로젝트(세미·파이널) · 2026.02 모듈 정리(팀 레포 main) · 2026.03 개인 브랜치(미병합)

Spring Cloud Gateway Spring Security · JWT OPA (Rego) Kafka Redis Resilience4j Java 21 · Spring Boot 3.5 OAuth2 (Kakao · Naver) OpenFeign JPA MySQL
BARO FARM 시스템 구성도 — 금색이 맡은 부분: API Gateway의 인증·인가 필터, OPA 서버와 정책 번들 서비스, auth-service, seller-service의 판매자 부분. 회색은 팀원이 맡은 서비스와 플랫폼
발표 시점(커밋 78eaa987) 구성 · 금색 = 맡은 부분

84

서비스·역할별 경로 규칙

OPA 정책 데이터 22파일 · 78eaa987

10–60초

상태 변경이 OPA에 닿는 번들 폴링 주기

설정값, 측정 아님

주문·결제, 재고, 정산, AI 추천·검색, 배포 인프라는 팀원이 맡았습니다. 게이트웨이의 트래픽 설정(요청 수 제한·재시도·서킷브레이커)은 인증·인가 필터와 함께 맡았습니다.

맡은 범위

영역본인팀원대표 커밋
회원·인증 서비스 baro-auth가입·이메일 인증 코드, JWT 로그인(HttpOnly 쿠키), 리프레시 회전·로그아웃, 비밀번호 변경·재설정, 카카오·네이버 OAuth와 계정 연결, 내 정보, 회원탈퇴(outbox), 관리자 사용자·판매자 상태 변경, hotlist 이벤트 발행—95946295 5739f946
API Gateway인증 필터, OPA 인가 필터(서킷브레이커·벌크헤드·2초), 기본 필터(Redis RequestRateLimiter, Retry, CircuitBreaker)와 Resilience4j 설정, fallback일부 라우트 설정fb81d45f 8cc62c58
OPA 정책rego, 서비스·역할별 경로 규칙(22파일, 경로 항목 84개), 정적 hotlist, opa-configK8s DaemonSet 배포 매니페스트ff7f5015 f60ada73
정책 번들 서비스 opa-bundleKafka hotlist 소비, 메모리 hotlist, bundle.tar.gz 생성·제공, 내부 접근 필터—c7adf324
판매자 도메인 baro-seller판매자 신청·사업자번호 검증, hotlist 이벤트로 판매자 상태 동기화, auth 호출용 Feign 클라이언트·토큰 전달 인터셉터(발표 시점 호출처 없음)농장(farm)5f76b5cd 1224e1a2
플랫폼—Eureka·Config 서버, K8s 매니페스트, CI/CD, 모니터링—
다른 서비스—주문·결제 SAGA, 재고, 정산 배치, AI 추천·검색—

시스템 구성

구성도는 위 커버 그림입니다(금색 = 맡은 부분, 회색 = 팀원, 점선 = 폴링·비동기).

  • 인가 판단은 OPA(PDP)가 하고, 게이트웨이는 그 결과를 집행(PEP)만 합니다.
  • 상태 변경은 auth-service → Kafka(opa-hotlist-events) → opa-bundle → OPA 번들 폴링으로 인가 데이터에 반영됩니다. 같은 토픽을 seller-service가 소비해 판매자 상태를 맞춥니다.
  • Spring Security는 auth-service에만 있습니다. 나머지 서비스는 게이트웨이가 쓴 X-User-* 헤더를 그대로 사용자 식별에 씁니다. 그래서 게이트웨이 필터가 신뢰 경계입니다.

요청 하나가 지나가는 길

요청 하나의 인가 경로. 브라우저가 쿠키를 실어 보낸 요청은 API Gateway의 라우트 필터인 Authentication 필터와 OpaAuthorization 필터를 차례로 지나고, 게이트웨이에는 기본 필터(RateLimiter·Retry·CircuitBreaker)도 걸려 있습니다(합친 실행 순서는 확인하지 않음). 인가 필터는 OPA에 request·subject·route.service를 보내 결과를 받고, true이면 하류 서비스로 라우팅합니다. 토큰 검증 실패는 401, 정책 결과 false나 결과 없음은 403, OPA 오류·2초 초과·서킷 열림·벌크헤드 포화는 503입니다. 토큰이 없으면 들어온 X-User-* 헤더를 그대로 둔 채 통과합니다.
게이트웨이 78eaa987 · 403은 정책이 기본 거부였던 01-22~01-26 기준

원본 크기로 보기 ↗

로그인하면 auth-service가 access·refresh JWT를 HttpOnly·Secure·SameSite=Strict 쿠키로만 내려 줍니다. access JWT는 HS256으로 서명하고 sub(이메일), uid, 역할, user_status, seller_status, flags를 담습니다. 유효기간은 access 7일, refresh 14일입니다.

판단과 집행을 나눴습니다. 경로·역할 규칙을 게이트웨이 코드 배포 없이 정책과 데이터로 바꾸기 위해서입니다. 대가는 요청마다 붙는 OPA 호출 한 번(2초 제한)입니다.

규칙은 서비스·역할별 데이터 묶음입니다. 게이트웨이는 질의에 route.service를 넣고, OPA는 그 서비스의 묶음만 평가합니다. 메서드 색인(index.method)으로 후보 규칙을 한 번 더 줄입니다. data[service] 가변 조회가 정책 패키지 자신을 가리켜 생긴 순환 오류는 명시적 데이터 루트로 풀었습니다. 대가로 서비스가 늘 때마다 rego를 고쳐야 하고, 라우트 id 규칙에 서비스 이름이 묶입니다. 규칙 한 건은 아래와 같습니다(baro-cloud/opa/policy/data/buyer/seller.json @78eaa987에서 발췌, 들여쓰기만 정리).

"buyer:products-create": {
  "name": "buyer:products-create",
  "path": "^(/buyer-service)?/api/v1/products$",
  "methods": ["POST"],
  "roles": ["SELLER", "ADMIN"],
  "deny_flags": ["PRODUCT_PUBLISH_BLOCKED"]
}

OPA가 실패하면 닫습니다. OPA 오류, 2초 초과, 서킷 열림, 벌크헤드 포화는 모두 503입니다. 가용성을 내준 선택이고, fail-open 대안은 설계 문서에 남겼습니다.

상태가 바뀌면

판매자 승인 한 번의 전파 시퀀스. 관리자가 판매자 상태 변경을 요청하면 auth-service가 트랜잭션 안에서 users.user_type을 SELLER로 바꾸고, 커밋 전에 opa-hotlist-events 토픽으로 subjectId를 키로 이벤트를 보냅니다(결과를 기다리지 않음). opa-bundle은 이벤트를 소비해 eventId로 중복을 거르고 bundle.tar.gz를 다시 쓰며, OPA는 10–60초 간격의 폴링으로 번들을 가져가 활성화합니다. seller-service도 같은 이벤트를 소비해 seller_status를 갱신합니다. 발표 커밋의 정책은 기본 허용이고, 판매자 승인 규칙은 01-26에 지웠습니다.
판매자 승인 한 번의 전파 — 78eaa987 기준

원본 크기로 보기 ↗

토큰에는 굵은 권한만 두고, 바로 반영할 상태는 hotlist로 보냈습니다. 이미 발급된 JWT는 만료 전까지 옛 역할을 담고 있습니다(access 7일). 그래서 사용자 정지·차단·탈퇴와 판매자 승인·거절·정지는 hotlist 데이터로 OPA에 보냅니다. 토큰의 seller_status는 항상 UNKNOWN이라 판매자 상태는 hotlist로만 알 수 있습니다. 대가는 OPA 번들 폴링 주기(10–60초, 설정값)만큼의 지연입니다.

번들은 OPA가 가져가는(pull) 방식이고, 이벤트마다 전체를 다시 만듭니다. opa-bundle은 기동할 때 data.json에서 hotlist를 복구합니다. OPA는 마지막 정상 번들을 계속 씁니다. 대가로 메모리 저장소라 인스턴스는 하나만 둘 수 있고, 번들 파일은 임시 파일 없이 덮어씁니다.

Kafka 키는 subjectId입니다. 사용자별 순서를 지키기 위해서입니다. 번들 서비스는 eventId로 중복을 거릅니다(최근 10,000개).

판매자 hotlist가 게이트웨이 인가에 쓰인 것은 승인 규칙이 정책에 있던 01-22~01-26뿐입니다. 발표 커밋의 정책은 판매자 hotlist를 읽지 않습니다. 그 기간에도 상품 서비스는 토큰의 역할 헤더로 판매자를 다시 검사했습니다.

이벤트를 내보내는 세 가지 방식

경로방식지켜지는 것빈틈
사용자·판매자 상태 → opa-hotlist-events (발표 시점)트랜잭션 안에서 KafkaTemplate.send, 결과를 기다리지 않음. 키 = subjectId사용자별 순서. 소비자가 eventId로 중복을 거름롤백돼도 이벤트가 나갈 수 있음. 브로커 장애면 로그만 남고 사라짐
회원탈퇴 → user-events (발표 시점, 01-20)outbox: 같은 트랜잭션에 이벤트 행 적재 → 1초 스케줄러 → send().get(5s) → PUBLISHED, 5회 실패면 FAILED탈퇴가 커밋되면 이벤트 행도 반드시 있음소비자가 아직 없음. 여러 인스턴스용 잠금·백오프 없음. 같은 탈퇴의 hotlist 전파는 outbox가 아니라 커밋 전 직접 전송
판매자 승인 (2026-03 개인 브랜치, 미병합)afterCommit 동기화 → @Async("opaEventExecutor") → Kafka승인 커밋과 응답이 브로커에 막히지 않음. 롤백된 승인은 나가지 않음전송이 실패하면 다음 토큰 갱신(최악 7일)까지 반영되지 않음. 관리자 경로만 해당(grantSeller는 여전히 트랜잭션 안 동기). 테스트 없음

즉시성이 필요한 인가 데이터와 유실되면 안 되는 탈퇴 이벤트를 다르게 다뤘지만, 발표 시점의 hotlist 경로는 유실도, 롤백된 변경의 이벤트(유령 이벤트)도 막지 못했습니다. 3월 브랜치의 커밋 뒤 전송은 그중 유령 이벤트 쪽만 막습니다.

회원·인증 데이터

회원·인증 서비스의 표. users를 중심으로 auth_credential(1:1, 이메일 가입자만), oauth_account(1:N), refresh_token(1:1, 사용자당 1개)이 user_id로, outbox_event가 aggregate_id(사용자 id 문자열)로 논리적으로 이어지고, email_verification은 사용자와 연결되지 않은 이메일 기준 표입니다. 오른쪽에는 발표 시점 별도 서비스였던 seller-service의 seller(users와 1:1, hotlist 이벤트로 동기화)와 팀원이 맡은 farm, 그리고 표가 아닌 OPA 번들 데이터(hotlist 사용자·판매자, 서비스·역할별 규칙)가 있습니다.
회원·인증 서비스의 표와 OPA 번들 데이터 — 78eaa987

원본 크기로 보기 ↗

  • 회원·인증 표 사이 관계는 JPA 연관 없이 user_id(BINARY(16) UUID)로 잇는 논리 관계입니다.
  • 리프레시 토큰은 사용자당 1개이고 갱신 때 회전하며, 비밀번호 변경·재설정·정지·탈퇴 때 지웁니다. 탈퇴는 한 트랜잭션에서 사용자를 익명화하고 자격 증명·소셜 계정·리프레시 토큰을 지운 뒤 outbox 행을 남깁니다.

발표 이후

2026-02에는 팀 레포 main에서 모듈을 정리했습니다(PR #92·#94·#102). auth와 seller를 baro-user로 합치고, baro-cloud/*를 최상위 모듈로 올리고, 알림을 baro-notification으로 나눴습니다. 게이트웨이에서는 Eureka 클라이언트를 뺐습니다. 이 과정에서 라우트의 OPA 인가 필터가 빠졌습니다(본인 커밋 3f98d694). 지금 main의 prod 프로필 라우트는 K8s 서비스 DNS 주소를 씁니다.

2026-03에는 개인 브랜치(미병합)에서 판매자 관리자 API를 도메인으로 분리했습니다. 승인 뒤 OPA 전파는 커밋 뒤 별도 실행기로 옮겼고, 그 결정을 ADR(커밋 59f643a6)로 남겼습니다. 이 변경은 관리자 경로에만 해당하고 테스트가 없습니다. 브랜치의 테스트는 컴파일되지 않습니다.

현재 상태와 한계

정책과 main의 상태(시간 순)
  • 2026-01-22 14:29 1224e1a2: 판매자 승인 규칙을 추가했습니다. 이때 정책은 기본 거부였습니다.
  • 01-25 19:15 218193b4부터 01-26 00:51 36edd3fe까지 약 5.5시간 동안 정책이 opa check를 통과하지 못했습니다.
  • 01-26 00:51 36edd3fe(본인): 판매자 승인 규칙을 지웠습니다. 00:54 d4b67d1b(본인): 기본 허용으로 바꿨습니다. 발표 커밋 78eaa987(01-27)도 기본 허용입니다.
  • 2026-02-12 801d3b9e(본인): OPA 번들 경로 설정과 컨트롤러 경로가 어긋났습니다(/baro-opa/bundle과 /opa/bundle). 02-22 3f98d694(본인): 라우트에서 OPA 인가 필터를 뺐습니다.
  • 지금 main(1ef2f99b)에는 OPA·번들 서비스 배포 매니페스트가 없고, 클린 클론에서 baro-user가 빌드되지 않습니다.
  • rego 테스트는 도입(ff7f5015)부터 main까지 한 번도 통과하지 않았습니다. 정책의 순환 오류(위 ‘규칙’ 문단)는 f60ada73에서 풀었지만, 테스트는 실행하면 rego_recursion_error로 끝납니다.
  • 다시 켜는 순서: 기본 거부 → 라우트 필터 재연결 → 번들 경로 일치 → rego 테스트가 실제로 도는 상태.

발표 시점 설계의 한계입니다.

한계영향고친다면
토큰이 없는 요청은 클라이언트가 보낸 X-User-* 헤더가 그대로 하류로 갑니다하류 서비스가 이 헤더를 믿으므로, 헤더만으로 다른 사용자로 행세할 수 있습니다들어온 X-User-*는 항상 지우고, 토큰이 없으면 익명 주체로만 넘깁니다
비밀번호에 64자 salt를 붙여 BCrypt에 넣습니다BCrypt 입력 한도(72바이트)를 넘겨 9자 이상 비밀번호의 가입·변경이 실패합니다별도 salt를 없애고 BCrypt 내장 salt만 씁니다
grant-seller 엔드포인트가 인증 없이 열려 있습니다(커밋 메시지 “임시로(세미용)“)판매자 권한을 얻는 경로가 열려 있습니다관리자 승인 경로로만 권한을 줍니다
access 토큰 유효기간이 7일입니다(15분에서 “테스트용”으로 바꾼 값이 남음)옛 역할이 오래 남고, 상태 전파가 유실되면 다음 토큰 갱신까지 반영되지 않습니다짧은 access 토큰과 리프레시 회전, 상태 전파는 outbox로
번들 엔드포인트는 사설 대역 전체를 허용하고, 내부 토큰 검사는 기본으로 꺼져 있습니다클러스터 안의 누구나 번들을 받을 수 있습니다토큰 검사를 기본으로 켜고 허용 대역을 줄입니다

팀 레포는 공개입니다. 설계 설명은 발표 커밋 78eaa987, 현재 상태는 main 1ef2f99b 기준입니다. 범위 표에 싣지 않은 대표 커밋은 회원·인증 5632084d 4fcc80c3 7baffb26 727501fb, 게이트웨이 b40ca9e9, 판매자 8e3decda a8aad588입니다.