연관관계는 엔티티(테이블) 간의 관계를 의미합니다.
데이터베이스에서 테이블 간의 관계를 설정하고 데이터의 무결성을 보장하는 가장 핵심적인 두 요소는 기본 키(Primary Key, PK)와 외래 키(Foreign Key, FK)입니다.
기본 키는 테이블에 있는 각 행(row)을 유일하게 식별할 수 있는 고유한 값입니다.
우리 모두가 고유한 주민등록번호를 갖고 있는 것과 같습니다.
| 특징 | 설명 | 예시 |
|---|---|---|
| 유일성 | 테이블 내 모든 행은 서로 다른 키 값을 가져야 한다. | 주민등록번호 |
| NOT NULL | 기본 키 값은 절대 비어있을 수 없다. | 주민등록번호가 없는 대한민국 사람은 없다. |
| 불변성 | 기본 키 값은 변경되면 안 된다. | 주민등록번호가 바뀌면 안 된다. |
외래 키는 한 테이블의 컬럼이 다른 테이블의 기본 키를 참조하는 것을 말합니다. 즉, 테이블 간의 관계를 맺어주는 연결 고리 역할을 합니다.
| 특징 | 설명 | 예시 |
|---|---|---|
| 관계 설정 | 두 테이블을 연결하여 의미 있는 데이터 관계를 만든다. | Team과 Member는 관계가 있다. |
| 중복 허용 | 외래 키 값은 기본 키와 달리 중복될 수 있다. | 하나의 Team은 여러 명의 Member를 가질 수 있다. |
| NULL 허용 | 외래 키 값은 비어 있을 수 있다. | Team이 없는 Member가 있을 수 있다. |
가장 먼저 이해해야 할 것은 관계의 '방향'과 '주인'입니다. 방향과 주인은 연관관계의 기본 규칙입니다.
한쪽 객체에서만 다른 쪽 객체를 참조할 수 있는 관계입니다.
💡 Member 자신이 속한 Team을 알지만, Team은 자신에게 속한 Member들을 모르는 관계입니다.
@Getter
@Entity
@Table(name = "members")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
// ...
}
@Getter
@Entity
@Table(name = "teams")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// ...
}
이 경우 Member 엔티티만 조회하면 team_id FK를 통해 Team을 알 수 있습니다.
이를 코드로 풀어보면 아래와 같습니다.
member.getTeam();
Member 테이블은 Team team을 필드로 가지고 있으므로 member.getTeam();이 가능합니다. 하지만 Team 테이블은 Member와 관련한 필드를 가지고 있지 않으므로, Team 엔티티 객체로 Member 조회는 불가능합니다.
양쪽 객체가 서로를 참조하는 관계입니다.
💡 Member는 Team을 알고, Team 역시 자신에게 속한 Member 목록을 아는 관계입니다.
@Getter
@Entity
@Table(name = "members")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
// ...
}
@Getter
@Entity
@Table(name = "teams")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
// ...
}
이 경우 Member 엔티티를 조회하면 team_id FK를 통해 Team을 알 수 있고, 또한 Team 엔티티를 조회하여 members 필드를 통해 어떤 Member들이 팀에 속해있는지 알 수 있습니다.
이를 코드로 풀어보면 아래와 같습니다.
// 이전과 동일
member.getTeam();
Member 테이블은 Team team을 필드로 가지고 있으므로 여전히 member.getTeam();이 가능합니다.
// Member 리스트를 조회할 수 있다.
team.getMembers();
Team 테이블은 Member와 관련한 필드로 private List<Member> members = new ArrayList<>(); 를 갖고 있으므로 Team 엔티티 객체로 Member 조회가 가능합니다.
단방향 관계에서의 연관관계의 주인은 경우의 수가 하나밖에 없습니다. 다른 쪽을 필드로 갖고 있는 엔티티 클래스가 하나 뿐이기 때문입니다. 위의 경우, Member 엔티티 클래스에
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
이렇게 Team 필드가 정의되어있기에 Member가 연관관계의 주인입니다.
양방향 관계에서는 두 객체(Member, Team)가 서로를 참조합니다. 이때 JPA는 "둘 중 누가 DB의 외래 키(FK)를 관리할지" 정해야 하는데, 이 관리 권한을 가진 쪽을 연관관계의 주인이라고 합니다.
mappedBy 속성을 사용해 "나는 주인이 아니며, 주인의 어떤 필드와 연결되어 있는지"를 명시합니다. 이쪽에서의 관계 설정은 DB에 반영되지 않고, 오직 조회만 가능합니다.쉽게 말하자면, @ManyToOne이 있는 쪽이 연관관계의 주인입니다.