프레임워크(Framework)/JPA

[JPA] 단방향 연관관계와 양방향 연관관계 - 엔티티 매핑(Entity Mapping) - 5

잇트루 2022. 12. 3. 01:01
반응형
본 내용은 온라인 강의 사이트 인프런의 김영한 님의 강의 내용이 포함되어 있습니다.
'자바 ORM 표준 JPA 프로그래밍 - 기본편'
 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

단방향 연관관계

위에서 발생한 문제를 해결하기 위해서는 객체지향 모델링을 해야 한다.

먼저, 단방향 연관관계 매핑을 할 경우 다음과 같이 나타낼 수 있다.

이처럼 Member 클래스에는 Team 객체를 필드에 선언하여 객체 참조가 가능하도록 설계해야 한다.

 

 

Member 클래스 수정

@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;

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

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

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

    // Getter
    // ...
    // Setter
    // ...
}

다(N)에 해당하는 Member 클래스의 필드에 Team 객체를 선언하여 객체 참조가 가능하도록 한다.

@ManyToOne

  • Member 클래스가 다(N)에 해당하므로 @ManyToOne 어노테이션을 사용할 수 있다.
  • @ManyToOne은 다대일(N:1) 연관관계임을 명시한다.

 

 

@JoinColumn(name = “TEAM_ID”)

  • Member 클래스의 Team 객체는 MEMBER 테이블의 TEAM_ID(PK)와 매핑해야 한다.
  • @JoinColumn 어노테이션의 name 속성에 TEAM 테이블의 외래 키인 TEAM_ID 컬럼을 지정하여 매핑할 수 있다.

 

 

실행 코드

// 팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);

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

// Member의 팀을 조회하고자 하는 경우
Member findMember = em.find(Member.class, member.getId());
// findMember를 통해 Team 객체를 바로 조회 가능
Team findTeam = findMember.getTeam();
System.out.println("findTeam = " + findTeam.getId());
System.out.println("findTeam = " + findTeam.getName());

tx.commit();
// 출력
findTeam = 1
findTeam = TeamA

이로써 Member 객체에서 참조를 통해 Team의 데이터를 참조하여 사용할 수 있게 된다.

 

수정하고자 하는 경우에도 객체 참조를 통해 쉽게 수정할 수 있다.

// 새로운 팀 저장
Team team2 = new Team();
team2.setName("TeamB");
em.persist(team2);

// 팀 조회
Team findTeam2 = em.find(Team.class, 2L);

// findMember의 팀을 findTeam2로 변경
findMember.setTeam(findTeam2);

tx.commit();

 

 

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

앞서 작성한 단방향 매핑은 Meber 객체에서 Team 객체를 참조할 수 있도록 했다.

하지만, Team에서는 Member를 참조할 수가 없다.

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

// Member에서 Team 조회는 가능
Team findTeam = findMember.getTeam();

// Team에서는 Member 조회 불가능하다.
// Member findMember2 = findTeam.getMember();

 

JPA에서는 Member와 Team 양쪽에서 서로를 참조할 수 있도록 양방향 연관관계 매핑을 할 수 있다.

관계형 데이터베이스 테이블

  • 테이블 연관관계는 단방향 연관관계 매핑이나 양방향 연관관계 매핑과는 차이가 없다.
  • 즉, 데이터베이스 테이블은 외래 키 하나로 양방향 연관관계가 맺어진 상태인 것이다.

 

 

양방향 객체 연관관계

  • 객체는 단방향 연관관계와는 달리 Member에는 Team 객체를, Team에는 Member 객체를 갖도록 하여 참조할 수 있도록 해야 한다.
  • 하나의 Team에는 여러 명의 Member가 들어갈 수 있기 때문에 List<Members> 형태로 매핑을 해야 한다.

 

 

양방향 객체 연관관계 매핑

Team 수정

@Entity
public class Team {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    // 추가된 부분
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    // Getter
    // ...
    // Setter
    // ...
}
  • Team 클래스에 양방향 매핑을 위한 List<Member>를 선언한다.
    • 팀에는 여러 명의 멤버가 존재할 수 있기 때문에 List이며 타입은 Member 객체

 

 

  • @OneToMany
    • 팀 하나당 여러 명의 Member가 속할 수 있다.
    • Team 클래스는 일(1)에 해당하므로 @OneToMany 어노테이션을 통해 매핑한다.
    • mappedBy = “team” 부분은 아래에서 따로 설명

 

 

실행 코드

//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);

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

// DB 반영
em.flush();
// 영속성 컨텍스트 초기화
em.clear();

// Team 객체를 통해 members 조회
Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers();

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

 

 

연관관계의 주인과 mappedBy

연관관계 매핑 어노테이션의 mappedBy 속성은 이해하기 상당히 어렵다.

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

 

객체와 테이블이 관계를 맺는 차이

테이블의 양방향 연관관계

// MEMBER에서 TEAM 조회
SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

// TEAM에서 MEMBER 조회
SELECT * FROM T JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
  • 데이터베이스 테이블 연관관계 = 1개
    • Member ↔ Team 연관관계 1개(양방향)
  • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
  • MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계를 가짐(Join)

 

 

객체의 양방향 관계

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

// Team
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
  • 객체 연관관계 = 2개
    • Member → Team 연관관계 1개(단방향)
    • Team → Member 연관관계 1개(단방향)
  • 객체의 양방향 관계는 단방향 관계 2개를 맺은 것이다.
  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
  • 양방향 객체 둘 중 하나는 외래 키를 관리해야 한다.

 

 

연관관계의 주인(Owner)

양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정해야 한다.
  • 연관관계의 주인만 외래 키를 관리해야 한다.(등록, 수정)
  • 주인이 아닌 클래스는 읽기만 가능하다.
  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니면 mappedBy 속성으로 주인을 지정한다.

 

 

주인(Owner)을 정하는 방법

  • 외래 키가 있는 곳을 주인으로 정한다.
  • 위 예시에서는 Member.team이 연관관계의 주인이 된다.
  • Team 클래스를 연관관계 주인으로 설정하지 못하는 것은 아니다.
    • Team을 수정할 경우 MEMBER 테이블에 SQL 쿼리를 날리는 문제가 발생한다.
    • 성능 이슈 문제가 있다.
  • 다대일 관계의 경우, 다(N)에 해당하는 쪽에 외래 키가 존재한다.
  • 따라서 @ManyToOne 어노테이션을 사용하는 클래스가 주인이다.

 

 

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

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

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

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

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

tx.commit();
  • Member에 있는 Team이 연관관계 주인이다.
  • 따라서 Team에 있는 members 리스트는 mappedBy 속성으로 인해 읽기 전용이다.
    • SQL update 또는 insert는 적용되지 않는다.

 

 

연관관계의 주인에 값을 입력해야 한다.

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

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

// 연관관계의 주인에 값을 설정해야 한다.
member.setTeam(team);
em.persist(member);

tx.commit();
  • 하지만, 위 코드는 문제가 있다.
  • team을 통해 members를 조회 값을 찾을 수 없는 문제가 발생한다.

 

 

연관관계의 주인에만 값을 넣는 경우의 문제점

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

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

member.setTeam(team);
em.persist(member);

// team.getMembers().add(member);
// 또는
// em.flush();
// em.clear();

Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();

for (Member m : members) {
    System.out.println("m = " + m.getName());
}
  • 위 코드를 실행해 보면, 데이터베이스에 매핑된 데이터가 잘 추가되었음에도 불구하고 members를 조회하는 코드가 정상적으로 작동하지 않는다.
  • 이러한 이유는 데이터베이스에서 값을 꺼내서 team의 members를 조회하는 것이 아닌 처음 team을 생성할 때 작성된 코드가 영속성 컨텍스트 1차 캐시에 저장되어 있기 때문이다.
  • 따라서 중간에 team에서도 member를 추가하는 작업을 하거나, flush()와 clear()를 호출하여 영속성 컨텍스트를 비워야 한다.
  • findTeam은 순수한 객체 상태이기 때문
    • 객체지향 설계를 고려해서도 team과 member 양쪽에 값을 넣어주는 것이 맞다.

 

 

양방향 연관관계의 주의사항

순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하는 것이 좋다.

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

Member member = new Member();
// member 값 설정
member.setName("member1");
member.setTeam(team);

// team에 member 추가
team.getMembers().add(member);

em.persist(team);
em.persist(member);

 

 

위 코드를 편리하게 관리하기 위해 연관관계 편의 메서드를 생성하는 것이 좋다.

편의 메서드 작성 방식은 두 가지가 있다.

1. 주인인 Member 클래스에서 setter 수정 또는 메서드를 추가

@Entity
public class Member {

    ...

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

    ...

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

    // setTeam 또는 changeTeam 둘 중 하나 사용

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

 

2. 주인이 아닌 Team 클래스에서 addMember() 메서드를 추가

@Entity
public class Team {

    ...

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

    public void addMember(Member member) {
        member.setTeam(this);
        members.add(member);
    }

    ...
}

 

 

양방향 매핑 시 무한루프를 조심해야 한다

toString(), lombok, JSON 생성 라이브러리에서 무한 루프 문제가 발생할 수 있다.

Member

@Entity
public class Member {

    ...

    @Override
    public String toString() {
        return "Member{" +
                "id=" + id +
                ", name='" + name + '\\'' +
                ", team=" + team + // team 호출
                '}';
    }
}

 

Team

@Entity
public class Team {

    ...

    @Override
    public String toString() {
        return "Team{" +
                "id=" + id +
                ", name='" + name + '\\'' +
                ", members=" + members + // members 호출
                '}';
    }
}

위 코드들을 살펴보자.

  • Member에서 toString() 메서드가 호출되면, team을 출력한다.
  • Team에서 toString() 메서드가 호출되면, members를 출력한다.
  • 즉, 한 곳이 호출되면 무한 루프를 돌며 서로를 계속 호출하게 된다.

 

 

lombok, JSON 생성 라이브러리를 사용할 경우에도 똑같은 상황이 발생할 수 있다.

  • lombok에서 toString()을 생성하지 않는 것이 좋다.
  • Controller에서 엔티티를 절대 반환하지 않는 것이 좋다.

 

 

참고

양방향 매핑

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료된 것이다.
  • 따라서, 최대한 단방향 매핑으로 설계를 끝낸다.
    • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것뿐이기 때문이다.
  • JPQL에서 역방향으로 탐색할 일이 많다.
  • 단방향 매핑을 잘 설계하고 양방향 매핑은 필요할 때 추가해도 된다.
    • 테이블에 영향을 주지 않기 때문

 

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

  • 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안 된다.
  • 연관관계의 주인은 외래 키의 위치를 기준으로 정해야 한다.

 

반응형