# 코드 품질 평가 (KUSITMS 2026 백엔드) > 분석 대상: branch `dev` / HEAD `cb41e36` / 2026-05-25 기준 > 평가 기준: 백엔드 평가표 1번 "코드 품질" 4개 소항목 (가중치 25%) > 분석 범위: `src/main/java/com/groute/groute_server/**` (main 326 / test 43 Java 파일) > 만족도 정의: **우수** (학생 프로젝트 상위 5%) / **충족** (평균 이상, 무난) / **부분충족** (눈에 띄는 결함 있음) / **미흡** (재작업 필요) --- ## 0. 종합 요약 | # | 소항목 | 만족도 | 한 줄 정당화 | | --- | ----------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | A | 코드는 유지보수와 확장이 용이하게 작성되었는가 | **충족** | 공통 예외/에러코드, 트랜잭션 후크, 환경별 yaml 분리가 견고하나, 도메인 객체의 책임 누수(AI 태깅 → User 상태)와 트랜잭션 어노테이션 컨벤션 흔들림이 누적 중 | | B | 프로젝트 구조가 잘 조직되고 모듈화되어 있는가 | **부분충족** | 패키지 트리는 직관적이나 (1) CONTRIBUTING.md와 실제 코드가 정면 충돌(도메인 JPA 의존 금지 vs 9개 도메인 모두 `jakarta.persistence` import) (2) `record` 도메인이 5개 다른 도메인의 허브가 되어 있음 | | C | SOLID 원칙 및 적절한 디자인 패턴이 적용되었는가 | **충족** | DIP/ISP가 헥사고날 두 모듈에서 정석적이고, Strategy/Decorator/Facade/Adapter 패턴이 실제로 동작. 다만 SRP가 두 거대 메서드(`AiTaggingService.completeTagging`, `NotificationScheduler.dispatch`)에서 무너짐 | | D | 코드는 깔끔하고 이해하기 쉽게, 간결하게 작성되었는가 | **우수** | 메서드 길이/네이밍/Javadoc/테스트 컨벤션 모두 학생 프로젝트 평균을 크게 상회. 다만 `User.java`의 Javadoc 정렬 버그와 `DateTimeFormatters` placeholder는 평가자에게 핀포인트로 잡힐 위험이 있어 P0로 정리 권장 | ### 우선순위 To-do 압축 (전체 항목 통합) **P0 (지금 PR 1개로 가능, 평가자에게 즉시 보임)** - [ ] `User.java:215-257` Javadoc 정렬 버그 수정 — `streakSnapshotAsOf` 용 Javadoc이 `markPendingCoachMark`에 잘못 붙어 있음 - [ ] `common/util/DateTimeFormatters.java` placeholder 상태 정리 — Javadoc 30줄에 실제 정의는 ZONE_KST 1개 - [ ] `CONTRIBUTING.md:246-247` 또는 `record/domain/**` `report/domain/**` 중 한 쪽 정정 — 문서는 "JPA 금지", 실제 9개 도메인 클래스 모두 `import jakarta.persistence.*` **P1 (구조 정리)** - [ ] `AiTaggingService.completeTagging:188-248` 3개 책임으로 분리 (StarTag 저장 / ScrumTitle commit / User 마일스톤) - [ ] `NotificationScheduler.dispatch:69-161` 트랜잭션 안 FCM 호출 분리 - [ ] `MINI_LIMIT` / `CAREER_LIMIT` 중복 정의 단일화 (`ReportLimits` 상수 클래스) - [ ] `ReportService.createReport` / `retryReport` 의 try/catch 블록 헬퍼로 추출 - [ ] `CompetencyCategory` 공용 enum을 `common/enums/`로 이동 **P2 (헥사고날 컨벤션 회복)** - [ ] `ScrumDailyQueryService:26` 만 유일하게 깨진 port 컨벤션 복원 - [ ] `ReportService` 클래스 단위 `@Transactional(readOnly=true)` 추가 - [ ] `home.service.HomeService` → `record.application.port.out.*` 직접 호출 우회 - [ ] Swagger 401/403 `@ApiResponse` 메타 어노테이션 추출 - [ ] `report.domain.Report.user` ManyToOne → `Long userId`로 단순화 **P3 (코스메틱)** - [ ] `ScrumBulkWriteService` 단계 주석 번호 정렬 (`// 2.` 두 번 등장) - [ ] `ErrorCode` prefix 통일 (`RECORD_*` vs `REC_*`, `STAR_*` vs `STAR_RECORD_*`) - [ ] `Scrum`/`StarRecord`/`Report` `@NoArgsConstructor(access = PROTECTED)` 통일 - [ ] OAuth Strategy 정석화 (provider 4종 이상 늘릴 계획 있을 때만) ### 평가자용 5분 검증 가이드 평가자가 다음 6개 위치만 열어 보면 본 문서의 핵심 90%를 직접 확인할 수 있다. 1. `CONTRIBUTING.md:237-258` — 선언된 아키텍처 규약 2. `src/main/java/com/groute/groute_server/record/domain/Scrum.java:6` — JPA import (B-2 규약 위반의 직접 증거) 3. `src/main/java/com/groute/groute_server/record/application/service/AiTaggingService.java:188-248` — C-2 SRP 위반의 직접 증거 4. `src/main/java/com/groute/groute_server/user/service/AccountHardDeleteService.java` + `AccountHardDeleteDbCleaner.java` — A·C 잘 된 패턴 (트랜잭션 경계 분리) 5. `src/main/java/com/groute/groute_server/auth/service/oauth/OAuth2EnvAwareAuthorizationRequestResolver.java:34-71` — C Decorator 패턴 적용 증거 6. `src/main/java/com/groute/groute_server/user/entity/User.java:215-257` — D Javadoc 정렬 버그 --- ## A. 코드는 유지보수와 확장이 용이하게 작성되었는가 ### 만족도: 충족 ### 근거 — 잘 된 점 - **ErrorCode 단일 enum + 도메인 prefix 컨벤션** (`common/exception/ErrorCode.java:22-94`). 도메인별 섹션 주석이 있고 `{DOMAIN}_{NNN}` 규칙으로 신규 코드 추가 시 위치가 명확하다. → 6개월 후 합류한 팀원도 코드 추가 위치를 헷갈리지 않음. - **GlobalExceptionHandler가 unhandled 500만 Discord 웹훅으로 알림 분리** (`common/exception/GlobalExceptionHandler.java:37-123`). BusinessException은 warn 로그만, 5xx만 외부 알림 — "알람 피로도"까지 고려된 운영 설계. - **`AfterCommitExecutor`로 "DB 트랜잭션 ↔ 외부 I/O 분리" 패턴을 공용화** (`common/transaction/AfterCommitExecutor.java`). 실사용처 2곳 확인 (`record/application/service/DeleteStarImageService.java:26`, `record/application/service/StarImageCascadeCleaner.java:31`) — "한 번 만들고 끝"이 아닌 실제로 재사용 중. - **`User` 엔티티가 풍부한 도메인 메서드 보유 (anemic 회피)**: `scheduleHardDelete`, `advanceCopyIndex`, `recordOnDate`, `streakSnapshotAsOf`, `markPendingCoachMark` (`user/entity/User.java:144,165,195,234,257`). 각각 Javadoc으로 분기 시나리오·호출 책임 명시. - **환경별 yaml 분리 + fail-fast** (`src/main/resources/application.yaml:167-285`). stg/prod에서 SSM 주입 누락 시 부팅 실패를 의도(주석으로 명시 — 라인 130, 158). - **Flyway 시간 기반 컨벤션 + `out-of-order: true`** (`application.yaml:39`, `src/main/resources/db/migration/V{yyMMddhhmmss}__*.sql` 16개). 병렬 작업 머지 충돌 흡수. - **알림 카피 풀이 SSM JSON으로 외부화** (`common/notification/copy/NotificationCopy.java:16`, `NotificationCopyConfig.java:37-47`). 카피 한 줄 바꿀 때 배포 불필요. - **JaCoCo 60% 게이트를 `check`에 묶음** (`build.gradle:93-114`). PR 직전 자동 검증. 학생 프로젝트에서 이 정도 게이트는 드물다. ### 근거 — 개선 필요 - **AI 태깅 완료가 User 상태를 직접 변경하는 책임 누수** (`record/application/service/AiTaggingService.java:238-247`). `AiTaggingService.completeTagging()`이 `user.markPendingCoachMark()` / `user.markPendingReportModal()`을 직접 호출. record 도메인이 user 도메인의 모달 트리거 정책을 알고 있는 형태 — 향후 "20번째 STAR 시 친구 초대 모달도 띄우자" 같은 변경이 오면 또 record를 수정해야 함. - **하드코딩된 매직 넘버**: `taggedCount == 10`, `taggedCount >= 20 && taggedCount % 10 == 0` (`AiTaggingService.java:243-246`)이 raw로 등장. MINI/CAREER 임계치는 **두 클래스에 중복 선언**되어 있다 (`ReportService.java:50-51`, `ReportTransactionalService.java:34-35`) — 한 곳만 바꾸면 일관성 깨짐. - **포트 누락으로 헥사고날 규약 한 곳에서 깨짐**: `ScrumDailyQueryService:26`만 유일하게 `ScrumJpaRepository`를 직접 의존. 클래스 Javadoc에서 "예외적으로 port 없이 둔다"고 합리화하나, 규약을 한 곳만 깨면 다음 사람이 "여기도 그래도 되네?" 하며 점점 깨지는 깨진 유리창 효과. - **재시도 실패 시 GENERATING 영구 stuck 가능** (`report/application/service/ReportService.java:124-132, 198-206`). AI 호출 실패 시 `report.fail()` + `saveReportPort.save(report)`만 호출 — 마지막 save가 또 실패하면 상태 복구 안 됨. AI 태깅과 달리 `@TransactionalEventListener` 같은 안전망 없음. - **`@Transactional` 컨벤션 비일관**: `AiTaggingService`는 클래스 단위 `readOnly = true` + 쓰기 메서드만 override (정석). 반면 `ReportService`는 클래스 단위 어노테이션 없이 메서드별 반복 (`ReportService.java:69, 147`). 새 메서드 추가 시 transactional 누락을 발견하기 어렵다. ### To-do - [ ] **[P1]** AI 태깅 완료 → User 상태 변경 책임 분리. `record/application/service/AiTaggingService.java:238-247`. `ApplicationEvent` 발행 + user/notification 도메인에서 `@TransactionalEventListener`로 구독. 예상 반나절. - [ ] **[P1]** `MINI_LIMIT` / `CAREER_LIMIT` 단일 진실원. `report/application/service/ReportService.java:50-51` + `ReportTransactionalService.java:34-35`. `report/domain/ReportLimits` 상수 클래스로 통합. 예상 30분. - [ ] **[P1]** STAR 모달 임계값(10, 20+10n)을 enum/상수로. `record/application/service/AiTaggingService.java:240-247`. 예상 30분. - [ ] **[P2]** `ScrumDailyQueryService:26`에 port/adapter 도입해 record 모듈 일관성 회복. `ScrumDailyQueryPort` 인터페이스 + 기존 JpaRepository 감싸는 adapter. 예상 1시간. - [ ] **[P2]** ReportService AI 실패 후 save 재시도/Outbox. `report/application/service/ReportService.java:130-132, 193-206`. MVP 단순화 시 최소 retryable 로그 + 수동 복구 스크립트 권장. 예상 반나절. --- ## B. 프로젝트 구조가 잘 조직되고 모듈화되어 있는가 ### 만족도: 부분충족 ### 근거 — 잘 된 점 - **패키지 트리가 CONTRIBUTING.md 선언(라인 161-258)과 일치**. 8개 도메인 (`auth, calendar, common, home, notification, record, report, user`) 모두 트리 구조가 문서대로. 평가자가 처음 봐도 "어디 가서 뭘 찾을지" 명확. - **헥사고날 모듈 두 곳의 port가 모두 진짜 `interface`**. `record/application/port/**`, `report/application/port/**` 전체 grep 결과 비-interface 위반 0건. 이름만 포트가 아닌 진짜 헥사고날. - **`report` 모듈이 `record` 직접 JPA 의존을 회피하려고 별도 어댑터로 격리**: `report/adapter/out/client/StarRecordClientAdapter.java:20-22`. 클래스 Javadoc에 "report 도메인이 record 도메인 내부에 직접 의존하지 않도록" 격리 의도 명시. `adapter/out/client` 하위 패키지를 별도 둔 분리도 모범적. - **역방향 의존을 use-case 인터페이스로 끊음**. `user/service/AccountHardDeleteDbCleaner.java:31-32, 50-51`이 `RecordAccountHardDeleteUseCase` / `ReportAccountHardDeleteUseCase` 인터페이스만 의존 — user → record/report 역방향 의존을 use-case로 해결한 정석 처리. - **알림 카피·FCM이 `common/notification` 하위에 명확히 격리** (`common/notification/copy/*`, `common/notification/fcm/*`). 노티 채널 추가(예: 이메일) 시 영향 격리됨. - **`PresignedUrlGeneratorPort` + S3/NoOp 이중 구현** (`common/storage/*.java`). 로컬 환경에서 S3 없이 부팅 가능. ### 근거 — 개선 필요 - **🚨 가장 큰 감점 — CONTRIBUTING.md ↔ 실제 코드 정면 충돌**. `CONTRIBUTING.md:247`이 *"`domain`은 어떤 외부 기술에도 의존하지 않는 순수 코드여야 합니다 (JPA, Spring 애너테이션 금지)"*라고 못박았으나, 실제로는 **9개 도메인 클래스 전부가 `import jakarta.persistence.*`**: - `record/domain/AiTaggingJob.java:6` - `record/domain/DailyCompetencyStat.java:7` - `record/domain/Project.java:3` - `record/domain/Scrum.java:6` - `record/domain/ScrumTitle.java:3` - `record/domain/StarImage.java:5` - `record/domain/StarRecord.java:6` - `record/domain/StarTag.java:3` - `report/domain/Report.java:6` - 평가자가 CONTRIBUTING.md 읽고 도메인 폴더 한 번 클릭하면 1초 안에 발견됨. - **`record`가 sink 도메인 (hub-and-spoke 의존)**. `record.domain.enums.CompetencyCategory`를 9개 위치가 import — `home/dto/RadarResult.java:5`, `home/dto/RadarResponse.java:5`, `calendar/dto/CalendarDailyPreviewResponse.java:6`, `calendar/dto/CalendarMonthlyResponse.java:6`, `calendar/repository/StarDailyRow.java:6`, `calendar/repository/ScrumStarTagRow.java:3`, `calendar/service/CalendarMonthlyView.java:7`, `calendar/service/CalendarHomeService.java:22`, `calendar/service/CalendarDailyPreviewView.java:6`. `CompetencyCategory`는 사실상 공용 enum인데 `record` 안에 묶여 있어 record 변경이 모든 도메인에 전파. - **`report.domain.Report`가 `user.entity.User`를 ManyToOne 직접 보유** (`report/domain/Report.java:16`). user 컬럼 변경이 report 도메인 빌드를 깨뜨림. `Long userId`만 보유했으면 결합도가 낮았을 것. - **`record.domain` 클래스가 user.entity.User에 ManyToOne 의존**: `Project.java:6`, `Scrum.java:10`, `ScrumTitle.java:7`, `StarRecord.java:11`, `DailyCompetencyStat.java:14` 동일 패턴. - **`home.service.HomeService`가 record의 out-port를 직접 호출** (`home/service/HomeService.java:14-18`). out-port는 헥사고날 모듈 자기 application 안에서만 부르도록 의도된 것 — 다른 모듈에서 out-port를 직접 부르는 건 의도와 어긋남. in-port나 별도 read-model 서비스로 우회해야 함. - **`common/notification` 위치 모호함** — 카피 풀 자체는 알림 도메인 자산이라 `notification/copy/`가 더 자연스러움. 학생 프로젝트라 큰 이슈 아님(P3). ### To-do - [ ] **[P0]** CONTRIBUTING.md 라인 246-247을 현실에 맞게 수정 (또는 도메인을 순수 클래스로 분리 — 학생 프로젝트 규모에선 비용 큼). 가장 가벼운 옵션: 라인 247을 *"`domain`은 JPA 엔티티로 두되, Spring/외부 라이브러리 의존은 금지한다. 외부 호출 분리는 port로 달성한다"*로 정정. 영향: `CONTRIBUTING.md:237-252`. 예상 15분. → 평가자 첫인상에 직접 타격이 가는 P0. - [ ] **[P1]** `CompetencyCategory` 공용 enum 추출. `record/domain/enums/CompetencyCategory.java` → `common/enums/CompetencyCategory.java`로 이동, 9개 import 경로 수정 (위 리스트). 예상 30분. - [ ] **[P2]** `report.domain.Report#user` 필드를 `Long userId`로 변환. `report/domain/Report.java:16` + Report에서 user 필드 사용처 정리. 예상 1시간. - [ ] **[P2]** `home.service.HomeService`의 `record.application.port.out.star.StarRecordRepositoryPort` 직접 호출 (`home/service/HomeService.java:15`)을 `record.application.port.in` 또는 별도 read-model 서비스로 우회. 예상 1시간. --- ## C. SOLID 원칙 및 적절한 디자인 패턴이 적용되었는가 ### 만족도: 충족 ### 근거 — 잘 된 점 - **DIP — record 모듈에 26개 port 인터페이스가 살아 있음**. query/write/cascade 등 의도별로 분리 (예: `StarRecordRepositoryPort`, `StarTagQueryPort`, `StarTagSavePort`, `ScrumWritePort`, `ScrumQueryPort` 별개로 분리 — `record/application/port/out/star/*`, `record/application/port/out/scrum/*`). 거대 Repository 1개가 아니라 **ISP를 의식한 세분화**. - **트랜잭션 분리 패턴이 의도적**: - `ReportService` ↔ `ReportTransactionalService` 분리로 "트랜잭션 커밋 후 외부 호출" 보장 (`report/application/service/ReportService.java:108-135`, `ReportTransactionalService.java:47-89`). - `AccountHardDeleteService` ↔ `AccountHardDeleteDbCleaner` 분리 + `REQUIRES_NEW` 격리 (`user/service/AccountHardDeleteService.java:68-73`, `AccountHardDeleteDbCleaner.java:48`). - 학생 프로젝트에서 self-invocation proxy 함정을 의식하고 별도 빈으로 분리한 사례는 드물다. - **Strategy 패턴 — OAuth provider 정규화**: `OAuthAttributes.from(registrationId, ...)`가 switch로 kakao/google/naver 분기 → 각각 정규화 (`auth/service/oauth/OAuthAttributes.java:33-37`). Provider 추가 시 한 함수만 수정. record/result 형태 immutable. - **Decorator 패턴 — env-aware OAuth resolver**: `OAuth2EnvAwareAuthorizationRequestResolver`가 `DefaultOAuth2AuthorizationRequestResolver`를 delegate로 감싸고 쿠키 발급 책임만 추가 (`auth/service/oauth/OAuth2EnvAwareAuthorizationRequestResolver.java:42-71`). Spring Security 표준 인터페이스 구현으로 갈아끼우기 쉽고, 책임이 정확히 "env cookie capture"로 한정됨. - **Facade 패턴 — `OAuthCallbackUrlResolver`**: SuccessHandler와 FailureHandler 두 곳에서 동일 콜백 URL 해석 로직 중복 제거 (`auth/service/oauth/OAuthCallbackUrlResolver.java:39-50`). - **Adapter 패턴 — Port와 1:1 어댑터** (`record/adapter/out/persistence/*Adapter.java`, `report/adapter/out/persistence/*Adapter.java`). 테스트에서 in-memory 대체 가능. - **LSP — `PresignedUrlGeneratorPort`의 NoOp vs S3 구현체 교환 가능** (`common/storage/S3PresignedUrlAdapter.java`, `NoOpPresignedUrlAdapter.java`). 환경 다운그레이드 시에도 동일 인터페이스로 동작. - **`@Async` 분리** — `AiTaggingAsyncExecutor`가 별도 빈으로 분리되어 Spring proxy 통과 보장 (`record/application/service/AiTaggingAsyncExecutor.java:23, 35`). self-invoke 함정 의식. ### 근거 — 개선 필요 - **🚨 SRP 위반 — `AiTaggingService.completeTagging` 한 메서드가 7가지 책임 수행** (`record/application/service/AiTaggingService.java:188-248`, 60줄): 1. StarRecord 조회·`record.tag()` 호출 2. primaryCategory enum 변환·예외 처리·로그 3. StarTag 일괄 저장 4. untagged 잔여 검사 5. ScrumTitle commit 6. User tagged count 집계 7. 코치마크/모달 플래그 세팅 (1, 10, 20+10n 분기) 6개 port를 동시에 의존. 평가자가 라인 188-248 한 번 훑으면 즉시 식별 가능. - **SRP 위반 — `NotificationScheduler.dispatch` 8단계 워크플로** (`notification/scheduler/NotificationScheduler.java:69-161`, 93줄): 시간 추출 → 슬롯 조회 → 작성자 제외 → 토큰 조회 → 카피 선택 → 발송 → invalid 비활성 → 인덱스 advance. - **트랜잭션 안에서 FCM 외부 호출** (`notification/scheduler/NotificationScheduler.java:70-71, 137`). 클래스 단위 `@Transactional`로 묶여 있는데 메서드 안에서 `fcmPushClient.send`를 호출 — DB 커넥션을 잡은 채 외부 FCM 응답을 기다림. 클래스 Javadoc(라인 41-42)이 "MVP 볼륨에서는 허용 가능"이라고 자체 명시 — **의식은 하고 있으나 코드는 문제 그대로**. 다른 서비스에서는 `AfterCommitExecutor`/`ReportTransactionalService` 분리로 잘 처리했는데 여기만 안 됨 → 컨벤션 일관성 흔들림. - **OCP — `OAuthAttributes.from`의 switch가 닫혀 있음** (`auth/service/oauth/OAuthAttributes.java:33-37`). provider 추가 시 enum 분기 + 새 정적 메서드 + switch case 동시 수정 필요. 진짜 Strategy로 가려면 `OAuthAttributeExtractor` 인터페이스 + provider별 빈 + `Map<String, Extractor>` 주입. **현 학생 프로젝트에서 provider 3개로 고정이라면 현 형태가 실용적** — "Strategy 적용"이라고 부르기엔 약함, 코드 자체는 OK. - **`@Transactional(readOnly)` 컨벤션 비일관** (A 항목과 동일 — `AiTaggingService` 정석 vs `ReportService` 메서드별 반복). ### To-do - [ ] **[P1]** `AiTaggingService.completeTagging` 분해. 책임별 3개 클래스로 추출 권장: - `StarTagPersister` (현재 200-222줄) - `ScrumTitleCommitter` (228-236줄) - `UserMilestoneTracker` (238-247줄) `record/application/service/AiTaggingService.java:188-248`. 예상 반나절. - [ ] **[P1]** `NotificationScheduler.dispatch`에서 FCM 호출 트랜잭션 분리. 1차 트랜잭션 → 토큰/카피 조회 → DTO 변환 → FCM은 트랜잭션 밖 → 2차 짧은 트랜잭션에서 advance + invalid 비활성. `notification/scheduler/NotificationScheduler.java:69-161`. 예상 반나절. - [ ] **[P2]** `ReportService` 클래스 단위 `@Transactional(readOnly = true)` 추가, 쓰기 메서드만 `@Transactional` override. `report/application/service/ReportService.java:44, 69, 147`. 예상 15분. - [ ] **[P3]** OAuth Strategy 정석화 (provider가 4종 이상 늘릴 계획 있을 때만). `auth/service/oauth/OAuthAttributes.java:33-37`. 예상 1시간. --- ## D. 코드는 깔끔하고 이해하기 쉽게, 간결하게 작성되었는가 ### 만족도: 우수 ### 근거 — 잘 된 점 - **단계별 번호 주석 + private 헬퍼로 105줄 메서드도 읽힘** — `ScrumSyncService.syncDailyScrum` (`record/application/service/ScrumSyncService.java:58-176`). - **Use Case 1개당 서비스 1개 일관 분할**. 대부분 30~70줄 규모: `UpdateStarRecordStepService`, `BulkCreateStarRecordService`, `HomeSummaryService`, `DeleteScrumService` 등. - **도메인 객체가 자기 상태 전환 메서드 보유 (anemic 회피)**. `Report.startRetry/complete/fail/isRetryAvailable` (`report/domain/Report.java:113-145`), `StarRecord.saveStep/complete/tag/isWriteLocked/isOwnedBy/isReadyForTagging` (`record/domain/StarRecord.java:84-126`). - **매직 넘버 거의 모두 상수화**: `MAX_SCRUMS_PER_DATE=5` (`ScrumBulkWriteService.java:40`), `EDIT_WINDOW_DAYS=14, MAX_ITEMS_PER_DATE=5` (`ScrumSyncService.java:42-43`), `REFRESH_COOKIE_NAME/COOKIE_PATH/SAME_SITE_STRICT` (`auth/service/TokenDeliveryService.java:27-29`). - **WHY 위주의 Javadoc**. 예시: `AuthService.java:16-27` ("Lua 스크립트로 원자 실행, 동시 요청 한 건만 성공"), `UserService.deleteMyAccount` (`user/service/UserService.java:94-111`)에 "refresh token 무효화는 멱등 케이스에도 호출 — 엣지 복구 효과" 명시. - **DTO 변환 정적 팩토리 일관**: 모든 컨트롤러가 `Response.from(useCase.x(request.toCommand(userId)))` 패턴 (`report/adapter/in/web/ReportController.java:63, 78, 103, 122, 148, 174, 200` 전부 동일). - **`ApiResponse` 정적 팩토리 4가지 오버로드로 호출부 단순화** (`common/response/ApiResponse.java:45-67`). - **컨트롤러 본문이 2~6줄**. 분기 없음, 책임 정확히 분리. - **컨트롤러 LocalDate 파싱이 `DateParam.parseIso` 한 곳에 응집** (`record/adapter/in/web/DateParam.java:9-24`) — Spring 기본 변환의 500 폴백 문제 회피 이유까지 주석. - **테스트 컨벤션 표준화**: `@Nested + DisplayName 한글`, `// given/when/then`, `should_X_when_Y`, AssertJ `assertThatThrownBy + extracting("errorCode")` 일관 (`ReportServiceTest.java:63-318`, `UserServiceTest.java:51-298`). - **테스트 픽스처 헬퍼 분리**: `user(id)`, `report(...)`, `starRecords(count)`, `ids(count)` (`ReportServiceTest.java:324-373`) — given 블록이 짧고 의도 위주. - **`StarImageCascadeCleaner` — DB 삭제 + 커밋 후 S3 삭제를 한 클래스에 응집**해 호출부(`ScrumSyncService`, `DeleteScrumService`)가 동일 불변식을 자동 준수 (`StarImageCascadeCleaner.java:18-62`). - **ErrorCode가 도메인별 섹션 주석으로 구분되고 `{DOMAIN}_{NNN}` 규칙 일관 적용** (`common/exception/ErrorCode.java:22-94`). ### 근거 — 개선 필요 - **🚨 `User.java` Javadoc 정렬 버그** — `streakSnapshotAsOf` 메서드(257행)용 Javadoc(216-232줄, `@param kstToday`까지 작성됨)이 그 직후 메서드인 `markPendingCoachMark`(234행)에 잘못 붙어 있다. 라인 233은 또 다른 한 줄 Javadoc(`/** AI 태깅으로 1번째 STAR가 확정될 때 호출 ... */`)이 시작되어 같은 메서드에 Javadoc이 2개 붙은 형태. 정작 `streakSnapshotAsOf`는 257행에 Javadoc 없이 떨어져 있음. IntelliJ가 "잘못된 @param" 경고를 표시. (`user/entity/User.java:215-257`) - **🚨 `DateTimeFormatters` placeholder 클래스** — 38줄 중 30줄이 Javadoc(클래스 포지셔닝, 사용 위치, 타임존 변환 책임 등)이고 실제 정의는 `ZONE_KST` 상수 1개. Javadoc 마지막에 "현재 상태: 응답 DTO 작업 전이므로 실제 포맷터 상수/메서드는 비어 있다. ... 추가한다 (예: `KST_DATE_DOT`, `KST_DATE_TIME_DOT`, `toKstDate(OffsetDateTime)` 등)" 라고 자기참조 TODO. (`common/util/DateTimeFormatters.java:1-38`) → 평가자가 "코드 깎아내기 사례"로 핀포인트 가능. - **`ReportService.createReport` / `retryReport`의 try/catch가 중복** (`report/application/service/ReportService.java:119-132` vs `198-206`). 동일 `try { aiResult = ...; report.complete(...); } catch (Exception e) { log.error(...); report.fail(); } saveReportPort.save(report);` 패턴 2회 반복. private 헬퍼로 추출 가능. - **`MINI_LIMIT=10` / `CAREER_LIMIT=20` 중복 정의** (`ReportService.java:50-51` + `ReportTransactionalService.java:34-35`). 양쪽 다 실사용 중(73줄과 117/120줄)이지만 변경 시 일관성 깨질 위험. - **Swagger `@ApiResponse` 보일러플레이트** — 매 엔드포인트마다 7~10줄 풀패키지(`@io.swagger.v3.oas.annotations.responses.ApiResponse(...)`)가 반복돼 본문(2줄)보다 어노테이션(40줄)이 훨씬 김. `report/adapter/in/web/ReportController.java:52-202` 7개 엔드포인트 전부 동일 패턴. `@CommonAuthErrors` 메타 어노테이션으로 묶을 만함. - **`ScrumBulkWriteService` 일련번호 주석 깨짐** — `// 2. 총 스크럼 수 ≤ 5` (라인 65) 직후 `// 2. projectId 소유권 검증` (라인 70). 이후 번호가 +1씩 밀려 있음. - **`Scrum` 도메인 행위가 빈약** — `updateCompetency` 1개뿐. `hasStar` 토글이 외부(`scrumWritePort.completeStar`)에서만 가능. 다른 도메인(`Report`, `StarRecord`)에 비해 비대칭. (`record/domain/Scrum.java:60-74`) ### 영역별 세부 평가 #### 1) 네이밍 - 잘된 점: Hexagonal 명명 규칙(`*Port`, `*Adapter`, `*UseCase`, `*Command`, `*View`, `*Result`)이 흔들리지 않음. Layered는 `*Service`/`*Repository`/`*Controller`. Service 클래스명이 동작 자체(`UpdateStarRecordStepService`, `BulkCreateStarRecordService`, `ScrumSyncService`). - 개선점: `StarRecord*` / `Star*` 혼용. ErrorCode prefix도 `STAR_NOT_FOUND` vs `STAR_RECORD_NOT_FOUND`, `RECORD_006` vs `REC_001` 불일치 (`common/exception/ErrorCode.java:56, 65`). `ReportTransactionalService`는 기술 키워드가 이름에 박혀 도메인 의미 약함 — `ReportPersistenceService` 같은 이름이 더 깔끔. #### 2) 메서드 길이/복잡도 - 잘된 점: 컨트롤러 메서드 모두 2~6줄. 대부분 서비스 메서드 30줄 이내. 중첩 깊이 ≤2단. 매개변수 5개 초과 메서드 없음. - 개선점: `ScrumBulkWriteService.bulkWrite` 86줄에 6단계 응집(`record/application/service/ScrumBulkWriteService.java:51-136`). 단계별 헬퍼로 추출하면 가독성 향상. #### 3) 매직 넘버/문자열 - 잘된 점: 위 본문 참고. 쿠키 속성까지 상수화. - 개선점: `Report.retryCount < 1` (`Report.java:144`)의 raw `1` → `MAX_RETRY_COUNT` 상수 권장. `Short.MAX_VALUE + 1` cap (`User.java:169-171`)은 short 컬럼 제약이라 자명하나 도메인 외 보호 목적이면 상수 추출 고려. #### 4) 주석 적정성 - 잘된 점: 클래스/메서드 Javadoc이 거의 모두 WHY 설명. 도메인 필드별 짧은 Javadoc로 nullable 정책까지 명시 (`User.java:73-80`). - 개선점: **위 P0 두 건** (`User.java` Javadoc 정렬, `DateTimeFormatters` placeholder). `ScrumBulkWriteService`의 `// 2.` 두 번 등장도 동일 범주. #### 5) 중복 코드 - 잘된 점: `StarImageCascadeCleaner`가 "DB delete + afterCommit S3 delete" 보일러플레이트 흡수. enum 라벨 변환 중복도 `parseJobRole`/`parseUserStatus`로 헬퍼화 (`user/service/UserService.java:122-136`). - 개선점: 위 본문의 `ReportService` AI try/catch 중복, `MINI/CAREER_LIMIT` 중복, Swagger `@ApiResponse` 보일러플레이트 3건. #### 6) 가독성 - 잘된 점: 조기 return 적극 활용 (`AuthService.reissue` (`auth/service/AuthService.java:38-55`), `Scrum.create`/`StarRecord.create` 팩토리의 `requireNonNull` 라인업). Stream 4단 chain도 의도가 한 호흡으로 읽힘 (`BulkCreateStarRecordService.java:64-69`). `User.streakSnapshotAsOf`의 분기 4종(NULL/≤1/==2/≥3)이 Javadoc과 1:1 매칭. - 개선점: `Boolean retryAvailable = report.isRetryAvailable() ? true : null;` (`ReportService.java:156`) — JSON null 직렬화 제외 의도가 코드만 봐서는 즉시 이해 어려움. 한 줄 주석 권장. `ScrumSyncService` 단계 5a/5b 라벨이 좋지만 `// 4.`와 `// 5.` 사이에 변수 선언이 끼어 흐름이 끊김 (`ScrumSyncService.java:103-126`). #### 7) Lombok / record / Stream 활용 - 잘된 점: 도메인 엔티티 `@Getter + @NoArgsConstructor` 최소 조합 — `@Setter`/`@Data` 남용 없음. Service `@RequiredArgsConstructor` 통일. `record`를 결과 DTO/Command/View에 적극 활용 (`CreateReportResult`, `UpdateOp` 등). `@AllArgsConstructor(access = PRIVATE)` + 정적 팩토리 → `ApiResponse` 불변. - 개선점: `User`는 `@NoArgsConstructor(access = PROTECTED)`인데 `Scrum`, `StarRecord`, `Report`는 그냥 `@NoArgsConstructor`. JPA 권장은 PROTECTED. 테스트에서 `ReflectionTestUtils`로만 호출하므로 PROTECTED로 좁혀도 무방 (`record/domain/Scrum.java:22`, `StarRecord.java:23`, `report/domain/Report.java:27`). #### 8) 컨트롤러/서비스 입출력 명확성 - 잘된 점: 모든 컨트롤러가 `Request.toCommand(userId)` → `useCase.x(command)` → `Response.from(result)` → `ApiResponse.ok(...)` 4단 흐름. `@Validated @RequestBody` / `@Valid @RequestBody` 적용. `@CurrentUser` 커스텀 어노테이션으로 userId 주입 통일. - 개선점: 같은 컨트롤러 안에서 메시지 유무 정책 흔들림 — `ApiResponse.ok(data)` vs `ApiResponse.ok("...", data)` 혼재 (`ReportController.java:63, 119, 146`). #### 9) 테스트 코드 가독성 - 잘된 점: BDD 컨벤션 완벽 적용. 픽스처 헬퍼 분리. `ReflectionTestUtils.setField`로 JPA ID 주입 표준적. `lenient().when(clock.instant())` 등 stubbing 전략까지 주석 (`UserServiceTest.java:238-243`). - 개선점: 테스트 픽스처에서 `java.util.stream.IntStream`, `LongStream`, `java.lang.reflect.Constructor` 등 FQCN 사용 — import로 끌어올리면 한결 깔끔 (`ReportServiceTest.java:325-326, 348, 371`). #### 10) 데드코드/사용 안 함 - 발견: `DateTimeFormatters` 클래스 본체가 placeholder (위 P0). - ~~`ReportService`의 `MINI_LIMIT`/`CAREER_LIMIT`이 dead~~ — **정정**: 73줄에서 실제 사용 중. `ReportTransactionalService`와 중복 정의가 문제일 뿐 dead 아님. - 그 외 표본 범위에선 unused method/field 발견 없음. ### To-do - [ ] **[P0]** `User.java` Javadoc 블록 정렬 수정. 216-232줄의 Javadoc 블록을 257줄(`streakSnapshotAsOf` 위)로 이동, 234줄의 `markPendingCoachMark` 위에는 한 줄 Javadoc만 남기기. `user/entity/User.java:215-257`. 예상 10분. - [ ] **[P0]** `DateTimeFormatters` 정리 또는 삭제. 두 옵션: (a) 실제로 필요한 포맷터(`KST_DATE_DOT`, `toKstDate(OffsetDateTime)`) 함께 추가, (b) `ZONE_KST` 상수만 남기고 클래스 Javadoc 5줄 이내로 축약. `common/util/DateTimeFormatters.java`. 예상 15분. - [ ] **[P1]** `ReportService` AI 호출 try/catch 중복 제거. `executeAiAndUpdate(Long reportId, Report report, List<StarRecord> starRecords, List<Scrum> scrums, String contextLabel)` private 헬퍼 추출. `report/application/service/ReportService.java:119-132, 198-206`. 예상 20분. - [ ] **[P1]** `MINI_LIMIT`/`CAREER_LIMIT` 중복 제거 — `ReportLimits` 상수 클래스 신설. `ReportService.java:50-51`, `ReportTransactionalService.java:34-35`. 예상 10분. - [ ] **[P2]** Swagger `@ApiResponse` 보일러플레이트 축약 — `@CommonAuthErrors` 메타 어노테이션 추출. 영향: 6개 컨트롤러 (`ReportController`, `UserController`, `HomeController`, `AuthController`, `ScrumController`, `StarRecordController`). 예상 1시간. - [ ] **[P3]** `ScrumBulkWriteService` 일련번호 주석 정렬 — `// 2.` 두 번 등장, 이후 +1씩 보정. `record/application/service/ScrumBulkWriteService.java:65-122`. 예상 5분. - [ ] **[P3]** `Scrum` 도메인 행위 보강 — `markStarCompleted()` / `unmarkStar()` 같은 hasStar 플래그 토글 메서드 추가. `record/domain/Scrum.java`, `UpdateStarRecordStepService.java:54-56`. 예상 30분. - [ ] **[P3]** 도메인 엔티티 `@NoArgsConstructor(access = PROTECTED)` 통일. `Scrum.java:22`, `StarRecord.java:23`, `Report.java:27`. 예상 10분. - [ ] **[P3]** `ErrorCode` prefix 통일 (`RECORD_*` vs `REC_*`, `STAR_*` vs `STAR_RECORD_*`). `common/exception/ErrorCode.java:50-73`. 예상 15분. - [ ] **[P3]** 테스트 코드 FQCN 정리 — `IntStream`, `LongStream`, `Map.of` 등 import 추가. `src/test/java/com/groute/groute_server/report/application/service/ReportServiceTest.java:325-373`. 예상 10분. --- ## 마무리 - 본 항목 25% 중 D(우수) + C(충족) + A(충족)은 학생 프로젝트 평균을 명확히 상회. 단 B(부분충족)이 종합 점수의 발목. - B의 핵심은 **CONTRIBUTING.md ↔ 코드 불일치 1건과 record hub-and-spoke 1건**. 둘 다 단일 PR로 해소 가능. - D의 P0 두 건 (`User.java` Javadoc 정렬, `DateTimeFormatters` placeholder)은 평가자가 코드 폴더 둘러볼 때 "흠집"으로 잡힐 가능성이 높음 — 합쳐서 25분 분량의 작업이라 P0로 분류. - C의 SRP 위반 두 곳(`AiTaggingService.completeTagging`, `NotificationScheduler.dispatch`)은 P1로 두되, 반나절씩 잡으면 한 스프린트 안에 해결 가능.