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

Lee Han Sol·2021년 10월 3일
1
post-thumbnail

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

이 글은 김영한님의 Inflearn 강의를 학습한 내용을 정리하였습니다.
글에 포함된 그림의 출처는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의와 자바 ORM 표준 JPA 프로그래밍입니다.

목표

객체 관계 매핑(ORM)에서 가장 어려운 부분은 객체 연관관계와 테이블 연관관계를 매핑하는 일이다.
이번 글의 목표는 객체 참조와 테이블의 외래 키 매핑 방법이다.

시작 전에 아래 용어를 알아두면 좋다.

  • 방향(Direction) : [단방향, 양방향]이 있다.

예를들어 회원과 팀이 관계가 있을 때 회원 → 팀 또는 팀 → 회원 둘 중 한 쪽만 참조하는 것을 단방향 관계라 하고, 회원 → 팀, 팀 → 회원 양쪽 모두 서로 참조하는 것을 양방향 관계라고 한다.
방향은 객체관계에서 존재하고 테이블 관계는 항상 양방향이다.

  • 다중성(Multiplicity) : [다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)] 다중성이 있다.

  • 연관관계의 주인(Owner) : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.

연관관계에 대한 설명으로 다음 시나리오를 사용한다.

  • 회원과 팀이 있다.
  • 회원은 하나의 팀에만 소속된다.
  • 회원과 팀은 다대일 관계다.

단방향 연관관계

여러 연관관계 중에서 다대일(N:1) 단방향 관계부터 알아보자.

객체 연관관계와 테이블 연관관계 (다:일)

위 그림을 분석해보자.

  • 객체 연관관계
    회원 객체는 Member.team 필드로 팀 객체와 연관관계를 맺는다.
    회원 객체와 팀 객체는 단방향 관계이다. 회원은 Member.team 필드를 통해서 팀을 알 수 있지만 반대로 팀은 회원을 알 수 없다.

  • 테이블 연관관계
    회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.
    회원 테이블과 팀 테이블은 양방향 관계이다. 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고 반대로 팀과 회원도 조인할 수 있다.(JOIN 사용)

외래 키를 이용한 양방향 조인

회원과 팀을 조인하는 SQL
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

팀과 회원을 조인하는 SQL
SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID

그림을 통해서 테이블은 외래 키를 통해 양방향으로 조회가 가능하지만 객체는 단방향으로만 조회가 가능한 것을 알 수 있다.
만약 객체간에 연관관계를 양방향으로 만들려면 반대쪽에도 필드를 추가해야 한다. (정확히는 단방향 2개를 만든다.)

객체 관계 매핑

위 내용에 JPA를 적용한 코드는 아래와 같다.

// 회원 엔티티
@Entity
public class Member {
  @Id
  @Column(name = "MEMBER_ID")
  private String id;
  private String username;
  
  //연관관계 매핑
  @ManyToOne
  @JoinColumn(name = "TEAM_ID")
  private Team team;
  
  //연관관계 설정
  public void setTeam(Team team) {
    this.team = team;
  }
  //Getter, Setter ...
}
// 팀 엔티티
@Entity
public class Team {
  @Id
  @Column(name = "TEAM_ID")
  private String id;
  private String name;
  //Getter, Setter ...
}

위 코드에서 아래 부분이 연관관계 매핑 코드이다.

@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;

분석해보자.

  • @ManyToOne : 다대일(N:1) 관계 매핑 정보이다. 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 명시해야 한다.

  • @JoinColumn(name="TEAM_ID") : 조인 컬럼은 외래 키를 매핑할 때 사용한다. (name 속성은 매핑할 외래 키 이름을 지정한다.)

@ManyToOne, @JoinColumn 속성 정리

@ManyToOne

속성기능기본값
optionalfalse로 설정하면 연관된 엔티티가 항상 있어야 한다.true
fetch글로벌 패치 전략을 설정한다.@ManyToOne의 경우 FetchType.EAGER
@OneToMany의 경우 FetchType.LAZY
cascade영속성 전이 기능을 사용한다.
targetEntity연관된 엔티티의 타입 정보를 설정한다.
(이 기능은 거의 사용하지 않는다.)

@JoinColumn

속성기능기본값
name매핑할 외래 키 이름필드면 + _ + 참조하는 테이블의 기본 키 컬럼명
referencedColumnName외래 키가 참조하는 대상 테이블의 컬럼명참조하는 테이블의 기본 키 칼럼명
foreignKey(DDL)외래 키 제약조건을 직접 지정
(이 속성은 테이블을 생성할 때만 사용한다.)
unique
nullable
insertable
updatable
columnDefinition
table
@Column의 속성과 같음

연관관계 사용

연관관계를 이용한 등록, 수정, 삭제, 조회를 사용해보자.

저장

public void testSave() {
  //팀1 저장
  Team team1 = new Team("team1", "팀1");
  em.persist(team1);
  
  //회원1 저장 
  Member member 1 = new Member("member1", "회원1");
  member1.setTeam(team1); //연관관계 생성 member1 -> team1
  em.persist(member1);
  
  //회원2 저장 
  Member member2 = new Member("member2", "회원2");
  member2.setTeam(team1); //연관관계 생성 member2 -> team1
  em.persist(member2);
}

주의!
연관관계의 엔티티는 반드시 영속 상태여야 한다.

위 코드를 실행한 결과를 확인해보면

실행된 SQL은 아래와 같다.

그리고 각 테이블에 값이 잘 입력되었는지 확인한 결과는 아래와 같다.

조회

연관관계가 있는 엔티티를 조회하는 방법은 아래 2가지다.

  • 객체 그래프 탐색 (객체 연관관계를 사용한 조회)
  • 객체지향 쿼리 사용 (JPQL)

우선 객체 그래프 탐색 방법을 사용한 코드와 결과를 알아보자.

조회 코드는

그리고 실행된 SQL과 출력은 아래와 같다.


다음은 객체지향 쿼리 방법인 JPQL을 사용한 방식을 알아보자.

코드부터 확인해보자.

SQL과 출력 결과는 아래와 같다.

수정

JPA를 이용한 수정은 영속 상태의 엔티티 필드를 수정하면 데이터베이스에 자동으로 반영해준다.

예제 코드는 아래와 같다.

SQL과 데이터베이스 반영은 아래와 같다.

연관관계 제거

연관관계 제거 예로 회원이 소속된 팀을 탈퇴해보자.

예제 코드를 보면 조회한 회원의 팀을 null 설정한다.

SQL과 데이터베이스 반영 결과를 보자.

연관된 엔티티 삭제

만약 member1, member2가 가입되어 있는 team1을 삭제하면 어떻게 될까?
> 외래 키 제약조건으로 인해, 데이터베이스에서 오류가 발생한다.

그러면 뭘해야 team1을 삭제할 수 있을까?
> 일단 member1, member2에서 team1 연관관계를 제거한 뒤 team1 엔티티를 삭제하면 된다.

양방향 연관관계

단방향 연관관계는 회원 → 팀 방향만 객체 그래프 탐색이 가능했다.
이번에는 팀 → 회원 으로 접근할 수 있도록 해보자.

회원과 팀의 연관관계를 나타낸 그림은 아래와 같다.

일단 테이블 연관관계는 단방향 연관관계 때와 달라진건 없다.

객체 연관관계에서 Team 객체에 필드로 회원 List가 추가되었다.
이로써 객체도 양방향 참조가 가능하다.

  • 회원 → 팀 (Member.team)
  • 팀 → 회원 (Team.members)

참고
JPA는 List를 포함해서 Collection, Set, Map 같은 다양한 컬렉션을 지원한다.

양방향 연관관계를 적용한 Member, Team 코드는 아래와 같다.

Member 코드는 단방향때와 같고 Team 코드는 @OneToMany(mappedBy = "team")와 List가 추가되었다.

@OneToMany(mappedBy = "team")에서 mappedBy 속성은 양방향 매핑일 때 사용한다. 이때 mappedBy의 값은 반대쪽 매핑 객체의 필드명이다.

양방향 연관관계를 조회해보자.
조회 코드는 아래와 같다.

그리고 실행한 SQL과 출력 결과이다.

연관관계의 주인

@OneToMany에서 mappedBy가 왜 필요할까?

결론부터 말하면 mappedBy는 연관관계의 주인이 아니라는 설정이다.

객체 관점에서 양방향 연관관계는 단방향 2개로 이루어진다. (팀 → 회원, 회원 → 팀)
그리고 테이블에서 양방향 연관관계는 외래 키로 관리된다. (회원 ↔ 팀)
여기서 외래 키는 하나인데 객체 참조는 두 포인트가 발생한다. 따라서 둘 사이에 차이가 발생한다.

그렇다면 외래 키 관리는 Member와 Team 객체 중 어디서 담당할까?
기본적으로 외래 키를 관리하는 테이블을 매핑한 엔티티가 외래 키를 담당하도록 한다. 이 때 외래 키를 담당하는 엔티티를 연관관계의 주인이라 한다.

양방향 매핑의 규칙: 연관관계의 주인

양방향 연관관계 매핑은 반드시 두 연관관계 중 하나를 연관관계 주인으로 정해야 한다.
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 엔티티는 읽기만 할 수 있다.
그리고 어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 된다.

  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.

연관관계의 주인은 외래 키 관리자가 된다.

참고
데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래키를 갖는다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 @ManyToOne에는 mappedBy 속성이 없다.

양방향 연관관계 저장

양방향 연관관계를 적용한 엔티티를 저장해보자.

이 코드는 단방향 연관관계에서 팀과 회원을 저장하는 코드와 완전히 같다.

이 코드에서 가장 중요한 연관관계 엔티티를 설정Member.joinTeam();에서 한다.

양방향 연관관계의 주의점

연관관계를 양방향으로 설정했으니 Team.members().add();에서 Member 엔티티를 추가하는 방법도 있다.
한 번 해보자.

코드는 잘 작성했고, 데이터베이스는 잘 반영하지 않았다.

왜 이런 결과가 나온것일까?

양방향 연관관계는 반드시 연관관계의 주인을 설정하고 연관관계의 주인이 외래 키를 관리한다고 했다. (더 구체적으로 외래 키에 대한 CRUD를 담당한다고 했다.)
Team 객체에서 List<Member>는 mappedBy 속성이 설정되었으니 연관관계의 주인이 아니다.
따라서 Team에서 회원을 추가해도 회원 테이블에 외래 키가 설정되지 않는다.

순수한 객체까지 고려한 양방향 연관관계

연관관계의 주인에만 값을 저장하고 주인이 아닌 곳은 값을 저장할 필요가 없겠다고 생각할 수 있다.
하지만 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다. 그렇지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 문제가 발생할 수 있다.

따라서 양방향 연관관계에서는 외래 키를 설정할 때 양쪽에 값을 설정하도록 하자.

빨간 테두리 부분을 보면 Team 엔티티에 회원을 추가하는 로직을 추가했다.

결과는 아래와 같다.

연관관계 편의 메소드

양방향 연관관계는 결국 양쪽 엔티티에 값을 넣어줘야 한다. 따라서 도메인에서 외래 키를 설정할 때 상대쪽에도 값을 넣을 수 있도록 하나의 기능으로 처리하자. (이를 연관관계 편의 메소드라고 한다.)

연관관계 편의 메소드 작성 시 주의사항

연관관계 편의 메소드를 사용할 때 주의할 점은 연관관계 엔티티를 수정할 때 반드시 기존 엔티티에서 제거하고 새로운 엔티티로 설정해야한다.

만약 아래 코드를 수행하지 않으면 다음과 같은 문제가 발생한다.

if (this.team != null) {
  this.team.getMembers().remove(this);
}

member1은 team1과 연관관계를 맺도록 수정했지만 team1.getMembers()는 여전히 member1을 참조하고 있다.

정리

JPA에서 가장 중요하면서 이해하기 까다로운 몇 개의 개념 중 하나를 또 넘었다.

연관관계에는 2가지 종류가 있다.

  • 단방향 연관관계
  • 양방향 연관관계

결국 두 연관관계는 외래 키를 관리하는 엔티티를 정하는 것이 중요하다.
mappedBy를 통해 연관관계의 주인과 그렇지 않은 엔티티를 정할 수 있다.

애플리케이션 설계는 단방향 매핑으로 마쳐야한다. 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것일 뿐이다.

profile
주 7일, 배움엔 끝이 없다

0개의 댓글