728x90
반응형
연관관계 매핑시 고려할 3가지
- 다중성(일대 다, 다대 일, 다대 다, 일대 일)
- 다대일(N : 1) : @ManyToOne
- 일대다(1 : N) : @OneToMany
- 일대일(1 : 1) : @OneToOne
- 다대다(N : N) : @ManyToMany
- 방향 (양방향, 단방향)
- DB 테이블은 외래 키(Foreign Key) 하나로 조인(Join)을 사용해서 양방향으로 쿼리가 가능합니다. 따라서 DB에는 방향의 개념이 없습니다.
- 그러나 객체의 경우, 참조용 필드를 가지고 있는 객체만 연관된 객체를 조회할 수 있으므로 방향의 개념이 존재합니다.
- 객체 관계에서 한쪽만 반대쪽을 참조하는 관계를 단방향, 양쪽 모두 서로를 참조하는 관계를 양방향이라고 합니다.
- 연관관계의 주인
- DB는 외래키(FK) 하나로 두 테이블이 연관관계를 맺습니다.
- 즉, 연관관계를 관리하는 포인트는 외래키 하나입니다.
- 반면 객체에서는 양방향 관계로 매핑하면 A -> B, B -> A, 두 곳에서 서로를 참조하므로 연관관계를 관리하는 포인트는 두 곳입니다.
- 따라서 JPA는 두 객체 중 하나를 정해서 외래키를 관리하게 만들어야 하는데, 여기서 외래키를 관리하는 객체를 연관관계의 주인이라고 합니다.
- 보통 외래키를 가진 테이블과 매핑되는 엔티티가 외래키를 관리하는 것이 효율적이므로, 보통 이곳을 연관관계의 주인으로 선택합니다.(일대 다 관계라면 다쪽에서 외래키를 관리)
- 주의할점은 외래키를 관리하는 연관관계의 주인만이 외래키를 변경할 수 있으며, 주인이 아닌 곳은 읽기만 가능하다는 것입니다.
무조건 양방향 관계를 하면 쉽지 않을까?
- 객체 입장에서 양방향 매핑을 했을 때 오히려 복잡해질 수 있습니다.
- 예를 들면, 사용자 엔티티는 보통 많은 엔티티와 연관 관계를 갖습니다. 이런 경우, 모든 엔티티를 양방향 관계로 설정하게 되면 사용자 엔티니는 엄청나게 많은 테이블과 연관 관계를 맺게 되고 많이 복잡해집니다.
- 그리고 다른 엔티티들도 불필요한 연관관계 매핑으로 인해 복잡성이 증가할 수 있습니다.
- 그래서 양방향으로 할지 단방향으로 할지 반드시 구분해줘야합니다.
- 저도 단방향으로 할지, 양방향으로 할지 로직을 짤 때 많이 헷갈렸었는데 일단 DB를 구성할 때 많이 참조하는 쪽, 예를 들면 사용자 테이블과 주문내역 테이블이 있을 때, 사용자 : 주문내역 = 1 : N관계를 갖게되는데 보통 사용자가 어떤 주문내역을 가지고 있는지를 주로 필요하기 때문에 일대 다 양방향 관계를 적용시킵니다.
- 단방향이 아닌 양방향 매핑을 하는 이유는 주문내역에서 외래키를 관리하기 때문입니다.
다대일(N : 1) - @ManyToOne
@Entity
public class Member{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
public class Team{
@Id @GenratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
}
- 회원은 Member.team으로 팀 엔티티를 참조할 수 있지만, 반대로 팀에는 회원을 참조하는 필드가 없습니다.
- 따라서 회원과 팀은 다대일 단방향 연관관계입니다.
@ManyToOne
@JoinColumn(name = "TEAM_ID")
- 다대일 연관관계이므로 @ManyToOne을 사용하였습니다.
- @JoinColumn을 사용하여 Member.team 필드를 TEAM_ID 외래키와 매핑하였습니다.
외래키로 매핑할 컬럼 지정하는 방법
- referencedColumnName을 통해 대상 테이블의 어떠한 컬럼을 FK로 사용할지 지정할 수 있습니다.
- 기본값은 참조하는 테이블의 기본키(PK) 컬럼명이므로 자동으로 기본키가 외래키로 매핑됩니다.
- name 속성은 연관관계의 주인 테이블에 FK를 저장할 컬럼명을 지정하는 것입니다.
- 예를 들어 referencedColumnName = "name"이며, name = "fk_name"인 경우, 대상 테이블의 name 컬럼 값을 FK로 사용하며, fk_name 컬럼에 저장됩니다.
@Entity
class Member{
@Id
@Column(name = "MEMBER_ID")
@GeneratedValue(stretegy = GenerationType.IDENTITY)
private Long id;
private String name;
}
@Entity
class Order{
@Id
@Column(name = "ORDER_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID", referencedColumnName = "name")
private Member member;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus;
}
다대일 양방향
@Entity
public class Team{
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
@OneToMany(mappedBy = "team")
- 위 어노테이션을 통해, 연관관계의 주인, 즉 외래키를 관리하는 필드(Member의 team 필드)를 명시했습니다.
- 연관관계의 주인을 지정하는 것은 양방향관계에서 어떤 엔티티에서 제어의 권한(외래키를 비롯한 테이블 레코드를 저장, 수정, 삭제 처리)를 갖는 실질적인 관계가 어떤 것인지 JPA에게 알려준다고 생각하면 됩니다.
- 연관관계의 주인은 연관관계를 갖는 두 객체 사이에서 조회, 저장, 수정, 삭제를 할 수 있지만, 연관 관계의 주인이 아니면 조회만 가능합니다.
- 연관관계의 주인이 아닌 객체에서 mappedBy 속성을 사용해서 주인을 지정해줘야 합니다.
- 외래키가 있는 곳을 연관관계의 주인으로 정하면 됩니다.
왜 연관 관계의 주인을 지정해야하는가?
- 두 객체(Board, Post)가 있고 양방향 연관관계를 갖는다고 생각해봅니다.
- 게시글(Post)의 게시판을 다른 게시판(Board)로 수정하려고 할 때, Post 객체에서 setBoard(...) 같은 메소드를 이용해서 수정하는게 맞는지, Board 객체에서 getPosts() 같은 메소드를 이용해서 List의 게시글을 수정하는게 맞는지 헷갈릴 수 있습니다.
- 두 객체 입장에서 두 방법 모두 맞지만 이렇게 객체에서 연관관계 관리 포인트가 두개일때는 테이블과 매핑을 담당하는 JPA입장에서는 혼란을 주게 됩니다.
- 즉, Post에서 Board를 수정할 때, FK를 수정할 지, Board에서 Post를 수정할 때 FK를 수정할지를 결정하기 어렵습니다.
- 그렇기 때문에 두 객체 사이의 연관 관계의 주인을 정해서 명확하게 Post에서 Board를 수정할 때만 FK를 수정하겠다! 라고 정하는 것입니다.
일대다(1:N) : @OneToMany
- 일대다 단방향 관계는 특이하게 Team 엔티티의 Team.members로 MEMBER 테이블의 TEAM_ID 외래 키를 관리합니다.
- MEMBER에서 FK를 관리하지만 Team 객체에서 다대일 단방향 관계를 매핑한다면 Team 객체에서는 MEMBER 테이블에서 관리하는 FK를 수정하기 위해 Team 객체를 저장할 방법이 없기 때문에 INSERT 또는 UPDATE한 후 MEMBER를 UPDATE하는 쿼리가 나갑니다.
@Entity
public class Member{
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
}
@Entity
public class Team{
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
}
- 일대다 단방향에서는 mappedBy 속성이 없어지며 @JoinColmn을 명시해 주었습니다.
- @JoinColumn을 사용하지 않으면, JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용하여 매핑하기 때문에, @JoinColumn을 붙여주어야 합니다.
일대다 양방향
- 일대다 양방향 매핑에서 @OneToMany는 데이터베이스의 특성상 연관관계의 주인이 될 수 없습니다.
- 왜냐하면 일대다 관계에서는 항상 다 쪽에 외래키가 존재하기 때문입니다.
- 이런 이유로, @ManyToOne에는 mappedBy 속성이 없습니다.
- 그러나 정말 일대다 양방향 매핑을 사용하고 싶다면, 일대다 단방향 매핑 반대편에, 같은 외래키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 하나 추가해주면 됩니다.
@Entity
public class Member{
@Id @GenratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) // 읽기 전용
private Team team;
}
@Entity
public class Team{
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
}
- 위처럼 각 엔티티에 @JoinColumn을 명시하여 두 엔티티가 같은 외래키를 관리하도록 만들어 준 후, 다(N) 쪽 엔티티를 읽기 전용으로 하나 만들어주면 됩니다.
- 하지만, 이 방법 역시, 단방향 매핑의 단점을 그대로 가지므로 지양합니다.
일대일(1:1) : @OneToOne
- 일대일 관계는 양쪽이 서로 하나의 관계만을 가집니다.
- 일대일 관계의 반대도 일대일 관계이며, 일대일 관계는 두 테이블 중 어느곳에서든 외래키를 가질 수 있습니다.
주 테이블에 외래키
- 주 객체가 대상 객체를 참조하는 것처럼, 주 테이블에 외래 키를 두고 대상 테이블을 참조하는 방법입니다.
- 해당 방법을 사용하기 위해서는 외래키에 데이터베이스 유니크 제약조건을 추가해주어야 합니다.
@Entity
public class Member{
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
@Entity
public class Locker{
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member member;
}
대상 테이블에 외래키
- 데이터베이스 개발자들이 선호하는 방식입니다.
- 테이블 관계를 일대일에서 일대다 관계로 변경할 때, 테이블의 구조를 그대로 유지할 수 있다는 장점이 있습니다.
- (일대다에서는 다 쪽이 항상 외래키를 가지므로, 대상 테이블에 외래키가 있다면, 테이블의 구조가 유지됩니다.)
@Entity
public class Locker{
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
}
@Entity
public class Member{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne(mappedBy = "member")
private Locker locker;
}
일대일 관계시 주의사항
- 프록시를 사용할 때, 외래키를 직접 관리하지 않는 일대일 관계는 지연 로딩을 설정하더라도 즉시 로딩됩니다.
- 그 이유는 JPA에서 1:1 관계를 설정할 때, '소유측(owning side)'과 '비소유측(non-owning side)'이라는 개념이 있습니다. 보통 소유측은 외래 키를 가지고 있는 쪽을 의미합니다.
- 비소유측에서 소유측을 참조할 때, JPA는 이 관계가 실제로 1:1인지 또는 1:N인지 확신할 수 없습니다.
- 왜냐하면 비소유측에서 외래 키가 실제로 어떤 엔티티를 가리키는지에 대한 정보가 없기 때문입니다.
- 더구나, 데이터베이스 제약 조건 없이 JPA만을 이용해 1:1 관계를 설정했다면, 실제로는 여러 엔티티가 같은 소유측 엔티티를 참조하게 될 수 있습니다.
- 이런 상황에서 JPA는 불필요한 데이터 불일치를 방지하기 위해 즉시 로딩을 사용합니다.
728x90
반응형
'개발 > Spring' 카테고리의 다른 글
JPA 엔티티 매핑 마스터하기: 기본부터 고급 전략까지 (0) | 2024.01.12 |
---|---|
영속성 컨텍스트의 이해: JPA의 핵심 기능 탐구 (0) | 2024.01.12 |
JPA 기초부터 실무 활용까지: Java ORM 표준의 이해 (0) | 2024.01.12 |
ORM의 이해: 데이터베이스와 객체 지향 프로그래밍의 효율적 연결 (0) | 2024.01.12 |
[JDBC, JPA] JDBC와 JPA (1) | 2023.01.04 |
댓글