[스프링부트]JPA-연관관계매핑

송송승현's avatar
Nov 29, 2024
[스프링부트]JPA-연관관계매핑
💡
객체 모델과 관계형 데이터베이스 모델 간의 불일치를 해결하면서 효율적인 데이터 관리를 가능하게 함

일대일(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

송승현의 블로그