
【AI 주도 제품 개발 제5회】 Spring Boot × Next.js: 인증·감사·알림까지 AI 설계 내용을 코드로 구현하기
요약
Spring Boot와 Next.js를 활용하여 인증, 감사, 알림 기능을 포함한 백엔드 시스템을 설계하고 구현하는 과정을 다룹니다. Docker를 통한 빌드 환경 통일, Flyway를 이용한 스키마 관리, 그리고 도메인 규칙을 애그리거트에 반영하는 실무적인 개발 패턴을 소개합니다.
핵심 포인트
- Docker를 활용한 재현 가능한 빌드 환경 구축
- Flyway와 Hibernate를 조합한 안정적인 DB 스키마 관리
- 도메인 규칙을 애그리거트 내부에 구현하여 데이터 무결성 확보
- 설계 리뷰에서 발견된 리스크를 테스트 케이스로 전환하여 검증
이전 기사는 이쪽
이 연재의 구성
| 회차 | 테마 | 주요 내용 |
|---|---|---|
| 제1회 | 완성형과 전체상 | 완성된 RADAR의 기능, 이용자, 핵심 업무, 리포지토리 구성 |
| ... |
Spring Boot와 PostgreSQL로 운영 백엔드를 구현하기
Docker로 빌드 환경을 통일하기
구현 시작 시, 호스트에는 JDK와 Gradle이 없었습니다.
선택지는 호스트에 설치하기, 코드만 만들기, Docker로 빌드하기의 세 가지입니다. 사용자는 Docker를 선택했습니다.
현재 리포지토리에서도 gradle:8.11-jdk21 이미지를 사용하는 절차가 남아 있습니다.
docker run --rm -v "$PWD":/app -w /app \
-v gradle-cache:/root/.gradle \
gradle:8.11-jdk21 gradle build -x test --no-daemon
로컬 환경을 임의로 변경하지 않고, 재현 가능한 실행 방법을 선택한 사례입니다.
페이즈 1에서 구현한 것
먼저 BC-001, BC-002, BC-003을 구현했습니다.
| BC | Java 패키지 | 주요 구현 |
|---|---|---|
| BC-001 | dealassignment | Deal, Assignment, 정식화, 이력 |
| BC-002 | risk | RiskFlag, RiskResponse, 탐지 |
| BC-003 | member | User, 역할(Role), 멤버 정보 |
Controller, Service, Repository를 분리하고, Flyway만이 스키마를 변경하는 구성으로 했습니다. Hibernate는 ddl-auto: validate를 사용하여, 마이그레이션과의 차분이 있으면 기동을 실패하게 합니다.
도메인 규칙을 애그리거트(Aggregate)에 두기
Assignment는 단순한 JPA 데이터 컨테이너가 아닙니다.
현재 구현에는 다음과 같은 규칙이 있습니다.
- 시작일은 월초, 종료일은 월말로 정규화한다
- 종료일이 없는 경우 미정 등의 라벨이 필요하다
- BP 어사인(Assign)에는 BP 단가가 필요하다
- 취소된 어사인은 변경할 수 없다
- 정식화는 중복 실행해도 상태를 망가뜨리지 않는다
public void formalize() {
if (this.type == AssignmentType.정식어사인) {
return;
...
업무 규칙을 Controller나 폼(Form)에만 두지 않고, 애그리거트 스스로가 부적절한 상태를 거부합니다.
테스트로 리뷰 과제를 확인하기
초기 페이즈에서는 중복 어사인 방지와 Idempotency-Key를 통합 테스트로 확인했습니다.
그 후 인증, 알림, 감사, 예실(예산 대비 실적), 영업 의뢰, 조달로 구현이 확장되었으며, 현재는 40개가 넘는 백엔드 테스트 클래스가 있습니다.
테스트 수 자체보다, 리뷰에서 발견한 리스크가 테스트 케이스로 변했다는 점이 중요합니다.
- 동일한 Idempotency-Key라면 동일한 결과를 반환한다
- 권한이 없는 역할(Role)은 조작할 수 없다
- SALES는 타인의 상담(Deal)을 가져올 수 없다
- 기간 중복을 DB가 거부한다
- 중요한 업무 조작 시 감사 로그(Audit Log)가 생성된다

BC별 Spring Boot 구현, Flyway를 통한 스키마 관리, 리뷰 리스크로부터 도출한 테스트를 하나로 연결한다
Docker 상에 빌드 환경을 통일하고, 상담·어사인, 지연 리스크, 멤버의 각 영역을 단계적으로 구현하면서 업무 규칙과 리뷰 지적 사항을 코드와 테스트로 녹여냈습니다.
Next.js·OIDC·권한 경계를 구현하기
인증을 나중에 추가하지 않기
페이즈 1 다음으로는 AWS 기반 구축보다 BC-005 인증을 우선시했습니다.
업무용 애플리케이션에서는 인증뿐만 아니라 누가 어떤 조작과 데이터에 접근할 수 있는가가 API 설계에 영향을 미칩니다. 모든 사용자가 전체 데이터를 볼 수 있는 상태로 화면을 다 만들어 버리면, 나중에 취득 조건과 테스트를 광범위하게 수정해야 합니다.
PermissionAggregate로 조작 권한을 나타내기
RADAR는 역할(Role)과 조작의 대응 관계를 PermissionAggregate로 집약하고 있습니다.
MATRIX.put(Role.PM_LEADER, EnumSet.of(
Operation.DEAL_VIEW,
Operation.ASSIGNMENT_CREATE_TENTATIVE,
...
이 매트릭스는 Nexus Architect가 작성한 actors-roles-permissions.md를 코드로 구현한 것입니다.
조작 권한과 행 수준 제어(Row-level Control)를 분리하기
SALES는 상담을 열람할 수 있다라는 권한만으로는 불충분합니다. 영업 담당자는 자신이 담당하는 상담 또는 영업 의뢰와 연결된 상담만 볼 수 있어야 합니다.
RADAR에서는 Controller의 @PreAuthorize 외에도, Service에서 소유자(Owner) 조건을 적용합니다.
구현 후 추가 개발 과정에서, SALES가 ownerUserId를 생략할 경우 모든 상담을 볼 수 있는 행 수준 제어(Row-level Control)의 결함도 발견되었습니다. 현재는 SALES로부터 전달된 검색 조건과 관계없이, 백엔드 측에서 자신의 ID로 덮어씁니다.
프론트엔드에서 버튼을 숨기는 것은 UX 측면의 보조 수단일 뿐입니다. 실제 인가(Authorization) 경계는 백엔드에 있습니다.
Cookie 세션을 SSR로 중계하기
Next.js는 App Router를 사용하여, 읽기는 Server Components로, 업데이트는 Server Actions로 집중시키고 있습니다.
SSR의 fetch는 브라우저를 거치지 않기 때문에, 받은 Cookie를 백엔드로 명시적으로 중계합니다.
const response = await fetch(`${BACKEND_URL}${path}`, {
...init,
headers: {
...
middleware.ts는 JSESSIONID의 존재 여부만을 빠르게 확인하며, app/layout.tsx의 requireSession()이 백엔드에 질의하여 실제 세션을 검증합니다.
OIDC의 계약 불일치를 실제 로그인으로 찾아내기
당초 Google Cloud Console에서의 클라이언트 등록은 개발 환경 외의 작업으로 분류하여 절차서로 분리해 두었습니다.
그 후, 실제 클라이언트를 등록하고 로그인을 시도해 본 결과, Google 측 인증 후에 /auth/callback에서 401 에러가 발생했습니다.
원인은 설정한 리다이렉트 URI(Redirect URI)와 Spring Security가 실제로 대기하는 콜백 경로(Callback Path)의 불일치였습니다.
.redirectionEndpoint(r -> r.baseUri("/auth/callback"))
이 지정을 추가하여 문제를 해결하였고, Google Workspace 계정을 통한 로그인 성립까지 확인했습니다.
설계서와 설정 파일이 일치해 보이더라도, 외부 서비스와의 계약은 E2E(End-to-End) 테스트를 통해서야 비로소 확인할 수 있는 경우가 있습니다.

OIDC와 Cookie 중계의 끝에서, 세션, PermissionAggregate, SALES의 행 수준 제어를 백엔드에 중첩시키기
인증, 조작 권한, 데이터 단위의 액세스 제어를 초기 단계에서 API에 포함시켰으며, Next.js에서 실제로 로그인하여 설정의 불일치까지 확인하고 수정했습니다.
알림·감사와 실제 이용으로부터의 프로덕트 재설계
커밋(Commit) 후에 알림 보내기
알림은 지연 리스크 등의 업무 이벤트에 따라 생성됩니다.
RADAR에서는 @TransactionalEventListener를 AFTER_COMMIT 단계에서 실행합니다.
@Async("notificationExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onRiskFlagDetected(RiskFlagDetectedEvent event) {
...
이는 업무 트랜잭션이 롤백(Rollback)되었음에도 알림만 전송되는 상황을 방지하기 위함입니다.
알림에는 dedup_key의 유니크(Unique) 제약이 있어 동일한 이벤트의 중복을 방지합니다. 도메인 모델에는 최대 3회의 재시도 횟수와 failed 상태가 있습니다. 전용 bounded thread pool도 준비하여, 알림 처리가 HTTP 요청 처리를 압박하지 않도록 구성했습니다.
다만, 현재의 발신처는 로그 출력용 플레이스홀더(Placeholder)입니다. 실패 시 retryCount를 늘리는 로직은 있지만, 다음 발신 시도를 시작하는 스케줄러나 큐는 아직 없습니다. 이메일이나 Google Chat 등 실제 채널과의 연결과 함께 남아있는 구현 과제입니다.
감사 로그(Audit Log)를 동일한 트랜잭션 내에 기록하기
정식화, 임시 할당, 리스크 대응 시에는 AuditService
를 핵심 업무와 동일한 트랜잭션(Transaction) 내에서 호출합니다.
감사 로그(Audit Log)에는 작업자, 작업 유형, 대상 리소스, 시각, 변경 전후의 값을 기록합니다.
현재 방침으로는 감사 로그 쓰기에 실패할 경우, 업무 작업도 롤백(Rollback)합니다. 한편, DB 사용자로부터 UPDATE/DELETE 권한을 박탈하는 INSERT-only 운영은 RDS 구축 시의 남은 과제입니다.

알림은 커밋(Commit) 후에 비동기 처리하고, 감사는 중요 업무와 동일한 트랜잭션으로 기록한다
초기 뼈대(Backbone)를 재검토하기
초기의 RADAR는 임시 할당(Provisional Assignment)을 프로덕트의 중심으로 두었습니다.
그 후, 실제 이용 측의 업무 설계와 대조해 보니, 조직이 가장 먼저 보는 것은 그룹별·월별 예산과 매출의 차이이며, 부족분을 영업, 할당, 조달로 채우는 흐름이라는 것을 알게 되었습니다.
예산과 매출 실적
↓ 부족분 파악
영업 요청
...
이 격차를 수용하여, RADAR는 다음을 추가했습니다.
- OrgGroup에 의한 조직 구조
- RevenueLine에 의한 월별 매출의 사실 원천(Source of Truth)
- SalesRequest에 의한 리더로부터 영업으로의 요청
- PROCUREMENT 역할
- 외부 인재 조달 컨텍스트(Context)
설계 이력과 실제 코드의 차이를 관리하기
docs/design-history/는 초기 파이프라인의 이력으로서 동결(Freeze)해 두었습니다. 나중에 기능을 추가할 때마다 과거 문서를 완성형으로 다시 쓰게 되면, 당시에 무엇을 판단했는지 알 수 없게 됩니다.
따라서 현재의 코드와 PRODUCT.md를 살아있는 상태로 취급하고, 초기 설계와의 차이점은 MEMORY.md와 구현 계획에 추가합니다.
AI 주도 개발(AI-Driven Development)에서는 생성물을 항상 최신으로 덮어쓰기보다, 다음의 세 가지를 구분하는 것이 추적하기 쉬워집니다.
| 종류 | RADAR에서의 위치 |
|---|---|
| 현재 동작하는 코드 | backend/, frontend/ |
| 현재의 원칙 | PRODUCT.md, DESIGN.md, CLAUDE.md |
| 과거의 판단 | docs/design-history/, docs/plans/ |

임시 할당 중심의 초기 구조를 예실(예산/실적), 영업 요청, 할당, 조달의 업무 플로우로 확장하고, 과거의 판단과의 차이점을 관리한다
알림과 감사의 정합성을 유지하는 토대를 구현하고, 미완성 부분을 명기하면서, 실제 이용을 통해 확인된 예실·영업 요청·할당·조달로 프로덕트의 중심을 확장했습니다.
마치며: Nexus Architect와 구현을 잇는 패턴
RADAR를 만든 흐름
본 연재에서는 완성된 RADAR에서 시작하여, 개발의 기점으로 돌아가 흐름을 추적했습니다.

업무 과제로부터 브레인스토밍(Wall-hitting), MVP, 실운영, 설계 리뷰, 구현, 재설계까지 연결하여, 배움을 다음 가설로 되돌린다
패턴 1: 완성상보다 먼저 검증 조건을 결정하기
신규 프로덕트에서는 기능 목록을 상세하게 만들수록 안심하게 됩니다.
RADAR에서 본番(Production) 개발로 나아갈 판단을 내릴 수 있었던 것은, 앱이 우선적으로 사용되는가, 임시 할당이 사용되는가라는 가설에 검증 방법과 재검토 기준을 두었기 때문입니다. MVP가 동작했다는 사실이 아니라, 실제 이용에서 기준을 충족했다는 것을 판단의 근거로 삼았습니다.
패턴 2: MVP의 코드가 아닌 배움을 계승하기
MVP는 Hono와 SQLite를 사용했고, 본番(Production)은 Spring Boot와 PostgreSQL을 사용합니다.
직접 계승한 것은 코드가 아닙니다. 이용된 화면, 임시 할당의 상태 전이(State Transition), 정식화의 트랜잭션 경계, 이용자의 판단입니다.
MVP를 아카이브(Archive)하고 본番(Production) 코드와 섞지 않음으로써, 각각의 목적이 명확해졌습니다.
패턴 3: 리뷰의 수치를 구현 과제로 바꾸기
MMI, DDD, 5관점 리뷰의 스코어는 평가를 위한 평가가 아닙니다.
- 책임 집중 → 패키지와 애그리거트(Aggregate)의 분리
- 중복 리스크 → PostgreSQL 배타 제약 조건(Exclusive Constraint)
- TOCTOU → 행 잠금(Row Lock) 후 사전 조건 확인
- 재전송 리스크 → Idempotency-Key
- 인증 부족 → OIDC와 PermissionAggregate
- 운영 설계 부족 → 모니터링, DR, 보안 문서
이와 같이 지적 사항을 설계 변경, 코드, 테스트로 바꾸는 것이 중요합니다.
패턴 4: 인간이 판단하는 영역을 남겨두기
RADAR에서는 AI 에이전트가 문서, 코드, 리뷰를 만들었습니다. 반면, 다음 판단은 사용자가 수행했습니다.
- 자사 이용을 우선한다
- PM/리더를 프라이머리 (Primary)로 한다
- 데이터 이전을 하지 않는다
- MVP를 실운영으로 내보낸다
- 3차 리뷰를 생략하고 구현으로 진행한다
- Docker로 빌드한다
- 인증을 AWS 기반보다 먼저 구현한다
- 실사용에 맞춰 예산/실적 및 조달을 추가한다
AI의 자율성을 높이는 것과 업무상의 판단을 맡기는 것은 별개입니다.
유형 5: 미완료 상태를 솔직하게 남기기
현재의 RADAR에도 미완료 항목이 있습니다.
- 어사인 (Assign) 조정 시간이 얼마나 단축되었는지에 대한 측정
- 지연 리스크를 조기에 감지할 수 있었는지에 대한 검증
- 알림의 실제 채널 연결
- RDS 상의 감사 로그 (Audit Log) INSERT-only 권한
- 운영 토폴로지 (Topology) 확정 후의 Cookie Domain 전략
- 프론트엔드 자동 테스트 기반
이것들을 숨기고 운영화 완료라고 표현하지 않고, 다음 판단 재료로서 남겨두는 것도 품질의 일부입니다.
마치며
Nexus Architect의 가치는 대량의 설계서를 생성하는 것만이 아닙니다.
모호한 업무 과제를 Vision, 가설, 스코프 (Scope), 화면, 도메인 (Domain), API, 운용, 구현으로 단계적으로 변환하고, 그 과정에서 인간이 판단할 수 있는 상태를 만드는 데 있습니다.
RADAR는 단 한 번의 프롬프트 (Prompt)로 완성된 프로덕트가 아닙니다. 대화 (Wall-hitting)를 통해 방향을 정하고, MVP로 확인하며, 리뷰를 통해 부족함을 발견하고, 구현을 통해 설계를 검증하며, 실사용을 통해 다시 구조를 바꾸고 있습니다.
AI 주도 프로덕트 개발에서 재사용할 수 있는 것은 최종 결과물의 형태보다, 이러한 왕복(Iteration)의 메커니즘입니다.
기획 단계의 대화부터 MVP 검증, 설계 리뷰, 구현까지를 하나의 흐름으로 연결하고, 인간의 판단과 미완료 항목을 명시하면서 얻어진 업무 지식을 프로덕트에 반영했습니다.
전 5회에 걸친 연재는 여기까지입니다. 끝까지 읽어주셔서 감사합니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기