Spring/자바 ORM 표준 JPA 프로그래밍

[JPA 기본편] 5. 연관관계 매핑 기초

kyung.Kh 2024. 5. 18. 03:19

단방향 연관관계 매핑

JPA를 사용해서 객체 연관관계와 테이블 연관관계를 매핑하면 위 그림과 같이 된다. 회원 객체의 Member.team 필드와 회원 테이블의 MEMBER.TEAM_ID의 왜래키 컬럼이 매핑되는 것이다.

회원과 팀의 관계가 다대일 관계이므로 회원 엔티티에 @ManyToOne을 추가하고 @JoinCalumn으로 Team의 TEAM_ID와 조인하였다.

회원 엔티티와 팀 엔티티

// 회원 엔티티
@Entity
public class Member {

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

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

    @ManyToOne  // Member와 Team의 관계가 Member의 입장에서 다대일이므로...
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    ...
}

// 팀 엔티티
@Entity
public class Team {

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

@JoinCalumn

: 외래키를 매핑할 때 사용한다.

  • name 속성에는 매핑할 외래키의 이름을 지정한다.
  • 생략이 가능하며 만약 생략한다면 외래키를 찾을 때 기본 전략을 사용하게 된다.

여기서는 회원과 팀 테이블은 TEAM_ID 외래키로 연관관계를 맺으므로 이 값을 지정한다.


양방향 연관관계

: 양쪽으로 참조해서 서로 조회할 수 있는 관계

  • 테이블 → 원래 방향 개념이 없어서 FK로 받으면 양쪽에서 모두 조회 가능
  • 객체 → 양쪽에 서로 객체를 넣어주어야 양쪽에서 모두 조회 가능. 한쪽에만 넣으면 한쪽 방향으로만 조회 가능

Member 입장에서는 다대일 관계이므로 @ManyToOne이 붙는다. Team의 입장에서는 일대다 관계이므로 @OneToMany가 붙고, 컬렉션을 추가해주고, mappedBy에는 다대일이 매핑되어 있는 변수명을 넣어주어야 한다.

// 회원 엔티티
@Entity
public class Member {

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

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

    @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")  // Member와 Team의 관계가 Team의 입장에서 일대다 관계...
    private List<Member> members = new ArrayList<>();  // mappedBy로 나의 반대편은 team과 걸려있다는 것을 명시
}

mappedBy

  • mappedBy를 이해하려면 객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.
  • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 2개를 양방향이라고 부르는 것이다.

객체에서 양방향 연관관계는 서로 다른 단방향 연관관계 2개를 잘 묶어서 보이게 한 것이고, 테이블은 외래키 하나로 두 테이블의 연관관계를 관리한다. 양방향 연관관계에서 객체의 참조는 둘인데 외래키는 하나가 되는것이므로 차이점이 발생한다.

그래서 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계 주인이라 한다.

연관관계의 주인

양방향 연관관계의 주인은 FK를 관리하게 되고, 주인이 아닌 쪽(mappedBy로 주인을 걸어줌)은 읽기만 가능하게 해야 한다.

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래키(FK)를 관리(등록, 수정)할 수 있으며, mappedBy 속성을 사용하지 않는다. 그리고 @ManyToOne(다)이 붙는다.
  • 연관관계의 주인이 아닌 쪽은 읽기만 가능하며 mappedBy 속성으로 주인을 지정한다. @OneToMany(mappedBy="주인")을 붙인다.

연관관계의 주인은 누구?

외래키가 있는 곳을 주인으로 정한다. 즉, '다'(Many)쪽을 주인으로 정하자!!

DB입장에서 보면 외래키가 있는 곳이 무조건 '다'이다. 즉, DB의 다(N)쪽인 Member가 연관관계의 주인이 되며 @ManyToOne이 된다.

ex) 자동차(1)와 바퀴(N)가 있으면 연관관계의 주인은 바퀴가 된다.


양방향 연관관계의 주의점

주인이 아닌 쪽에 값을 입력하여 저장이 안되는 경우가 있다. 양방향 매핑 시에는 객체 관점에서 순수 객체 상태를 고려해서 항상 양쪽에서 값을 설정하는 것이 가장 안전하다.

실습) 연관관계 매핑은 주인인 쪽에서는 무조건 해야한다. 만약, 역방향에서만 하면 설정이 안된다.

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

Member member = new Member();
member.setName("member1");
member.setTeam(team); // 연관관계 주인, member → team. 이렇게 주인인 쪽에서 연관관계를 설정해주어야함!

// 역방향(주인이 아닌 방향)만 연관관계 설정 → 이것만 하면 설정안됨
team.getMembers().add(member);

em.persist(member);

연관관계 편의 메소드

member.setTeam(team); // 연관관계 주인 , member → team
team.getMembers().add(member); // 주인x 저장 시 사용되지 않음, team → member

양방향 연관관계는 양쪽 관계를 모두 신경써야 한다. 만약에 위 둘 중 하나만 호출하면 양방향 관계가 깨질 수 있다.

따라서 양방향 관계에서는 두 코드를 하나인 것처럼 사용하는 것이 안전하다.

//회원 엔티티 ('다'쪽, 주인!)
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    …
}

//연관관계 편의 메소드
public void changeTeam(Team team) {
		this.team = team;
		team.getMembers().add(this); 
}

Member 클래스에서 setTeam() 메서드에서는 보통 매개변수로 받은 team을 this.team에 넣어준다. 그리고 매개변수로 받은 Team의 Member에 현재 Member 클래스를 추가하면 위 두 코드를 합친 것과 동일하다.

즉, member → team 설정과 team → member 설정을 모두 하는 것이다.

양방향 매핑 시 무한루프 주의

  • toString(), lombok : 양쪽에서 toString 함수를 만들면 계속 호출해서 무한루프 오류 발생 → 쓰지 말것을 권장
  • JSON 생성 라이브러리 : 컨트롤러에서 리턴값으로 엔티티를 JSON으로 변환할 때 무한루프 발생 → 컨트롤러에서 양방향 매핑되어 있는 엔티티를 반환하지 말고 DTO로 변환해서 반환할 것(엔티티가 바뀌면 API 스펙이 바뀜)

정리

  1. 처음 설계할 때는 단방향 매핑으로 설계해라. → 단방향 매핑만으로 이미 연관관계 매핑은 완료
  2. 양방향 매핑은 필요할 때 추가하면 됨 → 양방향 매핑을 추가한다고 테이블에 영향을 주지 않으니 단방향 매핑으로 다 끝낸다는 생각으로 설계를 하자.(양방향 매핑은 반대 방향으로 조회 기능이 추가된 것으로 JPQL에서 역방향으로 탐색할 일이 많음)

[출처]

https://www.inflearn.com/course/ORM-JPA-Basic/dashboard이 글은 김영한님의 JPA 강의 중 5장을 듣고 정리한 내용입니다

https://velog.io/@sooyoungh/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EB%A5%BC-%EB%A7%A4%ED%95%91%ED%95%B4%EB%B3%B4%EC%9E%90

https://ssdragon.tistory.com/77?category=1002157

728x90