[SpringBoot] Comment 도메인 리팩토링 사항 기록

2025. 10. 6. 17:45·트러블 슈팅&리팩토링

1. 숨어있던 N+1 문제 해결

[문제가 된 코드]

public interface CommentMapper {

        // CommentEntity 리스트 -> CommentResDTO
        default CommentResDTO toCommentResDTO(List<CommentEntity> commentEntities, Long postId, Long issueId,
                        CommentLikeService commentLikeService, Long currentMemberId) {
                if (commentEntities == null || commentEntities.isEmpty()) {
                        return new CommentResDTO(postId, issueId, List.of());
                }

                // 댓글들을 parentCommentId 기준으로 그룹화
                Map<Long, List<CommentEntity>> childCommentMap = commentEntities.stream()
                                .filter(comment -> comment.getParentCommentId() != null)
                                .collect(Collectors.groupingBy(CommentEntity::getParentCommentId));

                // 최상위 댓글만 필터링하고 CommentDTO로 변환
                List<CommentDTO> topLevelComments = commentEntities.stream()
                                .filter(comment -> comment.getParentCommentId() == null)
                                .map(comment -> toCommentDTO(comment, childCommentMap, commentLikeService, currentMemberId))
                                .collect(Collectors.toList());

                return new CommentResDTO(postId, issueId, topLevelComments);
        }

        // CommentEntity -> CommentDTO
        default CommentDTO toCommentDTO(CommentEntity commentEntity, Map<Long, List<CommentEntity>> childCommentMap,
                        CommentLikeService commentLikeService, Long currentMemberId) {
                if (commentEntity == null) {
                        return null;
                }

                // 대댓글 조회
                List<CommentEntity> childEntities = childCommentMap.getOrDefault(commentEntity.getCommentId(),
                                List.of());

                // 대댓글들을 재귀적으로 CommentDTO로 변환(빈 배열은 map이 실행되지 않음 -> 재귀 종료)
                List<CommentDTO> childComments = childEntities.stream()
                                .map(child -> toCommentDTO(child, childCommentMap, commentLikeService, currentMemberId))
                                .collect(Collectors.toList());

                // 해당 댓글의 좋아요 수 조회
                Long likeCount = commentLikeService.getLikeCount(commentEntity.getCommentId());

                // 현재 사용자가 이 댓글을 좋아요했는지 확인
                Boolean isLikedByCurrentUser = currentMemberId != null ? 
                                commentLikeService.isLikedByMember(commentEntity.getCommentId(), currentMemberId) : false;

                return new CommentDTO(
                                commentEntity.getCommentId(),
                                commentEntity.getAuthorEntity().getMemberId(),
                                commentEntity.getAuthorEntity().getName(),
                                commentEntity.getAuthorEntity().getProfileImgUrl(),
                                commentEntity.getContent(),
                                commentEntity.getCreatedAt(),
                                likeCount,
                                isLikedByCurrentUser,
                                childComments);
        }
- toCommentDTO 메서드에서 각 댓글마다 getLikeCount()와 isLikedByMember()를 개별 호출
- 댓글 100개면 200번의 추가 쿼리 발생!! 😢

N+1문제는 댓글 수가 많을 수록 심각한 성능저하가 발생할 수 있으니 최우선으로 해결해야 할 사항이었다.

2차 개발 때 댓글 좋아요 기능을 급하게 추가하다보니 놓친 것 같다.

[해결 후 코드]

public interface CommentMapper {

        // CommentEntity 리스트 -> CommentResDTO (N+1 최적화 버전)
        default CommentResDTO toCommentResDTO(List<CommentEntity> commentEntities, Long postId, Long issueId,
                        CommentLikeService commentLikeService, Long currentMemberId) {
                if (commentEntities == null || commentEntities.isEmpty()) {
                        return new CommentResDTO(postId, issueId, List.of());
                }

                // 모든 댓글 ID 수집
                List<Long> allCommentIds = commentEntities.stream()
                                .map(CommentEntity::getCommentId)
                                .collect(Collectors.toList());

                // 한번에 좋아요 수 조회 (N+1 방지)
                Map<Long, Long> likeCountMap = commentLikeService.getLikeCountsMap(allCommentIds);

                // 현재 사용자의 좋아요 여부 한번에 조회 (N+1 방지)
                Set<Long> likedCommentIds = currentMemberId != null ?
                                commentLikeService.getLikedCommentIds(currentMemberId, allCommentIds) : Set.of();

                // 댓글들을 parentCommentId 기준으로 그룹화
                Map<Long, List<CommentEntity>> childCommentMap = commentEntities.stream()
                                .filter(comment -> comment.getParentCommentId() != null)
                                .collect(Collectors.groupingBy(CommentEntity::getParentCommentId));

                // 최상위 댓글만 필터링하고 CommentDTO로 변환
                List<CommentDTO> topLevelComments = commentEntities.stream()
                                .filter(comment -> comment.getParentCommentId() == null)
                                .map(comment -> toCommentDTO(comment, childCommentMap, likeCountMap, likedCommentIds))
                                .collect(Collectors.toList());

                return new CommentResDTO(postId, issueId, topLevelComments);
        }

        // CommentEntity -> CommentDTO (N+1 최적화 버전)
        default CommentDTO toCommentDTO(CommentEntity commentEntity, Map<Long, List<CommentEntity>> childCommentMap,
                        Map<Long, Long> likeCountMap, Set<Long> likedCommentIds) {
                if (commentEntity == null) {
                        return null;
                }

                // 대댓글 조회
                List<CommentEntity> childEntities = childCommentMap.getOrDefault(commentEntity.getCommentId(),
                                List.of());

                // 대댓글들을 재귀적으로 CommentDTO로 변환(빈 배열은 map이 실행되지 않음 -> 재귀 종료)
                List<CommentDTO> childComments = childEntities.stream()
                                .map(child -> toCommentDTO(child, childCommentMap, likeCountMap, likedCommentIds))
                                .collect(Collectors.toList());

                // Map에서 좋아요 수 조회 (미리 로드된 데이터 사용)
                Long likeCount = likeCountMap.getOrDefault(commentEntity.getCommentId(), 0L);

                // Set에서 좋아요 여부 확인 (미리 로드된 데이터 사용)
                Boolean isLikedByCurrentUser = likedCommentIds.contains(commentEntity.getCommentId());

                return new CommentDTO(
                                commentEntity.getCommentId(),
                                commentEntity.getAuthorEntity().getMemberId(),
                                commentEntity.getAuthorEntity().getName(),
                                commentEntity.getAuthorEntity().getProfileImgUrl(),
                                commentEntity.getContent(),
                                commentEntity.getCreatedAt(),
                                likeCount,
                                isLikedByCurrentUser,
                                childComments);
        }

우선 조회한 모든 댓글의 ID를 수집한 후, Map을 이용해서 한번의 쿼리로 좋아요 수를 조회한다.

그리고 현재 이 댓글들을 조회하고 있는 사용자의 좋아요 여부도 Set을 이용해서 한번의 쿼리로 조회한다.

그다음 좋아요 수는 Long 타입으로, 좋아요 여부는 Boolean 타입으로 받아서 DTO로 변환한다.

이때 Map을 이용하는 서비스 메서드는 다음과 같다.

    // 여러 댓글의 좋아요 수를 Map으로 반환 (N+1 방지)
    public Map<Long, Long> getLikeCountsMap(List<Long> commentIds) {
        if (commentIds == null || commentIds.isEmpty()) {
            return Map.of();
        }

        List<Object[]> results = commentLikeRepository.countByCommentIds(commentIds);
        Map<Long, Long> likeCountMap = results.stream()
            .collect(Collectors.toMap(
                arr -> (Long) arr[0],
                arr -> (Long) arr[1]
            ));

        // 좋아요가 없는 댓글은 0으로 설정
        commentIds.forEach(id -> likeCountMap.putIfAbsent(id, 0L));

        return likeCountMap;
    }

{댓글아이디, 좋아요수} 이런 식으로 Map으로 묶어 반환한다.

여기서 results 리스트를 반환하는 레포지토리 로직은 아래와 같다.

    // 여러 댓글의 좋아요 수를 한번에 조회
    @Query("SELECT cl.commentLikeId.commentId, COUNT(cl) FROM CommentLikeEntity cl " +
            "WHERE cl.commentLikeId.commentId IN :commentIds " +
            "GROUP BY cl.commentLikeId.commentId")
    List<Object[]> countByCommentIds(@Param("commentIds") List<Long> commentIds);

댓글 아이디 리스트를 받고, 그 댓글 각각의 좋아요 수를 count해서 두 번째 요소로 넣는다.

GROUP BY로 commentId를 기준으로 묶이도록 한다.

코드를 계속 보다보니 놓친 부분이 하나 더 있는데, 바로 Object 타입을 사용한 것이다.

그래서 타입 안정성을 위해 projection을 사용하기로 했다.

차례로 interface, respository, service 부분이다.

/**
 * 댓글 좋아요 수 조회를 위한 Projection Interface
 */
public interface CommentLikeCount {
    Long getCommentId();
    Long getLikeCount();
}
    // 여러 댓글의 좋아요 수를 한번에 조회 (타입 안전한 Projection 사용)
    @Query("SELECT cl.commentLikeId.commentId as commentId, COUNT(cl) as likeCount FROM CommentLikeEntity cl " +
            "WHERE cl.commentLikeId.commentId IN :commentIds " +
            "GROUP BY cl.commentLikeId.commentId")
    List<CommentLikeCount> countByCommentIds(@Param("commentIds") List<Long> commentIds);
    // 여러 댓글의 좋아요 수를 Map으로 반환 (N+1 방지, 타입 안전)
    public Map<Long, Long> getLikeCountsMap(List<Long> commentIds) {
        if (commentIds == null || commentIds.isEmpty()) {
            return Map.of();
        }

        List<CommentLikeCount> results = commentLikeRepository.countByCommentIds(commentIds);
        Map<Long, Long> likeCountMap = results.stream()
            .collect(Collectors.toMap(
                CommentLikeCount::getCommentId,
                CommentLikeCount::getLikeCount
            ));

        // 좋아요가 없는 댓글은 0으로 설정
        commentIds.forEach(id -> likeCountMap.putIfAbsent(id, 0L));

        return likeCountMap;
    }

이렇게 리팩토링 했을 때의 장점은 다음과 같다.

✅ 타입 안전성: 컴파일 타임에 타입 체크
✅ 가독성: 메서드 참조로 의미가 명확함
✅ 유지보수성: 배열 인덱스 오류 방지
✅ IDE 지원: 자동완성과 리팩토링 가능
✅ 성능: 런타임 오버헤드 없음 (동일한 SQL 실행)

projection이 작동하는 로직은 다음과 같다.

 // JPQL 쿼리
  SELECT cl.commentLikeId.commentId as commentId, COUNT(cl) as likeCount
  FROM CommentLikeEntity cl
 
 /*
   - cl.commentLikeId.commentId as commentId: 조회한 값을 commentId라는 이름으로 반환
   - COUNT(cl) as likeCount: 집계 결과를 likeCount라는 이름으로 반환
 */

// 2. Projection Interface와의 매핑

  public interface CommentLikeCount {
      Long getCommentId();  // ← "commentId" 별칭과 매핑
      Long getLikeCount();  // ← "likeCount" 별칭과 매핑
  }

 

 Spring Data JPA는 별칭명과 getter 메서드명을 매칭한다.

  - 별칭 commentId → 메서드 getCommentId()
  - 별칭 likeCount → 메서드 getLikeCount()

별칭이 없는 경우엔 Spring Data JPA가 결과의 컬럼명을 Projection Interface의 메서드와 매칭할 수 없으며, 런타임 에러 발생 또는 null 값을 반환하게 된다.


2. 불필요한 엔티티 조회 제거

[문제가 된 코드]

    // 댓글 좋아요
    @CacheEvict(value = "comments", allEntries = true)
    @Transactional
    public void likeComment(Long commentId, Long memberId) {
        
        // 댓글 존재 확인
        CommentEntity commentEntity = commentRepository.findById(commentId)
                .orElseThrow(() -> CommentException.commentNotFoundException());

        // 회원 존재 확인
        MemberEntity memberEntity = memberRepository.findById(memberId)
                .orElseThrow(() -> MemberException.memberNotFoundException());

        // 이미 좋아요를 눌렀는지 확인
        CommentLikeId commentLikeId = new CommentLikeId(commentId, memberId);
        if (commentLikeRepository.existsById(commentLikeId)) {
            throw CommentException.commentAlreadyLikedException();
        }

        // 좋아요 생성
        CommentLikeEntity commentLikeEntity = CommentLikeEntity.builder()
                .commentLikeId(commentLikeId)
                .commentEntity(commentEntity)
                .memberEntity(memberEntity)
                .build();

        commentLikeRepository.save(commentLikeEntity);
    }

[해결 후 코드]

// 댓글 좋아요 (엔티티 조회 최적화)
    @CacheEvict(value = "comments", allEntries = true)
    @Transactional
    public void likeComment(Long commentId, Long memberId) {

        // 댓글 존재 확인 (존재 여부만 확인, 엔티티 조회 X)
        if (!commentRepository.existsById(commentId)) {
            throw CommentException.commentNotFoundException();
        }

        // 회원 존재 확인 (존재 여부만 확인, 엔티티 조회 X)
        if (!memberRepository.existsById(memberId)) {
            throw MemberException.memberNotFoundException();
        }

        // 이미 좋아요를 눌렀는지 확인
        CommentLikeId commentLikeId = new CommentLikeId(commentId, memberId);
        if (commentLikeRepository.existsById(commentLikeId)) {
            throw CommentException.commentAlreadyLikedException();
        }

        // 좋아요 생성 (JPA가 ID로 참조만 설정, 실제 엔티티 로드 안함)
        CommentLikeEntity commentLikeEntity = CommentLikeEntity.builder()
                .commentLikeId(commentLikeId)
                .commentEntity(commentRepository.getReferenceById(commentId))
                .memberEntity(memberRepository.getReferenceById(memberId))
                .build();

        commentLikeRepository.save(commentLikeEntity);
    }

existsById 메서드를 사용해서 엔티티 전체를 불러오는 대신 존재 여부만 간단하게 확인할 수 있도록 했다.

따라서 builder로 좋아요 객체를 생성할 때 comment, member 엔티티를 직접 넣는 것이 아니라 getReferenceById를 이용해 프록시 참조만 설정하게 함으로써 불필요한 SELECT 쿼리를 2개 제거하고 메모리 사용량을 감소시켰다.

✨ getReferenceById() vs findById()
- findById(): 즉시 SELECT 쿼리 실행, 전체 데이터 로드
- getReferenceById(): 프록시만 생성, 실제 사용 시점에 로드 (이 경우 FK만 필요하므로 로드 안됨)

3. 중복 로직 제거

[문제가 된 코드]

if (commentCreateReqDTO.getParentCommentId() != null) {
                CommentEntity parentComment = commentRepository.findById(commentCreateReqDTO.getParentCommentId())
                        .orElse(null);
                if (parentComment != null) {
                    Long parentAuthorUserId = parentComment.getAuthorEntity().getUserEntity().getUserId();
                    
                    // 대댓글 작성자와 원래 댓글 작성자가 다른 경우에만 알림 전송
                    if (!parentAuthorUserId.equals(currentUserId) && !parentAuthorUserId.equals(authorUserId)) {
                        String actionUrl = String.format("/org/%d/board/%d/post/%d", 
                                postEntity.getBoardEntity().getOrgEntity().getOrgId(),
                                postEntity.getBoardEntity().getBoardId(),
                                postEntity.getPostId());
                        
                        notificationEventService.sendCommentReplyNotification(
                            parentAuthorUserId,
                            commentEntity.getAuthorEntity().getName(),
                            parentComment.getContent(),
                            postEntity.getPostId(),
                            "post",
                            actionUrl
                        );
                    }
                }
            }

[해결 후 코드]

// 대댓글인 경우 원래 댓글 작성자에게도 알림 전송
            if (commentCreateReqDTO.getParentCommentId() != null) {
                sendParentCommentReplyNotification(
                    commentCreateReqDTO.getParentCommentId(),
                    currentUserId,
                    authorUserId,
                    commentEntity.getAuthorEntity().getName(),
                    postEntity.getPostId(),
                    "post",
                    String.format("/org/%d/board/%d/post/%d",
                        postEntity.getBoardEntity().getOrgEntity().getOrgId(),
                        postEntity.getBoardEntity().getBoardId(),
                        postEntity.getPostId())
                );

// 대댓글 알림 전송
    private void sendParentCommentReplyNotification(Long parentCommentId, Long currentUserId,
            Long excludeUserId, String authorName, Long targetId, String targetType, String actionUrl) {
        CommentEntity parentComment = commentRepository.findById(parentCommentId)
                .orElse(null);

        if (parentComment != null) {
            Long parentAuthorUserId = parentComment.getAuthorEntity().getUserEntity().getUserId();

            // 대댓글 작성자와 원래 댓글 작성자가 다르고, excludeUserId와도 다른 경우에만 알림 전송
            if (!parentAuthorUserId.equals(currentUserId) &&
                (excludeUserId == null || !parentAuthorUserId.equals(excludeUserId))) {
                notificationEventService.sendCommentReplyNotification(
                    parentAuthorUserId,
                    authorName,
                    parentComment.getContent(),
                    targetId,
                    targetType,
                    actionUrl
                );
            }
        }
    }

2번 사용되는 완전히 동일한 로직을 메서드로 따로 빼서 중복을 제거해주었다.

46줄의 중복 코드 → 19줄의 재사용 메서드로 축소하면서 가독성 및 유지보수성이 증가되었다! 

두 번째로는 무려 6개의 메서드에서 반복되는 코드를 유틸함수로 뺐다.

[문제가 된 코드]

 // 댓글 좋아요 취소
        @OrgAuth(accessLevel = Role.MEMBER)
        @DeleteMapping("/{commentId}/like")
        @Operation(summary = "댓글 좋아요 취소", description = "댓글 좋아요를 취소합니다.")
        public ResponseEntity<ApiResponse<Void>> unlikeComment(
                        @OrgId @PathVariable Long orgId,
                        @PathVariable @Min(value = 1, message = "댓글 ID는 1 이상이어야 합니다.") Long commentId) {

        // 현재 사용자 정보 가져오기
        Long currentMemberId = SecurityUtil.getCurrentMemberIdByOrgId(orgId);
        if (currentMemberId == null) {
            throw OrgException.orgNotFoundException();
        }

                // 현재 사용자 권한 확인
                commentLikeService.unlikeComment(commentId, currentMemberId);

                return ResponseEntity.ok(ApiResponse.success(null, "댓글 좋아요 취소에 성공했습니다."));
        }

[해결 후 코드]

// 댓글 좋아요 취소
        @OrgAuth(accessLevel = Role.MEMBER)
        @DeleteMapping("/{commentId}/like")
        @Operation(summary = "댓글 좋아요 취소", description = "댓글 좋아요를 취소합니다.")
        public ResponseEntity<ApiResponse<Void>> unlikeComment(
                        @OrgId @PathVariable Long orgId,
                        @PathVariable @Min(value = 1, message = "댓글 ID는 1 이상이어야 합니다.") Long commentId) {

                Long currentMemberId = validateAndGetCurrentMember(orgId);

                commentLikeService.unlikeComment(commentId, currentMemberId);

                return ResponseEntity.ok(ApiResponse.success(null, "댓글 좋아요 취소에 성공했습니다."));
        }

        // 현재 사용자 검증 및 조회
        private Long validateAndGetCurrentMember(Long orgId) {
                Long currentMemberId = SecurityUtil.getCurrentMemberIdByOrgId(orgId);
                if (currentMemberId == null) {
                        throw OrgException.orgNotFoundException();
                }
                return currentMemberId;
        }

현재 사용자 검증 및 조회는 매번 해줘야 하므로 따로 유틸 함수로 뽑아서 사용하는게 가독성, 유지보수성 측면에서도 좋다.

그리고 추가로 유효성 검증 하는 메서드가 조금 길어지는 경우, 아래와 같이 헬퍼 메서드로 뽑아서 사용하는 것이 관심사 분리 측면에서 좋다.

[전]

@Transactional
    public void createComment(CommentCreateReqDTO commentCreateReqDTO, Long currentMemberId) {

        if (commentCreateReqDTO.getPostId() == null && commentCreateReqDTO.getIssueId() == null) {
            throw CommentException.commentTargetRequiredException();
        }

        if (commentCreateReqDTO.getPostId() != null && commentCreateReqDTO.getIssueId() != null) {
            throw CommentException.commentTargetConflictException();
        }

        if (commentCreateReqDTO.getContent() == null || commentCreateReqDTO.getContent().trim().isEmpty()) {
            throw CommentException.commentContentRequiredException();
        }

        if (commentCreateReqDTO.getContent().length() > 1000) {
            throw CommentException.commentContentTooLongException();
        }

        PostEntity postEntity = null;

[후]

@Transactional
    public void createComment(CommentCreateReqDTO commentCreateReqDTO, Long currentMemberId) {

        validateCommentRequest(commentCreateReqDTO);

        PostEntity postEntity = null;
        IssueEntity issueEntity = null;

	...중략 
    
    // 댓글 요청 유효성 검증
    private void validateCommentRequest(CommentCreateReqDTO commentCreateReqDTO) {
        if (commentCreateReqDTO.getPostId() == null && commentCreateReqDTO.getIssueId() == null) {
            throw CommentException.commentTargetRequiredException();
        }

        if (commentCreateReqDTO.getPostId() != null && commentCreateReqDTO.getIssueId() != null) {
            throw CommentException.commentTargetConflictException();
        }

        if (commentCreateReqDTO.getContent() == null || commentCreateReqDTO.getContent().trim().isEmpty()) {
            throw CommentException.commentContentRequiredException();
        }

        if (commentCreateReqDTO.getContent().length() > 1000) {
            throw CommentException.commentContentTooLongException();
        }
    }

4. 매직 넘버 상수화

[문제가 된 코드]

        if (content.length() > 1000) {
            throw CommentException.commentContentTooLongException();
        }

[해결한 후 코드]

    private static final int MAX_COMMENT_LENGTH = 1000;
    
            if (content.length() > MAX_COMMENT_LENGTH) {
            throw CommentException.commentContentTooLongException();
        }

하드코딩 되어있던 최대 댓글 길이는 상수화해서 private static final로 따로 관리할 수 있도록 하는게 좋다.

이래야 다른 곳에서도 재사용 가능하고 유지보수하기도 편하다.


5. Optional 활용 개선

[전]

        // 현재 사용자의 해당 조직에서의 권한 확인
        return orgParticipantMemberRepository
                .findByOrgEntity_OrgIdAndMemberEntity_MemberIdAndStatus(orgId, currentMemberId, Status.ACTIVE)
                .map(opm -> opm.getRole().equals(Role.ADMIN) || opm.getRole().equals(Role.ROOT_ADMIN))
                .orElse(false);
    }

[후]

        // 현재 사용자의 해당 조직에서의 권한 확인
        return orgParticipantMemberRepository
                .findByOrgEntity_OrgIdAndMemberEntity_MemberIdAndStatus(orgId, currentMemberId, Status.ACTIVE)
                .map(opm -> opm.getRole().isAdminOrAbove())
                .orElse(false);
    }
    
    // enum
        public boolean isAdminOrAbove() {
        return this == ADMIN || this == ROOT_ADMIN;
    }

enum에 다른 곳에서도 쓰일 수 있을만한 메서드를 정의해놓고 서비스 로직에서 사용한다.

저작자표시 비영리 변경금지 (새창열림)

'트러블 슈팅&리팩토링' 카테고리의 다른 글

[Axios] 서버에서 사용자가 업로드한 파일을 받아오지 못하는 에러 핸들링  (0) 2025.05.03
[Next.js + Zustand] localStorage hydration 에러 핸들링  (0) 2025.05.02
[Node.js] prisma 클라이언트 초기화 오류 해결하기  (1) 2025.04.16
[Next.js] Image 경고(LCP, 종횡비) 해결하기  (0) 2025.04.10
[React/Zustand] 리액트 훅은 함수 컴포넌트 내부에서만 호출될 수 있습니다.  (0) 2025.03.02
'트러블 슈팅&리팩토링' 카테고리의 다른 글
  • [Axios] 서버에서 사용자가 업로드한 파일을 받아오지 못하는 에러 핸들링
  • [Next.js + Zustand] localStorage hydration 에러 핸들링
  • [Node.js] prisma 클라이언트 초기화 오류 해결하기
  • [Next.js] Image 경고(LCP, 종횡비) 해결하기
버그잡는고양이발
버그잡는고양이발
주니어 개발자입니다!
  • 버그잡는고양이발
    지극히평범한개발블로그
    버그잡는고양이발
  • 전체
    오늘
    어제
    • 분류 전체보기 (383)
      • React (16)
      • Next.js (5)
      • Javascript (5)
      • Typescript (4)
      • Node.js (2)
      • Cs (16)
      • 트러블 슈팅&리팩토링 (6)
      • Html (1)
      • Css (3)
      • Django (0)
      • vue (0)
      • Java (2)
      • Python (0)
      • 독서 (1)
      • 기타 (3)
      • 백준 (192)
      • swea (31)
      • 프로그래머스 (30)
      • 이코테 (4)
      • 99클럽 코테 스터디 (30)
      • ssafy (31)
      • IT기사 (1)
  • 블로그 메뉴

    • 홈
    • 태그
  • 인기 글

  • 태그

    99클럽
    개발자취업
    코딩테스트준비
    항해99
    Til
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
버그잡는고양이발
[SpringBoot] Comment 도메인 리팩토링 사항 기록
상단으로

티스토리툴바