[TIL/JPA] 기본 개념 : 연관관계 매핑 기초

2024. 8. 17. 19:33·TIL/JPA
728x90
반응형

이번 파트에서는 객체의 참조와 테이블의 외래 키를 매핑하는 것을 배우고 정리를 해보았다!

1. 연관관계가 필요한 이유

시나리오 및 연관관계

- 회원과 팀이 있다.

- 회원은 하나의 팀에만 소속될 수 있다.

- 회원과 팀은 N:1 관계이다.

 

객체를 테이블에 맞추어 모델링을 하면, 협력 관계를 만들 수 없고!

테이블은 외래 키(FK)로 조인을 사용해서 연관된 테이블을 찾고, 객체는 참조를 사용해서 연관된 객체를 찾는다.

테이블과 객체 사이에는 이런 큰 간격이 있다.

 

객체를 테이블에 맞추어 모델링한 코드 예시)

> Member

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
public class Member {

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

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

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

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getTeamId() {
        return teamId;
    }

    public void setTeamId(Long teamId) {
        this.teamId = teamId;
    }
}

 

> Team

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
public class Team {

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

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 

> Main 실행

import jpastudy.relationship.Member;
import jpastudy.relationship.Team;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;

public class RelationShipMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

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

            Long findTeamId = findMember.getTeamId();
            Team findTeam = em.find(Team.class, findTeamId);

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

        emf.close();
    }
}

 

위의 코드를 보면 객체를 참조해서 가지고 온다고 할 수 없으며, 객체를 가져오기 위해서 PK를 찾아서 해당 PK로 find를 해서 객체를 가지고 오는 것을 확인할 수 있다.


2. 단반향 연관관계

이 부분을 객체 지향 모델링으로 객체 연관관계를 사용하여 단방향 연관관계를 만들어보자!

 

Member 엔티티의 코드에서 TEAM_ID로 TEAM을 참고하고 있던 부분을 TEAM 자체를 연관관계를 갖게 수정하였다.

(객체의 참조와 테이블의 외래키를 매핑한 것이다.)

@ManyToOne이라는 어노테이션을 사용하여 Member 엔티티 기준으로 Member가 다(N)의 관계를 Team 엔티티가 1의 관계를 갖는다고 표시를 한다. 그리고 조인을 할 때 사용하는 컬럼명을 @JoinColumn 어노테이션으로 표시를 해주면 된다.

import jakarta.persistence.*;

@Entity
public class Member {

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

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

    @ManyToOne // Member 입장에서 N, Team 입장에서 1
    @JoinColumn(name = "TEAM_ID") // 조인하는 컬럼명
    private Team team;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Team getTeam() {
        return team;
    }

    public void setTeam(Team team) {
        this.team = team;
    }
}

 

위와 같이 수정을 하면 아래와 같은 그림으로 표시할 수 있다.

 

수정한 코드로 실행을 해보자!

import hellojpa.relationship.Member;
import hellojpa.relationship.Team;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;

public class RelationShipMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

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

            Member member = new Member();
            member.setName("member1");
            member.setTeam(team);
            em.persist(member);

	// 쿼리를 바로 날리기 위해서 flush와 clear 호출
            em.flush();
            em.clear();

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

            Team findTeam = findMember.getTeam();
            System.out.println("findTeam = " + findTeam.getName());

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

        emf.close();
    }
}

 

이전과 다르게 setTeam을 통해서 Team 객체 자체를 할당하여 연관관계를 형성할 수 있다.

연관관계 수정하는 부분도 아래와 같이 간단하게 수정이 가능하다.

// 새로운 팀 생성
Team newTeam = new Team();
newTeam.setName("newTeam");
em.persist(newTeam);

// 회원에 새로운팀을 설정
member.setTeam(newTeam);

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

테이블은 외래키만 있으면 조인을 통해서 양쪽 모두 찾아올 수 있음

객체는 Team에서 Member를 가져올 수 없음

출처 : 김영한 JPA 기본편

양방향 매핑해보기

> Member Entity(기존과 동일)

package hellojpa.relationship;

import jakarta.persistence.*;

@Entity
public class Member {

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

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

    @ManyToOne // Member 입장에서 N
    @JoinColumn(name = "TEAM_ID") // 조인하는 컬럼명
    private Team team;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Team getTeam() {
        return team;
    }

    public void setTeam(Team team) {
        this.team = team;
    }
}

 

> Team Entity

Team 기준에서 1:N 이므로, @OneToMany 어노테이션을 붙여준다.

1개의 팀에서는 여러 Member를 가질 수 있으니 List로 Member를 받을 수 있도록 선언해준다.

mappedBy 간단하게 말하면, 연관관계를 가지고 있는 것을 표시하고 있는 것인데 아래에서 조금 더 자세히 작성하도록 하겠다.

package hellojpa.relationship;

import jakarta.persistence.*;

import java.util.ArrayList;
import java.util.List;

@Entity
public class Team {

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

    @OneToMany(mappedBy = "team") // Team의 기준에서는 1:N, 1:N 매핑에서 연관관계를 가지고 있는 것을 표시하는 것. (Member의 team)
    private List<Member> members = new ArrayList<>();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Member> getMembers() {
        return members;
    }

    public void setMembers(List<Member> members) {
        this.members = members;
    }
}

 

> Main 실행

양방향 연관관계를 맺음으로써 Member 객체와 Team 객체를 서로 참조하여 불러올 수 있다.

package hellojpa;

import hellojpa.relationship.Member;
import hellojpa.relationship.Team;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;

import java.util.List;

public class RelationShipMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

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

            Member member = new Member();
            member.setName("member1");
            member.setTeam(team);
            em.persist(member);

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

            // 양방향 연관관계
            Member findMember = em.find(Member.class, member.getId());
            List<Member> members = findMember.getTeam().getMembers();

            for (Member m : members) {
                System.out.println("m = " + m.getName());
            }

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

        emf.close();
    }
}

 

연관관계의 주인과 mappedBy

객체와 테이블 간에 연관관계를 맺는 차이를 이해해야 한다.

객체의 연관관계 = 2개

회원 -> 팀 연관관계 1개(단방향)
팀 -> 회원 연관관계 1개(단방향)

 

- 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다.

(단방향 2개가 있는데 이것을 그냥 양방향이라고 하는 것)

- 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

 

테이블의 연관관계 = 1개

회원 <-> 팀의 연관관계 1개(양방향)

- 양방향이라고 했지만, 외래키만 있으면 양쪽 모두가 연관관계를 가질 수 있음(두 테이블의 연관관계를 관리)

양쪽으로 조인할 수 있다.

 

여기서 딜레마가 생긴다.

Member 데이터를 바꾸려고 하거나 새로운 팀에 들어가고 싶다면, Member 엔티티에 있는 team을 바꿔야 하는지 아니면 Team 엔티티의 members를 바꿔야 하는지 이런 딜레마가 생긴다.

 

이런 딜레마를 해결하기 위해서는 둘 중 하나로 외래 키를 관리해야 한다.

이것이 바로 연관관계의 주인(Owner)

양방향 매핑 규칙

- 객체의 두 관계중 하나를 연관관계의 주인으로 지정

- 연관관계의 주인만이 외래 키를 관리(등록, 수정)

- 주인이 아닌 쪽은 읽기만 가능

- 주인은 mappedBy 속성 사용 불가능(mappedBy는 내가 이것을 통해 연관관계를 맺었다 라는 의미)

- 주인이 아니면 mappedBy 속성으로 주인을 지정해줘야 한다

 

누구를 주인으로 할까? 외래 키가 있는 곳을 주인으로 정하자!

예제 코드에서는 Member.team이 연관관계의 주인이 된다.

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

 

DB에서 1:N 관계 중 N 관계를 가지고 있는 엔티티가 연관관계 주인이 된다고 생각하면 된다.

출처 : 김영한 JPA 기본편

 

양방향 매핑시 가장 많이 하는 실수: 연관관계의 주인에 값을 입력하지 않음

바로 코드로 살펴보자

member에 setTeam을 하지 않고, team에서 member를 가져와 add를 하면 아래와 같이 DB 결과가 나오게된다.

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

Member member2 = new Member();
member2.setName("member2");

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

em.persist(member2);

 

코드를 수정해서 살펴보자!

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

Member member2 = new Member();
member2.setName("member2");

teamB.getMembers().add(member2);
member2.setTeam(teamB); // 연관관계의 주인에 값 설정
em.persist(member2);

 

teamB.getMembers().add(member2);

위와 같이 양쪽다 값을 입력해주지 않아도 되지만 문제가 생길 수 있다.

flush(), clear()를 하면 1차 캐시에 아무것도 없기 때문에 DB에서 조회해오지만

flsu, clear를 하지 않으면 1차 캐시에 올라가져 있고 연관관계로 가지고 오려고 할 때 아무것도 없기 때문에 getMembers()를 해도 조회가 되지 않는다.

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

Member member2 = new Member();
member2.setName("member2");
member2.setTeam(teamB);
em.persist(member2);

// teamB.getMembers().add(member2);

Team findTeam = em.find(Team.class, teamB.getId()); // 1차 캐시
List<Member> members = findTeam.getMembers(); // 팀을 통해서 멤버를 가져올 수 없음

 

양방향 연관관계 주의

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

 

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

주의) 양쪽에 다 생성하면 무한루프 등 문제가 발생할 수 있으니 한 쪽에만 연관관계 편의 메소드를 생성하자!

Member 엔티티 안에서
public void changeTeam(Team team) { // set을 사용하지 않고 로직이 들어가면 메소드 명을 다르게 변경한다.
	this.team = team;
	team.getMembers().add(this);
}

또는 Team 엔티티 안에서
public void addMember(Member member) {
	member.setTeam(this);
    member.add(member)
}

 

- 양방향 매핑시에 무한 루프를 조심하자

ex) toString(), lombok, JSON 생성 라이브러리

toString을 예시로 Member에서 team을 호출할 때 team의 toString을 호출하는데, team의 toString을 호출하면 members의 toString을 호출해서 무한 루프에 걸려서 stackoverflow에 걸린다.

 

양방향 매핑 정리를 해보자

- 단방향 매핑만으로도 이미 연관관계 매핑은 완료된 것이다. 처음에는 단방향 매핑으로 설계를 끝내자!

- 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다

- JPQL에서 역방향으로 탐색할 일이 많다

- 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 된다(테이블에 영향을 주지 않음)

 

연관관계의 주인을 정하는 기준

- 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안된다.

- 연관관계의 주인은 외래 키의 위치를 기준으로 정해야한다.

728x90
반응형

'TIL > JPA' 카테고리의 다른 글

[TIL/JPA] 기본개념 : 프록시와 연관관계 관리[즉시(EAGER)로딩, 지연(LAZY)로딩]  (0) 2024.08.27
[TIL/JPA] 기본개념 : 고급 매핑(상속관계 매핑, @MappedSuperclass)  (0) 2024.08.24
[TIL/JPA] 기본 개념 : 다양한 연관관계 매핑  (0) 2024.08.22
[TIL/JPA] 기본 개념 : 엔티티 매핑  (0) 2024.08.12
[TIL/JPA] 기본 개념 : 영속성 관리 - 내부 동작 방식  (0) 2024.08.08
'TIL/JPA' 카테고리의 다른 글
  • [TIL/JPA] 기본개념 : 고급 매핑(상속관계 매핑, @MappedSuperclass)
  • [TIL/JPA] 기본 개념 : 다양한 연관관계 매핑
  • [TIL/JPA] 기본 개념 : 엔티티 매핑
  • [TIL/JPA] 기본 개념 : 영속성 관리 - 내부 동작 방식
야리니
야리니
오늘보다 내일 더 성장하는 개발자가 되기 위한 야리니 블로그입니다 :)
    반응형
    250x250
  • 야리니
    야리니의 step by step
    야리니
  • 링크

    • GitHub
    • Linkedin
  • 전체
    오늘
    어제
    • 분류 전체보기 (477)
      • TIL (379)
        • Java (97)
        • Kotlin (28)
        • JPA (16)
        • Spring (37)
        • Oracle (22)
        • JDBC (7)
        • Web(HTML, CSS, JS, jQuery) (90)
        • View Template (31)
        • AWS (7)
        • HTTP (7)
        • CS (5)
        • Linux, Unix (2)
        • Python (20)
      • Trouble Shooting(Error) (37)
      • Algorithm (15)
      • Git,GitHub (8)
      • Diary (23)
      • 독서 (9)
      • Etc (6)
        • Mac (1)
        • 학원준비과정 (2)
  • 블로그 메뉴

    • 방명록
    • 태그
  • 공지사항

    • 안녕하세요 :)
  • 인기 글

  • 태그

    쌍용교육센터
    백엔드 개발자
    oracle
    java
    코틀린
    CSS
    java기초
    HTML
    국비지원학원
    Kotlin
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
야리니
[TIL/JPA] 기본 개념 : 연관관계 매핑 기초
상단으로

티스토리툴바