Skip to content

fix(admin,application,identity): 데이터 소유권을 소유 시스템으로 되돌리고 끊긴 경로 연결 - #145

Merged
tlgms merged 4 commits into
bug/139-notice-not-visiblefrom
fix/data-ownership-grpc-delegation
Sep 13, 2026
Merged

fix(admin,application,identity): 데이터 소유권을 소유 시스템으로 되돌리고 끊긴 경로 연결#145
tlgms merged 4 commits into
bug/139-notice-not-visiblefrom
fix/data-ownership-grpc-delegation

Conversation

@tlgms

@tlgms tlgms commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

배경

데이터 소유권 검증에서 확인된 결함을 우선순위대로 고칩니다. 세 가지 모두 "소유자가 아닌 시스템이 데이터를 들고 있거나, 소유자에게 가는 경로가 아예 없는" 같은 뿌리입니다.

Important

#143 위에 스택된 PR입니다. base 가 bug/139-notice-not-visible 이라 #143 이 먼저 머지되어야 합니다. 마이그레이션 번호(V002V003)와 admin-adapter-out/deps.bzl#143 과 겹쳐서 develop 기준으로는 충돌합니다.

무엇을 고쳤나

1. 합격 발표 경로가 끊겨 있었다 (9aaa392)

application_db.pass_results 를 쓰는 코드가 프로덕션에 하나도 없어서, gRPC 가 내보내는 pass_status 는 항상 NOT_ANNOUNCED 였습니다. identity 의 합격 조회는 아예 값을 하드코딩하고 있었습니다.

  • application.protoAnnouncePassResults 추가 — admin 이 산출 결과를 위임하는 통로
  • identity getApplicationResult() 의 하드코딩 제거

2. identity 프로젝션이 낡은 상태에 머물렀다 (9aaa392)

프로젝션이 create/cancel 때만 갱신되어, application 에서 원서를 제출하면 identity 는 계속 DRAFT 로 보고 있었습니다. 그 상태로 취소하면 로컬 가드가 APPLICATION_CANCEL_NOT_ALLOWED 로 막았습니다.

  • 조회할 때마다 원본을 다시 읽어 프로젝션을 맞춥니다. 원본이 응답하지 않을 때만 마지막으로 본 값을 돌려줍니다.
  • 취소 가능 여부의 판단을 원본을 가진 application 에 맡깁니다.
  • 아웃박스와 이벤트 소비자는 구동부가 없어 그대로 두고, 브로커가 들어올 때 연결할 지점을 주석으로 남겼습니다.

3. admin 이 지원자 원본을 복제하고 있었다 (fb1bebd)

admin_db.applicantapplication_db.applicants 를 열세 컬럼 복제한 사본이었고, 이 테이블에 행을 넣는 코드가 플랫폼 어디에도 없었습니다. 지원자 조회·통계·전형·수험번호 발급·문서 발급이 전부 빈 테이블 위에서 돌고 있었습니다. ApplicantJpaEntity 주석이 예고한 대로 gRPC 조회로 대체합니다.

  • application.protoListApplicants 추가
  • admin_dbapplicant_screening 으로 축소 — 접수 번호·수험 번호·원서 도착 여부·전형 상태·성적만. 식별자는 application 의 지원자 식별자를 그대로 씁니다.
  • 접수 번호는 처음 본 원서에 한 번만 부여해 저장합니다. 조회 때마다 다시 계산하면 원서가 취소될 때 번호가 밀려 이미 발급한 수험표·파일 이름과 어긋납니다.
  • 성적 계산의 소유자를 ScoreCalculator 하나로 둡니다. admin 의 성적 정책은 항목별 원점수를 필요로 하는데 출처가 없어 한 번도 동작한 적이 없었습니다.
  • 전형 산출 결과를 AnnouncePassResults 로 반영합니다. 실패하면 admin 쪽 저장도 함께 되돌아갑니다.

4. admin 이 없는 위치에서 원서 원본을 찾고 있었다 (53c237c)

configuration 은 dsm_Entry/Backend/application/application_<접수코드>.pdf 로 올리는데 admin 은 자기 버킷의 application/application_<접수번호>.pdf 를 찾았습니다. 키 루트도 버킷도 달라 GET /applicants/{id}/application-document 가 파일이 실제로 있어도 항상 404 였습니다.

  • 객체 키 루트와 버킷을 configuration 과 맞춥니다.
  • 두 시스템이 서로를 의존하지 않아 컴파일러가 어긋남을 못 잡으므로, configuration 이 쓰는 키를 값으로 적어 둔 테스트로 못 박았습니다.

배포 시 필요한 조치

항목 조치
admin 환경변수 STORAGE_BUCKETAWS_S3_BUCKET (configuration 과 같은 값)
admin 환경변수 APPLICATION_GRPC_HOST / APPLICATION_GRPC_PORT 추가
admin DB V003__replace_applicant_with_screening.sql 적용 (applicant 테이블 DROP)

기존 applicant 행은 application 시스템이 없던 시절의 데이터라 실제 원서와 식별자를 맞출 방법이 없어 이관하지 않습니다. #139notice 와 같은 판단입니다.

API 변경

지원자 응답의 region · admissionType · graduationStatus · birthDatenull 일 수 있습니다. application 의 제출 검증이 이 값들을 요구하지 않기 때문이고, 값이 비어 있는 원서는 전형 산출과 지역·전형 분포 집계에서 제외합니다. applicantId 는 반대로 항상 채워집니다.

검증

Warning

로컬에서 빌드·테스트를 돌리지 못했습니다. 이 환경에 Bazel 이 설치되어 있지 않습니다(bazel: command not found). 아래 테스트는 작성만 했고 실행 결과를 확인하지 못했으니, CI 나 로컬에서 반드시 한 번 돌려 주세요.

추가한 테스트:

  • PassResultCommandServiceTest — 발표 저장, 없는 지원자 거부, 단계 중복 거부, PENDING 거부
  • ApplicationGrpcServiceTest — 발표 RPC 매핑, 미발표 상태 거부, 목록의 선택 항목 유무
  • GrpcApplicantDataAdapterTest — 원서 매핑, 미기재 항목 보존, 1차/최종 단계 구분, PENDING 미전송
  • DocumentNamingTest — configuration 이 쓰는 객체 키와 일치

남긴 것

검증에서 4~5번으로 분류한 항목은 제품 결정이 섞여 있어 이슈로 남깁니다.

  • 문의(Question) 엔티티가 없어 question_answer 가 고아 레코드가 되는 문제
  • faqs / recruitment_guidelines 에 쓰기 경로가 없는 문제
  • 일정이 4곳(configuration DB, application yaml, notification DB, observability yaml)에 흩어진 문제
  • configuration 의 EnvironmentVariable gRPC 서버에 소비자가 없는 문제

🤖 Generated with Claude Code

tlgms and others added 4 commits September 12, 2026 21:13
전형 결과는 admin 이 산출하지만 수험생에게 보이는 합격 여부의 소유자는
application 이다. 그런데 application_db.pass_results 를 쓰는 코드가 없어
gRPC 가 내보내는 pass_status 는 항상 NOT_ANNOUNCED 였다.

- application.proto 에 AnnouncePassResults RPC 추가. admin 이 산출 결과를
  위임할 통로가 생겼다. PENDING 은 발표가 아니므로 거부한다.
- 발표는 한 번에 전부 반영하거나 전부 거부한다. 일부만 반영되면 누가
  발표되었는지 admin 과 application 의 판단이 갈린다.
- identity 는 하드코딩한 NOT_ANNOUNCED 대신 원본 값을 돌려준다.

identity 의 프로젝션은 create/cancel 시점에만 갱신되어, application 에서
원서를 제출하면 낡은 DRAFT 에 머물렀다. 그 상태로 취소하면 로컬 가드가
APPLICATION_CANCEL_NOT_ALLOWED 로 막았다.

- 조회할 때마다 원본을 다시 읽어 프로젝션을 맞춘다. 원본이 응답하지 않을
  때만 마지막으로 본 값을 돌려준다.
- 취소 가능 여부는 원본을 가진 application 이 판단한다.

아웃박스와 이벤트 소비자는 구동부가 없어 그대로 두고, 브로커가 들어올 때
연결할 지점을 주석으로 남겼다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
admin_db.applicant 는 application_db.applicants 를 열세 컬럼 복제한 사본이었다.
게다가 이 테이블에 행을 넣는 코드가 플랫폼 어디에도 없어, 지원자 조회·통계·전형·
수험번호 발급·문서 발급이 모두 빈 테이블 위에서 돌고 있었다.
ApplicantJpaEntity 주석이 예고한 대로 gRPC 조회로 대체한다.

- application.proto 에 ListApplicants 추가. 제출된 원서 전체를 넘긴다.
  제출 시점에 채워지지 않을 수 있는 값(생년월일·지역·전형·학력)은 optional 로 두어
  admin 이 "미기재"와 "값 없음"을 구분할 수 있게 했다.
- admin_db 는 applicant_screening 으로 바꾼다. 접수 번호·수험 번호·원서 도착 여부·
  전형 상태·성적만 남기고 식별자는 application 의 지원자 식별자를 그대로 쓴다.
- 접수 번호는 처음 본 원서에 한 번만 부여해 저장한다. 조회 결과로 다시 계산하면
  원서가 취소될 때 번호가 밀려 이미 발급한 수험표·파일 이름과 어긋난다.
- 필터와 페이지는 메모리에서 처리한다. 두 저장소에 걸친 조건이라 한쪽 쿼리로
  내릴 수 없고, 기존 코드도 이미 회차 전체를 읽는 것을 전제로 한다.

성적도 두 시스템이 따로 계산하고 있었다. admin 의 성적 정책은 항목별 원점수를
필요로 하는데 그 출처가 없어 한 번도 동작한 적이 없다.

- ScoreCalculator 가 총점과 함께 항목별 원점수를 내고, admin 은 이 값을 받아 쓴다.
  계산의 소유자를 application 하나로 둔다.
- 전형 산출 결과는 AnnouncePassResults 로 application 에 반영한다. 반영이 실패하면
  admin 쪽 저장도 함께 되돌아간다.

지역이나 전형이 비어 있는 원서는 어느 정원으로 셀지 정할 수 없어 전형 산출과
지역·전형 분포 집계에서 제외한다.

API 변경: 지원자 응답의 region·admissionType·graduationStatus·birthDate 가 null 일
수 있고, applicantId 는 항상 채워진다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
configuration 은 `dsm_Entry/Backend/application/application_<접수코드>.pdf` 로 원서
원본을 올리는데, admin 은 자기 버킷의 `application/application_<접수번호>.pdf` 를
찾고 있었다. 키 루트도 버킷도 달라 `GET /applicants/{id}/application-document` 가
파일이 실제로 있어도 항상 APPLICATION_DOCUMENT_NOT_FOUND 였다.

- admin 의 객체 키에 configuration 과 같은 루트를 붙인다.
- admin 의 버킷을 configuration 과 같은 AWS_S3_BUCKET 으로 맞춘다.
- 두 시스템이 서로를 의존하지 않아 컴파일러가 어긋남을 못 잡으므로,
  configuration 이 쓰는 키를 값으로 적어 둔 테스트로 못 박는다.

배포 시 admin 의 STORAGE_BUCKET 을 configuration 과 같은 AWS_S3_BUCKET 으로
바꿔야 한다. admin 이 이미 올려 둔 수험표는 새 키로 다시 생성된다(요청 시점에
새로 만들어 올리는 구조라 이관이 필요 없다).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 비어 있는 값이 섞인 집합 비교를 명시적인 헬퍼로 바꿔 의도를 드러낸다.
- save 가 saveAll 을 자기호출하면 프록시를 지나지 않아 트랜잭션이 걸리지 않는다.
  두 진입점에 각각 경계를 두고 실제 저장은 private 으로 내린다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • ready-for-review
🚫 Excluded labels (none allowed) (2)
  • wip
  • do-not-review

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: d27f42b2-0ae6-4555-9c93-3c3ec93ed093

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@tlgms

tlgms commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

남긴 항목을 이슈로 끊었습니다.

문의(QnA) 건은 #144 에 이미 있어 새로 만들지 않았습니다.

#149 는 이 PR 에서 방어적으로 처리한 부분(지역이 비어 있는 원서를 전형 산출에서 제외)의 근본 원인입니다.

@kusuri12-09

Copy link
Copy Markdown
Member

대신 고쳐주셔서 감사합니다
develop에 합칠 예정이라면 base 브랜치 확인해주요

@tlgms

tlgms commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

이 PR 에 CI 가 붙지 않는 이유

리뷰하실 때 "체크가 왜 CodeRabbit 하나뿐이지?" 로 헷갈리실 수 있어 남깁니다. 조건이 두 겹입니다.

1. 빌드/테스트 워크플로가 develop 에 없습니다

.github/workflows/ci.yml (Bazel build //... + test //...) 은 아직 머지되지 않은 #95 브랜치에만 있습니다. develop 에는 build-image, build-images, commitlint, deploy 뿐입니다.

2. 트리거가 base 와 맞지 않습니다

on:
  pull_request:
    branches: [main, develop]

이 PR 은 #143 위에 스택되어 bug/139-notice-not-visible 을 향하므로, ci.yml 이 있었더라도 발화하지 않습니다. workflow_dispatch 도 없어 수동 실행도 안 됩니다.

따라서 #95 가 머지되고 이 PR 의 base 가 develop 으로 바뀐 뒤에야 CI 가 돕니다. 두 조건이 모두 필요합니다.

기준선: 지금 develop 계열에서 이미 깨져 있는 테스트

#95 의 마지막 실행(run 34419489779) 결과입니다.

INFO: Build completed, 2 tests FAILED, 49 total actions
//systems/identity/identity-adapter-out:test    FAILED in 85.9s
//systems/identity/identity-bootstrap:test      FAILED in 45.0s
  • 빌드는 통과합니다 (Bazel exit code 3 = 빌드 성공, 테스트 실패)
  • 실패한 두 개는 로그에 APPLICATION FAILED TO START 가 찍혀 있어, Redis/DB 가 필요한 통합 테스트로 보입니다
  • admin · application · configuration · notification · observability 는 전부 PASSED

이 PR 의 변경과 무관한 선행 실패입니다. 다만 이 PR 이 AccountApplicationDataPersistenceAdapter 를 수정했으므로, CI 가 돌면 이 두 개의 실패 원인이 기존과 같은지는 로그로 확인해야 합니다.

이 PR 의 검증 상태

작업 환경에 Bazel 이 없어(bazel: command not found) 컴파일과 테스트를 한 번도 실행하지 못했습니다. 추가한 테스트 4종은 작성만 된 상태입니다.

  • PassResultCommandServiceTest
  • ApplicationGrpcServiceTest (발표 RPC · 목록 RPC 케이스 추가)
  • GrpcApplicantDataAdapterTest
  • DocumentNamingTest

머지 전에 반드시 한 번 돌려 주세요.

@tlgms
tlgms merged commit 4c5f92e into bug/139-notice-not-visible Sep 13, 2026
1 check passed
@tlgms
tlgms deleted the fix/data-ownership-grpc-delegation branch September 13, 2026 12:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants