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

이재표·2023년 9월 22일
0

이전까지 패러다임 불일치에 대해 알아보았습니다.
이번 글에서는 연관관계를 좀 더 객체지향스럽게 설계하는 것에 대해 알아보겠습니다.

목표

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

    용어

    • 방향 : 단방향, 양방향
    • 다중성 : 다대일, 일대다, 일대일, 다대다
    • 연관관계의 주인 : 객체 양방향 연관관계는 관리주인이 필요

연관관계가 필요한 이유

!! 객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다 !!

개발의 객체와 관계가 적지않기에 연관관계가 꼭 필요하다

외래키는 N쪽에 있게됨. 하나의 팀에 여러 멤버가 소속될수 있고, 여러 멤버는 하나의 팀에 소속될수 있다는 뜻

연관관계가 없이 객체를 생성하게 된다면

@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("TeamA");
em.persist(team);
// em.persist() 실행시 pk값이 설정되고 영속성 컨텍스트에 넣어진다.
Member member = new Member();
member.setName("member1");
           member.setTeamId(team.getId());
em.persist(member);

tx.commit();

연관관계가 없다면 테이블을 join하여 연관된 객체를 불러오고 싶을때
select * from member m join team t on m.team_id=t.team_id와 같이 fk값을 통해 join하여 불러온다

하지만 fk를 직접 관리한다는것이 애매하다. 조회할때 em.find()를 통해 객체를 가져오게 되는데, 참조가 아니기 때문에 찾은 객체가 어느팀인지 바로 가져오지 못한다.

Member findMember=em.find(Member.class,member.getId());
Long teamId=findMember.getId();
Team findTeam = em.find(Team.class,teamId);

조회하길 원한다면 이런식으로 em.find()를 한번 더 사용하여 객체를 조회해야한다.

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

객체중심 모델링

객체를 객체중심으로 모델링할때의 장점을 알아보자!

단방향 연관관계

객체지향 모델링(객체 연관관계 사용)

 @Entity
 public class Member {
   @Id @GeneratedValue
   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;
   … 

멤버의 입장에서는 멤버(다)와 팀(일)의 관계이므로 다대일 관계로 @ManyToOne 어노테이션을 사용한다. 또한 join할때 fk값을 기준으로 join하여 참조된 객체를 가져와야하니 @JoinColumn을 붙혀준다.

즉 다음과 같아진다

연관관계 저장

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

쿼리를 보면 select가 안나가는데 그 이유는 persist하여 저장되면 1차캐쉬에 저장되기 때문이다.

연관관계 조회

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

양방향 연관관계와 연관관계의 주인

양방향 매핑

이전에는 member가 team을 꺼낼수 있었지만 team의 경우 member를 꺼낼수 없었다. 왜냐면 값이 없으니까!! 근데 team에서도 충분히 member를 꺼내고 싶을수 있고, 이때 사용하는 것이 양방향매핑이다.

테이블의 경우 방향이라는 개념이 없다. 그냥 kpk와 fk를 join하면 됨. 객체의 경우 member에서 team을 꺼내고 team에서 member를 꺼내는 방향이 존재

 @Entity
 public class Member {
 @Id @GeneratedValue
 private Long id;
 @Column(name = "USERNAME")
 private String name;
 private int age;
 @ManyToOne
 @JoinColumn(name = "TEAM_ID")
 private Team team;
 … 
 }

@Entity
 public class Team {
 @Id @GeneratedValue
 private Long id;
 private String name;
 @OneToMany(mappedBy = "team")
 List<Member> members = new ArrayList<Member>();
 …
 }

Team의 경우 한개의 Team에서 여러명의 Member에게 매핑하는것이므로 @OneToMany 어노테이션을 이용한다. 또한 자신이 어디에 매핑되는것인지 나타내기위해 mappedBy옵션에 '다'쪽에 선언되어있는 변수명을 적어준다.

양방향이 좋아보일수 있지만, 가능하면 단방향이 좋다. 왜냐면 양방향이 많아질수록 생각해야하는것이 많아지기 때문이다.

mappedBy가 그래서 뭔데?

객체와 테이블이 관계를 맺는 차이를 잘 확인하면 알수있다.


사실 객체의 양방향은 단방향 연관관계의 관계는 단방향이 두개있는것이다. 하지만 테이블 양방향 연관관계의 관계는 1개이다.
따라서 객체는 참조가 양쪽 객체에 다 있어야한다.

딜레마

Member를 바꾸거나 Team을 바꾸고 싶을때 Member객체의 team을 바꿔야할지, Team의 members를 바꿔야할지 헷갈리게됨. 근데 어쨋든 DB입장에서는 Member의 외래키값만 바뀌면 된다.

따라서 둘중 하나로 외래키를 관리해야한다.

이것이 바로 연관관계의 주인(Owner)이다.
양방향 매핑 규칙

  • 객체의 두 관계중 하나를 연과과계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리(등록,수정)
  • 주인이 아닌쪽은 읽기만 가능(중요)
  • 주인은 mappedBy속성 사용X
  • 주인이 아니면 mappedBy 속성으로 주인지정

그럼 누가 주인?

외래키가 있는곳을 주인으로 정해라!
예제에서는 Member.team이 연관관계의 주인!

물론 외래키가 있는곳을 주인으로 정하지 않아도되지만, 부수적인 문제점들이 생기게됨. 실수유발, 어플리케이션과 db가 서로 다른 테이블에 쿼리를 날림, 성능 등등

쉽게 말해서 N(다)쪽이 연관관계의 주인이 된다.
주인만이 update, insert가 가능. 주인이 아닌쪽은 조회만 가능

양방향 매핑시 가장 많이 하는 실수

연관관계의 주인에 값을 입력하지 않는 실수!!

Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);
em.persist(member);

그럼어떻게하지?

순수한 객체지향을 생각하면 그냥 양방향이면 양쪽에 다 넣어줘라!
find이전에em.flush(); em.clear();를 하지 않는다면 커밋이후에 db에 들어가니 1차캐시에서 객체를 가져오게되는데 그러면 주인이 아닌 객체에는 값이 설정되지 않은상태이니 원치않은결과가 나올수 있다. 따라서 양방향이면 양쪽에 다 값을 세팅하는게 편리하다.

연관관계 편의메서드

양방향이기에 두개의 객체를 각각 세팅해주면 까먹을수도 있으니 한번에 처리해주는 연관관계 편의메서드를 만들자!

public void setTeam(Team team) {
	this.team = team;
	team.getMembers().add(this);
}

이렇게하면 team쪽에도 member에 값이 들어가게되니 주인객체의 team을 설정할때 한번에 주인이 아닌 쪽 또한 설정된다.(member가 아닌 team에서 메서드를 만들어도 된다. 결국 주인쪽에 값이 들어가야한다는게 중요)

단순히 getter,setter로 사용하는것은 지양

양방향 연관관계 주의

  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
  • 양방향 매핑시에 무한 루프를 조심하자!!
    양방향일때 Member는 Team을 보여주고, 다시 Team이 같은 Member를 보여주고, Member가 다시 같은 Team을 반환하는 과정이 무한 반복된다.
    DTO로 변환해서 사용하는것을 추천한다.

양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료된것이다. 단방향만으로 설계를 끝내고 너무 많이 조회(객체 그래프 탐색)가 필요할때 추가하여 양방향매핑을 걸어줘도 됨(양방향이 된다고 테이블에 영향을 주지 않기때문에!).
  • JPQL에서 역방향으로 탐색할 일이 많음

연관관계의 주인을 정하는 기준

  • 비즈니스 로직이 기준이 아닌 외래키의 위치를 기준으로 정해야한다.

0개의 댓글