본문 바로가기

BackEnd/JPA

[JPA] 연관관계 매핑 기초

연관관계가 필요한 이유

 예제 시나리오

  • 회원과 팀이 있다.
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 회원과 팀은 다대일 관계이다.

 객체를 테이블에 맞추어 모델링

   ⇒ 연관관계가 없는 객체

자바 ORM 표준 JPA 프로그래밍

  ⇒ 참조 대신 외래키를 그대로 사용

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

@Entity
public class Team {
    @Id @generatedValue
    @Column(name = "team_id")
    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
    @Column(name = "member_id")
    private Long id;
    
    @Column(name = "username")
    private String name;
    
    // @Column(name = "team_id")
    // private String teamId;
    
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
    ...
}

@Entity
public class Team {
    @Id @generatedValue
    @Column(name = "team_id")
    private Long id;
    private String name;
}

  

  ⇒ 객체 지향 모델링

 

  ⇒ 연관관계 저장

// 팀 저장
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, 1L);

// 참조를 통한 연관관계 조회
Team findTeam = findMember.getTeam();

Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);

// 회원1에 새로운 팀B 설정
findMember.setTeam(teamB);

 

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

 양방향 매핑

자바 ORM 표준 JPA 프로그래밍

 

  ⇒ Member 엔티티는 단방향과 동일하나 Team 엔티티는 컬렉션 추가(양방향)

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    
    @Column(name = "username")
    private String name;
    
    // @Column(name = "team_id")
    // private String teamId;
    
    // 연관관계의 주인
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
    ...
}

@Entity
public class Team {
    @Id @generatedValue
    @Column(name = "team_id")
    private Long id;
    private String name;
    
    // 연관관계의 주인의 반대편
    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<>();
}

 

  ⇒ 반대 방향으로 객체 그래프를 탐색한다.

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

int membersize = findteam.getMembers().size();  // 역방향 조회

 

 연관관계의 주인과 mappedBy

  • 객체와 테이블간의 연관관계를 맺는 차이를 이해해야 한다.
  • 객체 연관관계  = 2개
    - 회원 → 팀 연관관계 1개 (단방향)
    - 팀 → 회원 연관관계 1개 (단방향)
  • 테이블 연관관계 = 1개
    - 회원 ↔ 팀의 연관관계 1개(양방향)

 객체의 양방향 관계

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

 테이블의 양방향 연관관계

  • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
  • MEMBER.TEAM_ID 외래키 하나로 양방향 연관관계를 갖는다. (양쪽으로 조인할 수 있다)
    SELECT *
    FROM MEMBER M
    JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID;
    
    SELECT *
    FROM TEAM T
    JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID;​
     

 연관관계의 주인(Owner) - 양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정한다.
  • 연관관계의 주인만이 외래 키를 관리한다. (등록/수정)
  • 주인이 아닌 쪽은 읽기만 가능하며 mappedBy 속성을 사용할 수 없다.
  • 주인이 아니면 mappedBy 속성으로 주인을 지정한다.

 누구를 주인으로??

  • 비즈니스 로직을 기준으로 연관관계의 주인을 선택해선 안되며 외래 키가 있는 곳을 주인으로 정해야 한다.
  • Member와 Team의 관계에서는 Member.team이 연관관계의 주인이다.

자바 ORM 표준 JPA 프로그래밍

 

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

  ⇒ 연관관계의 주인에 값을 입력하지 않음

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

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

// 역방향(주인이 아닌 방향)만 연관관계를 설정한다.
// 이럴경우 member의 team_id는 갱신되지 않는다.
team.getmembers().add(member);
em.persist(member);

 

 양방향 매핑 시 연관관계의 주인의 값을 입력해야 한다.

  ⇒ 순수한 객체 관계를 고려하면 항상 양쪽 다 값을 입력해야 한다. (물리적오류를 해소)

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

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

// 객체 관계를 고려한 연관관계 주인이 아닌 team에 member파라미터 설정
team.getmembers().add(member);

// 연관관계의 주인에 값을 설정한다.
member.setTeam(team);
em.persist(member);
  • 순수 객체 상태를 고려하여 항상 양쪽에 값을 설정하자.
  • 연관관계 편의 메서드를 생성하자.
    public void setTeam(Team team) {
        // 기존에 이미 팀이 존재한다면
        if(this.team != null) {
        	this.team.getMembers().remove(this); // 해당 팀의 관계 제거
        }
        this.Team = team;
        team.getMembers().add(this);
    }​
     
  • 양방향 매핑 시에는 무한 루프를 조심하자 ( 예 : toString(), lombok, JSON 생성 라이브러리)

 양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계의 매핑은 완료되었으나 편의상 조회용도로 양방향을 사용하기도 한다.
  • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.
  • JPQL에서 역방향으로 탐색할 일이 많다.
  • 단방향 매핑을 잘 하고 양방향은 필요시마다 추가해도 무방하다. (테이블에 영향 X)