--- 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` 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` 만 허용 — 을 작성하고, 의도적으로 위반된 코드를 추가해 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//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` / `void`) 안에 있는지 검사. - **How (hint)**: - `classes().that().areAnnotatedWith(RestController.class)` 로 controller selection - `.should()` 뒤에 custom `ArchCondition` 작성 — class 내부 method 순회 - 허용 set 정의: 해당 패키지 (e.g., `.web.dto.*`) 아래 record 인지, 또는 `ResponseEntity` 의 raw type 인지 - `ResponseEntity` 의 generic 인자 추출은 `JavaParameterizedType` 사용 - **함정** (의도적 노출): - `ResponseEntity` 처럼 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`) 포함 - 실패 메시지에 정확한 violating method signature 포함 - 변경 복구 후 build 다시 통과 - **공통 실수**: - rule 자체에 typo 가 있어 *항상* 실패 — 의도된 위반인지 unintended 위반인지 구분 필요 ### Step 4: `ResponseEntity` 위반 잡기 (심화) (~25min) - **What**: Step 3 의 위반을 `ResponseEntity` 형태로 변경. 현재 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` 패턴이 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` 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//architecture/ControllerReturnTypeRuleTest.java` — controller return type rule - (Step 5 했다면) `adapter-web/src/test/java//architecture/JsonLeakIntegrationTest.java` - **베이스라인 측정값** (Step 1): - Pre-rule violation count: - 위반 패턴: <패턴 목록> - **학습한 개념** (wiki/concepts 로 ingest 후보): - ArchUnit predicate 합성 (`and`/`or`/`not`) - `JavaParameterizedType` 으로 generic 인자 검사 - `ResponseEntity` 와 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 양쪽 확인 - **추출하지 않을 항목** (단순 학습):