CQRS 설계 - KimGyuBek/Threadly GitHub Wiki
Command (상태 변경) Query (상태 조회)
↓ ↓
CommandUseCase QueryUseCase
↓ ↓
CommandPort QueryPort
↓ ↓
CommandAdapter QueryAdapter
- CQRS(CommandQuery Responsibility Segregation) 패턴을 적용하여 상태 변경(
Command)과 상태 조회(Query)를 명확히 분리 - 각 도메인마다
Command와Query를 별도 계층으로 정의하여 책임을 구분하고, 구조적 일관성 확보 - 책임 분리, 확장성 확보

| 구분 | Command | Query |
|---|---|---|
| 목적 | 상태 변경 | 상태 조회 |
| 반환 타입 | void, Response DTO | 데이터 객체, Projection |
| 트랜잭션 | @Transactional | @Transactional(readOnly = true) |
PostCommandUseCase PostQueryUseCase
↓ ↓
createPost() getPost()
updatePost() getUserVisiblePostListByCursor()
softDeletePost() getPostEngagement()
... ...
- 목적: 비즈니스 유즈케이스 정의
-
특징:
Command/Query책임 명확히 분리
PostCommandPort PostQueryPort
↓ ↓
savePost() fetchById()
updatePost() fetchPostDetailsByPostIdAndUserId()
changeStatus() fetchUserVisiblePostListByCursor()
increaseViewCount() existsById()
... ...
- 목적: 외부 인프라 추상화
- 특징: 기술 중립적
각 하위 도메인도 Command/Query 분리:
-
PostLike:
PostLikeCommandUseCase/PostLikeQueryUseCase -
PostComment:
PostCommentCommandUseCase/PostCommendQueryUseCase -
PostCommentLike:
PostCommentLikeQueryUseCase/ `PostCommentLikeCommandUseCase'
쓰기(Command)와 읽기(Query)를 분리함으로써 각 계층이 담당하는 역할이 명확해지고 코드 복잡성이 줄어든다.
조회 요구사항 변경 시 쓰기 로직에 영향을 주지 않으며 반대의 경우도 동일하다. 따라서 유지 보수가 용이해진다.
Command는 상태 변경 검증에 집중하고, Query는 데이터 조회 결과 검증에 집중하여 테스트 가능하다.
현재는 Command, Query 같은 DB를 사용지만 추후 전용 DB 분리로 성능 최적화 및 확장이 용이하다.
Follow 기능
follow
├─ in
│ ├─ command
│ │ ├─ FollowCommandUseCase.java
│ │ └─ dto
│ └─ query
│ ├─ FollowQueryUseCase.java
│ └─ dto
└─ out
├─ FollowCommandPort.java
├─ FollowQueryPort.java
└─ projection
public interface FollowCommandUseCase {
FollowUserApiResponse followUser(FollowUserCommand command);
// ...
}
public interface FollowQueryUseCase {
CursorPageApiResponse getFollowRequestsByCursor(GetFollowRequestsQuery query);
// ...
}@Service
public class FollowCommandService implements FollowCommandUseCase {
@Transactional
@Override
public FollowUserApiResponse followUser(FollowUserCommand command) {...}
// ...
}
@Service
public class FollowQueryService implements FollowQueryUseCase {
CursorPageApiResponse getFollowRequestsByCursor(GetFollowRequestsQuery query){...}
// ...
}public record FollowRelationCommand(...) {...}
public record GetFollowersQuery(...) {...}public interface FollowCommandPort {
void createFollow(Follow follow);
// ...
}
public interface FollowQueryPort {
boolean isFollowing(String followerId, String followingId);
// ...
}@Repository
public class FollowPersistenceAdapter implements FollowCommandPort, FollowQueryPort {
private final FollowJpaRepository followJpaRepository;
@Override
public void createFollow(Follow follow) {...}
@Override
public boolean isFollowing(String followerId, String followingId) {...}
// ...
}public interface FollowerProjection {
String getFollowerId();
// ...
}- 예시:
LoginUserUseCase,DeActivateUserUserCase,WithdrawUserUseCase등 - 장점: 의도 명확, 파일 네임으로 "무슨 일을 하는지" 바로 보임
- 한계: 액션이 늘어날 수록 인터페이스가 증가
- 예시:
FetchUserUseCase,UpdateUserUseCase,SaveUserUsecase등 - 장점: CRUD 기준으로 묶어서 파일 수를 줄일 수 있어서 구조가 깔끔해짐
- 한계:
- 의미가 뭉개짐:
fetch안에 상세 조회/목록/통계 등 이질적 쿼리가 섞임 - 쓰기 로직과 조회 로직이 같은 서비스에 들어와서 책임 분리 실패
- 결과적으로 코드가 다시 방대해지고 응집도가 떨어짐
- 의미가 뭉개짐:
- 예시:
UserQueryUseCase,UserCommandUseCase등 - 장점:
- 읽기, 쓰기 책임을 명확히 분리해 응집도 상승
- 트랜잭션 관리 최적화:
Command는@Transactional,Query는@Transactional(readOnly = true) - 도메인 단위로 일관된 구조를 가지게 되어 확장 시에도 일관성 유지 가능
-
Command/Query를 별도의 단위로 테스트 가능해 검증이 단순해짐
- 트랜잭션 관리 최적화:
- 읽기, 쓰기 책임을 명확히 분리해 응집도 상승
- 현재는
Command와Query가 동일한 DB를 사용하여 강한 일관성을 보장하고 있음 - 추후 시스템 규모가 커지면 읽기/쓰기 저장소 분리를 고려할 수 있다.