연관 관계를 매핑할 때, 생각해야 할 것은 방향, 연관관계 주인, 다중성 있습니다.
테이블은 조인을 할때 외래키 하나로 모든 테이블을 양쪽으로 조인 할 수 있는반면에,
객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능합니다.
그렇기 때문에 두 객체 사이에 하나의 객체만 참조용 필드를 갖고 참조하면 단방향 관계, 두 객체 모두가 각각 참조용 필드를 갖고 참조하면 양방향 관계라고 합니다.
여기서 객체의 양방향 관계라는 말은 사실 두 객체가 각각 단방향을 가지고 있어서 양방향이라고 하는거지 사실은 서로 단방향 관계이다.
JPA를 사용하여 데이터베이스와 패러다임을 맞추기 위해서 객체는 단방향 연관 관계를 가질지, 양방향 연관 관계를 가질지 선택해야합니다.
선택은 비즈니스 로직에서 두 객체가 참조가 필요한지 여부를 고민해보면 됩니다.
비즈니스 로직에 맞게 선택했는데 두 객체가 서로 단방향 참조를 했다면 양방향 연관 관계가 되는 것입니다.
비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안됨
연관관계의 주인은 외래 키의 위치를 기준으로 정해야함.
다대일: @ManyToOne // 제일 많이 쓰임
일대다: @OneToMany
일대일: @OneToOne
다대다: @ManyToMany
데이터베이스를 기준으로 다중성을 결정합니다.
연관 관계는 대칭성을 갖습니다.
일대다 ↔ 다대일
일대일 ↔ 일대일
다대다 ↔ 다대다
Member와 Team의 관계로 예를 들겠습니다.(멤버는 하나의 팀만 가집니다.)
하나의 팀에 여러멤버가 소속될수있고, 많은팀에는 하나의 멤버가 소속되지못함
여러멤버는 하나의 팀을 가질수있고, 하나의 멤버는 여러팀에 소속되지못함.
데이터베이스를 기준으로 다중성(Member 'N' : Team '1')을 결정했습니다.
즉, 외래 키를 Member 'N'이 관리하는 일반적인 형태입니다. (참고로 데이터베이스는 무조건 다'N'쪽이 외래 키를 갖습니다.)
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
}
다대일 단방향에서는 다 쪽인 Member에서 @ManyToOne 만 추가해준 것을 확인할 수 있습니다.
반대로 Team에서는 참조하지 않습니다. (단방향이기 때문)
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
다대일 양방향으로 만드려면 일(1) 쪽에 @OneToMany 를 추가하고 양방향 매핑을 사용했으니 연관 관계의 주인을 mappedBy 로 지정해줍니다.
mappedBy로 지정할 때 값은 대상이 되는 변수명을 따라 지정하면 됩니다. 여기서는 Member 객체의 team라는 이름의 변수이기 때문에 team로 지정했습니다.
앞서 다대일의 기준은 연관관계의 주인 N 쪽에 둔 것이고 이번에 언급할 일대다의 기준은 연관관계의 주인을 1 쪽에 둔 것입니다.
참고로 실무에서는 일대다(1:N) 단방향은 잘 쓰지않는다.
→ 일대다(1:N) 단방향
데이터베이스 입장에서는 무조건 다(N)쪽에서 외래키를 관리합니다.
근데 일(1)쪽 객체에서 다(N) 쪽 객체를 조작(생성,수정,삭제)하는 방법입니다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "team_id")
private List<Member> members = new ArrayList<>();
}
@OneToMany에 mappedBy가 없어집니다. 양방향이 아니기 때문입니다.
여기선 좋지않은 단점들이 나옵니다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setName("DragonTiger");
em.persist(member);
Team team = new Team();
team.setName("DragonTigerTeam");
team.getMembers().add(member);
em.persist(team);
tx.commit();
}catch (Exception e){
tx.rollback();
}finally {
em.close();
}
emf.close();
Team 클래스가 연관관계의 주인인데 Member 테이블에 FK가 있기때문이다.
즉, team.getMembers().add(member); 이 코드는 자바세상에서만 적용가능하지 데이터베이스의 FK는 Member 테이블에 있기때문에 아래에 보면 팀테이블이 인서트 되고 업데이트 쿼리를 Member 테이블에 보낸다.
일만 수정한 것 같은데 다른 수정이 생겨 쿼리가 발생하는 것.
Team를 저장했는데 왜 Member가 수정이 되지? 이런 생각을 하게 만듦.
(위 문제는 규모가 작을땐 상관없는데 큰규모로 가게되면 매우 큰 단점이된다 직관적으로 파악하기 쉽지 않다. 이 문제가 가장 치명적이다.)
업데이트 쿼리 때문에 성능상 이슈는 그렇게 크지는 않음.
그렇기 때문에 TIP으로 일대다(1:N) 단방향 연관 관계 매핑이 필요한 경우는 그냥 다대일(N:1) 양방향 연관 관계를 매핑해버리는게 추후에 유지보수에 훨씬 수월하기 때문에 이 방식을 채택하는 것을 추천한다고 한다.
그런데 실무에서 사용을 금지하지 않는 이유는 되도록 피하는 게 좋지만, JPA 값 타입을 사용하는 것을 대신하여 사용할 때는 또 유용하다고 한다.
일대다 양방향은 공식적으로 존재하는 건 아니라서 생략하겠습니다.
키워드는 @JoinColumn(updatable = false, insertable = false) 이지만, 일대다 양방향을 사용해야할 때는 다대일 양방향 사용하도록 하는게 더 좋습니다.
결과적으로 일대다(1:N) 단방향, 양방향은 쓰지 말고 차라리 다대일(N:1) 양방향으로 쓰는 것이 맞다라고 단순화하여 결론 내리면 될 것 같습니다.
JPA에서는 아예 지원을 하지 않습니다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "Locker_id")
private Locker locker;
}
@Entity
public class Locker {
@Id @GeneratedValue
private Long id;
private String name;
@OneToOne(mappedBy="locker")
private Member member;
}
여기선 외래 키를 Member에서 관리하는 게 좋을 것인지, Locker에서 관리하는 게 좋을 것인지 생각을 해봐야합니다. 즉 테이블에 어디에 둘 것 인지를 생각해야합니다.
테이블은 한 번 생성되면 보통 굳어집니다. 변경이 어렵다는 얘기입니다.
그러나 비즈니스는 언제든 바뀔 수 있습니다.
하나의 멤버가 여러개의 Locker를 가진다고 변경이되면?
그러면 다(N)쪽인 Locker테이블에 외래 키가 있는 것이 변경에 유연합니다.
그러면 다(N)가 될 확률이 높은 테이블에 외래 키를 놓는게 무조건 좋을까? 그건 또 아니라고한다.
객체 입장에서 Member쪽(1)에서 외래 키를 갖게되면 Member를 조회할 때마다 이미 Locker의 참조를 갖고 있기 때문에 성능상 이득이 있습니다.
주 테이블에 외래 키
대상 테이블에 외래 키
사용 금지!!
중간 테이블이 숨겨져 있기 때문에 자기도 모르는 복잡한 조인의 쿼리(Query)가 발생하는 경우가 생길 수 있기 때문입니다.
다대다로 자동생성된 중간테이블은 두 객체의 테이블의 외래 키만 저장되기 때문에 문제가 될 확률이 높습니다. JPA를 해보면 중간 테이블에 외래 키 외에 다른 정보가 들어가는 경우가 많기 때문에 다대다를 일대다, 다대일로 풀어서 만드는 것(중간 테이블을 Entity로 만드는 것)이 추후 변경에도 유연하게 대처할 수 있습니다.
위 그림처럼 엔티티를 하나 생성해서 서로 다대일을 걸어주면 된다.