Posts [자바 ORM 표준 JPA 프로그래밍-기본편] 양방향 연관관계와 연관관계의 주인
Post
Cancel

[자바 ORM 표준 JPA 프로그래밍-기본편] 양방향 연관관계와 연관관계의 주인

본 포스팅은 인프러의 JPA 기본편을 수강하고 정리하는 내용입니다.


양방향 매핑

image

  • 테이블 의 연관관계에는 외래키 하나로 양방향이 다 있는 것이다. (사실상 방향이란 개념 자체가 없다)
    • Member에서 내가 속한 팀을 알고 싶으면 Member의 TEAM_ID(FK)와 TEAM의 TEAM_ID(PK)를 조인하면 됨
    • TEAM에서 속한 멤버들을 알고 싶으면 TEAM의 TEAM_ID(PK)와 Member의 TEAM_ID(FK)를 조인하면 됨
  • 객체 는 양쪽으로 가려면 둘 다 레퍼런스를 가질 수 있는 필드를 추가해야 한다.

Member 엔티티는 단방향과 동일하다.

1
2
3
4
5
6
7
8
9
10
11
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    @Column(name = "USERNAME")
    private String name;
    private int age;
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    …

Team 엔티티는 컬렉션 추가한다.

1
2
3
4
5
6
7
8
9
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;
    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<Member>();
    …
}

반대 방향으로 객체 그래프 탐색 할 수 있다.

1
2
3
//조회
Team findTeam = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size(); //역방향 조회

과연 양방향 매핑이 좋냐는 관점으로 봤을때 객체는 가급적이면 단방향이 좋다. 양방향으로하면 신경쓸게 점점 많기 때문이다.

연관관계의 주인과 mappedBy

  • mappedBy = JPA의 멘탈붕괴 난이도
  • mappedBy는 처음에는 이해하기 어렵다.
  • 객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.

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

  • 객체 연관관계 = 2개 (양방향이라지만 실제로는 단방향 2개이다)
    • 회원 -> 팀 연관관계 1개(단방향)
    • 팀 -> 회원 연관관계 1개(단방향)
  • 테이블 연관관계 = 1개
    • 회원 <-> 팀의 연관관계 1개(양방향)
    • 외래키 하나로 양쪽의 데이터들을 모든걸 조회할 수 있음
    • 사실은 방향이 없는거임

image

  • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단 뱡향 관계 2개다.
  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어 야 한다.
  • A -> B (a.getB())
  • B -> A (b.getA())
1
2
3
4
5
6
7
class A {
    B b;
}

class B {
    A a;
}
  • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
  • MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계 가짐(양쪽으로 조인할 수 있다.)
1
2
3
4
5
6
7
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

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

image

  • 둘 중에 어떤걸로 매핑을 해야되는지 딜레마가 온다.
    • Member 객체의 Team 값을 변경했을때 외래키값이 업데이트되야하나
    • Team 객체의 members 값을 변경했을때 외래키값이 업데이트되야하나

DB 입장에선 TEAM_ID(FK) 값만 업데이트되면 되는데 어떤걸로(Team이나 Member) 관리를 할지 주인을 정해야 한다.

연관관계의 주인(Owner)

양방향 매핑 규칙

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

누구를 주인으로? (중요)

  • 외래 키가 있는 있는 곳을 주인으로 정해라
    • 단순한 가이드이다.
    • ManyToOneMany(다) 쪽이 연관관계의 주인 으로 하면 된다.
    • 만약 Team을 주인으로 한다고 가정했을 때, Team의 memebers값이 변경됐는데 Member테이블이 업데이트가 일어난다? 문제가 발생하기 마련이다.
    • Many쪽을 주인으로 해야 설계가 깔끔하게 들어간다.
    • 외래키가 존재하는 엔티티에서 관리를 해야 나중에 문제도 발생하지 않는다.
  • 여기서는 Member.team이 연관관계의 주인

image

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

연관관계의 주인에 값을 입력하지 않는 경우이다.

1
2
3
4
5
6
7
8
9
10
11
Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");

//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);

em.persist(member);

image

mappedBy 속성이 지정된 가짜 매핑은 단순한 읽기 전용 이므로 실제 DB에 반영되지 않는다. 양방향 매핑시 연관관계의 주인에 값을 입력해야 한다. 순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 한다.

객체지향적으로 생각해보면 양쪽에 다 값을 걸어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
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);

image

위의 코드처럼 진짜 주인(Member)에 설정을 해야 DB에 반영된다.

양쪽다 값을 입력하지 않는 경우

총 2가지 문제가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);

//회원 저장
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);

Team findTeam = em.find(Team.class, team.getId()); // 1차 캐시
List<Member> members = findTeam.getMembers();

for (Member m : members) {
    System.out.println("m = " + m.getUsername());
}

tx.commit();

1) 만약 One쪽인 Team에 값을 걸지 않는 경우 Team클래스는 1차 캐시 로 부터 엔티티를 가져와서 사용하게 되는데 그러면 List<Member> 에는 값이 없게 된다.

2) 테스트 케이스 작성 할 때 JPA없이 동작하도록 순수하게 자바 코드 상태로 작성한다. 그 케이스에도 Member.getTeam() 이 되는데 반대로 Team.getMembers() 하면 Null이 나오게 되어 문제가 발생한다.

Note: 추가적으로 persist()로 member를 저장 후 flush와 clear를 수행하면 1차 캐시 가 아닌 실제 DB로부터 가져오게 된다. 이땐 정상적으로 지연로딩을 통해 값을 불러올 수 있다.

양방향 연관관계 주의 - 실습

  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
  • 연관관계 편의 메소드 를 생성하자
    • member.setTeam(team);team.getMembers().add(member);를 항상 두 개 너어야하는데 사람이란게 실수하기 마련이다.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      
        @Entity
        public class Member {
        ...
              
        @ManyToOne
        @JoinColumn(name = "TEAM_ID")
        private Team team;
      
        public Team getTeam() {
            return team;
        }
      
        public void changeTeam(Team team) {
            this.team = team;
            team.getMembers().add(this);
        }
      
    • 이처럼 진짜 주인에 값을 넣을때 자동으로 가짜 주인에도 넣을 수 있도록 편의 메소드를 활용하자
    • 연관관계 편의 메서드나 JPA상태를 변경하는건 setter 를 활용하지 않는다.
    • 이는 단순하게 getter setter 관례에 의한게 아니라 어떤 작업을 수행하는지 명확하게 알 수 있어 좋다.
    • 예를 들어, setTeam() setter메서드로 수행하면 단순하게 Team만 설정하는 것 같다. 하지만 changeTeam() 메서드로 수행하면 명확하게 추가적인 로직이 들어가겠구나란걸 알 수 있다.
    • Team.addMember() 메서드를 추가해서 member.setTeam(this); memebers.add(member) 로직을 추가해도 상관없다. 즉, 연관관계 편의 메서드는 일에 넣어도 되고 다에 넣어도 된다. 실제 애플리케이션 개발할 때의 상황보고 정하면 된다.
  • 양방향 매핑시에 무한 루프를 조심하자
    • 예: toString(), lombok, JSON 생성 라이브러리 => entity를 컨트롤러에서 response로 직접 보낼때
    • lombok 에서 toString() 만드는거 왠만하면 쓰지말고 혹시 써도 위와 같은건 빼고써라
    • JSON 생성 라이브러리 와 관련해서 컨트롤러에서 entity를 절대 반환하지 마라. 왠만하면 DTO로 변환해서 반환해라. entity를 반환할 경우 2가지 문제 소지가 있다.
      • 1)무한루프 문제가 생길 수 있다
      • 2)나중에 entity를 변경하는 순간 api 스펙이 바껴버린다

양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료(중요!!!)
    • JPA 모델링할때 단방향 매핑으로 처음에 설계를 끝내야함(양방향 매핑 하면 안됨)
    • 처음에 설계를 할 때 주문시스템이다하면 연관된 테이블이 엄청 많을 것이다(많으면 100개)
    • 실무에선 사실 객체만으로 설계할 수 없다. 테이블 설계를 먼저 그리면서 객체 설계를 같이 들어가야 한다.
    • 그 시점에 테이블 관계에서 대략적인 foreign key가 나온다. 결국 Many(다) 쪽에서 단방향 매핑(ManyToOne, OneToOne)을 다 걸어서 들어가야 하는데 이때 양방향 매핑을 하지마라.
    • 처음엔 무조건 단방향 매핑으로 설계를 끝내고, 그리고 나서 반대방향으로 조회 기능이 필요할 때 양방향을 사용해라.
    • JPA를 쓸때 객체랑 테이블을 설계하는 것은 단방향 매핑만으로 이미 완료가 된 것이다.
  • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
  • JPQL에서 역방향으로 탐색할 일이 많음
  • 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨(중요!!!!)
    • 테이블에 영향을 주지 않는다.
    • 테이블 손댈 필요없이 엔티티에 코드 몇 줄만 추가하면 된다. 큰 노고가 들어가지 않는다.

정리

JPA 사용시 엔티티 설계 할 때

1) 단방향 매핑으로 다끝낸다.

2) 일대다 일때 쪽에 연관관계 매핑을 전부 설정해주고 설계를 끝낸다.

3) 실제 애플리케이션 개발하는 단계에서 양방향 매핑을 고려한다.

  • 객체 입장에서 양방향 매핑이란게 별로 크게 이득이 되는게 없다.

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

  • 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안됨
  • 연관관계의 주인은 실제 테이블의 외래 키 위치를 기준으로 정해야함
This post is licensed under CC BY 4.0 by the author.

[자바 ORM 표준 JPA 프로그래밍-기본편] 연관관계 매핑 기초

[자바 ORM 표준 JPA 프로그래밍-기본편] 실전 예제2 - 연관관계 매핑 시작