테이블 설계와 연관관계 매핑(단방향, 양방향)

so.oy·2024년 6월 20일
1

JPA

목록 보기
3/3

본 내용은 인프런의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 수강하고 정리한 글입니다.

JPA에서 가장 중요한 것 중 하나는 연관관계 매핑이라고 할 수 있습니다. 이는 "객체와 테이블을 매핑하여 패러다임 불일치 문제를 해결"하고자 만들어진 JPA의 사용 목적과도 일치합니다.


객체와 테이블의 연관관계 차이

테이블 - 외래키를 통한 JOIN으로 관계 설정
객체 - 참조를 통한 관계 설정

우선 객체와 데이터베이스 테이블간의 차이를 이해하고 이 둘 사이의 매핑이 어떻게 이루어지는지 이해하는 것이 중요합니다.
테이블의 경우, 외래키를 통해서 테이블 간 연관관계를 설정합니다. 그에 반해 객체는 참조를 통해서 관계를 맺습니다.

아래의 예제를 통해 객체의 참조와 테이블의 외래키를 어떻게 매핑하는 것이 올바른지 알아보겠습니다.

객체를 테이블에 맞추어 모델링


객체를 테이블에 맞추어 모델링 할 경우엔 MEMBER 테이블에 TEAM_ID의 외래키를 갖게 되고, 참조가 아닌 외래키를 그대로 사용하게 됩니다.

MEMBER

@Entity
public class Member {
	@Id @GenerateValue
    private Long id;
    
	private int age;
  
    @Column(name = "USERNAME")
    private String name;

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

TEAM

@Entity
public class Team {
	@Id @GenerateValue
    private Long id;
  
    private String name;
    ...
}

만약 찾은 멤버가 어느 팀 소속인지 알고 싶다면 어떻게 코드를 작성해야 할까요?

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

Long findTeamId = findMember.getTeamId();

Team findTeam = em.find(Team.class, findTeamId);
  1. 멤버를 찾고
  2. 멤버에서 팀 아이디를 찾아오고
  3. 찾아온 팀 아이디로 팀을 찾아야 합니다.

이처럼 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력관계를 만들 수 없습니다.
테이블은 단지 외래키 team_id를 가지고 있을뿐 객체끼리 어떠한 관계설정이 되어있지 않기 때문에, 바로 TEAM 객체를 찾는 것이 불가능합니다. 이러한 설계는 어떤 것을 조회할때 항상 데이터베이스에 접근해야하기때문에 비효율적이며, 객체지향적이지 않다고 할 수 있습니다.

그렇다면 어떻게 작성하는 것이 옳은 방법일까요?

용어 이해

이를 위해서 이해해야하는 기본 용어는 크게 3가지가 있습니다.
그 중 오늘은 단방향 연관관계와 양방향 연관관계에 대해서 알아보도록 하겠습니다.

  • 방향(Direction) : 단방향, 양방향
  • 다중성 (Muliplicity) : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)
  • 연관관계의 주인 (Owner) : 객체지향 양방향 연관관계는 관리 주인이 필요

1. 단방향 연관관계

데이터베이스 테이블의 경우 외래키 하나로 양쪽 테이블의 조인이 가능합니다. 그렇기 때문에 데이터베이스 테이블에는 방향이라는 개념이 존재하지 않습니다.
그러나 객체는 참조하는 객체를 가진 쪽에서만 다른 객체를 참조하는 것이 가능합니다. 이처럼 한 방향으로 객체를 참조하는 것을 단방향 관계라고 합니다.

표에서 보듯이, MEMBER객체는 외래키가 아닌 TEAM 참조값을 그대로 가지고 있는 것을 볼 수 있습니다.
여기서 MEMBERTEAM을 참조하고 있기 때문에 접근이 가능하지만, TEAM에서 MEMBER는 확인이 불가능한 단방향 연관관계입니다. 즉, member.getTeam()은 가능하지만, team.getMember()는 불가능합니다.
이것을 코드로 표현하면 어떻게 하면 될까요?

// Member
@Entity
public class Member {

	@Id @GenerateValue
    private Long id;
  
    @Column(name = "USERNAME")
    private String name;
    
    private int age;

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

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


// TEAM
@Entity
public class Team {

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

하나의 팀에 여러명의 member가 소속되니까, MEMBER 쪽에 @ManyToOne으로 다대일 연관관계를 설정해줍니다. 해당 어노테이션을 통해서 어느쪽이 N인지 JPA에게 관리하도록 해줍니다.
@JoinColumn(name = "TEAM_ID")으로 외래키 team_id와 매핑해줍니다. 외래키 TEMA_ID를 직접 사용했던 테이블 중심 설계와 다르게 @JoinColumn 어노테이션을 사용해 매핑해 줍니다.

연관관계 저장

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

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

이처럼 member에 team_id가 아닌 team 객체 자체를 넣어줍니다.

연관관계 조회

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

// 참조를 통해서 연관관계 조회
Team findTeam = findMember.getTeam();

데이터 중심 설계의 조회 방식과 다르게, findMember.getTeam()을 통해 객체 그래프 탐색이 가능하게 됩니다.

수정

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

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

2. 양방향 연관관계

양방향 관계란 양쪽에서 참조하는 객체를 가져, 서로 참조가 가능한 것을 의미합니다.
엄밀히 말하면 양방향 관계라는 것은, 각각 객체를 참조하고 있는 두 개의 단방향 관계를 의미합니다.

Member 객체에는 Team 참조값이 있고, Team 객체에는 member를 List로 양방향에서 참조값을 갖습니다.

// Member
@Entity
public class Member {

	@Id @GenerateValue
    private Long id;
  
    @Column(name = "USERNAME")
    private String name;
    
    private int age;

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

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

// Team
@Entity
public class Team {

	@Id @GenerateValue
    private Long id;
  
    private String name;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    ...
}

MEMBER와 TEAM은 (N:1)의 연관관계를 갖기 때문에, Team 쪽에는 @OneToMany 어노테이션을 통해 매핑해주면 됩니다.
mappedBy는 현재 1 : N 매핑에서 무엇과 연결되어 있는지 나타내주는 것으로 반대쪽 매핑의 필드명을 적어주면 됩니다. Member의 team과 매핑되어있음을 나타내줍니다.

// 조회
Member findMember = em.find(Member.class, member.getId);
List<Member> members = findMember.getTeam().getMembers();

for (Member member : members) {
System.out.println("m = " + member.getUsername());
}

// 결과
// m = member1
// m = member2

이렇게 양방향 모두로 객체 그래프 탐색이 가능하게 되었습니다.

연관관계의 주인, mappedBy 설정

사실, 두 객체가 서로 양방향 연관관계를 맺는다는 것은 두 개의 단방향 연관관계가 있다는 것을 의미합니다.
위의 코드에서도 보았다시피, 회원에서 팀으로 참조값 하나, 팀에서 회원으로 참조값이 존재합니다. 서로 다른 단방향 연관관계 2개를 양방향인 것처럼 보이게 할 뿐입니다. 반면에 데이터베이스 테이블은 외래 키 하나로 양쪽에서 조인이 가능합니다.

이렇게 데이터베이스 테이블과 객체가 관계를 맺는 차이를 해결하기 위해서 JPA에서는 연관관계의 주인을 지정해주어야합니다.

mappedBy는 양방향 연관관계의 주인을 지정합니다. 객체의 두 관계중 하나를 연관관계의 주인으로 지정합니다. 연관관계의 주인만이 외래 키를 관리(등록, 수정) 할 수 있으며, 주인이 아닌 쪽은 읽기만 가능합니다.

mappedBy 속성은 연관관계의 주인이 아닌 객체에 사용합니다. mappedBy라는 이름 그대로, 어느쪽에 매핑되었는지를 표현해줍니다.

그렇다면 연관관계의 주인을 어떻게 지정해야 할까요?

연관관계의 주인은 외래 키를 가지고 있는 쪽으로 하면 됩니다.

Fk를 가지고 있는 쪽이 연관관계의 주인이며, '진짜 매핑'이라고 합니다. 주인의 반대편은 '가짜매핑'이 되는 것이죠.
데이터베이스 테이블의 관계로 생각했을때는 외래키가 있는쪽이 N, 없는 쪽은 1이기 때문에, N쪽을 연관관계의 주인으로 생각하면 됩니다.

양방향 연관관계의 주의점

만약 연관관계의 주인이 아닌쪽에 값을 넣으면 어떤 문제가 발생할까요?

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

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

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

em.persist(member);

member1을 저장하고 TEAM의 컬렉션에 담은 후 저장했습니다. 여기에서 member1은 제대로 저장이 될까요?
위 코드를 실행하고 데이터베이스에서 회원 테이블을 조회하면 다음과 같은 결과가 나옵니다.

MEMBER_IDUSERNAMETEAM_ID
member1회원1null

외래 키 TEAM_ID에 null 값이 들어가있는 것을 볼 수 있습니다.
양방향 연관관계를 설정할때, 연관관계의 주인만이 외래 키를 관리(등록, 수정) 할 수 있으며, 주인이 아닌 쪽은 읽기만 가능하다고 했습니다.
해당 코드에서는 연관관계의 주인이 아닌 역방향에만 값을 넣었기 때문에 위와 같은 문제가 발생한 것입니다. 가짜매핑에 값을 넣으면 JPA는 읽기만 가능합니다.

반대로 연관관계의 주인에 값을 넣어보면 아래와 같이 MEMBER의 TEAM_ID가 올바르게 들어간 것을 볼 수 있습니다.

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

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

em.persist(member);
MEMBER_IDUSERNAMETEAM_ID
member1회원11

순수한 객체 관계를 고려한 연관관계 매핑

그렇다면 정말 연관관계의 주인에만 값을 넣어주면 되는 걸까요?

가장 권장되는 방법은 양쪽 모두에 값을 입력하는 것입니다.

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

Member member = new Member();
member.setName("member1");
member.setTeam(team);		// 양쪽에 값 세팅
em.persist(member);

team.getMembers().add(member);	// 양쪽에 값 세팅

JPA에서 양방향 매핑 시 양쪽 모두에 연관관계를 설정하는 이유는 JPA가 JAVA 객체 관점과 데이터베이스 테이블 관점에서 관계를 관리하기 때문입니다.

연관관계의 주인은 외래키를 관리하며, 연관관계의 주인이 아닌쪽은 읽기만 가능합니다. 이로 인해 연관관계의 주인쪽에서만 setTeam()과 같은 메서드를 사용하여 연관관계를 설정하면 JPA가 이를 테이블에 반영할 수 있으나, JAVA 객체 상에서는 양쪽 모두에 반영할 필요가 있습니다.
그렇기 때문에 테스트 케이스 작성 시와 같은 JPA를 사용하지 않을때 문제가 발생합니다. 이때는 JPA 없이 순수 자바 코드를 통해 테스트를 하기 때문에 양쪽 모두에 값을 세팅해주지 않으면 양방향 연관관계를 올바르게 사용할 수 없습니다.

따라서, JAVA 객체 상의 일관성을 유지하고 순수한 객체 관계를 고려하여 양쪽 모두에 설정하는 것이 좋습니다.

연관관계 편의 메서드

연관관계 편의 메서드는 양쪽 모두에 연관관계를 세팅해야하는 불편함을 해소하고자 사용하는 방법입니다.

위의 코드와 같이 member.setTeam(team);, team.getMembers().add(member); 처럼 각각 코드를 작성하다보면, 한쪽에만 작성한다던지하는 실수가 발생할 수 있습니다.

public class Member {
  
  private Team team;
  
  public void setTeam(Team team) {
    this.team = team;
    team.getMembers().add(this);	// 연관관계 편의
  }
  
  ...
}

이렇게 setTeam() 메서드를 연관관계 편의 메서드로 변경해 양방향 관계를 설정할 수 있습니다.

0개의 댓글

관련 채용 정보