연관관계 매핑

D A S H B O A R D
D E V E L O P
S E C U R I T Y
 연관관계 매핑 소개
 예제 시나리오를 통한 이해
 연관관계 주인
Reference

 연관관계 매핑 소개

연관관계 매핑은 객체와 테이블간의 연관관계를 맺어주는 것입니다. 객체는 참조를 사용하여 연관된 객체를 참조하고, DB는 외래키를 사용하여 연관된 테이블을 참조합니다. 따라서 객체의 연관관계를 DB에 저장하려면 참조를 사용하는 객체 모델링과 외래키를 사용하는 테이블 모델링 간의 차이를 이해하고 매핑을 해주어야 합니다.
JPA에서는 다양한 연관관계 매핑을 지원합니다. 대표적인 매핑 방법으로는 @ManyToOne, @OneToMany, @OneToOne, @ManyToMany 등이 있습니다. 이러한 매핑을 통해 객체 간의 연관관계를 맺어주어 객체지향적인 개발을 할 수 있습니다.

 예제 시나리오를 통한 이해

회원과 팀
회원은 하나의 팀에만 소속할 수 있다.
회원과 팀은 N:1 관계이다.

1. 객체를 테이블에 맞추어 모델링 - 연관관계 없는 객체

Memeber 객체

@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID ") private Long id; @Column(name = "USERNAME") private String username; @Column(name = "TEAM_ID") private Long teamId;
Java
복사

Team 객체

@Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name;
Java
복사

객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.

테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
객체는 참조를 사용해서 연관된 객체를 찾는다.
테이블과 객체 사이에는 이러한 큰 간극이 존재한다.

저장 시

//팀 저장 Team team = new Team(); team.setName("TeamA"); em.persist(team); //회원 저장 Member member = new Member(); member.setUsername("member1"); member.setTeamId(team.getId()); em.persist(member);
Java
복사
왼쪽 저장하는 코드에서 member.setTeamId() 와 같이 외래 키 식별자를 다룸으로 써 전혀 객체지향적인 모습을 보여주지 않는다.
객체지향적으로 바꾸려면 member.setTeam(team); 이 훨씬 맞는 모습이어야 한다.

조회 시

Member findMember = em.find(Member.class, member.getId()); Long findTeamId = findMember.getTeamId(); Team findTeam = em.find(Team.class, findTeamId);
Java
복사
조회 시에도 마찬가지로 Member에서 Team을 조회하려면 Member를 받아오고 Member 객체에서 또 다시 team Id를 받아온 후 Team을 조회해야 한다. → 즉, 객체 지향적이지 않다.

2. 객체지향 모델링 - 단방향 연관관계

해당 연관관계는 N : 1로 이루어져 있다. 즉, 다대일 관계이다.
여기서 우리는 객체지향 모델링을 하기 위해 단방향 연관 관계를 이용해 매핑한다.
다양한 매핑 관계가 존재하는데, 대표적으로 @ManyToOne, @OneToMany, @OneToOne, @ManyToMany 가 있다.
여기서는 N : 1 관계가 있기 때문에 @ManyToOne을 사용한다. → 또한 MEMBER가 N 이되는데, 이 때 MEMBER가 왜 N이 되는지 모른다면 RDB 공부를 하고 오는 것을 추천한다!
또한 객체로의 매핑이기 때문에 @JoinColumn 을 사용하여 외래키를 연결시켜주어야 한다. → 이는 아래 연관관계 주인을 참고
위의 연관관계 매핑 결과는 아래 그림과 같다.

Memeber 객체

@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID ") private Long id; @Column(name = "USERNAME") private String username; //@Column(name = "TEAM_ID") //private Long teamId; @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team;
Java
복사

Team 객체

@Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name;
Java
복사

객체지향 모델링

저장 시

//팀 저장 Team team = new Team(); team.setName("TeamA"); em.persist(team); //회원 저장 Member member = new Member(); member.setName("member1"); member.setTeam(team); //단방향 연관관계 설정, 참조 저장 em.persist(member);
Java
복사
테이블에 맞추어 모델링 한것과는 다르게 member 객체에서 연관된 team 객체를 한번에 저장할 수 있는 모습을 확인할 수 있다!

조회 시

//조회 Member findMember = em.find(Member.class, member.getId()); //참조를 사용해서 연관관계 조회 Team findTeam = findMember.getTeam();
Java
복사
테이블에 맞추어 모델링 한것과는 다르게 member 객체에서 외래키를 통해 새로운 객체를 조회하는 것이 아닌 바로 객체를 조회할 수 있다.

수정 시

// 새로운 팀B Team teamB = new Team(); teamB.setName("TeamB"); em.persist(teamB); // 회원1에 새로운 팀B 설정 member.setTeam(teamB);
Java
복사
연관관계에 대한 수정도 맞찬가지로 setTeam()을 통해 바로 진행시킬 수 있다.

2. 객체지향 모델링 - 양방향 연관관계

사용 자체는 단방향 연관관계와 다를 것이 없다.
Member는 그대로 두고 Team 객체에서 Member로의 매핑은 1: N 관계가 되기 때문에 @OneToMany 를 이용한다.
여기서 mappedBy라는 속성을 이용해주어야 하는데 이는 연관관계 매핑에서 가장 중요한 개념이다. → mappedBy의 값은 JoinColumn 어노테이션이 붙어 있는 변수 명이다.
이는 객체와 테이블이 관계를 맺는 차이를 알아야한다.

객체와 테이블이 관계를 맺는 차이

객체의 연관관계 ⇒ 2개
회원 ⇒ 팀 : 단방향 연관관계 1개
팀 ⇒ 회원 : 단방향 연관관계 1개
⇒ 객체를 양방향으로 참조하려면, 단방향 연관관계를 2개 만들어 사용해야 한다.
테이블의 연관관계 ⇒ 1개
회원 팀 양방향 연관관계 1개
⇒ 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
즉, 객체에서의 양방향 연관관계는 실제로는 존재하지 않고 단방향 연관관계로 서로 이어주었다는 의미이다.
실제로, 단방향 연관관계에서 보았던 테이블 연관관계와 양방향 연관관계에서 본 테이블 연관관계가 같은 것을 볼 수 있다.

Memeber 객체

@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID ") private Long id; @Column(name = "USERNAME") private String username; //@Column(name = "TEAM_ID") //private Long teamId; @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team;
Java
복사

Team 객체

@Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>();
Java
복사
위 설명에서 마한 바와 같이 mappedBy의 속성 값으로 @JoinCloumn 어노테이션이 있는 변수가 들어감

 연관관계 주인

양방향 연관관계 매핑 시 주의해야 한다.
Member 객체외 Team 객체가 서로 연관관계가 매핑되어 있기 때문에 Member에서도 Team을 바꿀 지, Team에서 Member를 관리할지 알 수 없다.
물론 둘 다 사용 가능하지만, 둘 다 사용할 경우 복잡성만 늘어나고 유지보수에도 좋지 않다.
그렇기 때문에 연관관계의 주인을 설정한 후, 주인이 아닌쪽은 읽기만 가능하도록 해야 한다.
웬만하면 단방향 매핑만 사용할 수 있다면 단방향 매핑만 사용 ⇒ 물론 실무에서는 역방향 탐색할 일이 많기 때문에 양방향을 자주 사용하긴 함
단방향 매핑만으로도 이미 연관관계 매핑은 완료
양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
JPQL에서 역방향으로 탐색할 일이 많음
단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨 (테이블에 영향을 주지 않음)

양방향 매핑 규칙

객체의 두 관계중 하나를 연관관계의 주인으로 지정
주인은 mappedBy 속성 사용X
연관관계의 주인만이 외래 키를 관리(등록, 수정) → N : 1에서 N에 해당되는 테이블
주인이 아닌쪽은 읽기만 가능
주인이 아니면 mappedBy 속성으로 주인에 대한 매핑 지정
즉, mappedBy를 사용하지 않은, 외래키를 가지고 있는 객체를 주인으로 지정해라(N:1 관계에서 N에 해당되는 테이블) → 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안됨
⇒ 위 Member와 Team의 예제에서는 Member가 주인이 되는 것

많이 하는 실수 및 주의점

Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setName("member1"); //역방향(주인이 아닌 방향)만 연관관계 설정 team.getMembers().add(member); em.persist(member);
Java
복사
해당 코드는 연관관계의 주인인 Member의 값을 입력해주지 않았다.
Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setName("member1"); team.getMembers().add(member); //연관관계의 주인에 값 설정 member.setTeam(team); //** em.persist(member);
Java
복사
순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 한다.
주의! ⇒ 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
연관관계 편의 메소드를 생성하자
⇒ 아래와 같은 코드로 양방향 연관관계 설정 시 양쪽의 값을 함께 입력해주는 매서드
public void addMember(Member member){ member.setTeam((this)); members.add(member); }
Java
복사
양방향 매핑시에 무한 루프를 조심하자 → 예) 예: toString(), lombok, JSON 생성 라이브러리
⇒ 아래와 같이 toString() 같은 경우에서 만약 Member를 toString으로 호출할 시 team이 출력되고 team이 출력되면 또 Members라는 객체가 또 전부 출력되는 무한루프가 발생할 수 있다.
@Override public String toString() { return "Member{" + "id=" + id + ", username='" + username + '\'' + ", team=" + team + '}'; }
Java
복사
@Override public String toString() { return "Team{" + "id=" + id + ", name='" + name + '\'' + ", members=" + members + '}'; }
Java
복사
Controller에서는 entity를 절대로 반환하지 말아라!!! ⇒ 실무에서 API를 사용하는데 Entity를 반환한다고 하면, 만약 Entity가 변경될 경우 API 명세 자체가 바뀌어 버리는 상황이 되기 때문에 큰 문제가 발생한다.