💻Today's Schedule
개천절
✍ Today I Learned
CODEKATA
- 알고리즘 (72번 달리기 경주)
[프로그래머스/JAVA] 달리기경주
문제 설명 얀에서는 매년 달리기 경주가 열립니다. 해설진들은 선수들이 자기 바로 앞의 선수를 추월할 때 추월한 선수의 이름을 부릅니다. 예를 들어 1등부터 3등까지 "mumu", "soe", "poe" 선수들이
fargoewave.tistory.com
개인과제 Level 2-1 JPA Cascade
문제
앗❗ 실수로 코드를 지웠어요!
- 할 일을 새로 저장할 시, 할 일을 생성한 유저는 담당자로 자동 등록되어야 합니다.
- JPA의 `cascade` 기능을 활용해할 일을 생성한 유저가 담당자로 등록될 수 있게 해 주세요.
@OneToMany(mappedBy = "todo")
private List<Manager> managers = new ArrayList<>();
풀이
- @OneToMany 관계에서 cascade = CascadeType.ALL 옵션을 추가해, Todo 엔터티가 저장될 때 Manager 엔터티도 함께 저장되도록 수정했다.
- cascade = CascadeType.ALL : @OneToMany 매핑된 managers 필드에 CascadeType.ALL을 추가하여, Todo 엔터티가 저장될 때 Manager 엔터티도 함께 저장된다.
- orphanRemoval = true : 불필요한 Manager 엔터티가 Todo에서 삭제될 때 자동으로 삭제된다.
@OneToMany(mappedBy = "todo", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Manager> managers = new ArrayList<>();
public Todo(String title, String contents, String weather, User user) {
this.title = title;
this.contents = contents;
this.weather = weather;
this.user = user;
this.managers.add(new Manager(user, this));
}
개인과제 Level 2-2 N+1
문제
CommentController 클래스의 getComments() API를 호출할 때 N+1 문제가 발생하고 있어요. N+1 문제란, 데이터베이스 쿼리 성능 저하를 일으키는 대표적인 문제 중 하나로, 특히 연관된 엔티티를 조회할 때 발생해요.
해당 문제가 발생하지 않도록 코드를 수정해 주세요.
@RestController
@RequiredArgsConstructor
public class CommentController {
private final CommentService commentService;
@PostMapping("/todos/{todoId}/comments")
public ResponseEntity<CommentSaveResponse> saveComment(
@Auth AuthUser authUser,
@PathVariable long todoId,
@Valid @RequestBody CommentSaveRequest commentSaveRequest
) {
return ResponseEntity.ok(commentService.saveComment(authUser, todoId, commentSaveRequest));
}
@GetMapping("/todos/{todoId}/comments")
public ResponseEntity<List<CommentResponse>> getComments(@PathVariable long todoId) {
return ResponseEntity.ok(commentService.getComments(todoId));
}
}
public interface CommentRepository extends JpaRepository<Comment, Long> {
@Query("SELECT c FROM Comment c JOIN c.user WHERE c.todo.id = :todoId")
List<Comment> findByTodoIdWithUser(@Param("todoId") Long todoId);
}
풀이
- N+1 문제가 발생할 수 있는 부분은 CommentRepository의 findByTodoIdWithUser 메서드가 Comment와 User만 조회하고 있을 때, Comment에 추가로 연관된 다른 엔티티가 있을 경우 각각의 댓글마다 추가 쿼리가 실행되고 있는 부분이다.
- 이를 해결하기 위해서 fetch join로 수정해 Comment와 관련된 연관 엔티티들을 한 번에 가져오도록 수정했다.
- fetch join을 사용하면 연관된 엔티티를 함께 조회할 수 있다. 이를 통해 여러 쿼리를 실행하는 대신 한 번의 쿼리로 연관된 모든 엔티티를 함께 가져온다.
public interface CommentRepository extends JpaRepository<Comment, Long> {
@Query("SELECT c FROM Comment c JOIN c.user WHERE c.todo.id = :todoId")
List<Comment> findByTodoIdWithUser(@Param("todoId") Long todoId);
}
N+1 문제란?
- N+1 문제는 데이터베이스 성능을 저하시키는 대표적인 문제로, 특히 ORM(Object-Relational Mapping)을 사용하는 환경에서 자주 발생한다. 이 문제는 한 번의 쿼리로 조회된 엔티티와 연관된 엔티티들을 추가로 조회할 때 발생하는 쿼리 오버헤드로 인해 성능이 떨어지는 현상을 의미한다.
- N+1 문제는 처음에 1번의 쿼리로 데이터를 조회했지만, 그와 연관된 엔티티를 각각 조회하기 위해 N번의 추가 쿼리가 발생하는 상황을 의미하는데 예를 들어, 부모 엔티티 1개를 조회하는 쿼리에서 자식 엔티티를 조회할 때, 자식 엔티티의 개수만큼 추가 쿼리가 발생하는 것이 대표적인 예이다.
// 처음 1번 쿼리: 모든 댓글을 조회
SELECT * FROM comments WHERE todo_id = 1;
// N번의 추가 쿼리: 각 댓글에 연결된 사용자 조회
SELECT * FROM users WHERE user_id = 1;
SELECT * FROM users WHERE user_id = 2;
SELECT * FROM users WHERE user_id = 3;
...
- 처음 1번의 쿼리로 모든 댓글을 가져왔지만, 각 댓글에 연결된 사용자를 조회하기 위해 추가로 N개의 쿼리가 실행된다. 여기서 N은 조회된 댓글을 수이다. 이처럼 엔티티 수만큼 추가적인 쿼리가 발생하는 상황을 N+1 문제라고 한다.
Fetch join을 사용하지 않고도 N+1 문제를 해결할 수 있을까?
Fetch Join
Fetch Join을 사용하면 한 번의 쿼리로 연관된 엔티티를 함께 조회할 수 있다.
- 사용 상황
- 연관된 엔티티를 한 번의 쿼리로 모두 가져와야 할 때
- 즉시 로딩이 필요하고, 연관된 데이터의 양이 적을 때
// CommentRepository에서 fetch join을 사용해 Comment와 User를 한 번에 조회
@Query("SELECT c FROM Comment c JOIN FETCH c.user WHERE c.todo.id = :todoId")
List<Comment> findByTodoIdWithUser(@Param("todoId") Long todoId);
- 장점
- 한 번의 쿼리로 연관된 엔티티를 모두 로드할 수 있어 N+1 문제를 해결할 수 있다.
- 성능이 향상되고, 추가적인 쿼리가 발생하지 않는다.
- 단점
- 연관된 데이터가 많을 경우 메모리 사용량이 증가할 수 있다
- 복잡한 연관관계가 있을 경우, 쿼리 성능이 오히려 저하될 수 있다.
Betch Fetching
Batch Fetching은 한 번에 여러 엔티티를 가져오는 방식으로, 연관된 엔티티를 일괄처리하여 쿼리 성능을 개선할 수 있다.
- 연관된 데이터가 많을 때, 쿼리를 효율적으로 나눠서 처리하고 싶을 때
- 데이터가 많을 경우, 한 번에 많은 데이터를 처리하는 것을 피하고 싶을 때
// Comment 엔티티에 Batch Fetching 설정
@Entity
@BatchSize(size = 10) // 한 번에 10개의 엔티티를 로드
public class Comment {
// 엔티티 필드
}
//또는 전역 설정으로 application.properties 파일에서 설정
spring.jpa.properties.hibernate.default_batch_fetch_size=10
- 장점
- 쿼리 횟수를 줄여 성능을 최적화할 수 있다.
- 한 번에 적절한 크기의 데이터를 처리하므로 메모리 효율이 좋다.
- 단점
- 연관된 엔티티가 많을 경우 배치 크기를 잘못 설정하면 성능 저하가 발생할 수 있다.
- 모든 상황에서 최적의 성능을 보장하지는 않으며, 조건에 따라 추가적인 최적화가 필요할 수 있다.
EntityGraph
EntityGraph는 특정 쿼리에서 필요한 연관된 엔티티만 선택적으로 즉시 로딩할 수 있도록 도와준다.
- 사용 상황
- JPQL을 사용하지 않고도 연관된 엔티티를 선택적으로 즉시 로딩하고 싶을 때.
- 동적 쿼리 작성 없이 성능을 최적화하고 싶을 때.
// EntityGraph를 사용해 Comment와 User를 함께 로드
@EntityGraph(attributePaths = {"user"})
List<Comment> findByTodoId(Long todoId);
- 장점
- JPQL 없이도 연관된 엔티티를 한 번에 로드할 수 있다.
- 필요한 엔티티만 로드하여 불필요한 데이터 로딩을 방지할 수 있다.
- 단점
- 복잡한 쿼리 조건이 필요한 경우에는 적합하지 않을 수 있다.
- Fetch Join과 달리 더 많은 메모리 리소스를 사용할 수 있다.
QueryDSL
QueryDSL은 동적 쿼리를 작성할 때 유용한 라이브러리로, N+1 문제를 유연하게 해결할 수 있다.
- 사용 상황
- 동적 쿼리를 사용해야 하고, 타입 안전성도 필요할 때.
- 비즈니스 로직이 복잡한 경우.
// QueryDSL을 사용하여 Comment와 User를 함께 조회
public List<Comment> findByTodoIdWithUser(Long todoId) {
QComment comment = QComment.comment;
QUser user = QUser.user;
return queryFactory.selectFrom(comment)
.leftJoin(comment.user, user).fetchJoin()
.where(comment.todo.id.eq(todoId))
.fetch();
}
- 장점
- 동적 쿼리 작성이 가능하며, 복잡한 조건 처리도 용이하다.
- Fetch Join과 결합하여 성능 최적화를 할 수 있다.
- 단점
- 프로젝트에 추가적인 설정 및 의존성이 필요하다.
Lazy Loading 최적화
Lazy Loading은 연관된 엔티티를 필요한 시점에만 로드하는 방식이다. 이를 통해 불필요한 로딩을 방지할 수 있지만, 잘못 사용하면 N+1 문제가 발생할 수 있다.
- 사용 상황
- 데이터 양이 많을 경우, 연관된 엔티티를 필요한 시점에만 로드해야 할 때.
- 즉시 로딩이 오히려 성능에 문제를 일으킬 수 있을 때.
// 기본적으로 @OneToMany나 @ManyToOne 관계에서 Lazy Loading이 설정된다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
- 장점
- 필요한 시점에만 데이터를 가져와 메모리 사용량을 줄인다.
- 즉시 로딩에 비해 유연하게 데이터를 관리할 수 있다.
- 단점
- 지연 로딩이 잘못 사용되면 N+1 문제가 발생할 수 있다.
- 실제로 사용되지 않는 데이터를 가져오기 위해 추가적인 쿼리가 발생할 수 있다.
📝 회고
과제 마감 기한이 길어서 오늘은 과제 대신 도커 학습을 해보려고 했는데,
다음 레벨에 다뤄보지 않았던 쿼리 DSL이나 Spring Security 내용이 있어서 진도를 조금 더 나갔다.
다음 주에 모의면접도 있고, 개인 일정이 좀 있어서 생각보다 부지런하게 마무리해야겠다.
그리고 그동안 N+1 문제에 대해서 동기들이 의견을 나눌 때도 크게 생각해보지 않았는데
과제에 문제가 포함되어 있어 한번 더 학습했다.
사실 모의면접 예상 질문에도 이 문제가 포함되어 있는데, 모의 면접을 이번 과제를 통해 준비하려고
문제를 해결하는 여러 가지 방법을 학습하느라 포스팅이 좀 길어졌다.
역시 학습은 바로바로 ;; 편식하지 말구 ;;
🔖 Tomorrow's Goal
- 필수 과제 끝내기!