연관관계 매핑

Gyeongjae Ham·2023년 5월 24일
0

JPA

목록 보기
5/12
post-thumbnail

해당 시리즈는 김영한님의 JPA 로드맵을 따라 학습하면서 내용을 정리하는 글입니다

테이블을 중심으로 엔티티를 만들 경우

@Entity
public class Member {
	@Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    
    ...
}

@Entity
public class Order {
	@Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;
    
    @Column(name = "member_id")
    private Long memberId;
    
    ...
}
  • 위와 같이 설계가 된다면 객체 지향적인 개발과 다르게 호출되는 코드를 작성하게 됩니다
// 찾은 주문 객체에서
Order order = em.find(Order.class, 1L);

// 주문한 멤버 객체의 아이디 값을 받아와서
Long memberId = order.getMemberId();

// 그 아이디 값으로 멤버를 찾아내야 한다
Member findMember = em.find(Member.class, memberId);
  • 하지만 위 엔티티들을 아래와 같이 수정하면 바로 Member 객체를 불러올 수 있습니다
@Entity
public class Member {
	@Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    
    ...
}

@Entity
public class Order {
	@Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;
    
    private Member member;
    
    ...
}
// 찾은 주문 객체에서
Order order = em.find(Order.class, 1L);

// 바로 그 주문을 한 멤버를 불러올 수 있습니다
Member findMember = order.getMember();

테이블 설계의 문제점

  • 현재 방식은 객체 설계를 테이블 설계에 맞춘 방식입니다
  • 테이블의 외래키(FK)를 객체에 그대로 가져와서 옮겨놓았습니다
  • 객체 그래프 탐색이 불가능하게 됩니다
  • 참조가 없으므로 UML도 잘못됩니다

    UML 이란?
    Unified Modeling Language의 약자입니다
    UML은 개발자들이 실제 개발단계에 들어가기 전에 다이어그램을 통해서 프로그램의 전체적인 설계, 필요한 변수, 함수 등을 정하는 등 전반적으로 직접 코딩하기 전 계획을 시각화한 것입니다

연관관계가 필요한 이유

  • 객체를 테이블에 맞춰서 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없습니다
    • 테이블은 외래키로 조인을 사용해서 연관된 테이블을 찾습니다
    • 객체는 참조를 사용해서 연관된 객체를 찾습니다

단방향 연관관계 매핑

@Entity
public class Member {
	...
    @ManyToOne // 이 관계를 표현하는 애노테이션
    @JoinColumn(name = "TEAM_ID") // 이 객체가 데이터베이스의 어떤 컬럼과 연관관계를 맺는지
    private Team team;
    ...
}

양방향 매핑(반대 방향으로 객체 그래프 탐색)

  • 테이블에서는 외래키만 지정되면 관계가 맺어진 테이블들의 내용을 볼 수 있지만 객체는 그렇지 않습니다
  • 위에서 Member 클래스에만 Team에 대한 관계를 설정해 줬기 때문에 Member에서만 Team의 정보를 알 수 있다는 차이점이 발생합니다
  • 따라서 객체들의 연관관계를 맺는 과정에서는 테이블과 다르게 한가지 과정이 추가되어야 합니다. 바로 1:N의 관계에서 1에 해당하는 객체에도 관계에 대해서 알려줘야 한다는 점입니다
...
// Team 입장에서의 관계를 표현하는 애노테이션과
// 어떤 필드와 관계를 맺고 있는지 관계 대상이 되는 필드의 이름을 적어주어야 합니다
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>(); 
// add 할 때 NPE를 방지하기 위해서 관례적으로 ArrayList로 많이 초기화 합니다
...

연관관계의 주인과 mappedBy

  • 객체와 테이블이 관계를 맺는 차이
    • 객체 연관관계 = 2개(단방향 연관관계 두 개)
      • 회원 -> 팀 연관관계 1개(단방향)
      • 팀 -> 회원 연관관계 1개(단방향)
    • 테이블 연관관계 = 1개
      • 회원 <-> 팀의 연관관계 1개(양방향)
  • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개입니다
  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 합니다
  • 테이블은 외래키 하나로 두 테이블의 연관관계를 관리합니다
  • MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계를 가지게 됩니다(양쪽으로 조인할 수 있습니다)

  • 따라서 객체에서 맺은 두 단방향 연결(team, members) 중 하나를 데이터베이스의 외래키와 연관관계를 매핑하는 데 있어서 어떤 걸 연결해야 하는지 고민을 하게 됩니다
  • 따라서 규칙으로 둘 중 하나로 연관관계를 정하게 됩니다 그게 바로 연관관계의 주인이 됩니다

연관관계의 주인(Owner)

양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정해야 합니다
  • 연관관계의 주인만이 외래키를 관리합니다(등록, 수정)
  • 주인이 아닌 다른 한쪽은 읽기만 가능합니다
  • 주인은 mappedBy 속성을 사용하지 않습니다
  • 주인이 아니면 mappedBy 속성으로 주인을 지정합니다

어떤 쪽을 주인으로 해야 할까요?

  • 외래키가 있는 곳을 주인으로 정합니다
  • 우리가 살펴보는 예제에서는 Member.team이 연관관계의 주인입니다

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

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

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

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

// 역방향(주인이 아닌 쪽)만 연관관계 설정
team.getMembers().add(member);
em.persist(member);

  • 주인이 아닌 쪽은 읽기만 가능하므로 저렇게 값을 넣어도 외래키에 값이 들어가지 않습니다

양방향 매핑 시 연관관계 주인에 값을 입력해야 합니다

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

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

// 연관관계 주인에 값 설정
member.setTeam(team);
em.persist(member);

  • 위와 같이 주인 쪽에서 값을 입력해주면 관계가 잘 반영됩니다

하지만 순수한 객체 관계를 고려해서 항상 양쪽에 다 값을 입력하도록 합니다

  • 데이터베이스에 문제 없이 잘 들어갔는데 왜 읽기 전용인 역방향 쪽에서도 값을 넣어야만 할까요? 첫 번째로는 객체 지향스럽지 않다는 문제가 있고, 두 번째로 아래 예제와 같은 문제가 발생합니다
Team team = new Team();
team.setName("TeaA");
em.persist(team);

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

// 연관관계 주인에 값 설정
member.setTeam(team);

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

Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();

for (Member m : members) {
	System.out.println("m = " + m.getUsername());
}
  • 위 코드는 우리가 원하는 대로 Member의 리스트를 반환해주며 아주 잘 작동합니다
  • 하지만 중간의 em.flush(), em.clear() 부분이 없다면 어떻게 될까요?
    • 그렇다면 findTeam은 데이터베이스가 아닌 em.persist(team)일 때 1차 캐시에 저장된 그 Team 객체가 나오게 됩니다
    • 하지만 우리는 잘 알다시피 그 Team 객체안에는 Member의 리스트가 없는 상태인 빈 껍데기입니다
Team team = new Team();
team.setName("TeaA");
em.persist(team);

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

// 연관관계 주인에 값 설정
member.setTeam(team);
team.getMembers().add(member);

Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();

for (Member m : members) {
	System.out.println("m = " + m.getUsername());
}
  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하도록 합니다
  • 사람은 언제나 실수할 수 있기 때문에 메소드에서 연관관계를 맺도록 설정해두면 편합니다
// 예를 들면 Member 클래스에서 setTeam이라는 setter 메서드 안에 설정을 합니다

public void setTeam(Team team) {
	this.team = team;
    // 여기에 설정해두면 주인 쪽에서 값을 넣을 때 자동으로 맺어지니 실수할 일이 줄어들겠죠?
    team.getMembers().add(this);
}
// 아니면 Team 클래스에서 설정하도록 하실 수도 있습니다
public void addMember(Member member) {
	member.setTeam(this);
    members.add(member);
}
  • 양방향 매핑 시에 무한 루프를 조심하도록 합니다
    • ex) toString(), lombok, JSON 생성 라이브러리
    • 무한 루프에 빠지면서 Stackoverflow에 빠지게 됩니다
    • 따라서 lomboktoString 기능을 사용하는 것은 주의하거나 지양하도록 하고, 써야 한다면 한쪽에서 끊는 옵션을 설정해서 사용하도록 해야 합니다
    • JSON 생성 라이브러리의 경우에는 애초에 Entity 자체를 JSON 값으로 리턴하는 것을 하지 않으면 됩니다. 엔티티의 경우 언제든 변경될 가능성이 있고, 그 경우 API 스펙 자체가 변경되는 등 여러 파생된 문제점들이 퍼지기 때문에 꼭 필요한 명세만 기록된 DTOResponse 등을 만들어서 사용하도록 합시다

양방향 매핑 정리

  • 처음 설계할때는 단방향 매핑으로만 설계를 하도록 합니다
    • 단방향 매핑만으로도 이미 연관관계 매핑은 완료된 상태입니다
  • 하지만 실무에서 개발을 하다보면 꽤 많은 경우 JPQL에서 역방향으로 탐색할 경우가 발생합니다
  • 필요한 때가 되면 그 때, 양방향을 추가하도록 합니다(테이블에 영향을 주지 않기 때문입니다)
profile
Always be happy 😀

0개의 댓글