Repository: KUIT-Space/KUIT_Space_BackEnd
Pull request #362: Refactor/#356/게시글 상세조회
Author: login: seongjunnoh
Created: 2025-03-23T16:15:05Z | Merged: 2025-03-25T05:06:24Z

## 📝 요약
1. 게시글 상세조회 api 리펙토링
2. 좋아요 on/off api 버그 fix

<게시글 상세 조회의 response 설명>
- 게시글이 비익명 게시글인 경우 -> 게시글 작성자의 실제 닉네임, 프로필 이미지 url 반환
- 게시글이 익명 게시글(= 익명 질문)인 경우 -> 게시글 작성자의 닉네임을 "익명 스페이서" & 프로필 이미지는 null 로 반환
- 댓글이 비익명인 경우 -> 댓글 작성자의 실제 닉네임, 프로필 이미지 url 반환
- 댓글이 익명인 경우 -> 댓글 작성자의 닉네임을 "익명 스페이서 n"(n은 익명 댓글의 순서) & 프로필 이미지는 null로 반환
- 댓글이 삭제된 댓글인 경우 -> 댓글 작성자의 닉네임과 프로필 이미지를 null & 댓글의 내용을 "삭제된 댓글입니다." 로 반환

이슈 번호 : #356 

## 🔖 변경 사항
<게시글 상세조회 api 리펙토링>
1. 해당 게시글의 익명 댓글 작성자 닉네임을 "익명 스페이서 1", "익명 스페이서 2", ,,, 이런 식으로 변경 
기존에는 익명 댓글의 작성자 닉네임을 "익명 스페이서 + postId * 10 + spaceMemberId" 이렇게 설정하였는데, 이걸 에브리타임의 익명 댓글처럼 보이도록 수정하였습니다

2. CQRS 패턴 적용
기존 게시글 상세조회 api의 서비스 클래스는 response를 구성하기 위해 필요한 모든 도메인 엔티티들을 load한 후, 이것들을 가공해서 controller로 적절한 DTO를 반환하는 형식이었습니다.
다만 도메인 엔티티들을 모두 load하는 과정에서 DB 조회가 너무 많이 발생한다고 생각해서 도메인 엔티티와 분리된 Query 전용 adapter와 모델을 추가하여 api 작동 시 DB 조회를 최소화 하도록 코드를 리펙토링 하였습니다.
참고 : https://1715leekc.tistory.com/240

3. QueryDsl 적용
1처럼 리펙토링을 진행하려고 하니 복잡한 쿼리문과 동적 쿼리문을 jpql만으로 작성하는 것에 한계를 느껴 QueryDsl을 적용해서 PostDetailQueryAdapter, CommentDetailQueryAdapter를 구성하였습니다.

<좋아요 on/off api 버그 fix>
좋아요 on/off api의 테스트 도중 off request가 제대로 동작하지 않는 것을 확인했습니다.
원인은 해당 api의 서비스 클래스에서 LikePersistenceAdapter 의 메서드를 호출할때, 인자값들의 순서를 잘못 맞춰서 주입했기 때문이었습니다.


## ✅ 리뷰 요구사항

## 📸 확인 방법 (선택)
<img width="1613" alt="image" src="https://github.com/user-attachments/assets/216ef778-afc4-46ca-8d8c-cf752c716af4" />
<img width="1399" alt="image" src="https://github.com/user-attachments/assets/46d3a2f0-2789-4fc7-a05f-876a149d9e1f" />

일단 로컬 서버에서 게시글 생성, 익명/비익명 질문 생성, 익명/비익명 댓글 생성, 댓글 삭제, 좋아요 on/off 등 여러 작업을 해봤을때는 제대로 동작한다는 것을 확인했습니다.
dev 서버로 배포후에 여러 명이서 특정 게시글에 각자 request를 날렸을떄에도 잘 동작하는지 확인해보면 될 거 같습니다.

<br/>

---

### 📌 PR 진행 시 이러한 점들을 참고해 주세요

    * P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
    * P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
    * P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)



Diff:
diff --git a/src/main/java/space/space_spring/domain/post/adapter/in/web/readPostDetail/ResponseOfCommentDetail.java b/src/main/java/space/space_spring/domain/post/adapter/in/web/readPostDetail/ResponseOfCommentDetail.java
index 668bc2e2..6c33838e 100644
--- a/src/main/java/space/space_spring/domain/post/adapter/in/web/readPostDetail/ResponseOfCommentDetail.java
+++ b/src/main/java/space/space_spring/domain/post/adapter/in/web/readPostDetail/ResponseOfCommentDetail.java
@@ -11,7 +11,7 @@ public class ResponseOfCommentDetail {
 
     private String creatorProfileImageUrl;
 
-    private boolean isPostOwner;
+    private Boolean isPostOwner;
 
     private String content;         // 댓글 내용은 String (Content X)
 
@@ -21,13 +21,13 @@ public class ResponseOfCommentDetail {
 
     private int likeCount;
 
-    private boolean isLiked;
+    private Boolean isLiked;
 
-    private boolean isActiveComment;
+    private Boolean isActiveComment;
 
     @Builder
-    private ResponseOfCommentDetail(String creatorName, String creatorProfileImageUrl, boolean isPostOwner, String content, String createdAt,
-                                    String lastModifiedAt, int likeCount, boolean isLiked, boolean isActiveComment) {
+    private ResponseOfCommentDetail(String creatorName, String creatorProfileImageUrl, Boolean isPostOwner, String content, String createdAt,
+                                    String lastModifiedAt, int likeCount, Boolean isLiked, Boolean isActiveComment) {
         this.creatorName = creatorName;
         this.creatorProfileImageUrl = creatorProfileImageUrl;
         this.isPostOwner = isPostOwner;
@@ -43,13 +43,13 @@ public static ResponseOfCommentDetail of(InfoOfCommentDetail info) {
         return ResponseOfCommentDetail.builder()
                 .creatorName(info.getCreatorName())
                 .creatorProfileImageUrl(info.getCreatorProfileImageUrl())
-                .isPostOwner(info.isPostOwner())
+                .isPostOwner(info.getIsPostOwner())
                 .content(info.getContent())
                 .createdAt(info.getCreatedAt())
                 .lastModifiedAt(info.getLastModifiedAt())
-                .likeCount(info.getLikeCount().getNumber())
-                .isLiked(info.isLiked())
-                .isActiveComment(info.isActiveComment())
+                .likeCount(info.getLikeCount())
+                .isLiked(info.getIsLiked())
+                .isActiveComment(info.getIsActiveComment())
                 .build();
     }
 
diff --git a/src/main/java/space/space_spring/domain/post/adapter/in/web/readPostDetail/ResponseOfReadPostDetail.java b/src/main/java/space/space_spring/domain/post/adapter/in/web/readPostDetail/ResponseOfReadPostDetail.java
index 8830ceaa..80ebd736 100644
--- a/src/main/java/space/space_spring/domain/post/adapter/in/web/readPostDetail/ResponseOfReadPostDetail.java
+++ b/src/main/java/space/space_spring/domain/post/adapter/in/web/readPostDetail/ResponseOfReadPostDetail.java
@@ -25,13 +25,13 @@ public class ResponseOfReadPostDetail {
 
     private int likeCount;
 
-    private boolean isLiked;
+    private Boolean isLiked;
 
     private List<ResponseOfCommentDetail> responseOfCommentDetails;
 
     @Builder
     private ResponseOfReadPostDetail(String creatorName, String creatorProfileImageUrl, String createdAt, String lastModifiedAt,
-                                     String title, String content, List<String> attachmentUrls, int likeCount, boolean isLiked,
+                                     String title, String content, List<String> attachmentUrls, int likeCount, Boolean isLiked,
                                      List<ResponseOfCommentDetail> responseOfCommentDetails) {
         this.creatorName = creatorName;
         this.creatorProfileImageUrl = creatorProfileImageUrl;
@@ -52,10 +52,10 @@ public static ResponseOfReadPostDetail of(ResultOfReadPostDetail result) {
                 .createdAt(result.getCreatedAt())
                 .lastModifiedAt(result.getLastModifiedAt())
                 .title(result.getTitle())
-                .content(result.getContent().getValue())
+                .content(result.getContent())
                 .attachmentUrls(result.getAttachmentUrls())
-                .likeCount(result.getLikeCount().getNumber())
-                .isLiked(result.isLiked())
+                .likeCount(result.getLikeCount())
+                .isLiked(result.getIsLiked())
                 .responseOfCommentDetails(
                         result.getInfoOfCommentDetails().stream()
                                 .map(ResponseOfCommentDetail::of)
diff --git a/src/main/java/space/space_spring/domain/post/adapter/out/persistence/board/BoardPersistenceAdapter.java b/src/main/java/space/space_spring/domain/post/adapter/out/persistence/board/BoardPersistenceAdapter.java
index 9ec73831..2c3e392f 100644
--- a/src/main/java/space/space_spring/domain/post/adapter/out/persistence/board/BoardPersistenceAdapter.java
+++ b/src/main/java/space/space_spring/domain/post/adapter/out/persistence/board/BoardPersistenceAdapter.java
@@ -35,6 +35,7 @@ public Long createBoard(Board board) {
         BoardJpaEntity boardJpaEntity = boardMapper.toJpaEntity(spaceJpaEntity, board);
         return boardRepository.save(boardJpaEntity).getId();
     }
+
     @Override
     public Optional<Board> load(Long boardId){
         return boardRepository.findById(boardId).map(boardMapper::toDomainEntity);
diff --git a/src/main/java/space/space_spring/domain/post/adapter/out/persistence/comment/CommentQueryAdapter.java b/src/main/java/space/space_spring/domain/post/adapter/out/persistence/comment/CommentQueryAdapter.java
new file mode 100644
index 00000000..3d3b3761
--- /dev/null
+++ b/src/main/java/space/space_spring/domain/post/adapter/out/persistence/comment/CommentQueryAdapter.java
@@ -0,0 +1,80 @@
+package space.space_spring.domain.post.adapter.out.persistence.comment;
+
+import com.querydsl.core.types.Projections;
+import com.querydsl.core.types.dsl.Expressions;
+import com.querydsl.jpa.JPAExpressions;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+import space.space_spring.domain.post.adapter.out.persistence.like.QLikeJpaEntity;
+import space.space_spring.domain.post.adapter.out.persistence.post.QPostJpaEntity;
+import space.space_spring.domain.post.adapter.out.persistence.postBase.QPostBaseJpaEntity;
+import space.space_spring.domain.post.application.port.out.comment.CommentDetailQueryPort;
+import space.space_spring.domain.post.application.port.out.comment.CommentDetailView;
+import space.space_spring.domain.spaceMember.domian.QSpaceMemberJpaEntity;
+import space.space_spring.global.common.enumStatus.BaseStatusType;
+
+import java.util.List;
+
+@Repository
+@RequiredArgsConstructor
+public class CommentQueryAdapter implements CommentDetailQueryPort {
+
+    private final JPAQueryFactory queryFactory;
+
+    @Override
+    public List<CommentDetailView> loadCommentDetail(Long postId, Long spaceMemberId) {
+        QPostCommentJpaEntity comment = QPostCommentJpaEntity.postCommentJpaEntity;
+        QSpaceMemberJpaEntity commentCreator = QSpaceMemberJpaEntity.spaceMemberJpaEntity;
+        QPostJpaEntity parentPost = QPostJpaEntity.postJpaEntity;
+        QLikeJpaEntity commentLike = QLikeJpaEntity.likeJpaEntity;
+        QPostBaseJpaEntity commentBase = QPostBaseJpaEntity.postBaseJpaEntity;
+
+        return queryFactory.select(
+                        Projections.constructor(CommentDetailView.class,
+                                // 댓글 작성자 id
+                                commentBase.spaceMember.id,
+                                // 댓글 작성자 이름
+                                commentBase.spaceMember.nickname,
+                                // 댓글 작성자 프로필 이미지 URL
+                                commentBase.spaceMember.profileImageUrl,
+                                // 게시글 작성자와 댓글 작성자 비교
+                                Expressions.booleanTemplate(
+                                        "({0} = {1})",
+                                        commentBase.spaceMember.id, parentPost.postBase.spaceMember.id),
+                                // 댓글 내용
+                                commentBase.content,
+                                // 댓글 생성일 및 수정일
+                                commentBase.createdAt,
+                                commentBase.lastModifiedAt,
+                                // 댓글 좋아요 개수 -> 서브쿼리로 집계
+                                JPAExpressions.select(commentLike.id.count())
+                                        .from(commentLike)
+                                        .where(commentLike.postBase.id.eq(comment.id)
+                                                .and(commentLike.isLiked.eq(true))
+                                                .and(commentLike.status.eq(BaseStatusType.ACTIVE))),
+                                // 스페이스 멤버가 해당 댓글을 좋아하는지
+                                JPAExpressions.selectOne()
+                                        .from(commentLike)
+                                        .where(commentLike.postBase.id.eq(comment.id)
+                                                .and(commentLike.spaceMember.id.eq(spaceMemberId))
+                                                .and(commentLike.isLiked.eq(true))
+                                                .and(commentLike.status.eq(BaseStatusType.ACTIVE)))
+                                        .exists(),
+                                // 댓글의 상태 -> 삭제된 댓글인지 아닌지
+                                Expressions.booleanTemplate(
+                                        "CASE WHEN {0} = {1} THEN true ELSE false END",
+                                        commentBase.status, Expressions.constant(BaseStatusType.ACTIVE)),
+                                // 익명 댓글 여부
+                                comment.isAnonymous
+                        )
+                )
+                .from(comment)
+                .join(comment.postBase, commentBase)
+                .join(comment.post, parentPost)
+                .join(commentBase.spaceMember, commentCreator)
+                .where(parentPost.id.eq(postId))
+                .orderBy(commentBase.createdAt.asc())
+                .fetch();
+    }
+}
diff --git a/src/main/java/space/space_spring/domain/post/adapter/out/persistence/like/LikePersistenceAdapter.java b/src/main/java/space/space_spring/domain/post/adapter/out/persistence/like/LikePersistenceAdapter.java
index 23bd2885..3d1021b1 100644
--- a/src/main/java/space/space_spring/domain/post/adapter/out/persistence/like/LikePersistenceAdapter.java
+++ b/src/main/java/space/space_spring/domain/post/adapter/out/persistence/like/LikePersistenceAdapter.java
@@ -59,7 +59,7 @@ public boolean hasSpaceMemberLiked(Long spaceMemberId, Long targetId) {
     }
 
     @Override
-    public void changeLikeState(Long spaceMemberId, Long targetId, boolean changeTo) {
+    public void changeLikeState(Long targetId, Long spaceMemberId, boolean changeTo) {
         LikeJpaEntity likeJpaEntity = likeRepository.findBySpaceMemberIdAndPostBaseIdAndStatus(spaceMemberId, targetId, BaseStatusType.ACTIVE)
                 .orElseThrow(() -> new CustomException(LIKE_NOT_FOUND));
 
diff --git a/src/main/java/space/space_spring/domain/post/adapter/out/persistence/post/PostQueryAdapter.java b/src/main/java/space/space_spring/domain/post/adapter/out/persistence/post/PostQueryAdapter.java
new file mode 100644
index 00000000..98a5151c
--- /dev/null
+++ b/src/main/java/space/space_spring/domain/post/adapter/out/persistence/post/PostQueryAdapter.java
@@ -0,0 +1,111 @@
+package space.space_spring.domain.post.adapter.out.persistence.post;
+
+import com.querydsl.core.group.GroupBy;
+import com.querydsl.core.types.Projections;
+import com.querydsl.core.types.dsl.Expressions;
+import com.querydsl.jpa.JPAExpressions;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+import space.space_spring.domain.post.adapter.out.persistence.attachment.QAttachmentJpaEntity;
+import space.space_spring.domain.post.adapter.out.persistence.like.QLikeJpaEntity;
+import space.space_spring.domain.post.adapter.out.persistence.postBase.QPostBaseJpaEntity;
+import space.space_spring.domain.post.application.port.out.post.PostDetailQueryPort;
+import space.space_spring.domain.post.application.port.out.post.PostDetailView;
+import space.space_spring.domain.spaceMember.domian.QSpaceMemberJpaEntity;
+import space.space_spring.global.common.enumStatus.BaseStatusType;
+import space.space_spring.global.exception.CustomException;
+
+import java.util.List;
+
+import static space.space_spring.global.common.response.status.BaseExceptionResponseStatus.POST_NOT_FOUND;
+
+@Repository
+@RequiredArgsConstructor
+public class PostQueryAdapter implements PostDetailQueryPort {
+
+    private static final String ANONYMOUS_POST_CREATOR_NICKNAME = "익명 스페이서";
+    private final JPAQueryFactory queryFactory;
+
+    /**
+     * 게시글 기본 정보 조회 쿼리 1번
+     * 게시글의 첨부파일 조회 쿼리 1번
+     * -> 총 2번의 쿼리로 구성
+     * -> 한번의 쿼리로 갈 수 있었으나 hibernate 버전과 queryDsl의 버전이 맞지않아 에러가 발생해서 일단 쿼리 2번으로 분리하였음
+     */
+    @Override
+    public PostDetailView loadPostDetail(Long postId, Long spaceMemberId) {
+        QPostJpaEntity post = QPostJpaEntity.postJpaEntity;
+        QPostBaseJpaEntity postBase = QPostBaseJpaEntity.postBaseJpaEntity;
+        QSpaceMemberJpaEntity postCreator = QSpaceMemberJpaEntity.spaceMemberJpaEntity;
+        QAttachmentJpaEntity attachment = QAttachmentJpaEntity.attachmentJpaEntity;
+        QLikeJpaEntity postLike = QLikeJpaEntity.likeJpaEntity;
+
+        // 1. 게시글 기본 정보를 조회 (첨부파일은 제외)
+        PostDetailView detail = queryFactory
+                .select(Projections.constructor(PostDetailView.class,
+                        // 게시글 작성자 이름: 익명 여부에 따라 결정
+                        Expressions.stringTemplate(
+                                "CASE WHEN {0} = true THEN {1} ELSE {2} END",
+                                post.isAnonymous,
+                                Expressions.constant(ANONYMOUS_POST_CREATOR_NICKNAME),
+                                postCreator.nickname),
+                        // 게시글 작성자 프로필 이미지 URL
+                        postCreator.profileImageUrl,
+                        // 게시글 생성 시각
+                        postBase.createdAt,
+                        // 게시글 수정 시각
+                        postBase.lastModifiedAt,
+                        // 제목
+                        post.title,
+                        // 내용
+                        postBase.content,
+                        // 첨부파일 url list -> 빈 리스트
+                        Expressions.constant(java.util.Collections.emptyList()),
+                        // 좋아요 수 -> 서브쿼리로 집계
+                        JPAExpressions.select(postLike.id.count())
+                                .from(postLike)
+                                .where(postLike.postBase.id.eq(postBase.id)
+                                        .and(postLike.isLiked.eq(true))
+                                        .and(postLike.status.eq(BaseStatusType.ACTIVE))),
+                        // 스페이스 멤버가 해당 게시글을 좋아하는지
+                        JPAExpressions.selectOne()
+                                .from(postLike)
+                                .where(postLike.postBase.id.eq(post.id)
+                                        .and(postLike.spaceMember.id.eq(spaceMemberId))
+                                        .and(postLike.isLiked.eq(true))
+                                        .and(postLike.status.eq(BaseStatusType.ACTIVE)))
+                                .exists()
+                ))
+                .from(post)
+                .leftJoin(post.postBase, postBase)
+                .leftJoin(postBase.spaceMember, postCreator)
+                .where(post.id.eq(postId)
+                        .and(postBase.status.eq(BaseStatusType.ACTIVE)))
+                .fetchOne();
+
+        if (detail == null) {
+            throw new CustomException(POST_NOT_FOUND);
+        }
+
+        // 2. 첨부파일 URL 리스트를 별도로 조회
+        List<String> attachmentUrls = queryFactory
+                .select(attachment.attachmentUrl)
+                .from(attachment)
+                .where(attachment.postBase.id.eq(postId))
+                .fetch();
+
+        // 3. return
+        return PostDetailView.builder()
+                .creatorName(detail.getCreatorName())
+                .creatorProfileImageUrl(detail.getCreatorProfileImageUrl())
+                .createdAt(detail.getCreatedAt())
+                .lastModifiedAt(detail.getLastModifiedAt())
+                .title(detail.getTitle())
+                .content(detail.getContent())
+                .attachmentUrls(attachmentUrls)
+                .likeCount(detail.getLikeCount())
+                .isLiked(detail.getIsLiked())
+                .build();
+    }
+}
diff --git a/src/main/java/space/space_spring/domain/post/application/port/in/readPostDetail/InfoOfCommentDetail.java b/src/main/java/space/space_spring/domain/post/application/port/in/readPostDetail/InfoOfCommentDetail.java
index 9e95ab2c..3b0597ca 100644
--- a/src/main/java/space/space_spring/domain/post/application/port/in/readPostDetail/InfoOfCommentDetail.java
+++ b/src/main/java/space/space_spring/domain/post/application/port/in/readPostDetail/InfoOfCommentDetail.java
@@ -5,13 +5,14 @@
 import space.space_spring.global.util.NaturalNumber;
 
 @Getter
+@Builder
 public class InfoOfCommentDetail {
 
     private String creatorName;
 
     private String creatorProfileImageUrl;
 
-    private boolean isPostOwner;
+    private Boolean isPostOwner;
 
     private String content;
 
@@ -19,23 +20,9 @@ public class InfoOfCommentDetail {
 
     private String lastModifiedAt;
 
-    private NaturalNumber likeCount;
+    private int likeCount;
 
-    private boolean isLiked;
-
-    private boolean isActiveComment;
-
-    @Builder
-    public InfoOfCommentDetail(String creatorName, String creatorProfileImageUrl, boolean isPostOwner, String content, String createdAt, String lastModifiedAt, NaturalNumber likeCount, boolean isLiked, boolean isActiveComment) {
-        this.creatorName = creatorName;
-        this.creatorProfileImageUrl = creatorProfileImageUrl;
-        this.isPostOwner = isPostOwner;
-        this.content = content;
-        this.createdAt = createdAt;
-        this.lastModifiedAt = lastModifiedAt;
-        this.likeCount = likeCount;
-        this.isLiked = isLiked;
-        this.isActiveComment = isActiveComment;
-    }
+    private Boolean isLiked;
 
+    private Boolean isActiveComment;
 }
diff --git a/src/main/java/space/space_spring/domain/post/application/port/in/readPostDetail/ResultOfReadPostDetail.java b/src/main/java/space/space_spring/domain/post/application/port/in/readPostDetail/ResultOfReadPostDetail.java
index ce4562db..9c837133 100644
--- a/src/main/java/space/space_spring/domain/post/application/port/in/readPostDetail/ResultOfReadPostDetail.java
+++ b/src/main/java/space/space_spring/domain/post/application/port/in/readPostDetail/ResultOfReadPostDetail.java
@@ -8,6 +8,7 @@
 import java.util.List;
 
 @Getter
+@Builder
 public class ResultOfReadPostDetail {
 
     private String creatorName;
@@ -20,28 +21,13 @@ public class ResultOfReadPostDetail {
 
     private String title;
 
-    private Content content;
+    private String content;
 
     private List<String> attachmentUrls;
 
-    private NaturalNumber likeCount;
+    private int likeCount;
 
-    private boolean isLiked;
+    private Boolean isLiked;
 
     private List<InfoOfCommentDetail> infoOfCommentDetails;
-
-    @Builder
-    public ResultOfReadPostDetail(String creatorName, String creatorProfileImageUrl, String createdAt, String lastModifiedAt, String title, Content content,
-                                  List<String> attachmentUrls, NaturalNumber likeCount, boolean isLiked, List<InfoOfCommentDetail> infoOfCommentDetails) {
-        this.creatorName = creatorName;
-        this.creatorProfileImageUrl = creatorProfileImageUrl;
-        this.createdAt = createdAt;
-        this.lastModifiedAt = lastModifiedAt;
-        this.title = title;
-        this.content = content;
-        this.attachmentUrls = attachmentUrls;
-        this.likeCount = likeCount;
-        this.isLiked = isLiked;
-        this.infoOfCommentDetails = infoOfCommentDetails;
-    }
 }
diff --git a/src/main/java/space/space_spring/domain/post/application/port/out/comment/CommentDetailQueryPort.java b/src/main/java/space/space_spring/domain/post/application/port/out/comment/CommentDetailQueryPort.java
new file mode 100644
index 00000000..036f116f
--- /dev/null
+++ b/src/main/java/space/space_spring/domain/post/application/port/out/comment/CommentDetailQueryPort.java
@@ -0,0 +1,8 @@
+package space.space_spring.domain.post.application.port.out.comment;
+
+import java.util.List;
+
+public interface CommentDetailQueryPort {
+
+    List<CommentDetailView> loadCommentDetail(Long postId, Long spaceMemberId);
+}
diff --git a/src/main/java/space/space_spring/domain/post/application/port/out/comment/CommentDetailView.java b/src/main/java/space/space_spring/domain/post/application/port/out/comment/CommentDetailView.java
new file mode 100644
index 00000000..eeaef99f
--- /dev/null
+++ b/src/main/java/space/space_spring/domain/post/application/port/out/comment/CommentDetailView.java
@@ -0,0 +1,51 @@
+package space.space_spring.domain.post.application.port.out.comment;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+public class CommentDetailView {
+
+    private Long creatorId;     // 댓글 작성자의 spaceMemberId
+
+    private String creatorName;
+
+    private String creatorProfileImageUrl;
+
+    private Boolean isPostOwner;
+
+    private String content;
+
+    private LocalDateTime createdAt;
+
+    private LocalDateTime lastModifiedAt;
+
+    private Long likeCount;
+
+    private Boolean isLiked;
+
+    private Boolean isActiveComment;
+
+    private Boolean isAnonymousComment;
+
+    // Querydsl에서 사용되는 생성자
+    public CommentDetailView(Long creatorId, String creatorName, String creatorProfileImageUrl,
+                             Boolean isPostOwner, String content,
+                             LocalDateTime createdAt, LocalDateTime lastModifiedAt,
+                             Long likeCount, Boolean isLiked, Boolean isActiveComment, Boolean isAnonymousComment) {
+        this.creatorId = creatorId;
+        this.creatorName = creatorName;
+        this.creatorProfileImageUrl = creatorProfileImageUrl;
+        this.isPostOwner = isPostOwner;
+        this.content = content;
+        this.createdAt = createdAt;
+        this.lastModifiedAt = lastModifiedAt;
+        this.likeCount = likeCount;
+        this.isLiked = isLiked;
+        this.isActiveComment = isActiveComment;
+        this.isAnonymousComment = isAnonymousComment;
+    }
+}
diff --git a/src/main/java/space/space_spring/domain/post/application/port/out/like/ChangeLikeStatePort.java b/src/main/java/space/space_spring/domain/post/application/port/out/like/ChangeLikeStatePort.java
index 6377da04..8c62e3f1 100644
--- a/src/main/java/space/space_spring/domain/post/application/port/out/like/ChangeLikeStatePort.java
+++ b/src/main/java/space/space_spring/domain/post/application/port/out/like/ChangeLikeStatePort.java
@@ -1,7 +1,5 @@
 package space.space_spring.domain.post.application.port.out.like;
 
-import space.space_spring.domain.post.domain.Like;
-
 public interface ChangeLikeStatePort {
 
     void changeLikeState(Long targetId, Long spaceMemberId, boolean changeTo);
diff --git a/src/main/java/space/space_spring/domain/post/application/port/out/post/PostDetailQueryPort.java b/src/main/java/space/space_spring/domain/post/application/port/out/post/PostDetailQueryPort.java
new file mode 100644
index 00000000..d412e357
--- /dev/null
+++ b/src/main/java/space/space_spring/domain/post/application/port/out/post/PostDetailQueryPort.java
@@ -0,0 +1,6 @@
+package space.space_spring.domain.post.application.port.out.post;
+
+public interface PostDetailQueryPort {
+
+    PostDetailView loadPostDetail(Long postId, Long spaceMemberId);
+}
diff --git a/src/main/java/space/space_spring/domain/post/application/port/out/post/PostDetailView.java b/src/main/java/space/space_spring/domain/post/application/port/out/post/PostDetailView.java
new file mode 100644
index 00000000..e218df2e
--- /dev/null
+++ b/src/main/java/space/space_spring/domain/post/application/port/out/post/PostDetailView.java
@@ -0,0 +1,46 @@
+package space.space_spring.domain.post.application.port.out.post;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Getter
+@Builder
+public class PostDetailView {
+
+    private String creatorName;
+
+    private String creatorProfileImageUrl;
+
+    private LocalDateTime createdAt;
+
+    private LocalDateTime lastModifiedAt;
+
+    private String title;
+
+    private String content;
+
+    private List<String> attachmentUrls;
+
+    private Long likeCount;
+
+    private Boolean isLiked;
+
+    // Querydsl에서 사용할 생성자
+    public PostDetailView(String creatorName, String creatorProfileImageUrl,
+                          LocalDateTime createdAt, LocalDateTime lastModifiedAt,
+                          String title, String content, List<String> attachmentUrls,
+                          Long likeCount, Boolean isLiked) {
+        this.creatorName = creatorName;
+        this.creatorProfileImageUrl = creatorProfileImageUrl;
+        this.createdAt = createdAt;
+        this.lastModifiedAt = lastModifiedAt;
+        this.title = title;
+        this.content = content;
+        this.attachmentUrls = attachmentUrls;
+        this.likeCount = likeCount;
+        this.isLiked = isLiked;
+    }
+}
diff --git a/src/main/java/space/space_spring/domain/post/application/service/ReadPostDetailService.java b/src/main/java/space/space_spring/domain/post/application/service/ReadPostDetailService.java
index dfe19671..9c4c15c0 100644
--- a/src/main/java/space/space_spring/domain/post/application/service/ReadPostDetailService.java
+++ b/src/main/java/space/space_spring/domain/post/application/service/ReadPostDetailService.java
@@ -2,39 +2,39 @@
 
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import space.space_spring.domain.post.application.port.in.readPostDetail.InfoOfCommentDetail;
 import space.space_spring.domain.post.application.port.in.readPostDetail.ReadPostDetailCommand;
 import space.space_spring.domain.post.application.port.in.readPostDetail.ReadPostDetailUseCase;
 import space.space_spring.domain.post.application.port.in.readPostDetail.ResultOfReadPostDetail;
-import space.space_spring.domain.post.application.port.out.LoadAttachmentPort;
-import space.space_spring.domain.post.application.port.out.LoadBoardPort;
-import space.space_spring.domain.post.application.port.out.LoadCommentPort;
-import space.space_spring.domain.post.application.port.out.LoadPostPort;
-import space.space_spring.domain.post.application.port.out.like.LoadLikePort;
+import space.space_spring.domain.post.application.port.out.*;
+import space.space_spring.domain.post.application.port.out.comment.CommentDetailQueryPort;
+import space.space_spring.domain.post.application.port.out.comment.CommentDetailView;
+import space.space_spring.domain.post.application.port.out.post.PostDetailQueryPort;
+import space.space_spring.domain.post.application.port.out.post.PostDetailView;
+import space.space_spring.domain.post.application.service.strategy.AnonymousCommentProcessingStrategy;
+import space.space_spring.domain.post.application.service.strategy.CommentDetailProcessingStrategy;
+import space.space_spring.domain.post.application.service.strategy.InactiveCommentProcessingStrategy;
+import space.space_spring.domain.post.application.service.strategy.NormalCommentProcessingStrategy;
 import space.space_spring.domain.post.domain.Board;
-import space.space_spring.domain.post.domain.Comment;
 import space.space_spring.domain.post.domain.Post;
-import space.space_spring.domain.spaceMember.application.port.out.LoadSpaceMemberInfoPort;
-import space.space_spring.domain.spaceMember.application.port.out.NicknameAndProfileImage;
 import space.space_spring.global.exception.CustomException;
-import space.space_spring.global.util.NaturalNumber;
 import space.space_spring.global.util.post.ConvertCreatedDate;
 
+import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 
 import static space.space_spring.global.common.response.status.BaseExceptionResponseStatus.*;
 
 @Service
 @RequiredArgsConstructor
+@Transactional(readOnly = true)
 public class ReadPostDetailService implements ReadPostDetailUseCase {
 
+    private final PostDetailQueryPort loadPostDetailPort;
+    private final CommentDetailQueryPort loadCommentDetailPort;
     private final LoadBoardPort loadBoardPort;
     private final LoadPostPort loadPostPort;
-    private final LoadCommentPort loadCommentPort;
-    private final LoadLikePort loadLikePort;
-    private final LoadSpaceMemberInfoPort loadSpaceMemberInfoPort;
-    private final LoadAttachmentPort loadAttachmentPort;
 
     @Override
     public ResultOfReadPostDetail readPostDetail(ReadPostDetailCommand command) {
@@ -45,39 +45,66 @@ public ResultOfReadPostDetail readPostDetail(ReadPostDetailCommand command) {
         // 2. validation
         validate(board, post, command);
 
-        // 3. comment 조회 -> status 상관없이 모든 댓글들 조회 -> 삭제된 댓글은
-        List<Comment> comments = loadCommentPort.loadByPostIdWithoutStatusFilter(post.getId());
+        // 3. 해당 게시글의 상세 정보 조회
+        PostDetailView postDetailView = loadPostDetailPort.loadPostDetail(post.getId(), command.getSpaceMemberId());
 
-        // 4. post, comment 들의 좋아요 개수 계산
-        // -> 이거 매번 db 조회 ??
-        // 일단 경민이가 만들어둔 메서드 사용
-        NaturalNumber postLikeCount = loadLikePort.countLikeByPostId(post.getId());
-        Map<Long, NaturalNumber> commentLikeCountMap = loadLikePort.countLikesByPostIds(comments.stream().map(Comment::getId).toList());
+        // 4. 해당 게시글에 달린 댓글 정보 조회 및 후처리
+        List<CommentDetailView> rawCommentDetailViews = loadCommentDetailPort.loadCommentDetail(post.getId(), command.getSpaceMemberId());
+        List<CommentDetailView> processedCommentDetailViews = processCommentDetails(rawCommentDetailViews);
 
-        // 5. 댓글 상세 정보 생성
-        List<InfoOfCommentDetail> infoOfCommentDetails = comments.stream()
-                .map(comment -> buildCommentDetail(comment, post, command, commentLikeCountMap))
-                .toList();
+        // 5. return
+        return ResultOfReadPostDetail.builder()
+                .creatorName(postDetailView.getCreatorName())
+                .creatorProfileImageUrl(postDetailView.getCreatorProfileImageUrl())
+                .createdAt(ConvertCreatedDate.setCreatedDate(postDetailView.getCreatedAt()))
+                .lastModifiedAt(ConvertCreatedDate.setCreatedDate(postDetailView.getLastModifiedAt()))
+                .title(postDetailView.getTitle())
+                .content(postDetailView.getContent())
+                .attachmentUrls(postDetailView.getAttachmentUrls())
+                .likeCount(postDetailView.getLikeCount().intValue())
+                .isLiked(postDetailView.getIsLiked())
+                .infoOfCommentDetails(mapToInfoOfCommentDetails(processedCommentDetailViews))
+                .build();
+    }
 
-        // 6. return
-        NicknameAndProfileImage postCreator = loadSpaceMemberInfoPort.loadNicknameAndProfileImageById(post.getPostCreatorId());
-        List<String> attachmentUrls = loadAttachmentPort.loadAttachmentUrlByTargetId(post.getId());
-        boolean hasSpaceMemberLikedToPost = loadLikePort.hasSpaceMemberLiked(command.getSpaceMemberId(), post.getId());
+    private List<InfoOfCommentDetail> mapToInfoOfCommentDetails(List<CommentDetailView> commentDetailViews) {
+        List<InfoOfCommentDetail> result = new ArrayList<>();
+        for (CommentDetailView comment : commentDetailViews) {
+            result.add(InfoOfCommentDetail.builder()
+                    .creatorName(comment.getCreatorName())
+                    .creatorProfileImageUrl(comment.getCreatorProfileImageUrl())
+                    .isPostOwner(comment.getIsPostOwner())
+                    .content(comment.getContent())
+                    .createdAt(ConvertCreatedDate.setCreatedDate(comment.getCreatedAt()))
+                    .lastModifiedAt(ConvertCreatedDate.setCreatedDate(comment.getLastModifiedAt()))
+                    .likeCount(comment.getLikeCount().intValue())
+                    .isLiked(comment.getIsLiked())
+                    .isActiveComment(comment.getIsActiveComment())
+                    .build());
+        }
+        return result;
+    }
 
-        String postCreatorNickname = post.getIsAnonymous() ? "익명 스페이서" : postCreator.getNickname();
+    private List<CommentDetailView> processCommentDetails(List<CommentDetailView> rawCommentDetailViews) {
+        List<CommentDetailView> processed = new ArrayList<>();
+
+        // 전략 리스트
+        List<CommentDetailProcessingStrategy> strategies = List.of(
+                new InactiveCommentProcessingStrategy(),
+                new AnonymousCommentProcessingStrategy(),
+                new NormalCommentProcessingStrategy()
+        );
+
+        for (CommentDetailView view : rawCommentDetailViews) {
+            for (CommentDetailProcessingStrategy strategy : strategies) {
+                if (strategy.supports(view)) {
+                    processed.add(strategy.process(view));
+                    break;
+                }
+            }
+        }
 
-        return ResultOfReadPostDetail.builder()
-                .creatorName(postCreatorNickname)
-                .creatorProfileImageUrl(postCreator.getProfileImageUrl())
-                .createdAt(ConvertCreatedDate.setCreatedDate(post.getBaseInfo().getCreatedAt()))
-                .lastModifiedAt(ConvertCreatedDate.setCreatedDate(post.getBaseInfo().getLastModifiedAt()))
-                .title(post.getTitle())
-                .content(post.getContent())
-                .attachmentUrls(attachmentUrls)
-                .likeCount(postLikeCount)
-                .isLiked(hasSpaceMemberLikedToPost)
-                .infoOfCommentDetails(infoOfCommentDetails)
-                .build();
+        return processed;
     }
 
     private void validate(Board board, Post post, ReadPostDetailCommand command) {
@@ -89,41 +116,4 @@ private void validate(Board board, Post post, ReadPostDetailCommand command) {
             throw new CustomException(POST_IS_NOT_IN_BOARD);
         }
     }
-
-    private InfoOfCommentDetail buildCommentDetail(Comment comment, Post post, ReadPostDetailCommand command, Map<Long, NaturalNumber> commentLikeCountMap) {
-        // 댓글 작성자 정보 조회
-        NicknameAndProfileImage commentCreator = loadSpaceMemberInfoPort.loadNicknameAndProfileImageById(comment.getCommentCreatorId());
-        String creatorNickname = commentCreator.getNickname();
-        String creatorProfileImageUrl = commentCreator.getProfileImageUrl();
-
-        // 익명 댓글인 경우 이름 재정의
-        // TODO : 익명인 댓글 작성자의 네이밍 알고리즘 개선
-        if (comment.getIsAnonymous()) {
-            creatorNickname = "익명 스페이서 " + (post.getId() * 10) + comment.getCommentCreatorId();
-            creatorProfileImageUrl = null;      // 프론트에서 익명 디폴트 이미지 보여주기?? 아니면 s3에 익명 디폴트 이미지 저장 & 로드?
-        }
-
-        // 댓글이 비활성화된 경우 내용 수정
-        boolean isActiveComment = comment.getBaseInfo().isActive();
-        if (!isActiveComment) {
-            comment.changeContent("삭제된 댓글입니다.");
-            creatorNickname = null;
-            creatorProfileImageUrl = null;
-        }
-
-        boolean hasSpaceMemberLikedToComment = loadLikePort.hasSpaceMemberLiked(command.getSpaceMemberId(), comment.getId());
-        boolean isPostOwner = post.isPostCreator(comment.getCommentCreatorId());
-
-        return InfoOfCommentDetail.builder()
-                .creatorName(creatorNickname)
-                .creatorProfileImageUrl(creatorProfileImageUrl)
-                .isPostOwner(isPostOwner)
-                .content(comment.getContent())
-                .createdAt(ConvertCreatedDate.setCreatedDate(comment.getBaseInfo().getCreatedAt()))
-                .lastModifiedAt(ConvertCreatedDate.setCreatedDate(comment.getBaseInfo().getLastModifiedAt()))
-                .likeCount(commentLikeCountMap.getOrDefault(comment.getId(), NaturalNumber.of(0)))
-                .isLiked(hasSpaceMemberLikedToComment)
-                .isActiveComment(isActiveComment)
-                .build();
-    }
 }
diff --git a/src/main/java/space/space_spring/domain/post/application/service/strategy/AnonymousCommentProcessingStrategy.java b/src/main/java/space/space_spring/domain/post/application/service/strategy/AnonymousCommentProcessingStrategy.java
new file mode 100644
index 00000000..e712d7c1
--- /dev/null
+++ b/src/main/java/space/space_spring/domain/post/application/service/strategy/AnonymousCommentProcessingStrategy.java
@@ -0,0 +1,38 @@
+package space.space_spring.domain.post.application.service.strategy;
+
+import space.space_spring.domain.post.application.port.out.comment.CommentDetailView;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AnonymousCommentProcessingStrategy implements CommentDetailProcessingStrategy {
+
+    private static final String ANONYMOUS_COMMENT_CREATOR_NICKNAME = "익명 스페이서";
+    private final Map<Long, String> anonymousNicknameMap = new HashMap<>();
+    private int anonymousCount = 1;
+
+    @Override
+    public boolean supports(CommentDetailView comment) {
+        return comment.getIsActiveComment() && comment.getIsAnonymousComment();
+    }
+
+    @Override
+    public CommentDetailView process(CommentDetailView comment) {
+        Long creatorId = comment.getCreatorId();
+        String nickname = anonymousNicknameMap.computeIfAbsent(creatorId,
+                id -> ANONYMOUS_COMMENT_CREATOR_NICKNAME + " " + anonymousCount++);
+        return CommentDetailView.builder()
+                .creatorId(comment.getCreatorId())
+                .creatorName(nickname)
+                .creatorProfileImageUrl(null)
+                .isPostOwner(comment.getIsPostOwner())
+                .content(comment.getContent())
+                .createdAt(comment.getCreatedAt())
+                .lastModifiedAt(comment.getLastModifiedAt())
+                .likeCount(comment.getLikeCount())
+                .isLiked(comment.getIsLiked())
+                .isActiveComment(comment.getIsActiveComment())
+                .isAnonymousComment(comment.getIsAnonymousComment())
+                .build();
+    }
+}
diff --git a/src/main/java/space/space_spring/domain/post/application/service/strategy/CommentDetailProcessingStrategy.java b/src/main/java/space/space_spring/domain/post/application/service/strategy/CommentDetailProcessingStrategy.java
new file mode 100644
index 00000000..5665f4f6
--- /dev/null
+++ b/src/main/java/space/space_spring/domain/post/application/service/strategy/CommentDetailProcessingStrategy.java
@@ -0,0 +1,9 @@
+package space.space_spring.domain.post.application.service.strategy;
+
+import space.space_spring.domain.post.application.port.out.comment.CommentDetailView;
+
+public interface CommentDetailProcessingStrategy {
+
+    boolean supports(CommentDetailView comment);
+    CommentDetailView process(CommentDetailView comment);
+}
diff --git a/src/main/java/space/space_spring/domain/post/application/service/strategy/InactiveCommentProcessingStrategy.java b/src/main/java/space/space_spring/domain/post/application/service/strategy/InactiveCommentProcessingStrategy.java
new file mode 100644
index 00000000..2a3b4e36
--- /dev/null
+++ b/src/main/java/space/space_spring/domain/post/application/service/strategy/InactiveCommentProcessingStrategy.java
@@ -0,0 +1,30 @@
+package space.space_spring.domain.post.application.service.strategy;
+
+import space.space_spring.domain.post.application.port.out.comment.CommentDetailView;
+
+public class InactiveCommentProcessingStrategy implements CommentDetailProcessingStrategy {
+
+    private static final String INACTIVE_COMMENT_CONTENT = "삭제된 댓글입니다.";
+
+    @Override
+    public boolean supports(CommentDetailView comment) {
+        return !comment.getIsActiveComment();
+    }
+
+    @Override
+    public CommentDetailView process(CommentDetailView comment) {
+        return CommentDetailView.builder()
+                .creatorId(comment.getCreatorId())
+                .creatorName(null)
+                .creatorProfileImageUrl(null)
+                .isPostOwner(comment.getIsPostOwner())
+                .content(INACTIVE_COMMENT_CONTENT)
+                .createdAt(comment.getCreatedAt())
+                .lastModifiedAt(comment.getLastModifiedAt())
+                .likeCount(comment.getLikeCount())
+                .isLiked(comment.getIsLiked())
+                .isActiveComment(comment.getIsActiveComment())
+                .isAnonymousComment(comment.getIsAnonymousComment())
+                .build();
+    }
+}
diff --git a/src/main/java/space/space_spring/domain/post/application/service/strategy/NormalCommentProcessingStrategy.java b/src/main/java/space/space_spring/domain/post/application/service/strategy/NormalCommentProcessingStrategy.java
new file mode 100644
index 00000000..4b03410d
--- /dev/null
+++ b/src/main/java/space/space_spring/domain/post/application/service/strategy/NormalCommentProcessingStrategy.java
@@ -0,0 +1,16 @@
+package space.space_spring.domain.post.application.service.strategy;
+
+import space.space_spring.domain.post.application.port.out.comment.CommentDetailView;
+
+public class NormalCommentProcessingStrategy implements CommentDetailProcessingStrategy {
+
+    @Override
+    public boolean supports(CommentDetailView comment) {
+        return comment.getIsActiveComment() && !comment.getIsAnonymousComment();
+    }
+
+    @Override
+    public CommentDetailView process(CommentDetailView comment) {
+        return comment;
+    }
+}
diff --git a/src/main/java/space/space_spring/domain/post/domain/Comment.java b/src/main/java/space/space_spring/domain/post/domain/Comment.java
index 9a7a4d1c..dfd9a16c 100644
--- a/src/main/java/space/space_spring/domain/post/domain/Comment.java
+++ b/src/main/java/space/space_spring/domain/post/domain/Comment.java
@@ -55,4 +55,6 @@ public void changeContent(String content) {
     public void changeAnonymous(Boolean isAnonymous) {
         this.isAnonymous = isAnonymous;
     }
+
+
 }


Reviews:
user: login: drbug2000
body: P2 : `CommentQueryAdapter`가 adapter임에도 너무 많은 책임을 진다고 생각합니다

P1 : 현재 같은 사용자가 작성한 익명 댓글에 같은 번호가 부여되는 로직이 아닌 것 같습니다. 추후에 로직을 추가하실 예정인가요?
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T14:05:28Z
user: login: drbug2000
body: 오 리팩토링된 코드 너무 깔끔하고 보기 좋네요
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-25T00:27:24Z
user: login: seongjunnoh
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T01:58:23Z

Review comments:
diff_hunk: @@ -0,0 +1,38 @@
+package space.space_spring.domain.post.application.service.strategy;
+
+import space.space_spring.domain.post.application.port.out.comment.CommentDetailView;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AnonymousCommentProcessingStrategy implements CommentDetailProcessingStrategy {
+
+    private static final String ANONYMOUS_COMMENT_CREATOR_NICKNAME = "익명 스페이서";
+    private final Map<Long, String> anonymousNicknameMap = new HashMap<>();
+    private int anonymousCount = 1;
+
+    @Override
+    public boolean supports(CommentDetailView comment) {
+        return comment.getIsActiveComment() && comment.getIsAnonymousComment();
+    }
+
+    @Override
+    public CommentDetailView process(CommentDetailView comment) {
+        Long creatorId = comment.getCreatorId();
+        String nickname = anonymousNicknameMap.computeIfAbsent(creatorId,
path: src/main/java/space/space_spring/domain/post/application/service/strategy/AnonymousCommentProcessingStrategy.java
user: login: drbug2000
body: 매우 직관적이고 단순해서 좋네요 
created_at: 2025-03-25T00:26:02Z
author_association: COLLABORATOR
diff_hunk: @@ -45,39 +45,66 @@ public ResultOfReadPostDetail readPostDetail(ReadPostDetailCommand command) {
         // 2. validation
         validate(board, post, command);
 
-        // 3. comment 조회 -> status 상관없이 모든 댓글들 조회 -> 삭제된 댓글은
-        List<Comment> comments = loadCommentPort.loadByPostIdWithoutStatusFilter(post.getId());
+        // 3. 해당 게시글의 상세 정보 조회
+        PostDetailView postDetailView = loadPostDetailPort.loadPostDetail(post.getId(), command.getSpaceMemberId());
 
-        // 4. post, comment 들의 좋아요 개수 계산
-        // -> 이거 매번 db 조회 ??
-        // 일단 경민이가 만들어둔 메서드 사용
-        NaturalNumber postLikeCount = loadLikePort.countLikeByPostId(post.getId());
-        Map<Long, NaturalNumber> commentLikeCountMap = loadLikePort.countLikesByPostIds(comments.stream().map(Comment::getId).toList());
+        // 4. 해당 게시글에 달린 댓글 정보 조회 및 후처리
+        List<CommentDetailView> rawCommentDetailViews = loadCommentDetailPort.loadCommentDetail(post.getId(), command.getSpaceMemberId());
+        List<CommentDetailView> processedCommentDetailViews = processCommentDetails(rawCommentDetailViews);
 
-        // 5. 댓글 상세 정보 생성
-        List<InfoOfCommentDetail> infoOfCommentDetails = comments.stream()
-                .map(comment -> buildCommentDetail(comment, post, command, commentLikeCountMap))
-                .toList();
+        // 5. return
+        return ResultOfReadPostDetail.builder()
+                .creatorName(postDetailView.getCreatorName())
+                .creatorProfileImageUrl(postDetailView.getCreatorProfileImageUrl())
+                .createdAt(ConvertCreatedDate.setCreatedDate(postDetailView.getCreatedAt()))
+                .lastModifiedAt(ConvertCreatedDate.setCreatedDate(postDetailView.getLastModifiedAt()))
+                .title(postDetailView.getTitle())
+                .content(postDetailView.getContent())
+                .attachmentUrls(postDetailView.getAttachmentUrls())
+                .likeCount(postDetailView.getLikeCount().intValue())
+                .isLiked(postDetailView.getIsLiked())
+                .infoOfCommentDetails(mapToInfoOfCommentDetails(processedCommentDetailViews))
+                .build();
+    }
 
-        // 6. return
-        NicknameAndProfileImage postCreator = loadSpaceMemberInfoPort.loadNicknameAndProfileImageById(post.getPostCreatorId());
-        List<String> attachmentUrls = loadAttachmentPort.loadAttachmentUrlByTargetId(post.getId());
-        boolean hasSpaceMemberLikedToPost = loadLikePort.hasSpaceMemberLiked(command.getSpaceMemberId(), post.getId());
+    private List<InfoOfCommentDetail> mapToInfoOfCommentDetails(List<CommentDetailView> commentDetailViews) {
+        List<InfoOfCommentDetail> result = new ArrayList<>();
+        for (CommentDetailView comment : commentDetailViews) {
+            result.add(InfoOfCommentDetail.builder()
+                    .creatorName(comment.getCreatorName())
+                    .creatorProfileImageUrl(comment.getCreatorProfileImageUrl())
+                    .isPostOwner(comment.getIsPostOwner())
+                    .content(comment.getContent())
+                    .createdAt(ConvertCreatedDate.setCreatedDate(comment.getCreatedAt()))
+                    .lastModifiedAt(ConvertCreatedDate.setCreatedDate(comment.getLastModifiedAt()))
+                    .likeCount(comment.getLikeCount().intValue())
+                    .isLiked(comment.getIsLiked())
+                    .isActiveComment(comment.getIsActiveComment())
+                    .build());
+        }
+        return result;
+    }
 
-        String postCreatorNickname = post.getIsAnonymous() ? "익명 스페이서" : postCreator.getNickname();
+    private List<CommentDetailView> processCommentDetails(List<CommentDetailView> rawCommentDetailViews) {
+        List<CommentDetailView> processed = new ArrayList<>();
+
+        // 전략 리스트
+        List<CommentDetailProcessingStrategy> strategies = List.of(
+                new InactiveCommentProcessingStrategy(),
+                new AnonymousCommentProcessingStrategy(),
+                new NormalCommentProcessingStrategy()
+        );
+
+        for (CommentDetailView view : rawCommentDetailViews) {
+            for (CommentDetailProcessingStrategy strategy : strategies) {
+                if (strategy.supports(view)) {
+                    processed.add(strategy.process(view));
+                    break;
path: src/main/java/space/space_spring/domain/post/application/service/ReadPostDetailService.java
user: login: drbug2000
body: 오호 여기도 전략 패턴 적용하셨군요
읽기 너무 편하네요
created_at: 2025-03-25T00:27:01Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,38 @@
+package space.space_spring.domain.post.application.service.strategy;
+
+import space.space_spring.domain.post.application.port.out.comment.CommentDetailView;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AnonymousCommentProcessingStrategy implements CommentDetailProcessingStrategy {
+
+    private static final String ANONYMOUS_COMMENT_CREATOR_NICKNAME = "익명 스페이서";
+    private final Map<Long, String> anonymousNicknameMap = new HashMap<>();
+    private int anonymousCount = 1;
+
+    @Override
+    public boolean supports(CommentDetailView comment) {
+        return comment.getIsActiveComment() && comment.getIsAnonymousComment();
+    }
+
+    @Override
+    public CommentDetailView process(CommentDetailView comment) {
+        Long creatorId = comment.getCreatorId();
+        String nickname = anonymousNicknameMap.computeIfAbsent(creatorId,
path: src/main/java/space/space_spring/domain/post/application/service/strategy/AnonymousCommentProcessingStrategy.java
user: login: seongjunnoh
body: Map 구조 사용해서 동일 익명 댓글 작성자 확인 & computeIfAbsent 메서드 활용해서 댓글 작성자 닉네임 처리 
이렇게 구현해봤습니다
created_at: 2025-03-25T01:58:22Z
author_association: COLLABORATOR

PR comments:
user: login: seongjunnoh
created_at: 2025-03-24T14:13:09Z
body: > P2 : `CommentQueryAdapter`가 adapter임에도 너무 많은 책임을 진다고 생각합니다
> 
> P1 : 현재 같은 사용자가 작성한 익명 댓글에 같은 번호가 부여되는 로직이 아닌 것 같습니다. 추후에 로직을 추가하실 예정인가요?

1. CommentQueryAdapter는 특정 게시글에 대한 List<CommentDetailView>를 반환하는 책임을 가지고 있고, List<CommentDetailView>를 구성하는 책임은 CommentQueryAdapter 혼자 가지는게 맞다고 생각했습니다. 
DB 조회한 결과에 대해 후처리 하는 작업까지 CommentQueryAdapter가 수행하는거 때문에 너무 많은 책임을 가진다고 하신거 같은데, 이 로직을 서비스로 옮기는게 더 적절하다고 생각하시는 건가요?

2. 아 이 부분은 놓쳤습니다. 수정하도록 하겠습니다
author_association: COLLABORATOR
user: login: drbug2000
created_at: 2025-03-24T14:33:15Z
body: > > P2 : `CommentQueryAdapter`가 adapter임에도 너무 많은 책임을 진다고 생각합니다
> > P1 : 현재 같은 사용자가 작성한 익명 댓글에 같은 번호가 부여되는 로직이 아닌 것 같습니다. 추후에 로직을 추가하실 예정인가요?
> 
> 1. CommentQueryAdapter는 특정 게시글에 대한 List를 반환하는 책임을 가지고 있고, List를 구성하는 책임은 CommentQueryAdapter 혼자 가지는게 맞다고 생각했습니다.
>    DB 조회한 결과에 대해 후처리 하는 작업까지 CommentQueryAdapter가 수행하는거 때문에 너무 많은 책임을 가진다고 하신거 같은데, 이 로직을 서비스로 옮기는게 더 적절하다고 생각하시는 건가요?
> 2. 아 이 부분은 놓쳤습니다. 수정하도록 하겠습니다

1. 네 "조회"인데 조회를 넘어서 익명 처리, 삭제된 댓글 처리 까지 담당하고 있어서 중간에 service나 다른 객체들이 책임을 나눠 주면 좋을 것 같습니다. 꼭 service가 아니더라도
실제로 코드 길이가 상당히 길기도 하고, "조회"와 "후처리" 가 연관 되어있지도 않은 것 같아서요
author_association: COLLABORATOR
user: login: seongjunnoh
created_at: 2025-03-24T16:40:19Z
body: > > > P2 : `CommentQueryAdapter`가 adapter임에도 너무 많은 책임을 진다고 생각합니다
> > > P1 : 현재 같은 사용자가 작성한 익명 댓글에 같은 번호가 부여되는 로직이 아닌 것 같습니다. 추후에 로직을 추가하실 예정인가요?
> > 
> > 
> > 
> > 1. CommentQueryAdapter는 특정 게시글에 대한 List를 반환하는 책임을 가지고 있고, List를 구성하는 책임은 CommentQueryAdapter 혼자 가지는게 맞다고 생각했습니다.
> >    DB 조회한 결과에 대해 후처리 하는 작업까지 CommentQueryAdapter가 수행하는거 때문에 너무 많은 책임을 가진다고 하신거 같은데, 이 로직을 서비스로 옮기는게 더 적절하다고 생각하시는 건가요?
> > 2. 아 이 부분은 놓쳤습니다. 수정하도록 하겠습니다
> 
> 1. 네 "조회"인데 조회를 넘어서 익명 처리, 삭제된 댓글 처리 까지 담당하고 있어서 중간에 service나 다른 객체들이 책임을 나눠 주면 좋을 것 같습니다. 꼭 service가 아니더라도
>    실제로 코드 길이가 상당히 길기도 하고, "조회"와 "후처리" 가 연관 되어있지도 않은 것 같아서요

상준님 말처럼 현재 QueryAdapter가 조회 기능을 넘어서 그 외 비즈니스 로직에 해당하는 작업또한 수행하고 있는 것이 적절한 책임 분리가 이뤄진것으로 보기는 어려운거 같네요!
좋은 지적 감사합니다!! 리뷰 1, 2 모두 반영해서 빠르게 코드 수정 후 추가 커밋 날려보겠습니다!
author_association: COLLABORATOR
user: login: seongjunnoh
created_at: 2025-03-24T18:09:55Z
body: > > > > P2 : `CommentQueryAdapter`가 adapter임에도 너무 많은 책임을 진다고 생각합니다
> > > > P1 : 현재 같은 사용자가 작성한 익명 댓글에 같은 번호가 부여되는 로직이 아닌 것 같습니다. 추후에 로직을 추가하실 예정인가요?
> > > 
> > > 
> > > 
> > > 1. CommentQueryAdapter는 특정 게시글에 대한 List를 반환하는 책임을 가지고 있고, List를 구성하는 책임은 CommentQueryAdapter 혼자 가지는게 맞다고 생각했습니다.
> > >    DB 조회한 결과에 대해 후처리 하는 작업까지 CommentQueryAdapter가 수행하는거 때문에 너무 많은 책임을 가진다고 하신거 같은데, 이 로직을 서비스로 옮기는게 더 적절하다고 생각하시는 건가요?
> > > 2. 아 이 부분은 놓쳤습니다. 수정하도록 하겠습니다
> > 
> > 
> > 
> > 1. 네 "조회"인데 조회를 넘어서 익명 처리, 삭제된 댓글 처리 까지 담당하고 있어서 중간에 service나 다른 객체들이 책임을 나눠 주면 좋을 것 같습니다. 꼭 service가 아니더라도
> >    실제로 코드 길이가 상당히 길기도 하고, "조회"와 "후처리" 가 연관 되어있지도 않은 것 같아서요
> 
> 상준님 말처럼 현재 QueryAdapter가 조회 기능을 넘어서 그 외 비즈니스 로직에 해당하는 작업또한 수행하고 있는 것이 적절한 책임 분리가 이뤄진것으로 보기는 어려운거 같네요! 좋은 지적 감사합니다!! 리뷰 1, 2 모두 반영해서 빠르게 코드 수정 후 추가 커밋 날려보겠습니다!

상준님 피드백 2개 모두 반영 + 전략패턴을 사용하여 리펙토링 진행해서 추가록 push 했습니다!
로컬에서 테스트 또한 완료하였습니다.

<img width="1242" alt="image" src="https://github.com/user-attachments/assets/71de33a6-402e-462b-8b42-9687a6153ab3" />

author_association: COLLABORATOR