JPA에 대해서 공부하면서 헷갈렸던 연관관계에 대해서 정리해보려고한다.

@ManyToOne을 통해 MEMBER테이블의 외래키(TEAM_ID)를 Team테이블의 기본키와 매핑한다. 이때 JPA는 내부적으로 @ManyToOne이 Team team필드 위에 달려있으니까 Team엔티티와 연관되어있음을 알게된다. 그리고 Team엔티티에서 @Id로 설정된 필드를 확인하고, 해당 필드가 Team엔티티의 기본키임을 알게된다.
@ManyToOne을 통해 MEMBER테이블의 외래키와 Team테이블의 기본키를 매핑해서 외래키-기본키 관계를 설정한다. (@ManyToOne은 객체간의 관계(다대일관계)와 테이블간의 관계(외래키-기본키관계)를 매핑한다.)
그리고 @ManyToOne이나 @OneToMany는 연관관계를 정의하는데에도 사용된다. 즉, 어떤 연관관계(단방향 또는 양방향)로 설정할지를 지정한다.
- Member엔티티의 필드에는 Team team이 있지만 db에는 객체를 저장할 수 없으므로 Member엔티티를 em.persist()로 저장할땐, db에 team 객체가 아니라 @JoinColumn으로 명시했던 외래키인 TEAM_ID 컬럼에 Team엔티티의 기본키가 저장된다. 이때 Team엔티티에서 @Id로 설정된 필드의 값이 저장된다.
참고로 @JoinColumn, @ManyToOne 같은 애노테이션과 상관없이, JPA에서 객체를 저장할 때는 해당 객체의 @Id로 설정된 필드의 값이 저장되는 것이 원칙이다.- @ManyToOne을 통해 MEMBER 테이블의 외래키(TEAM_ID)와 TEAM 테이블의 기본키를 매핑해줘야
member.getTeam()을 호출하면, JPA는 내부적으로 MEMBER 테이블에서 현재 member의 JoinColumn으로 명시했던 TEAM_ID값(=외래키값)을 가져와서 TEAM테이블의 기본키를 참조하여 해당 Team 데이터를 조회하고, Team 객체를 생성해서 반환한다.
- member.getTeam()이 호출되면 내부적으로 MEMBER테이블에서 현재 member의 JoinColumn으로 명시했던 TEAM_ID값(=외래키값)을 가져온다.
- Team테이블의 기본키가 이 외래키와 일치하는 Team 데이터를 찾는 SQL쿼리를 실행한다.
- 조회된 데이터로 Team객체를 생성해서 반환한다.
- 연관관계 : 두 객체(또는 테이블)간의 관계
- 연관관계 정의 : 객체(또는 테이블)간의 연관관계를 정의.
- 연관관계 매핑: 객체간의 관계(참조)와 테이블간의 관계(외래키-기본키관계)를 매핑하는것. 객체간의 관계에서는 Member객체는 team필드를 통해 Team객체를 참조한다.
테이블간의 관계에서는 MEMBER테이블의 외래키(TEAM_ID)를 통해 TEAM테이블을 참조한다. 이렇게 객체와 테이블은 구조적으로 다르기 떄문에 객체간의 관계와 테이블간의 관계를 동일하게 해주기 위해 연관관계 매핑이 필요하다. team필드를 통해 Team객체를 참조하던것을 외래키(TEAM_ID)를 통해 기본키(id)를 참조하도록 변경한것이라고 생각하면된다.- 연관관계 저장 : 객체간의 관계를 저장
- 연관관계 수정 : 객체간의 관계를 수정
- 연관관계 설정 : member.setTeam()이나 team.getMembers().add(member)와 같은 메서드를 통해 값을 설정해주는것(=참조를 저장하는것)
단방향연관관계와 양방향연관관계의 차이점은,
단방향연관관계는 예를들어 A만 B를 참조할수있고 A에 @ManyToOne과 @JoinColumn이 있지만, B에서는 A를 참조할수없고 B에는 @OneToMany나 @JoinColumn이 없다.
참고로 단방향연관관계에서 양방향연관관계로 변경하더라도 테이블구조는 동일하다.
※※ 단방향이든 양방향이든 위 모든 내용들(JoinColum~ManyToOne)은 동일하게 동작한다. ※※
💡참고로 테이블은 연관관계가 1개이다.
- 테이블간의 연관관계는 외래키와 기본키의 조인으로 이루어진다.(외래키-기본키관계)
- MEMBER테이블의 외래키와 TEAM테이블의 기본키를 조인해서 데이터를 찾으면된다.
- 테이블 연관관계는 방향의 개념이 사실 없다.
👇 List를 넣어줄떈 new ArrayList<>();로 초기화 해주는것이 관례이다.

양방향 연관관계 뿐만 아니라 , 단방향 연관관계에서도 연관관계의 주인이 있다.
mappedBy = "team" :
양방향 연관관계에서 사용된다. 연관관계의 주인이 아닌 쪽에서 사용한다.
연관된 엔티티의 team필드가 연관관계의 주인임을 나타낸다.
외래키를 누가 관리할지 적어준다. 연관관계의 주인이 외래키를 관리한다. 양방향 연관관계에서는 mappedBy로 외래키를 관리할 곳(연관관계의 주인)을 지정해줘야한다.
Member엔티티에 있는 team필드나 Team엔티티에 있는 members필드 중 하나로 외래키를 관리해야한다.
mappedBy를 선언한 쪽은 연관관계의 주인이 아닌 필드이며, 단순히 주인이 관리하는 외래키를 참조한다. 그래서 Team에서 members필드를 사용할때, Member테이블의 외래키(TEAM_ID)를 참조한다.
members필드를 사용할때 Member테이블의 외래키(TEAM_ID)를 참조하기때문에 team.getMember()를 호출하면, JPA는 내부적으로 TEAM 테이블에서 현재 team의 기본키값을 가져와서 MEMBER테이블의 외래키를 참조하여 해당 Member 데이터를 조회하고, Member객체를 생성해서 기존 비어있는 리스트에 추가한다.
1. team.getMember()가 호출되면 내부적으로 TEAM테이블에서 현재 team의 기본키값을 가져온다.
2. Member테이블의 외래키인 TEAM_ID가 이 기본키와 일치하는 Member 데이터를 찾는 SQL쿼리를 실행한다.
ex) SELECT * FROM MEMBER WHERE TEAM_ID = 1;
3. 조회된 데이터로 Member객체를 생성해서 기존 비어있는 리스트에 추가한다.
💡참고로, 일반적으로 다대일관계에서 외래키가 다(多)쪽에 있기때문에 다(多)쪽이 무조건 연관관계의 주인이 된다.
Team team = new Team();
em.persist(team);
Member member1 = new Member();
Member member2 = new Member();
Member member3 = new Member();
member1.setTeam(team); // 이렇게 해야함
member2.setTeam(team); // 이렇게 해야함
em.persist(member1);
em.persist(member2);
em.persist(member3);
team.getMembers().add(member3);
// 이렇게만 하면, member3의 외래키(TEAM_ID)가 설정되지않음.
// 즉 member3의 team필드는 여전히 null이다.
List<Member> newMembers = new ArrayList<>();
Member member4 = new Member();
em.persist(member4);
newMembers.add(member4);
team.setMembers(newMembers);
// 이렇게만 하면, member4의 외래키(TEAM_ID)가 설정되지않음.
// 즉 member4의 team필드는 여전히 null이다
tx.commit();
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);
//em.flush(); // 영속성 컨텍스트에 있는 쿼리를 db에 전송한다.
//em.clear(); // 영속성 컨텍스트를 초기화
// 이렇게 하면, 위 em.persist()를 통해 영속성컨텍스트에 있는 Member를 가져오는게 아니라,
// em.find()를 할때 db에서 select를 해서 가져온다.
Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers(); // 이 부분
for (Member m : members) {
System.out.println("m = " + m.getName());
}
👆이 코드에서,
1차캐시에서 조회한 Team에 대해서 team.getMembers()를 호출하면 값이 없다.
db에서 조회한 Team에 대해서 team.getMembers()를 호출하면 값이 있다.
1차캐시 기준으로 member.team은 member.setTeam(team) 으로 인해 member의 team필드에 값이 들어갔으나, team.members는 어떤 작업도 해주지 않았기 때문에 값이 추가되어있지않은 비어있는 리스트이다.
이 상태이기때문에 em.find()를 통해 1차 캐시에서 team을 find()하면 team.members가 비어있는것으로 나오는것이다.
하지만 플러시 및 영속성컨텍스트를 초기화 해준 후에
Team findTeam = em.find(Team.class, team.getId());를 통해 db에서 team을 find()하면, TEAM 테이블에서 Team데이터를 조회하는 SELECT쿼리를 실행하고 조회된 데이터로 Team객체를 생성해서 리턴하고, team.getMembers()를 호출하면 추가로 SELECT쿼리를 실행하고 조회된 데이터로 Member객체를 생성해서 기존 비어있는 리스트에 추가해준다.
그래서 이때는 team.members가 비어있지 않은것이다.
참고로, em.flush()와 em.clear()를 호출하면 영속성 컨텍스트가 초기화되고 1차캐시가 비워진다.
그리고나서 em.find()를 호출하면 1차캐시에 데이터가 없기 때문에 DB에서 데이터를 조회한다.
1차 캐시에 데이터가 있으면 쿼리 실행 없이 1차캐시에서 조회한다.
1차캐시에 데이터가 없으면 쿼리 실행 해서 db에서 조회한다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
... getter and setter
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<>();
public void addMember(Member member) {
member.setTeam(this); // 필드값 변경
members.add(member); // List에 값 추가
// this는 team.addMember(member);을 할때의 team 객체를 말한다.
// 즉, team.addMember(member);을 호출한 Team 객체 자신을 의미한다.
}
... getter and setter
}
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
em.persist(member);
team.addMember(member);
em.flush(); // 영속성 컨텍스트에 있는 쿼리를 db에 전송한다.
em.clear(); // 영속성 컨텍스트를 초기화
// 이렇게 하면, 위 em.persist()를 통해 영속성컨텍스트에 있는 Member를 가져오는게 아니라,
// em.find()를 할때 db에서 select를 해서 가져온다.
//team.addMember(member);
Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();
for (Member m : members) {
System.out.println("m = " + m.getName());
}
👆이 코드에서,
💡em.flush() 이전에 내용을 수정
- em.persist() 이후에 내용을 수정했지만 em.flush() 이전에 수정된 내용은 수정된 내용까지 INSERT 쿼리에 반영된다.
- 하지만 엔티티의 필드값이 변경되면 UPDATE쿼리가 추가적으로 생성된다.
- 컬렉션(List)에 값이 추가된것은 UPDATE쿼리가 생성되지않고 INSERT 쿼리에 반영된다.
- 이렇게 하면, SQL저장소에는 INSERT 쿼리2개와 UPDATE쿼리가 있다.
💡em.flush() 이후에 내용을 수정
- em.flush() 이후에 내용을 수정하면, 수정된 내용에 대해서 INSERT쿼리 및 UPDATE쿼리가 생성되지않는다.
- 영속성 컨텍스트에서 영속상태의 엔티티에 대해 dirty checking(변경감지)가 발생하면 UPDATE쿼리가 생성되는데, 이 시점에는 em.clear()로 인해 team과 member는 영속 상태가 아니기 때문이다. 그래서 수정된 내용이 반영되지않는다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public void setTeam(Team team) {
this.team = team;
}
... getter and setter
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<>();
public void addMember(Member member) {
member.setTeam(this); // 필드값 변경
members.add(member); // List에 값 추가
// this는 team.addMember(member);을 할때의 team 객체를 말한다.
// 즉, team.addMember(member);을 호출한 Team 객체 자신을 의미한다.
}
... getter and setter
}
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
em.persist(member);
team.addMember(member);
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public void setTeam(Team team) {
this.team = team;
}
... getter and setter
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
... getter and setter
}
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);
- 양방향연관관계로하면 편의메서드인 addMember()을 만들어서 양쪽에 값을 넣어주면되고, 단방향연관관계로하면 Member에만 setTeam()으로 값을 넣어주면된다.
💡단방향 연관관계에서 두 엔티티중 어느쪽에 @ManyToOne, @JoinColum, private Team team, getTeam(), setTeam()을 지정할까?
✅ DB설계 관점에서, 데이터베이스에서 다대일 관계를 만들려면 외래키가 있는 테이블을 정해야한다. 보통 다대일 관계에서 외래키는 다(N)쪽 테이블에 있다.
그래서 단방향 연관관계에서 두 엔티티중 어느쪽에서 @ManyToOne, @JoinColum, private Team team, getTeam(), setTeam()을 지정할지는,
다대일관계에서는 외래키가 있는 테이블과 매핑되는 엔티티에 지정해준다.
💡@ManyToOne / @OneToMany
✅ 다쪽에 ManyToOne을, 일쪽에 OneToMany를한다.
다대일 양방향 연관관계로 보면,
다대일 관계는 다쪽에서 일쪽 방향이므로 Many(다)To(대)One(일)이고,
일대다 관계는 일쪽에서 다쪽 방향이므로 One(일)To(대)Many(다)이다.
💡JPA가 다대일 관계에서, 외래키 컬럼이 어느 테이블에 생성될지 결정하는방법
1. 기본원칙 : 외래키는 항상 다쪽 테이블에 생성된다.
2. 테이블결정 : @ManyToOne 애노테이션이 있는 엔티티의 테이블에 생성된다. 여기서는 Member엔티티에 @ManyToOne이 있으므로 Member엔티티와 매핑된 테이블인 MEMBER에 외래키가 생성된다.
3. @JoinColumn으로 외래키 컬럼의 이름을 지정한다.
💡addMember()
- 연관관계의 주인인 Member엔티티의 team필드에 값을 설정해줘야, member.getTeam()도 할수 있고, team.getMembers()도 할수있다.
- 이렇게 Member엔티티의 team필드에 값이 설정되어야 Member엔티티를 db에 저장할때 MEMBER테이블의 TEAM_ID 컬럼에 Team엔티티의 기본키가 들어가게된다.
💡외래키 컬럼의 값은 어떤 방법으로 값이 할당되고, @Joincolumn에 name속성으로 지정해주는 이유
- Member엔티티의 team필드에 값이 설정되면(=setTeam(team)), MEMBER테이블의 TEAM_ID 컬럼에 Team엔티티의 기본키가 들어가는 이유는 team 객체가 em.persist(team) 호출 시, Team 엔티티의 기본 키가 생성되고 Team 엔티티의 @Id로 설정된 필드의 값이 초기화된다.
이후에 em.persist(member) 호출 시, db에는 객체를 저장할 수 없으므로 db에 저장할때, db에 team 객체가 아니라 @JoinColumn으로 명시했던 외래키인 TEAM_ID 컬럼에 Team엔티티의 기본키가 저장된다. 이때 Team엔티티에서 @Id로 설정된 필드의 값이 저장된다.
em.persist(member)를 하고 insert쿼리가 발생 시 이때 같이 외래키 TEAM_ID도 함께 insert쿼리에 포함되기 때문에 추가적인 UPDATE쿼리가 필요없다.
참고로 @JoinColumn, @ManyToOne 같은 애노테이션과 상관없이, JPA에서 객체를 저장할 때는 해당 객체의 @Id로 설정된 필드의 값이 저장되는 것이 원칙이다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
/* @ManyToOne
@JoinColumn(insertable = false, updatable = false)
private Team team; */ 이렇게 하면 일대다 양방향
... getter and setter
}
@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<>();
... getter and setter
}
Member member = new Member();
member.setName("member1");
em.persist(member);
Team team = new Team();
team.setName("teamA");
team.getMembers().add(member);
em.persist(team);
일대다 단방향
- members에 @JoinColumn으로 설정되어있기 때문에, 외래키를 관리하는 연관관계의 주인이다.
- 외래키는 항상 다쪽 테이블에 있고, 그래서 다쪽이 연관관계의 주인되어야하지만, 일대다 연관관계에서는 '일'쪽이 연관관계의 주인이다. => 복잡하므로 다대일 양방향 연관관계를 하는게 더 낫다.
- Team엔티티의 members필드인 리스트에 값을 추가 및 변경(team.getMembers.add(member), team.setMembers())하고 Team엔티티를 저장하면, MEMBER 테이블에 대한 UPDATE쿼리가 추가적으로 생성되고, MEMBER 테이블의 외래키(TEAM_ID)를 추가 및 변경할 수 있다.
=> 연관관계의 주인인 members를 통해 외래키를 관리한다.
일대다 양방향
- members에 @JoinColumn으로 설정되어있기 때문에, 외래키를 관리하는 연관관계의 주인이다.
- 외래키는 항상 다쪽 테이블에 있고, 그래서 다쪽이 연관관계의 주인되어야하지만, 일대다 연관관계에서는 '일'쪽이 연관관계의 주인이다. => 복잡하므로 다대일 양방향 연관관계를 하는게 더 낫다.
- 연관관계의 주인이 members이므로 member.team쪽에 mappedBy를 해줘야할것같지만, 일반적으로는 ManyToOne쪽이 연관관계의 주인이기 때문에 ManyToOne에는 mappedBy속성이 없다. 그래서 member.team에는 @JoinColumn(insertable=false, updatable=false)를 해준다. 그럼 외래키를 관리하지않고 읽기전용 필드로 사용되므로 연관관계의 주인이 아니다.
- @JoinColumn(insertable=false, updatable=false)랑 mappedBy는 외래키를 관리하는 연관관계의 주인이 아니고 읽기전용필드임을 의미한다는거에서 동일하다.
- 일대다 단뱡향관계에서도 members가 연관관계의 주인이고
일대다 양방향관계에서도 members가 연관관계의 주인이다.- 참고로 일대다 연관관계 일때도 member와 team을 db에 저장하고나서, member.getTeam()이나 team.getMembers()의 동작 방식은 다대일 연관관계 일때와 동일하다.
💡참고로, db에 위와같이 Member와 Team을 저장한다음에, team.getMembers()을 해서 Member를 구한 다음에, Member를 통해 MEMBER테이블에 있는 Member의 TEAM_ID값을 알고싶으면 어떻게해야하나요?
=> team.getMembers()로 가져온 Member 객체에는 TEAM_ID를 알 수 있는 필드가 없어서 Member의 TEAM_ID값을 알수없다.
따라서, Member의 TEAM_ID값을 알고싶으면 양방향 연관관계로 만들어서 Member 엔티티에 @ManyToOne을 추가하고 Member 객체에서 member.getTeam().getId(); 를 통해 TEAM_ID를 알수있다.
💡JPA가 일대다 관계에서, 외래키 컬럼이 어느 테이블에 생성될지 결정하는방법
1. 기본원칙 : 외래키는 항상 다쪽 테이블에 생성된다.
2. 테이블결정 : @OneToMany 애노테이션이 붙은 컬렉션 필드의 제네릭 타입을 확인한다. 여기서는List<Member>이므로 Member엔티티와 매핑된 테이블인 MEMBER에 외래키가 생성된다.
3. @JoinColumn으로 외래키 컬럼의 이름을 지정한다.
=> 일반적으로 외래키는 항상 다쪽 테이블에 생성된다. 그래서 @ManyToOne 애노테이션이 있는 엔티티의 테이블에 생성된다.
하지만 일대다 단방향 관계에서는 @OneToMany 애노테이션이 붙은 컬렉션 필드의 제네릭 타입을 확인하고, 해당 제네릭 타입의 엔티티와 매핑된 테이블에 @JoinColumn으로 지정한 외래키 컬럼이 생성된다.
💡외래키 컬럼의 값은 어떤 방법으로 값이 할당되고, @Joincolumn에 name속성으로 지정해주는 이유
Team엔티티의 members필드에 값이 설정되면(=team.getMembers().add(member)), MEMBER테이블의 TEAM_ID 컬럼에 Team엔티티의 기본키가 들어가는 이유는
team 객체가 em.persist(team) 호출 시, Team 엔티티의 기본 키가 생성되고 Team 엔티티의 @Id로 설정된 필드의 값이 초기화된다.
이후에 MEMBER 테이블에 대한 UPDATE쿼리가 추가적으로 생성되고, MEMBER 테이블의 외래키(TEAM_ID)가 추가 및 변경된다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
... getter and setter
}
@Entity
public class Locker {
@Id @GeneratedValue
private Long id;
private String name;
/* @OneToOne(mappedBy = "locker")
private Member member; */ 이렇게 하면 일대일 양방향
... getter and setter
}
일대일 연관관계
- 다대일 연관관계와 유사하다.
- 주 테이블이나 대상 테이블 중에 아무곳이나 외래키를 넣어도됨.
다대다 연관관계
- 다대다 연관관계일때는 중간에 엔티티를 두어, 중간 엔티티를 기준으로 다대일 단방향 연관관계나, 다대일 양방향 연관관계로 해야한다.
- 다대다 연관관계일때는 중간에 엔티티를 두어 일대다, 다대일 관계로 풀어내야한다.
==> 이때 말하는 일대다와 다대일은 일대다 연관관계나 다대일 연관관계에서의 "일대다", "다대일" 의 개념이 아니라, 다대일 양방향 연관관계에서, 한쪽에서는 다대일 관계이고 반대쪽에서는 일대다 관계인데, 이때 말하는 "일대다", "다대일"의 개념을 말하는것이다.
즉, 다대일 양방향 연관관계에서의 "다대일"쪽의 반대편을 의미하는 "일대다"를 말하는것이다. 다대일 양방향 연관관계는 한쪽에서는 "다대일" 관계이고, 반대쪽에서는 "일대다" 관계이다.
서로 다른 두개의 연관관계가 아닌, 하나의 연관관계를 양쪽에서 각각 어떤 관계로 보는지를 나타내는것이다.