Files
llm-wiki/raw/daily-tasks/develop/2026-05-29-archunit-controller-domain-return-rule.md
T

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
raw/branch-notes/feature-boundary-validation-mapping-contract
raw/project-notes/ca-skeleton-operational-contract
ca-skeleton-operational-contract feature-boundary-validation-mapping-contract 2026-05-29 2026-05-28
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 / 부모 (필수)

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 에는 passwordHashversion 이 그대로 흘러간다. 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

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

자동 검증:

# 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 다루기
  • 다음 과제 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-implementedfeature-boundary-validation-mapping-contract Claims To Verify 1번째 항목 status 를 plannedactually-implemented 로 갱신
    • locally-verified → build pass + 의도적 위반 build fail 양쪽 확인
  • 추출하지 않을 항목 (단순 학습):