seaking110 님의 블로그
N+1 문제 + 동시성 제어 본문
N + 1 문제란?
- N + 1 문제는 ORM을 사용할 때 발생하는 성능 문제
- 하나의 쿼리로 N개의 객체를 로딩한 후 각 객체에 연관된 데이터를 추가로 조회하는 개별 쿼리가 n번 싱핼 되면서 총 N + 1번의 쿼리가 발생하는 문제
- 시스템의 확장성과 성능에 심각한 영향을 미칠 수 있는 구조적 문제
- 요청이 동시에 여러 사용자로부터 발생한다면 데이터베이스는 수만개의 쿼리를 추가 처리!
- FetchType.LAZY는 성능상의 이유로 사용해야 하지만 N+1 이 발생할 수 있음
예시
- 모든 책을 조회하는 쿼리 1회 실행
- 각 책에 대한 리뷰를 조회하는 N번의 추가 퀴리 실행
public void printBooksWithReviews() {
// 모든 책을 조회하는 쿼리 1회 실행
List<Book> books = bookRepository.findAll();
// 각 책에 대한 리뷰를 조회하는 N번의 추가 쿼리 실행
books.forEach(book -> {
// 해당 책의 ID를 사용하여 리뷰를 조회
List<Review> reviews = reviewRepository.findByBookId(book.getId());
});
}
- 위 코드는 절대 짜면 안되는 N + 1 형식의 코드
해결 방법
- JOIN FETCH 사용
- 관계가 있는 엔티티를 한 번의 쿼리로 함께 로드
- 매우 추천하는 방법
- 필요한 모든 데이터를 포함한 효율적인 쿼리를 생성하여 성능을 크게 향상!
- inner join과 비슷하나 inner join은 데이터베이스 레벨
- 배치 사이즈 설정
- 완전한 해결책은 아님
- 하지만 성능 향상을 해줌
- @BatchSize 어노테이션을 사용하여 한번에 로드할 연관 엔티티 수를 조정 가능
- DTO 사용
- 뷰나 API 응답으로 필요한 데이터만 선택적으로 전달
- 불필요한 데이터를 로드하지 않아 성능을 향상
- 매우 추천하나 근본적인 해결책은 아님
- Entity Graphs 사용
- 특정 쿼리에 대한 엔티티의 로딩 전략을 세밀하게 제어 가능
- 유연성을 제공하므로 추천
- JPA 2.1 부터 지원
- @EntityGraph(attributePaths = {"reviews}) 처럼 사용
- FetchType.EAGER 사용
- 연관된 엔티티가 항상 필요한 경우 미리 로드하여 지연이 발생하지 않도록 함
- FetchType.LAZY를 사용해야 성능이 뛰어나기 때문에 권장하지 않음
- 너무 많은 데이터를 불필요하게 로드하므로 특히 많은 연관관계가 있는 경우 성능 저하를 초래
총정리
N+1 해결 방법 | 사용 사례 | 권장 여부 | 주요 특징 | 주의점 |
JOIN FETCH | 관계가 있는 엔티티를 한번의 쿼리로 함께 로드 | 매우 추천 | 한번의 쿼리로 필요한 모든 데이터 로드 | 반환되는 데이터의 양이 많아짐 |
배치 사이즈 설정 | 대량의 연관 데이터를 로드 시 | 상황에 따라 선택 | N+1 퀴리 수를 줄이나단지 성능 향상 | 적절한 배치 크기를 설정해야함 |
DTO 사용 | 뷰나 API 응답에 필요한 데이터만 전달 | 매우 추천 | 불필요한 데이터 로드 방지 | 데이터 변환 과정이 필요 |
Entity Graphs | 특정 쿼리에서 필드 로드 방식을 제어 | 복잡한 경우 매우 추천 | 쿼리 세밀 제어 가능 | 복잡한 설정이 필요할 수 있음 |
FetchType.EAGER | 연관된 엔티티가 항상 필요한 경우 | 권장 X | 연관 렝니팉를 미리 로드하여 지연 없음 | 불필요한 데이터 로드로 성능 저하 가능성 |
동시성 제어
- 락 이란
- 주로 데이터베이스 관리 시스템이나 다중 스레딩 환경에서 사용하는 중요한 개념
- 특정 자원에 대한 접근을 제어하여 동시성을 관리하고 데이터의 일관성 및 무결성을 유지하는 메커니즘
- 자원 공유 : 여러 사용자나 프로세스가 동일한 데이터에 동시에 접근하려 할 때, 락은 이들 중 하나만이 데이터를 수정할 수 있도록 허용하여 데이터의 일관성을 보장
- 데이터 무결성 : 데이터베이스에서 트랜잭션이 실행되는 동안 데이터 무결성을 유지하기 위해 락이 사용됨
- 락의 종류
- 공유 락
- 데이터를 읽을 때 사용
- 공유 락이 걸린 데이터는 다른 사용자도 읽을 수 있지만 수정은 할 수 없음
- 독점 락
- ㅋ
- ㅋ
- 공유 락
비관적 동시성 제어 (Pessimistic Concurrency Control)
- 충돌이 발생할 것이라고 비관적으로 가정하고 데이터에 접근하기 전에 락을 사용하여 해당 데이터 보호
- 주로 데이터베이스 트랜잭션이 길거나 충돌 가능성이 높을 때 사용
- 데이터를 읽거나 수정하려는 동안 해당 데이터를 락을 걸고 다른 트랙잭션이 해당 데이터를 접근하는 것을 방지
- 장점 : 데이터 무결성을 확실히 보장
- 단점 : 락으로 인해 시스템의 처리 성능이 저하될 수 있으며, 데드락 발생 가능성이 있음
낙관적 동시성 제어 (Optimistic Concurrency Control)
- 낙관적 동시성 제어는 충돌이 자주 발생하지 않는다고 낙관적으로 가정하고 트랜잭션이 데이터를 커밋할 때만 충돌을 검사
- 주로 읽기 작업이 많고 쓰기 작업이 적을 때 적합
- 데이터에 대한 락 없이 트랜잭션을 수행하고 커밋 시점에 변경 사항이 있는지 확인하여 충돌을 검사
- 장점 : 동시성 수준이 높고 시스템의 처리 성능에 미치는 영향이 비교적 적음
- 단점 : 충돌 발견 시 수행한 모든 작업을 롤백해야함
데드락 (DEADLOCK)
- 두개 이상의 트랜잭션이 서로의 락을 기다리면서 무한 대기 상태에 빠지는 현상
- 각 트랜잭션이 다른 트랜잭션이 소유한 자원을 요구할 때 발생
- CPU 시간, 메모리, 파일, 장치 등의 자원에 대한 경쟁에서 발생
해결 방법
- 예방
- 데드락을 탐지하여 데드락에서 회복하기
- 트랜잭션 진행방향을 같은 방향으로 처리
- 트랜잭션 처리속도를 최소화
- SET LOCK_TIMEOUT : 잠금 해제 시간 설정
'Today I Learned' 카테고리의 다른 글
Cookie, JWT (0) | 2025.02.26 |
---|---|
테스트 코드! (0) | 2025.02.25 |
HttpMessageConverter란 무엇인가 (0) | 2025.02.21 |
뉴스 피드 팀 프로젝트를 마치며 (0) | 2025.02.20 |
[Git] Conventional Commits에 대해 (0) | 2025.02.18 |