코딩 이래요래
위클리 페이퍼 - 9주차 본문
Q. JPA에서 발생하는 N+1 문제의 발생 원인과 해결 방안에 대해 설명하세요.
JPA에서는 연관관계가 @ManyToOne, @OneToOne인 경우 fetchType의 default는 EAGER이다.
즉시 로딩이란, 예를 들어 User와 BinaryContent(Profile)의 연관관계가 @OneToOne일 경우, User를 조회하면 JPA는 내부적으로 BinaryContent까지 함께 조회하는 것을 말한다.
반면 fetchType을 LAZY로 설정하면, User를 조회할 때는 User 엔티티만 조회하는 쿼리를 작성하여 데이터를 조회하고,
이후에 user.getProfile()처럼 연관 필드에 실제 접근하는 순간에 BinaryContent를 SELECT 별도의 쿼리가 실행된다.
이를 지연로딩이라고 부른다.
예를 들어 List<User>를 조회한 뒤 각 User마다 user.getProfile()을 호출하면, 1개의 List<User> 조회 쿼리 + N개의 Profile 조회 쿼리가 발생하게 된다. 이를 N+1 문제라고 부르며, 대량 데이터 처리 시 성능 저하의 원인이 된다.
N+1 문제를 해결하기 위해서 아래와 같은 방법을 적용할 수 있다.
1. EntityGraph
- JPA 표준 기능으로, 엔티티에 정의된 연관 필드를 동적으로 fetch join 하도록 설정한다.
- @EntityGraph(attributePaths = {"profile"})
2. Fetch Join
- JPQL에서 JOIN FETCH 구문을 사용해 한 번의 쿼리로 연관 데이터를 모두 조회할 수 있다.
- @Query("SELECT u FROM User u JOIN FETCH u.profile")
3. Batch Size 설정 (Collection 지연 로딩 시)
- @OneToMany, @ManyToMany 컬렉션 로딩에 효과적
- Hibernate가 여러 개의 Lazy 엔티티를 IN 절로 한꺼번에 조회
1)application.yaml 설정 방법(전체 적용)
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 50
- 한 번에 최대 50개의 연관 엔티티를 IN 절로 묶어서 조회함
2) 엔티티별 설정
@OneToMany(mappedBy = "user")
@BatchSize(size = 50)
private List<Message> messages;
- 특정 필드에만 배치 사이즈를 적용
List<User> users = userRepository.findAll(); // 10명의 유저
for (User user : users) {
List<Message> messages = user.getMessages(); // @OneToMany
}
- 일반 Lazy 로딩 시
- 1번: SELECT * FROM user
- + 10번: SELECT * FROM message WHERE user_id = ? (10번 반복)
- batch_size = 50 설정 시
- 1번: SELECT * FROM user
- +1번: SELECT * FROM message WHERE user_id IN (?, ?, ..., ?) ← 10개 user_id를 IN절로 한 번에 조회
Q. 트랜잭션의 ACID 속성 중 격리성(Isolation)이 보장되지 않을 때 발생할 수 있는 문제점들을 설명하고, 이를 해결하기 위한 트랜잭션 격리 수준들을 설명하세요.
트랜잭션의 ACID 속성
약어 | 의미 | 설명 |
A | Atomicity (원자성) | 모든 작업이 전부 수행되거나, 전혀 수행되지 않아야 함 → 중간에 일부만 적용되는 상태는 허용되지 않음 |
C | Consistency (일관성) | 트랜잭션 실행 전후의 데이터 상태는 제약조건/규칙을 항상 만족해야 함 → 무결성 제약 조건 위반이 없어야 함 |
I | Isolation (격리성) | 동시에 여러 트랜잭션이 실행되더라도 서로 간섭 없이 독립적으로 수행되어야 함 → 트랜잭션 간의 간섭, Dirty Read 등 방지 |
D | Durability (지속성) | 트랜잭션이 커밋된 이후에는 시스템이 고장나더라도 데이터는 영구히 보존되어야 함 → 디스크에 안전하게 기록되어야 함 |
Isolation이 보장되지 않으면?
- Dirty Read : 다른 트랜잭션이 커밋하지 않은 데이터를 읽음
- A 트랜잭션이 수정한 데이터를 B 트랜잭션이 접근했는데, A가 ROLLBACK함
- Non-repeatable Read : 같은 데이터를 두 번 읽었을 때 값이 달라짐
- A 트랜잭션이 두 번 SELECT하는 사이 B가 UPDATE
- Phantom Read : 같은 조건의 SELECT에서 행 수가 달라짐
- A 트랜잭션이 where age > 20로 3명 조회 → B가 한 명 INSERT → A가 다시 조회하니 4명
트랜잭션 4가지 격리 수준
수준 | 설명 | 허용되는 현상 |
READ_UNCOMMITTED | 커밋되지 않은 데이터도 읽을 수 있음 | Dirty Read, Non-repeatable Read, Phantom Read 발생 |
READ_COMMITTED (Oracle, Spring 기본값) | 커밋된 데이터만 읽을 수 있음 | Non-repeatable Read, Phantom Read 발생 |
REPEATABLE_READ (MySQL 기본값) | 트랜잭션 동안 같은 행을 반복 조회하면 항상 같은 값 | Phantom Read 발생 |
SERIALIZABLE | 가장 엄격한 격리 수준. 트랜잭션을 순차적 실행처럼 동작시킴 | 모든 현상 방지 (가장 느림) |
대부분의 서비스는 READ_COMMITTED로 충분함. 하지만 정합성이 중요한 트랜잭션은 REPEATABLE_READ 또는 SERIALIZABLE로 올리고, Locking 기법을 병행하여 사용함
Locking 기법?
- Pessimistic Lock (비관적 락)
- 데이터를 읽거나 수정할 때 먼저 락을 걸고 다른 트랜잭션의 접근을 차단
- 다른 트랜잭션이 데이터를 수정할 가능성이 높다라고 가정하는 방식
- Optimistic Lock (낙관적 락)
- 락을 걸지 않고 우선 작업을 진행하고, 트랜잭션 종료 시점에 버전 정보를 비교하여 충돌 여부 판단
- 충돌이 잘 발생하지 않을 것이라고 가정하는 방식
- 동일 데이터를 동시에 수정한 두 트랜잭션 중 먼저 커밋한 쪽은 성공
- 나중에 커밋한 트랜잭션은 버전 불일치로 OptimisticLockException 발생
- ROLLBACK후 재시도
'위클리 페이퍼' 카테고리의 다른 글
위클리 페이퍼 11주차 (3) | 2025.06.30 |
---|---|
위클리 페이퍼 10주차 (5) | 2025.06.23 |
위클리 페이퍼 8주차 (3) | 2025.05.30 |
위클리 페이퍼 - 7주차 (3) | 2025.05.25 |
AOP (Aspect Oriented Programming) (0) | 2025.05.18 |