본문 바로가기
JPA

연관관계 매핑 기초

by 성건희 2020. 4. 6.
반응형
연관관계 기초

객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다. - 조영호 님

 

객체를 테이블에 맞추어 모델링

외래 키 식별자를 직접 다룸

 

생성할 때

Team team = new Team();

Member member = new Member();
member.setTeamId(team.getId());

 

조회할때

Member findMember = em.find(Member.class, member.getId());

Long findTeamId = findMember.getTeamId();
Team findTeam = em.find(Team.class, findTeamId);

 

위 방식은 필요할 때 마다 DB에서 계속 꺼내야 한다.

객체지향 스럽지가 않다..

 

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

  • 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾음
  • 객체는 참조를 사용해서 연관된 객체를 찾음
  • 테이블과 객체 사이에는 이런 큰 간격이 있음

 

객체지향 모델링

객체 연관관계 사용

 

Member 와 Team의 연관관계는 어떻게 정해야 할까?

하나의 Team에 여러 Member이 있다고 가정 (1)

 

Member 엔티티

@Entity
public class Member {
    
    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}

 

Team 엔티티

@Entity
public class Team {
    
    @Id @GeneratedValue
    @Column(name = "team_id")
    private Long id;
}

 

@ManyToOne

Member 는 n이고 Team은 1이므로 Member 엔티티 속성에 @ManyToOne을 붙여야 한다.

 

@JoinColumn

연관관계를 맺을 때 조인할 컬럼명은 무엇인지 지정해 주는 것

생략하면 필드명 + "_" + 참조하는 테이블의 기본키(@Id) 컬럼명 전략으로 외래 키를 매핑한다.

 

지연로딩 / 즉시로딩

ToMany는 기본적으로 지연로딩이다.

ToOne은 기본적으로 즉시로딩이다.

 

아마 연관관계를 만드신 분이 ToMany는 가져올 부분이 많으니까 즉시로딩으로 하면

성능이 떨어질 것이라 생각하고 그렇게 만든 것 같다.

 

양방향 연관관계와 연관관계의 주인

JPA에서 가장 어렵고 중요한 부분이므로 집중하기!!!

 

객체와 테이블의 가장 큰 차이

테이블 연관관계에서는 외래 키 하나양방향이 가능하다.

객체 연관관계에서는 양방향으로 하려면 Member와 Team 모두 서로를 가지고 있어야 한다.

 

위 예제에서는 Member에서는 Team을 조회할 수 있지만, Team에서는 Memer를 조회할 수 없다.

Member와 Team을 양방향 조회하려면 Team을 다음과 같이 바꿔주어야 한다.

 

@Entity
public class Team {
    
    @Id @GeneratedValue
    @Column(name = "team_id")
    private Long id;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

 

mappedBy

처음에 JPA를 공부할 때 이게 도데체 뭐지??? 하면서 고생을 엄청했다.

이것을 이해하기 위해서는 객체와 테이블간의 연관관계를 맺는 차이를 이해해야 한다.

 

객체는 연관관계가 2개다.

  • Member -> Team (단방향 1개)
  • Team -> Member (단방향 1개)
  • 양방향이라고 하지만 사실은 단방향이 2개다.

 

객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.

객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

 

테이블은 연관관계가 1개다.

  • Member <-> Team (양방향 1개)
  • 외래 키(team_id)를 통해 양방향이 가능하다.

 

테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.

member.team_id 외래 키 하나로 양뱡향 연관관계를 가진다. (양쪽으로 조인할 수 있다)

  • SELECT * FROM member m JOIN team t ON m.team_id = t.team_id
  • SELECT * FROM team t JOIN member m ON t.team_id = m.team_id

 

둘 중 하나로 외래 키를 관리해야 한다.

객체의 참조는 2개의 단방향으로 되어있다.

그러면서 다음과 같이 딜레마에 빠진다.

  • Member 의 Team의 값을 바꿨을 때 Member 테이블의 외래 키 값이 업데이트 되어야 하는가
  • Team의 Members의 값을 바꿨을 때 Member 테이블의 외래 키 값이 업데이트 되어야 하는가

 

예를들어 Member 엔티티에는 Team을 넣어놨는데, Team 엔티티에 Member를 넣어놓지 않았다면??

DB 曰 : "?..... 값을 넣어야되 말아야되? ㅡㅡ?"

 

연관관계의 주인을 정해서 한곳에서만 관리하도록 한다.

양방향 매핑 규칙

  • 객체의 두 관계중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정)
  • 주인이 아닌쪽은 읽기만 가능
  • 주인은 mappedBy 속성 사용 X
  • 주인이 아니면 mappedBy 속성으로 주인 지정

누구를 주인으로 해야되지?

외래 키가 있는 곳을 주인으로 정해라

Member 엔티티에 Team 의 FK가 있으므로 Member 가 주인!

 

물론, Team을 주인으로 정할 수 있다.

하지만 Team을 주인으로 정하면 다음과 같은 문제가 발생한다.

 

Team의 Member를 수정하게 되면 다른 테이블에서 업데이트 쿼리가 나간다.

나는 Team을 바꿨는데 왜 Member에서 업데이트 쿼리가 나가지? 혼동이 옴ㅋㅋ

 

또한 성능 이슈도 있다.

Member가 주인이면 Member를 INSERT 할 때 Member 안에 team_id (FK)가 있으므로 한방 쿼리로 날릴 수 있다.

Team이 주인이면 Team을 INSERT할 때 Team INSERT 쿼리 1개, Member UPDATE 쿼리 1개. 총 2개의 쿼리가 나간다.

 

객체는 가급적이면 단방향이 좋다. 양방향이면 신경쓸 부분이 많아진다.

 

양방향 매핑시 가장 많이 하는 실수

연관관계의 주인에 값을 입력하지 않는 상황

Team team = new Team();

Member member = new Member();

team.getMembers().add(member);

 

JPA를 사용하면 현업에서 자주 등장하는 실수다.

연관관계의 주인이 아니면 읽기만 가능하기 때문에 실제 DB에 반영되지 않는다.

따라서 DB에 반영하기 위해서는 연관관계의 주인에 값을 넣어야 한다.

Team team = new Team();

Member member = new Member();

member.setTeam(team);

 

정상적으로 DB에 값이 들어간다.

하지만객체지향 적으로 생각해보면 양쪽에 값이 들어가야 맞다는 생각이 든다.

(사실 지연로딩을 통해서 team.getMembers.add(member)를 하지 않아도 정상적으로 값이 들어가긴한다.)

 

그래서 사실 '그럼 굳이 양쪽으로 처리를 안해도 되는거 아냐? 번거롭기만 한데' 라는 생각이 든다.

나도 그랬다.

하지만 양쪽 모두 값을 넣어주지 않으면 다음과 같은 문제가 발생한다.

 

영속성 컨텍스트의 1차 캐시로 발생하는 문제

Team team = new Team();
em.persist(team);

Member member = new Member();
member.setTeam(team);
em.persist(member);

Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();

 

Team findTeam = em.find(Team.class, team.getId());

이 순간에는 DB의 값을 가져오는 게 아니라 영속성 컨텍스트의 1차 캐시에 있는 team을 가져온다.

따라서 영속성 컨텍스트의 team 엔티티는 member를 넣어주지 않았기 때문에 member를 찾을 수 없다.

이것을 해결하려면 team을 조회하기 전에 em.flush(), em.clear()를 해줘서 DB에 반영 후 영속성 컨텍스트를 비워줘야한다.

하지만 이것 역시 객체 지향과는 어울리지 않는 방식이므로 연관관계 편의 메서드를 사용하자.

 

양방향 연관관계 주의

순수 객체 상태를 고려해서 항상 양쪽에 값을 설정해야한다.

연관관계 편의 메서드를 생성하자.

양방향 매핑 시 무한 루프를 조심하자. - ex) toString(), lombok, JSON 생성 라이브러리

  • JSON 생성 라이브러리 엔티티를 직접 컨트롤러에서 Response로 보낼 때, 엔티티를 JSON 바꾸는 작업 도중 무한루프 발생 이 부분은 음악 API를 개발할 때 겪었던 내용이라 와닿았다ㅋㅋ 컨트롤러에서는 절대로 엔티티를 반환하지 마라 컨트롤러에서 엔티티를 반환하면 무한루프, 엔티티를 변경하는 순간 API 스펙이 바뀌어버림 문제가 발생 따라서 DTO로 변환해서 반환하는 것을 추천한다.

 

연관관계 편의 메서드

Member 엔티티

...
public void changeTeam(Team team) {
    this.team = team;
    team.getMembers().add(this);
} 

메서드의 이름을 setter로 만들면 단순 자바빈 규약으로 인식할 수 있다.

따라서 changeTeam 같이 '어떤 로직을 수행하는 구나' 명확하게 나타내는 것이 좋은 방식이다.

 

위 예제는 Member에 연관관계 편의 메서드를 만들었지만, 반대로 Team에 만들어도 된다.

어디에 연관관계 편의 메서드를 만들어야 할까? 에 대한 문제는 상황에 따라 다르므로 상황에 맞게 결정하면 된다.

중요한 것은 양쪽 모두 편의메서드가 있으면 문제(무한루프 등..)가 될 수 있으므로 한쪽만 만들자.

 

양방향 매핑 정리

단방향 매핑만으로도 이미 연관관계 매핑은 완료된다.

양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.

실무에서는 JPQL에서 역방향으로 탐색할 일이 많다.

단방향 매핑을 잘 해두면 양방향은 필요할 때 추가해도 된다.

  • 단방향에서 양방향으로 바꿔도 테이블을 손댄부분은 없다 (테이블에 영향을 주지 않음)

 

연관관계의 주인을 정하는 기준

비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안됨

연관관계의 주인은 외래 키의 위치를 기준으로 정해야 함

 

참고

- 자바 ORM 표준 JPA 프로그래밍 - 기본편

반응형

'JPA' 카테고리의 다른 글

JPA 를 공부하면서 알게 된 내용 정리 1  (0) 2022.01.26
다양한 연관관계 매핑  (0) 2020.04.09
엔티티 매핑  (0) 2020.04.06
영속성 관리 - 내부 동작 방식  (0) 2020.04.03
JPA를 왜 써야 하는가?  (0) 2020.04.03

댓글