연관관계 매핑

식빵·2021년 12월 30일
0

JPA 이론 및 실습

목록 보기
7/17

🍀 개요

이 장의 핵심은 객체의 연관관계와 테이블의 외래 키를 매핑하는 것이다.

본격적인 시작에 앞서 연관관계 매핑 키워드 미리 알아두기

  • 방향 Direction : 단방향과 양방향이 있다.
    • 단방향: A 객체 -> B 객체 참조
    • 양방향: A 객체 -> B 객체, B 객체 -> A 객체 참조
  • 다중성 Mulitiplicity : N:1 , 1:1, 1:N, N:M 이 있다.

  • 연관관계 주인 owner : 외래키를 실제 관리하는 참조 필드



🍀 단방향 연관관계

일단 아래와 같은 연관 관계가 있다고 가정하자.

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

위의 시나리오를 구현하기 위해서 Member(=회원), Team Entity를 작성해보자.

package hello.jpa;

import javax.persistence.*;

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @Column(name = "TEAM_ID")
    private Long teamId;

    // GETTER, SETTER 생략
}
package hello.jpa;

import javax.persistence.*;

@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    // GETTER, SETTER 생략
}

그런데 위처럼 작성한 코드는 객체를 제대로 다루는 방식일까?
객체는 다른 객체와 연관관계를 맺을 때는 참조를 사용한다.
지금처럼 Member 엔티티가 Team 엔티티의 특정 필드의 값을 갖는 것은 객체에서 말하는 연관관계라고 하기 힘들다.

그러면 한번 코드를 아래처럼 바꿔보자.

package hello.jpa;

import javax.persistence.*;

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    private Team team; // 참조를 사용하도록 코드 변경

    // GETTER, SETTER 생략
}

그럴듯하다.
하지만 생각해면 우리가 DataBase의 Table 에서 서로 연관관계를 맺을 때 외래키를 사용한다.
DB의 외래키객체의 참조라는 개념은 서로 상이한데, JPA에서는 어떤 방법으로 매핑을 할까?


방법은 아래와 같다.

package hello.jpa;

import javax.persistence.*;

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne	// 추가1! Member <-> Team은 N:1 관계
    @JoinColumn(name = "TEAM_ID") // 추가2! 외래키의 이름
    private Team team;

    // GETTER, SETTER 생략
}

이렇게 하면 단방향 연관관계 매핑은 끝이다!

  • @ManyToOne 를 통해서 Member과 Team이 N:1의 다중성을 갖는다
  • @JoinColumn 을 통해서 실제 DB Table 외래키 이름을 매핑한다.




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

🐛 객체와 테이블 양방향 연관관계 매핑법

이번엔 양방향 매핑을 알아보자.
앞서서는 Member -> Team 방향으로 단방향 연관관계를 갖었다.
이번에는 Team -> Member 방향으로도 연관관계를 갖는 코드를 짜보자.

@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team") // 반대편 연관관계를 갖는 엔티티의 필드명
    private List<Member> members = new ArrayList<>();

    // GETTER, SETTER 생략
}

@OneToMany 는 이해가 되지만, mappedBy = "team"은 대체 뭘까?
알려주기 이전에 잠시 아래와 같은 문제점을 생각해보자.

객체는 양쪽에서 연관관계를 맺을 때 위 코드처럼 서로 참조값을 갖는 변수를 갖으면 된다.
하지만 DataBase Table 에서의 하나의 외래키로 연관관계를 맺는다.


여기서 정말 애매한 점이 나온다.

앞서 단방향 연관관계에서는 객체에서 하나의 참조값 변수, 그리고 하나의 DataBase Table 외래키를 매핑하면 끝났다. 덕분에 참조값 변수 하나로 외래키를 insert, update, 즉 관리가 됐다.

그렇다면 현재 상황에서는 어떨까?
테이블의 TEAM_ID(FK) 값은 Member 객체의 team 이 수정되었을 때 update되어야 하는가?
아니면 Team 객체의 members 가 수정되었을 때 update되어야 하는가?

JPA 에서는 이런 모호함을 없애기 위해
두 개의 참조 필드 중 하나가 외래키를 관리하는 룰을 만들었다.
그리고 외래키를 도맡아서 관리하는 하는 클래스의 필드를 우리는 연관관계의 주인이라고 한다.


위에서 본 Team 엔티티의 코드를 보면 @OneToMany 애노테이션의 인자값으로 mappedBy를 쓰는데, 이것은 자신이 연관관계의 주인이 아님을 의미한다.

그리고 mappedBy의 값은 실제 연관관계의 주인인 엔티티의 필드명(Member.team)이다.




🐛 양바향 매핑 규칙

양방향 매핑에는 다음과 같은 규칙이 있으니 알아두자.

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



🐛 누구를 주인으로?

이건 답이 정해져 있다. 외래키가 있는 쪽이 주인이다.

이러면 헷갈리지도 않고, 성능 이슈도 없고 좋다.
이후 생길 수 있는 많은 고민거리가 해결된다.

그러니 "N:1의 관계에서는 N 쪽이 연관관계 주인"이라고 항상 생각하자.

연관관계 주인은 단순하게 DB 테이블에서 N 쪽이라고 생각하자.




🍀 양방향 연관관계 주의점

🐛 연관관계 편의 메소드

  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하는 연관관계 편의 메소드를 작성하자.

  • 연관관계 편의 메소드 작성시 알아둘 점

    • 연관관계 편의 메소드는 관례적인 setXXX 라는 이름 말고 다른 걸 쓰자.
    • 단순한 setter가 아니라 어떤 중요한 로직이 있구나~ 하고 인식 (취향이긴 하다)
    • 연관관계 편의 메소드는 둘 중 한 군데에서만 작성. 재수 없으면 무한 루프 걸림.
    • 실제 개발을 하면 편의 메소드를 둘 중 어디에 둬야될지 상황에 따라 결정하게 됨.
  • 연관관계 편의 메소드의 예
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {

    @Id
    @Column(name = "member_id")
    private String id;

    private String username;

    // 연관관계 매핑
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

    // 연관관계 편의 메소드
    public void setTeam(Team team) {
    
        if(this.team != null) {
            this.team.getMembers().remove(this);
        }
    
        this.team = team;
        team.getMembers().add(this);
    }
    
    //... 생략 ...    
}

🐛 무한 루프 조심!

  • 양방향 매핑시에 무한 루프를 조심하자
    • 예: toString(), lombok, JSON 생성 라이브러리
    • toString, lombok 사용시 연관관계 필드는 제외시키자
    • JSON 라이브러리는 주로 Controller 단에서 값을 반환할 때 사용하니
      엔티티를 반환하지 않으면 된다.
    • Controller 단에서 엔티티를 사용하면
      • 무한 루프가 생길 수 있다.
      • Entity가 바뀌면 API 스펙이 기존과 달라져 버린다. 굉장한 혼란을 일으킴!
      • 그래서 Controller에서는 엔티티를 단순한 DTO로 변환하는 것이 좋다.




🍀 양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료된다.
    그러니 양방향 매핑을 꼭 할 필요는 없다.
  • 양방향 매핑은 반대 방향으로 객체 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
  • JPQL에서 역방향으로 탐색할 일이 많음
  • 양방향 매핑은 필요할 때 추가해도 됨 ( 테이블에 영향을 주지 않음 )





🍀 실습용 코드


🐛 엔티티 코드

@Entity
@Getter @Setter
@NoArgsConstructor
public class Member {

    @Id
    @Column(name = "member_id")
    private String id;

    private String username;

    // 연관관계 매핑
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

    public Member(String id, String username) {
        this.id = id;
        this.username = username;
    }

    // 연관관계 편의 메소드 - 사실은 setTeam 보다 더 비즈니스적인 이름을 줘야한다.
    public void setTeam(Team team) {

        if(this.team != null) {
            this.team.getMembers().remove(this);
        }

        this.team = team;
        team.getMembers().add(this);
    }

}
@Entity
@Getter @Setter
@NoArgsConstructor
public class Team {

    @Id
    @Column(name = "team_id")
    private String id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Team(String id, String name) {
        this.id = id;
        this.name = name;
    }
}

🐛 테스트 코드

public static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();


    try {
        tx.begin();

        // 팀 1 저장
        Team team1 = new Team("team1", "팀1");
        em.persist(team1);

        // 회원1 저장
        Member member1 = new Member("member1", "회원1");
        member1.setTeam(team1);
        em.persist(member1); //JPA에서 엔티티 저장시, 연관된 모든 엔티티는 "영속 상태"여야 함

        // 회원2 저장
        Member member2 = new Member("member2", "회원2");
        member2.setTeam(team1);
        em.persist(member2);

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

        // 조회 방법 2가지:
        // 1.객체 그래프 탐색
        /*
        Member member1 = em.find(Member.class, "member1");
        Team team = member1.getTeam();
        System.out.println(team.getName());
        */

        // 2. jpql
        /*
        String jpql = "select m from Member m join m.team t where " +
                "t.name=:teamName";

        List<Member> resultList = em.createQuery(jpql, Member.class)
                .setParameter("teamName", "팀1")
                .getResultList();

        for (Member member : resultList) {
            System.out.println("member.getUsername() = " + member.getUsername());
        }
        */


        // 수정
        /*
        Team team2 = new Team("team2", "팀2");
        em.persist(team2);

        Member member1 = em.find(Member.class, "member1");
        member1.setTeam(team2);

        em.flush();
        em.clear();
        */


        // 연관관계 제거
        /*
        Member member1_ = em.find(Member.class, "member1");
        member1_.setTeam(null);

        em.flush();
        em.clear();
        */


        // 연관된 엔티티 삭제 - 연관관계 제거 후에 가능함! 외래키 제약 조건
        /*
        Member member1 = em.find(Member.class, "member1");
        member1.setTeam(null);
        Member member2 = em.find(Member.class, "member2");
        member2.setTeam(null);

        Team team1 = em.find(Team.class, "team1");
        em.remove(team1);
        */

        tx.commit();
    } catch (Exception e) {
        e.printStackTrace();
        tx.rollback();
    } finally {
        em.clear();
    }

    emf.close();
}

양방향 테스트는 작성하지 않았다.
양방향 테스트는 외래키를 직접 관리하는 필드와, 그렇지 않은 필드를 통해서
외래키 관리 여부를 보고, 연관관계 편의 메소드가 잘되는지 확인만 하면 된다.




🍀 참고

자바 ORM 표준 JPA 프로그래밍
인프런 JPA 관련 로드맵

profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글