[JPA기본] 5. 연관관계 매핑 기초

kiwonkim·2021년 11월 28일
0

[ 이전 포스팅 ]

@Entity 로 클래스와 테이블의 매핑을 진행하였다. 유일하며 불변해야하는 기본키는 @GeneratedValue 로 자동 생성 방식을 사용하였다. 전략으로는 IDENTITY 나 SEQUECE 를 사용 주로 사용한다. 그러면 1:N 과 같은 연관 관계는 클래스에 어떻게 적용할까?

테이블의 연관관계를 클래스에 적용하는 방식에 대해 알아보자.


[ 테이블 방식 모델링 ]

상황가정

회원과 팀이 있다고 가정하자. 회원은 하나의 팀에만 소속될 수 있다. 즉 회원과 팀은 1:N 관계이다. 1:N 관계에서는 N 에서 FK 를 갖는다.

테이블 방식 객체 모델링

테이블 에서는 FK 로 상대 테이블의 PK 를 갖는다. 이 방식 그대로 객체를 모델링 해보자.
Member 는 FK인 teamId 를 필드로 갖게 설계될 것이다.

테이블 방식 모델링 한계

1번 Member 의 team을 조회하고 싶다고 가정해보자.

Member1 = em.find(Member.class, 1L); // 1번 멤버 조회
Member1_team = em.find(Team.class, Member1.getTeamId); // 1번의 teamId 로 team 조회

Member 객체는 Team 객체가 아닌 FK인 teamId를 필드로 갖는다. 따라서 Member1 을 찾고, 그 teamId로 다시 조회를 team 을 찾아야한다. 쿼리가 두번 필요한 것이다.

그냥 Member 객체가 속한 Team 객체를 필드로 들고 있으면 안될까? 그게 더 객체지향적 설계이다


[ 객체 지향적 모델링 ]

테이블과 클래스의 연관관계 차이

연관관계를 표현하면 위와 같다. 이때 객체와 테이블의 차이가 존재한다.

  1. 테이블은 FK를 속성으로 갖는다. 하지만 객체는 객체 자체를 필드로 갖는다.
  2. FK를 가지면 Join을 통해 양방향의 데이터를 접근할 수 있다. 하지만 객체는 다른 객체를 보유한 쪽에서만 접근이 가능하다.(단방향)

즉, Member가 Team 객체를 필드로 갖으면, 객체에서 Member 가 속한 팀은 확인할 수 있지만, Team에 속한 Member들은 확인할 수 없다. 이를 위해서는 Team 객체도 Member 객체들을 가져야한다.

객체지향 모델링

어플리케이션에서는 객체가 객체를 필드로 갖고 있는 것이 자연스러우며, 그것이 객체지향적 설계이다. 우리는 연관관계시 원래대로 객체를 갖도록 설계하자. 그 후 JPA 의 도움으로 어노테이션을 통해 FK 를 지정하면 객체지향 프로그래밍과 DB 설계를 병행할 수 있다.


[ 단방향 매핑 ]

단방향 매핑이란

단방향 매핑이란 연관관계를 갖는 두 객체 중 한쪽만 다른 객체를 필드로 갖고 있는 상황이다. 즉 Member 가 Team 객체를 필드로 갖고, Team 객체는 Member 객체를 필드로 갖지 않는 상태를 단방향 매핑이라고 한다.

단방향 매핑만으로도 연관 관계 매핑과 FK 설정은 완료된다. 하지만 Team 의 필드에 Member 가 없기에 Team 에 속한 Member 들을 조회할 수는 없다.

단방향 매핑 적용법

Member 의 Team 객체에 @ManyToOne 같은 자신 입장에서 연관관계를 어노테이션으로 추가하고, @JoinColumn 에서 매핑할 외래키 이름을 지정해주면 된다. 테이블 자동생성 기능을 사용할 경우, @JoinColumn 에서 지정한 이름으로 외래키가 테이블에 생성된다.


[ 양방향 매핑 ]

양방향 매핑이란

양방향 매핑이란 연관관계를 갖는 양쪽 객체 모두가 서로를 필드로 갖는 상황이다. 단방향 매핑에서는 Team 이 Member 를 필드로 갖지 않는다. 그래서 Team 객체를 통해 해당 Team의 Member들을 조회할 수 없다고 하였다. 그런데 Team의 Member 를 조회할 일이 필요할 경우 Team 의 필드로 Member 객체를 두면 된다.

양방향 매핑 적용법

단방향 매핑으로 이미 FK 생성이 완료되었다. 양방향 매핑쪽에서는 @OneToMany 등 관계를 입력해주고, mappedBy 속성으로 단방향 매핑의 필드명을 입력하면 된다.


[ 연관관계의 주인 ]

FK 변경필요시 딜레마

양방향 매핑을 통해 Member 도 Team 객체를 갖고, Team 도 MemberList 객체를갖는다.

그런데 Member 의 Team 이 변경되는 상황. 즉 Fk가 변경해야하면 어떻게할까?
Member 내의 Team 객체를 바꿔야할까? 아니면 Team 의 MemberList 객체를 바꿔야할까?

-> 테이블은 FK 존재해도 양방향 접근이 가능하지만, 객체에서는 양방향 접근을 위해 양쪽에 필드로 추가하여서, 이런 딜레마가 생긴 것이다. 즉 외래키와 관련된 양쪽 필드 중 외래키를 관리하는 필드는 누구인가를 결정해야한다. 이 외래키를 결정하는 쪽의 객체가 "연관 관계의 주인" 이다.

연관관계의 주인

양방향 매핑에서 두 객체 중 한쪽의 필드는 FK 를 결정하고, 반대쪽은 FK 결정을 못하며 조회만 가능하도록 해야한다. Team 객체의 Member List 필드는 FK 설정이 완료된 상태에서 조회만을 위해 추가되었다고 하였다. 따라서 여기서는 Member 가 연관관계의 주인이 된다. Member 의 Team 객체를 변경하면 테이블의 FK가 변하지만, Team의 MemberList는 아무리 변경해도 FK에 영향을 주지 않는다. 다만 읽기에 사용할 뿐이다.

누구를 주인으로?

양방향 매핑에서는 외래키가 있는 곳을 주인으로 하자. 예를 들어 Team 을 연관관계의 주인으로 하면, Team 의 memberList 를 변경시키면 실제 쿼리는 Member 의 FK를 변경하도록 쿼리가 나가기 때문에 덜 직관적이다.

FK 변경시 주의점

양방향 매핑에서 FK 를 변경하려면 연관관계 주인의 필드만 변경하면 된다. 즉 Member 객체의 Team 만 바꾸면 된다. 다만 다음과 같은 경우를 보자.

//teamA 생성
Team team = new Team()
team.setName("teamA");
em.persist(team);

//memberA 생성. teamA에 들어가도록함.
Member memberA = Member();
member.setName("kim");
member.setTeam(team);
em.persist(memberA);

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

//teamA 조회후 members 보면 null;
Team teamA = em.find(Team.class, 1L);
List<Member> members = teamA.getMembers(); // member1 기대하나 null.

teamA 를 생성하고, memberA 의 필드에 teamA 를 넣어 FK 를 저장했다. 이제 flush 가 일어나면 DB의 FK에 기록될 것이다.

그 밑에 teamA 를 조회해서 members 를 찍어보았다. 그런데 이 때 영속성 컨텍스트의 1차 캐시에서 teamA 를 가져오기 때문에 members 가 null 인 상태이다. 원하는 결과와 다른 결과가 도출된다.

void setTeam(Team team) {
	team.members.add(this); // 주인 아닌놈에도 추가
    this.team = team;
}

따라서 양방향 매핑에서 연관관계 주인의 값에 FK 를 set 할때. 주인이 아닌 쪽에도 추가하도록 편의 메서드를 짜서 사용하는 것이 좋다. 실무에서는 setTeam 이 아닌 별도의 이름을 사용하자.


[ 결론 ]

1:N 과 같은 연관관계를 클래스에서는 필드에 다른 객체를 갖도록 하여 구현한다. @JoinColumn 을 통해 FK 의 이름을 정할 수 있다.
한쪽 클래스에서만 객체를 갖는 상태를 단방향매핑, 양쪽 모두 서로의 객체를 갖는 상태를 양방향매핑이라 한다.
양방향 매핑에서 FK 값을 결정하는 클래스가 연관관계의 주인이다. 주인은 FK를 갖는 쪽으로 설정하자. 주인이 아닌쪽은 mappedBy 로 필드명을 입력한다.
연관관계주인의 객체의 필드가 변경되면 FK 값이 변경된다. 다만 영속성 컨텍스트에서 꺼내 사용할 때를 대비해 편의메서드로 양쪽 다 바꿔주자.

0개의 댓글