본문 바로가기
Back-End/Database

[JPA] 연관관계 매핑

by 달의 조각 2022. 9. 10.
이 글은 김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 수강하며 정리한 글입니다.

 

객체와 테이블 연관 관계의 차이
객체의 참조와 테이블의 외래키 매핑


방향, 다중성, 연관관계의 주인

 

  객체를 테이블에 맞춰서 모델링 하면 협력 관계를 만들 수 없다. 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾지만, 객체는 참조를 사용해서 연관된 객체를 찾기 때문이다.

아래와 같이 외래키의 식별자를 직접 다루게 된다. member1이 속한 팀을 알고 싶을 때에도 member1의 teamId를 통해 team 객체를 찾아야 하는 과정이 계속 생긴다. 객체 지향적이지 않다.

// 팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);

// 회원 저장
Member member = new Member();
member.setUsername("member1");
member.setTeamId(team.getId()); // 여기
em.persist(member);

 


 

단방향 연관 관계

 

teamId (X) team (O)

// @Column(name = "TEAM_ID")
// private Long teamId;
// Member.java
@ManyToOne // Member: Many, Team: One
@JoinColumn(name = "TEAM_ID") // 조인하는 컬럼
private Team team;
// 팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);

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

Getter와 Setter를 통해 객체지향적으로 간단히 저장, 조회, 수정 등의 작업을 할 수 있다.

단방향만으로도 연관 관계 매핑은 완료되었다. 양방향은 단순히 반대 방향으로 조회하는 기능이 추가된 것이다. 단방향 매핑을 잘하고, 양방향은 필요할 때 추가하자. (테이블에 영향을 주지 않는다.)

다대일(N이 연관 관계의 주인이 되는 관계) 단방향 매핑을 사용하고, 반대 참조가 필요하다면(JPQL에서 해당 경우가 많음) 참조를 하나 더 추가하여 양방향 매핑(양쪽 참조)을 사용하자.

일대일 관계에서는 아무 테이블에 외래키를 설정할 수 있으며, 외래키에 DB 유니크 제약 조건을 추가한다. 다대일 단방향 매핑과 유사하다.

 


 

양방향 연관 관계와 주인

 

엔티티에 List만 추가되었을 뿐, 테이블 설계가 달라지지 않는다

  • 테이블: 외래키 하나로 양쪽 조인이 가능하다.
    ➡️ (양방향) 연관 관계 1개
  • 객체: 하나의 테이블에만 참조를 두면 단방향 접근만 가능하다. 참조하는 테이블에도 참조를 두어야 서로의 객체에 접근할 수 있다.
    ➡️ (단방향) 연관 관계 2개
// Team.java
@OneToMany(mappedBy = "team") // Member의 변수 team과 연결되어 있다 = team으로 매핑 되어 있다
private List<Member> members = new ArrayList<>(); // 연관 관계의 주인이 아니므로 값을 넣어도 아무 일이 일어나지 않는다

 

🌟 어떤 클래스가 외래키를 관리할 것인가?

  만약, 한 Member가 Team을 바꾸려면 Member의 team 객체를 변경해야 할까, Team 클래스의 members 객체를 바꿔야 할까? DB에서는 Member 테이블의 외래키만 바꾸면 된다. 단방향일 때는 상관없지만, 두 개의 단방향으로 관계를 맺어서 신경 쓸 포인트가 늘어난 것이다.

  • 외래키가 있는 곳(N)을 연관 관계의 주인으로 지정해서 외래키를 관리하도록 하자.
  • 주인은 mappedBy (= 매핑되었다) 속성을 사용하지 않고, 주인이 아닌 쪽은 mappedBy 속성으로 주인을 지정한다.
  • 연관 관계의 주인만이 등록과 수정이 가능하며, 주인이 아닌 쪽은 읽기만 가능하다.

 

🌟 주의

  • 양방향 매핑 시 연관 관계의 주인에 값을 입력해야 한다. 순수한 객체 관계를 고려해서 항상 양쪽 다 값을 입력하자.
  • 1번 과정의 번거로움과 실수를 방지하기 위해 연관관계 편의 메소드를 생성하자.
  • 양방향 매핑 시 무한 루프를 조심하자.
    ➡️ toString(), lombok, JSON 생성 라이브러리(컨트롤러에서는 절대 엔티티를 반환하지 말고, DTO를 사용하자.)
// Member.java => Team 클래스에 만들어도 되지만 한쪽에만 만들 것
// 연관관계 편의 메소드
public void changeTeam(Team team) {
    this.team = team;
    team.getMembers().add(this); // 여기 => 나 자신 인스턴스 Member
}
더보기

🌕 연관 관게 주인의 역방향으로도 설정해야 하는 이유

1

Team, Member 객체 persist

// team.getMembers().add(member); (1)

// em.flush(); (2)
// em.clear();

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

commit

1번처럼 연관 관계 주인의 역방향으로 값을 입력하거나, 2번처럼 플러시를 하는 경우에는 DB에 값이 반영된다.

하지만 1번 또는 2번 코드를 주석처리 하면 find 시 데이터베이스가 아닌 1차 캐시에서(영속성 컨텍스트에 등록 시기 persist) 데이터를 가지고 온다.

DB에서 연관 관계를 로딩하지 않아서 순수한 객체를 가져온 것이라고 볼 수 있으므로 findTeam을 통해 접근한 members에는 아무것도 존재하지 않는다.

2

테스트 케이스를 작성할 때 JPA 사용 없이 순수 자바 코드를 사용하면 문제가 생길 수 있다.

댓글