-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/#35 - Feed/FeedComment CQRS 적용 #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
BaeJunH0
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트 코드 부분 수정부탁드려용
src/main/java/com/example/bak/community/infra/command/CommunityCommandAdaptor.java
Show resolved
Hide resolved
src/main/java/com/example/bak/feed/application/command/FeedCommandService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/example/bak/feed/application/query/dto/FeedResult.java
Outdated
Show resolved
Hide resolved
src/main/java/com/example/bak/feed/application/query/FeedQueryService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/example/bak/feed/infra/query/jdbc/mapper/FeedDetailRowMapper.java
Show resolved
Hide resolved
src/main/java/com/example/bak/feedcomment/application/command/port/UserDataPort.java
Outdated
Show resolved
Hide resolved
src/main/java/com/example/bak/feedcomment/domain/FeedComment.java
Outdated
Show resolved
Hide resolved
src/test/java/com/example/bak/feed/application/FeedServiceUnitTest.java
Outdated
Show resolved
Hide resolved
src/test/java/com/example/bak/feed/domain/FeedRepositoryStub.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request refactors the Feed and Comment modules to align with a CQRS (Command Query Responsibility Segregation) pattern and improve domain modeling. Key changes include splitting FeedService into FeedCommandService and FeedQueryService, and similarly for CommentService into CommentCommandService and CommentQueryService. New command and query ports (FeedCommandPort, FeedQueryPort, CommentCommandPort, CommentQueryPort, CommunityValidationPort, ProfileDataPort) and their respective DTOs and adapters were introduced to enforce this separation. The Feed and Comment domain entities were simplified by removing direct JPA relationships (e.g., @ManyToOne, @OneToMany) and instead storing IDs, making them more focused on business logic. The CommentController was renamed and updated to use the new command/query services, and userId is now retrieved from an @AuthUser annotation instead of the request body. Swagger documentation was also updated to reflect these API changes and include more detailed error responses. Review comments highlighted the need to correct SQL table names in FeedJdbcRepositoryImpl from feed_comments to comments, to explicitly save Profile entities in UserCommandAdaptor after creation, and to consider introducing a FeedValidationPort for more efficient existence checks, similar to CommunityValidationPort. Additionally, a suggestion was made to move the validateAuthor logic into the Feed domain object to enrich the domain model.
src/main/java/com/example/bak/feed/infra/query/jdbc/FeedJdbcRepositoryImpl.java
Outdated
Show resolved
Hide resolved
| public ProfileResult createProfile(Long userId, String name, String nickname) { | ||
| User user = userJpaRepository.findById(userId).orElseThrow(() -> new BusinessException( | ||
| ErrorCode.USER_NOT_FOUND)); | ||
| Profile profile = Profile.create(name, nickname); | ||
| user.addProfile(profile); | ||
| profile.assignUser(userId); | ||
| return ProfileResult.from(profile.getName(), profile.getNickname()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 createProfile 메소드는 Profile 인스턴스를 생성하지만 데이터베이스에 저장하지 않습니다. User와 Profile이 분리되면서 Cascade 옵션이 제거되었기 때문에, 생성된 Profile 엔티티를 명시적으로 저장해야 합니다. ProfileJpaRepository를 주입받아 profile 객체를 저장하는 로직을 추가해주세요. 예를 들어:
@Override
public ProfileResult createProfile(Long userId, String name, String nickname) {
Profile profile = Profile.create(name, nickname);
profile.assignUser(userId);
profileJpaRepository.save(profile); // 이 줄을 추가해야 합니다.
return ProfileResult.from(profile.getName(), profile.getNickname());
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/gemini 음 @SecondaryTable 로 User Profile 을 구성하는 형태는 어때? 하나의 객체로 여러 table에 매핑되는 형태 DDD 의 구조를 해치지않고 구성할 수 있을 거 같은데
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요, @polyglot-k 님. 좋은 질문 감사합니다.
@SecondaryTable을 사용하여 User와 Profile을 구성하는 방식은 하나의 도메인 객체로 여러 테이블을 매핑하여 DDD 관점에서 응집도를 높일 수 있는 좋은 접근 방식입니다. 특히 Profile이 User의 필수적인 속성으로, 독립적인 생명주기를 가지지 않고 항상 User와 함께 관리되어야 하는 경우에 효과적일 수 있습니다.
하지만 현재 PR의 변경 사항들은 User와 Profile을 별도의 엔티티로 분리하여 각자의 생명주기를 관리하고, User 엔티티가 Profile 엔티티에 대한 직접적인 Cascade 관계를 가지지 않도록 하는 방향으로 진행되고 있습니다. 이는 애그리거트 간의 강결합을 해소하고 각 엔티티의 책임을 명확히 하려는 의도로 보입니다.
이러한 분리된 엔티티 구조에서는 Profile 엔티티가 생성된 후 데이터베이스에 명시적으로 저장되어야 합니다. @SecondaryTable을 사용하게 되면 Profile은 User 엔티티의 일부가 되므로, 현재의 createProfile과 같은 독립적인 Profile 생성 로직은 적합하지 않게 됩니다.
따라서 현재 PR의 설계 방향을 유지한다면, Profile 엔티티를 생성한 후 profileJpaRepository.save(profile);를 통해 명시적으로 저장하는 것이 일관된 접근 방식입니다. 만약 User와 Profile을 @SecondaryTable로 다시 결합하는 것을 고려하신다면, 이는 현재 PR의 큰 설계 변경이 될 수 있으므로, 그에 따른 도메인 모델 및 서비스 로직의 전반적인 재검토가 필요할 것입니다.
src/main/java/com/example/bak/comment/application/command/CommentCommandService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/example/bak/feed/application/command/FeedCommandService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/example/bak/community/infra/command/CommunityValidationAdapter.java
Outdated
Show resolved
Hide resolved
| public ProfileResult createProfile(Long userId, String name, String nickname) { | ||
| User user = userJpaRepository.findById(userId).orElseThrow(() -> new BusinessException( | ||
| ErrorCode.USER_NOT_FOUND)); | ||
| Profile profile = Profile.create(name, nickname); | ||
| user.addProfile(profile); | ||
| profile.assignUser(userId); | ||
| return ProfileResult.from(profile.getName(), profile.getNickname()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/gemini 음 @SecondaryTable 로 User Profile 을 구성하는 형태는 어때? 하나의 객체로 여러 table에 매핑되는 형태 DDD 의 구조를 해치지않고 구성할 수 있을 거 같은데
|
/gemini review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
이 PR은 CQRS 패턴을 적용하여 피드와 댓글 기능의 아키텍처를 개선하는 중요한 변경을 담고 있습니다. 애그리거트 간의 강한 결합을 해소하고, 명확한 책임 분리를 통해 시스템의 유지보수성과 확장성을 높인 점이 인상적입니다. 전반적으로 코드의 구조가 명확해지고 도메인 간의 의존성이 줄어든 것을 확인할 수 있었습니다.
다만, 리뷰 과정에서 몇 가지 잠재적인 문제점을 발견했습니다. 특히 User와 Profile 엔티티 간의 관계 설정 로직에 데이터 불일치를 유발할 수 있는 부분이 있으며, JDBC를 사용한 조회 로직에서 예외 처리와 유지보수성 측면에서 개선이 필요한 부분이 있습니다. 자세한 내용은 각 파일의 리뷰 코멘트를 참고해주세요.
| public ProfileResult createProfile(Long userId, String name, String nickname) { | ||
| User user = userJpaRepository.findById(userId).orElseThrow(() -> new BusinessException( | ||
| ErrorCode.USER_NOT_FOUND)); | ||
| Profile profile = Profile.create(name, nickname); | ||
| user.addProfile(profile); | ||
| profile.assignUser(userId); | ||
| profileJpaRepository.save(profile); | ||
| return ProfileResult.from(profile.getName(), profile.getNickname()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbh010204 고민해보고, Issue 작성해주세요
| Profile profile = repository.queryForObject(sql, params, new ProfileMapper()); | ||
| return Optional.ofNullable(profile); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
findById 메소드에서 repository.queryForObject()를 사용하고 있는데, 이 메소드는 결과가 없을 경우 EmptyResultDataAccessException을 발생시킵니다. 따라서 Optional.ofNullable()로 감싸더라도 Optional.empty()가 반환되지 않고 예외가 발생하여 메소드 시그니처(Optional<Profile>)와 다르게 동작합니다. 이는 findByName, findUserById 메소드에도 동일하게 존재하는 문제입니다.
결과가 없을 때 Optional.empty()를 안전하게 반환하려면 query() 메소드를 사용하는 것이 더 간결하고 안전합니다.
| Profile profile = repository.queryForObject(sql, params, new ProfileMapper()); | |
| return Optional.ofNullable(profile); | |
| List<Profile> profile = repository.query(sql, params, new ProfileMapper()); | |
| return profile.stream().findFirst(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbh010204 고민해보고, Issue 작성해주세요
| public Page<FeedSummary> findAll(Pageable pageable) { | ||
| MapSqlParameterSource params = new MapSqlParameterSource() | ||
| .addValue("limit", pageable.getPageSize()) | ||
| .addValue("offset", pageable.getOffset()); | ||
|
|
||
| String sql = SUMMARY_SELECT + '\n' + SUMMARY_GROUP_BY + '\n' | ||
| + buildOrderByClause(pageable) + '\n' | ||
| + "LIMIT :limit OFFSET :offset"; | ||
|
|
||
| List<FeedSummary> content = jdbc.query(sql, params, feedSummaryMapper); | ||
| long total = countFeeds(); | ||
|
|
||
| return new PageImpl<>(content, pageable, total); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
findAll 메소드에서 페이지네이션을 위한 전체 카운트를 가져오는 countFeeds() 로직이 분리되어 있어 유지보수 시 문제가 발생할 수 있습니다. 현재는 필터링 조건이 없어서 SELECT COUNT(*) FROM feeds가 올바르게 동작하지만, 향후 findAll에 WHERE 절 등 필터링 조건이 추가될 경우 countFeeds()는 이를 반영하지 않아 잘못된 total 값을 반환하게 됩니다. 이는 페이지네이션 오류로 이어질 수 있습니다.
데이터 조회와 카운트 조회 로직이 가능한 한 FROM과 WHERE 절을 공유하도록 구조를 개선하는 것이 좋습니다. 이렇게 하면 필터링 로직이 변경될 때 두 쿼리가 항상 동기화되어 버그를 방지할 수 있습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbh010204 고민해보고, Issue 작성해주세요
polyglot-k
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
부분적으로 오타난 부분 수정하고 올립니다!
나머지 작업은 Issue 로 재개하시면 될 거 같습니다
Warning
이슈와 프로젝트(Project) 연결 시 반드시 주의하세요.
이는 업무 효율도를 위해서 사용하는 것이지 때문에 협조 부탁드립니다. 추가로, 완료 된 PR 이라면 리뷰어를 직접적으로 연결해주세요 :)
📌 연결된 이슈
📖 주요 변경 사항
(src/main/java/com/example/bak/feed/domain/Feed.java).
(src/main/java/com/example/bak/feedcomment/domain/FeedComment.java).
(src/main/java/com/example/bak/feedcomment/application/command/FeedCommentCommandService.java).
🔖 작업 내용 요약
빨간선: 다른 어그리게이트 간 참조
파란선: 다른 Bounded Context 간 참조
😎 질문