티스토리 뷰

JAVA/JPA

연관관계 매핑 기초

최진영 2021. 8. 12. 14:28

 객체 지향에서 연관관계를 만드는 목표가 뭘까?

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

-(객체지향의 사실과 오해)

 

 일단 먼저 테이블에서 두 테이블의 연관관계를 매핑하기 위해서는 어떻게 매핑할까?

 FK(외래키)를 한 테이블에 두어서 각각의 테이블을 검색할 때 join을 하면 검색할 수 있다. 다음과 같은 테이블이 있을 때 우리는 두 테이블의 연관관계를 검색하고 확인할 수 있다.

테이블에서는 fk 하나만으로도 매핑할 수 있다.

SELECT m.*, t.* FROM MEMBER m
JOIN TEAM t ON m.TEAM_ID = t.TEAM_ID;

SELECT m.*, t.* FROM TEAM t
JOIN MEMBER m on t.TEAM_ID = m.TEAM_ID;

 즉 하나의 테이블에 fk하나만 설정되어 있으면 서로의 테이블을 가지고 연관관계가 매핑이 될 수 있다. 이를 객체로 표현하면 다음과 같을 것이다.

@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;
  
  @Column(name = "NAME")
  private String name;
  
  ...
}

 

 근데 뭔가 마음에 안든다... 우리는 하나의 객체가 다른 객체를 확인할 때 뭐로 매핑할까? 바로 참조값이다. 지금과 같이 id값을 통해서 불완전하게 변환을 할 경우 우리는 다음과 같은 문제에 봉착한다.

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

Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId());
em.persist(member);

 이렇게 영속성 컨텍스트에 저장하고, 바로 member를 찾는다고 해보자.

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

Team team = em.find(Team.class, findMember.getTeamId());

findMember를 찾았지만 team의 정보를 찾기 위해서는 또 find해서 복잡한 과정을 거쳐야한다. 따라서 객체를 테이블의 fk와 같이 테이블 중심으로 모델링을 하게 되면 둘의 연관관계가 손쉽게 연결되지가 않는다.

 

  • 테이블은 FK의 join을 사용해서 연관된 테이블 정보를 찾는다.
  • 객체는 참조를 사용해서 연관된 객체의 정보를 찾는다.

 이 두 패러다임의 차이가 발생하는 것이다.

 

 따라서 JPA에서는 이를 해결하기 위해서 객체끼리 매핑할 수 있게끔 지원을 해준다.

 

단방향 매핑

 결국 객체지향적인 매핑에서의 가장 큰 중요사항은 참조레퍼런스이다.

 테이블처럼 id값을 가지고 연관관계를 짓지 않는다는 것이다. 다음처럼 말이다.


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

 Member라는 객체 안에 team이라는 참조값이 들어가있다. 우리가 흔히 보던 객체의 모습이다. 테이블에서 Member가 fk를 가지고 있는 것처럼 Member객체 내에 Team에 대한 참조값이 들어가 있는 것이다. 이렇게하면 앞서 이야기한 것처럼 Member를 찾아놓고 Team을 또 찾아야하는 불상사가 일어나지 않는다.

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

Team findTeam = findMember.getTeam();

 수정할 때도 마찬가지이다. 찾은 member에서 team id를 꺼내서 team을 또 찾아서 그걸 수정해야하는 이전과는 다르게 찾은 member의 team을 바로 수정하기만하면 된다. 다음과 같이.

Team newTeam = new Team();
newTeam.setName("TEAMB");
em.persist(newTeam);

findMember.setTeam(newTeam);

 

양방향 매핑

 지금은 Member라는 fk를 가진 테이블의 클래스에서 매핑을 하는 것을 알아보았다. 근데 양방향에서 서로의 참조값을 궁금해할 수도 있다. 즉, Team에서 Member를 궁금해할 수 있다는 것이다. 이때 사용하는 것이 양방향 매핑이다.

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

List<Member> members = findTEam.getMembers();

 아까전에는 Member로 Team을 찾았지만 지금은 Team으로 member들을 찾는 과정이다.

 

 단, 위에 코드에 보이는 것처럼 한가지 주의해야할 점이 있다.

mappendBy

 사실 지금까지 객체와 테이블의 패러다임 차이때문에 설명하면서 흘러왔던 내용이지 크게 그 둘이 다르지는 않ㄴ다.

  • 테이블 연관관계 : 1개
    • 회원 <-> 팀 연관관계 1개(fk하나로 양방향 매핑)
  • 객체 연관관계 : 2개
    • 회원 -> 팀 연관관계 1개
    • 팀 -> 회원 연관관계 1개

 테이블의 경우 fk만 걸려있기만 하면 양쪽 어디에서 검색을 하건 서로의 정보를 알 수 있지만 객체의 경우 각 객체의 필드에 참조값이 없으면 검색자체가 불가능하다.

 member가 team의 참조값을 가지고 있더라고 team입장에서는 member에 대한 정보를 알 기 힘들기 때문이다.

 

 따라서 객체의 입장에서는 양방향이란, 그냥 단방향 2개를 만들어서 양방향처럼 보이게한 것 뿐이라는 이야기이다. 그럼 여기서 문제가 된다. "어떤 것"을 기준으로 관리를 하느냐이다.

 

 테이블의 경우 무조건 하나의 fk를 가지고 한번에 join하기 때문에 주인이라는 개념이 없지만 양방향 매핑의 경우 둘 다 외래키에 접근하고자하기 때문에 연관관계의 주인을 지정해주어야한다.

 다음은 양방향 매핑 규칙이다.

  • 객체의 두 관계 중 하나만을 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래키를 관리하여 등록, 수정 가능
  • 주인이 아닌 쪽은 읽기만 가능
  • 주인이 아닌 쪽이 mappedBy 속성으로 주인 지정

 

 보통은 주인을 fk가 있는 곳을 주인으로 지정한다.

 

 양방향 매핑이 안좋은 점은 여러가지가 있기는 하지만 앞서 이야기한대로 주인을 잘 지정해주어야한다. 결국 양방향 매핑이 지양해야하는 이유는 휴먼 에러로 인한 실수인데, 이는 주인만이 수정된다라는 jpa의 특징때문에 이어지는 이슈라고 볼 수 있다.

 아무리 주인이 아닌쪽에 연관관계를 설정하고자 해도 실제로는 읽기기능만 가능하기 때문에 지정이 되지 않는다는 것이다. 다음과 같이 말이다.

// Member : 주인
Team team = new Team();

Member member = new Member();

team.getMembers().add(member);

em.persist(member);

 이렇게 해도 실제 db의 MEMBER fk(TEAM_ID)에는 null값이 들어가 매핑이 되어있지 않다.

 

 따라서 양방향 연관관계는 실수가 잦기 때문에 사용을 지양해야하며 사용을 해야할 경우에는 양쪽 값에 설정하는 것을 잊지 말아야 한다.

 

 굳이 사실 단방향 매핑만으로도 이미 연관관계의 매핑은 완료되었기 때문에 굳이 양방향 매핑이 필요하지는 않다. 즉, 양방향매핑은 사실상 반대방향에서의 조회 기능이 추가된 것일뿐이므로 단방향 매핑을 잘 하고 양방향은 필요할 때만 추가하도록 하자.

'JAVA > JPA' 카테고리의 다른 글

JPA 모든 N+1 발생 케이스과 해결책  (0) 2021.11.23
엔티티 매핑  (0) 2021.08.10
영속성 관리 - 내부 동작 방식  (0) 2021.08.09
JPA 소개 및 특징  (0) 2021.08.09
댓글
최근에 올라온 글
Total
Today
Yesterday