이번 글에서는 <연관관계가 필요한 이유, 단방향 연관관계>에 대해 알아보겠습니다.
이 시리즈 글은 김영한 님의 강의, 을 보고 적은 것임을 알려드립니다. (강추)

1. 연관관계가 필요한 이유


<시나리오>

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

객체 테이블에 맞추어 모델링(참조 대신 외래 키를 그대로 사용)

image.png

아래는 회원과 팀 클래스입니다.

@Entity
public class Member {

  @Id @GeneratedValue
  private Long id;

  @Column(name = "USERNAME") 
  private String name;

  @Column(name = "TEAM_ID")
  private Long teamId;
  ...
  }

@Entity
public class Team {

  @Id @GeneratedValue
  private Long id;
  private String name;
  ...
  }

아래는 팀과 회원을 저장하는 코드입니다.

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

//회원 저장
Member member = new Member();
member.setName("conas");
member.setTeamId(team.getId());
em.persist(member);

이 상황에서 만약 회원의 팀을 찾으려면 어떻게 할까요?

방법은 다음과 같습니다.

Member findMember = em.find(Member.class, member.getId());

Long findTeamId = findMember.getId();
Team findTeam = em.find(Team.class, findTeamId);

연관관계가 없다면 위와 같이, member의 team을 가져오는데 계속 꺼내와야합니다.
member를 꺼내오고, id를 가져와서 다시 팀을 가져오는... 회원의 팀을 가져오기 위해 많은 비용이 듭니다.
그리고 객체 지향스럽지 않은 코드가 됩니다.

객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없습니다.

  • 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾습니다.
  • 객체 참조를 사용해서 연관된 객체를 찾습니다.
  • 테이블과 객체 사이에는 이런 큰 간격이 있습니다.

2. 단방향 연관관계


회원과 팀의 관계를 통해 다대일 단방향 관계를 알아보겠습니다.

image.png

  • 객체 연관 관계
    • 회원 객체(Member)는 Member.team 필드(멤버 변수)로 팀 객체와 연관관계를 맺습니다.
    • 회원 객체와 팀 객체는 단방향 관계입니다. 회원은 Member.team 필드를 통해 팀을 알 수 있지만, 팀 객체로 소속된 회원들을 알 수 없기 때문입니다. member ➡️ team 의 조회는 member.getTeam()으로 가능하지만, team ➡️ member 를 접근하는 필드는 없습니다.
  • 테이블 연관관계

    • 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺습니다.
    • 회원 테이블과 팀 테이블은 양방향 관계입니다. 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고, 반대로 팀과 회원도 조인할 수 있습니다. 예를 들어, MEMBER 테이블의 TEAM_ID 외래 키 하나로 MEMBER JOIN TEAMTEAM JOIN MEMBER 둘 다 가능합니다.
    • 회원과 팀을 조인하는 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개입니다. 반면에 테이블은 외래 키 하나로 양방향으로 조인할 수 있습니다.

  • 객체 연관관계 vs 테이블 연관관계 정리

    • 객체는 참조(주소)로 연관관계를 맺습니다.
    • 테이블은 외래 키로 연관관계를 맺습니다.
    • 참조를 사용하는 객체의 연관관계는 단방향입니다.
    • 외래 키를 사용하는 테이블의 연관관계는 양방향입니다. (A JOIN B가 가능하면 반대로 B JOIN A도 가능)

연관관계 사용

Member, Team class

@Entity
public class Member {

  @Id @GeneratedValue
  private Long id;

  @Column(name = "USERNAME") 
  private String name;

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

  // Getter, Setter ...
}

@Entity
public class Team {

  @Id @GeneratedValue
  private Long id;
  private String name;

  // Getter, Setter ...
}

연관관계 저장

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

//회원 저장
Member member = new Member(); 
member.setName("conas");
member.setTeam(team); //단방향 연관관계 설정, 참조 저장 
em.persist(member);

이전의 코드와 다른점은 member에 teamId를 넣지 않고, team을 넣어준다는 것입니다.

참조로 연관관계 조회 - 객체 그래프 탐색

//조회
Member findMember = em.find(Member.class, member.getId());
//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();

연관관계 수정

// 새로운 팀B
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);

// 회원1에 새로운 팀B 설정
member.setTeam(teamB);

@ManyToOne, @JoinColumn

@ManyToOne: 다대일(N:1) 관계라는 매핑 정보입니다. 위에서 봤던 회원과 팀은 다대일 관계입니다. 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 합니다.

속성 기능 기본값
optional false로 설정하면 연관된 엔티티가 항상 있어야 한다 true
fetch 글로벌 패치 전략을 설정한다. (자세한 내용은 추후에) @ManyToOne=FetchType.EAGER, @OneToMany=FetchType.LAZY
cascade 영속성 전이 기능을 사용한다.(자세한 내용은 추후에)
targetEntity 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않음

@JoinColumn: 외래 키를 매핑할 때 사용합니다.

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

JoinColumn 어노테이션은 생략 가능합니다.
@JoinColumn을 생략하면 외래 키를 찾을 때 기본 전략을 사용합니다.
기본 전략: 필드명 + _ + 참조하는 테이블의 컬럼명