[JPA] 연관관계 매핑 1

조성우·2024년 3월 27일

JPA - 공부

목록 보기
6/16
post-thumbnail
  • 대부분의 엔티티는 다른 엔티티와 연관관계가 있음
  • 그런데 객체는 참조를 사용해서 관계를 맺고 테이블은 외래키를 사용해서 관계를 맺음
  • 객체 관계 매핑(ORM)에서 가장 어려운 부분이 바로 객체 연관관계와 테이블 연관관계를 매핑하는 것임
  • 방향: [단방향, 양방향]이 있다.
    회원과 팀 관계가 있을 때 회원 → 팀 또는 팀 → 회원 둘 중 한 쪽만 참조하는 것을 단방향,
    양쪽 모두 서로 참조하는 것을 양방향 관계라 함. 방향은 객체관계에만 존재함
  • 다중성: [다대일 N:1, 일대다 1:N, 일대일 1:1, 다대다 N:M] 다중성이 있음
    회원과 팀 관계에서 여러 회원은 한 팀에 속하므로 회원과 팀은 다대일 관계이고,
    반대로 한 팀에 여러 회원이 소속될 수 있으므로 팀과 회원은 일대다 관계임
  • 연관관계의 주인: 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 함

객체를 테이블에 맞추어 모델링한다면?

@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);

//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId());

em.persist(member);

외래키 식별자를 직접 다룸


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

//연관관계가 없음
Team findTeam = em.find(Team.class, team.getId());

식별자로 다시 조회


위는 객체 지향적인 방법이 아님
즉, 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없음

  • 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
  • 객체는 참조를 사용해서 연관된 객체를 찾는다.
  • 테이블과 객체 사이에는 이런 큰 간격이 있다.

단방향 연관관계

@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;}
  • 객체의 참조와 테이블의 외래키를 매핑

//팀 저장
Team team = new Team();
team.setName("TeamA");

em.persist(team);

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

em.persist(member);
  • 연관관계 저장

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

//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();
  • 참조로 연관관계 조회 - 객체 그래프 탐색

양방향 매핑

(Member 엔티티 코드는 단방향과 동일)


@Entity
public class Team {

	@Id @GeneratedValue
	private Long id;
    
	private String name;

	@OneToMany(mappedBy = "team")
	List<Member> members = new ArrayList<Member>();}
  • Team 엔티티에 컬렉션 추가

//조회
Team findTeam = em.find(Team.class, team.getId());

int memberSize = findTeam.getMembers().size(); //역방향 조회
  • 반대 방향으로 그래프 탐색

mappedBy (feat. 연관관계 주인)

객체와 테이블 간에 연관관계를 맺는 차이를 이해해보자

  • 객체 연관관계 = 2개
    • 회원 -> 팀, 연관관계 1개 (단방향)
    • 팀 -> 회원, 연관관계 1개 (단방향)
  • 테이블 연관관계 = 1개
    • 회원 <-> 팀, 연관관계 1개 (양방향)

객체의 양방향 연관관계

  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 함
  • 즉, 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개

테이블의 양방향 연관관계

  • 테이블은 외래키 하나로 두 테이블의 연관관계를 관리
  • 즉, 외래 키 하나로 양방향 연관관계를 가짐 (양쪽으로 조인 가능)

연관관계의 주인 (Owner)

두 객체 중 하나로 외래 키를 관리해야 함!

양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인이 외래키를 관리 (등록, 수정)
  • 주인이 아닌 쪽은 읽기만 가능
  • 주인은 mappedBy 속성 사용 X
  • 주인이 아니면 mappedBy 속성으로 주인 지정 (어떤 컬럼에 의해 매핑되었음을 표시)

누구를 주인으로?

외래 키가 있는 곳을 주인으로 정하자 (외래키가 있는 곳이 항상 N이 됨, N 쪽이 연관 관계의 주인)


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

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

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

//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);
em.persist(member);
  • 연관관계의 주인에 값을 입력하지 않음
  • 이렇게 입력할 경우 MEMBER의 TEAM_ID가 null로 입력되어 연관관계가 설정되지 않음
  • 양방향 매핑시 연관관계의 주인에 값을 입력 (순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력)
Team team = new Team();
team.setName("TeamA");
em.persist(team);

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

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

em.persist(member);

team.getMembers().add(member)를 입력하지 않아도 team.getMembers()로 값을 조회하면 JPA는 정상적으로 값을 가져온다. 그러나 em.flush(), em.clear() 등으로 영속성 컨텍스트를 정리하지 않은 상태에서는 1차 캐시에 들어있는 엔티티를 가져오므로 team.getMembers().add(member)를 입력하지 않았을 경우 team.getMembers()로 값을 조회하면 아무 것도 조회되지 않는다.


양방향 연관관계 주의

  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자

  • 연관관계 편의 메소드를 생성하자

    public void changeTeam(Team team) {  // 로직이 섞여있을 경우 Getter, Setter 사용을 피하자.
        this.team = team;
        team.getMembers().add(this);
    }
    // or
    public void addMember(Member member) {
        member.setTeam(this);
        members.add(member);
    }
    • 연관관계 편의 메소드가 양쪽에 있을 경우 문제의 소지가 될 확률이 높으니 한쪽에만 생성
  • 양방향 매핑시에 무한 루프를 조심하자

    • toString(), lombok, JSON 생성 라이브러리 (team.getMembers().toString -> member.getTeam().toString() -> Loop...)
    • 컨트롤러에서 Entity를 절대 반환하지 말고 DTO로 반환할 것 (무한 루프 방지, 일관된 API 스펙)

양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료 (단방향 매핑을 우선적으로!)
  • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
    • 객체 입장에선 크게 이득이 없음 (고민거리가 많아짐)
    • JPQL에서 역방향으로 탐색할 일이 많음 (이때 양방향을 고려하면 됨!)
  • 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨 (테이블에 영향을 주지 않음)
  • 연관관계의 주인을 정하는 기준은 비즈니스 로직을 기준이 아닌 외래키의 위치를 기준으로 정해야 함

0개의 댓글