본문 바로가기

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

05. 연관관계 매핑 기초

💡 본 게시글은 김영한님의 인프런(Inflearn) 강의 자바 ORM 표준 JPA 프로그래밍 - 기본편에 대해 공부하고, 정리한 내용입니다.


1. 객체와 테이블 연관관계의 이해

JPA에서 객체와 테이블의 연관관계를 어떻게 매핑하고 관리할 것인가는 매우 중요합니다. 이 글에서는 연관관계의 기본 개념과 매핑 방법을 설명하고, 실무에서 사용하는 올바른 패턴을 구체적으로 다룹니다.

1) 목표

  • 객체와 테이블 연관관계의 차이를 이해합니다.
  • 객체의 참조와 테이블의 외래 키를 매핑하는 방법을 익힙니다.
  • 연관관계의 주요 용어를 이해합니다:
    • 방향(Direction): 단방향, 양방향
    • 다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)
    • 연관관계의 주인(Owner): 객체 양방향 연관관계에서는 관리 주인이 필요합니다.

2) 연관관계가 필요한 이유

  • 객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것입니다.
    • 조영호, '객체지향의 사실과 오해'에서 제시된 바와 같이, 객체들은 서로 협력해야 하며, 이는 객체 간의 관계 설정을 통해 이루어집니다.

3) 예제 시나리오

  • 회원(Member)팀(Team)이 있으며, 회원은 하나의 팀에만 소속될 수 있습니다.
  • 회원과 팀은 다대일(N:1) 관계입니다.

(1) 객체를 테이블에 맞추어 모델링 (연관관계가 없는 객체)

기본적으로 객체와 테이블 사이에는 관계를 맺기 위한 외래 키 또는 참조 필드가 필요합니다.

객체의 데이터 중심 설계

  • Member 엔티티 예시:
@Entity
public class Member {
    @Id 
    @GeneratedValue
    private Long id; // 기본 키

    @Column(name = "USERNAME")
    private String name; // 사용자 이름

    @Column(name = "TEAM_ID")
    private Long teamId; // 외래 키 대신 사용되는 필드

    // Getter, Setter ...
}
  • Team 엔티티 예시:
@Entity
public class Team {
    @Id 
    @GeneratedValue
    private Long id; // 기본 키

    private String name; // 팀 이름

    // Getter, Setter ...
}

데이터 중심 설계의 문제점

// 팀 저장
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도 직접 조회해야 함
Team findTeam = em.find(Team.class, team.getId());
  • 문제점: Member에서 Team을 조회하려면 teamId를 통해 다시 find()를 호출해야 합니다. 이는 객체지향스럽지 않고 협력 관계를 만들기 어렵습니다.

(2) 테이블과 객체의 차이

  • 테이블은 외래 키로 조인을 사용해 연관된 테이블을 찾습니다.
  • 객체는 참조를 사용해 연관된 객체를 찾습니다.
  • 이러한 차이 때문에 객체와 테이블 사이에는 큰 간격이 존재합니다.

4) 단방향 연관관계

(1) 객체 연관관계 사용

  • 객체의 참조와 테이블의 외래 키를 매핑하여 단방향 연관관계를 설정합니다.
  • Member 엔티티의 단방향 매핑 예시:
@Entity
public class Member {
    @Id 
    @GeneratedValue
    private Long id;

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

    @ManyToOne // N:1 관계 설정
    @JoinColumn(name = "TEAM_ID") // 외래 키 매핑
    private Team team; // Team 객체 참조

    // Getter, Setter ...
}

(2) 연관관계 저장

  • 팀과 회원을 저장하는 예시:
// 팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);

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

(3) 참조로 연관관계 조회 - 객체 그래프 탐색

// 조회
Member findMember = em.find(Member.class, member.getId());
// 참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();
  • 객체지향적인 방법으로 참조를 통해 연관된 객체를 직접 탐색할 수 있습니다.

(4) 연관관계 수정

  • 연관된 팀을 변경하는 예시:
// 새로운 팀B 생성
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);

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

5) 양방향 매핑

  • 테이블의 연관관계는 외래 키 하나로 양방향을 모두 지원합니다.
    (실제로는 방향이라는 개념 자체가 없습니다.)

(1) 양방향 매핑 설정

  • Member 엔티티:
@Entity
public class Member {
    @Id 
    @GeneratedValue
    private Long id;

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

    @ManyToOne // N:1 관계 설정
    @JoinColumn(name = "TEAM_ID") // 외래 키 매핑
    private Team team; // Team 객체 참조

    // Getter, Setter ...
}
  • Team 엔티티: @OneToMany 컬렉션을 추가합니다.
@Entity
public class Team {
    @Id 
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team") // 역방향 매핑
    private List<Member> members = new ArrayList<>(); // Member 객체 참조 리스트

    // Getter, Setter ...
}

(2) 반대 방향으로 객체 그래프 탐색

// 조회
Team findTeam = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size(); // 역방향 조회
  • Note: 객체는 양쪽으로 가려면 양쪽에 레퍼런스를 가질 수 있는 필드를 추가해야 합니다.

(3) 양방향 매핑의 이해

  • 객체와 테이블의 관계를 이해해야 합니다.
    • 객체 연관관계: 실제로는 단방향 2개입니다.
      • 회원 -> 팀 연관관계 1개 (단방향)
      • 팀 -> 회원 연관관계 1개 (단방향)
    • 테이블 연관관계: 외래 키 하나로 양방향 모두 지원합니다.
      • 회원 <-> 팀의 연관관계 1개 (양방향)

6) 연관관계의 주인과 mappedBy

(1) mappedBy의 이해

  • mappedBy 속성은 연관관계의 주인이 아닌 쪽에 사용됩니다.
  • 연관관계의 주인만이 외래 키를 관리합니다(등록, 수정).
  • 주인이 아닌 쪽은 읽기만 가능합니다.

(2) 연관관계의 주인 설정하기

  • 양방향 매핑 규칙:
    • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정합니다.
    • ManyToOneMany(다) 쪽을 연관관계의 주인으로 설정합니다.
    • 외래 키가 있는 곳을 주인으로 정하는 것이 좋습니다.
  • Member 엔티티는 주인, Team 엔티티는 mappedBy 사용:
@Entity
public class Team {
    @Id 
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team") // 역방향 매핑
    private List<Member> members = new ArrayList<>(); // Member 객체 참조 리스트

    // Getter, Setter ...
}

(3) 연관관계 편의 메서드 생성

  • **

연관관계 편의 메서드**를 생성하여 실수를 줄입니다.

@Entity
public class Member {
    // ...
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    public Team getTeam() {
        return team;
    }

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}
  • 객체 상태를 고려하여 항상 양쪽에 값을 설정해야 합니다.
  • Note: 연관관계 편의 메서드는 setter를 사용하지 않습니다.

7) 양방향 매핑시 주의사항

  • 양방향 매핑 시 가장 많이 하는 실수는 연관관계의 주인에 값을 입력하지 않는 것입니다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);

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

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

em.persist(member);
  • mappedBy 속성이 지정된 가짜 매핑은 단순한 읽기 전용이므로 실제 DB에 반영되지 않습니다.
  • 연관관계의 주인(Member)에 설정을 해야 DB에 반영됩니다.

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

  • 무한 루프가 발생할 수 있는 상황:
    • toString(), lombok, JSON 생성 라이브러리
    • 컨트롤러에서 엔티티를 직접 반환하지 말고, DTO로 변환해서 반환해야 합니다.

8) 양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료되었습니다.
  • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 필요할 때만 추가합니다.
  • 양방향 매핑은 테이블에 영향을 주지 않고, 엔티티에 코드 몇 줄만 추가하면 됩니다.

9) 정리

  1. 단방향 매핑으로 설계를 시작합니다.
  2. 양방향 매핑은 필요할 때 추가합니다.
  3. 연관관계의 주인은 외래 키가 있는 곳으로 설정합니다.
  4. 객체 상태를 고려하여 항상 양쪽에 값을 설정하고, 연관관계 편의 메서드를 사용하여 실수를 방지합니다.
  5. 무한 루프와 같은 문제를 피하기 위해 엔티티를 DTO로 변환해서 반환해야 합니다.

'Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글

06. 다양한 연관관계 매핑  (1) 2024.09.08
04. 엔티티 매핑  (1) 2024.09.08
03. 영속성 관리 - 내부 동작 방식  (0) 2024.08.11
02. JPA 시작하기  (0) 2024.08.11
01. JPA 소개  (0) 2024.07.24