본문 바로가기
JPA

[JPA] 연관관계 매핑 기초

by 민죠미 2022. 7. 28.

본 게시글은 김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 정리한 내용입니다.

 

연관관계가 필요한 이유

이전 강의 까지 객체에 외래키 를 직접 필드로 넣는 방식으로 설계를 하였지만 그런 방식은 객체지향적이라고 할 수 없다. JPA는 자바 표준 ORM 으로서 객체는 객체답게, 테이블은 테이블 답게 설계하기 위해 존재한다. 때문에 패러다임 불일치를 해결하기 위해 연관관계 매칭을 하여야한다.

참조 대신 객체가 외래 키를 그대로 사용하는 것은 객체 끼리의 연관관계가 없는 것과 같다.
객체를 테이블에 맞추어 모델링 하는 경우 아래와 같이 저장, 조회가 이루어지는데 이는 객체 지향적이라고 볼 수 없다.

//팀 저장
 Team team = new Team();
 team.setName("TeamA");
 em.persist(team);
 
 //회원 저장
 Member member = new Member();
 member.setName("member1");
 member.setTeamId(team.getId());
 em.persist(member);
//조회
 Member findMember = em.find(Member.class, member.getId());
 
 //연관관계가 없음
 Team findTeam = em.find(Team.class, team.getId());
객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.
- 조영호, 객체지향의 사실과 오해

객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
테이블은 외래키로 조인을 사용하여 연관 테이블을 찾고, 객체는 참조를 사용하여 연관된 객체를 찾는다.

 

단방향 연관관계

객체의 참조와 테이블의 외래키를 매핑하자.

위와 같이 ORM 매핑할 경우 저장과 조회는 다음과 같이 바뀐다.

//팀 저장
 Team team = new Team();
 team.setName("TeamA");
 em.persist(team);
 
 //회원 저장
 Member member = new Member();
 member.setName("member1");
 member.setTeam(team); //단방향 연관관계 설정, 참조 저장
 em.persist(member);
//조회
 Member findMember = em.find(Member.class, member.getId());
 
//참조를 사용해서 연관관계 조회
 Team findTeam = findMember.getTeam();



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

다음과 같이 Member와 Team이 양방향 관계를 가졌다고 생각해보자.

Member 엔티티

@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 엔티티

@Entity
 public class Team {
     @Id @GeneratedValue
     private Long id;
     
     private String name;
     
     @OneToMany(mappedBy = "team") // Member 클래스의 Team 참조 변수명 team
     List<Member> members = new ArrayList<Member>();
     …
 }

Team 에서 Member 탐색

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

 

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

  • 객체 연관관계 = 2개
    • 회원 -> 팀 연관관계 1개(단방향)
    • 팀 -> 회원 연관관계 1개(단방향)
  • 테이블 연관관계 = 1개
    • 회원 <-> 팀의 연관관계 1개(양방향)

객체의 양방향 관계

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

테이블의 양방향 연관관계

  • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리 (실질적으로 방향이라는 개념이 없음)
  • 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

 

연관관계의 주인

만약 팀을 바꾸고 싶을 경우 Member의 team을 수정해야할까 Team의 members를 수정해야할까?
테이블 입장에선 MEMBER의 외래키 TEAM_ID만 바꾸면 되는 상황

양방향 매핑 규칙

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


연관관계의 주인은 외래키가 있는 곳(MEMBER)을 주인으로 정하는 것이 좋다. (비즈니스 로직을 기준으로 선택하면 안됨)

 

양방향 매핑 주의사항

연관관계의 주인에 값을 입력하지 않은 경우

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

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

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

em.persist(member);

이 경우 MEMBER 테이블의 member1 의 팀 정보가 입력되지 않는다

member.setTeam(team); //**

따라서 위의 코드를 추가한다. JPA 로직상 team.getMembers().add(member); 가 없더라도 member1의 team정보가 DB에 저장되지만, 순수한 객체 관계를 고려하면 항상 양쪽 다 값을 입력해야한다. 하지만 실수로 빠뜨릴 가능성이 높으므로 연관관계 편의 메소드를 생성하는 것도 방법이다. 이 경우 편의메소드는 한쪽 클래스에만 생성한다. (주인/상대 가릴 것 없이 상황에 따라 좋은 곳)

편의 메소드 예

//Member 클래스에 만들 경우
public void changeTeam(Team team){
    this.team=team;
    team.getMembers().add(this);
}

//Team 클레스에 만들 경우
public void addMember(Member member){
    member.setTeam(this);
    members.add(member);
}

 

  • 양방향 매핑시에 무한 루프를 조심 (toString(), lombok, JSON 생성 라이브러리 등)
  • Entity를 Controller 에서 직접 반환하지 말것 (Dto 사용)
    => 값 변경 우려, Entity 수정시 Api 스펙까지 바뀌어 버림

 

양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료됨
  • 양방향 매핑은 반대 방향으로의 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
  • JPQL에서 역방향으로 탐색할 일이 많음
  • 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨 (테이블에 영향을 주지 않음)

댓글