멀티테넌트 회원·인증 설계 4편 – 통합회원 서비스 지원 플랫폼에 실제로 적용해 보니

·2025년 11월 27일
post-thumbnail

🧬 개요

앞선 글에서:

  • 1편: tenant / user / tenant_user 도메인 패턴
  • 2편: JWT + Redis 기반 인증 구조
  • 3편: 역할/권한(RBAC) 설계 패턴

까지 개념 위주로 정리했다.

이번 4편은 이 개념들을 실제 프로젝트에 어떻게 녹였는지,
그리고 구현 레벨에서 어떤 선택지가 있고 나는 무엇을 택했는지까지 함께 정리한 글이다.

가상의 이름을 쓰면,
“여러 서비스가 입점해서 공통 회원을 쓰는 통합회원 서비스 지원 플랫폼”
에서 이 구조들을 어떻게 적용했는지 소개하는 형태다.

  • 어떤 요구사항에서 멀티테넌트 관점이 필요했는지
  • tenant / user / tenant_user를 어떻게 매핑했는지
  • JWT/Redis/RBAC를 실제로 어디까지 썼는지
  • Spring Security에서 권한 체크를 어떻게 구성했는지
  • “메뉴 기반 권한 테이블 + API 매핑”을 썼던 경험과, 그 장단점
  • 여러 구현 옵션 중 어떤 방식을 선택했는지

까지 담아보려고 한다.


✨ TL;DR

  • 요구사항 한 줄 요약:

    “여러 입점 서비스가 하나의 회원 풀을 공유하지만,
    각 서비스별로 가입/약관/권한/상태는 따로 관리하고 싶다.”

  • 도메인 모델:

    • tenant → 입점 서비스
    • user → 통합 회원(공통 계정)
    • tenant_user → 서비스별 회원 관계(상태/약관/역할)
  • 인증:

    • JWT Access Token + Redis 기반 Refresh Token
    • 토큰 클레임에 tenant_id, tenant_user_id, roles 포함
    • Redis 키: auth:refresh:{tenantId}:{userId}:{deviceId}
  • RBAC:

    • 플랫폼 공통 역할: PLATFORM_ADMIN
    • 서비스(tenant) 공통 역할: TENANT_ADMIN, TENANT_USER
    • permission은 기능 단위로 적당히 묶어서 관리
  • 구현 레벨 설계:

    • API → permission 매핑은 코드에서 고정
      (예: @PreAuthorize("@authz.hasPermission('ORDER_READ')"))
    • role → permission 매핑은 DB에서 관리 + 애플리케이션 로컬 캐시
    • 메뉴 기반 권한 구조(user_rolerole_menumenu_api 조인)도 썼었고,
      “메뉴에 없는 API는 화이트리스트”로 푸는 식으로 운용했던 적이 있다.
  • 처음부터 “모든 걸 커스터마이징 가능한 IAM”을 만들기보다는,

    • 입점 서비스가 바로 써먹을 수 있는 최소한의 멀티테넌트 구조를 먼저 잡고
    • 이후 확장할 수 있는 여지를 남겨두는 쪽을 선택했다.

1. 서비스 배경 – 입점 서비스들이 공유하는 통합회원 플랫폼

플랫폼 컨셉은 대략 이랬다.

  • 여러 개의 입점 서비스(앱/웹)가 존재한다.
  • 사용자는 한 번 계정 생성 후,
    • 입점 서비스마다 별도의 회원 가입 없이
    • “해당 서비스 이용에 필요한 최소 동의”만 거치고 이용할 수 있어야 한다.
  • 입점 서비스 입장에서는:
    • “우리 서비스에 아직 가입하지 않은 고객”과
    • “한 번이라도 가입/이용해 본 고객”을 구분하고 싶어 하고,
    • 서비스마다
      • 약관,
      • 알림 수신 동의,
      • 회원 상태/등급/역할
        이 다를 수 있다.

추가 요구도 있었다.

  • 추후 B2B 성격의 입점사(기업 파트너)가 들어올 수도 있어
    • 서비스별 “관리자 계정” 개념이 필요할 수 있다.
  • 통합회원 플랫폼은 입점 서비스들의 공통 인증·회원 시스템으로 동작해야 하고,
    • API를 통해 각 서비스와 연동된다.

이 흐름에서 자연스럽게:

  • “입점 서비스 = tenant”
  • “통합 회원 = 공유 user 풀”
  • “서비스별 가입 상태/권한 = tenant_user”

라는 멀티테넌트 관점으로 바라보는 게 설계하기 편했다.


2. 도메인 설계 – tenant / user / tenant_user 매핑

2.1 핵심 테이블 구조

플랫폼의 기본 도메인 구조는 다음과 같이 정리했다.

user  -- 통합 회원(공통 계정)
---------
id (PK)
email
password_hash
name
phone
status          -- 통합 계정 단위의 상태(정상/잠김/삭제 등)
...

tenant  -- 입점 서비스
---------
id (PK)
code
name
status          -- 서비스 상태(정상/중지 등)
...

tenant_user  -- 서비스별 회원 관계
---------
id (PK)
tenant_id      (FK -> tenant.id)
user_id        (FK -> user.id)
status         -- 가입, 휴면, 탈퇴, 차단 등 (서비스 단위)
role_code      -- TENANT_ADMIN / TENANT_USER 등
agreed_terms_ver   -- 서비스별 약관 동의 버전
agreed_marketing   -- 서비스별 마케팅 동의 여부
joined_at
...

역할/권한까지 확장하면:

role
---------
id
code           -- ex) PLATFORM_ADMIN, TENANT_ADMIN, TENANT_USER
scope          -- GLOBAL / TENANT
name

permission
---------
id
code           -- ex) ORDER_READ, ORDER_MANAGE, USER_MANAGE
name

role_permission
---------------
role_id        (FK)
permission_id  (FK)

tenant_user_role
-----------------
tenant_user_id (FK)
role_id        (FK)

의미는 단순하다.

  • user:

    • 이메일/비밀번호/휴대폰 등 공통 계정 정보를 가진 “사람”
  • tenant:

    • 입점 서비스(혹은 입점사/브랜드)
  • tenant_user:

    • “user가 특정 tenant에 가입했는가?”

      • 했다면 상태/약관/역할 정보는 무엇인가?
  • role, permission, role_permission, tenant_user_role:

    • “이 테넌트에서 이 회원이 어떤 역할을 가지고,
      그 역할이 어떤 권한(permission)을 포함하는가?”를 표현

3. 인증 구조 – JWT + Redis를 어디까지 썼나

3.1 Access / Refresh 전략

2편에서 정리한 패턴을 그대로 적용했다.

  • Access Token

    • 짧은 TTL (예: 15분 내외)
    • API 호출 시 Authorization: Bearer ... 로 사용
  • Refresh Token

    • 길게 (예: 14~30일)
    • Redis에 저장하고, 재발급 시 검증/회전

3.2 토큰 클레임 설계

입점 서비스 기준으로는
“어떤 테넌트 컨텍스트에서 로그인했는가”가 핵심이라, Access Token에는 다음 정보를 넣었다.

{
  "sub": "user-12345",
  "tenant_id": "svc-001",
  "tenant_user_id": "svc001-user-6789",
  "roles": ["TENANT_USER"],
  "iat": 1730000000,
  "exp": 1730003600
}
  • tenant_id
    → 이 토큰이 어떤 서비스 컨텍스트에서 발급되었는지
  • tenant_user_id
    tenant + user 조합을 나타내는 키 (있으면 도메인 조회가 편해짐)
  • roles
    → 이 서비스 내에서의 역할 (일반 사용자 / 관리자 등)

통합회원 플랫폼은 하나의 user가 여러 tenant에 가입할 수 있고,
각 테넌트마다 다른 상태/역할을 가질 수 있다.

그래서 토큰은 항상 특정 tenant 컨텍스트를 기준으로 발급하도록 했다.

3.3 Redis 키 스킴

여러 서비스에서 동시에 로그인할 수 있기 때문에,
Refresh Token은 tenantId + userId + deviceId 단위로 관리했다.

auth:refresh:{tenantId}:{userId}:{deviceId}
  -> { refreshToken, issuedAt, expiresAt, userAgent, ip, ... }
  • 같은 유저라도 서비스마다, 단말마다 세션을 분리해서 관리할 수 있다.
  • “이 서비스에서만 모든 기기 로그아웃” 같은 기능을 만들기 쉽다.

Access Token은 TTL을 짧게 가져가고,
Refresh Token을 삭제하는 것만으로도 대부분의 요구사항을 커버할 수 있었다.


4. RBAC – 플랫폼 vs 서비스 역할 나누기

입점 구조 특성상, 역할은 두 레벨로 나눴다.

  1. 플랫폼 전체 역할 (GLOBAL)
  2. 서비스(tenant) 내부 역할 (TENANT)
  • PLATFORM_ADMIN
    → 통합회원 플랫폼 운영자 (모든 tenant 데이터에 접근)
  • TENANT_ADMIN
    → 특정 서비스의 관리자
  • TENANT_USER
    → 그 서비스의 일반 사용자

초기에는 TENANT_ADMIN, TENANT_USER 정도의 작은 세트만으로도
입점 서비스들이 필요로 하는 대부분의 시나리오는 커버할 수 있었다.

권한(permissions)은 API 하나하나까지 잘게 쪼갰다기보다는,
기능 묶음 단위로 정의했다.

예:

  • USER_MANAGE – 서비스 내 사용자 관리/조회
  • ORDER_MANAGE – 주문 관련 관리
  • STATS_VIEW – 통계 조회

그리고 역할은 이러한 permission을 묶어서 구성했다.


5. 구현 레벨 – Spring Security + JWT + RBAC 조합하기

요구는 이랬다.

  • 컨트롤러 단위로

    • “이 API는 ADMIN만 가능”
    • “이 API는 ADMIN + MANAGER 가능”
  • 이런 정책을 운영 중에 바꿀 수 있으면 좋겠다.

그래서 먼저 설계 옵션들을 정리해 보고,
그중 실제로 취한 선택과, 과거에 경험했던 다른 방식까지 비교해보는 식으로 접근했다.

5.1 구현 옵션 정리

옵션 A – JWT에 permission까지 직접 넣기

  • 로그인 시:

    1. tenant_user의 role 목록 조회
    2. role → permission 매핑 계산
    3. permission 리스트를 JWT 클레임에 포함
  • 장점

    • API 호출 시 DB/Redis 조회 필요 없이 토큰만 보고 권한 판단 가능
    • 성능상 가장 단순하고 빠른 경로
  • 단점

    • role/permission 구성이 바뀌어도,
      이미 발급된 Access Token의 만료(exp)까지는 이전 권한이 유지
    • permission이 많아지면 토큰이 비대해질 수 있음

옵션 B – JWT에는 role만, role → permission은 서버/캐시에

  • JWT에는 roles만 담고,

  • “이 role이 어떤 permission을 가지는지”는 서버에서 관리

  • 장점

    • role/permission 구성이 바뀌면 토큰 재발급 없이도 서버에서 바로 반영 가능
    • 토큰이 가벼움 (permission 목록이 길어져도 상관 없음)
  • 단점

    • role → permission 구조를 어딘가에 캐싱/관리해야 함
    • 멀티 인스턴스 환경이면 캐시 동기화 전략이 필요

옵션 C – endpoint → permission까지 DB로 관리

  • GET /api/ordersORDER_READ

  • POST /api/ordersORDER_MANAGE
    같은 매핑을 endpoint_permission 테이블로 관리하는 방식

  • 장점

    • 운영 화면에서 “이 API를 어떤 permission으로 보호할지”를 변경 가능
  • 단점

    • 구현 난이도/복잡도가 높고,
    • URL/리팩토링, 리버스 프록시 구조와도 긴밀하게 엮인다.
    • 디버깅 난이도가 올라간다.

→ 이 정도까지 유연성이 꼭 필요하지 않다면,
보통은 A/B 조합 선에서 정리하는 편이 더 현실적이다.


5.2 이 프로젝트에서 기본적으로 선택한 방식

이 플랫폼에서는 다음과 같이 정리했다.

  1. API → permission 매핑은 코드에서 고정

    • 각 API는 “어떤 permission이 있어야 호출 가능한지”만 선언한다.
    • 예: ORDER_READ, ORDER_MANAGE, USER_MANAGE
  2. role → permission은 DB에서 관리 + 애플리케이션 로컬 캐시

    • 운영자가 “어떤 ROLE에게 어떤 permission을 줄지”는
      DB/관리 화면에서 동적으로 바꿀 수 있다.
  3. JWT에는 role만 포함

    • permission은 토큰에 싣지 않고,
    • 요청 시 현재 사용자의 role → permission 매핑을 캐시에서 가져와 판단한다.

컨트롤러 단 예시

// 주문 목록 조회 – ORDER_READ 권한 필요
@PreAuthorize("@authz.hasPermission('ORDER_READ')")
@GetMapping("/api/orders")
public List<OrderDto> listOrders() { ... }

// 주문 생성 – ORDER_MANAGE 권한 필요
@PreAuthorize("@authz.hasPermission('ORDER_MANAGE')")
@PostMapping("/api/orders")
public OrderDto createOrder(...) { ... }

여기서 "ORDER_READ", "ORDER_MANAGE"API의 책임이고,
“어떤 ROLE이 이 permission을 가지는가”는 DB/관리화면에서 결정한다.

authz.hasPermission 구현 예시

@Component("authz")
public class AuthorizationHelper {

    private final PermissionService permissionService;

    public AuthorizationHelper(PermissionService permissionService) {
        this.permissionService = permissionService;
    }

    public boolean hasPermission(String requiredPermission) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null || !auth.isAuthenticated()) {
            return false;
        }

        TenantUserPrincipal principal = (TenantUserPrincipal) auth.getPrincipal();

        // 1) 이 tenant_user가 가진 permission 목록 조회 (캐시 활용)
        Set<String> userPermissions =
                permissionService.getPermissionsOfTenantUser(principal.getTenantUserId());

        // 2) requiredPermission 포함 여부 확인
        return userPermissions.contains(requiredPermission);
    }
}

TenantUserPrincipal은 JWT에서 읽어온 userId, tenantId, tenantUserId, roles 등을 들고 있는 커스텀 Principal이라고 보면 된다.

PermissionService의 역할

public interface PermissionService {
    Set<String> getPermissionsOfTenantUser(Long tenantUserId);
}

구현체에서는:

  1. tenant_user_id가 가진 role 목록 조회
  2. role → permission 매핑(캐시)을 이용해서 permission 세트 계산
  3. 결과를 일정 시간 로컬 캐시에 저장

정도까지만 하면,
권한 체크 시에는 Set.contains() 정도의 비용으로 판단할 수 있다.


5.3 캐싱 전략 – Redis vs 로컬 캐시

RBAC 정보는 “자주 바뀌지 않는 데이터”라서,
매 요청마다 Redis/DB를 조회하는 구조는 의도적으로 피했다.

정리한 전략은 다음과 같다.

  1. role → permission 매핑

    • 애플리케이션 로컬 캐시에 올려둔다.
      (예: Caffeine, ConcurrentHashMap + 리로드)

    • 권한 설정이 바뀌면:

      • 관리 화면에서 “권한 설정 갱신” 버튼을 두거나,
      • Redis pub/sub, “버전 키” 등을 이용해 캐시를 무효화
  2. tenant_user → role 목록

    • 로그인 시점에 한 번 조회하고,
    • 필요하면 tenant_user_id 기준으로 짧은 TTL 로컬 캐시 사용
  3. Redis의 역할

    • 이 RBAC 구조에서는 “권한 체크용 데이터”를 Redis에 계속 두지 않고,
    • 주로 Refresh Token/세션, 전역 설정, 캐시 무효화 신호 등에 사용

5.4 실무에서 썼던 다른 방식 – 메뉴 기반 권한 + API 매핑

위 구조를 정리하기 전에,
실제로는 메뉴 중심의 권한 구조를 먼저 운영했던 적도 있다.

당시 상황은 이랬다.

“어떤 API가 어떤 책임인지도 아직 명확히 정리되지 않은 상태였고,
화면(UI) 기준으로 ‘어떤 메뉴에 어떤 사람이 들어갈 수 있는지’를 먼저 정의한 다음,
그 메뉴가 내부적으로 호출하는 API를 매핑해서 권한을 풀어갔다.”

도메인/테이블 구조는 대략 이런 느낌이었다.

user_role        -- 사용자별 역할
---------
user_id
role_id

role_menu        -- 역할별 메뉴 접근 권한
---------
role_id
menu_id

menu_api         -- 메뉴에서 호출되는 API 매핑
---------
menu_id
http_method      -- GET / POST ...
path_pattern     -- /api/orders, /api/users/** 등

권한 체크 로직은:

  1. 로그인한 사용자의 역할 목록 조회 (user_role)

  2. 그 역할들이 접근 가능한 메뉴 목록 조회 (role_menu)

  3. 그 메뉴들이 호출하는 API 목록 조회 (menu_api)

  4. 이 세 테이블을 조인해서 user별로 허용된 API 리스트를 계산

  5. 그 리스트에 없는 API들은

    • 별도 화이트리스트(항상 허용) 로 처리하거나
    • 아예 “메뉴에서 접근하지 않는 내부 API”로 간주

즉,

  • “메뉴에 연결된 API만 권한 체크 대상”
  • “메뉴에 매핑되지 않은 API는 화이트리스트”

라는 구조였고,
메뉴 + 역할만 관리하면 자연스럽게 API 접근 제어가 따라오는 형태였다.

이 방식의 장점

  • UI 관점과 권한 관점이 일치

    • 운영자가 보는 것도 “메뉴 트리”이고,
      권한 설정도 “이 메뉴에 어느 역할이 들어갈 수 있는지” 기준이라 직관적이다.
  • “API 책임이 아직 정리되지 않은 상황”에서
    당장 권한 관리 기능을 만들어야 할 때 현실적인 타협안이 된다.

  • 메뉴에 안 매핑된 API는

    • health check, 공통 공개 정보 조회,
    • 내부 배치용 API
      같이 예외적으로 다뤄야 하는 엔드포인트에 쓰기 쉬웠다.

이 방식의 한계 / 트레이드오프

지나고 보니, 몇 가지 아쉬운 점도 분명했다.

  1. 권한 모델이 UI 구조에 너무 종속된다

    • 메뉴 ID가 곧 permission처럼 동작하다 보니,
    • 메뉴 트리를 리팩토링하거나 화면 설계를 크게 바꾸면
      권한 정책도 같이 요동친다.
    • “같은 API가 여러 메뉴에서 호출되는” 상황에서는
      추론이 더 어려워지기도 했다.
  2. API 단위 책임이 선명하게 보이진 않는다

    • “이 API는 ORDER_READ 권한이 필요하다”처럼
      도메인 단어로 설명되는 권한 모델이 아니라,
    • “이 API는 X 메뉴에 매핑되어 있고,
      그 메뉴에 접근 가능한 사람만 쓸 수 있다”에 가까운 구조였다.
    • 시간이 지날수록 메뉴 구조와 API 책임이 조금씩 어긋날 위험이 있었다.
  3. 화이트리스트 관리에 신경 써야 한다

    • 메뉴에 매핑되지 않은 API를 다 화이트리스트로 눌러버리면,

    • 어느 순간 의도치 않게 열려 있는 엔드포인트가 생길 수 있다.

    • “내부 호출만 있어야 하는 API인데,
      외부에서 직접 호출이 가능한 상태로 남아 있는” 케이스를 피하려면

      • 네트워크 레벨(internal LB, VPN 뒤에 두기),
      • 인증 방식 분리 등으로 보완이 필요했다.
  4. 조인이 복잡하고, 성능/복잡도도 신경 써야 한다

    • user → role → menu → api 조인 쿼리는
      권한 계산 시점마다 수행하기에는 무거울 수 있다.
    • 실무에서는 로그인/세션 초기화 시점에 한 번 계산하고 캐싱하거나,
      menu_api 자체를 미리 role별로 플랫하게 풀어놓는 방향으로 최적화가 필요했다.

그럼에도 불구하고,
“메뉴 중심 사고가 강한 조직/프로젝트에서,
권한 체계와 UI를 한 번에 다룰 수 있었다”
는 점에서
당시로서는 현실적인 선택이었던 방식이기도 했다.

이번 시리즈에서 정리한 RBAC 구조는,
이런 메뉴 기반 구조의 장단점을 한 번 겪고 나서:

  • 메뉴와 상관없이,
  • 도메인 관점의 permission 이름으로,
  • API 책임을 명확히 표현하고 싶어서

그 위에 다시 정리해 본 버전에 가깝다.


6. 좋았던 점과 트레이드오프

6.1 좋았던 점

  1. 입점/통합 구조 설명이 쉬웠다

    도메인 모델을 tenant / user / tenant_user로 정리해놓으니,

    • 신규 입점 서비스에게 구조 설명할 때,
    • 내부 개발자에게 “어디에 무엇이 있는지” 설명할 때,

    둘 다 “하나의 user, 여러 tenant_user 관계”라는 그림 하나로 전달이 가능했다.

  2. 입점 서비스 추가/제거가 구조적으로 안전했다

    • 신규 서비스 추가: tenant 레코드 하나 추가
    • 서비스 종료: 해당 tenant 관련 데이터(tenant_user, 로그 등)만 정리

    다양한 입점 서비스가 들어왔다 나가는 구조에서
    “서비스가 도메인 모델의 1급 시민”이라는 것이 꽤 큰 장점이었다.

  3. 권한 정책 변경을 배포 없이 처리할 수 있었다

    • “이 API를 이제 MANAGER도 쓸 수 있게 해달라” 같은 요구가 들어오면:

      • API → permission 매핑은 그대로 두고,
      • role → permission 매핑만 DB에서 수정하면 된다.
    • JWT에는 role만 포함하고 permission은 캐시에서 계산하기 때문에
      토큰 재발급 없이도 정책 변경을 반영할 수 있었다.

  4. 메뉴 기반 권한 구조를 거쳐 온 덕분에, RBAC를 “현실적인 감각”으로 설계할 수 있었다

    • 실제로는 메뉴 기반 user_rolerole_menumenu_api 조인 구조를 먼저 써봤고,

    • 그 경험 덕분에

      • UI 구조에 너무 종속되지 않으면서,
      • 도메인 관점의 permission을 유지하는 게 왜 중요한지
        를 체감할 수 있었다.

6.2 트레이드오프/고민 지점

  1. 모든 곳에 tenant를 끌고 다녀야 한다

    • 서비스/리포지토리/쿼리마다

      • userId만 사용하고 싶은 순간에도
      • 대부분 tenantId를 함께 받아야 했다.
    • 코드가 조금 더 길어지지만,

      • “tenant 없이 동작하는 핵심 로직”이 남지 않는다는 점에서는
        일종의 안전장치 역할을 했다.
  2. 플랫폼 운영자(PLATFORM_ADMIN) 권한은 별도 고려가 필요하다

    • 대부분의 권한 체크는 tenant_user 기준으로 돌아가지만,

    • 플랫폼 운영자는 여러 테넌트 데이터를 한 번에 봐야 한다.

    • 그래서:

      • 운영자 전용 API,
      • 운영자용 audit log
        를 분리해서 설계하는 쪽이 더 안전했다.
  3. 메뉴 기반 권한 구조와 RBAC 구조를 혼합 사용할 때의 복잡도

    • 일부 시스템/관리 화면은 여전히 “메뉴 단위”로 권한을 보고 싶어했고,
    • 백엔드/도메인 관점에서는 “permission 단위”로 보는 게 더 자연스러웠다.
    • 둘 사이를 연결하는 계층(예: menu → permission 매핑)을 잘 설계해 두지 않으면
      특정 메뉴/화면에서 실제 호출되는 API 권한을 추적하기가 점점 어려워진다.

7. 비슷한 플랫폼을 설계한다면 체크해볼 포인트

“여러 서비스가 입점하는 통합회원 플랫폼”을 새로 설계한다면
아래 질문들을 한 번씩 점검해 볼 만하다.

7.1 도메인/테넌트 구조

  • 우리 서비스에서 tenant에 해당하는 단위는 무엇인가?
    (입점 서비스인지, 고객사인지, 브랜드인지)

  • 한 사용자가 여러 tenant에 가입할 수 있는가?

  • 서비스별/조직별로

    • 가입 상태,
    • 약관,
    • 마케팅 동의,
    • 역할/등급
      이 달라지는가?

→ 그렇다면 tenant / user / tenant_user 패턴을 그대로 적용하기 좋다.

7.2 인증/세션

  • 클라이언트 입장에서:

    • “서비스별 세션”이 필요한가,
    • 아니면 “플랫폼 통합 세션” 하나만 있으면 되는가?
  • 로그아웃/강제 로그아웃을

    • 서비스 단위로 할지,
    • 계정 전체로 할지,
    • 둘 다 필요할지?

→ 필요 수준에 따라
auth:refresh:{tenantId}:{userId}:{deviceId}와 비슷한 키 스킴을 참고할 수 있다.

7.3 RBAC & 메뉴 구조

  • 최소한 필요한 역할은 어떤 것들인가?
    (플랫폼 운영자 / 서비스 관리자 / 일반 사용자 정도면 충분한가?)

  • role → permission 관계를

    • 코드에 고정할지,
    • DB/관리 화면에서 운영할지?
  • 메뉴 구조와 권한 구조를

    • 어디까지 묶을지,
    • 어디부터는 분리된 계층으로 볼지?

✅ 마무리

이번 4편에서는
1~3편에서 정리한 개념들을

“여러 서비스가 입점하는 통합회원 서비스 지원 플랫폼”

이라는 시나리오에 실제로 적용해 보면서,

  • 도메인 모델(tenant / user / tenant_user)
  • JWT + Redis 기반 인증 구조
  • 플랫폼/테넌트 역할을 나눈 RBAC
  • Spring Security 기반 구현 방식
    (API → permission은 코드, role → permission은 DB + 캐시)
  • 그리고, 한 단계 전에는
    메뉴 기반 권한(user_rolerole_menumenu_api) 조인 구조를 사용했던 경험까지

함께 정리했다.

개념적으로는 선택지가 여러 가지였지만,

  • 토큰에는 role만 담고,
  • API 단위 permission은 코드로 고정하고,
  • role → permission은 DB에서 동적으로 관리하는 방식이

이 플랫폼의 요구사항과 규모에서는 적당한 균형점이라고 판단했다.
메뉴 기반 권한 구조는 “UI와 권한을 동시에 다루기 쉬운 현실적인 선택지”였고,
그 경험 위에서 보다 도메인 친화적인 RBAC 구조를 재정의할 수 있었다.

다음 편에서는 이 구조를 인프라 레벨까지 확장해서,

B2B SaaS 관점에서의 테넌트별 스케일링·분리·Rate Limit 전략을 정리해 볼 예정이다.


참고 문헌

  1. Spring 공식 문서, “Method Security”, Spring Security Reference.

  2. Spring Security API Docs, “@PreAuthorize Annotation”.

  3. Baeldung, “Introduction to Spring Method Security”.

  4. Okta Developer Blog, “Spring Method Security with PreAuthorize”.

  5. Kraksaprofessional, “Spring Boot – Implementing Method Level Security with @PreAuthorize”, Medium.

profile
하나씩 꽉꽉 눌러 담는 실무 개발 노트 ✍️📦

0개의 댓글