Skip to content

Refactor: 테스트 코드 작성#60

Merged
gratisreise merged 10 commits intomainfrom
dev
Mar 9, 2026
Merged

Refactor: 테스트 코드 작성#60
gratisreise merged 10 commits intomainfrom
dev

Conversation

@gratisreise
Copy link
Owner

@gratisreise gratisreise commented Mar 9, 2026

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 게시물 생성 시 생성된 게시물의 ID를 응답으로 반환하도록 개선했습니다.
  • 개선 사항

    • 내부 아키텍처를 개선하고 테스트 커버리지를 강화했습니다.
    • 데이터베이스 쿼리 성능을 최적화했습니다.

- CommentResponse, CommentArticleResponse, Reply에 @builder 패턴 적용
- Reply 생성자를 정적 팩토리 메서드 from()으로 변경
- 코드 포맷팅 (들여쓰기 2 spaces 통일)
- CommentRepositoryCustom 인터페이스 제거
- NotificationResponse.from()에서 new 대신 @builder 패턴 사용
- 전체 코드 포맷팅 (들여쓰기 2공백 통일)
- 불필요한 공백 라인 제거
- Article 도메인 엔티티, DTO, Repository 코드 포맷팅
- 테스트 코드 JUnit assertions에서 AssertJ로 마이그레이션
- ArticleCreateResponse DTO 추가
- 테스트 코드 실제 동작에 맞게 수정
- AuthService: 회원가입, 로그인, 토큰 재발급, 로그아웃 테스트
- TokenService: 토큰 생성, 재발급, 추출 테스트
- CategoryReader 테스트: getCategories, getById, getByMemberIdAndCategoryName, getCategory
- CategoryWriter 테스트: createCategory, updateCategory, deleteCategory
- BDD 스타일 Mock 검증 및 AssertJ assertions 적용
- CommentReaderTest: getById, getMyComments 테스트
- CommentWriterTest: createComment, updateComment, deleteComment 테스트
- MemberReader: getById, getByNickname, getByEmail, existsByProviderId, existsByEmail 테스트
- MemberWriter: update, deleteById, saveOrUpdate 테스트
- NotificationSettingReader: isDisabled, getNotificationSettings 테스트
- NotificationSettingWriter: createNotificationSetting, toggleNotification 테스트
- MemberService: getMember, updateMember, delete 테스트
@gratisreise gratisreise merged commit 8229ea0 into main Mar 9, 2026
0 of 2 checks passed
@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Caution

Review failed

The pull request is closed.

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'linters', 'tools', 'pre_merge_checks', 'path_filters', 'path_instructions'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ef65b2d3-de63-49ce-95b7-58244771b7a6

📥 Commits

Reviewing files that changed from the base of the PR and between 076932e and 821b9e0.

⛔ Files ignored due to path filters (2)
  • .serena/cache/java/document_symbols.pkl is excluded by !**/*.pkl
  • .serena/cache/java/raw_document_symbols.pkl is excluded by !**/*.pkl
📒 Files selected for processing (119)
  • .claudedocs/api/article-api.yaml
  • .claudedocs/api/auth-api.yaml
  • .claudedocs/api/category-api.yaml
  • .claudedocs/api/comment-api.yaml
  • .claudedocs/api/member-api.yaml
  • .claudedocs/api/notification-api.yaml
  • .claudedocs/db/schema.sql
  • .claudedocs/screen/design-system.md
  • .claudedocs/screen/linear-colortheme.json
  • .claudedocs/screen/prompt.md
  • .claudedocs/screen/screens.md
  • .claudedocs/screen/stichpromptguide.md
  • .claudedocs/tech/tech.md
  • .serena/memories/article-domain/structure.md
  • build.gradle
  • gradle.properties
  • settings.gradle
  • src/main/java/com/mylog/MylogApplication.java
  • src/main/java/com/mylog/common/CommonValue.java
  • src/main/java/com/mylog/common/annotations/MemberId.java
  • src/main/java/com/mylog/common/annotations/OAuthServiceType.java
  • src/main/java/com/mylog/common/db/BaseEntity.java
  • src/main/java/com/mylog/common/enums/AnalyzeStatus.java
  • src/main/java/com/mylog/common/enums/OauthProvider.java
  • src/main/java/com/mylog/common/enums/WritingStyle.java
  • src/main/java/com/mylog/common/exception/BusinessException.java
  • src/main/java/com/mylog/common/exception/ErrorCode.java
  • src/main/java/com/mylog/common/exception/GlobalExceptionHandler.java
  • src/main/java/com/mylog/common/resolver/MemberIdArgumentResolver.java
  • src/main/java/com/mylog/common/response/BaseResponse.java
  • src/main/java/com/mylog/common/response/ErrorResponse.java
  • src/main/java/com/mylog/common/response/PageResponse.java
  • src/main/java/com/mylog/common/response/SuccessResponse.java
  • src/main/java/com/mylog/common/response/classes/Pagination.java
  • src/main/java/com/mylog/common/security/CustomAccessDeniedHandler.java
  • src/main/java/com/mylog/common/security/CustomAuthenticationEntryPoint.java
  • src/main/java/com/mylog/common/security/CustomUserDetails.java
  • src/main/java/com/mylog/common/security/ExceptionHandlerFilter.java
  • src/main/java/com/mylog/common/security/JwtAuthenticationFilter.java
  • src/main/java/com/mylog/common/security/JwtProvider.java
  • src/main/java/com/mylog/common/security/SecurityConfig.java
  • src/main/java/com/mylog/common/validation/Password.java
  • src/main/java/com/mylog/common/validation/PasswordValidator.java
  • src/main/java/com/mylog/config/QuerydslConfig.java
  • src/main/java/com/mylog/config/WebConfig.java
  • src/main/java/com/mylog/domain/article/ArticleController.java
  • src/main/java/com/mylog/domain/article/ArticleService.java
  • src/main/java/com/mylog/domain/article/dto/request/ArticleCreateRequest.java
  • src/main/java/com/mylog/domain/article/dto/request/ArticleSearchRequest.java
  • src/main/java/com/mylog/domain/article/dto/request/ArticleUpdateRequest.java
  • src/main/java/com/mylog/domain/article/dto/response/ArticleCreateResponse.java
  • src/main/java/com/mylog/domain/article/dto/response/ArticleResponse.java
  • src/main/java/com/mylog/domain/article/dto/response/ArticleTestResponse.java
  • src/main/java/com/mylog/domain/article/entity/Article.java
  • src/main/java/com/mylog/domain/article/entity/ArticleTag.java
  • src/main/java/com/mylog/domain/article/entity/ArticleTagId.java
  • src/main/java/com/mylog/domain/article/entity/Tag.java
  • src/main/java/com/mylog/domain/article/repository/ArticleRepository.java
  • src/main/java/com/mylog/domain/article/repository/ArticleRepositoryCustom.java
  • src/main/java/com/mylog/domain/article/repository/ArticleTagRepository.java
  • src/main/java/com/mylog/domain/article/repository/TagRepository.java
  • src/main/java/com/mylog/domain/article/repository/impl/ArticleRepositoryImpl.java
  • src/main/java/com/mylog/domain/article/service/ArticleReader.java
  • src/main/java/com/mylog/domain/article/service/ArticleWriter.java
  • src/main/java/com/mylog/domain/article/service/TagReader.java
  • src/main/java/com/mylog/domain/article/service/TagWriter.java
  • src/main/java/com/mylog/domain/comment/CommentController.java
  • src/main/java/com/mylog/domain/comment/dto/CommentArticleResponse.java
  • src/main/java/com/mylog/domain/comment/dto/CommentCreateRequest.java
  • src/main/java/com/mylog/domain/comment/dto/CommentResponse.java
  • src/main/java/com/mylog/domain/comment/dto/CommentUpdateRequest.java
  • src/main/java/com/mylog/domain/comment/dto/Reply.java
  • src/main/java/com/mylog/domain/comment/entity/Comment.java
  • src/main/java/com/mylog/domain/comment/repository/CommentRepository.java
  • src/main/java/com/mylog/domain/comment/service/CommentReader.java
  • src/main/java/com/mylog/domain/comment/service/CommentWriter.java
  • src/main/java/com/mylog/domain/member/MemberController.java
  • src/main/java/com/mylog/domain/member/MemberService.java
  • src/main/java/com/mylog/domain/member/dto/MemberResponse.java
  • src/main/java/com/mylog/domain/member/dto/NotificationSettingResponse.java
  • src/main/java/com/mylog/domain/member/dto/UpdateMemberRequest.java
  • src/main/java/com/mylog/domain/member/entity/Member.java
  • src/main/java/com/mylog/domain/member/entity/NotificationSetting.java
  • src/main/java/com/mylog/domain/member/repository/MemberRepository.java
  • src/main/java/com/mylog/domain/member/repository/NotificationSettingRepository.java
  • src/main/java/com/mylog/domain/member/service/MemberReader.java
  • src/main/java/com/mylog/domain/member/service/MemberWriter.java
  • src/main/java/com/mylog/domain/member/service/NotificationSettingReader.java
  • src/main/java/com/mylog/domain/member/service/NotificationSettingWriter.java
  • src/main/java/com/mylog/domain/notification/Notification.java
  • src/main/java/com/mylog/domain/notification/NotificationController.java
  • src/main/java/com/mylog/domain/notification/dto/NotificationResponse.java
  • src/main/java/com/mylog/domain/notification/repository/NotificationRepository.java
  • src/main/java/com/mylog/domain/notification/repository/NotificationRepositoryCustom.java
  • src/main/java/com/mylog/domain/notification/service/NotificationReader.java
  • src/main/java/com/mylog/domain/notification/service/NotificationWriter.java
  • src/main/java/com/mylog/external/s3/S3Config.java
  • src/main/java/com/mylog/external/s3/S3Service.java
  • src/main/resources/application-dev.yml
  • src/main/resources/application-prod.yml
  • src/test/java/com/mylog/domain/article/service/ArticleReaderTest.java
  • src/test/java/com/mylog/domain/article/service/ArticleServiceTest.java
  • src/test/java/com/mylog/domain/article/service/ArticleWriterTest.java
  • src/test/java/com/mylog/domain/article/service/TagReaderTest.java
  • src/test/java/com/mylog/domain/article/service/TagWriterTest.java
  • src/test/java/com/mylog/domain/auth/service/AuthServiceTest.java
  • src/test/java/com/mylog/domain/auth/service/TokenServiceTest.java
  • src/test/java/com/mylog/domain/category/service/CategoryReaderTest.java
  • src/test/java/com/mylog/domain/category/service/CategoryWriterTest.java
  • src/test/java/com/mylog/domain/comment/service/CommentReaderTest.java
  • src/test/java/com/mylog/domain/comment/service/CommentWriterTest.java
  • src/test/java/com/mylog/domain/member/MemberServiceTest.java
  • src/test/java/com/mylog/domain/member/service/MemberReaderTest.java
  • src/test/java/com/mylog/domain/member/service/MemberWriterTest.java
  • src/test/java/com/mylog/domain/member/service/NotificationSettingReaderTest.java
  • src/test/java/com/mylog/domain/member/service/NotificationSettingWriterTest.java
  • src/test/java/com/mylog/domain/notification/service/NotificationReaderTest.java
  • src/test/java/com/mylog/domain/notification/service/NotificationWriterTest.java
  • src/test/resources/application.yml

📝 Walkthrough

Walkthrough

이 PR은 다양한 도메인에서의 대규모 리팩토링으로, OpenAPI 문서 제거, Gradle 설정 업데이트, 보안/응답 처리 강화, QueryDSL 기반 데이터 접근 개선, 그리고 DTO 생성 패턴 표준화를 포함합니다. 동시에 단위 테스트 스위트가 광범위하게 추가되었습니다.

Changes

Cohort / File(s) Summary
설정 및 빌드
build.gradle, gradle.properties, settings.gradle
dev 도구 제거, springdoc 및 spotless 버전 변경, Java 홈 경로 추가로 인한 멀티모듈 구조 단순화
API 문서 제거
.claudedocs/api/*, .claudedocs/screen/*, .claudedocs/db/schema.sql, .claudedocs/tech/tech.md
OpenAPI 사양 및 설계 문서 일괄 제거
아키텍처 참고 문서 추가
.serena/memories/article-domain/structure.md
아티클 도메인 구조 및 CQRS 패턴 설명 문서
보안 및 응답 처리 강화
src/main/java/com/mylog/common/response/ErrorResponse.java, src/main/java/com/mylog/common/response/SuccessResponse.java, src/main/java/com/mylog/common/security/JwtProvider.java, src/main/java/com/mylog/common/security/SecurityConfig.java
ErrorResponse factory 메서드 추가, SuccessResponse에 HTTP 상태 기반 메서드 추가, JWT refresh 토큰 멤버ID 추출 로직 변경, UserDetailsService 의존성 제거
쿼리 설정
src/main/java/com/mylog/config/QuerydslConfig.java
QueryDSL JPAQueryFactory 빈 등록 및 생성자 기반 의존성 주입 추가
아티클 도메인
src/main/java/com/mylog/domain/article/*
ArticleCreateResponse 신규 추가, Repository QueryDSL 기반 재구현, ArticleReader/Writer 응답 타입 변경, 태그 조회 메서드 제거
댓글 도메인
src/main/java/com/mylog/domain/comment/*
CommentResponse/Reply에 @Builder 추가, 생성자 기반에서 팩토리 메서드로 변경, CommentRepositoryCustom 제거
회원 도메인
src/main/java/com/mylog/domain/member/*
UpdateMemberRequest 검증 어노테이션 재배치, NotificationSetting 저장 메서드 정리
알림 도메인
src/main/java/com/mylog/domain/notification/*
NotificationResponse에 @Builder 추가, 읽음 상태 알림만 조회하는 쿼리 메서드 추가, NotificationRepositoryCustom 제거
테스트
src/test/java/com/mylog/domain/*/service/*Test.java
ArticleReader, ArticleService, ArticleWriter, TagReader, TagWriter, AuthService, TokenService, CategoryReader, CategoryWriter, CommentReader, CommentWriter, MemberService, MemberReader, MemberWriter, NotificationSetting*Reader/Writer, NotificationReader/Writer 단위 테스트 신규 추가 (15개 이상의 테스트 클래스)

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant ArticleController as ArticleController
    participant ArticleService as ArticleService
    participant ArticleWriter as ArticleWriter
    participant ArticleRepository as ArticleRepository (QueryDSL)
    participant S3Service as S3Service

    Client->>ArticleController: POST /api/articles (multipart form)
    ArticleController->>ArticleService: createArticle(request, memberId, file)
    ArticleService->>ArticleWriter: create(request, memberId, imageUrl)
    ArticleWriter->>ArticleRepository: save(article)
    ArticleRepository-->>ArticleWriter: Article (with id)
    ArticleWriter->>ArticleRepository: saveTag (async)
    ArticleWriter-->>ArticleService: Article
    ArticleService-->>ArticleController: ArticleCreateResponse(articleId)
    ArticleController-->>Client: 201 Created with response body
Loading
sequenceDiagram
    participant Client as Client
    participant ArticleController as ArticleController
    participant ArticleService as ArticleService
    participant ArticleReader as ArticleReader
    participant ArticleRepositoryImpl as ArticleRepositoryImpl (QueryDSL)
    participant Database as Database

    Client->>ArticleController: GET /api/articles/all?page=0&size=10
    ArticleController->>ArticleService: getArticles(pageable)
    ArticleService->>ArticleReader: getArticles(pageable)
    ArticleReader->>ArticleRepositoryImpl: findAllCustom(pageable)
    ArticleRepositoryImpl->>ArticleRepositoryImpl: Build QueryDSL query with fetch joins
    ArticleRepositoryImpl->>Database: Execute: SELECT article JOIN member JOIN category
    Database-->>ArticleRepositoryImpl: Results
    ArticleRepositoryImpl-->>ArticleReader: Page<Article>
    ArticleReader-->>ArticleService: Page<ArticleResponse>
    ArticleService-->>ArticleController: Page<ArticleResponse>
    ArticleController-->>Client: 200 OK with paginated articles
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • Recover: 머지 충돌 해결 및 불필요 코드 정리 #51: ArticleController의 createArticle 메서드 서명 변경 및 @MemberId 파라미터 처리 통일
  • 50: JwtProvider, SecurityConfig 등 보안 계층 변경사항 오버랩
  • 56: ArticleRepositoryImpl의 QueryDSL 기반 리팩토링, ArticleService/Reader/Writer 변경사항 오버랩

Poem

쿼리는 DSL로, 응답은 빌더로 🏗️
커스텀 리포지토리 작별 인사,
테스트는 군데군데 촘촘하게,
문서는 사라지고 코드만 남아
리팩토링의 봄이 왔네요 🌱


🔍 코드 리뷰 주요 지적사항

🚨 주의 깊게 검토가 필요한 영역

1. TagWriter.saveTag() 메서드 미구현 (HIGH RISK)

// src/main/java/com/mylog/domain/article/service/TagWriter.java
`@Async`
public void saveTag(List<String> tags, Article article) {
    // 메서드가 완전히 비어있음
}

문제점:

  • 태그 저장 로직이 구현되지 않음
  • ArticleWriter.create()가 태그 저장을 호출하지만, 실제 저장이 일어나지 않음
  • 비동기 실행이므로 race condition 위험성 존재
  • 데이터 일관성 문제: 아티클은 저장되지만 태그는 저장 안 됨

개선 권장사항:

`@Async`
public void saveTag(List<String> tags, Article article) {
    if (tags == null || tags.isEmpty()) {
        return;
    }
    // TODO: 태그 저장 로직 구현
    // 1. 기존 태그 조회/생성
    // 2. ArticleTag 저장
    // 3. 저장 실패 시 로깅 및 폴백 처리
}

2. NotificationReader.receiveNotification() 필터링 의도 모호 (MEDIUM RISK)

// src/main/java/com/mylog/domain/notification/service/NotificationReader.java
public Page<NotificationResponse> receiveNotification(Long memberId, Pageable pageable) {
    Member member = memberReader.getById(memberId);
    return repository.findByMemberAndReadTrue(member, pageable)
        .map(NotificationResponse::from);
}

문제점:

  • 읽지 않은 알림(read=false)은 조회하지 않음
  • API 응답으로 읽음 상태 알림만 반환
  • 읽지 않은 알림은 어디서 조회하나? 별도 메서드 필요한 상태

개선 권장사항:

  • 메서드명 명확히: receiveReadNotifications() 또는 getReadNotifications()
  • 문서화: JavaDoc에 "읽음 상태의 알림만 조회" 명시

3. JwtProvider.getRefreshMemberId() 타입 변환 안전성 (MEDIUM RISK)

public long getRefreshMemberId(String token) {
    Claims claims = getRefreshClaims(token);
    return claims.get(MEMBER_ID, Long.class);  // Long 타입으로 클레임 추출
}

문제점:

  • 클레임에 MEMBER_ID가 없으면 null 반환
  • null을 primitive long으로 언박싱하면 NullPointerException 발생 가능
  • 에러 핸들링 부재

개선 권장사항:

public long getRefreshMemberId(String token) {
    Claims claims = getRefreshClaims(token);
    Object memberId = claims.get(MEMBER_ID);
    if (memberId == null) {
        throw new BusinessException(ErrorCode.TOKEN_INVALID);
    }
    return ((Number) memberId).longValue();
}

4. ArticleWriter.create() 반환값 변경 및 태그 저장 순서 (MEDIUM RISK)

public Article create(ArticleCreateRequest request, Long memberId, String imageUrl) {
    // ... 아티클 생성 로직 ...
    Article article = articleRepository.save(article);  // DB에 저장
    
    if (!request.tagNames().isEmpty()) {
        tagWriter.saveTag(request.tagNames(), article);  // 비동기 호출
    }
    
    return article;  // 저장된 아티클 반환 (tag는 비동기로 나중에 저장)
}

문제점:

  • 아티클은 DB에 저장되지만, 태그는 비동기로 저장됨
  • 클라이언트가 즉시 아티클 ID를 받고 조회할 때 태그가 없을 수 있음
  • 비동기 저장 실패 시 처리 메커니즘 없음
  • 응답 구성 단계와 저장 단계의 불일치

개선 권장사항:

public Article create(ArticleCreateRequest request, Long memberId, String imageUrl) {
    // ... 동기적으로 아티클+태그 저장 완료 후 반환
    Article article = articleRepository.save(article);
    
    if (!request.tagNames().isEmpty()) {
        tagWriter.saveTag(request.tagNames(), article);
        // 비동기를 원한다면 CompletableFuture 활용 및 로깅/재시도 정책 추가
    }
    
    return article;
}

5. SecurityConfig에서 UserDetailsService 제거 (LOW-MEDIUM RISK)

// Before:
public SecurityFilterChain filterChain(HttpSecurity http, UserDetailsService userDetailsService) throws Exception

// After:
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception

확인 사항:

  • UserDetailsService가 다른 곳에서 주입되는가?
  • CustomUserDetailsService가 별도로 빈으로 등록되어 있는가?
  • Spring Security의 기본 UserDetailsService 빈이 생성되는가?
  • 테스트에서 UserDetailsService 모킹이 필요한 경우 영향이 있을 수 있음

6. S3 파일 삭제 권한 검증 누락 (MEDIUM RISK)

// src/main/java/com/mylog/external/s3/S3Service.java
public void deleteImage(String imageUrl) {
    // 이미지 URL의 소유권 검증 없음
    // 아무 URL이나 삭제 가능할 수도 있음
}

개선 권장사항:

  • 삭제 전 이미지 URL이 현재 사용자의 자산인지 검증
  • ArticleService 레벨에서 권한 체크 후 호출하는지 확인

✅ 좋은 개선사항

  1. ErrorResponse Factory 메서드: 다양한 에러 타입 처리 표준화
  2. SuccessResponse HTTP 상태 메서드: REST 응답 규칙 준수 강화
  3. QueryDSL 도입: 타입 안전 쿼리, N+1 문제 해결, 유지보수성 향상
  4. 광범위한 단위 테스트: 도메인 로직 검증 강화
  5. @builder 패턴: 불변성 강화 및 복잡한 객체 생성 간소화
✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

@coderabbitai coderabbitai bot mentioned this pull request Mar 12, 2026
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