참조를 통한 연결관계는 언제나 단방향이다. 따라서 객체간에 연관관계를 양방향으로 만들고 싶다면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다. 이렇게 양쪽에서 서로 참조하는 것을 양방향 연관관계라고 한다. 양방향 연결관계는 엄밀히 말하자면 단방향 관계 2개다.
반면 테이블에서는 외래 키 하나로 양방향으로 조인이 가능하다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
private String userName;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "TEAM_ID")
private Long teamId;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
Member 엔티티 (N:1)
@ManyToOne
: 다대일(N:1) 관계 매핑정보, featch = FetchType.LAZY
속성을 추가하면 team 데이터가 실제로 필요할 때만 DB를 조회한다.
@JoinColumn
: 외래 키를 매핑할 때 사용, name
속성은 매핑할 외래 키의 이름을 지정한다(생략가능).
Team 엔티티 (1:N)
@OneToMany
: 일대다(1:N) 관계 매핑정보, mappedBy
속성은 양방향 매핑일 때 사용하는데, 반대쪽 매핑의 필드 이름을 값으로 준다.
단방향 매핑만으로도 테이블과 객체의 연관관계 매핑이 완료되지만, 위와 같이 양방향으로 구성하면 객체 그래프 탐색 기능이 추가된다
단방향 매핑을 할 때, 일대다(1:N) 관계에서는 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점 때문에 INSERT
SQL문을 추가로 실행해야 하는 단점이 있다. 때문에 단방향 매핑이라면 가급적이면 다대일(N:1) 연관관계를 구성하는 것이 좋다.
연관관계의 주인
엔티티를 양방향 연관관계로 설정하면 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 한다(객체와 달리 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리하기 때문). 이것을 연관관계의 주인 이라고 한다.
연관관계의 주인만이 DB 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있고, 주인이 아닌 쪽은 읽기만 가능하다.
연관관계의 주인을 정한다는 것은 외래 키 관리자를 선택하는 것이다.
엔티티를 조회할 때 연관된 엔티티들이 항상 조회되는 것은 아니다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
private String userName;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "TEAM_ID")
private Long teamId;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
Member 엔티티를 다루는 로직에서 Team 엔티티를 전혀 사용하지 않는다면, 연관된 팀 엔티티까지 DB에서 함께 조회하는 것은 효율적이지 않다.
JPA는 이런 문제를 해결하기 위해 엔티티가 실제로 사용될 때까지 DB 조회를 지연하는 지연로딩
방식을 사용한다.
이 때 실제 엔티티 객체 대신에 DB 조회를 지연할 수 있는 가짜 객체를 프록시 객체
라고 한다.
위에서 설명한 프록시 객체는 주로 연관된 엔티티를 지연 로딩할 때 사용한다.
즉시로딩
: 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
@ManyToOne(fetch = FetchType.EAGER)
지연로딩
: 연관된 엔티티를 실제 사용할 때 조회한다.
@ManyToOne(fetch = FetchType.LAZY)
NULL 제약조건과 JPA 조인 전략
@JoinColumn에
nullable = false
속성을 이용하면 외래 키가 NULL 값을 허용하지 않음을 JPA에게 알려주어 JOIN 시에 INNER JOIN을 사용한다.정리하자면 JPA는 선택적 관계면 외부 조인을, 필수 관계면 내부 조인을 사용한다.
JPA는 CASCADE 옵션을 통해 영속성 전이를 제공한다.
영속성 전이를 사용하면 부모 엔티티가 저장될 떄 자식 엔티티도 함께 저장할 수 있다.
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
이럴 때 영속성 전이를 사용하면 부모만 영속 상태로 만들면 연관된 자식까지 한 번에 영속 상태로 만들 수 있다.
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<>();
}
@Entity
public class Child {
@Id @GeneratedValue
private Long id;
@ManyToOne
private Parent parent;
}
cascade = CascadeType.PERSIST
옵션을 사용하면 부모를 영속화할 때 연관된 자식들도 함께 영속화된다(부모 저장, 연관된 자식들도 저장).
영속화는 연관관계 매핑과는 아무런 관련이 없다. 그저 연관된 엔티티를 같이 영속화하는 편리함을 제공할 뿐이다.
cascade = CascadeType.REMOVE
옵션을 설정하면 부모 엔티티를 삭제할 경우 연관된 자식 엔티티들도 함께 삭제된다. 삭제 순서는 외래 키 제약조건을 고려하여 자식을 먼저 삭제하고 부모를 삭제한다.
이 옵션을 설정하지 않고 부모 엔티티를 삭제하는 경우, 자식 테이블에 설정된 외래 키 제약조건에 의해 DB에서 외래 키 무결성 예외가 발생한다.
JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공한다. 이것을 고아 객체 제거라 한다.
orphanRemoval = true
옵션을 설정하면 참조가 제거된 엔티티는 고아 객체로 보고 삭제한다. 이 기능은 특정 엔티티가 개인 소유하는 엔티티에만 사용할 수 있다. 따라서 orphanRemoval
옵션은 @OneToOne
, @OneToMany
에만 사용할 수 있다.
CascadeType.REMOVE와 orphanRemoval = true의 차이
CascadeType.REMOVE와 orphanRemoval = true모두 부모 엔티티가 삭제되면 연관된 자식 엔티티가 삭제된다.
하지만, CascadeType.REMOVE의 경우에는 부모 엔티티가 자식 엔티티와의 관계를 제거해도 자식 엔티티는 삭제되지 않고 그대로 남아있다.
반면, orphanRemoval = true 옵션은 부모와 자식 엔티티의 관계가 끊어지면 자식 엔티티가 고아 객체로 판단되어 자식 엔티티가 삭제된다.
참고 자료: 자바 ORM표준 JPA 프로그래밍 - 김영한 저