JPA - 연관관계 매핑 기초

bp.chys·2020년 5월 18일
0

JPA

목록 보기
3/15

연관관계 매핑이 필요한 이유

  • 객체지향의 설계 목표는 '자율적인 객체들의 협력 공동체를 만드는 것'
  • 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력관계를 만들 수 없다.
  • 테이블과 객체는 연관관계에 있어서 패러다임이 아예 다르다.
    • 테이블은 외래키로 조인을 사용해서 연관된 테이블을 찾는다.
    • 하지만 객체는 참조를 사용해서 연관된 객체를 찾는다.

객체를 간접 참조하는 경우

  • 조회 시 객체 내부의 다른 값을 참조하려면 외래키로 테이블 조회가 한번 더 필요하다.
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    
    @Column(name = "TEAM_ID")
    private Long teamId;   // 참조 대신 외래키를 그대로 사용
    ...
}
//Main.java
...
Member findMember = em.find(Member.class, member.getId());

// 바로 참조할 수 있는 연관관계가 없다.
Team findTeam = em.find(Team.class, findMember.getTeamId());
// Team findTeam = findMember.getTeam(); 이 더 객체지향적인 방법이다.

단방향 연관관계

  • Member가 teamId가 아닌 team을 직접 참조할 수 있도록 설계하면, 데이터 중심이 아닌 객체 중심의 모델링을 할 수 있다.
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;   // 외래키 대신 참조를 사용
    ...
}
// 조회
Member findMember = em.find(Member.class, member.getId());
// 참조를 사용한 연관관계 조회
Team findTeam = findMember.getTeam();

양방향 연관관계

양방향 매핑

  • Member에서는 이제 getTeam을 사용해 본인이 참조하고 있는 Team에 접근할 수 있다.
  • 하지만 반대로 Team에서는 Member에 대한 어떤 정보도 갖고 있지 않기 때문에 자신을 참조하고 있는 Member로 접근이 불가능하다.
  • 이를 해결하기 위해서 양방향 매핑이 필요하다.
  • 다대일 관계에서 '다'쪽은 객체를, '일'쪽은 컬렉션을 갖고 있어야 한다.
  • 실무에선 가급적 단방향 매핑으로 설계를 마무리해야한다.

연관관계의 주인과 mappedBy

  • Member에서 Team을 수정하거나, Team에서 Member 컬렉션을 수정한다고 했을 때 도대체 언제 DB에 반영을 시켜야하는 것일까?
  • 객체는 외래키가 있는 쪽이 바로 연관관계가 주인이 되어야 한다.
  • 이 연관관계의 주인만이 외래키를 관리할 수 있어야한다.(등록, 수정)
  • 주인이 아닌 쪽은 읽기만 가능할 뿐이다.

  • 주인이 아닌 쪽은 mappedBy 속성을 줌으로써 주인을 표시해야 한다.
  • 이때 주인은 상대편의 인스턴스 변수명으로 한다. (테이블 필드명 x)
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;   // 외래키 대신 참조를 사용
    ...
}

@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<>();  // 컬렉션 선언과 초기화를 함께 해주는 것은 JPA 관례이다.
    ...
}

양방향 연관관계 주의할 점

1. 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자.

Team team = new Team();
team.setName("TeamA");
em.persist(team);

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

// 여기서 team의 members를 조회하면 아무것도 안나온다.
// 현재 team은 1차 캐시의 상태이다. 
// 만약 캐시를 비우고 나서 team.getMembers를 한다면, <Select:Team>, <Select:Member> 쿼리가 실행되면서 정상적으로 출력이 될 것이다.
// 하지만 캐시를 비우지 않은 순수 객체 상태에서는 정상적으로 역방향 참조를 할 수 없다.
for (Member member : team.getMembers()) {
    System.out.println(member.getId());
}

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

  • 연관관계 편의 메소드란 단방향 관계를 맺을 때 한번에 양방향 관계를 맺을 수 있도록 하나의 함수에 로직을 추가하는 것이다.
  • 연관관계 주인 쪽에 정의하는 것이 좋다.
// Member.java
...
public void changeTeam(Team team) {
    this.team = team;
    team.getMembers.add(team);
    // 수정 시 로직 추가 요구됨
...

3. 양방향 매핑시에 무한 루프를 조심하자.

  • toString(), lombok, JSON 생성 라이브러리 등은 양방향 참조시 무한루프에 빠질 수 있다.
    • 따라서 Controller에서는 응답을 Entity로 내리면 안된다.

결론

JPA로 객체간의 연관관계를 매핑하는 방법은 크게 외래키로 간접참조하는 방법이 있고, 객체를 직접 참조하는 방법이 있다. 객체를 직접 참조하게 되면 연관관계 객체의 내부를 별도의 쿼리조회없이 접근할 수 있기 때문에 보다 객체지향적인 프로그래밍이 가능하다.

그러나 모든 연관관계에서 직접 참조를 하게 될 경우, 연관관계 객체를 의도치 않게 변경시킬 위험이 있기 때문에 같은 도메인의 어그리게잇에 한해서만 사용하는 것이 좋을 듯 하다.

직접 참조를 하더라도 단방향 매핑과, 양방향 매핑이 있다. 기본적으로 초기 설계는 모두 단방향으로 완료할 수 있어야하며 양방향은 꼭 필요한 경우에만 매핑을 허용하는 것이 좋다.
양방향 매핑이 불가피한 경우라면 설계를 다시 고민해보거나, 값을 빠지는 경우나 무한루프를 주의하면서 사용해야한다.

참고자료

  • 자바 ORM 표준 JPA 프로그래밍, 김영한 저
profile
하루에 한걸음씩, 꾸준히

0개의 댓글