벌써 새해도 20일 넘게 지났다. 이제 와서 지난 한 해를 반추해 보니 별일 없는 듯 무언가 많이 지나갔다. 가장 큰 건 아주 원하는 곳은 아니긴 했지만 취업을 한 것이니 요즘 같은 시기에 그저 다행일 따름이다.
JPA 관련해서 글 올리는 게 오늘로 5번째다, 분명 처음 시작할 때에는 '1주일에 1개는 올리겠다!'라는 생각으로 시작했는데 아직 이만치밖에 못한 것에 벌써 느슨해 진건 아닌지 스스로 반성을 하게 된다. 아무리 3교대 근무라지만 쉬는 날에 할 수도 있었는데 말이다(...).
각설하고 연관관계 매핑에 대해 알아보자.
위 포스트는 인프런 김영한님 강의(자바 ORM 표준 JPA 프로그래밍 - 기본편)의 내용을 포함하고 있습니다.
연관관계?
객체에서 왜 연관관계가 필요한 걸까? '어차피 DB 테이블에 테이블 연관관계가 있는데 그거를 그대로 가져다 쓰면 안 되나?' 할 수도 있다.
먼저 참조를 사용하는 대신 테이블 연관관계에 맞추어 객체를 모델링했을 때의 코드를 보자.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@Column(name = "TEAM_ID")
private Long teamId;
/* … */
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
/* … */
}
@Test
public void test() {
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId()); // 외래키 값을 직접 다룸
em.persist(member);
//조회
Member findMember = em.find(Member.class, member.getId());
// 팀 조회 --> 회원 객체와 연관관계가 없음
Team findTeam = em.find(Team.class, team.getId());
}
위 코드와 같이 테이블에 맞추어 모델링을 하게 되면 객체 간 협력관계를 만들 수 없어 연관관계가 애매해진다.
그냥 봐도 테스트 코드가 어딘가 객체지향적이지 않거니와 나이브해 보이지 않는가(...).
이는 테이블과 객체의 연관관계를 다루는 방식의 차이에서 기인한다.
테이블은 외래키로 Join 하여 연관 테이블을 찾아내는 반면 객체는 참조를 사용하여 연관된 객체를 찾아내기 때문이다.
그렇기 때문에 우리는 객체의 특성에 맞춘 연관관계를 사용해야 한다.
연관관계의 종류
단방향 연관관계
이 연관관계는 한 객체에서 다른 객체 방향의 한 방향으로 연관관계를 거는 것으로, 연관관계를 거는 객체에 연관관계가 걸리는 객체의 참조를 저장하는 방식이다. 어떻게 하는지는 아래 코드를 참고하면 된다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
/* 이전 방식
@Column(name = "TEAM_ID")
private Long teamId;
*/
// 객체지향 모델링 (객체 참조 - 테이블 외래키 매핑)
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
/* … */
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
/* … */
}
@Test
public void test() {
// 팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
// 회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); //단방향 연관관계 --> 참조 저장
em.persist(member);
// 회원 조회
Member findMember = em.find(Member.class, member.getId());
// 참조를 사용하여 연관관계 조회
Team findTeam = findMember.getTeam();
}
이렇게 연관관계를 설정하게 되면 Member 객체에서 Team 객체로는 1:0 또는 1:1 관계가 성립하며, DB 테이블로는 Team 테이블의 TEAM_ID 컬럼을 기본키로 하고 Member 테이블에 TEAM_ID를 외래키 삼아 Member 테이블과 Team 테이블이 N:1 연관관계를 가지게 된다.
연관관계를 수정할 때에는 새로 연관관계를 지정할 객체를 새로 저장하여 영속화 한 다음 기존에 지정한 객체를 대체하면 된다.
// 새로운 팀B
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);
// 회원 1에 새로운 팀 설정
member.setTeam(teamB);
양방향 연관관계
이 연관관계는 말 그대로 서로 다른 두 객체에 양방향으로 연관관계를 거는 것이다. 어떻게 하는지는 아래 코드를 참고하자.
@Entity
public class Member { // 단방향 연관관계와 동일
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
/* 이전 방식
@Column(name = "TEAM_ID")
private Long teamId;
*/
// 객체지향 모델링 (객체 참조 - 테이블 외래키 매핑)
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
/* … */
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
// 컬렉션 추가
@OneToMany(mappedBy = "team")
List <Member> members = new ArrayList<Member>();
/* … */
}
@Test
public void test() {
// 팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
// 회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); //단방향 연관관계 --> 참조 저장
em.persist(member);
// 회원 조회
Member findMember = em.find(Member.class, member.getId());
// 참조를 사용하여 연관관계 조회
Team findTeam = findMember.getTeam();
}
여기서 코드의 Team 객체를 보면 mappedBy라는 처음 보는 것이 등장한다. 이는 조금 뒤에 나올 연관관계의 주인과 관련이 있는 것으로 지금 당장은 양방향 연관관계 설정에 필요한 옵션인 것만 알아두자.
이렇게 양방향 연관관계를 설정하고 나면 드는 의문 하나가 '어디를 기준으로 연관관계를 관리해야 하는가?' 일 것이다. 단방향이야 설정한 쪽에서 관리하면 그만이지만 양방향은 두 군데 다 설정하다 보니 고민될 법도 하다.
이를 위해서는 양방향 관계의 구조를 이해해야 하는데, 사실 객체의 양방향 관계는 실제 양방향 관계가 아니라 서로 다른 단방향 관계로 이루어져 있고 DB에서는 외래키 하나로 두 테이블의 연관관계를 관리한다. 그렇기 때문에 둘 중 한 군데에서 외래키를 관리해야 한다.
하여 연관관계를 관리하는 쪽을 '연관관계의 주인'으로 하여 외래키를 관리해야 한다. 연관관계의 주인만 외래키를 등록, 수정할 수 있고 아닌 쪽은 읽기만 가능하다. 또 mappedBy 속성은 주인이 아닌 쪽만 설정(주인 지정)이 가능하다.
그렇다면 연관관계의 주인은 어느 쪽으로 정해야 할까? 그에 대한 답은 외래키 관리에 있다. 즉, 외래키가 있는 쪽을 주인으로 정해야 한다. DB 테이블 관점에서 보자면 N:1 관계에서 N 쪽의 테이블로 정하면 된다.
이때 많이들 하는 실수가 연관관계의 주인 객체에 값을 저장하지 않고 그 반대편에만 저장하는 것이다. 이렇게 되면 연관관계 주인 객체에서 상대편 객체의 외래키를 알 수가 없게 되어버린다. 그렇기 때문에 양방향 연관관계를 설정할 때에는 연관관계의 주인에 반드시 저장을 해주어야 한다.
정리
연관관계를 설정할 때에는 단방향 매핑만으로도 연관관계 매핑은 끝이다. 다만 반대 방향에서도 조회를 위해 양방향 매핑을 추가한 것이다.
그러므로 어지간해서는 단방향 매핑을 사용하고 양방향 매핑은 정 필요한 경우에만 추가를 해주면 된다. 이렇게 해도 테이블의 N:1 연관관계는 유지되어 테이블에 영향이 없다.
또 양방향 연관관계에서는 항상 양쪽에 값을 설정해 주는 게 좋고, 매핑할 때에는 무한 루프에 걸리는 상황에 유의해야 한다. (ex. toString())
'Development > Java(Spring, JPA, etc.)' 카테고리의 다른 글
| [JPA 시리즈] 2 - 엔티티 매핑 (2 - 컬럼 매핑과 기본 키 매핑) (0) | 2025.12.11 |
|---|---|
| [JPA 시리즈] 2 - 엔티티 매핑 (1 - 엔티티 매핑 / 스키마 자동 생성) (0) | 2025.11.27 |
| [JPA 시리즈] 1 - 영속성 컨텍스트 (2 - 준영속) (0) | 2025.11.19 |
| [JPA 시리즈] 1 - 영속성 컨텍스트(1) (0) | 2025.11.12 |