Skip to content

Refactor: 권한/기수 시스템 리팩토링 - #562

Merged
cowboysj merged 10 commits into
developfrom
refactor/cohort-role-system
Sep 29, 2026
Merged

cowboysj merged 10 commits into
developfrom
refactor/cohort-role-system

Conversation

@cowboysj

Copy link
Copy Markdown
Member

Summary


1. 개요

역할을 MASTER / CORE / ORGANIZER / DEEPER / GUEST 다섯 가지로 고정하고, member_roles.cohort_id와 cohorts.is_active로 역할과 기수를 분리합니다. 활성 기수는 application.yml 하드코드가 아닌 DB와 전환 API만으로 관리합니다. 구시스템 테이블(member_authorities, authorities)을 제거하며, PATCH /v1/roles/members/{memberId} 요청 body가 Breaking change입니다. 로컬 스모크 테스트 16개 케이스를 통과하였습니다.


2. 배경

기존 시스템에서 다음 문제가 확인되었습니다.

  1. 역할이 기수와 결합되어 있어 새 기수마다 roles 테이블에 row가 추가됩니다("17기 운영진", "18기 디퍼" 등).
  2. role_permissions 매핑이 실질적인 구분 없이 모든 role에 36개 권한 전체를 부여합니다.
  3. 권한 저장이 member_authorities, member_roles, member_permissions 세 테이블에 분산되어 있습니다.
  4. member_cohorts에 유니크 제약이 없어 실제 중복 데이터가 존재합니다.
  5. "현재 기수" 결정 경로가 application.yml(하드코드)과 getLatestCohort()(DB 조회) 두 가지로 혼재되어 있습니다.

이번 리팩토링은 표준 RBAC 구조를 유지한 상태에서 역할을 다섯 가지로 고정하고, 기수 정보를 별도 컬럼으로 분리하여 위 문제들을 해소합니다.


3. 주요 변경 사항

3-1. 도메인 및 엔티티

변경 내용 대상 사유
RoleType에 Master 추가, alias map에 "master", "마스터" 등록 RoleType.kt canonical role 다섯 개 중 MASTER 부여·판정 경로 확보
MemberRole aggregate에 cohortId: CohortId? 추가 MemberRole.kt 역할별 기수 정보를 명시적으로 저장
MemberRoleAssignment VO 신설 MemberRoleAssignment.kt (roleName, cohortId) 튜플 기반 판정 로직을 순수 함수화
Cohort aggregate에 isActive, activatedAt 추가 Cohort.kt 활성 기수 정보를 데이터로 명시
CohortEntity / MemberRoleEntity 매핑 반영 엔티티 계층 Aggregate 변경 사항 반영
MemberPermissionEntity에 "READ 되지 않음" 안전장치 주석 MemberPermissionEntity.kt orphan 데이터 유지 방침에 따른 오용 방지
MemberAuthorityPersistencePort 삭제 domain port 구시스템 제거

3-2. 서비스 계층

변경 내용 대상 사유
CurrentCohortRoleResolver.isAssignmentEffective()에 새 판정 규칙 구현 CurrentCohortRoleResolver.kt MASTER·GUEST는 항상 유효, CORE·ORGANIZER·DEEPER는 활성 기수 참여자만 유효
ROLE_PRIORITY 최상위에 Master 배치 상동 대표 role 선택 우선순위 반영
CohortRoleService.createLatestCohortRoles() 비활성화 CohortRoleService.kt 기수별 role row 생성 중단
CohortCommandService.activateCohort() 단일 active 보장 트랜잭션 CohortCommandService.kt 활성 기수 전환 시 기존 active 자동 해제
CohortRepository.activate()에서 deactivateAll 후 activate CohortRepository.kt 활성 기수 단일성 보장
CohortQueryService.getActiveCohort()를 DB 조회로 통일 CohortQueryService.kt application.yml의 cohort.value 하드코드 제거
MemberAuthorityService, CohortProperties 삭제 application 구시스템·하드코드 활성 기수 설정 제거
AfterPartyCommandService의 when(roleType)에 Master 케이스 추가 AfterPartyCommandService.kt Sealed class 확장에 따른 exhaustive when 유지

판정 규칙 요약

  • MASTER: 기수 무관 항상 유효
  • CORE / ORGANIZER / DEEPER: 활성 기수 참여자만 유효 (미참여 시 필터링·자동 회수)
  • GUEST: 로그인만 가능, 부여 권한 0개

활성 기수 업데이트 경로는 PATCH /v1/cohorts/{cohortId}/activate API 하나뿐입니다. application.yml 하드코드는 완전히 제거되었습니다.

3-3. 컨트롤러 및 API

변경 내용 대상 사유
신규 endpoint: GET /v1/cohorts/active, PATCH /v1/cohorts/{cohortId}/activate CohortAdminController.kt, CohortAdminApi.kt 활성 기수 조회·전환 API 제공
UpdateMemberRoleRequest body를 {cohort, isAdmin}에서 {roleType, cohortId}로 변경 UpdateMemberRoleRequest.kt 도메인 모델과의 정합성 확보. roleType 정규식에 MASTER 포함
ConvertDeeperToOrganizerRequest 삭제 member presentation 중복 endpoint /v1/members/authority/organizer 정리

3-4. 인프라 및 설정

변경 내용 대상 사유
application.yml, application-local.yml에서 cohort.value 하드코드 제거 resources 활성 기수는 DB를 단일 source of truth로 사용
SQL 마이그레이션 3개 파일로 통합 prod/pending/20260913_*.sql 기존 12개 스크립트 통합. 각 파일에 VERIFY·ROLLBACK 주석 포함

4. API 변경 요약

4-1. Breaking change (요청 body 형식)

PATCH /v1/roles/members/{memberId} 요청 body 형식이 변경되었습니다.

구분 body
변경 전 { "cohort": "17", "isAdmin": true }
변경 후 { "roleType": "MASTER | CORE | ORGANIZER | DEEPER | GUEST", "cohortId": 17 }
  • roleType 유효값은 MASTER | CORE | ORGANIZER | DEEPER | GUEST 입니다.
  • 옛 형식으로 호출 시 400 응답(roleType: 필수 입력값입니다)이 반환됩니다.
  • validation은 다섯 값을 모두 허용하지만, 서비스 계층은 여전히 ORGANIZER, DEEPER에 대해서만 실제 부여를 허용합니다. MASTER / CORE 부여는 별도 슈퍼어드민 API로 관리할 예정입니다.

4-2. 삭제된 endpoint

Endpoint 결과
PATCH /v1/members/authority/organizer 404 (GLOBAL-404-01)

대체: PATCH /v1/roles/members/{memberId}에 { "roleType": "ORGANIZER", "cohortId": <기수 ID> }로 호출합니다.

4-3. 신규 endpoint

Endpoint 목적 권한
GET /v1/cohorts/active 활성 기수 조회 인증 필요
PATCH /v1/cohorts/{cohortId}/activate 활성 기수 전환 (기존 active 자동 해제) update:cohort

활성 기수 전환은 트랜잭션 내에서 기존 활성 기수 해제와 새 활성 기수 지정을 원자적으로 수행합니다.

5. DB 마이그레이션

5-1. 파일 구성

실행 순서 파일 요약
1 prod/pending/20260913_backup_member_roles.sql member_roles에 legacy_role_id, legacy_role_name 추가 및 원본 값 복사
2 prod/pending/20260913_role_system_seed.sql cohorts.is_active / activated_at, cohort_id=0 특수 슬롯; roles 다섯 개 시드와 role_permissions 매트릭스(MASTER 23, CORE 23, ORGANIZER 17, DEEPER 7, GUEST 0); member_roles.role_id 재매핑; member_cohorts 중복 제거 및 UNIQUE; member_authorities / authorities 아카이빙 후 DROP
3 prod/pending/20260913_schema_alignment.sql member_roles.cohort_id 및 인덱스; legacy_role_name 파싱으로 cohort_id 채우기; canonical 이외 roles 아카이빙 후 삭제; member_cohorts.cohort_value DROP

각 파일 하단에 읽기 전용 VERIFY 섹션과 역순 복구용 ROLLBACK 주석이 포함되어 있습니다.

5-2 롤백 절차

각 SQL 파일 하단의 ROLLBACK 주석에 포함된 쿼리를 역순으로 실행합니다.

  1. 20260913_schema_alignment.sql 롤백 — _archive_roles_20260913 테이블에 의존합니다.
  2. 20260913_role_system_seed.sql 롤백 — _archive_member_authorities_20260913, _archive_authorities_20260913와 legacy_role_id를 이용해 원본 역할을 복원합니다.
  3. 20260913_backup_member_roles.sql 롤백 — legacy 컬럼을 DROP합니다.

주의: role_permissions 원본 매트릭스는 별도 스냅샷이 없으면 완전 복원이 불가합니다.


6. FE 대응 필요 사항

  • PATCH /v1/roles/members/{memberId} 요청 body를 신 형식(roleType, cohortId)으로 교체

- roles 5개 canonical(MASTER/CORE/ORGANIZER/DEEPER/GUEST) 고정
- member_roles.cohort_id 컬럼 도입 및 활성 기수 기반 판정 로직
- cohorts.is_active + PATCH /v1/cohorts/{id}/activate 신설
- member_authorities/authorities 구시스템 제거
- SQL 마이그레이션 스크립트 prod/pending/20260913_*.sql 추가
- application/build.gradle.kts: mainClass를 CoreApplicationKt에서 CoreApplication으로 원복 (develop 시점 값)
- prod/pending/20260913_*.sql 3개를 20260914_*.sql로 리네임 및 파일 내부 날짜 참조 갱신
@coderabbitai

coderabbitai Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 16ea2b64-e5e0-44cc-aafa-3a317b78e057


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cowboysj cowboysj self-assigned this Sep 14, 2026
…le-system

# Conflicts:
#	application/src/main/resources/application.yml
#	persistence/src/main/kotlin/core/persistence/member/repository/MemberRepository.kt
@cowboysj
cowboysj force-pushed the refactor/cohort-role-system branch from 86a0a47 to aa7628e Compare September 14, 2026 12:17
@cowboysj cowboysj changed the title Refactor/cohort role system [Refactor] 권한/기수 시스템 리팩토링 Sep 14, 2026
@uykm

uykm commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

고생하셨어요!
변경사항이 커서 코드에 코멘트에 남기긴 좀 어려울 것 같고, 일단 여기에 한 번에 남겨보겠습니다 😂

  1. 코어도 디프만 기수를 활용하기로 했던 게 코드엔 반영이 된 것 같은데, 마이그레이션은 반영이 안된 것 같더라구요.
  • 20260914_schema_alignment.sql 이 파일에서 cohort_id를 ORGANIZER/DEEPER에만 채우고있던데, CORE도 cohort_id를 지정해줘야 할 것 같아요
  1. "N기 운영진/디퍼", '(N-16) 코어"로 조회되어야 하는 부분들이 그냥 "ORGANIZER", "DEEPER", "CORE"로 반환이되는 것 같은데 확인 한 번 부탁드려요!

  2. 멤버 승인 로직에 문제가 있긴 하네요

  • 멤버 승인할 때 활성 기수 Deeper로 자동 승인되는 구조인데, 승인 요청할 때 어떤 권한, 기수을 요청하는건지를 지금 안받고 있다보니까 이렇게 되어있는 것 같거든요
  • 나중에 아래와 같은 식으로 바뀐다고 하면, 예를 들어 "17기 디퍼였던 사람이 18기에 다시 지원"한다고 했을 때, "17기 디퍼"라는 데이터는 soft delete 처리되고, "18기 디퍼"라는 데이터가 생성되는 구조입니다
    • 그래서 일단 승인했을때 해당 member_roles에 새로운 레코드만 쌓는 걸로 수정하면 되지 않을까 싶어요.
image
  1. 현재 활성 기수가 없으면 MASTER 외 전원이 GUEST 처리되는 구조인 것 같아요.
  • GUEST는 아닌데 활성 기수가 아닌 멤버들을 위한 권한을 또 만들 필요는 없을 것 같고, CurrentCohortRoleResolver에서 Guest가 아니고 활성기수도 아닌지 검증하는 메서드 추가하면 될 것 같네요.
    (미리 추가할 필요도 없긴 한데, 일단 나중에 놓칠 수도 있어서!)
  • 현재 활성 기수가 없으면 MASTER 외 전원이 GUEST 처리되는 구조인 것 같아요.
  1. 운영진이나 디퍼 권한 부여는 있는데 MASTER나 CORE 권한을 부여하는 경로가 없는 것 같네요.

  2. getIsAdminByMemberIds 이게 멤버들이 운영진인지 아닌지 map으로 쌓는 메서드 같은데 n+1 문제가 있다고 하네요

@cowboysj
cowboysj force-pushed the refactor/cohort-role-system branch from 4a16a9f to ab96200 Compare September 15, 2026 15:38

@hwistlezz hwistlezz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생 많으셨습니다!!

아래의 다중 기수 케이스 하나 확인해 주시면 감사하겠습니다!

Comment on lines +76 to 89
private fun isAssignmentEffective(
assignment: MemberRoleAssignment,
context: CohortRoleContext,
): Boolean {
val isActiveMember = context.activeCohortId != null && context.activeCohortId in context.memberCohortIds
return when (assignment.roleName) {
RoleType.Master.code, RoleType.Guest.code -> true
RoleType.Core.code, RoleType.Organizer.code, RoleType.Deeper.code -> {
if (!isActiveMember) return false
assignment.cohortId?.value?.let { it in context.memberCohortIds } ?: isActiveMember
}
else -> false
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 활성(active) 기수가 18기일 때,
이전 기수인 17기에서 운영진이었던 사람이 18기에 디퍼로 참여한 경우를 생각해봣습니다

회원의 기수 참여 이력이 17기와 18기를 모두 포함한다면,
현재 조건 assignment.cohortId?.value?.let { it in context.memberCohortIds } ?: isActiveMember 에서는
17기 운영진의 cohortId도 memberCohortIds에 포함돼서, 이전 기수인 17기 운영진 권한을 유효하게 판정할 수 있을 것 같아요

위의 예시의 이해가 쉬우시도록 표로 정리해봤습니다

검사 항목 17기 운영진 18기 디퍼
역할에 연결된 기수 17기 18기
그 기수가 회원의 참여 이력에 포함되는가? 예 예
현재 코드의 역할 판정 유효 유효
현재 활성 기수인 18기 역할만 인정한다면 제외되어야 함 인정

여기에 역할의 기수가 회원의 기수 참여 아력에 있는지보다, 활성 기수와 같은지를 비교하는 방법은 어떨까요??

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 감사합니다!
말씀해주신대로 현재 조건은 유저의 전체 참여 이력에 포함되기만 하면 유효로 판정하고 있어서, 포함 여부가 아니라 활성 기수와의 일치 여부로 비교하도록 수정했습니다. (in → ==)

@cowboysj

Copy link
Copy Markdown
Member Author

고생하셨어요! 변경사항이 커서 코드에 코멘트에 남기긴 좀 어려울 것 같고, 일단 여기에 한 번에 남겨보겠습니다 😂

  1. 코어도 디프만 기수를 활용하기로 했던 게 코드엔 반영이 된 것 같은데, 마이그레이션은 반영이 안된 것 같더라구요.
  • 20260914_schema_alignment.sql 이 파일에서 cohort_id를 ORGANIZER/DEEPER에만 채우고있던데, CORE도 cohort_id를 지정해줘야 할 것 같아요
  1. "N기 운영진/디퍼", '(N-16) 코어"로 조회되어야 하는 부분들이 그냥 "ORGANIZER", "DEEPER", "CORE"로 반환이되는 것 같은데 확인 한 번 부탁드려요!
  2. 멤버 승인 로직에 문제가 있긴 하네요
  • 멤버 승인할 때 활성 기수 Deeper로 자동 승인되는 구조인데, 승인 요청할 때 어떤 권한, 기수을 요청하는건지를 지금 안받고 있다보니까 이렇게 되어있는 것 같거든요

  • 나중에 아래와 같은 식으로 바뀐다고 하면, 예를 들어 "17기 디퍼였던 사람이 18기에 다시 지원"한다고 했을 때, "17기 디퍼"라는 데이터는 soft delete 처리되고, "18기 디퍼"라는 데이터가 생성되는 구조입니다

    • 그래서 일단 승인했을때 해당 member_roles에 새로운 레코드만 쌓는 걸로 수정하면 되지 않을까 싶어요.
image 4. 현재 활성 기수가 없으면 MASTER 외 전원이 GUEST 처리되는 구조인 것 같아요.
  • GUEST는 아닌데 활성 기수가 아닌 멤버들을 위한 권한을 또 만들 필요는 없을 것 같고, CurrentCohortRoleResolver에서 Guest가 아니고 활성기수도 아닌지 검증하는 메서드 추가하면 될 것 같네요.
    (미리 추가할 필요도 없긴 한데, 일단 나중에 놓칠 수도 있어서!)
  • 현재 활성 기수가 없으면 MASTER 외 전원이 GUEST 처리되는 구조인 것 같아요.
  1. 운영진이나 디퍼 권한 부여는 있는데 MASTER나 CORE 권한을 부여하는 경로가 없는 것 같네요.
  2. getIsAdminByMemberIds 이게 멤버들이 운영진인지 아닌지 map으로 쌓는 메서드 같은데 n+1 문제가 있다고 하네요

@uykm 님 리뷰주신 사항들 반영해보았습니다.
확인 부탁드립니다!

  1. CORE cohort_id 마이그레이션
  • 2609142330_schema_alignment.sql에 CORE의 경우 기존 코어 1기 → cohort_id = 17, 2기는 18로 매핑되도록 추가했습니다.
  1. role 표시 문자열
  • RoleDisplayName을 새로 만들어 응답이 기존처럼 나갈 수 있도록 하였습니다. (해당 API는 멤버 관리 화면이 수정되면 수정될 수도 있을 것 같습니다.)
  1. 승인 로직 append 방식
  • ensureCohortRoleAssigned 헬퍼를 새로 추가해, 승인 시 기존 role 이력을 soft delete 하지 않고 (roleType, cohortId) 조합이 없을 때만 새 레코드를 추가하도록 하였습니다.
  • ex) 17기 디퍼였던 사람이 18기 재승인되면 (DEEPER, 17), (DEEPER, 18) 둘 다 살아있고, 화면은 CurrentCohortRoleResolver 가 활성 기수 기준으로 필터링해서 18기 디퍼로만 보입니다.
  1. 현재 활성 기수가 없으면 MASTER 외 전원이 GUEST 처리되는 구조
  • 리뷰 주신 대로 미리 추가할 필요는 없어 보여서 이번 PR에는 반영하지 않았고, 나중에 실제로 이 판정이 필요한 로직이 생길 때 함께 추가하면 될 것 같습니다.
  1. MASTER / CORE 부여 경로
  • CORE 부여는 RoleCommandService.updateMemberRole 의 require 조건에 CORE 를 추가해서 열었습니다.
  • PATCH /v1/roles/members/{memberId} 에 body {"roleType":"CORE","cohortId":18} 로 요청 가능합니다.
  • MASTER 는 별도 슈퍼어드민 API 로 관리해야 할 것 같아 우선 제외하였습니다.

@uykm uykm changed the title [Refactor] 권한/기수 시스템 리팩토링 Refactor: 권한/기수 시스템 리팩토링 Sep 29, 2026
…le-system

# Conflicts:
#	application/src/main/kotlin/core/application/member/application/service/auth/EmailPasswordAuthService.kt
@cowboysj
cowboysj merged commit 00797bd into develop Sep 29, 2026
2 checks passed
@cowboysj
cowboysj deleted the refactor/cohort-role-system branch September 29, 2026 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants