Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public ResponseEntity<ErrorResponse> customException(CustomException e) {
public ResponseEntity<ErrorResponse> exceptionHandler(Exception e) {
log.error("Exception: ", e);
ErrorResponse response = ErrorResponse.builder()
.code("500")
.code(e.getMessage())
.message("서버 오류입니다.")
.build();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.juniortown.backend.like.controller;

import org.juniortown.backend.like.dto.response.LikeResponse;
import org.juniortown.backend.like.service.LikeService;
import org.juniortown.backend.user.dto.CustomUserDetails;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class LikeController {
private final LikeService likeService;
@PostMapping("/posts/likes/{postId}")
public ResponseEntity<LikeResponse> like(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable Long postId) {
Long userId = customUserDetails.getUserId();
LikeResponse response = likeService.likePost(userId, postId);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.juniortown.backend.like.dto.response;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class LikeResponse {
private Long userId;
private Long postId;
private Boolean isLiked;
@Builder
public LikeResponse(Long userId, Long postId, Boolean isLiked) {
this.userId = userId;
this.postId = postId;
this.isLiked = isLiked;
}
}
42 changes: 42 additions & 0 deletions backend/src/main/java/org/juniortown/backend/like/entity/Like.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.juniortown.backend.like.entity;

import org.juniortown.backend.post.entity.Post;
import org.juniortown.backend.user.entity.User;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "likes")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Like {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;

@ManyToOne
@JoinColumn(name = "post_id", nullable = false)
private Post post;

@Builder
public Like(User user, Post post) {
this.user = user;
this.post = post;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.juniortown.backend.like.exception;

import org.juniortown.backend.exception.CustomException;

import lombok.Getter;


public class LikeFailureException extends CustomException {
public static final String MESSAGE = "게시글이 삭제되었거나, 좋아요 처리 실패";
public LikeFailureException(Exception e) {
super(MESSAGE, e);
}
@Override
public int getStatusCode() {
return 500;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.juniortown.backend.like.repository;

import java.util.Optional;

import org.juniortown.backend.like.entity.Like;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface LikeRepository extends JpaRepository<Like, Long> {
Optional<Like> findByUserIdAndPostId(Long userId, Long postId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.juniortown.backend.like.service;

import java.util.Optional;

import org.juniortown.backend.like.dto.response.LikeResponse;
import org.juniortown.backend.like.entity.Like;
import org.juniortown.backend.like.exception.LikeFailureException;
import org.juniortown.backend.like.repository.LikeRepository;
import org.juniortown.backend.post.entity.Post;
import org.juniortown.backend.post.repository.PostRepository;
import org.juniortown.backend.user.entity.User;
import org.juniortown.backend.user.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class LikeService {
private final LikeRepository likeRepository;
private final UserRepository userRepository;
private final PostRepository postRepository;
@Transactional
public LikeResponse likePost(Long userId, Long postId) {
User user = userRepository.getReferenceById(userId);
Post post = postRepository.getReferenceById(postId);
Comment on lines +26 to +27
Copy link

@coderabbitai coderabbitai bot Jul 8, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

엔티티 존재 여부 검증 필요

getReferenceById는 지연 로딩 프록시를 반환하므로 실제 엔티티가 존재하지 않을 수 있습니다. 엔티티 존재 여부를 검증하는 것이 좋습니다.

-User user = userRepository.getReferenceById(userId);
-Post post = postRepository.getReferenceById(postId);
+User user = userRepository.findById(userId)
+    .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
+Post post = postRepository.findById(postId)
+    .orElseThrow(() -> new IllegalArgumentException("게시글을 찾을 수 없습니다."));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
User user = userRepository.getReferenceById(userId);
Post post = postRepository.getReferenceById(postId);
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
Post post = postRepository.findById(postId)
.orElseThrow(() -> new IllegalArgumentException("게시글을 찾을 수 없습니다."));
🤖 Prompt for AI Agents
In backend/src/main/java/org/juniortown/backend/like/service/LikeService.java
around lines 26 to 27, replace getReferenceById calls with a method that fetches
the entity and verifies its existence, such as findById. Check if the returned
Optional is present; if not, handle the absence appropriately (e.g., throw an
exception) to ensure the user and post entities actually exist before
proceeding.

Copy link
Owner Author

Choose a reason for hiding this comment

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

만약 post나 user가 존재하지 않으면 likeRepository.save() 할 때 외래키 제약 조건에 의해서(너가 추천해줘서 그제야 넣긴했지만) 예외를 터뜨리기 때문에 굳이 매번 좋아요 할 때마다 user와 post를 DB에서 직접 조회할 필요는 없다고 생각함.

Copy link

Choose a reason for hiding this comment

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

@Doncham 맞습니다! 외래키 제약 조건이 있다면 getReferenceById를 사용하는 접근 방식이 더 효율적입니다.

실제로 존재하지 않는 엔티티에 대해 getReferenceById로 프록시를 생성하고, likeRepository.save() 시점에 외래키 제약 조건 위반으로 예외가 발생하는 것이 맞고, 이는 이미 기존의 try-catch 블록에서 LikeFailureException으로 처리되고 있네요.

매번 좋아요 요청마다 사용자와 게시글 존재 여부를 확인하기 위해 추가 DB 조회를 하는 것보다, 데이터베이스 레벨에서 참조 무결성을 보장하는 방식이 성능상 더 유리합니다.

🐰 (nodding thoughtfully)

   (\   /)
  ( ._.)
 o_(")(")

✏️ Learnings added
Learnt from: Doncham
PR: Doncham/JuniorTown#10
File: backend/src/main/java/org/juniortown/backend/like/service/LikeService.java:26-27
Timestamp: 2025-07-08T15:21:25.357Z
Learning: LikeService에서 getReferenceById를 사용하는 이유: 외래키 제약 조건이 있으면 실제 엔티티 존재 여부를 사전에 확인할 필요가 없고, save() 시점에 제약 조건 위반으로 예외가 발생하므로 프록시 객체를 사용하는 것이 성능상 더 효율적이다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Doncham
PR: Doncham/JuniorTown#10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.329Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.

Optional<Like> like = likeRepository.findByUserIdAndPostId(userId, postId);

if(like.isEmpty()){
try{
likeRepository.save(Like.builder()
.user(user)
// 저장하는 시점에 post가 삭제될 수 있으니까 try-catch로 잡음.
.post(post)
.build()
);
return LikeResponse.builder()
.userId(userId)
.postId(postId)
.isLiked(true)
.build();
}catch (Exception e) {
throw new LikeFailureException(e);
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

예외 처리 개선 필요

현재 구현에서는 다음과 같은 문제가 있습니다:

  1. 너무 광범위한 예외 처리
  2. 동시성 문제 (race condition) 가능성
 if(like.isEmpty()){
   try{
-    likeRepository.save(Like.builder()
-      .user(user)
-      .post(post)
-      .build()
-    );
+    Like newLike = Like.builder()
+      .user(user)
+      .post(post)
+      .build();
+    likeRepository.save(newLike);
     return LikeResponse.builder()
       .userId(userId)
       .postId(postId)
       .isLiked(true)
       .build();
-  }catch (Exception e) {
+  } catch (DataIntegrityViolationException e) {
+    // 동시성으로 인한 중복 처리
+    throw new LikeFailureException("이미 좋아요가 처리되었습니다.");
+  } catch (Exception e) {
     throw new LikeFailureException(e);
   }

필요한 import 추가:

+import org.springframework.dao.DataIntegrityViolationException;
🤖 Prompt for AI Agents
In backend/src/main/java/org/juniortown/backend/like/service/LikeService.java
between lines 30 and 46, the current exception handling is too broad and may
mask specific issues, and there is a potential race condition when saving likes
concurrently. Refine the exception handling by catching more specific exceptions
related to database constraints or entity state, and implement a mechanism to
handle or prevent race conditions, such as checking existence again before
saving or using database constraints with proper exception handling. Also, add
any necessary imports for the specific exceptions you handle.

else{
likeRepository.deleteById(like.get().getId());

return LikeResponse.builder()
.userId(userId)
.postId(postId)
.isLiked(false)
.build();
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

좋아요 취소 시 예외 처리 추가

좋아요 취소 시에도 예외 처리가 필요합니다.

 else{
-  likeRepository.deleteById(like.get().getId());
+  try {
+    likeRepository.deleteById(like.get().getId());
+  } catch (Exception e) {
+    throw new LikeFailureException("좋아요 취소에 실패했습니다.", e);
+  }
   
   return LikeResponse.builder()
     .userId(userId)
     .postId(postId)
     .isLiked(false)
     .build();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
else{
likeRepository.deleteById(like.get().getId());
return LikeResponse.builder()
.userId(userId)
.postId(postId)
.isLiked(false)
.build();
}
else{
try {
likeRepository.deleteById(like.get().getId());
} catch (Exception e) {
throw new LikeFailureException("좋아요 취소에 실패했습니다.", e);
}
return LikeResponse.builder()
.userId(userId)
.postId(postId)
.isLiked(false)
.build();
}
🤖 Prompt for AI Agents
In backend/src/main/java/org/juniortown/backend/like/service/LikeService.java
around lines 47 to 56, the code deletes a like without handling potential
exceptions. Add exception handling around the likeRepository.deleteById call to
catch and properly handle any exceptions that may occur during the deletion
process, ensuring the service remains robust and errors are managed gracefully.


}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import org.juniortown.backend.post.dto.request.PostCreateRequest;
import org.juniortown.backend.post.dto.response.PageResponse;
import org.juniortown.backend.post.dto.response.PostSearchResponse;
import org.juniortown.backend.post.dto.response.PostWithLikeCount;
import org.juniortown.backend.post.dto.response.PostResponse;
import org.juniortown.backend.post.dto.response.PostWithLikeCountProjection;
import org.juniortown.backend.post.service.PostService;
import org.juniortown.backend.user.dto.CustomUserDetails;
import org.springframework.data.domain.Page;
Expand Down Expand Up @@ -54,9 +55,12 @@ public ResponseEntity<PostResponse> update(@AuthenticationPrincipal CustomUserDe

// 게시글 목록 조회, 페이지네이션 적용
@GetMapping("/posts/{page}")
public ResponseEntity<PageResponse<PostSearchResponse>> getPosts(@PathVariable int page) {
Page<PostSearchResponse> posts = postService.getPosts(page);
PageResponse<PostSearchResponse> response = new PageResponse<>(posts);
public ResponseEntity<PageResponse<PostWithLikeCountProjection>> getPosts(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable int page) {
Long userId = customUserDetails.getUserId();
// null체크

Page<PostWithLikeCountProjection> posts = postService.getPosts(userId, page);
PageResponse<PostWithLikeCountProjection> response = new PageResponse<>(posts);
return ResponseEntity.ok(response);
}
Comment on lines +58 to 65
Copy link

@coderabbitai coderabbitai bot Jul 8, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

인증 사용자 null 체크 구현 필요

주석에서 언급된 null 체크를 실제로 구현해야 합니다.

 public ResponseEntity<PageResponse<PostWithLikeCountProjection>> getPosts(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable int page) {
+  if (customUserDetails == null) {
+    throw new IllegalArgumentException("인증된 사용자 정보가 필요합니다.");
+  }
   Long userId = customUserDetails.getUserId();
-  // null체크
+  if (userId == null) {
+    throw new IllegalArgumentException("사용자 ID가 없습니다.");
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public ResponseEntity<PageResponse<PostWithLikeCountProjection>> getPosts(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable int page) {
Long userId = customUserDetails.getUserId();
// null체크
Page<PostWithLikeCountProjection> posts = postService.getPosts(userId, page);
PageResponse<PostWithLikeCountProjection> response = new PageResponse<>(posts);
return ResponseEntity.ok(response);
}
public ResponseEntity<PageResponse<PostWithLikeCountProjection>> getPosts(
@AuthenticationPrincipal CustomUserDetails customUserDetails,
@PathVariable int page
) {
if (customUserDetails == null) {
throw new IllegalArgumentException("인증된 사용자 정보가 필요합니다.");
}
Long userId = customUserDetails.getUserId();
if (userId == null) {
throw new IllegalArgumentException("사용자 ID가 없습니다.");
}
Page<PostWithLikeCountProjection> posts = postService.getPosts(userId, page);
PageResponse<PostWithLikeCountProjection> response = new PageResponse<>(posts);
return ResponseEntity.ok(response);
}
🤖 Prompt for AI Agents
In
backend/src/main/java/org/juniortown/backend/post/controller/PostController.java
around lines 58 to 65, the code has a comment about null checking the
authenticated user but lacks the actual implementation. Add a null check for the
customUserDetails parameter at the start of the method, and if it is null,
return an appropriate error response such as
ResponseEntity.status(HttpStatus.UNAUTHORIZED).build() to handle unauthenticated
access properly.

Copy link
Owner Author

Choose a reason for hiding this comment

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

이거 따로 공부해야 하긴함

Copy link

Choose a reason for hiding this comment

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

@Doncham 네, 맞습니다! Spring Security의 인증 처리는 별도로 공부하시는 것이 좋겠네요.

주요 학습 포인트들을 정리해드리면:

  1. @AuthenticationPrincipal 어노테이션의 동작 원리
  2. Spring Security의 인증 필터 체인
  3. JWT 토큰 검증과 사용자 인증
  4. 예외 처리와 에러 응답 설계

당장은 주석으로 남겨두시고, 나중에 Spring Security를 제대로 학습하신 후에 적용하시면 될 것 같습니다.

🐰 계속 배우려는 자세가 훌륭하네요! 차근차근 공부하시면서 진행하세요.


✏️ Learnings added
Learnt from: Doncham
PR: Doncham/JuniorTown#10
File: backend/src/main/java/org/juniortown/backend/post/controller/PostController.java:58-65
Timestamp: 2025-07-08T15:09:35.094Z
Learning: 사용자 Doncham은 Spring Security와 인증 처리에 대해 별도로 공부하는 것을 선호하며, 당장 구현보다는 학습을 통해 제대로 이해한 후 적용하는 접근 방식을 취한다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Doncham
PR: Doncham/JuniorTown#10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.329Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,35 @@
import lombok.Getter;

@Getter
public class PostSearchResponse {
public class PostWithLikeCount {
private Long id;
private String title;
private String username;
private Long userId;
private Long likeCount;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime deletedAt;

public PostSearchResponse(Post post) {
public PostWithLikeCount(Post post) {
this.id = post.getId();
this.title = post.getTitle();
// 이거 null 체크 어지럽네
this.username = post.getUser().getName();
this.userId = post.getUser().getId();
this.createdAt = post.getCreatedAt();
this.updatedAt = post.getUpdatedAt();
this.deletedAt = post.getDeletedAt();
}
Comment on lines +20 to 28
Copy link

@coderabbitai coderabbitai bot Jul 8, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

likeCount 필드 초기화 누락

Post 객체를 받는 생성자에서 likeCount 필드가 초기화되지 않아 null 값이 될 수 있습니다. 이는 NullPointerException을 발생시킬 수 있습니다.

다음 수정을 제안합니다:

 public PostWithLikeCount(Post post) {
 	this.id = post.getId();
 	this.title = post.getTitle();
 	this.username = post.getUser().getName();
 	this.userId = post.getUser().getId();
+	this.likeCount = 0L; // 기본값 설정
 	this.createdAt = post.getCreatedAt();
 	this.updatedAt = post.getUpdatedAt();
 	this.deletedAt = post.getDeletedAt();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public PostWithLikeCount(Post post) {
this.id = post.getId();
this.title = post.getTitle();
// 이거 null 체크 어지럽네
this.username = post.getUser().getName();
this.userId = post.getUser().getId();
this.createdAt = post.getCreatedAt();
this.updatedAt = post.getUpdatedAt();
this.deletedAt = post.getDeletedAt();
}
public PostWithLikeCount(Post post) {
this.id = post.getId();
this.title = post.getTitle();
this.username = post.getUser().getName();
this.userId = post.getUser().getId();
this.likeCount = 0L; // 기본값 설정
this.createdAt = post.getCreatedAt();
this.updatedAt = post.getUpdatedAt();
this.deletedAt = post.getDeletedAt();
}
🤖 Prompt for AI Agents
In
backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java
around lines 20 to 28, the constructor that takes a Post object does not
initialize the likeCount field, which can lead to null values and potential
NullPointerExceptions. To fix this, retrieve the like count from the Post object
(e.g., post.getLikeCount()) and assign it to the likeCount field within the
constructor.

Copy link
Owner Author

Choose a reason for hiding this comment

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

이 코드는 사실상 사용을 하지 않음. JPA가 인터페이스 기반 프로젝션을 통해 구현체를 직접 주입해주기 때문에 삭제해도 무방할듯

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@Doncham 맞습니다! 인터페이스 기반 프로젝션 방식이 훨씬 깔끔하네요.

확인해보겠습니다:

인터페이스 기반 프로젝션을 사용하면 JPA가 런타임에 구현체를 생성해주므로 별도의 DTO 클래스가 필요 없죠. 더 간결하고 효율적인 접근 방식입니다.


🏁 Script executed:

#!/bin/bash
# PostWithLikeCount 클래스의 사용처 확인
echo "=== PostWithLikeCount 클래스 사용처 검색 ==="
rg "PostWithLikeCount" --type java -A 2 -B 2

echo -e "\n=== PostWithLikeCountProjection 인터페이스 사용처 검색 ==="
rg "PostWithLikeCountProjection" --type java -A 2 -B 2

Length of output: 22763


@Doncham
확인 결과 PostWithLikeCount 클래스는 실제 로직에서 참조되지 않고, PostService·PostController에서 import만 남아 있으며 구현은 전부 PostWithLikeCountProjection을 사용하고 있습니다. 안전하게 삭제해도 무방하겠습니다. (불필요한 import·주석도 함께 정리 권장) 👍🏻


✏️ Learnings added
Learnt from: Doncham
PR: Doncham/JuniorTown#10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.329Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


// 생성자 기반 DTO 매핑
public PostWithLikeCount(Long id, String title, String username, Long userId, Long likeCount, LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime deletedAt) {
this.id = id;
this.title = title;
this.username = username;
this.userId = userId;
this.likeCount = likeCount;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.deletedAt = deletedAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.juniortown.backend.post.dto.response;

import java.time.LocalDateTime;

public interface PostWithLikeCountProjection {
Long getId();
String getTitle();
String getUsername();
Long getUserId();
Long getLikeCount();
Boolean getIsLiked();
LocalDateTime getCreatedAt();
LocalDateTime getUpdatedAt();
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
package org.juniortown.backend.post.repository;



import org.juniortown.backend.post.dto.response.PostWithLikeCountProjection;
import org.juniortown.backend.post.entity.Post;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface PostRepository extends JpaRepository<Post, Long>, PostRepositoryCustom {
Page<Post> findAllByDeletedAtIsNull(Pageable pageable);

@Query(
"SELECT p.id AS id, p.title AS title, u.name AS username, u.id AS userId, COUNT(l.id) AS likeCount, " +
"p.createdAt AS createdAt, p.updatedAt AS updatedAt, " +
"CASE WHEN EXISTS (" +
" SELECT 1 FROM Like l2 WHERE l2.post.id = p.id AND l2.user.id = :userId" +
") THEN true ELSE false END AS isLiked " +
"FROM Post p " +
"JOIN p.user u " +
"LEFT JOIN Like l ON l.post.id = p.id " +
"WHERE p.deletedAt IS NULL " +
"GROUP BY p.id, p.title, u.name, u.id, p.createdAt, p.updatedAt, p.deletedAt"
)
Page<PostWithLikeCountProjection> findAllWithLikeCount(@Param("userId") Long userId, Pageable pageable);
Comment on lines +16 to +28
Copy link

@coderabbitai coderabbitai bot Jul 8, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

성능 최적화 고려사항

복잡한 JPQL 쿼리이지만 기능적으로는 올바릅니다. 성능 최적화를 위한 몇 가지 제안사항이 있습니다:

  1. EXISTS 서브쿼리는 성능에 영향을 줄 수 있습니다
  2. likes 테이블의 user_id, post_id에 대한 복합 인덱스가 필요합니다
  3. posts 테이블의 deleted_at 컬럼에 인덱스가 필요합니다

더 나은 성능을 위해 다음과 같은 데이터베이스 인덱스를 추가하세요:

CREATE INDEX idx_likes_user_post ON likes(user_id, post_id);
CREATE INDEX idx_posts_deleted_at ON posts(deleted_at);
🤖 Prompt for AI Agents
In
backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java
around lines 16 to 28, the JPQL query uses an EXISTS subquery which may impact
performance. To optimize, add database indexes on the likes table for the
composite columns (user_id, post_id) and on the posts table for the deleted_at
column. Specifically, create the indexes idx_likes_user_post on likes(user_id,
post_id) and idx_posts_deleted_at on posts(deleted_at) to improve query
efficiency.

Copy link
Owner Author

Choose a reason for hiding this comment

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

성능 테스트하면서 진행할게요

Copy link

Choose a reason for hiding this comment

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

@Doncham 좋은 접근 방식이네요! 성능 테스트를 통해 실제 병목 지점을 파악하시는 것이 올바른 최적화 방법입니다.

테스트 시 다음 사항들을 모니터링하시면 도움이 될 것 같습니다:

  • 쿼리 실행 시간
  • 대량 데이터에서의 응답 시간
  • 동시 요청 처리 성능

필요하시면 언제든지 추가 조언을 요청해 주세요! 📊


🧠 Learnings used
Learnt from: Doncham
PR: Doncham/JuniorTown#10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.329Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.



}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import java.time.Clock;

import org.juniortown.backend.post.dto.response.PostSearchResponse;
import org.juniortown.backend.post.dto.response.PostWithLikeCount;
import org.juniortown.backend.post.dto.response.PostWithLikeCountProjection;
import org.juniortown.backend.post.exception.PostNotFoundException;
import org.juniortown.backend.post.dto.request.PostCreateRequest;
import org.juniortown.backend.post.dto.response.PostResponse;
Expand Down Expand Up @@ -71,11 +72,11 @@ public PostResponse update(Long postId, Long userId, PostCreateRequest postCreat
return new PostResponse(post);
}
@Transactional(readOnly = true)
public Page<PostSearchResponse> getPosts(int page) {
public Page<PostWithLikeCountProjection> getPosts(Long userId, int page) {
Pageable pageable = PageRequest.of(page, PAGE_SIZE, Sort.by("createdAt").descending());
Page<Post> postPage = postRepository.findAllByDeletedAtIsNull(pageable);

return postPage.map(post -> new PostSearchResponse(post));
Page<PostWithLikeCountProjection> postPage = postRepository.findAllWithLikeCount(userId, pageable);
return postPage;
//return postPage.map(post -> new PostWithLikeCount(post));
}
@Transactional(readOnly = true)
public PostResponse getPost(Long postId) {
Expand Down
Loading