연관관계 매핑
객체와 테이블 연관관계의 차이를 이해하고 , 객체의 참조와 테이블의 외래키를 매핑하는 것에
대해 알아본다.

@Entity
public class Members {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Teams teams;
@Entity
public class Teams {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
다음과 같은 두개의 객체 (테이블) 이 존재할 때 , 멤버는 하나의 팀에 속할수 있고 , 하나의 팀은 여러 명의 멤버를 가질 수 있는 N:1 관계이다.
여기서 주의해야 할 점은 , Member 테이블에 ForeignKey(외래키)도 포함해 주었는데(private Teams teams) 앞으로 '객체'를 '테이블'에 맞추어 모델링을 하는 것을 기본 베이스로 깔고 간다.

다음과 같은 코드를 작성한다고 하면 , team 을 저장하는 것은 문제 없지만 , 회원을 저장시에 , team.getId()를 통해 아이디를 꺼내오고 , 해당 아이디로 다시 member.setTeamId() 로 저장한다.
이는 member에서 외래키 식별자인 TeamId 를 직접 접근하는 문제가 생기기 때문에 좋지 않은 방법이다.
객체를 테이블에 맞추어 데이터 중심으로 모델링하면 , 협력 관계를 만들 수 없다.
테이블은 외래키 조인을 통해 연관된 테이블을 찾는 반면 , 객체는 참조를 사용해서 연관된 객체를 찾는다.
테이블과 객체 간의 이러한 간격을 줄이기 위해 연관 관계 개념을 도입했다.
단방향 연관 관계

앞서 설명했듯이 , Member 테이블에 Team team (외래키)를 넣어서 매핑한다.
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Teams teams;
멤버 테이블에서 N:1 관계이므로 @ManyToOne 어노테이션을 붙여주고 , Team 테이블에서 Join 하고자 하는 칼럼을 @JoinColumn 어노테이션을 이용해 작성한다.

이전의 코드에서 멤버에 팀을 저장하는 경우 , 외래키를(teamId) 통해 직접 설정하였지만 지금의 경우 , 멤버테이블에서
setTeam 으로 참조하여 저장하게 된다.
양방향 연관관계

양방향 연관관계와 단방향 연관관계의 차이점은 Team 테이블에도 List members 라는 변수를 설정해주는 것이다.
@OneToMany(mappedBy = "teams")
private List<Members> membersList = new ArrayList<>();
Team 테이블에서는 Member 와 1:N 관계이므로 @OneToMany 를 사용하고 mappedBy 를 사용한다.
mappedBy 는 단순히 어떤 것과 매핑되었는지 확인하고 작성해주면 된다.
앞선 Member 테이블에서는 @ManyToOne 으로 Teams teams 와 매핑되었으므로 mappedBy = "teams" 로 작성한다.
객체간의 연관관계는
회원 -> 팀 연관관계 1개 ( 단방향 )
팀 -> 회원 연관관계 1개 ( 단방향 )
으로 총 2개이다. => 이는 곧 양방향 연관관계
테이블간의 연관관계는
회원 <-> 팀 연관관계 1개 ( 양방향 ) 이다.
객체를 양방향으로 참조하기 위해서는 단방향 연관관계를 2개 만들면 된다.

Member 는 team 과 연관관계 매핑이 돼있고 , Team 은 member 와 연관관계 매핑이 돼 있다.
만약 , A 팀에 속하는 member1 을 B 팀으로 바꾸고 싶다고 하는 경우 , Member 테이블에서 수정을 해야할 지 ,
Team 테이블에서 수정을 해야할 지 기준을 정할 수가 없다.
Member 테이블에서 수정하는 경우 , member.setTeam(B);
Team 테이블에서 수정하는 경우 , team.setMembers(member1);
즉 두 가지 방법으로 바꿀 수 있는데 바꾸는 기준은 '연관관계의 주인' 지정을 통해 진행한다.
연관관계 주인(Owner)
객체의 두 관계중 , 하나를 연관관계의 주인으로 지정하고 , 연관관계의 주인만이 외래키를 관리한다. (등록 , 수정)
주인이 아닌 쪽은 읽기만 가능하고 , 주인은 mappedBy 속성을 사용하지 않는다.
반대로 주인이 아닌 경우에 mappedBy 속성을 사용한다.
주인을 정하는 기준은 바로 외래키가 있는 곳이 기준이다.
1:N 의 경우 , N 에 외래키가 포함되므로 Member 가 연관관계의 주인이 된다.
Members members = new Members();
members.setUsername("members1");
em.persist(members);
Teams teams = new Teams();
teams.setName("teamA");
teams.getMembersList().add(members);
em.persist(teams);
다음과 같은 코드를 실행할 경우 ,

위 그림과 같이 TEAM_ID 에 null 값이 들어감을 확인할 수 있다.
이는 위에서 말했던 연관관계의 주인을 잘못 생각했기 때문이다.
연관관계의 주인은 N:1 의 관계 중 , N 쪽인 바로 member 테이블이 바로 주인이다.
연관관계의 주인에서 등록 또는 수정 해야 하는데 , 위 경우는 teams.getMembersList().add 에서 볼 수 있듯이 연관관계의 주인이 아닌 곳에서 add 요청을 했기 때문에 null 값이 들어가는 오류가 발생하는 것이다.
Teams teams = new Teams();
teams.setName("teamA");
em.persist(teams);
Members members = new Members();
members.setUsername("members1");
members.setTeams(teams);
em.persist(members);
- 정상적인 처리 / 연관관계의 주인 테이블 에서 값을 수정한다.
※ 주인 테이블에서만 값을 등록 / 수정하는 것으로 배웠지만 flush, claer 를 하지 않는 경우 , DB 가 아닌 1차 캐시에서 값을 바로 가져오기 때문에 JPA 가 값을 못읽어온다거나 , 테스트 케이스 작성 시 원하는 값이 나오지 않는 경우 발생
이를 위해서 매핑된 쪽에도 값을 넣어준다.
teams.getMembersList().add(members);
하지만 위 경우는 깜빡해서 빠뜨리는 경우가 종종 발생하고 이러한 경우를 방지하기 위해
연관관계 편의 메소드를 생성한다.
public void setTeams(Teams teams) {
this.teams = teams;
teams.getMembersList().add(this);
}
set을 호출하는경우 자동으로 반대편 테이블에도 add 가 호출돼서 편리하다.
(setTeams 메소드명을 changeTeams 명으로 변경)
정리
※
처음 설계 시에는 단뱡향 매핑만으로 설계한다.
양방향 매핑은 반대 방향으로 조회 기능이 추가된 것 뿐 , 단방향 매핑을 우선시하고 양방향(mappedBy)은 필요할 때
추가하면 된다.
연관관계의 주인을 정하는 기준은 반드시 외래 키의 위치를 기준으로 정한다. (N이 있는 곳)
* 해당 글은 'Infleran'의 김영한 강사님의 자료를 참조하였습니다.
'JPA Basic' 카테고리의 다른 글
JPA Basic - 6 (0) | 2021.12.30 |
---|---|
JPA Basic - 5 (0) | 2021.12.29 |
JPA Basic - 3 (0) | 2021.11.18 |
JPA Basic - 2 (0) | 2021.11.16 |
JPA Basic - 1 (0) | 2021.11.16 |
댓글