엔티티들은 대부분 다른 엔티티와 연간관계가 있다. 객체는 참조를 사용해서 관계를 맺고 테이블을 외래 키를 사용해서 관계를 맺는다. 이번 포스팅에선 객체의 참조와 테이블의 외래 키를 매핑하는 것을 알아보자.
연관관계 중에서는 다대일(N:1) 단방향 관계를 먼저 이해해야 한다. 회원과 팀의 관계를 통해 다대일 단방향 관계를 알아보자.

회원 객체는 Member.team 필드로 팀 객체와 연관관계를 맺는다.
회원 객체와 팀 객체는 단방향 관계이다. 회원은 Member.team 필드를 통해서 팀을 알 수 있지만 반대로 팀은 회원을 알 수 없다.
회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.
회원 테이블과 팀 테이블은 양방향 관계이다. 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고 반대로 팀과 회원도 조인할 수 있다.
참조를 통한 연관관계는 언제나 단방향이다. 객체 간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다. 이렇게 양쪽에서 참조하는 것을 양방향 연관관계라고 한다. 정확히는 단방향 연관관계 2개이지만 논리적으로 양방향 연관관계로 생각할 수 있다.
순수하게 객체만 사용한 연관관계를 살펴보자. 다음 코드는 JPA를 사용하지 않은 순수한 회원과 팀 클래스의 코드이다.
public class Member {
private String id;
private String username;
private Team team;
public void setTeam(Team team){
this.team = team;
}
}
public class Team {
private String id;
private String name;
}
다음은 회원1과 회원2를 팀1에 소속시키는 예제이다.
public static void main(String[] args){
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
Team team = new Team("team1", "팀1");
member1.setTeam(team);
member2.setTeam(team);
}
다음 그림은 클래스와 인스턴스 관계를 나타낸다.

다음 그림은 순수한 객체에서 다대일 클래스의 그림이다.

다음 그림은 순수한 객체에서 다대일 인스턴스의 그림이다.
다음은 회원1이 속한 팀1을 조회하는 코드이다.
Team findTeam = member1.getTeam();
이처럼 객체는 참조를 통해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라고 한다.
데이터베이스 테이블의 회원과 팀의 관계를 살펴보자. 다음은 회원 테이블과 팀 테이블의 DDL이다.
CREATE TABLE MEMBER (
MEMBER_ID VARCHAR(255) NOT NULL,
TEAM_ID VARCHAR(255),
USERNAME VARCHAR(255),
PRIMARY KEY (MEMBER_ID)
)
CREATE TABLE TEAM (
TEAM_ID VARCHAR(255) NOT NULL,
NAME VARCHAR(255),
PRIMARY KEY (TEAM_ID)
)
ALTER TABLE MEMBER ADD CONSTRAINT FK_MEMBER_TEAM
FOREIGN (TEAM_ID)
REFERENCES TEAM
다음은 회원1과 회원2를 팀1에 소속시키는 SQL이다.
INSERT INTO TEAM(TEAM_ID, NAME) VALUES ('team1', '팀1');
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME)
VALUES('member1', 'team1', '회원1');
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME)
VALUES('member2', 'team1', '회원2');
다음은 회원1이 소속된 팀을 조사하는 SQL이다.
SELECT T.*
FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.ID
WHERE M.MEMBER_ID = 'member1';
이처럼 데이터베이스는 외래 키를 사용해서 연관관계를 탐색할 수 있는데 이것을 조인이라고 한다.
지금까지 객체만 사용한 연관관계와 테이블만 사용한 연관관계를 각각 알아보았는데 이제 JPA를 사용해서 둘을 매핑하는 방법을 알아보자.

다음은 회원 엔티티를 매핑한 코드이다.
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
@ManyToOne // 다대일 연관관계 매핑
@JoinColumn(name = "TEMA_ID")
private Team team;
public void setTeam(Team team){
this.team = team;
}
}
다음은 팀 엔티티를 매핑한 코드이다.
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
}
@ManyToOne과 @JoinColumn 어노테이션을 사용해 연관관계를 매핑하였다.
@JoinColumn은 외래 키를 매핑할 때 사용한다.
다음 표는 @JoinColumn의 주요 속성들이다.
| 속성 | 기능 | 기본값 |
|---|---|---|
| name | 매핑 할 외래 키 이름 | 필드명+_+참조하는 테이블의 기본 키 칼럼명 |
| referencedColumnName | 외래 키가 참조하는 대상 테이블의 칼럼명 | 참조하는 테이블의 기본키 칼럼명 |
| foreignKey(DDL) | 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다. | |
| unique | @Column의 속성과 같다. | |
| nullable | @Column의 속성과 같다. | |
| insertable | @Column의 속성과 같다. | |
| updatable | @Column의 속성과 같다. | |
| columnDefinitiontable | @Column의 속성과 같다. |
@ManyToOne 어노테이션은 다대일 관계에서 사용한다.
다음 표는 @ManyToOne의 주요 속성들이다.
| 속성 | 기능 | 기본값 |
|---|---|---|
| optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. | true |
| fetch | 글로벌 패치 전략을 설정한다. | @ManyToOne=FetchType.EAGER |
| @OneToMany=FetchType.LAZY | ||
| cascade | 영속성 전이 기능을 사용한다. |
연관관계를 등록, 수정, 삭제, 조회하는 예제를 통해 어떻게 사용하는지 알아보자.
다음은 연관관계를 매핑한 엔티티를 저장하는 예제이다.
public void testSave() {
// 팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
// 회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // 연관관계 설정 member1->team1
em.persist(member1);
// 회원2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // 연관관계 설정 member2->team1
em.persist(member2);
}
회원 엔티티는 팀 엔티티를 참조하고 저장했다. JPA는 참조한 팀의 식별자를 외래 키로 사용해서 적절한 등록 쿼리를 생성한다. 이때 실행된 SQL은 다음과 같다.
INSERT INTO TEAM(TEAM_ID, NAME) VALUES ('team1', '팀1');
INSERT INTO MEMBER(MEMBER_ID, USERNAME, TEAM_ID)
VALUES('member1', '회원1', 'team1');
INSERT INTO MEMBER(MEMBER_ID, USERNAME, TEAM_ID)
VALUES('member2', '회원2', 'team1');
연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지이다.
다음은 member와 연관된 team을 조회하는 예제이다.
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
이처럼 객체를 통해 연관된 엔티티를 조회하는 것을 객체 그래프 탐색이라고 한다.
다음은 팀1 소속이던 회원을 새로운 팀2에 소속하도록 수정한 예제이다.
private static void updateRelation(EntityManager em) {
Team team2 = new Team("team2", "팀2");
em.persist(team2);
Member member = em.find(Member.class, "member1");
member.setTeam(team2);
}
앞에서 설명했듯이 수정은 em.update() 같은 메소드가 없다. 단순히 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다. 그리고 변경사항을 데이터베이스에 자동으로 반영한다.
다음은 회원1을 팀에 소속되지 않도록 변경하는 예제이다.
Member member1 = em.find(Member.class, "member1");
member1.setTeam(null) // 연관관계 제거
member1의 team 참조를 null로 설정하여 연관관계를 제거하였다.
연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다. 그렇지 않으면 외래 키 제약조건으로 인해, 데이터베이스에서 오류가 발생한다. 팀1에는 회원1과 회원2가 소속되어 있다. 이때 팀1을 삭제하려면 연관관계를 먼저 끊어야 한다.
member1.setTeam(null); // 회원1 연관관계 제거
member2.setTeam(null); // 회원2 연관관계 제거
em.remove(team); // 팀 삭제
이번 글에서는 객체와 테이블의 연관관계를 이해하고, JPA를 사용해 이를 매핑하는 방법을 알아보았다. 이를 통해 객체 지향 설계와 데이터베이스 설계 간의 간극을 줄이고, 더 나은 데이터 관리와 탐색을 가능하게 한다.