본문 바로가기
Study/spring

자바 ORM 표준 JPA 프로그래밍(7) - 연관관계 매핑 기초

by 유경호 2020. 12. 28.
반응형

 

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

 

목차
• 연관관계가 필요한 이유
• 단방향 연관관계
• 양방향 연관관계와 연관관계의 주인


연관관계 매핑?

객체 연관관계와 테이블 연관관계를 매핑하는 것, 객체의 참조와 테이블의 외래 키를 매핑한다.

 

핵심 키워드 살펴보기

  • 방향(Direction) : 회원과 팀이 관계가 있을 때
    • 단방향 : 회원 → 팀, 팀 → 회원 둘 중 한 쪽만 참조하는 관계
    • 양방향 : 회원 → 팀, 팀 → 회원 양쪽 모두 서로 참조하는 관계
  • 다중성(Multiplicity) : 다대일(n:1), 일대다(1:n), 일대일(1:1), 다대다(n:m)
  • 연관관계의 주인(Owner) : 객체를 양방향 연관관계로 만든다면, 연관관계의 주인을 정해야함.

 

연관관계를 알아보기 위한 예제 시나리오

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

 

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

객체를 테이블에 맞추어 모델링 (참조 대신에 외래 키를 그대로 사용)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    @Column(name = "USERNAME")
    private String name;
    @Column(name = "TEAM_ID")
    private Long teamId;
    …
}
 
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;
    …
}
cs

 

객체를 테이블에 맞추어 모델링 (외래 키 식별자를 직접 다룸)

1
2
3
4
5
6
7
8
9
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId());
em.persist(member);
cs

 

→ 객체지향스럽지 못하다.

객체를 테이블에 맞추어 모델링 (식별자로 다시 조회, 객체 지향적인 방법은 아니다.)

1
2
3
4
//조회
Member findMember = em.find(Member.class, member.getId());
//연관관계가 없음
Team findTeam = em.find(Team.class, team.getId());
cs

 

단방향 연관관계

객체 지향 모델링 (객체 연관관계 사용)

객체 연관관계

  • 회원 객체는 Member.team 필드로 팀 객체와 연관관계를 맺는다.
  • 회원 객체와 팀 객체는 단방향 관계다. 회원은 Member.team 필드를 통해서 팀을 참조할 수 있으나 팀은 회원을 참조할 필드가 없다. 예를 들면 member 객체는 member.getTeam()을 통해 참조하는 team을 가져올 수 있는 반면 team은 member를 참조할 수가 없다.

 

테이블 연관관계

  • 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.

  • 회원 테이블과 팀 테이블은 양방향 관계다. 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고 또한 팀과 회원을 조인할 수도 있다.

  • 객체 연관관계와 테이블 연관관계의 차이

    참조를 통한 연관관계는 언제나 단방향이다. 객체간에 연관관계를 양방향으로 만들고자 한다면 반대쪽에도 참조하는 필드를 만들어 보관하게 해야한다. 결국 객체 연관관계를 양방향으로 하고자 한다면 연관관계를 두 개를 만들어줘야한다.(1 → 2, 2 →1) 이것은 양방향 관계이나 정확히는 서로 다른 방향 관계 2개가 있는 것이다. 반면에 테이블은 외래 키 하나로 양방향 조인이 가능하다.

 

객체 지향 모델링 (ORM 매핑)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
 
    @Column(name = "USERNAME")
    private String name;
 
    private int age;
 
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
cs

 

  • 객체 연관관계: 회원 객체의 Member.team 필드 사용
  • 테이블 연관관계: 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼 사용

 

위 예제를 살펴보면 연관관계 매핑 어노테이션을 통해 객체 연관관계와 테이블 연관관계를 매핑한 것을 알 수 있다.

  • @ManyToOne: 다대일(n:1) 관계라는 매핑 정보이다.
  • @JoinColumn: 조인 컬럼은 외래 키를 매핑할 때 사용한다.
    • name: 매핑할 외래 키 이름을 지정하는 속성

 

연관관계 저장

1
2
3
4
5
6
7
8
9
//팀 저장
 Team team = new Team();
 team.setName("TeamA");
 em.persist(team);
 //회원 저장
 Member member = new Member();
 member.setName("member1");
 member.setTeam(team); //단방향 연관관계 설정, 참조 저장
 em.persist(member);
cs

→ em.persist(team)을 통해 생성한 team을 저장하고, 영속된 team을 생성한 member의 참조에 담아 em.persist(member)를 수행한다. 이렇게 되면 member를 저장할 때, 참조하고 있는 영속된 team 엔티티의 id를 외래 키로 하여 데이터베이스에 저장해준다.

 

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

1
2
3
4
//조회
 Member findMember = em.find(Member.class, member.getId());
//참조를 사용해서 연관관계 조회
 Team findTeam = findMember.getTeam();
cs

 

→ 이 처럼 객체를 통해 연관된 엔티티를 조회하는 것을 객체 그래프 탐색이라 한다.

 

연관관계 수정

1
2
3
4
5
6
7
8
// 새로운 팀B
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);
 
// 회원1에 새로운 팀B 설정
Member member = em.find(Member.class"member1");
member.setTeam(teamB);
cs

 

→ member1을 조회해 member에 담아 영속한 뒤, 새로운 팀B로 바꿔준 다음 commit하게 되면 플러시가 일어나면서 변경을 감지하고 UPDATE 쿼리를 날린다.

 

연관관계 제거

1
2
Member member = em.find(Member.class"member1");
member.setTeam(null);
cs

 

 

 

지금까지 member → team 으로의 접근만 가능한 다대일 단방향 매핑을 알아보았다. 이제는 반대 방향인 team → member의 단방향 매핑을 추가한 양방향 연관관계를 알아보자.

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

양방향 매핑

객체의 연관관계

  • 회원 → 팀: 회원의 member.team 을 통해 팀을 참조
  • 팀 → 회원: 팀의 team.members를 통해 회원을 참조

 

양방향 매핑(Member 엔티티는 단방향과 동일)

1
2
3
4
5
6
7
8
9
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;
    @OneToMany(mappedBy = "team"
    List<Member> members = new ArrayList<Member>();
    …
}
cs

 

→ 팀과 회원은 일대다 관계다. 따라서 팀 엔티티에는 컬렉션인 List members를 추가했다.

연관관계 매핑 어노테이션

  • @OneToMany : 일대다 관계를 매핑하기 위해 사용
    • mappedBy : 양방향 매핑일 때 사용되는 속성, 반대쪽 매핑의 필드 이름을 값으로 준다

 

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

1
2
3
//조회
Team findTeam = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size(); // 팀 -> 회원, 역방향 조회
cs

 

→ 팀에서 회원 컬렉션으로 객체 그래프 탐색을 사용해서 회원을 조회할 수 있다.

 

실행결과

Hibernate: 
    /* insert me.kyeongho.Team
        */ insert 
        into
            Team
            (team_id, name) 
        values
            (null, ?)
Hibernate: 
    /* insert me.kyeongho.Member
        */ insert 
        into
            Member
            (member_id, createdDate, description, lastModifiedDate, roleType, team_id, name) 
        values
            (null, ?, ?, ?, ?, ?, ?)
Hibernate: 
    select
        member0_.member_id as member_i1_0_0_,
        member0_.createdDate as createdd2_0_0_,
        member0_.description as descript3_0_0_,
        member0_.lastModifiedDate as lastmodi4_0_0_,
        member0_.roleType as roletype5_0_0_,
        member0_.team_id as team_id7_0_0_,
        member0_.name as name6_0_0_,
        team1_.team_id as team_id1_1_1_,
        team1_.name as name2_1_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.team_id=team1_.team_id 
    where
        member0_.member_id=?
Hibernate: 
    select
        members0_.team_id as team_id7_0_0_,
        members0_.member_id as member_i1_0_0_,
        members0_.member_id as member_i1_0_1_,
        members0_.createdDate as createdd2_0_1_,
        members0_.description as descript3_0_1_,
        members0_.lastModifiedDate as lastmodi4_0_1_,
        members0_.roleType as roletype5_0_1_,
        members0_.team_id as team_id7_0_1_,
        members0_.name as name6_0_1_ 
    from
        Member members0_ 
    where
        members0_.team_id=?
m = Kyeongho

연관관계의 주인과 mappedBy

연관관계의 주인

정확하게는 객체에는 양방향 연관관계가 없다. 서로 다른 단방향 연관관계 2개를 어플리케이션 로직으로 양방향인 것처럼 보이게 할 뿐이다. 반면에 데이터베이스는 외래 키 하나로 양쪽이 서로 조인이 가능하다.

엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘이지만 외래 키는 하나이다. 여기서 둘 사이에 차이가 발생한다. 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인(Owner)이라 한다.

 

mappedBy

어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하여 설정한다.

  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.

 

아래의 그림처럼 둘 중 하나로 외래 키를 관리해야 한다.

누구를 주인으로?

  • 외래 키가 있는 있는 곳을 주인으로 정하는 걸 권장
    → 데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 갖는다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다.
  • 여기서는 Member.team이 연관관계의 주인

Member 엔티티 (연관관계의 주인)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity
@Table(name = "Member")
public class Member {
 
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;
 
    ...
 
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
 
    ...
}
cs

 

Team 엔티티

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
public class Team {
 
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;
 
    private String name;
 
    @OneToMany(mappedBy = "team"// mappedBy에는 반대편 엔티티의 필드명을 적어준다.
    private List<Member> members = new ArrayList<>();
 
    ...
}
cs

 

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

1
2
3
4
5
6
7
8
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);
em.persist(member);
cs

-> member -> team의 연관관계 참조가 설정되지 않아, 외래 키가 저장되지 않는다.

 

양방향 매핑시 연관관계의 주인에 값을 입력해야 한다. (순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 한다.)

1
2
3
4
5
6
7
8
9
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.persist(member);
cs

 

 

양방향 연관관계 주의

  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
  • 연관관계 편의 메소드를 생성하자
  • 양방향 매핑시에 무한 루프를 조심하자
    예: toString(), lombok, JSON 생성 라이브러리

 

연관관계 편의 메소드

1
2
3
4
5
6
7
public void setTeam(Team team) {
        if(this.team != null) {
            this.team.getMembers().remove(this);
        }
        this.team = team;
        team.getMembers().add(this);
}
cs

 

→ 연관관계의 주인 필드의 setter 메소드를 위와 같이 양방향 관계를 한꺼번에 설정할 수 있게끔 하는것이 연관관계 편의 메소드이다. 기존에 있던 연관관계 참조가 있다면 그 참조가 나를 참조하는 연관관계를 지워준다. 하지만 이 예시도 하나의 예시일 뿐 양방향 연관관계를 설정해 주는 로직은 반대편 엔티티에 있어도 된다.

반응형