fix: 하네스 제거 및 keycloak 문서 보강
This commit is contained in:
@@ -1 +0,0 @@
|
||||
../../../vault/50-journal/daily-tasks/develop/2026-05-29-archunit-controller-domain-return-rule.md
|
||||
@@ -0,0 +1,236 @@
|
||||
---
|
||||
title: daily-task / develop / archunit-controller-domain-return-rule
|
||||
source_type: daily-task
|
||||
track: develop
|
||||
status: raw
|
||||
status_label: not-started
|
||||
difficulty: intermediate
|
||||
duration_estimate: 120
|
||||
prerequisites:
|
||||
- "[[raw/branch-notes/feature-boundary-validation-mapping-contract]]"
|
||||
- "[[raw/project-notes/ca-skeleton-operational-contract]]"
|
||||
parent_project: ca-skeleton-operational-contract
|
||||
parent_branch: feature-boundary-validation-mapping-contract
|
||||
target_date: 2026-05-29
|
||||
created: 2026-05-28
|
||||
tags: [daily-task, validation, mapper, testing]
|
||||
---
|
||||
|
||||
# daily-task / develop / archunit-controller-domain-return-rule
|
||||
|
||||
> Layer: `raw/daily-tasks/develop/` — **개발 트랙 일일 실습 과제**.
|
||||
> `status_label`: `not-started` → 시작 시 `in-progress` → 종료 시 `done`
|
||||
> `difficulty`: `intermediate` (ArchUnit 기본 사용 경험 가정, predicate 합성은 새로움)
|
||||
> `duration_estimate`: 120 (Pomodoro 4-5개)
|
||||
>
|
||||
> **이 과제의 위치**: develop 트랙 1일차. [[raw/branch-notes/feature-boundary-validation-mapping-contract]] 의 첫 Claims To Verify ("controller 가 domain object 를 직접 반환하지 않는지") 를 *코드에서 강제* 하는 ArchUnit rule 을 작성한다.
|
||||
|
||||
## Parent / 부모 (필수)
|
||||
|
||||
- **Parent project**: [[raw/project-notes/ca-skeleton-operational-contract]] (§4 Boundary Validation & Mapper Contract)
|
||||
- **연관 branch**: [[raw/branch-notes/feature-boundary-validation-mapping-contract]] — D1 (모든 경계에 validation/mapping 책임), D8 (domain object → response DTO 직접 노출 금지)
|
||||
|
||||
## 1. 학습 목표 / Learning Objectives
|
||||
|
||||
- [ ] **L1**: ArchUnit 의 `ArchRuleDefinition.classes().that()...should()` 체인으로 controller class 의 method return type 제약 rule 을 작성할 수 있다
|
||||
- [ ] **L2**: 의도적 위반 코드 추가 시 build 가 *정확히* 위반된 rule 이름 + violating method signature 메시지로 깨짐을 확인할 수 있다
|
||||
- [ ] **L3**: rule 이 `@Controller`, `@RestController` 양쪽 모두 cover 하고, `ResponseEntity<T>` wrapper 의 generic 인자도 검사하는지 직접 검증할 수 있다
|
||||
- [ ] **L4 (optional, 시간 남으면)**: integration test 로 actual JSON response payload 에 domain entity field (e.g., `version`, `createdBy`) 가 leak 되지 않음을 검증할 수 있다
|
||||
|
||||
## 2. 스토리라인 / WHY (Storyline)
|
||||
|
||||
어제 보강한 `feature-boundary-validation-mapping-contract` 의 D8 결정 — *domain object 를 response DTO 로 직접 노출 금지* — 은 *문서상 합의* 일 뿐, 실제 코드는 Jackson 의 implicit reflective serialization 으로 controller method 가 `return entity` 라고 적어도 build 가 통과한다.
|
||||
|
||||
다음 신입이 이 결정을 모르고 `return ticket` 으로 적어도 컴파일러는 침묵하고, JSON response 에는 `passwordHash` 와 `version` 이 그대로 흘러간다. PR 리뷰어가 매번 *손으로* 잡아내야 하는 것은 contract 가 아니라 사회적 합의일 뿐. **사회적 합의는 컴파일러를 이기지 못한다.**
|
||||
|
||||
오늘은 *그 단 한 가지* rule — controller method return type 은 DTO record 또는 `ResponseEntity<DTO record>` 만 허용 — 을 작성하고, 의도적으로 위반된 코드를 추가해 build 가 깨지는 것을 *눈으로* 확인한다. 이 단 한 줄의 rule 이 다음 1년의 boundary leak 50건을 막을 것이다.
|
||||
|
||||
## 3. 환경 / Environment
|
||||
|
||||
**개발 도구**:
|
||||
|
||||
- Java: 21 (LTS)
|
||||
- Build: Gradle 8.x
|
||||
- IDE 권장: IntelliJ IDEA 2025.x
|
||||
- 라이브러리: `com.tngtech.archunit:archunit-junit5:1.3.0`, Spring Boot 3.3.x, JUnit 5.10+
|
||||
|
||||
**사전 셋업**:
|
||||
|
||||
```bash
|
||||
cd ~/workspace/ca-tmpl
|
||||
git checkout main && git pull
|
||||
git checkout -b daily-task/develop/archunit-controller-domain-return-rule
|
||||
|
||||
# 현재 ArchUnit 의존성 확인
|
||||
./gradlew :adapter-web:dependencies | grep archunit
|
||||
|
||||
# 기존 ArchUnit test 위치 확인
|
||||
find . -name 'CleanArchitectureTest.java' -path '*/test/*'
|
||||
|
||||
# 빌드 정상 확인
|
||||
./gradlew :adapter-web:test --tests '*CleanArchitectureTest'
|
||||
```
|
||||
|
||||
**예상 변경 파일**:
|
||||
|
||||
- `adapter-web/src/test/java/<base>/architecture/ControllerReturnTypeRuleTest.java` (신규)
|
||||
- 또는 기존 `CleanArchitectureTest.java` 에 메서드 추가
|
||||
|
||||
## 4. 사전 지식 / Prerequisites
|
||||
|
||||
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] — D1, D8, Claims To Verify 첫 항목 정독
|
||||
- [[raw/project-notes/ca-skeleton-operational-contract]] §4 — Boundary Validation & Mapper Contract
|
||||
- ArchUnit 핵심 API (모르면 5분만 보고 시작):
|
||||
- `JavaClasses` 로딩 (`new ClassFileImporter().importPackages(...)`)
|
||||
- `ArchRuleDefinition.methods()` chain
|
||||
- `DescribedPredicate` 합성 (`and`, `or`, `not`)
|
||||
|
||||
## 5. 단계별 과제 / Exercises
|
||||
|
||||
### Step 1: 베이스라인 — 현재 위반 grep (~20min)
|
||||
|
||||
- **What**: 현재 ca-tmpl 의 controller code 에 이미 `return entity` 또는 `return domainObject` 패턴이 있는지 확인. 사전 측정.
|
||||
- **How (hint)**: `grep -r "return.*Entity\b" adapter-web/src/main/java` / IDE에서 `@RestController` annotated class 들의 method return type 한 줄로 정렬해서 listing
|
||||
- **Done when**:
|
||||
- 현재 위반 카운트 N개 명시 (0이어도 무방 — 기준선만 확보)
|
||||
- §7 결과물 섹션에 "baseline violation: N" 기록
|
||||
|
||||
### Step 2: ArchUnit rule 작성 (~30min)
|
||||
|
||||
- **What**: `ControllerReturnTypeRuleTest.java` 에 단일 `@ArchTest` rule 작성. controller class 의 모든 public method 의 return type 이 *허용 set* (DTO record / `ResponseEntity<DTO>` / `void`) 안에 있는지 검사.
|
||||
- **How (hint)**:
|
||||
- `classes().that().areAnnotatedWith(RestController.class)` 로 controller selection
|
||||
- `.should()` 뒤에 custom `ArchCondition<JavaClass>` 작성 — class 내부 method 순회
|
||||
- 허용 set 정의: 해당 패키지 (e.g., `<base>.web.dto.*`) 아래 record 인지, 또는 `ResponseEntity` 의 raw type 인지
|
||||
- `ResponseEntity<T>` 의 generic 인자 추출은 `JavaParameterizedType` 사용
|
||||
- **함정** (의도적 노출):
|
||||
- `ResponseEntity<DomainEntity>` 처럼 wrapper 안에 domain 이 숨는 경우 — generic 인자도 검사해야 함
|
||||
- record 가 *DTO 패키지가 아닌 domain 패키지에 있는* 경우 — 패키지 위치도 검사
|
||||
- **Done when**:
|
||||
- `./gradlew :adapter-web:test --tests '*ControllerReturnType*'` 통과
|
||||
- rule 코드 30줄 이내 (복잡하면 분리)
|
||||
|
||||
### Step 3: 의도적 위반 → build 깨짐 확인 (~25min)
|
||||
|
||||
- **What**: 임의의 controller method return type 을 domain entity 로 *임시* 변경 → build 실행 → 에러 메시지 *정확히 읽고* 확인 → rule 이름이 메시지에 포함되는지 검증 → 위반 복구
|
||||
- **How (hint)**:
|
||||
- 가장 단순한 GET controller method 선택
|
||||
- return type 만 변경 (구현은 그대로 두고 `(DomainType) (Object) responseDto` cast 같은 hack 사용)
|
||||
- build 실패 시 stack trace 가 아니라 **violation 메시지** 의 첫 줄을 읽을 것
|
||||
- **Done when**:
|
||||
- 실패 메시지에 rule description (예: `controllers should return only DTO record or ResponseEntity<DTO record>`) 포함
|
||||
- 실패 메시지에 정확한 violating method signature 포함
|
||||
- 변경 복구 후 build 다시 통과
|
||||
- **공통 실수**:
|
||||
- rule 자체에 typo 가 있어 *항상* 실패 — 의도된 위반인지 unintended 위반인지 구분 필요
|
||||
|
||||
### Step 4: `ResponseEntity<DomainEntity>` 위반 잡기 (심화) (~25min)
|
||||
|
||||
- **What**: Step 3 의 위반을 `ResponseEntity<DomainEntity>` 형태로 변경. 현재 rule 이 이 패턴도 잡는가? 못 잡으면 rule 보강.
|
||||
- **How (hint)**:
|
||||
- ArchUnit 의 `JavaMethod.getReturnType()` 은 raw type만 반환 — generic 인자는 `getRawReturnType()` 외 `getReturnType()` 의 `JavaParameterizedType` cast 필요
|
||||
- 또는 더 간단한 우회: `ResponseEntity` 인 경우에만 별도 검사 분기
|
||||
- **트레이드오프 의식** (시니어 사고):
|
||||
- rule 을 정교하게 만들수록 false positive 줄지만 rule 복잡도 ↑
|
||||
- 대안: ArchUnit 대신 lightweight `@JsonView` 정책 + DTO 패키지 격리 → 다른 trade-off
|
||||
- *이 결정은 본 과제 범위 밖이지만 §8 회고에 기록할 것*
|
||||
- **Done when**:
|
||||
- `ResponseEntity<DomainEntity>` 패턴이 build 실패로 검출됨
|
||||
- rule 코드가 여전히 50줄 이내
|
||||
|
||||
### Step 5 (선택): integration test 로 JSON leak 검증 (~20min)
|
||||
|
||||
- **What**: 정상 endpoint 호출 → response JSON 을 deserialize → domain entity 의 internal field (e.g., `passwordHash`, `version`, `auditingFields.createdBy`) 가 *없음* 을 assert
|
||||
- **How (hint)**:
|
||||
- `@SpringBootTest(webEnvironment = RANDOM_PORT)` + `TestRestTemplate`
|
||||
- JSON path assertion 또는 `Map<String, Object>` deserialize 후 keyset 검사
|
||||
- 금지 field set 을 명시적으로 정의 (whitelist 아닌 blacklist — 추가 field 는 허용)
|
||||
- **Done when**:
|
||||
- test 통과 + 의도적으로 controller 가 entity 반환하도록 변경 시 test 실패
|
||||
- 변경 복구
|
||||
|
||||
## 6. 검증 / Assessment
|
||||
|
||||
**자동 검증**:
|
||||
|
||||
```bash
|
||||
# 1) 빌드 + 단위 테스트
|
||||
./gradlew clean :adapter-web:test
|
||||
# 합격 기준: exit 0
|
||||
|
||||
# 2) 본 과제의 ArchUnit rule
|
||||
./gradlew :adapter-web:test --tests '*ControllerReturnType*'
|
||||
# 합격 기준: PASS 로그 + rule 1개 이상 executed
|
||||
|
||||
# 3) 의도적 위반 시 빌드 깨기 (수동)
|
||||
# - controller method return type 임시 변경
|
||||
# - ./gradlew :adapter-web:test → FAILED
|
||||
# - 메시지 확인 → 복구
|
||||
|
||||
# 4) (Step 5) integration test
|
||||
./gradlew :adapter-web:test --tests '*JsonLeakIntegrationTest'
|
||||
# 합격 기준: exit 0
|
||||
```
|
||||
|
||||
**수동 self-check**:
|
||||
|
||||
- [ ] rule description 이 한 줄로 명확 (남이 봐도 무엇을 검사하는지 알 수 있음)
|
||||
- [ ] 의도적 위반 메시지가 rule description + violating method signature 둘 다 포함
|
||||
- [ ] rule 이 controller 패키지 *외부* class 는 검사하지 않음 (false positive 없음)
|
||||
- [ ] commit 메시지가 "왜" 를 답함 (예: "Enforce controller→DTO return type to prevent domain leak in JSON response")
|
||||
- [ ] **시니어 사고 체크** — 본 rule 의 trade-off 1-2개 (예: false positive 가능 시나리오, rule 우회 방법 — generic Object 반환 등) 를 §8 회고에 기록
|
||||
|
||||
## 7. 결과물 / Outcomes
|
||||
|
||||
- **commit / PR**:
|
||||
- 브랜치: `daily-task/develop/archunit-controller-domain-return-rule`
|
||||
- commits: <해시 + 1줄 메시지>
|
||||
- PR URL (있다면):
|
||||
- **신규/변경 파일**:
|
||||
- `adapter-web/src/test/java/<base>/architecture/ControllerReturnTypeRuleTest.java` — controller return type rule
|
||||
- (Step 5 했다면) `adapter-web/src/test/java/<base>/architecture/JsonLeakIntegrationTest.java`
|
||||
- **베이스라인 측정값** (Step 1):
|
||||
- Pre-rule violation count: <N>
|
||||
- 위반 패턴: <패턴 목록>
|
||||
- **학습한 개념** (wiki/concepts 로 ingest 후보):
|
||||
- ArchUnit predicate 합성 (`and`/`or`/`not`)
|
||||
- `JavaParameterizedType` 으로 generic 인자 검사
|
||||
- `ResponseEntity<T>` 와 ArchUnit 의 generic erasure 다루기
|
||||
- **다음 과제 thread**:
|
||||
- request DTO 가 application service signature 에 직접 나타나는지 검사 (`feature-boundary-validation-mapping-contract` Claims To Verify 2번째 항목)
|
||||
- MapStruct generated code 의 architecture exemption 검증
|
||||
- `@JsonView` 또는 DTO 패키지 격리 대안의 trade-off 비교
|
||||
|
||||
## 8. 회고 / Reflection (~5min)
|
||||
|
||||
- **막혔던 곳** (몇 분 / 어디서):
|
||||
- **예상과 다른 점**:
|
||||
- 예: ArchUnit 의 generic type 처리 방식이 예상과 달랐다 / `ResponseEntity` 의 raw type 만 가능한 줄 알았는데 generic 도 가능했다 / 의도적 위반 메시지가 stack trace 안에 묻혀 있었다
|
||||
- **다음 반복에서 개선할 점**:
|
||||
- 베이스라인 측정 자동화? IDE 단축키? grep alias?
|
||||
- rule 작성 전 *제일 단순한 1개 메서드* 부터 잡고 정교화하는 순서?
|
||||
- **부수 효과로 발견한 것**:
|
||||
- 예: 현재 코드베이스의 다른 패턴 위반 발견
|
||||
- **이 과제의 난이도가 적정했는가**: `너무 쉬움` / `적정` / `너무 어려움`
|
||||
- **시니어 사고 체크 항목** (필수):
|
||||
- 본 rule 의 trade-off 1-2개를 명시했는가?
|
||||
- 우회 가능 시나리오를 예측했는가?
|
||||
- 본 rule 이 잡지 *못하는* 경계 leak 패턴은? (예: `Object` 반환, raw `Map`, exception body)
|
||||
|
||||
## 9. 출처 / Sources
|
||||
|
||||
| Source | 정당화 영역 |
|
||||
|---|---|
|
||||
| [[raw/company-tech-blogs/skillable-hands-on-lab-structure]] | template 9-section 구조 |
|
||||
| [[raw/company-tech-blogs/deliberate-practice-software-developers-redgreencode]] | §5 단계 분할 + §8 reflection |
|
||||
| [[raw/branch-notes/feature-boundary-validation-mapping-contract]] | D1, D8, Claims To Verify 1번째 항목 (본 과제가 검증하는 결정) |
|
||||
| [[raw/project-notes/ca-skeleton-operational-contract]] | §4 Boundary Validation & Mapper Contract |
|
||||
|
||||
## 10. 완료 후 정리 / Closure
|
||||
|
||||
- **최종 status_label**: `done` | `abandoned`
|
||||
- **소요 시간 실측**: <분> (vs duration_estimate 120) — 차이는 §8 회고에
|
||||
- **promotable 후보**:
|
||||
- `actually-implemented` → `feature-boundary-validation-mapping-contract` Claims To Verify 1번째 항목 status 를 `planned` → `actually-implemented` 로 갱신
|
||||
- `locally-verified` → build pass + 의도적 위반 build fail 양쪽 확인
|
||||
- **추출하지 않을 항목** (단순 학습):
|
||||
Reference in New Issue
Block a user