본문 바로가기

Dot Programming/JPA

[JPA] 연관관계 매핑 기초

연관관계 매핑을 공부하기 위해서는 객체와 테이블 연관관계 차이를 이해해야 한다.
> 객체의 참조와 테이블 외래 키 매핑

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

객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.’
조영호(객체지향의 사실과 오해)

 

요구사항

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

 

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

 

 

2. 엔티티 생성시 참조 대신 외래 키를 그대로 사용

Member.class

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

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

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

    public Member(){
    }

}

 

Team.class

@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;

    private String name;
}

 

 

3. 외래 키 식별자를 직접 다룸

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

//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId());
em.persist(member);

 

객체를 테이블에 맞춰 모델링을 하게되면 아래와 같이 DB에 저장이 된다.

 

 

이렇게 저장되어 있는 경우 만약 member1이 속한 팀 소속을 조회하려면 어떻게해야 할까?

  1. 첫 번째로 Member테이블에 먼저 member1을 조회하여 teamId를 찾아내고
  2. 두 번째로 그 teamId로 Team테이블을 조회하여 찾아낸다.
//조회
Member findMember = em.find(Member.class, member.getId());

//연관관계가 없음
Team findTeam = em.find(Team.class, findMember.getTeamId());

 

Member와 Team 두 객체는 연관관계가 없기 때문에 꼬리물기식으로 계속 조회해줘야한다. 전혀 객체지향스럽지 못한 방식이다.

 

객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.

  > DB에서 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다

  > 객체는 참조를 사용해서 연관된 객체를 찾는다.

 

객체와 테이블은 각각 외래키와 참조를 통해 연관관계를 설정하기 때문에 두개의 패러다임이 완전히 다르다. 그렇기 때문에 연관관계 매핑이 필요한 것이다. 매핑의 방식으로는 단방향, 양방향이 있다.

 

2. 단방향 연관관계

1. 객체 연관관계 사용 

Member객체에 teamId를 저장하는 것이 아닌 Team을 참조한다

 

2. 객체의 참조와 테이블의 외래 키를 매핑

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

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

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

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

 

3. ORM 매핑

 

4. 연관관계 저장

member에 teamA를 저장하고 싶을 때 member.setTeam(team); 

team객체를 참조하여 저장해주면 단방향 연관관계가 설정된다.

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

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

 

 

5. 참조로 연관관계 조회 - 객체 그래프 탐색

연관관계가 설정되어있으므로 순차적으로 탐색을 하지 않고 간단하게 참조로 조회해주면 된다.

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

//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();

 

그러면 아래와 같이 조회를 할 수 있다.

 

team 조회

 

 

6. 연관관계 수정

연관관계 설정을 수정하고 싶을 때는 다시 set메소드를 통해 바꿔주면 update쿼리를 통해 외래 키가 업데이트 된다.

// 새로운 팀B
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);

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

 

 

 

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

JPA부분에서 가장 어려운 두가지가 영속성 컨텍스트 매커니즘의 이해와 바로 이 부분 양방향 연관관계와 연관관계의 주인이다. 이 부분이 어려운 이유는 연관관계에서 객체는 참조를 사용하고 테이블은 외래키로 조인을 하는데 여기서 발생하는 패러다임의 차이때문이다.

 

 

양방향 매핑

말그대로 양방향으로 참조가 가능한 관계이다. 

 

 

테이블은 변화가 없다. 그냥 조인을 하면 양방향으로 알 수 있기 때문에 따로 설정할 부분이 없다. 테이블은 외래키 하나로 양방향이 다 가능하기 때문에 애초에 양방향이라는 개념이 따로 없다.

 

문제는 객체이다. 객체는 Team과 Memeber이 1대다 관계로 단방향관계일 때는 Member에서 Team을 참조(Team team)할 수 있는데 Team에서는 Memeber로 갈 수 있는 방법이 없었다. 그래서 양방향에서는 Team객체에 참조(List members)할 수 있는 값을 넣어줘야한다.

 

1. Member 엔티티는 단방향과 동일

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

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

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

 

2. Team 엔티티는 컬렉션 추가

@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;

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

mappedBy : 연관관계의 반대편에 누구랑 매핑되어있는지 명시해주면 된다.

 

 

3. 반대 방향으로 객체 그래프 탐색

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

// 역방향 조회 Member -> Team
List<Member> members =findMember.getTeam().getMembers();

//조회 Team
Team findTeam = em.find(Team.class, team.getId());

//역방향 조회 Team -> Member
int memberSize = findTeam.getMembers().size(); 

 

 

연관관계의 주인과 mappedBy

객체와 테이블이 관계를 맺는 차이를 다시 한번 살펴보자.

객체 연관관계 = 2개

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

 

테이블 연관관계 = 1개

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

 

객체의 양방향 연관관계

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

 

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

 

테이블의 양방향 연관관계

테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.

 

MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계를 가져서 양쪽으로 다 조인할 수 있다.


'

여기서 딜레마가 온다. 객체의 참조는 단방향이 2개로 이루어져있다.

그러면 이 2개의 생성된 객체 중 무엇으로 DB 외래키랑 매핑을 해야할까?

 

 

  1. Member에 있는 Team team이 update되었을 때 DB에 있는 TEAM_ID(FK)가 update되어야 할까?
  2. 아니면 Team에 있는 List members가 update되었을 때 DB에 있는 TEAM_ID(FK)가 update되어야 할까?

 

 


 

이러한 문제를 해결하기 위해 객체가 양방향 연관관계를 맺을 때 연관관계의 주인(Owner)가 필요하다. 그 주인을 지정할 때 사용하는 것이 바로 mappedBy이다.

 

양방향 매핑규칙을 보면 다음과 같다.

양방향 매핑 규칙

  1. 객체의 두 관계중 하나를 연관관계의 주인으로 지정
  2. 연관관계의 주인만이 외래 키를 관리(등록, 수정)
  3. 주인이 아닌쪽은 읽기만 가능
  4. 주인은 mappedBy 속성 사용X
  5. 주인이 아니면 mappedBy 속성으로 주인 지정

 

그래서 mappedBy는 주인이 아닌 다른 쪽의 참조에서 사용하고 주인은 mappedBy를 사용하지 않는다.

 

주인은 누가?

주인은 외래 키가 있는 곳으로 주인으로 지정해야한다.

 

여기서는 Member.team이 연관관계의 주인이다. 그래서 Member에 있는 team이 업데이트 될때마다 DB에 있는 외래키 값도 업데이트 된다. 그래서 주인이 아닌 Team.members는 읽기만 가능하다.

 


 

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

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

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

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

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

em.persist(member);

읽기전용 Team.members로 값을 입력하면 null값이 발생한다

 

 

 

2. 양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.

member.getTeam(team);

 

순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 한다.

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

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

team.getMembers().add(member);  // 양쪽 다 값 설정
//연관관계의 주인에 값 설정
member.setTeam(team); //**


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

Team findTeam = em.find(Team.class, team.getId()); // 1차캐시에 없으면 못 읽어옴 (그래서 양쪽 값 설정)

em.persist(member);

 

연관관계 편의 메소드 생성

Member기준 연관관계 편의 메소드 생성

Member객체 setTeam()메소드에 Team에 값을 설정하게 해주어 양쪽 다 값을 넣어준다.

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

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

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

    //...

    public void setTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}
Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");
member.setTeam(team); // * 연관관계 편의 메소드 생성 * 

 

setTeam메소드 이름을 다른 set메소드와는 다르게 이름을 설정해주는 것이 좋다. (직관적으로 알아채기도 쉬움)

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

 

Team 기준 연관관계 편의 메소드 생성

반대로 Team객체에 설정해줘도 된다.

@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;

    private String name;

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

    public void addMember(Member member){
        member.setTeam(this);
        members.add(member);
}
//2. 양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");
            
team.addMember(member); // * Team 방향 연관관계 편의 메소드 생성 *

 

여기서 주의할 점은 한 곳에서만 연관관계 편의 메소드를 작성해야한다. 양쪽 다 설정하면 문제가 발생할 수도 있다. 위치 선정 기준은 그 때 개발 상황에 따라 알맞은 곳에 설정하면 된다.  

 

양방향 매핑시 무한 루프 조심

Member.toString()

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

 

Team.toString()

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

 

양쪽 중 하나의 toString()만 호출해도 무한루프가 발생한다.

Team.toString() -> members 호출-> Member의 모든 메소드 호출 -> Member.toString() -> team 호출 -> 무한 반복

Team.toString() -> members 호출-> Member의 모든 메소드 호출 -> Member.toString() -> team 호출 -> 무한 반복

StackOverflow 발생

 

☛ toString() 뿐만 아니라, lombok, JSON 생성라이브러리에서도 무한 루프 발생을 조심해야 한다.

 

내용 정리 

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

 

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

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

❈출처

인프런 김영한 개발자님 강의 - 자바 ORM 표준 JPA 프로그래밍