Skip to content

221 Refactor Post 및 하위 도메인#242

Merged
gisu1102 merged 67 commits intodevelopfrom
refactor/post-feat
Oct 4, 2025
Merged

221 Refactor Post 및 하위 도메인#242
gisu1102 merged 67 commits intodevelopfrom
refactor/post-feat

Conversation

@gisu1102
Copy link
Contributor

@gisu1102 gisu1102 commented Aug 22, 2025

#️⃣ PR 개요

post 도메인 및 연관 서비스(poll, comment, reply, 'best', 'hit') 전반을 구조적 리팩토링 및 모듈화했습니다.
목표는 응집도 강화, 결합도 완화, 복잡도 관리이며, 이를 통해 유지보수성과 품질 지표를 개선했습니다.


📝 주요 변경 사항 (이전 PR 이후)

1. CQRS 패턴 적용 - 단일 책임 원칙 준수

  • 문제: 서비스 내 책임 과다로 인해 유지보수성 저하 (PostService Cognitive Complexity = 41)
  • 해결: CQRS 분리로 복잡도 개선 (Post Service 평균 10)
  • Command/Query 책임 분리: PostService에서 비즈니스 로직과 DTO 변환 로직 분리
  • PostQueryService: 데이터 조회, 페이징, 비즈니스 검증에만 집중
  • PostDtoAssembler: DTO 변환, 사용자 정보 해석, 상호작용 데이터 조합에만 집중
  • 단일 책임 원칙: 각 컴포넌트가 하나의 명확한 책임만 담당하도록 구조 개선

2. 예외 처리 구조 개선

  • ErrorCode 기반 예외 체계: 도메인별 에러코드 분리 (Post ↔ Poll ↔ Scheduler)
  • PollException: Poll 도메인 전용 예외 클래스 신규 생성
  • 일관된 예외 처리 패턴 적용

3. 서비스 간 의존성 해결

  • Poll CQRS 구조: Post ↔ Poll 간 순환 의존성 해결
  • UserInfo와 UserDTO 통합: post,comment,reply 내 사용자 상호작용 정보와 프로필 정보의 일관된 표현
  • 도메인 경계 명확화 및 결합도 완화

4. 테스트 커버리지 대폭 향상

  • PostDtoAssemblerTest: 신규 컴포넌트 단위 테스트 추가
  • 기존 테스트 리팩토링: 새로운 아키텍처에 맞게 업데이트
  • 다양한 시나리오 커버 (익명 게시물, Poll 게시물, 삭제된 사용자 등)

5. 데이터 정합성 개선

  • 원자적 증감 연산: 투표 증감시 동시성 문제 해결을 위한 안전한 카운터 증감 처리
  • 데이터 무결성 보장 및 성능 최적화

📊 리팩토링 전후 지표 비교 (SonarQube)

지표 리팩토링 전 리팩토링 후 설명
Cyclomatic Complexity 182 232 추가기능(develop), 메서드의 분기 수. 방어/검증 로직 추가로 증가했으나 허용 가능 수준.
Cognitive Complexity 76 59 사람이 이해하기 어려운 복잡도. 분리·단순화로 개선됨.
Lines of Code 2000 2543 코드 줄 수. 기능 추가, 분리와 방어 로직 추가로 증가.
Coverage (%) 1.4 84 테스트 커버리지. 대폭 향상.
Line Coverage (%) 1.6 85 실행 줄 단위 커버리지.
Condition Coverage (%) 0 79 분기 조건 커버리지. 테스트 품질 개선 근거.
Duplication (%) 2.8 0 코드 중복률. 완전 제거됨.
Code Smells 50 (Debt 1d, 0.9%) 29 (Debt 1h17m, 0.1%) 유지보수 비용 지표. -22h 43m 감소.

💡 요약

  • 구조적 리팩토링을 통해 응집도와 모듈화를 강화하고 결합도를 완화
  • 인지 복잡도(Cognitive Complexity)와 중복률은 명확히 개선
  • 테스트 커버리지는 1%대 → 80%대 이상으로 증가
  • Code Smells와 Technical Debt도 크게 줄어듦
  • Cyclomatic Complexity는 증가했지만, 이는 의도적인 방어·검증 로직 추가 및 기능추가로 인한 것으로 관리 가능한 수준
  • Cognitive Complexity 감소로 실제 가독성은 개선됨

💬 참고사항

추후 개선 필요사항

  • PostScheduler 외부 프로세스(Python script) 에러 대응 정책 수립 필요성 판단 → 추후 논의

기술적 세부사항

  • 이전 PR 내용 반영 완료: 기존 피드백 모두 적용
  • SonarQube 분석: 로컬 환경에서 실행
    • 비교 기준: develop 브랜치 (post 도메인 한정)
    • 객관적 품질 지표 기반 개선 효과 검증

호환성

  • 기존 API 호환성 유지: 외부 서비스 인터페이스 변경 최소화
    • 외부 서비스(user, report 등)에서 호출하는 PostDto 반환 로직은 내부적으로 PostDtoAssembler로 교체했지만, 외부에 노출되는 데이터 스펙은 동일
    • Controller 계층의 엔드포인트 및 요청/응답 DTO 구조 변경 없음

Summary by CodeRabbit

  • 신기능
    • 게시글 목록/상세에 투표 정보가 함께 제공됩니다.
    • 댓글·대댓글 조회 시 좋아요 상태 등 사용자 상호작용 정보가 포함됩니다.
    • 인기글(Top3/베스트) 조회 응답이 정비되었습니다.
  • 변경/리팩터
    • 읽기/쓰기 기능을 분리하여 성능과 안정성을 개선했습니다.
    • 생성/수정/삭제 요청의 응답 페이로드를 Void로 통일했습니다.
    • 익명 표기 및 사용자 프로필 노출 로직을 일관되게 정리했습니다.
    • 이미지 삭제 시 검증과 오류 처리를 강화했습니다.
  • 스케줄러
    • 공지 수집 작업의 예외 처리와 로그를 개선했습니다.
  • 테스트
    • 게시글/댓글/투표/조회수 영역에 단위 테스트를 대폭 추가했습니다.

gisu1102 added 30 commits July 8, 2025 23:49
- PostDetailResponseDTO, PollInfoResponseDTO를 포함하는 PostPageItemResponseDTO 래퍼 도입
- 목록/상세/베스트 등 모든 게시글 응답을 래퍼 DTO(List<PostPageItemResponseDTO> 등)로 일원화
- 중복 DTO 변환 로직 toPageItemDTO로 통합, Object 반환/instanceof 분기 제거
- 이전 postDetailResponse 변경 전파 사항
- PostService의 이미지, 조회수, 베스트, 투표 등 부가 기능/서브 로직을 SeperatedPostService로 분리
- PostService는 핵심 비즈니스 흐름/응답 조립에만 집중하도록 응집도(cohesion) 강화
- 부가 기능은 별도 서비스로 위임하여 결합도(coupling) 낮춤
- 테스트 코드도 DI 구조에 맞게 리팩토링
- post, comment, reply 의 user 정보 mapping
- 댓글/대댓글 추가/삭제 시 댓글 수 및 BestScore 반영 순서 정비
- 조회 로직 내 사용자 정보 매핑, 응답 DTO 생성 등 책임 분리
- 중복 제거 및 성능 고려  유저 정보 조회 방식 적용
- 상위 도메인 Repository 직접 참조 제거 (Comment → Post 등)
- 상위 도메인 쿼리 서비스(PostQueryService 등)에서 유효성 검사 처리
- 의존성 순환 문제 발생 → Query/Command 분리로 해결
- Post 도메인 내 Scheduler 및 ErrorCode 의존성 정리
- 도메인 계층별로 올바른 ErrorCode 및 예외 처리 방식 적용
- 각 서비스내 redisBestService 호출 Best Domain으로 책임 이동
@gisu1102 gisu1102 added the Status: Pending 보류 중인 이슈 label Aug 22, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 22, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

이 PR은 Post/Comment/Reply/Poll 전반에 CQRS(명령/조회) 구조를 도입하고, 관련 컨트롤러·서비스·DTO·엔티티·예외 코드를 대대적으로 재구성했습니다. 레거시 통합 서비스와 예외들을 제거/대체하고, 페이지·상세 응답 DTO를 재설계했으며, 스케줄러 실행 로직을 공통화했습니다. 단위 테스트가 광범위하게 추가되었습니다.

Changes

Cohort / File(s) Summary
Controller CQRS 전환
.../post/controller/PostController.java, .../comment/controller/CommentController.java, .../comment/reply/controller/ReplyCommentController.java, .../poll/controller/PollController.java, .../board/notice/controller/NoticeController.java
단일 서비스 의존성을 Command/Query 서비스로 분리. 반환 타입을 Void/구체 DTO로 정리. 패키지/의존성 경로 갱신.
Post 서비스 분리/추가
.../post/service/PostCommandService.java, .../post/service/PostQueryService.java, .../post/service/PostDtoAssembler.java, .../post/service/PostInteractionService.java, .../post/service/PostService.java(삭제)
게시글 생성/수정/삭제(명령), 조회/페이지/베스트(조회), DTO 조립기, 이미지/조회수 상호작용 서비스 추가. 기존 PostService 제거.
Comment/Reply CQRS
.../comment/service/CommentCommandService.java, .../comment/service/CommentQueryService.java, .../comment/reply/service/ReplyCommandService.java, .../comment/reply/service/ReplyQueryService.java, .../comment/service/CommentService.java(삭제), .../comment/domain/reply/service/ReplyCommentService.java(삭제)
댓글/대댓글 생성·수정·삭제(명령) 및 조회(쿼리) 서비스 신설. 레거시 서비스 삭제 및 의존 경로 정리.
Poll 서비스 재구성
.../poll/service/PollCommandService.java, .../poll/service/PollQueryService.java, .../poll/service/PollService.java(삭제), .../poll/repository/PollRepository.java
투표 생성/투표/취소(명령) 및 조회(쿼리) 서비스 추가. 옵션별 원자적 증감 메서드 도입. 레거시 PollService 삭제.
Best 서비스 추가
.../post/domain/best/BestService.java
Redis 베스트와 영속 엔티티 연결 서비스 추가: Top3 ID, 페이지 조회, 점수 적용/삭제 제공.
DTO 재설계(페이지/상세/사용자)
.../post/dto/response/PostPageResponse.java, .../post/dto/response/PostDetailResponseDTO.java, .../post/dto/response/PostPageItemResponseDTO.java, .../post/dto/response/PostPollDetailResponseDTO.java, .../post/dto/response/UserDto.java(삭제), .../post/dto/UserDto.java(신규), .../post/dto/UserInfo.java(신규), .../post/dto/response/PollInfoResponseDTO.java
상세/페이지 DTO를 빌더/합성 구조로 개편. 페이지 아이템에 Poll 정보를 결합. 응답용 UserDto 제거, 도메인 공용 UserDto/UserInfo 신설.
Comment/Reply DTO/엔티티 변경
.../comment/dto/response/CommentResponseDTO.java, .../comment/entity/CommentEntity.java, .../comment/reply/entity/ReplyCommentEntity.java
사용자 표시 데이터 공급을 UserDto 기반으로 변경. 내부 UserInfo 제거→외부 타입 사용. 생성자에서 id 제거, 정적 팩토리 추가. 패키지 경로 수정.
예외·에러 코드 도입/정리
.../post/exception/PostErrorCode.java, .../post/exception/PostException.java, .../post/schedular/exception/SchedulerErrorCode.java, .../post/schedular/exception/SchedulerException.java, .../post/domain/comment/exception/*, .../post/domain/reply/exception/*, .../post/domain/hits/exception/*, .../post/domain/poll/exception/*, 제거: SchedulerException.java, StateUpdateException.java, Poll*Exception.java
각 도메인별 GlobalErrorCode 기반 에러코드/예외 신설. 기존 RuntimeException 파생 예외 제거.
리포트 연계 업데이트
.../report/service/ReportService.java, .../report/dto/response/ReportListResponseDto.java, .../report/dto/response/ReportedPostDetailResponseDTO.java, .../report/controller/ReportController.java
Report 서비스가 Query 서비스들에 의존하도록 변경. DTO 합성 구조 반영. 주입 의존성 정리.
리포지토리/주석 보강
.../comment/repository/CommentRepository.java, .../comment/reply/repository/ReplyCommentRepository.java
@repository 추가 및 패키지 경로 정리.
하위 시스템 경로/임포트 정리
.../like/service/LikeService.java, .../notification/service/NotificationService.java, .../comment/reply/dto/*, .../comment/reply/service/ReplyEventListener.java
패키지/임포트 경로를 domain.comment.reply.* 등으로 정규화.
엔티티 생성/행위 변경
.../post/entity/PostEntity.java, .../post/entity/PostAnonymous.java, .../poll/entity/PollEntity.java, .../poll/entity/PollVoteEntity.java
생성자에서 id 제거 및 정적 팩토리 도입, 방어적 복사, 카운트 처리/익명번호 API 조정, Poll 옵션 카운트 초기화/원자적 증감 전제로 단순화.
스케줄러 리팩터링
.../post/schedular/PostsScheduler.java
Python 실행 공통 메서드 도입, 에러 코드 사용, 로그 메시지/예외 처리 통일.
테스트 대거 추가
src/test/java/...
Post/Comment/Reply/Poll/Best/Hits/스크립트 스케줄러 등 유닛 테스트 다수 추가 및 경로 정리.
기타
src/main/resources 서브모듈 포인터, 이메일 테스트 임포트 경로 정리
리소스 서브모듈 커밋 갱신, 테스트 DTO 패키지 이동 반영.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant PC as PostController
  participant PCS as PostCommandService
  participant PQS as PostQueryService
  participant PDA as PostDtoAssembler
  participant PI as PostInteractionService
  participant PR as PostRepository

  rect rgba(200,230,255,0.3)
  note over U,PC: 게시글 생성 (Command)
  U->>PC: POST /posts (PostCreateRequestDTO, images)
  PC->>PCS: createPost(dto, images)
  PCS->>PI: handleImageUpload(images)
  PCS->>PR: save(PostEntity.create(...))
  PCS-->>PC: 완료
  PC-->>U: 201 SingleResponse<Void>
  end

  rect rgba(220,255,220,0.3)
  note over U,PC: 게시글 상세 조회 (Query)
  U->>PC: GET /posts/{id}
  PC->>PQS: getPostWithDetail(id)
  PQS->>PR: findById(id, not deleted)
  PQS->>PI: increaseHits(post, currentUser)
  PQS->>PDA: toPageItem(post, currentUser)
  PDA-->>PQS: PostPageItemResponseDTO
  PQS-->>PC: PostPageItemResponseDTO
  PC-->>U: 200 SingleResponse<PostPageItemResponseDTO>
  end
Loading
sequenceDiagram
  autonumber
  actor U as User
  participant CC as CommentController
  participant CCS as CommentCommandService
  participant CQS as CommentQueryService
  participant RCS as ReplyCommandService
  participant RQS as ReplyQueryService

  rect rgba(255,240,200,0.3)
  note over U,CC: 댓글 작성 (Command)
  U->>CC: POST /posts/{id}/comments
  CC->>CCS: addComment(postId, dto)
  CCS-->>CC: 완료
  CC-->>U: 201 SingleResponse<Void>
  end

  rect rgba(255,220,240,0.3)
  note over U,CC: 댓글 조회 (Query)
  U->>CC: GET /posts/{id}/comments
  CC->>CQS: getCommentsByPostId(id)
  CQS->>RQS: getRepliesByCommentId(...)
  CQS-->>CC: ListResponse<CommentResponseDTO>
  CC-->>U: 200
  end

  rect rgba(240,240,255,0.3)
  note over U: 대댓글 작성 (Command)
  U->>RCS: POST /comments/{id}/replies
  RCS-->>U: 201 SingleResponse<Void>
  end
Loading
sequenceDiagram
  autonumber
  actor U as User
  participant PollC as PollController
  participant PollCmd as PollCommandService
  participant PollQry as PollQueryService
  participant PR as PollRepository
  participant PVR as PollVoteRepository

  rect rgba(220,250,255,0.3)
  U->>PollC: POST /polls
  PollC->>PollCmd: createPoll(dto)
  PollCmd->>PR: save(PollEntity.from(...))
  PollCmd-->>PollC: 완료
  PollC-->>U: 201 SingleResponse<Void>
  end

  rect rgba(220,255,230,0.3)
  U->>PollC: POST /posts/{id}/poll/vote
  PollC->>PollCmd: votingPoll(postId, dto)
  PollCmd->>PVR: exists / save vote
  PollCmd->>PR: incOption(...)
  PollCmd-->>PollC: 완료
  PollC-->>U: 201 SingleResponse<Void>
  end

  rect rgba(255,235,235,0.3)
  U->>PollC: DELETE /posts/{id}/poll/vote
  PollC->>PollCmd: deleteVoting(postId)
  PollCmd->>PVR: findByPollIdAndUserId / delete
  PollCmd->>PR: dcrOptionIfPositive(...)
  PollCmd-->>PollC: 완료
  PollC-->>U: 200 SingleResponse<Void>
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

Priority : High, Status: Reviewing

Suggested reviewers

  • BHC-Chicken

Poem

귀가 쫑긋, 코드밭을 뛰어넘는 토끼
명령은 왼손, 조회는 오른손에 쥐고
딱! 쪼개진 흐름 위로 팝콘 같은 테스트가 피어오르네
폴링은 톡, 댓글은 콩, 별점은 반짝⭐
오늘도 야무지게, 캐롯-리퀘스트 합격! 🥕

✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/post-feat

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@doma17
Copy link
Contributor

doma17 commented Aug 23, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 23, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@doma17 doma17 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다. CQRS 패턴에 대해 새로 알게되었네요.

@coderabbitai coderabbitai bot added Priority : High 우선순위 상 Status: Reviewing 처리하고 리뷰 중인 이슈 labels Aug 23, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 38

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (18)
src/main/java/inu/codin/codin/domain/post/domain/comment/reply/service/ReplyEventListener.java (2)

16-20: 트랜잭션 커밋 전 알림 발송 위험 — AFTER_COMMIT로 전환 권장

현재 @EventListener는 트랜잭션 경계와 무관하게 즉시 동작할 수 있어, 롤백 시에도 알림이 나갈 수 있습니다. @TransactionalEventListener(phase = AFTER_COMMIT)로 전환하면 커밋 확정 후에만 발송됩니다. 외부 호출(알림)은 보통 사후-커밋이 안전합니다.

-import org.springframework.context.event.EventListener;
+import org.springframework.transaction.event.TransactionPhase;
+import org.springframework.transaction.event.TransactionalEventListener;
...
-    @Async
-    @EventListener
+    @Async
+    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
     public void handleReplyNotificationEvent(ReplyNotificationEvent event){
         notificationService.sendNotificationMessageByReply(event.getPostCategory(), event.getUserId(), event.getPostId(), event.getContent());
     }

참고: fallbackExecution = true는 트랜잭션 밖에서 발행된 이벤트도 처리합니다. 전역 정책에 따라 조정하세요.


19-19: NotificationService.sendNotificationMessageByReply 시그니처 확인 및 방어 로직 부재

시그니처 정합성
확인 결과, ReplyNotificationEvent의 필드 타입(PostCategory, ObjectId, String, String)과 NotificationService.sendNotificationMessageByReply(PostCategory, ObjectId, String, String)의 파라미터가 정확히 일치합니다.

방어 로직 부재
sendNotificationMessageByReply 내부에서는 사용자 존재 여부만 확인하며, content에 대해 길이 제한, 금칙어 필터링, HTML 이스케이핑 등의 방어 로직이 전혀 적용되어 있지 않습니다. UI를 통해 전달되는 텍스트를 별도 검증 없이 전파할 경우, 메시지 오버플로우나 XSS·불법 문자 노출 등의 리스크가 발생할 수 있습니다.

권장 조치
NotificationService.sendNotificationMessageByReply 메서드에 아래 방어 로직 추가
1. 최대 길이 검증 (예: 200자)
2. 금칙어 목록에 따른 필터링/치환
3. HTML/XML 이스케이핑 또는 Markdown 정규화
ReplyNotificationEvent 생성 시점에 VO(Value Object) 레벨에서 선(先)검증 적용 고려
• 방어 로직 커버리지를 위한 단위 테스트 및 경계값 테스트 추가

점검 위치
• src/main/java/inu/codin/codin/domain/notification/service/NotificationService.java
public void sendNotificationMessageByReply(...)
• src/main/java/inu/codin/codin/domain/post/domain/comment/reply/dto/event/ReplyNotificationEvent.java

src/test/java/inu/codin/codin/domain/email/service/PasswordResetEmailServiceTest.java (1)

21-24: Mockito eq 정적 임포트 누락으로 컴파일 에러 발생

본 파일에서 eq(...)를 사용하지만 import static org.mockito.ArgumentMatchers.eq; 정적 임포트가 없습니다. 현재 정적 임포트는 any, anyString만 포함되어 있어 컴파일 시 심볼을 찾지 못합니다.

다음 수정으로 해결하세요:

 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.*;
src/test/java/inu/codin/codin/domain/email/service/JoinEmailAuthServiceTest.java (2)

20-22: Mockito eq 정적 임포트 누락으로 컴파일 에러 발생

verify(emailTemplateService).sendTemplateEmail(eq(...), ...)에서 eq가 사용되지만 정적 임포트가 없습니다. 현재 파일에도 동일 문제가 존재합니다.

다음 라인을 추가하세요:

 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.*;

1-180: 모든 테스트 파일에 eq 정적 임포트 추가 필요

다음 테스트 클래스들에서 eq() 사용 시
import static org.mockito.ArgumentMatchers.eq;
정적 임포트가 누락되어 있어 컴파일/실행 오류가 발생할 수 있습니다. 각 파일의 상단에 해당 import 구문을 추가해주세요.

  • src/test/java/inu/codin/codin/domain/post/PostQueryServiceTest.java
  • src/test/java/inu/codin/codin/domain/post/PostDtoAssemblerTest.java
  • src/test/java/inu/codin/codin/domain/post/domain/comment/CommentCommandServiceTest.java
  • src/test/java/inu/codin/codin/domain/post/domain/comment/CommentQueryServiceTest.java
  • src/test/java/inu/codin/codin/domain/post/domain/poll/PollCommandServiceTest.java
  • src/test/java/inu/codin/codin/domain/email/service/JoinEmailAuthServiceTest.java
  • src/test/java/inu/codin/codin/domain/email/service/PasswordResetEmailServiceTest.java
  • src/test/java/inu/codin/codin/domain/post/domain/comment/reply/ReplyQueryServiceTest.java
  • src/test/java/inu/codin/codin/domain/post/domain/comment/reply/ReplyCommandServiceTest.java
  • src/test/java/inu/codin/codin/domain/post/domain/best/BestServiceTest.java
  • src/test/java/inu/codin/codin/domain/block/service/BlockServiceTest.java

파일 상단에 다음 한 줄을 추가하세요:

import static org.mockito.ArgumentMatchers.eq;

필요하다면 org.mockito.Mockito.*, org.mockito.ArgumentMatchers.* 등의 정적 임포트를 통일하여 관리성을 높여도 좋습니다.

src/main/java/inu/codin/codin/domain/post/domain/comment/reply/dto/request/ReplyAnonnymousUpdateRequestDTO.java (2)

8-8: 클래스명 오탈자 수정 및 파일명 변경 필요

리포지토리 전체 검색 결과, 잘못된 클래스명 ReplyAnonnymousUpdateRequestDTO를 참조하는 곳은 해당 파일 외에는 없었습니다. 따라서 아래와 같이 파일명과 클래스 선언만 변경하면 되며, 다른 참조를 추가로 수정할 필요는 없습니다.

• 변경 대상

  • 경로:
    src/main/java/inu/codin/codin/domain/post/domain/comment/reply/dto/request/ReplyAnonnymousUpdateRequestDTO.java

  • src/main/java/inu/codin/codin/domain/post/domain/comment/reply/dto/request/ReplyAnonymousUpdateRequestDTO.java

• 수정할 클래스 선언

-public class ReplyAnonnymousUpdateRequestDTO {
+public class ReplyAnonymousUpdateRequestDTO {

위와 같이 파일명과 클래스명만 교정해 주시면 됩니다.


9-11: primitive boolean 필드에 @NotNull 적용 무효 — Boolean 타입 전환 또는 @NotNull 제거 필요

아래 필드에 대해 primitive boolean 타입은 애초에 null이 될 수 없어 @NotNull 검증이 동작하지 않습니다.
API 의도에 따라 아래 두 가지 옵션 중 하나를 선택하여 반영해주세요.

• 대상 위치

  • 파일: src/main/java/inu/codin/codin/domain/post/domain/comment/reply/dto/request/ReplyAnonnymousUpdateRequestDTO.java
  • 라인: 9–11

• 옵션 A) 필수 값 강제 (클라이언트에서 반드시 전송해야 함)

  • 타입을 래퍼 Boolean으로 변경
  • @NotNull(message = "...") 으로 커스텀 메시지 추가
  • (추가) OpenAPI 스펙 상 필수임을 표시하려면 @Schema(requiredMode = Schema.RequiredMode.REQUIRED) 활용
-    @Schema(description = "익명 여부", example = "true")
-    @NotNull
-    private boolean anonymous;
+    @Schema(description = "익명 여부", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "anonymous는 필수입니다.")
+    private Boolean anonymous;

• 옵션 B) 기본값 허용 (전송하지 않을 경우 false로 처리)

  • primitive boolean 유지
  • @NotNull 제거
-    @Schema(description = "익명 여부", example = "true")
-    @NotNull
-    private boolean anonymous;
+    @Schema(description = "익명 여부", example = "true")
+    private boolean anonymous;

위 두 옵션 중 API 스펙에 맞는 방식을 선택하여 반영 부탁드립니다.

src/main/java/inu/codin/codin/domain/notification/service/NotificationService.java (1)

73-81: 전송 성공 시에만 로그 저장하도록 동작 일치화

Topic 전송은 성공 시에만 로그 저장, 사용자 전송은 실패해도 로그 저장으로 동작이 불일치합니다. 일관성 있게 성공 시에만 저장하는 쪽으로 정렬하는 것을 제안합니다.

-        try {
-            fcmService.sendFcmMessage(msgDto);
-            log.info("[sendFcmMessage] 알림 전송 성공");
-        } catch (Exception e) {
-            log.error("[sendFcmMessage] 알림 전송 실패 : {}", e.getMessage());
-        }
-        saveNotificationLog(msgDto, data);
+        try {
+            fcmService.sendFcmMessage(msgDto);
+            log.info("[sendFcmMessage] 알림 전송 성공");
+            saveNotificationLog(msgDto, data);
+        } catch (Exception e) {
+            log.error("[sendFcmMessage] 알림 전송 실패 : {}", e.getMessage());
+        }
src/main/java/inu/codin/codin/domain/post/domain/hits/service/HitsService.java (2)

67-67: Redis 응답 파싱에서 ClassCastException 위험 — 강제 캐스팅 제거 필요.

(String) hits로의 다운캐스팅은 Redis 클라이언트 구현에 따라 String 이외 타입(예: Long)이 반환될 때 ClassCastException을 유발합니다. String.valueOf(...)로 안전 파싱해 주세요.

권장 diff:

-        else return Integer.parseInt((String)hits);
+        else return Integer.parseInt(String.valueOf(hits));

61-66: @async이 자기 호출(self-invocation)로 인해 적용되지 않습니다 — 비동기 복구 보장 방식으로 전환 필요.

동일 빈 내부에서 recoveryHits를 호출하면 프록시가 우회되어 @Async가 적용되지 않습니다(게다가 protected 가시성). 의도한 "Cache miss 시 비동기 복구"가 실행되지 않을 수 있습니다. 호출 지점에서 명시적 비동기 실행으로 전환하고, 불필요한 @Async를 제거하는 것을 권장합니다. 또한 Redis 비가용 시 복구 호출을 생략해 예외 가능성을 줄이세요.

권장 최소 변경 diff:

@@
 import org.springframework.stereotype.Service;
+import java.util.concurrent.CompletableFuture;
@@
-        if (hits == null) {
-            recoveryHits(postId);
-            return hitsRepository.countAllByPostId(postId);
-        }
+        if (hits == null) {
+            if (redisHealthChecker.isRedisAvailable()) {
+                CompletableFuture.runAsync(() -> recoveryHits(postId));
+            }
+            return hitsRepository.countAllByPostId(postId);
+        }
@@
-    @Async
-    protected void recoveryHits(ObjectId postId) {
+    protected void recoveryHits(ObjectId postId) {
         int hits = hitsRepository.countAllByPostId(postId);
         redisHitsService.recoveryHits(postId, hits);
     }

대안(선호): 전용 컴포넌트로 분리하여 프록시 기반 @async 유지

// new file: HitsCacheRecoveryService.java
@Service
@RequiredArgsConstructor
public class HitsCacheRecoveryService {
    private final RedisHitsService redisHitsService;
    private final HitsRepository hitsRepository;

    @Async
    public void recoveryHits(ObjectId postId) {
        int hits = hitsRepository.countAllByPostId(postId);
        redisHitsService.recoveryHits(postId, hits);
    }
}

그리고 HitsService에서는 HitsCacheRecoveryService를 주입받아 호출합니다.

Also applies to: 74-75, 12-12

src/main/java/inu/codin/codin/domain/like/service/LikeService.java (3)

80-84: Redis 비가용 시 Best 점수 적용에서 예외 가능성 — 가드 추가 필요.

redisBestService.applyBestScore 호출이 Redis 헬스 체크 밖에 있어, Redis 다운 시 예외가 날 수 있습니다. 다른 Redis 호출과 동일하게 가드를 적용해 주세요.

권장 diff:

-        if (likeType == LikeType.POST) {
-            redisBestService.applyBestScore(1, new ObjectId(likeTypeId));
-            log.info("Redis에 Best Score 적용 - postId: {}", likeTypeId);
-        }
+        if (likeType == LikeType.POST) {
+            if (redisHealthChecker.isRedisAvailable()) {
+                redisBestService.applyBestScore(1, new ObjectId(likeTypeId));
+                log.info("Redis에 Best Score 적용 - postId: {}", likeTypeId);
+            }
+        }

113-126: @async 자기 호출 문제로 비동기 복구가 동작하지 않습니다 — 호출 지점 비동기화로 전환 권장.

getLikeCount에서 같은 빈의 recoveryLike를 호출하고 있어 @Async가 적용되지 않습니다. 호출 지점에서 비동기 실행하고, 메서드의 @Async는 제거해 주세요. Redis 비가용 시에는 복구를 생략하도록 가드도 추가합니다.

권장 diff:

@@
-        int likeCount = likeRepository.countByLikeTypeAndLikeTypeIdAndDeletedAtIsNull(likeType, likeTypeId);
-        recoveryLike(likeType, likeTypeId, likeCount);
+        int likeCount = likeRepository.countByLikeTypeAndLikeTypeIdAndDeletedAtIsNull(likeType, likeTypeId);
+        if (redisHealthChecker.isRedisAvailable()) {
+            java.util.concurrent.CompletableFuture.runAsync(() -> recoveryLike(likeType, likeTypeId, likeCount));
+        }
         return likeCount;
@@
-    @Async
-    protected void recoveryLike(LikeType entityType, String entityId, int likeCount) {
+    protected void recoveryLike(LikeType entityType, String entityId, int likeCount) {
         redisLikeService.recoveryLike(entityType.name(), entityId, likeCount);
     }

선호 대안: 전용 LikeCacheRecoveryService로 분리하여 @Async 유지(구현 패턴은 Hits와 동일).

Also applies to: 128-131


1-164: 패키지 이동에 따른 import 경로 일괄 점검 필요
패키지 리팩토링 후에도 ReplyCommentRepository에 대한 기존 경로(inu.codin.codin.domain.post.domain.comment.reply.repository)가 다수 남아 있어, 컴파일 타임 오류를 방지하기 위해 아래 파일들의 import 경로를 새로운 패키지 구조로 수정해야 합니다.

– src/test/java/inu/codin/codin/domain/post/domain/comment/reply/ReplyQueryServiceTest.java (라인 9, 11)
– src/test/java/inu/codin/codin/domain/post/domain/comment/reply/ReplyCommandServiceTest.java (라인 11)
– src/main/java/inu/codin/codin/domain/report/service/ReportService.java (라인 10)
– src/main/java/inu/codin/codin/domain/notification/service/NotificationService.java (라인 12)
– src/main/java/inu/codin/codin/domain/post/domain/comment/reply/service/ReplyCommandService.java (라인 12)
– src/main/java/inu/codin/codin/domain/post/domain/comment/reply/service/ReplyQueryService.java (라인 10)
– src/main/java/inu/codin/codin/domain/like/service/LikeService.java (라인 12)

각 파일에서

import inu.codin.codin.domain.post.domain.comment.reply.repository.ReplyCommentRepository;

import inu.codin.codin.domain.post.domain.comment.domain.reply.repository.ReplyCommentRepository;

(또는 새 패키지 경로에 맞춰)로 변경해 주세요.

src/main/java/inu/codin/codin/domain/post/domain/comment/reply/dto/request/ReplyCreateRequestDTO.java (2)

16-18: 익명 기본값 문서-구현 불일치 및 유효성 애노테이션 오용

설명에는 기본값이 true로 명시되어 있으나, 현재 구현은 기본값이 false(자바 기본값)이며, 원시 boolean 타입에 @NotNull은 무의미합니다. 클라이언트가 필드를 생략하면 의도와 다르게 비익명으로 처리될 수 있습니다.

다음과 같이 기본값을 명시하고 @NotNull을 제거하는 것을 권장합니다.

-    @Schema(description = "게시물 익명 여부 default = true (익명)", example = "true")
-    @NotNull
-    private boolean anonymous;
+    @Schema(description = "대댓글 익명 여부", example = "true", defaultValue = "true")
+    private boolean anonymous = true;

9-11: Jackson 기본 바인딩 정책 적용으로 인한 필드 미매핑 위험 – DTO에 바인딩 어노테이션 및 생성자/세터 추가 필요

프로젝트 전반에 Jackson2ObjectMapperBuilderCustomizerspring.jackson.* 설정이 없으므로, 기본 ObjectMapper의 필드·세터 가시성(PUBLIC_ONLY)만 적용됩니다. 이로 인해 private 필드에 세터가 없으면 JSON 요청 데이터를 매핑하지 못할 수 있습니다.
아래와 같이 DTO 클래스에 Jackson 바인딩을 보장할 어노테이션과 생성자/세터를 추가해 주세요.

• 수정 대상

  • src/main/java/inu/codin/codin/domain/post/domain/comment/reply/dto/request/ReplyCreateRequestDTO.java

• 권장 접근 방식 1: 기본 생성자 + 세터

 @Getter
- public class ReplyCreateRequestDTO {
+ @NoArgsConstructor(access = AccessLevel.PROTECTED)
+ public class ReplyCreateRequestDTO {
     @Schema(description = "댓글 내용", example = "content")
     @NotBlank
     private String content;
+
+    // 필요 시, 개별 필드의 직렬화 이름 보장
+    @JsonProperty("content")
+    public void setContent(String content) {
+        this.content = content;
+    }
 }

• 권장 접근 방식 2: 생성자 기반 바인딩

 @Getter
 public class ReplyCreateRequestDTO {
-    @Schema(description = "댓글 내용", example = "content")
-    @NotBlank
-    private String content;
+    private final String content;
+
+    @JsonCreator
+    public ReplyCreateRequestDTO(
+        @JsonProperty("content")
+        @NotBlank
+        String content
+    ) {
+        this.content = content;
+    }
 }

위 조치 중 하나를 적용하여 Jackson이 private 필드를 올바르게 바인딩하도록 보장해 주세요.

src/main/java/inu/codin/codin/domain/post/entity/PostEntity.java (1)

71-75: updateNotice에서 addAll의 NPE 위험 및 중복 가능성

postImageUrls 인자가 null일 경우 NPE가 발생합니다. 또한 동일 URL이 중복으로 누적될 여지가 있습니다.

 public void updateNotice(String title, String content, List<String> postImageUrls) {
   this.title = title;
   this.content = content;
-  this.postImageUrls.addAll(postImageUrls);
+  if (postImageUrls != null && !postImageUrls.isEmpty()) {
+      // 중복 방지(선택): 삽입 전 집합화
+      java.util.Set<String> dedup = new java.util.LinkedHashSet<>(this.postImageUrls);
+      dedup.addAll(postImageUrls);
+      this.postImageUrls = new java.util.ArrayList<>(dedup);
+  }
 }
src/main/java/inu/codin/codin/domain/post/domain/comment/dto/response/CommentResponseDTO.java (2)

58-69: Builder 생성자의 Booleanboolean 전환 (NPE 리스크 제거)

생성자 파라미터가 Boolean anonymous로 선언되어 있어 null 입력 시 오토 언박싱에서 NPE가 발생합니다. 필드가 boolean인 만큼 생성자 파라미터도 boolean으로 통일하세요.

-    public CommentResponseDTO(String _id, String userId, String content,
-                              String nickname, String userImageUrl, Boolean anonymous ,
+    public CommentResponseDTO(String _id, String userId, String content,
+                              String nickname, String userImageUrl, boolean anonymous ,
                               List<CommentResponseDTO> replies, int likeCount,
                               boolean isDeleted, LocalDateTime createdAt, UserInfo userInfo) {

108-126: repliesFrom에서 다운캐스팅 버그 및 상태 손실

  • ReportedCommentDetailResponseDTOCommentResponseDTO 캐스팅은 런타임 ClassCastException을 유발합니다.
  • 빌더 재생성 시 isDeleted를 세팅하지 않아 항상 기본값(false)로 덮입니다.

메서드 시그니처를 List<CommentResponseDTO>로 바꾸고, 캐스팅을 제거하며, 누락된 isDeleted를 전달하세요.

-    public CommentResponseDTO repliesFrom(List<ReportedCommentDetailResponseDTO> updatedReplies) {
-        List<CommentResponseDTO> commentReplies = updatedReplies.stream()
-                .map(reply -> (CommentResponseDTO) reply) // 변환
-                .toList();
-
-        return CommentResponseDTO.builder()
+    public CommentResponseDTO repliesFrom(List<CommentResponseDTO> updatedReplies) {
+        return CommentResponseDTO.builder()
                 ._id(this._id)
                 .userId(this.userId)
                 .content(this.content)
                 .nickname(this.nickname)
                 .userImageUrl(this.userImageUrl)
                 .anonymous(this.anonymous)
-                .replies(commentReplies) // 수정된 대댓글 리스트 적용
+                .replies(updatedReplies) // 수정된 대댓글 리스트 적용
                 .likeCount(this.likeCount)
+                .isDeleted(this.isDeleted)
                 .createdAt(this.createdAt)
                 .userInfo(this.userInfo)
                 .build();
     }

Comment on lines +42 to +47
private static AutoCloseable securityUtilsMock;

@BeforeEach
void setUp() {
securityUtilsMock = Mockito.mockStatic(SecurityUtils.class);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Mockito static mocking 사용법 오류 — 타입 및 스텁 방식 수정 필요

mockStatic 반환을 AutoCloseable로 받으면 스텁 API(when, verify)를 쓸 수 없습니다. MockedStatic<SecurityUtils>로 선언하고 이를 통해 스텁하세요.

-    private static AutoCloseable securityUtilsMock;
+    private static org.mockito.MockedStatic<SecurityUtils> securityUtilsMock;
@@
-    void setUp() {
-        securityUtilsMock = Mockito.mockStatic(SecurityUtils.class);
-    }
+    void setUp() {
+        securityUtilsMock = Mockito.mockStatic(SecurityUtils.class);
+    }
@@
-    void tearDown() throws Exception {
-        securityUtilsMock.close();
-    }
+    void tearDown() {
+        securityUtilsMock.close();
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static AutoCloseable securityUtilsMock;
@BeforeEach
void setUp() {
securityUtilsMock = Mockito.mockStatic(SecurityUtils.class);
}
private static org.mockito.MockedStatic<SecurityUtils> securityUtilsMock;
@BeforeEach
void setUp() {
securityUtilsMock = Mockito.mockStatic(SecurityUtils.class);
}
@AfterEach
void tearDown() {
securityUtilsMock.close();
}
🤖 Prompt for AI Agents
In
src/test/java/inu/codin/codin/domain/post/domain/comment/reply/ReplyCommandServiceTest.java
around lines 42 to 47, the static mock is stored as AutoCloseable which prevents
using the MockedStatic stubbing API; change the field type to
MockedStatic<SecurityUtils>, assign Mockito.mockStatic(SecurityUtils.class) to
it, use that mockedStatic instance for when/verify stubs (e.g.
mockedStatic.when(...)), and ensure you close it in an @AfterEach method by
calling mockedStatic.close().

Comment on lines +66 to +67
given(SecurityUtils.getCurrentUserId()).willReturn(userId);
given(replyCommentRepository.save(any())).willAnswer(inv -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

정적 메서드 스텁 방식 수정

정적 메서드는 given(SecurityUtils.getCurrentUserId())가 아니라 securityUtilsMock.when(...)으로 스텁해야 합니다.

-        given(SecurityUtils.getCurrentUserId()).willReturn(userId);
+        securityUtilsMock.when(SecurityUtils::getCurrentUserId).thenReturn(userId);

위 변경을 다음 위치 모두에 적용해 주세요.

  • Line 66-67 (addReply_정상생성_성공)
  • Line 98-99 (addReply_본인게시물_알림미발송)
  • Line 137-138 (addReply_다른사용자게시물_알림발송)

Also applies to: 98-99, 137-138

🤖 Prompt for AI Agents
In
src/test/java/inu/codin/codin/domain/post/domain/comment/reply/ReplyCommandServiceTest.java
around lines 66-67, 98-99 and 137-138, replace the static-method style stub
using given(SecurityUtils.getCurrentUserId()) with the mock's when(...)
stubbing: use securityUtilsMock.when(...) to stub
SecurityUtils.getCurrentUserId() to return the test userId in each test case
(addReply_정상생성_성공, addReply_본인게시물_알림미발송, addReply_다른사용자게시물_알림발송), keeping the
rest of the save repository answer stubbing unchanged.

Comment on lines +184 to +186
doNothing().when(SecurityUtils.class);
SecurityUtils.validateUser(userId);
given(commentQueryService.findCommentById(reply.getCommentId())).willReturn(comment);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

정적 void 메서드 스텁 오류

doNothing().when(SecurityUtils.class); SecurityUtils.validateUser(userId);는 Mockito에서 지원되지 않습니다. MockedStatic으로 다음처럼 처리하세요.

-        doNothing().when(SecurityUtils.class);
-        SecurityUtils.validateUser(userId);
+        securityUtilsMock.when(() -> SecurityUtils.validateUser(userId));

validateUser가 예외를 던지지 않는 happy-path만 검증한다면 위와 같이 “아무 것도 하지 않음” 스텁이면 충분합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
doNothing().when(SecurityUtils.class);
SecurityUtils.validateUser(userId);
given(commentQueryService.findCommentById(reply.getCommentId())).willReturn(comment);
securityUtilsMock.when(() -> SecurityUtils.validateUser(userId));
given(commentQueryService.findCommentById(reply.getCommentId())).willReturn(comment);

given(postQueryService.getUserAnonymousNumber(any(), any())).willReturn(1);
given(likeService.getLikeCount(eq(LikeType.REPLY), any())).willReturn(3);
given(SecurityUtils.getCurrentUserId()).willReturn(new ObjectId());
given(likeService.isLiked(eq(LikeType.COMMENT), any(),(ObjectId) any())).willReturn(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

LikeType 불일치로 인한 테스트/로직 혼선 가능성 (REPLY vs COMMENT).

getLikeCount에는 LikeType.REPLY를 사용하면서, isLiked 호출에는 LikeType.COMMENT를 사용하고 있습니다. 대댓글(Reply)에 대한 좋아요 타입이 일관되어야 합니다. 현재처럼 혼재하면 실제 구현과의 불일치가 테스트에서 가려질 수 있습니다.

적용 diff(해당 위치들을 REPLY로 통일하는 예):

-        given(likeService.isLiked(eq(LikeType.COMMENT), any(),(ObjectId) any())).willReturn(false);
+        given(likeService.isLiked(eq(LikeType.REPLY), any(), (ObjectId) any())).willReturn(false);
@@
-        given(likeService.isLiked(eq(LikeType.COMMENT), any(),(ObjectId) any())).willReturn(true);
+        given(likeService.isLiked(eq(LikeType.REPLY), any(), (ObjectId) any())).willReturn(true);
@@
-        given(likeService.isLiked(LikeType.COMMENT, replyId.toString(), userId)).willReturn(true);
+        given(likeService.isLiked(LikeType.REPLY, replyId.toString(), userId)).willReturn(true);
@@
-        given(likeService.isLiked(LikeType.COMMENT, replyId.toString(), userId)).willReturn(false);
+        given(likeService.isLiked(LikeType.REPLY, replyId.toString(), userId)).willReturn(false);
@@
-        given(likeService.isLiked(eq(LikeType.COMMENT), any(),(ObjectId) any())).willReturn(false);
+        given(likeService.isLiked(eq(LikeType.REPLY), any(), (ObjectId) any())).willReturn(false);

필요 시 실제 도메인 정책(Reply에 COMMENT 타입 사용)이 있다면 반대로 getLikeCount도 COMMENT로 통일해 주세요.

Also applies to: 125-125, 167-181, 189-198, 223-223

🤖 Prompt for AI Agents
In
src/test/java/inu/codin/codin/domain/post/domain/comment/reply/ReplyQueryServiceTest.java
around lines 77 (and also apply the same change at 125, 167-181, 189-198, 223),
the test mixes LikeType.COMMENT with getLikeCount using LikeType.REPLY; update
the likeService.isLiked(...) calls to use LikeType.REPLY (or, if the domain
policy is that replies use COMMENT, change getLikeCount calls instead) so that
the LikeType is consistent for Reply-related assertions across all referenced
lines.

Comment on lines +39 to +49
private static AutoCloseable securityUtilsMock;

@BeforeEach
void setUp() {
securityUtilsMock = Mockito.mockStatic(SecurityUtils.class);
}

@AfterEach
void tearDown() throws Exception {
securityUtilsMock.close();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Mockito 정적 메서드(stub) 사용 오류: SecurityUtils 스텁이 실제로 적용되지 않습니다

mockStatic 사용 시 BDD 스타일의 given(SecurityUtils...)로는 스텁이 걸리지 않습니다. MockedStatic<SecurityUtils>when(...).thenReturn(...) 형태로 교체해야 합니다. 그렇지 않으면 실제 SecurityUtils가 호출되어 테스트가 환경 의존/불안정해집니다.

아래 패치를 적용해 주세요.

@@
-    private static AutoCloseable securityUtilsMock;
+    private MockedStatic<SecurityUtils> securityUtilsMock;
@@
-    void setUp() {
-        securityUtilsMock = Mockito.mockStatic(SecurityUtils.class);
-    }
+    void setUp() {
+        securityUtilsMock = Mockito.mockStatic(SecurityUtils.class);
+    }
@@
-    void tearDown() throws Exception {
-        securityUtilsMock.close();
-    }
+    void tearDown() throws Exception {
+        securityUtilsMock.close();
+    }
@@
-        given(SecurityUtils.getCurrentUserId()).willReturn(userId);
-        given(SecurityUtils.getCurrentUserRole()).willReturn(UserRole.ADMIN);
+        securityUtilsMock.when(SecurityUtils::getCurrentUserId).thenReturn(userId);
+        securityUtilsMock.when(SecurityUtils::getCurrentUserRole).thenReturn(UserRole.ADMIN);
@@
-        given(SecurityUtils.getCurrentUserId()).willReturn(new ObjectId());
-        given(SecurityUtils.getCurrentUserRole()).willReturn(UserRole.ADMIN);
+        securityUtilsMock.when(SecurityUtils::getCurrentUserId).thenReturn(new ObjectId());
+        securityUtilsMock.when(SecurityUtils::getCurrentUserRole).thenReturn(UserRole.ADMIN);
@@
-        given(SecurityUtils.getCurrentUserId()).willReturn(new ObjectId());
-        given(SecurityUtils.getCurrentUserRole()).willReturn(UserRole.ADMIN);
+        securityUtilsMock.when(SecurityUtils::getCurrentUserId).thenReturn(new ObjectId());
+        securityUtilsMock.when(SecurityUtils::getCurrentUserRole).thenReturn(UserRole.ADMIN);
@@
-        given(SecurityUtils.getCurrentUserId()).willReturn(new ObjectId());
-        given(SecurityUtils.getCurrentUserRole()).willReturn(UserRole.ADMIN);
+        securityUtilsMock.when(SecurityUtils::getCurrentUserId).thenReturn(new ObjectId());
+        securityUtilsMock.when(SecurityUtils::getCurrentUserRole).thenReturn(UserRole.ADMIN);
@@
-        given(SecurityUtils.getCurrentUserId()).willReturn(new ObjectId());
-        given(SecurityUtils.getCurrentUserRole()).willReturn(UserRole.ADMIN);
+        securityUtilsMock.when(SecurityUtils::getCurrentUserId).thenReturn(new ObjectId());
+        securityUtilsMock.when(SecurityUtils::getCurrentUserRole).thenReturn(UserRole.ADMIN);
@@
-        given(SecurityUtils.getCurrentUserId()).willReturn(new ObjectId());
-        given(SecurityUtils.getCurrentUserRole()).willReturn(UserRole.ADMIN);
+        securityUtilsMock.when(SecurityUtils::getCurrentUserId).thenReturn(new ObjectId());
+        securityUtilsMock.when(SecurityUtils::getCurrentUserRole).thenReturn(UserRole.ADMIN);

추가로, 정적 모킹이 동작하려면 mockito-inline 의존성이 필요합니다. 아래 스크립트로 빌드 파일에 포함돼 있는지 확인해 주세요.

Also applies to: 58-60, 98-100, 115-117, 133-135, 149-151, 166-167


🏁 Script executed:

#!/bin/bash
# Gradle 또는 Maven에서 mockito-inline 의존성 유무 확인
rg -n -C2 -g '!**/build/**' -g '!**/target/**' -e 'mockito-inline|mockito-inline-jupiter|org\.mockito:mockito-inline' || true

Length of output: 126


🏁 Script executed:

#!/bin/bash
# Locate build files and check for mockito-inline dependency
find . -maxdepth 3 -type f \( -name 'build.gradle' -o -name 'build.gradle.kts' -o -name 'pom.xml' \) | while read file; do
  echo "=== Checking $file ==="
  rg -n 'mockito-inline|org\.mockito:mockito-inline' "$file" || echo "No match in $file"
done

Length of output: 326


Mockito 정적 메서드 모킹 오류 수정 및 mockito-inline 의존성 추가

PostCommandServiceTest에서 Mockito.mockStatic(SecurityUtils.class)로 생성한 정적 모킹은 BDD 스타일의 given(SecurityUtils…)로는 적용되지 않습니다. 반드시 MockedStatic<SecurityUtils> 타입을 사용하고, securityUtilsMock.when(...).thenReturn(...) 형태로 스텁을 걸어야 테스트가 올바르게 동작합니다. 또한 mockito-inline 모듈을 추가해야 정적 모킹이 활성화됩니다.

수정 사항 요약:

  • 필드 타입 변경

    • 기존
      private static AutoCloseable securityUtilsMock;
    • 수정
      private MockedStatic<SecurityUtils> securityUtilsMock;
  • 스텁 설정 방식 변경 (given(...)when(...).thenReturn(...))

    - given(SecurityUtils.getCurrentUserId()).willReturn(userId);
    - given(SecurityUtils.getCurrentUserRole()).willReturn(UserRole.ADMIN);
    + securityUtilsMock.when(SecurityUtils::getCurrentUserId).thenReturn(userId);
    + securityUtilsMock.when(SecurityUtils::getCurrentUserRole).thenReturn(UserRole.ADMIN);

    (위 패턴을 39-49, 58-60, 98-100, 115-117, 133-135, 149-151, 166-167 라인 전 구간에 일괄 적용)

  • @BeforeEach / @AfterEach는 그대로 유지

    @BeforeEach
    void setUp() {
        securityUtilsMock = Mockito.mockStatic(SecurityUtils.class);
    }
    
    @AfterEach
    void tearDown() {
        securityUtilsMock.close();
    }
  • build.gradle에 mockito-inline 의존성 추가

    dependencies {
        testImplementation 'org.mockito:mockito-inline:4.+'
        // 필요 시 정확한 최신 버전으로 대체
    }

대상 파일:

  • src/test/java/inu/codin/codin/domain/post/PostCommandServiceTest.java (39–49, 58–60, 98–100, 115–117, 133–135, 149–151, 166–167)

@CodIN-INU CodIN-INU deleted a comment from coderabbitai bot Sep 20, 2025
@gisu1102 gisu1102 merged commit edad72b into develop Oct 4, 2025
1 check passed
@gisu1102 gisu1102 mentioned this pull request Oct 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority : High 우선순위 상 Priority: Medium 우선순위 중 🔨 Refactor 코드 리팩토링 Status: Pending 보류 중인 이슈 Status: Reviewing 처리하고 리뷰 중인 이슈 ✅ Test test 관련

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants