일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 스프링 시큐리티
- spring boot
- 컨테이너
- 스프링 배치
- Spring
- vm
- computer science
- 스프링
- HTTP
- spring cloud
- mysql
- spring batch
- 가상화
- JPA
- Spring Security
- Java
- 자바
- 배포
- 백엔드
- 영속성 컨텍스트
- virtualization
- 웹 서버
- 도커
- CI/CD
- CS
- web server
- Container
- ORM
- 데이터베이스
- 스프링 부트
- Today
- Total
개발 일기
[섹션5] 연관관계 매핑 기초 본문
객체의 참조와 테이블의 외래 키를 어떻게 매핑하는가!
연관관계가 필요한 이유
ex) 회원과 팀이 있을때 회원은 하나의 팀에만 소속될 수 있으며 회원과 팀은 다대일 관계(N:1)다.
우선, 참조가 아닌 객체를 테이블에 맞추어 모델링한다고 가정해보자. 즉, 연관관계가 없는 객체이다.
관계형 데이터베이스를 다뤄본 개발자라면 위의 그림을 이해하는데는 그닥 어렵지 않을 것이다. 하나의 팀에 여러명의 맴버가 포함될 수 있기 때문에 member테이블에서 FK로 team_id를 가지는 것이다. 이러한 테이블을 바탕으로 객체에 맞게 해보면 Member객체가 teamId(테이블에서의 외래키)값을 그대로 들고있는 것이다.
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId());
// 여기서 .setTeam()이 아닌 .setTeamId로 해야하는 이슈가 있다.
em.persist(member);
// 조회 시에도 findMember를 해서 맴버 객체를 찾아준 뒤
// 또 .getTeamId()를 통해 팀ID를 찾고 그 ID로 팀 객체를 꺼내야 한다.
Member findMember = em.find(Member.class, member.getId());
Long findTeamId = findMember.getTeamId();
Team findTeam = em.find(Team.class, findTeamId);
위와 같이 저장, 조회 등의 과정에서 연관관계가 없기 때문에 이슈들이 발생한다.
테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾지만 객체는 참조를 사용해서 연관된 객체를 찾아야 한다.
위와 같은 이슈로 테이블 중심적 모델링이 아닌 객체 지향 모델링(객체 연관관계 사용)을 통한 설계가 필요하다.
단방향 연관관계
테이블 중심적 모델링과 다르게 객체 지향 모델링은 Member 객체에서 team_id가 아닌 Team 객체 그 자체를 들고 있다.
// Member 객체에서... 객체 지향 모델링!
// JoinColumn을 통해 FK를 지정해 주는 것이다.
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
위와 같이 수정해주면 된고 아래 그림과 같이 연관관계가 생기게 되는 것이다.
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); //단방향 연관관계 설정, 참조 저장 이 부분이 바뀜(.setTeam()으로 처리 가능)
em.persist(member);
//조회
Member findMember = em.find(Member.class, member.getId());
//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();
양방향 연관관계와 연관관계의 주인 - 기본
이 파트는 정말 어렵고 복잡하니 집중하자.
우선 단방향 연관관계의 차이를 언급해보자면 위의 예시에서 member.getTeam()을 통해서 맴버에서 팀으로 조회는 가능하지만 team.getMember()은 불가능하다. 그러나 이 양쪽이 다 가능한 것이 바로 양방향 연관관계이다.
테이블은 team 테이블의 PK인 team_id를 member 테이블의 FK로 사용하기 때문에 team_id로 이 두 테이블을 JOIN하면 서로 서로 알 수 있다. 그러나 객체는 단방향 연관관계에서와 같이 Member 객체에서는 Team 객체로 갈 수 있었지만 그 반대는 가능하지 않았다. 그래서 Team 객체에다가 `List members`를 넣어주면서 양방향 객체 연관관계가 된다.
// Team 객체에서...
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<Member>();
// 그렇게 되면? 아래와 같이 양방향 조회가 가능해 진다.
//조회
Team findTeam = em.find(Team.class, team.getId());
//역방향 조회
int memberSize = findTeam.getMembers().size();
연관관계의 주인과 mappedBy
양방향 연관관계에서 객체 연관관계는 단방향 연관관계 2개가 합쳐진 것으로 볼 수 있다.
- 회원 to 팀 단방향 연관관계 1개
- 팀 to 회원 단방향 연관관계 1개
이렇게 2가지이다. 각각에 참조값을 넣어 놓고 참조하기 때문에 2개로 봐야한다.
즉, 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다.
그러나 테이블 연관관계는 외래 키 하나로 두 테이블의 연관관계를 관리한다.(JOIN)
연관관계의 주인
이때, 서로 다른 연관 관계 2개가 합쳐진 양방향의 경우 Member에서 Team으로 가는 `Team team` 참조값, Team에서 Member로 가는`List members` 참조값 이렇게 두가지가 있는데 그러면 이 두 가지 참조값 중 어떤 것을 업데이트했을 때 실제 데이터베이스의 외래키 값(team_id)이 업데이트 되도록 할지 고민이 될 수 있다.

- 이때, 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
- 연관관계의 주인만이 외래 키를 관리(등록 및 수정)가 가능
- 주인이 아닌 쪽은 읽기만 가능
- 일대다(1:N)관계에서 다(N)쪽이 주인이다.
- 주인은 mappedBy 속성을 사용하지 않고 주인이 아닌 쪽에서 mappedBy 속성으로 주인을 지정해 준다.
- 주인 쪽이라고 해서 비즈니스 적으로 중요한 것은 아니다.
그래서 누구를 주인으로 해야하는가?
외래 키가 있는 곳을 주인으로 정해야 한다.
즉, 위의 예시에서는 member 테이블에서 team_id인 FK를 들고 있기 때문에 Member 객체가 주인이 된다.
조금 더 자세히 말해보자면 Member객체의 team 필드(Member.team)가 연관관계의 주인이고 주인의 반대편이 Team객체의 members 리스트인 (Team.members)가 된다.

양방향 연관관계와 연관관계의 주인 - 주의점, 정리
1. 연관관계의 주인을 통해서 값을 넣어줘야함
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);
em.persist(member);
위와 같이 Team 객체의 `List member`에 직접 맴버를 추가해줘도 연관관계의 주인이 아닌 쪽에서 값을 넣어 줬기 때문에 실제로는 아무 쿼리도 발생하지 않고 테이블 상으로 member 테이블의 team_id는 null이다.
그렇기 때문에 아래와 같이 수정해야 한다.
//연관관계의 주인에 값 설정
member.setTeam(team);
이렇게 되면 연관관계의 주인을 통해 처리되기 때문에 member 테이블의 team_id값도 업데이트 되어 정상적으로 처리 된다. 당연히 Team 객체의 List members에도 업데이트가 된다.
2. 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자 -> 연관관계 편의 메소드 생성
그러나 .setTeam()만으로 처리되지 않는 경우도 있어서 team.getMembers().add(member);를 함께 써줘야 하는데 빠져먹는 경우가 있을 수 있으니 아싸리 setter에다가 이 코드를 넣어서 한번에 처리되도록하자!
// Member 객체에서...
// 원래 setTeam()인데 편의 메소드를 활용했으니 이름을 따로 지정해주자 김영한님은 changeTeam()이라고 하신다고한다.
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this); // 이렇게해주기
}
//또는
public void addMember(Member member {
member.setTeam(this);
members.add(member);
}
위와 같이 두가지 방향에서 편의 메소드는 한쪽만 선택해서 해주면 된다. 이것은 자기마음이다. 단, 두가지 모두 해서는 안되고 꼭 하나만 선택하자. 최악의 경우 무한 루프 가능성.
3. toString(), lombok, JSON 생성 라이브러리에서 양방향 매핑 시 무한루프 주의!(StackOverFlow)
양방향 매핑은 언제하는가?
단방향 매핑만으로도 이미 연관관계 매핑은 완료된다.
양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색)기능이 추가 된 것인데
처음에는 단방향 매핑만으로 모든 연관관계 매핑을 완료해두고
JPQL에서 역방향으로 탐색할 일이 발생한다. 그때 되서야 양방향 매핑으로 전환해주면 된다.
이때, 단방향 매핑만 잘 해 놓는다면 Entity 테이블을 건드릴 필요없이 코드 몇 줄이면 처리 된다.
연관관계의 주인
비즈니스 로직을 기준으로 연관관계 주인을 선택하는 것이 아니라
연관관계의 주인은 외래 키의 위치를 기준으로 정해야 함
'Back-End > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[섹션 7] 고급 매핑 (0) | 2024.04.02 |
---|---|
[섹션 6] 다양한 연관관계 매핑 (0) | 2024.04.01 |
[섹션4] 엔티티 매핑 (1) | 2024.03.25 |
[섹션3] 영속성 관리 - 내부 동작 방식 (0) | 2024.03.25 |
[섹션1] JPA 소개 (2) | 2024.03.24 |