Skip to content

refactor: 자녀 조회에 입구를 만들고 소유권 답을 하나로 맞춘다 - #141

Merged
RosieOh merged 1 commit into
mainfrom
refactor/child-read-port
Oct 5, 2026
Merged

RosieOh merged 1 commit into
mainfrom
refactor/child-read-port

Conversation

@RosieOh

@RosieOh RosieOh commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Refs #132 (4a 단계)

도메인 경계 위반 268건 중 자녀·사용자 리포지토리 직접 참조가 가장 큰 덩어리였습니다. 자녀는 user 도메인에 속하는데 쓰는 쪽은 health·policy·facility 입니다. 그 도메인들이 ChildRepository 를 그냥 주입해 쓰면서 두 가지가 복제됐습니다.

1. 소유권 검증이 세 벌이었고, 같은 질문에 답이 셋으로 갈렸다

경로 남의 아이를 요청하면 문제
/children/… 404 맞는 동작
HealthService.assertChildOwnedByUser 403 그 자녀가 존재한다는 사실을 알려준다. ID 를 훑으면 "이 번호는 누군가의 자녀" 목록을 만들 수 있다
/health/vaccines/schedule, /health/checkups/schedule 500 위 403 이 catch (Exception e) 에 걸려 CareServiceException 으로 덮였다. 권한 거부가 서버 장애로 집계되고, 클라이언트는 자기가 잘못 보냈다는 걸 알 수 없다

ChildOwnershipContractTest 가 여섯 경로를 전부 두드려 404 를 확인합니다. 테스트를 먼저 쓰니 500 두 건이 바로 드러났습니다(expected: 404 but was: 500).

함께 고친 것: HealthService 의 catch (Exception e) 10곳 중 8곳에 우리 예외를 그대로 통과시키는 catch (CareCodeException e) { throw e; } 를 넣었습니다. 없으면 404·403 이 전부 500 으로 덮입니다.

Slack 알림까지는 가지 않았습니다 — 알림은 최후 핸들러에서만 울리고 CareServiceException 은 자기 핸들러가 있습니다. 테스트로 확인했고, 나중에 최후 핸들러로 떨어지게 바뀌면 그때부터 울리므로 그 경우도 테스트로 막아 뒀습니다.

2. 월령 계산이 다섯 벌이었고, 생일이 없을 때 처리가 곳마다 달랐다

정책 추천·지원금 비교·마감 알림·제보 요청·대기 등록이 각자 ChronoUnit.MONTHS.between(child.getBirthDate(), today) 를 들고 있었고 null 처리가 제외 / 0 / 통과로 갈렸습니다. 월령은 추천 결과를 좌우하는 입력이라 하나만 어긋나면 같은 아이에게 다른 정책이 나옵니다.

ChildView.ageMonths(기준일) 한 곳으로 모았습니다. 생일이 없으면 null 이고 0 으로 바꾸지 않습니다 — 0개월은 "갓 태어난 아이" 로 읽혀 신생아 정책이 추천됩니다.

판단은 여전히 호출하는 쪽이 합니다. 마감 알림만 "생일을 모르면 배제하지 않는다" 로 다른데, 놓친 손해가 성가심보다 크다는 판단이라 의도된 차이입니다. 계산은 하나, 판단은 각자.

입구의 모양

public interface ChildDirectory {
    List<ChildView> childrenOf(Long userId);
    ChildView requireOwnedChild(Long childId, Long parentUserId);  // 없는 자녀와 남의 자녀를 구분하지 않는다
}

엔티티가 아니라 ChildView(읽기 전용 값)를 돌려줍니다. 쓰는 쪽이 자녀 정보를 고치거나 child.getUser() 로 사용자 정보까지 타고 들어가는 일이 없습니다.

곁에서 드러난 것

  • FacilityWaitlist.child 가 @ManyToOne Child 였는데 읽는 곳이 없었습니다. CHILD_ID 를 채우는 용도뿐이라 바로 위 facilityId 와 같은 방식(Long childId)으로 맞췄습니다. DB 외래키와 중복 등록 제약은 그대로입니다.
  • 자녀 미등록·생일 없음·남의 대기 기록 수정이 500 이었습니다. 각각 400·400·403 으로 고쳤습니다.

baseline 재생성

생성자에 ChildDirectory 가 추가되면서 위반 설명에 박힌 생성자 서명이 바뀌어, 기존 위반 10건이 새 위반으로 보였습니다. 1단계와 같은 상황이라 freeze.refreeze 를 한 번 켜고 규칙별 숫자가 늘지 않은 것을 확인한 뒤 되돌렸습니다.

숫자

  • ArchUnit baseline 383 → 364 (도메인 경계 268 → 249)
  • ChildRepository 를 쓰는 도메인: policy 5곳·facility 1곳 → 0. health 5곳은 엔티티가 필요해 남겼습니다(4b)
  • 전체 테스트 624개, 실패 0
  • OpenAPI 스펙 변화 없음 — 응답 모양을 바꾸지 않았습니다. 상태 코드만 500 → 404/400/403 으로 정확해집니다

도메인 경계 위반 268건 중 자녀·사용자 리포지토리 직접 참조가 가장 큰 덩어리였다.
자녀는 user 도메인에 속하는데 쓰는 쪽은 health·policy·facility 다. 그 도메인들이
ChildRepository 를 그냥 주입해 쓰면서 두 가지가 복제됐다.

1) 소유권 검증이 세 벌이었고, 같은 질문에 답이 셋으로 갈렸다

   /children/...                        → 404 (맞는 동작)
   HealthService.assertChildOwnedByUser → 403
   /health/{vaccines,checkups}/schedule → 500

   403 은 그 자녀가 존재한다는 사실을 알려준다. ID 를 훑으면 "이 번호는 누군가의
   자녀" 목록을 만들 수 있다. 500 은 그 403 이 catch (Exception e) 에 걸려
   CareServiceException 으로 덮인 결과다. 권한 거부가 서버 장애로 집계되고,
   클라이언트는 자기가 잘못 보냈다는 걸 알 수 없다.

   ChildOwnershipContractTest 로 여섯 경로를 두드려 404 를 확인한다. 테스트를 먼저
   쓰니 500 두 건이 바로 드러났다(expected: 404 but was: 500).

   함께: HealthService 의 catch (Exception e) 10곳 중 8곳에 우리 예외를 그대로
   통과시키는 catch (CareCodeException e) { throw e; } 를 넣었다. 없으면 404·403 이
   전부 500 으로 덮인다. Slack 알림까지는 가지 않았다 — 알림은 최후 핸들러에서만
   울리고 CareServiceException 은 자기 핸들러가 있다(테스트로 확인).

2) 월령 계산이 다섯 벌이었고, 생일이 없을 때 처리가 곳마다 달랐다

   정책 추천·지원금 비교·마감 알림·제보 요청·대기 등록이 각자
   ChronoUnit.MONTHS.between(...) 을 들고 있었고 null 처리가 제외/0/통과로 갈렸다.
   월령은 추천 결과를 좌우하는 입력이라 하나만 어긋나면 같은 아이에게 다른 정책이
   나온다. ChildView.ageMonths(기준일) 한 곳으로 모았다. 생일이 없으면 null 이고
   0 으로 바꾸지 않는다 — 0개월은 "갓 태어난 아이" 로 읽혀 신생아 정책이 추천된다.

   판단은 여전히 호출하는 쪽이 한다. 마감 알림만 "생일을 모르면 배제하지 않는다" 로
   다른데, 놓친 손해가 성가심보다 크다는 판단이라 의도된 차이다. 계산은 하나, 판단은 각자.

입구는 엔티티가 아니라 ChildView(읽기 전용 값)를 돌려준다. 쓰는 쪽이 자녀 정보를
고치거나 child.getUser() 로 사용자 정보까지 타고 들어가는 일이 없다.

곁에서 드러난 것
- FacilityWaitlist.child 가 @manytoone Child 였는데 읽는 곳이 없었다. CHILD_ID 를
  채우는 용도뿐이라 바로 위 facilityId 와 같은 방식(Long childId)으로 맞췄다.
  DB 외래키와 중복 등록 제약은 그대로다.
- 자녀 미등록·생일 없음·남의 대기 기록 수정이 500 이었다. 각각 400·400·403 으로 고쳤다.

생성자 서명이 바뀌어 기존 위반 10건이 새 위반으로 보였다. 1단계와 같은 상황이라
freeze.refreeze 를 한 번 켜고 규칙별 숫자가 늘지 않은 것을 확인한 뒤 되돌렸다.

- ArchUnit baseline 383 → 364 (도메인 경계 268 → 249)
- ChildRepository 를 쓰는 도메인: policy 5곳·facility 1곳 → 0 (health 5곳은 엔티티가
  필요해 남겨 둠)
- 전체 테스트 624개, 실패 0. OpenAPI 스펙 변화 없음 — 응답 모양을 바꾸지 않았다

Refs #132
@RosieOh
RosieOh merged commit 2e97acf into main Oct 5, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant