Skip to content

[ 아키텍처 ] 같은 일을 여러 방식으로 해서 구조가 지저분하다 #132

Open
0 of 3 issues completed

Description

@RosieOh

무엇이 문제인가

로직이 엉킨 게 아니라 같은 일을 여러 방식으로 하고 있다. 세어 봤다.

증상 수치
예외 체계 CareServiceException 96건 · BusinessException 51건 · 도메인 전용 94건
catch (Exception e) 로 원인 덮어쓰기 ChatbotService 11회 · HealthService 10회 (상태 코드가 무너진다)
Map<String, Object> 응답 컨트롤러 24곳 (OpenAPI 에 타입이 object 로만 나가 계약 대조가 못 본다)
소유권 검증 구현 3곳 (ChildService · GrowthChartService · HealthService)
다른 도메인 리포지토리 직접 참조 UserRepository 24곳 · ChildRepository 12곳
도메인 코드가 core 에 core/controller/CareFacilityApiController 373줄
문서와 코드 불일치 문서는 조합 계층을 domain.*.facade 라 하는데 실제는 domain.*.app
거대 서비스 HealthService 977줄 · UserService 626 · CommunityService 616

가장 구조적인 것: Child 엔티티는 domain/user 에 있는데 ChildService 는 domain/health 에 있다. 아이 하나가 두 도메인에 쪼개져 있어 아이를 쓰는 모든 도메인이 domain.user 를 import 한다(도메인 간 결합 1위: health→user 30건).

단계

  • 0. 규칙을 테스트로 고정 (현재 위반은 baseline, 새 위반만 실패)
  • 1. 자리 정리 (core 의 도메인 코드, 조합 계층 분리, 패키지 이름, 문서 일치)
  • 2. 예외 한 체계로
  • 3. Map 응답 → DTO
  • 4. Child 를 제 자리로
  • 5. 거대 서비스 분해

Activity

  1. added this to the Refactor Sprint milestone on Oct 4, 2026
  2. added theissue type on Oct 4, 2026
  3. RosieOh commented on Oct 23, 2026

    @RosieOh
    ContributorAuthor

    여덟 단계로 489 → 241, 그리고 멈추는 선

    단계 내용 baseline
    0 구조 규칙을 ArchUnit 테스트로 고정 489
    1a·1b core 분리, careFacility → facility 391
    2 "찾을 수 없습니다" 34곳 → ResourceNotFoundException 391
    3a·3b 중복 엔드포인트 7개 삭제, Map 응답 17곳 → DTO 383
    4a 자녀 조회 입구(ChildDirectory), 소유권 답 통일 364
    5a·5b·5c 도달 불가 코드 제거, 아이 조회 분리, HealthService 3분할 357
    6 어드민 컨트롤러 4개를 서비스로 331
    7 남은 컨트롤러 6개 → 컨트롤러→리포지토리 0 291
    8 인증·인가 기반 코드 → core→domain 0 241

    남은 241건 중 237건이 "다른 도메인의 리포지토리를 직접 쓰지 않는다" 한 규칙입니다.
    이 숫자는 0 으로 만들지 않기로 했습니다. 근거는 docs/quality/architecture-rules.md 에 적었습니다 (#151).

    23개 엔티티가 @ManyToOne User 를 들고 있다 — 전부 DB 외래키
    

    JPA 로 외래키를 채우려면 엔티티가 필요하고, 그래서 UserRepository 를 주입합니다. 정리 부채가 아니라 설계 비용입니다. 4a 에서 FacilityWaitlist.child 를 ID 로 바꾼 건 읽는 곳이 없어서 가능했고, 나머지는 응답에서 읽습니다 — 끊으면 목록 화면에 N+1 이 생깁니다.

    규칙은 남깁니다. 237 은 "고쳐야 할 목록" 이 아니라 "여기까지는 알고 있다" 는 선이고, 새 도메인이 남의 리포지토리를 주입하면 238 이 되어 빌드가 실패합니다.

    이 이슈의 값은 숫자가 아니었습니다

    규칙을 맞추려고 코드를 옮기기 전에 그 안을 읽은 것이 값을 했습니다. 각 단계에서 나온 것:

    단계 찾은 것
    2 "찾을 수 없습니다" 34곳이 500 이었다 (운영 알림까지 울릴 수 있는 자리)
    3a 알림 설정 API 가 저장하지 않고 받은 값을 돌려줬다. 통계는 HIGH=5, NORMAL=15 가 코드에 박혀 있었다
    4a 소유권 검증 3벌의 답이 404·403·500 으로 갈렸다. 403 은 자녀의 존재를 알려줬다
    5b 월령 계산 6벌 중 하나만 단위가 달라 돌 지난 아이 전부에게 신생아 정책이 추천됐다
    5c "일정" API 두 개가 일정이 아니었다 (권장 시기 항상 null, OVERDUE 가 나올 수 없음)
    6 어드민 "알림 발송" 이 발송하지 않았다. 게시글 조치가 숨김/하드삭제로 갈렸다
    7 관리자 탈퇴가 개인정보를 남겼다. 비로그인 전환 클릭이 한 건도 집계되지 않았다
    8 카카오 와이어 포맷이 우리 API 응답 패키지에 있었다
    후속 탈퇴한 계정에 알림이 계속 발송됐다 (#149)

    OpenAPI 237 → 226 경로(중복·거짓 엔드포인트 15개 삭제), 테스트 601 → 650개.

    남겨 둔 것

    • CommunityService 가 SecurityContextHolder 를 직접 쓰는 1건 — baseline 에서 계속 추적
    • catch (Exception e) 로 원인을 덮는 자리 (ChatbotService 등) — HealthService 8곳은 4a 에서 처리
    • 어드민 조회용 포트 — 만들 가치가 없다고 판단한 근거를 문서에 적었습니다
  4. RosieOh commented on Oct 24, 2026

    @RosieOh
    ContributorAuthor

    중간 점검 — 세어 본 현재 상태

    이슈 본문의 수치를 오늘 다시 세었다. 체크한 것은 측정으로 확인된 것뿐이다.

    증상 처음 지금 단계
    ArchUnit 규칙 고정 없음 9개 규칙 · 위반 241개 동결 0 ✅
    core 의 도메인 컨트롤러 CareFacilityApiController 373줄 없음 (core/controller 비어 있음) 1 ✅
    조합 계층 이름 문서 facade / 코드 app app 7개 · facade 0개, 문서 일치 1 ✅
    Map<String,Object> 응답 컨트롤러 24곳 2 파일 3 ✅
    예외 체계 CareServiceException 96 · BusinessException 51 89 · 84 2 ❌
    catch (Exception e) ChatbotService 11 · HealthService 10 전체 82곳 2 ❌
    Child 위치 엔티티 domain/user, 서비스 domain/health 그대로 4 ❌
    HealthService 977줄 385줄 5 부분
    UserService 626줄 626줄 5 ❌
    CommunityService 616줄 616줄 5 ❌

    ArchUnit 베이스라인은 489 → 241 로 줄었다. 테스트는 601 → 681개.

    2번(예외 한 체계로)이 왜 안 움직였나

    BusinessException 이 51 → 84 로 늘었다. 새로 만진 코드는 전부 그쪽을 쓰기 때문이다.
    CareServiceException 은 96 → 89 로 거의 그대로다. 즉 새 코드만 정리되고 기존 96곳은
    손대지 않았다.
    두 체계가 공존하는 상태가 이어지고 있다.

    한 번에 바꾸면 상태 코드가 조용히 달라질 수 있다 — 두 예외의 기본 매핑이 다르고, 그걸 기대하는
    계약 테스트가 있다. 바꾸려면 매핑 차이를 먼저 테스트로 드러내고 옮겨야 한다.

    4번(Child)을 손대지 않은 이유

    가장 구조적인 문제로 적어 놨지만 아직 그대로다. Child 엔티티를 옮기면 @ManyToOne 으로
    엮인 다른 엔티티의 import 가 전부 따라 움직이고, ArchUnit 베이스라인의 기존 위반이 전부
    "새 위반" 으로 보인다
    — 파일이 옮겨지면 키가 달라지기 때문이다. 재동결이 필요하고, 그러면
    그 시점의 다른 위반까지 같이 묻힌다. 분리해서 한 PR 로 해야 한다.

    남은 237개 도메인 경계 위반을 0으로 만들지 않기로 한 이유

    23개 엔티티가 @ManyToOne User 를 들고 있다. 이건 설계 취향이 아니라 DB 의 FK 다.
    끊으려면 ID 만 들고 조회를 따로 해야 하는데, 목록 화면마다 N+1 이 생긴다. 경계를 지키려고
    쿼리를 늘리는 건 거꾸로다. 베이스라인으로 더 늘지 않게 막아 두는 쪽이 맞다.

    남은 단계(2·4·5)는 각각 독립적으로 닫을 수 있다. 하위 이슈로 쪼개는 것이 맞아 보인다.

  5. RosieOh commented on Oct 24, 2026

    @RosieOh
    ContributorAuthor

    2단계를 파다가 실제 결함이 나왔다 — #158 / PR #159

    "예외 한 체계로" 를 시작하려고 CareServiceException 89곳을 들여다봤더니, 체계가 지저분한
    문제가 아니라 상태 코드가 틀린 문제였다.

    CareServiceException 은 상태 코드를 문자열로 들고 다니고, 전역 핸들러의 매핑이 네 가지
    (UNAUTHORIZED / FORBIDDEN / USER_NOT_FOUND / KAKAO*)뿐이라 나머지가 전부 500 이다.
    throw 47곳 중 39곳이 코드를 주지 않는다.

    실제로 나가고 있던 것:

    상황 나가던 코드
    그 시간에 예약 자리가 없음 500
    이미 취소한 예약을 또 취소 500
    끝나는 시각이 시작보다 앞 500
    남의 알림을 요청 500 (소유권 검사는 403 을 던졌다)

    마지막이 가장 나쁘다. catch (Exception e) 가 안쪽의 403 을 삼켜 권한 탐색 시도가 로그에
    서버 장애로 쌓인다.
    요청 경로에서 그렇게 감싸는 블록이 24곳이었다.

    PR #159 에서 고쳤다. 실패 테스트 7개를 먼저 세워 500 쪽으로 실패하는 것을 확인한 뒤 고쳤고,
    기존 예약 테스트 3개가 깨졌다 — 그 테스트가 "거부가 500 으로 나간다" 를 고정하고 있었다.

    2단계에 남은 것

    #159 는 상태 코드가 틀린 곳만 고쳤다. 두 예외 체계는 그대로 공존한다.

    • CareServiceException 은 여전히 문자열 코드로 상태를 정하고, 코드를 주지 않으면 500 이다.
      같은 함정이 남아 있다.
    • 합치려면 두 체계의 기본 매핑 차이를 먼저 테스트로 드러내야 한다. 한 번에 바꾸면 상태 코드가
      조용히 달라지고, 그걸 기대하는 계약 테스트가 있다.
    • 백그라운드 동기화(external/publicdata)의 catch (Exception e) 는 원인을 덮어쓰는 문제가
      그대로다. HTTP 와 무관해 fix: 사용자 잘못을 서버 잘못으로 보고하지 않는다 (입력·권한 오류가 500) #159 범위에서 뺐지만, 실패 원인이 로그에만 남고 동기화 상태에는
      드러나지 않는다.

    근본 해결은 CareServiceException 을 없애고 ErrorCode 를 직접 드는 BusinessException
    하나로 모으는 것
    이다. ErrorCode 는 HttpStatus 를 직접 들고 있어 이 함정이 없다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions