본 정리 내용은 "김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편" 을 듣고 정리한 내용이며, 설명에 필요한 사진 중 중요한 사진이라고 생각한 사진을 강의자료에서 가져왔음을 출처로서 밝힙니다.

 

 

단방향 연관관계와 양방향 연관관계를 예제 코드를 작성하면서 정리해보았다. 예제의 시나리오는 다음과 같다.

1. 회원과 팀이 있다.

2. 회원은 하나의 팀에만 소속될 수 있다.

3. 회원과 팀은 다대일 관계이다.

 

 

 

객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다. 
테이블은 외래키로 조인을 사용해서 연관 테이블을 찾는다. 하지만 객체는 참조를 사용해서 연관된 객체를 찾는다. 테이블과 객체 사이에는 이런 큰 간격이 있다. 이를 어떻게 해결 할 수 있을까? 

 

 

 

 

단방향 연관 관계

객체 연관관계를 사용한 설계

 

 

테이블 연관관계를 보면 MEMBER에서 TEAM_ID를 외래키로 가짐으로써 멤버가 어떤 팀에 소속되어 있는지 알 수 있다. 이러한 테이블 구조를 보고 객체의 연관관계를 생각해본다면 Member클래스에서 team을 다대일로 매핑해줌으로써 멤버가 어떤 팀에 소속되는지 알 수 있다.

 

 

 

teamId를 단순하게 Long형의 아이디값으로 갖는 것이 아니라 @ManyToOne 을 해줌으로써 다대일 매핑을 해줄 수 있다. 테이블 구조로 생각해보면 FK키가 있는 쪽이 항상 N, 즉 '다' 가 된다. 객체 연관관계에서도 N인 쪽을 @ManyToOne해주고, @JoinColumn(name = "TEAM_ID) 해주어서 외래키로 사용할 컬럼과 그 컬럼의 이름을 정해주는 것이다.

 

 

이와 같이 사용하면 된다.

TeamA를 하나 만들어서 영속성 컨텍스트에 영속을 시켜주고, memberA을 만들어서 영속성 컨텍스트에 영속을 시켜주는데 이 때, member.setJpaTeam(team); 해서 멤버를 팀에다가 넣어주면 된다. 

 

em.persist(team); 을 하면 JpaTeam의 id가 @GeneratedValue이기 때문에 먼저 DB에 들러서 PK값을 얻어온 후 영속성 컨텍스트에 저장을 한다. 

그 후에 member를 만들어서 member.setJpaTeam(team); 하게 되면 JPA가 위에서 걸어준 @ManyToOne 애노테이션과 @JoinColumn을 보고 연관관계를 파악하여 자동으로 PK값을 FK로 사용을 한다.

 

 

 

조회 할때에는 em.find(Member.class, member.getId()); 하여 findMember를 만들고, findMember.getTeam(); 하여 바로 사용할 수 있다.

이 때, DB로 날아가는 쿼리를 보면 Member와 Team을 Join해서 select하는 것을 볼 수 있다. 

 

 

 

 

 

 

 

 

 

양방향 연관관계

양방향 매핑

 

 

위의 단방향 연관관계와 비교해보면 테이블 연관관계는 똑같다. 

양방향 연관관계를 매핑해주는 작업을 해도 테이블에는 영향이 없는 것이다. 가장 좋은 설계는 단방향 연관관계로 설계하는 것이고, 필요시에 양방향 연관관계를 걸어주는 것이 바람직하다.

 

테이블 연관관계에서 보면 TEAM_ID를 FK로 가짐으로서 멤버가 어떤 팀에 소속되었는지 알 수 있었고, 또 반대로 팀에 어떤 멤버들이 소속되어 있는지도 알 수 있다. 테이블은 사실 방향의 개념이 없는 것이다. 

 

단방향 연관관계를 생각해보면, Member클래스에는 TEAM_ID를 JoinColumn으로 하는 필드를 만들어줌으로써 멤버가 어떤 팀에 속했는지 알 수 있지만, 반대로 Team에서는 Member로 바로 접근이 불가능했었다.
이것이 바로 테이블과 객체 사이의 차이점이다.

 

 

양방향 매핑에서는 Team 엔티티에 컬렉션을 추가해 주어야 한다. Team 엔티티에 List를 추가해줌으로써 이 팀의 어떤 멤버가 있는지 담을 수 있는 것이다.

 

Team은 Member와 일대다 관계이다. 즉 '1' 에 해당됨으로 @OneToMany 애노테이션을 붙여주면 된다. 여기서 중요한 것은 mappedBy 속성이다. mappedBy는 말 그대로 '나는 누구에게 매핑되어져 있다'를 표시해주는 것이다. 여기에서 "team"은 내가 매핑당하고 있는 FK값이 되는 변수명이다.

 

 

 

위와 같이 양방향 연관관계 매핑이 된것을 조회해볼 수 있다.

em.find(Member.class, member.getId()); 하여 findMember를 만들고, 이 findMember가 속한 팀을 getJpaTeam() 하여 가져오고 그 팀의 getMembers() 하여 컬렉션을 얻어올 수 있는 것이다.

Team에 컬렉션을 만들어서 서로 양방향 매핑을 해줌으로써 이런 조회가 가능해 진 것이다.

 

 

 

그런데, 중요한것이 있다.

아까 mappedBy를 Team에다가 써주었었는데, 그러면 mappedBy는 대체 어느 방향에 써주어야 하는 것일까?

 

 

우선 왜 mappedBy와 같은 것을 써줘야 하는지 이해를 해보자.

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

 

 

 

반면에 테이블을 생각해보면 테이블은 외래키 하나로 두개의 테이블의 연관관계를 관리할 수 있다. MEMBER.TEAM_ID 외래키 하나로 양방향 연관관계를 가지게 되는것이다. 

 

 

그렇다면 결국, 객체는 Team team든지 List members 든지, 둘중에 하나로 외래 키를 관리해야하는 것이다.

그래서 가장 중요한 것이 바로 연관관계의 주인(Owner)를 정하는 것이다!

 

 

연관관계의 주인

- 양방향 매핑 규칙이다.
- 객체의 두 관계중 하나를 연관관계의 주인으로 지정해야 한다.
- 연관관계의 주인만이 외래키를 관리(등록, 수정)한다.
- 주인이 아닌쪽은 읽기만 가능하다.
- 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아니면 mappedBy 속성으로 주인을 지정한다.
외래 키가 있는 곳을 주인으로 정해라. (중요!!!)
ManyToOne(1:N에서 N)인 곳이 주인이 된다. (중요!!!)
- 자동차와 자동차 바퀴가 있으면, 물론 자동차가 비즈니스적으로는 중요하지만, 자동차의 주인은 바퀴! 가 된다고 기억하자.

 

 

 

 

 

 

 

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

 

주인이 아닌 방향에 연관관계 설정한 예시 - 잘못됨

 

역방향(주인이 아닌 방향)만 연관관계 설정을 하면 외래키 값이 null이 된다.  왜?!
Team에 있는 mappedBy된 것은 읽기 전용이다. JPA에서 update할때나 insert할때는 이 객체를 보지 않는다. 즉, JpaMember에 있는 jpateam이 주인인데, 주인이 아닌곳에 값을 넣었기 때문에 null 이 나오는 것이다.
양방향 매핑 시 연관관계의 주인에 값을 입력해야 한다.

 

 

주인에 올바르게 값을 넣은 예시 - 올바름

 

위의 코드에서 member.setJpaTeam(team)까지 하면 사실 JPA입장에선 맞는 코드이다. Team만들고 영속성 컨텍스트에 올리고, 디비에서 가져온 키값을 (1차캐시에 있는) 외래키로 가져와서 setTeam해주면 문제가 없기 때문이다. 그 이후에 getMembers(); 하여 호출하게 되면 JPA는 실제 사용하는 시점이라고 판단하여 Member에 대한 select 쿼리를 또 한번 날려준다.

그런데, 순수한 객체 관계를 고려하면 항상 양쪽 다 값을 입력해야 한다. 왜 그럴까?

 

 

 

 

객체 지향적으로 문제가 있음을 보여주는 예시

 

문제 1 . flush(), clear()가 없으면?

 

-> 1차 캐시에 있는것을 find해준다.  그러면 컬렉션에 값이 없다.. 객체지향적으로 생각했을 때 team, member에 모두 값을 넣어주는게 맞다.

 


문제2. 테스트 케이스 작성할 때도 같은 문제

 

-> 위의 상황과 비슷하게 member는 조회가 되는데 team은 없는 상황이 나올수도 있다.

 

 

 

 

 

 

양방향 연관관계가 있으면 양쪽의 값을 모두 넣어주어야 하는 번거로움을 조금이라도 줄이고자 연관관계 편의 메서드를 사용해도 좋다.

 

 

예제에서 member.setJpateam(team); 하면 set할때 team참조를 저장하여 연관관계를 설정해주었다. 이 때, 나 자신의 인스턴스(this, 여기서는 member객체)를 mappedBy쪽의 컬렉션에 add를 해주는 것이다. 이렇게 하면 set과 동시에 양쪽의 값을 모두 넣어줄 수 있다.

 

 

 

반대로 team에 member를 추가할 때 연관관계 편의 메서드를 사용할 수도 있다.

임의로 addMember 메서드를 만들어서 member.setJpateam(this);로 현재 team객체를 set에 넣고, 컬렉션에 member를 add해주면 양쪽의 값을 모두 넣어줄 수 있다.

 

두 방법중 정답은 없다. 이 예제에서 보면 팀을 만들 때 그 팀에 속한 멤버를 넣어줄 수도 있는 것이고, 멤버를 만들 때 그 멤버가 속할 팀을 정해줄 수도 있는 것이다. 개발할 비즈니스 모델에 맞추어서 만들면 된다.

단, 2가지를 한번에 사용하면 충돌이 일어날 수 있기 때문에 한가지 방법만 사용하자.

 

 

 

 

추가적으로 주의할 것!

- toString(), lombok, JSON생성 라이브러리 를 사용할 때 무한루프를 조심하자.

  -> 양방향 관계인 양쪽 엔티티에 모두 toString()을 Override하여 사용하면 컬렉션이 있는 쪽의 엔티티 안의 값들을 호출하게 되고 서로 무한으로 왔다갔다 호출이 된다. 롬복 라이브러리의 @Data 애노테이션을 사용하면 @ToString 을 포함하고 있어서 자동으로 toString()을 오버라이딩하기 때문에 주의해야 한다.


- JSON생성 라이브러리로 인한 문제 : 컨트롤러에서 entity를 반환하지 말자! 엔티티를 변경하면 API 스펙이 바뀐다. dto로 변환해서 반환하는것을 추천.

'JAVA > Spring JPA' 카테고리의 다른 글

JPA의 프록시와 연관관계 관리  (0) 2022.01.03
상속관계 매핑과 매핑 정보 상속(@MappedSuperclass)  (0) 2021.12.30
다양한 연관관계 매핑  (0) 2021.12.29
엔티티 매핑  (0) 2021.12.25
영속성 컨텍스트  (0) 2021.12.24

+ Recent posts