실험일지 1 - JPA (양방향 연관관계, mappedBy, JoinColumn, ToString)
JPA와 연관관계의 특징에 대해서는. 책과 강의, 그리고 프로젝트를통해서 많이 공부할 수 있었다.
하지만, 모두 정답만, 모두 알려진 방법만 따라서 그 형식식대로만 코드를 작성하지 않았나 싶은 생각이 들었다.
그래서 틀린 내용을 보아도 왜 틀린건지 그 이유를 가져오는 능력이 부족한 것 같아 그런 노력의 일환으로 실험일지를 시작하게 되었다.
지속적으로 궁금한 내용이 생길 때마다 테스트해보는 식으로 글이 업데이트 될 것 같다.
첫 실험일지로는 연관관계의 주인이 뒤바뀐 경우를 생각해보는 연습을 해보려고 한다.
양방향 연관관계
양방향 연관관계의 경우 항상 말하는 내용이 있다.
@ManyToOne, @OneToMany 로 이루어져 있고, 단방향인 관계가 2개로 구성되어 있음
객체 관계에서는 항상 이 점을 유의해야 한다. 데이터베이스 관점에서는 외래키로 양방향 탐색이 가능하다.
이 방향이 중요한 이유는
1. 어디서 어디로 정보를 탐색하는가.(R)
2. 어디서 어디로 정보를 수정하는가(CUD)
두가지로 정의할 수 있다. 가령 Team에서 Player만을 조회하는 요구사항만 있다면, 단방향으로 조회하고, Player에서도 Team을 조회해야 한다면 양방향 연관관계가 필요하다. 객체 그래프 탐색을 위해서라면 그런 관계가 필연적이다.
언제든 필요에 따라 요구사항에 따라 개발은 달라질 수 있다.
그러면 우리는 다음으로 생각해야 하는 내용은 객체와 엔티티 간의 차이다.
JPA를 사용하는 이유는 객체지향과 데이터베이스 간의 패러다임의 격차를 해소하기 위해 존재한다.
하지만 차이를 극복하는 과정에서는 많은 내용을 조심해야 한다.
앞서 설명한 내용처럼 JPA는 외래키처럼 두 객체중 하나를 정해 테이블처럼 외래키를 관리해야만 한다.
그러면, 연관관계의 주인이 되는 쪽이 Member라는 사실은 책에도 많이 나와있으니 넘어가보자.
오히려 반대쪽에 있는 경우를 살펴보자.
OneToMany 쪽에(Team) mappedBy 속성을 사용하고 다음과 같이 테스트코드를 작성해보자.
결과
오른쪽의 Player 테이블 결과를 보면, 외래키를 관리하는 쪽이 null인 것을 볼 수 있다.
왜냐하면 외래키는 Player에게 있다. 그리고 연관관계의 주인이다.
Player가 외래키를 관리하는 건 자기 자신의 테이블에서 CUD를 할 수 있음을 의미한다.
그런데 Team의 add 메소드를 보면 team에서는 this.players.add메소드로 team -> player 관계에 대해서는 설정을 했다.
하지만 player -> team 관계에 대해서는 정보를 반영할 수 있는 부분이 add메소드에서 전혀 찾아볼 수 없었다.
mappedBy된 대상은 내가 선언하지 않은 "또다른 물리적 엔티티"가 연관관계의 주인이 된다는 것을 의미한다.
결국 객체에서 관계의 주인인 Player쪽 객체에 team 관계가 설정되지 않아 데이터베이스에 반영되는 시점에서 team_id가 반영이 되지 않은 문제가 발생한 것이다. 해결은 쉽다. player.setTeam(this) ; 를 team의 add메소드에 추가하면 된다.
그러니 꼭 알아야 할 점은 player -> team, team -> player로 객체의 상태를 모두 반영해주고, 데이터베이스에 저장해야 패러다임의 차이를 해결할 수 있다.
Hibernate의 Mapping Table 자동 생성
테스트를 해보다가 신기한 내용을 발견하게 되었다.
Hibernate:
create table team_players (
players_id integer not null,
team_id integer not null,
primary key (players_id, team_id)
) engine=InnoDB
mappedBy키워드를 OneToMany에서 사용하지 않은 경우 다음과같이 mapping테이블을 생성하는 쿼리가 HIbernate에서 날아가게 된다.
1. 이는 Hibernate의 특징으로 JPA 양방향 연관관계의 경우 자동으로 매핑테이블을 생성해준다. 생성을 원하지 않는 경우, mappedBy키워드를 사용한다.
2. 그리고 OneToMany의 경우 default가 LazyLoading이다. (ManyToOne의 경우 default가 EagerLoading이다)
이 기능만 사용하게 되면 언뜻봐서 유용할지도 모르겠지만, 개발자 입장에서는 예기치 못한 기능의 동작일 수 있으므로, 매핑테이블을 직접 구현하거나, 발생하지 않도록 처리를 해야 한다.
vs JoinColumn
기본적으로 mappedBy는 연관관계가 아니라는 것을 보여주고, 연결되어 있는 반대편이 연관관계의 주인이라고 선언한다.
테스트를 통해 얻은 결과는 다음과 같다.
테스트 조건 : OneToMany, ManyToOne 연관관계로 설정됨(위와 동일)
mappedBy O , JoinColumn O : 정상 저장
mappedBy O , JoinColumn X : 정상 저장
mappedBy X , JoinColumn O : 정상 저장 but 매핑테이블 생성(hibernate) => mapping테이블에는 정보가 없음
mappedBy X , JoinColumn X : 정상 저장 but 매핑테이블 생성(hibernate) => mapping테이블에는 정보가 없음
결과
1. mappedBy키워드가 없어도 연관관계의 주인은 설정할 수 있다.
2. JoinColumn은 생략되어도 정보는 정상 저장된다.
3. 다만, 둘다 없는 경우 hibernate에서 mapping테이블을 생성하기 때문에 이 점을 유의해서 사용해야 한다.
referencedColumnName
조인의 대상이 되는 '테이블의 필드' 이름을 직접 지정한다. => JoinColumn 생략 불가.
JPA와 ToString()
JPA에선 연관관계를 고려할 때 toString을 주의해야 한다.
양방향 연관관계이고, toString을 재정의한 경우
team에서 player를 호출하고, player에서 team을 호출하는 상호호출 관계가 무한으로 반복되어 문제가 발생할 수 있다.
Collection의 toString()을 재정의한 경우
osiv가 false이면, 트랜잭션 범위가 끝날 때. 세션이 종료되기 떄문에 지연로딩한 엔티티나 컬렉션에 접근할 수 없다. 따라서 toString에서 컬렉션을 참조하면 컬렉션의 내용을 로딩하려고 할 때 LazyInitialization이 발생한다.
이는 osiv가 true인 경우에도 테스트 코드에서는 @transactional 범위 밖이라면 발생할 수 있으니 유의해서 사용해야 한다.