객체 모델과 관계형 데이터베이스 모델 간의 불일치를 해결하면서 효율적인 데이터 관리를 가능하게 함
일대일(One-to-One)관계
- 하나의 엔티티가 다른 엔티티와 1:1로 매핑되는 관계
- @OneToOne 어노테이션을 사용하여 정의
- 예제
@Entity
public class User {
@Id @GeneratedValue
private Long id;
@OneToOne
private Profile profile;
}
일대다(One-to-Many)관계
- 하나의 엔티티가 여러 엔티티와 열결되는 관계
- 단방향 관계 설정이 어렵고, 양방향 매핑을 많이 사용
- @OneToMany 어노테이션을 사용해 정의
- 예제
@Entity
public class User {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Post> posts = new ArrayList<>();
}
@Entity
public class Post {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
다대일(Many-To-One)관계
- 여러 엔티티가 하나의 엔티티와 열결되는 관계
- 외래 키가 포함된 테이블이 연관관계의 주인 역할을 함
- @ManyToOne 어노테이션을 사용해 정의
- 예제
@Entity
public class Post {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "author_id")
private Author author;
}
다대다(Many-To-Many)관계
- 여러 엔티티가 서로 다수와 연결되는 관계
- 중간 매개 테이블을 정의하여 다대다 관계를 사용하는 것이 일반적
- @ManyToMany 어노테이션을 사용해 정의
- 예제
@Entity
public class Student {
@Id @GeneratedValue
private Long id;
@ManyToMany
@JoinTable(name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses = new ArrayList<>();
}
고려사항
- 연관관계의 주인(owner)
- 관계의 주인이 되는 엔티티가 외래 키를 관리하며 데이터베이스와 직접적인 매핑 역할을 함
- mappedBy 속성은 주인이 아님을 명시
- 양방향과 단방향
- 양방향 관계 : 엔티티 간의 참조를 서로 설정
- 단방향 관계 : 하나의 엔티티만 다른 엔티티를 참조
주요 속성
- mappedBy : 연관관계의 주인이 아닌 엔티티에서 사용하여 양방향 관계 정의
- cascade : 연관된 엔티티를 함께 저장, 삭제
- fetch : 로딩 방벙 정의
- EAGER : 즉시 로딩
- 엔티티를 조회할 때 연관된 엔티티를 함께 가져옴
- 연관 엔티티가 많을 경우 성능 저하 발생
- LAZY : 지연 로딩
- 연관된 엔티티를 실제로 사용할 때 데이터를 가져옴
- 성능 최적화를 위해 기본적으로 사용
LAZY 전략
설명
- 연관된 데이터를 실제로 사용할 때 로딩하는 방식
- 필요하지 않는 데이터를 미리 로딩하지 않으므로 초기 로딩 시 성능 최적화 가능
- 연관된 데이터를 사용하지 않을 겨우 데이터베이스 접근을 최소화
- 메모리와 네트워크 자원을 절약하고 , 엔티티가 가벼워짐
- 실제 연관된 데이터를 참조하려는 시점에서 데이터베이스에서 쿼리를 실행함
- 연관 데이터를 사용하는 시점에 추가 쿼리가 발생, 영속성 컨텍스트가 닫혀 있으면 LazyInitializationException 발생 가능
- 보통 DB세션을 뷰 레이어까지 열어두게 되면 발생가능성이 높음(OSIV)
- 예제
@Entity
public class User {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Post> posts = new ArrayList<>();
}
User user = em.find(User.class, userId); // Post는 로딩되지 않음
List<Post> posts = user.getPosts(); // 여기서 추가 데이터베이스 쿼리가 실행됨
OSIV
- JPA의 영속성 컨텍스트는 데이터베이스 연결을 유지하며 엔티티를 관리
- OSIV는 이 영속성 컨텍스트를 트랜젝션이 끝난 후에도 뷰 레이어까지 확장하여 유지
- OSIV 활성화 상태
- 데이터베이스 세션이 Controller와 View까지 열려 있음
- 이로 인해 LAZY 로딩이 데이터 View에서 접근이 가능
- 뷰에서 지연로딩을 남발하면 다수의 추가 쿼리 문제가 발생
- 영속성 컨텍스트가 뷰까지 열려 있으면 데이터베이스 커넥션이 길게 유지 되어 자원소모가 커짐
- 대규모 트래픽 처리 시 병목이 발생
- 트랜잭션 범위를 벗어난 데이터 로딩으로 인해 설계가 불명확해질 가능성이 있음
- OSIV 비활성화 상태
- 데이터베이스 세션이 Service에서 트랜잭션이 종료되면 닫힘
- 트랜잭션 외부에서 LAZY 로딩 데이터를 접근하려고 하면 LazyInitializationException 발생
지연로딩의 동작 방식
- JPA에서 지연 로딩은 연관된 엔티티를 실제로 액세스할 때까지 로딩을 미룬다는 의미
- 지연 로딩 필드는 프록시 객체로 초기화, 해당 필드에 접근할 때 데이터베이스 쿼리가 실행
- 영속성 컨텍스트(EntityManager)가 닫힌 상태에서 지연 로딩을 호출하면 데이터베이스 접근이 불가능해져 LazyInitializationException이 발생
직렬화와의 충돌
- 지연 로딩된 필드는 Hibernate 프록시 객체로 존재
- 이 프록시 객체는 실제 데이터를 로드하기 위해 영속성 컨텍스트(EntityManager)를 필요
- 그러나 영속성 컨텍스트는 보통 직렬화 시점에 닫혀 있으므로 데이터 로딩이 불가능
- LazyInitializationException 또는 직렬화 오류가 발생
EAGER 전략
- 연관된 데이터를 즉시 로딩하는 방식
- 엔티티가 조회될 때, 연관된 엔티티를 함께 로딩
- 연관된 데이터를 자주 사용하는 경우 유용
- 데이터베이스에서 JOIN 쿼리를 사용하여 한 번에 가져오거나 별도의 추가 쿼리를 실행
- 연관 데이터를 함께 가져오기 때문에 추가적인 데이터베이스 쿼리가 줄어듦
- 코드가 간결하고 예측 가능
- 필요 없는 데이터를 미리 로딩하여 메모리 및 성능 낭비 가능
- 연관 데이터가 많아질 경우, 쿼리의 복잡도가 증가하거나 대량의 데이터를 메모리에 로드하게 됨
- 예제
@Entity
public class User {
@Id @GeneratedValue
private Long id;
@OneToOne(fetch = FetchType.EAGER)
private Profile profile;
}
User user = em.find(User.class, userId); // Profile이 함께 로딩됨
Share article