[3] JPA 프로그래밍 (6) - 연관관계 매핑 1 / 단방향&양방향 연관관계 / 연관관계 주인, mappedBy

김정욱·2021년 3월 4일
0

[3] JPA 프로그래밍

목록 보기
6/15
post-thumbnail

순서

  • 연관관계의 필요성
    : 연관관계가 왜 필요한지 2가지 예시를 통해 설명
    • 객체 -> 테이블에 맞춘 설계
    • 객체 -> 객체지향 설계
  • 객체와 테이블의 차이
    • 참조 vs 외래키
  • 양방향 연관관계
  • 양방향 연관관계 필요성 & 주의점

연관관계 필요성

[ 테이블에 맞춘 설계 (연관관계 X) ]

  • Member 객체에는 teamId라는 필드값이 존재
  • MemberTeam간 객체 연관관계는 없다
  • DB관점에서 보면 외래키가 있어 보이지만, 객체 관점에서는 아무관계도 X

[ 코드 ]

  • Member 객체
  • Team 객체
  • Member객체에 있는 Team에 대한 name을 알려면?
    • Member에 있는 teamId를 가져온다
    • teamIdTeam 테이블에서 name을 조회한다
      --> 2번의 과정이나 걸림, 만약 객체 참조라면 바로될텐데..

[ 객체지향 설계 (단방향 연관관계) ]

  • Member 테이블에 단순 teamId가 아닌 Team 객체를 참조시킴
  • 객체 참조로 인해 객체간 단방향 연관관계가 생겼다

[ 코드 ]

  • Member 객체
  • Member객체에 Team 객체 자체를 참조시킴
  • 관계는 @ManyToOne --> 여러 멤버가 하나의 팀에 소속될 수 있으니까
  • Member객체에 있는 Team에 대한 name을 알려면?
    • Member에 있는 Team객체 자체에서 name을 조회!

[ 차이점 ]

  • 객체지향 설계를 하면 단순한 value가 아닌 객체 자체를 참조시킨다
  • 객체 자체를 참조 --> 객체간 연관관계 형성
  • 참조하는 연관관계의 데이터를 가져오기가 훨씬 편함!
    (테이블에 맞춘 설계라면 쿼리를 한번 더 쏴야함)
/* 테이블에 맞춘 설계 */
  //팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
  //회원 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId());
em.persist(member);


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

객체와 테이블의 차이

[ 설명 ]

  • 객체와 테이블은 관계를 맺는 차이가 존재
  • 객체는 2개의 단방향으로 양방향을 만들어줘야 하지만.
    테이블은 FK 하나로 양방향 연관관계가 형성된다!
    -> 즉, 객체와 테이블간 차이가 있고, 객체에서 양방향을 만들기 위해서
         추가적인 작업이 필요
    --> Team 객체에서 복수의 Member를 저장할 필드 변수가 추가되어야함!
  • 객체 연관관계 (총 2개의 단방향 연관관계)
    • Member -> Team 연관관계 1개 (단방향)
    • Team -> Member 연관관계 1개 (단방향)
  • 테이블 연관관계 (총 1개의 양방향 연관관계)
    • Member <-> Team 연관관계 1개 (양방향)

양방향 연관관계

[ 과정 ]

  • 양방향 연관관계를 만들기 위해서는 3가지 작업이 필요
    • 추가적인 필드 생성(한쪽만)
    • 연관관계 주인 설정
    • 연관관계 편의 메소드 작성 --> 추후 설명

[ 의문 ]

  • 누구를 주인으로 ? / 어떤쪽에 추가필드 생성?
    • 주인 : 외래키가 있는 곳을 주인으로 설정 (권장)
      (1 : N 관계에서 항상 N에 외래키가 생긴다 --> N이 주인)
    • 추가필드 : 외래키가 없는 곳에 추가필드를 생성
  • 이유는 ?
    • 만약 Team연관관계의 주인으로 설정하면 Team 수정시 Member가 수정되는 쿼리가 생김
      --> 부자연스럽다(다른 테이블의 쿼리가 날라감;)
    • Member연관관계 주인이면 Member 수정시 Member 수정 쿼리가 생김
      --> 자연스럽다

연관관계 주인 ?

[ 필요 이유 ]

  • 객체는 테이블과 달리 기본적으로 단방향 연관관계 상태
  • 객체의 양방향 연관관계를 만들기 위해서 필드변수를 추가해야 한다!
    (Listmembers)
  • 그랬더니, 하나의 DB column값이 객체에 있는 2개의 필드에 영향을 주는 상황을 볼 수 있다
    --> DB입장에서는 Member 테이블의 FK가 도대체 두 필드중 어떤 값에 의해 변경되어야 하는지 혼돈에 빠지는 것
    --> 2개의 필드주인이 되는 필드를 정해서 FK가 참조하도록 해야한다
  • 즉, 우리는 객체간 양방향 연관관계를 만들기 위해 연관관계의 주인(Owner)을 만들어야는 이유를 깨달음

[ 설명 ]

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

[ 코드 ]

  • Member 객체 (단방향때와 변동 X)
  • Team 객체
  • 양방향 연관관계를 해주기 위해 2가지 수행
    • @OneToMany : 하나의 team에는 여러 member가 들어가기 때문
    • mappedBy = "team"
      : 연관관계 주인인 Member객체의 team필드를 가리킨다
      (mappedBy연관관계 주인을 가리켜줘야 한다)

연관관계 편의 메소드 ?

[ 연관관계 주인 사용시 ]

  • 연관관계 주인이 아닌 쪽에 값을 추가하면 ?
    --> 실제 DB에는 원하는 값이 안들어감
    (FK는 연관관계 주인을 참조해서 변경되기 때문)
/* Team과 Member는 양방향 연관관계가 설정되어 있다 */
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");

team.getMembers().add(member);
/* 연관관계 주인이 아닌 team에서 memberlist에 member를 추가함
 --> DB의 FK는 연관관계 주인을 보고 참조하므로 실제 Member DB에 TeamId는
      설정이 안된것으로 간주되어 null이 들어간다 */
em.persist(member);

  • 연관관계 주인에 값을 넣은 올바른 상황
/* Team과 Member는 양방향 연관관계가 설정되어 있다 */
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");

member.setTeam(team);
/* 연관관계의 주인에 값 설정 
    --> FK가 바라보는 필드기 때문에 알아서 내부적으로
        해당 PK를 빼서 FK값으로 넣어준다 */
em.persist(member);


[ 고민해야할 점 ]

  • 위 예제를 보았을때, 우리는 그러면 연관관계 주인이 아닌 값에는 직접 추가를 안해줘도 되는게 확실한지 고민해봐야 한다.
  Team team = new Team();
  team.setName("TeamA");
  em.persist(team);
  Member member = new Member();
  member.setName("member1");

  /* 이부분 주석 없애면 밑에 iter 출력에 출력이 된다 */
  //team.getMembers().add(member);

  member.setTeam(team);
  System.out.println("===============");
  /* 아직 flush() 되지 않았기 때문에 team.members에는 해당 값이
     들어가있지 않다! */
  for (Member m : team.getMembers()) {
      System.out.println("m.getName() = " + m.getName());
  }
  System.out.println("===============");
  em.persist(member);
  tx.commit();
  • 물론 연관관계 주인인 member에만 설정하고 등록하면 DB상문제가 없음
    --> 하지만, DB에 등록된다는 것은 flush()된 이후
    라는 것
    --> 그 전에 team.members에는 값이 들어가 있지 않다!
  • 즉, 순수한 객체에도 값을 추가해주는 과정이 필요하다!
    --> 위처럼 team.getMembers().add(member);로 해줘도 되지만,
    번거롭기도하고 가시적으로 좋지 않다.
    --> 이것을 member.setTeam(team)의 내부에서 처리하면 깔끔해짐!
    --> 이것이 바로 의존관계 편의 메서드

[ 연관관계 편의 메서드 ]

  • 연관관계 주인순수 객체 모두 동기화해주는 편의 메서드
  • set과 같은 함수 이름은 너무 관례적이니 change로 바꾸어 내부에 로직이 있음을 알림
/* Member에 setTeam을 아래로 변경! */
...
    public void chanheTeam(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");
  //team.getMembers().add(member);

  member.chanheTeam(team);
  System.out.println("===============");
  /* 아직 flush() 되지 않았기 때문에 team.members에는 해당 값이
     들어가있지 않다! */
  for (Member m : team.getMembers()) {
      System.out.println("m.getName() = " + m.getName());
  }
  System.out.println("===============");
  em.persist(member);
  tx.commit();

결과

  • 순수 객체에도 값이 동기화되어 있다!

양방향 연관관계 필요성 & 주의점

[ 필요성 ]

  • 양방향 매핑을 하기 위해서 발생하는 비용
    • 추가 필드 & 연관관계 편의 메서드 작성
    • 연관관계 주인 설정
  • 양방향 연관관계는 사실 객체의 반대 방향으로 객체 그래프 탐색 기능이 추가된 것
    (역방향 탐색)
  • 필요할 때만 추가적으로 만들어서 사용하는 것이 좋다.(권장)
  • 즉, 기본적으로 단방향 연관관계로 만든 후 필요할 때 추가!

[ 주의점 ]

  • 순수 객체 상태를 고려해서 반드시 연관관계 메소드 작성!
  • 무한루프 조심! (자주 발생)
    • toString()
      : 객체에서 toString()을 생성하면 객체 자체를 출력할 수 있게 된다.
      이 때, 객체 안에 참조 객체를 출력하기 위해 또 그 객체에 대한 toString()을 출력 --> 계속 무한루프가 돈다! -->StackOverFlow 발생
    • JSON생성 라이브러리
      : response에 객체를 보낼 때 자동으로 JSON 생성 라이브러리로 해당 객체를
      JSON화
      하게 된다! 이 때 JSON화 되는 객체안에 또 다른 객체를 JSON화 하게되고 계속 계속 타고 들어간다! (무한루프)
      --> Entity를 반환하지 말고, DTO를 사용해서 반환해야한다!
profile
Developer & PhotoGrapher

0개의 댓글