13 KiB
title, source_type, track, status, status_label, difficulty, duration_estimate, prerequisites, parent_project, parent_branch, target_date, created, tags
| title | source_type | track | status | status_label | difficulty | duration_estimate | prerequisites | parent_project | parent_branch | target_date | created | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| daily-task / develop / archunit-controller-domain-return-rule | daily-task | develop | raw | not-started | intermediate | 120 |
|
ca-skeleton-operational-contract | feature-boundary-validation-mapping-contract | 2026-05-29 | 2026-05-28 |
|
daily-task / develop / archunit-controller-domain-return-rule
Layer:
raw/daily-tasks/develop/— 개발 트랙 일일 실습 과제.status_label:not-started→ 시작 시in-progress→ 종료 시donedifficulty: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+
사전 셋업:
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()chainDescribedPredicate합성 (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에서@RestControllerannotated class 들의 method return type 한 줄로 정렬해서 listing - Done when:
- 현재 위반 카운트 N개 명시 (0이어도 무방 — 기준선만 확보)
- §7 결과물 섹션에 "baseline violation: N" 기록
Step 2: ArchUnit rule 작성 (~30min)
- What:
ControllerReturnTypeRuleTest.java에 단일@ArchTestrule 작성. controller class 의 모든 public method 의 return type 이 허용 set (DTO record /ResponseEntity<DTO>/void) 안에 있는지 검사. - How (hint):
classes().that().areAnnotatedWith(RestController.class)로 controller selection.should()뒤에 customArchCondition<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) responseDtocast 같은 hack 사용) - build 실패 시 stack trace 가 아니라 violation 메시지 의 첫 줄을 읽을 것
- Done when:
- 실패 메시지에 rule description (예:
controllers should return only DTO record or ResponseEntity<DTO record>) 포함 - 실패 메시지에 정확한 violating method signature 포함
- 변경 복구 후 build 다시 통과
- 실패 메시지에 rule description (예:
- 공통 실수:
- 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()의JavaParameterizedTypecast 필요 - 또는 더 간단한 우회:
ResponseEntity인 경우에만 별도 검사 분기
- ArchUnit 의
- 트레이드오프 의식 (시니어 사고):
- 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
자동 검증:
# 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:
- 위반 패턴: <패턴 목록>
- 학습한 개념 (wiki/concepts 로 ingest 후보):
- ArchUnit predicate 합성 (
and/or/not) JavaParameterizedType으로 generic 인자 검사ResponseEntity<T>와 ArchUnit 의 generic erasure 다루기
- ArchUnit predicate 합성 (
- 다음 과제 thread:
- request DTO 가 application service signature 에 직접 나타나는지 검사 (
feature-boundary-validation-mapping-contractClaims To Verify 2번째 항목) - MapStruct generated code 의 architecture exemption 검증
@JsonView또는 DTO 패키지 격리 대안의 trade-off 비교
- request DTO 가 application service signature 에 직접 나타나는지 검사 (
8. 회고 / Reflection (~5min)
- 막혔던 곳 (몇 분 / 어디서):
- 예상과 다른 점:
- 예: ArchUnit 의 generic type 처리 방식이 예상과 달랐다 /
ResponseEntity의 raw type 만 가능한 줄 알았는데 generic 도 가능했다 / 의도적 위반 메시지가 stack trace 안에 묻혀 있었다
- 예: ArchUnit 의 generic type 처리 방식이 예상과 달랐다 /
- 다음 반복에서 개선할 점:
- 베이스라인 측정 자동화? IDE 단축키? grep alias?
- rule 작성 전 제일 단순한 1개 메서드 부터 잡고 정교화하는 순서?
- 부수 효과로 발견한 것:
- 예: 현재 코드베이스의 다른 패턴 위반 발견
- 이 과제의 난이도가 적정했는가:
너무 쉬움/적정/너무 어려움 - 시니어 사고 체크 항목 (필수):
- 본 rule 의 trade-off 1-2개를 명시했는가?
- 우회 가능 시나리오를 예측했는가?
- 본 rule 이 잡지 못하는 경계 leak 패턴은? (예:
Object반환, rawMap, 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-contractClaims To Verify 1번째 항목 status 를planned→actually-implemented로 갱신locally-verified→ build pass + 의도적 위반 build fail 양쪽 확인
- 추출하지 않을 항목 (단순 학습):