본문 바로가기

Dev/JPA

연관관계 매핑 기초

이번 글에서는 연관관계에 대해서 적어보고자 한다.


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

나중에 경험치가 쌓였을 때 이책을 한번 봐봐야 겠다.


연관관계가 필요한 이유

예제 시나리오

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

테이블 관계에서는 TEAM이 1이고 MEMBER가 N으로 1:N 관계이다.

이를 객체 연관관계로 표현하면 외래키(FK)인 TEAM_ID를 teamId로 그대로 표현해서 사용

객체연관관계 vs 테이블 연관관계

코드로 표현하자면 아래와 같다..

try{

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

  Member member = new Member();
  member.setUsername("member1");
  
  
  // 이부분이 객체지향 스럽지 않다.
  // member.setTeam 이라고 해야 객체지향스러울것 같은데..
  // 지금상황은 외래키 식별자를 직접 다루는 것 이다.
  member.setTeamId(team.getId());
  em.persist(member);

  // 연관관계가 없기 때문에 계속 JPA한테 물어서 디비에서 꺼내와야하는 상황이 생긴다.
  Member findMember = em.find(Member.class, member.getId());

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

tx.commit();

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

  • 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
  • 객체는 참조를 사용해서 연관된 객체를 찾는다.
  • 테이블과 객체 사이에는 이런 큰 간격이 있다.

단방향 연관관계

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

 

객체지향스럽게 모델링 하는 것은 아래와 같이 하겠다는 것이다.

Member에 Team의 id값을 가져오는게 아니고 Team의 참조값을 그대로 가져왔다!

객체지향스럽게 모델링

@Entity
public class Member {

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

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

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

    // 위처럼 외래키(FK)를 그대로 사용하는 것이 아니고!
    // 아래와 같이 Team의 참조값을 가져오겠다는 뜻
    // Team에 에러가 나는 이유는 JPA한테 이 둘(Member와 Team)의 관계가 어떤지 알려줘야 한다. 1:1인지 1:N인지
    @ManyToOne
    @JoinColumn(name = "TEAM_ID") // Team 참조랑 테이블에서 TEAM_ID랑 매핑해줘야 한다!
    private Team team;

객체 연관관계 매핑(ORM 매핑)

객체 연관관계로 매핑하게 되면 아래와 같이 코드가 변경하여 사용할 수 있다.

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

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

//            member.setTeamId(team.getId());
  member.setTeam(team);
  em.persist(member);

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

//            Long findTeamId = findMember.getTeamId();
//            Team findTeam = em.find(Team.class, findTeamId);
  Team findTeam = findMember.getTeam();
  System.out.println("findTeam = " + findTeam.getName());

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

 

테이블 연관관계에서는 TEAM_ID라는 외래키 하나로 서로를 알 수 있다.

  • MEMBER 입장에서내가 소속한 팀을 알고 싶으면 / MEMBER의 TEAM_ID와 TEAM의 TEAM_ID를 조인하면 알 수 있다.
  • TEAM 입장에서 우리팀에 어떤 멤버가 속해있는지 알고싶으면 / TEAM의 TEAM_ID와 MEMBER의 TEAM_ID를 조인하면 알 수 있다.

문제가 되는 것은 이제 객체 연관관계이다.

  • 이전에서는 Member가 Team을 가졌기 때문에 Member에서 Team으로는 갈 수 있는데 반대로 Team에서 Member로 갈 수 없었다.
  • 그래서 지금 Team에다가 List members를 넣어줘야만 Team에서도 Member로 갈 수 있게 된다.

양방향 연관관계

코드로 수정된 Team 엔티티와 실행하는 부분을 적어보면...

@Entity
public class Team {

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

    // 이렇게 ArrayList()로 초기화 해주는 것이 관례
    // mappedBy는 나는 뭐랑 연결되어있지? 를 적어주는 것
    // 이렇게 해줘야 이제 반대로도 탐색이 가능하다!
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    
    
    
    
    try{

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

        Member member = new Member();
        member.setUsername("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.getUsername());
        }
       
	tx.commit();

m값 확인 : member1 


연관관계의 주인과 mappedBy

  • 객체와 테이블간에 연관관계를 맺는 차이를 이해하면 된다.

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

- 객체 연관관계 = 2개

→ 양방향 연관관계라고 얘기하지만 실제적으로는 단방향 연관관계가 두개인 것이다.

  • 회원(Member) → 팀(Team) 연관관계 1개(단방향)
  • 팀(Team) → 회원(Member) 연관관계 1개(단방향)

- 테이블 연관관계 = 1개

→ 테이블은 연관관계는 TEAM_ID라는 외래키(FK) 값으로 연관관계의 모든 것이 끝난다고 보면 된다.

   (키 값으로 서로를 알 수 있기 때문에)

  • 회원(Member) ↔ 팀(Team) 연관관계 1개(양방향)

객체의 양방향 관계 vs 테이블의 양방향 관계

 

  • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다!!!
  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

객체의 양방향 관계

  • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
  • MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계 가짐 (양쪽으로 조인할 수 있다)

테이블의 양방향 관계


  • 근데 객체의 양방향 관계를 하게되면 딜레마 현상이 오게 된다!
  • 객체를 두방향으로 만들어 놓다 보니까 둘 중 어떤거로 외래 키를 관리해야 하는지의 상황에 놓이게 된다.
  • Member의 Team team을 바꿔야 할지 Team의 List members를 바꿔야할지... 하지만 데이터베이스 입장에서는 TEAM_ID (외래키) 값만 업데이트 되면 된다.
  • 이러한 상황이 있다보니까 규칙이 생기게 된다.

객체의 양방향의 딜레마


연관관계의 주인 (Owner)

양방향 매핑 규칙

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

누구를 주인으로 지정할까?

  • 외래키가 있는 곳을 주인으로 정할  것
  • 여기에서는 Member.team이 연관관계의 주인
  • Team과 Member로 봤을 때 Member가 다이기 때문이다.

주인(Owner) 지정


여기까지 해서 내용을 주저리 주저리 적어봤는데.. 비유를 통해서 다시한번 정리해보자!

벌써 스타크래프트 3탄까지 왔네!

어김없이 필자는 프로토스 유저기때문에 프로토스 유닛을 위주로 설명해볼게!


프로토스에서 최종 유닛하면 떠오르는 것이 뭘까? 그래 맞아.. 이녀석이지..

프로토스 캐리어

그림에서 보는 것 처럼 이녀석은 여러마리의 인터셉터라는 녀석을 데리고 다녀.. 그리고 실질적으로 인터셉터가 공격하지.

캐리어하고 인터셉터와의 관계는 캐리어(1) : 인터셉터(N) 관계를 가지게 돼..

하나의 캐리어는 여러마리의 인터셉터를 가질 수 있고 인터셉터들은 하나의 캐리어에 속하게 되지..

 

이제 테이블 관계로 이것을 나타내면

각각의 인터셉터들은 자기가 어느 캐리어 소속인지 키값을 들고있고

캐리어도 키값을 들고있어.. 그래서 이 키를 통해서 서로 조인해서 알 수 있지..

 

그림으로는 이렇게 표현할 수 있어

테이블 연관관계

하지만 객체 연관관계 형식인 경우에는 각각이 비즈니스 관계가 되는 것이야..

그래도 실질적으로 싸우는 녀석은 인터셉터고.. 캐리어라는 몸체에 의존해서 타고있는 거니까 관계를 생성하자!!

객체 단방향 연관관계

이렇게 관계를 맺게 되면 JPA는 멍청하기때문에 우리의 관계를 명확하게 전달해줘야해..

보통은 쪽수가 많은쪽 즉 인터셉터(N)쪽에 명시를 해주는게 있는데..

코드로 표현하자면 이렇게 표현하는 거야..

    // 쪽수가 많은 인터셉터쪽에 ManyToOne 어노테이션을 써준다.
    // JoinColumn 어노테이션을 사용해서 인터셉터가 타고있는 본체인 캐리어를 지칭해준다.
    // 나는 이녀석이랑 관계를 맺고 있어라고 JPA에게 알려준다
    @ManyToOne
    @JoinColumn(name = "TEAM_ID") // Team 참조랑 테이블에서 TEAM_ID랑 매핑해줘야 한다!
    private Team team;

자 이렇게 코드를 적어주게되면..

JPA같은 경우는 아! 인터셉터라는 녀석은 캐리어와 관계가 있구나? 하고 알게되

그래서 인터셉터는 캐리어를 직접 접근해서 사용할 수 있지..

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

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

  //인터셉터(member)는 아래와 같이 member.setTeam 해서 바로 접근할 수 있다.
  member.setTeam(team);
  em.persist(member);

  Member findMember = em.find(Member.class, member.getId());
	
  //조회할 경우에도 findMember.getTeam 처럼 바로 접근할 수 있다.  
  Team findTeam = findMember.getTeam();
  System.out.println("findTeam = " + findTeam.getName());

근데 말이지.. 인터셉터들은 자기가 탈 캐리어가 뭔지 관계를 맺어줌으로써 JPA가 인식해서 알려주는데..

반대로 캐리어는 나한테 타고있는 인터셉터가 누군지 접근해서 정보를 알 턱이 없는거야..

 

즉 캐리어도 JPA한테 나도 인터셉터와 관계가 있다는 것을 알려줘야 하지! 이것을 양방향 연관관계를 맺는다고 해

그림으로 표현하면 이렇게 할 수 있지..

이제 양방향 연관관계를 맺고 관계를 맺었다는 걸 JPA한테 알려주려면..

캐리어(Team)가 있는 쪽에도 뭔가를 해줘야하는데!

코드로 표현하면 이렇게 해야 해

    // OneToMany 어노테이션을 통해서 1대인 캐리어쪽에 지정을 해준다.
    // mappedBy에 대해서는 뒤에서 좀더 설명!
    // 캐리어 입장에서는 여러대의 인터셉터를 태워야하니까 규칙으로 List의 ArrayList형태로 초기화하는게 관례!
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

자 근데 이렇게 되면 한가지 커다란 문제에 놓이게 돼..

무슨말이냐고?

JPA라는 녀석은 이제 두가지 큰 고민을 하게 되어있어!

1. JPA는 객체형 연관관계로 이루어져있기 때문에 양방향 연관관계라고 해도 실질적으로는 단방향 연관관계가 2개인거야

   (인터셉터에서 캐리어로 가는 관계 1개 + 캐리어에서 인터셉터로 가는 관계 1개)

2. 아니 관계가 두개인데 그러면.. JPA는 둘 중에 어떤게 변경됬을 때를 기준으로 데이터베이스를 변경하지???

즉! 연관관계의 주인(Owner)이 누구인지를 모르니 지정을 해줘야한다는 뜻이야!

 

자 그러면 누구를 주인으로 지정해야 할까? 헷갈릴 경우가 많은데! 명확하게 정의해줄게

주인은 말이지... 이녀석이야! 왜냐고?

인터셉터(연관관계주인)

잘 생각해봐봐!

이 캐리어라는 녀석은 직접 공격을 하지않아! 실질적으로 공격하는 녀석은 이 인터셉터(연관관계 중 N인 방향)라는 녀석이거든!

캐리어(연관관계 중 1인 방향)라는 녀석은 단순히 인터셉터를 태워서 이동해주는 이동수단일 뿐이야 힘이 하나도 없어!

 

양방향 매핑 규칙

  • 객체의 두 관계중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정) - 인터셉터
  • 주인이 아닌쪽은 읽기만 가능 - 캐리어(인터셉터를 태우고 이동만 하는 녀석)
  • 주인은 mappedBy 속성 사용 X  - 인터셉터
  • 주인이 아니면 mappedBy 속성으로 지정 - 캐리어(주인이 아니기 때문에 mappedBy를 각인 함으로써 구분해준다)

 

즉, 연관관계에서 외래키의 위치를 기준으로 주인 지정하자는 것이지!!

 

땅! 땅! 땅!  연관관계 중 외래키의 위치를 기준(N방향) 쪽이 연관관계 주인임을 정하리라!!

이렇게 규칙을 정해놓으면 헷갈릴 이유가 없어!

그림으로 표현해보면 이렇게 되는 거야!

 

연관관계의 주인(Owner)

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

JPA 상속관계 매핑  (0) 2020.07.30
다양한 연관관계 매핑  (0) 2020.07.28
엔티티 매핑  (0) 2020.07.12
JPA내부 구조(영속성 관리)  (0) 2020.07.06
JPA의 등장  (0) 2020.06.30