Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@jbh010204
Copy link
Member

@jbh010204 jbh010204 commented Dec 3, 2025

Warning

이슈와 프로젝트(Project) 연결 시 반드시 주의하세요.
이는 업무 효율도를 위해서 사용하는 것이지 때문에 협조 부탁드립니다. 추가로, 완료 된 PR 이라면 리뷰어를 직접적으로 연결해주세요 :)

📌 연결된 이슈

📖 주요 변경 사항

변경 된 사항에 대해서 개조식으로 작성해주세요

🔖 작업 내용 요약

작업한 내용에 대해서, 간단하게 요약해주세요

image

빨간선: 다른 어그리게이트 간 참조
파란선: 다른 Bounded Context 간 참조

😎 질문

Bounded Context와 Aggregate 부분 표현에 집중 해서 CQRS까지는 자세하게 포함을 못했습니다.. ㅎ

@jbh010204 jbh010204 self-assigned this Dec 3, 2025
@jbh010204 jbh010204 added the 🚀 IMPROVEMENT 성능 개선 label Dec 3, 2025
Copy link
Member

@BaeJunH0 BaeJunH0 left a comment

Choose a reason for hiding this comment

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

테스트 코드 부분 수정부탁드려용

@jbh010204 jbh010204 requested a review from BaeJunH0 December 9, 2025 09:51
Copy link

@gemini-code-assist gemini-code-assist bot left a 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.

Comment on lines 22 to 26
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());
}

Choose a reason for hiding this comment

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

critical

createProfile 메소드는 Profile 인스턴스를 생성하지만 데이터베이스에 저장하지 않습니다. UserProfile이 분리되면서 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());
}

Copy link
Contributor

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 의 구조를 해치지않고 구성할 수 있을 거 같은데

Choose a reason for hiding this comment

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

안녕하세요, @polyglot-k 님. 좋은 질문 감사합니다.

@SecondaryTable을 사용하여 UserProfile을 구성하는 방식은 하나의 도메인 객체로 여러 테이블을 매핑하여 DDD 관점에서 응집도를 높일 수 있는 좋은 접근 방식입니다. 특히 ProfileUser의 필수적인 속성으로, 독립적인 생명주기를 가지지 않고 항상 User와 함께 관리되어야 하는 경우에 효과적일 수 있습니다.

하지만 현재 PR의 변경 사항들은 UserProfile을 별도의 엔티티로 분리하여 각자의 생명주기를 관리하고, User 엔티티가 Profile 엔티티에 대한 직접적인 Cascade 관계를 가지지 않도록 하는 방향으로 진행되고 있습니다. 이는 애그리거트 간의 강결합을 해소하고 각 엔티티의 책임을 명확히 하려는 의도로 보입니다.

이러한 분리된 엔티티 구조에서는 Profile 엔티티가 생성된 후 데이터베이스에 명시적으로 저장되어야 합니다. @SecondaryTable을 사용하게 되면 ProfileUser 엔티티의 일부가 되므로, 현재의 createProfile과 같은 독립적인 Profile 생성 로직은 적합하지 않게 됩니다.

따라서 현재 PR의 설계 방향을 유지한다면, Profile 엔티티를 생성한 후 profileJpaRepository.save(profile);를 통해 명시적으로 저장하는 것이 일관된 접근 방식입니다. 만약 UserProfile@SecondaryTable로 다시 결합하는 것을 고려하신다면, 이는 현재 PR의 큰 설계 변경이 될 수 있으므로, 그에 따른 도메인 모델 및 서비스 로직의 전반적인 재검토가 필요할 것입니다.

Comment on lines 22 to 26
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());
}
Copy link
Contributor

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 의 구조를 해치지않고 구성할 수 있을 거 같은데

@polyglot-k
Copy link
Contributor

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a 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 패턴을 적용하여 피드와 댓글 기능의 아키텍처를 개선하는 중요한 변경을 담고 있습니다. 애그리거트 간의 강한 결합을 해소하고, 명확한 책임 분리를 통해 시스템의 유지보수성과 확장성을 높인 점이 인상적입니다. 전반적으로 코드의 구조가 명확해지고 도메인 간의 의존성이 줄어든 것을 확인할 수 있었습니다.

다만, 리뷰 과정에서 몇 가지 잠재적인 문제점을 발견했습니다. 특히 UserProfile 엔티티 간의 관계 설정 로직에 데이터 불일치를 유발할 수 있는 부분이 있으며, JDBC를 사용한 조회 로직에서 예외 처리와 유지보수성 측면에서 개선이 필요한 부분이 있습니다. 자세한 내용은 각 파일의 리뷰 코멘트를 참고해주세요.

Comment on lines 23 to 28
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());
}

Choose a reason for hiding this comment

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

critical

프로필 생성 시 User 엔티티에 profileId가 업데이트되지 않아 데이터 불일치가 발생할 수 있습니다. 현재 코드는 Profile 객체에 userId를 할당하고 저장하지만, User 객체의 profileId 필드는 갱신하지 않습니다. 이로 인해 UserProfile 간의 관계가 완전히 설정되지 않아 user.profileIdnull로 남게 됩니다.

User 엔티티를 로드하고, 생성된 Profile의 ID를 User에 설정한 후 영속성 컨텍스트에 의해 변경이 감지되도록 로직을 수정해야 합니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

@jbh010204 고민해보고, Issue 작성해주세요

Comment on lines +31 to +32
Profile profile = repository.queryForObject(sql, params, new ProfileMapper());
return Optional.ofNullable(profile);

Choose a reason for hiding this comment

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

critical

findById 메소드에서 repository.queryForObject()를 사용하고 있는데, 이 메소드는 결과가 없을 경우 EmptyResultDataAccessException을 발생시킵니다. 따라서 Optional.ofNullable()로 감싸더라도 Optional.empty()가 반환되지 않고 예외가 발생하여 메소드 시그니처(Optional<Profile>)와 다르게 동작합니다. 이는 findByName, findUserById 메소드에도 동일하게 존재하는 문제입니다.

결과가 없을 때 Optional.empty()를 안전하게 반환하려면 query() 메소드를 사용하는 것이 더 간결하고 안전합니다.

Suggested change
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();

Copy link
Contributor

Choose a reason for hiding this comment

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

@jbh010204 고민해보고, Issue 작성해주세요

Comment on lines +69 to +82
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);
}

Choose a reason for hiding this comment

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

high

findAll 메소드에서 페이지네이션을 위한 전체 카운트를 가져오는 countFeeds() 로직이 분리되어 있어 유지보수 시 문제가 발생할 수 있습니다. 현재는 필터링 조건이 없어서 SELECT COUNT(*) FROM feeds가 올바르게 동작하지만, 향후 findAllWHERE 절 등 필터링 조건이 추가될 경우 countFeeds()는 이를 반영하지 않아 잘못된 total 값을 반환하게 됩니다. 이는 페이지네이션 오류로 이어질 수 있습니다.

데이터 조회와 카운트 조회 로직이 가능한 한 FROMWHERE 절을 공유하도록 구조를 개선하는 것이 좋습니다. 이렇게 하면 필터링 로직이 변경될 때 두 쿼리가 항상 동기화되어 버그를 방지할 수 있습니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

@jbh010204 고민해보고, Issue 작성해주세요

Copy link
Contributor

@polyglot-k polyglot-k left a comment

Choose a reason for hiding this comment

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

부분적으로 오타난 부분 수정하고 올립니다!

나머지 작업은 Issue 로 재개하시면 될 거 같습니다

@polyglot-k polyglot-k merged commit 344ce1d into develop Dec 14, 2025
2 checks passed
@polyglot-k polyglot-k deleted the feat/#35 branch December 14, 2025 05:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🚀 IMPROVEMENT 성능 개선

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[📌 Feature] Feed 계열(FeedComment 포함) CQRS 적용

4 participants