[강의 정리] 연관관계 매핑 기초 - 양방향

나무·2023년 12월 1일

JPA 

목록 보기
5/11
post-thumbnail

객체 관계 매핑 - 양방향★★★

여지껏 단방향만 다뤄 보았으니 이번엔 양방향매핑을 살펴보자.

양방향 관계

연관관계의 경우 앞에서도 설명했지만,

테이블은 양방향,
객체는 단방향

이라고 분류를 했다. 그리고 그 뒤에 객체도 각 엔티티 객체가 서로를 참조하게끔 하여 양방향 관계 "인것" 처럼 설계를 할 수 있다고도 설명을 했다.

그럼 먼저 양방향 관계처럼 보이도록 한번 엔티티를 조금 수정해보자!

엔티티 수정 (@OneToMany, mappedBy)

Team 엔티티

이전까지는 단방향 관계였기에 Team 은 멤버를 향해 객체 그래프 탐색을 할 수가 없었다.

하지만 위와 같이 Member 에 대한 정보를 필드에 추가함으로써 이제 탐색이 가능해졌다.

@OneToMany

여기서 조금 다른 점은 참조 변수의 타입이 List 타입이라는것과 어노테이션이 @OneToMany 라는 것이다. 이는 위에서도 언급했듯이

Member : Team = 1:N 관계

이기 때문에 위와 같이 매핑을 해주었다.

mappedBy

이게 양방향관계의 핵심 내용이며 바로 뒤에 설명할 연관관계 주인 과 밀접한 관계를 가지고 있는 키워드이다.

우선은 이렇게 엔티티를 설계 해주자.

객체 탐색 테스트

이제 양방향 매핑을 완료했으니 한번 테스트를 해볼차례다.

맨유에 소속한 모든 선수들 이름을 출력해보자.

우선 DB의 데이터를 위와 같이 세팅해놓은 상태에서,

team 을 조회해보면

다음과 같이 출력되는 것을 확인할 수 있다.

우리는 조회 코드에서 SQL 혹은 JPQL 작성없이 오직 순수 자바코드만 으로 맨유에 소속된 모든 선수들을 출력할 수 있었다.

만일 @ManyToOne 단방향 관계였다면 우리는

TypedQuery<Member> query = entityManager.createQuery(
    "SELECT m FROM Member m WHERE m.team.id = :teamId", Member.class);
query.setParameter("teamId", teamId);
List<Member> teamMembers = query.getResultList();

대충 이런식의 JPQL 코드를 작성했어야만 했을 것이다.


우린 객체를 양방향 관계로 설정함으로써 보다 편리하게 데이터를 조회 할 수 있다는것을 확인해 보았다.

이제 mappedBy 떡밥을 회수할 차례이다.

연관관계 주인

두 엔티티 간의 양방향 연관관계에서 외래 키(Foreign Key)를 관리하는 엔티티를 연관관계의 주인이라고 한다.

즉, 실제 테이블 상에서 FK 를 지니고 있는 테이블의 엔티티가 주인 엔티티가 된다는 것이다.
MEMBERTEAM의 ERD를 살펴보면 다음과 같다.

FK는 MEMBER 테이블이 가지고 있기 때문에 두 엔티티간의 연관관계 주인은 Member 엔티티 가 된다.

그럼 이 연관관계 주인은 도대체 왜 정하는걸가? 그림을 통해 확인해보자.

※ 편의를 위해 '다대일' 이 아닌 '일대일' 관계로 예시를 들겠다.

두 엔티티는 현재 서로를 참조하고 있다. 그 말은 즉, 멤버는 팀의 값을, 팀은 멤버의 값을 읽을 수 있으며 심지어는 값을 바꿀 수도 있다.

위 그림과 같이,

Member 엔티티에는 선수명을 "손흥민" 으로 수정되어있고
Team 엔티티에는 선수명을 "황희찬" 수정하였다.

문제는 이 두 엔티티들이 이 상태로 영속성에 들어간 다음 커밋이 되버린다면 데이터의 일관성이 깨지게 되버린다. 두 엔티티가 모두 서로의 값을 바꾸고 수정할 수 있는 권한이 있기에 이런 문제가 발생할 수 있는것이다. (읽기는 상관 없음)

즉, 데이터를 수정할 수 있는 권한, 연관 관계의 주인은 오직 한 객체만이 쥐고 있어야한다.

그래서 우리는 연관관계의 주인을 지정하고자 mappedBy 를 사용한것이다.

mappedBy

그럼 mappedBy 는 언제 어디에 써야할까?

위에 작성했던 Team 엔티티를 다시 확인해보자.

우리는 양방향 관계 설정 당시 Team 엔티티mappedBy 를 걸어주었다.

그럼 "team" 은 도대체 뭐냐?

바로 Member 엔티티 의 참조 변수의 이름이다.

즉, mappedBy = "OOO"주인이 아닌 엔티티에 걸어주는 것이며

'주인이 참조하고 있는 OOO 변수에 제가 매핑되어 있습니다~'

를 말해주고 있는것이다.

정리 하자면

  • 연관관계의 주인은 외래키를 관리 하고 데이터베이스 연관관계와 매핑된다.

  • 연관관계 주인만이 외래키(참조 객체)를 변경할 수 있다. (읽기는 모두 가능)

  • mappedBy = "OOO" 는 주인이 아닌 엔티티에 걸어주는 속성이며 속성값은 주인 엔티티의 참조 변수 명이다.

  • ※ [참고] 다대일 관계에서는 무조건 웬만하면 '다' 가 외래 키를 가진다. 그래서 @ManyToOne 에는 애초에 mappedBy 속성이 없다.

결국 이것도 패러다임의 불일치로 발생하는 문제이며 JPA에서는 이를 해결하기위해 연관관계 주인 이라는 개념을 도입한 것이다.

양방향 연관관계 저장

주인 엔티티에 저장

바르샤와 선수 3명 (메시, 사비, 페드로) 가 모두 저장이 되었다.

비 주인 엔티티에 저장

각 엔티티는 테이블에 저장이 되었지만 연관관계가 설정 안되어있는걸 확인할 수 있다.

주인 엔티티로 수정

Member 엔티티를 DB에서 조회한다음 참조하고 있는 Team 을 통해 이름을 "psg" 로 변경해주었다.

나머지 "삭제" 는 여러분이 직접 해보시기 바란다. 꼭 해보시길!! (스샷 찍기 힘들어유 ㅠㅜ)

[주의]

꼭 주인 엔티티를 통해 저장,수정,삭제를 해야한다!!! 많은 개발자들이 이 부분에서 실수를 저지른다.

순수 객체의 상태도 고려 하자!

데이터를 저장하는 작업은 주인 엔티티 만이 가능하다고 하였다. 그럼 정말로 진짜 주인 엔티티 객체에만 값을 저장하면 땡일까?

답은 "아니오" 이다.

물론 앞에서 보았다시피 주인 객체에만 값을 저장을 했어도 DB에는 잘 반영이 된다.

그런데 만일 테스트코드를 작성한다면?

팀과 선수를 저장하는 코드를 다시 한번 살펴보자.

주인엔티티는 Member 이기때문에 각 멤버 객체들(messi, xavi, pedro) 에는 팀을 할당해주었지만 barca 객체에는 그 어떤 멤버들도 저장되어있지않다.

이 코드를 JPA 를 사용하지 않는 환경에서 테스트 코드를 작성하고 실행한다고 가정해보자. 당연히 치명적인 오류가 발생하게 된다.

messi, xavi, pedro 객체에는 각각 모두 barca 가 할당이 되어 있지만,

barca 는 그 어떤 선수들도 가지지 않고 있기 때문에 순수 객체 끼리의 연관관계는 맺어지지 않게 되므로 테스트에서 실패하게된다.

즉,

List<Member> members = List.of(messi, xavi, pedro);
barca.setMembers(members);

이렇게 barca 에도 멤버들을 전부 추가해놔야 테이블 관점에서도, 객체 관점에서도 완전한 양방향 연관관계를 맺을 수 있는것이다.

연관관계 편의 메소드 (리팩토링!!!)

결국 양방향 연관관계의 경우 양쪽 객체들을 전부 다 신경 써줘야한다. 그런데 너무 복잡하지 않은가?

결국 두 객체를 연관 맺어주는 코드를 둘다 작성해야하는데 여간 번거로운게 아니며 자칫 까먹고 한쪽을 안맺어줄 수도 있다.

한번 리팩토링을 해보자.

Member 엔티티

...
	// 연관관계 설정
    public void setTeam(Team team) {
        if (this.team != null) {// 이미 연관관계를 맺은 팀이 있다면
            this.team.getMembers().remove(this); // 해당 팀으로 가서 현재 멤버(this)를 제거
        }
        this.team = team; // 연관관계 맺기 1
        team.getMembers().add(this); // 연관관계 맺기 2
    }
    ...

이제 연관관계를 맺을때 주인 엔티티 (Member) 에서 setTeam() 만 한번 호출하면 양쪽 객체가 모두 연관관계를 맺게 된다.


정리

양방향 관계를 맺는것은 분명 장점이 존재한다. 하지만 그 장점을 누리기 위해선 굉장히 많은것들을 신경써줘야하는 둥 유지보수가 어려워진다.

실제 프로젝트는 테이블 수십개가 우습게 넘어간다. 그리고 하나의 테이블이 여러개의 테이블과 또 연관관계를 맺는데 이때 모든 연관관계를 양방향으로 맺는다고 생각해보자.

복잡성이 엄청 증가하게되며 신경쓸것들이 굉장히 많아진다. 사실 연관관계는 단방향 매핑으로 이미 끝이 난것이다. 양방향 관계는 오직 객체 그래프 탐색 기능이 추가가 된것이다.

그러기에 미리 양방향관계를 설정하기 보단

1) 우선 모든 객체 설계 단계에서는 관계를 모두 단방향으로 설정한다.

2) 그리고 추후에 어플리케이션을 개발에 돌입하면서 양방향관계가 굉장히 필요한 경우에 관계를 맺어주는걸 추천한다.


+ 순환참조 주의

양방향 연관관계의 경우 정말 무시무시한 에러가 발생할 수 있는데 바로 순환참조이다.

toString(),
JSON 변환 라이브러리

를 사용할 경우 순환참조가 발생할 수 있다.

toString(), JSON 직렬화

롬복을 사용하면 toString() 이 자동 생성되는 경우가 많은데 이 때 , 서로가 참조를 하고 있기 때문에 무한한 순환 참조가 발생하게된다.

member의 toString()

public String toString(){
	return 
	"id="+id+"," +
    "name="+name+"," +
    "team.id=" + team.id +"," +
    "team.name=" + team.name +"," +
    "team.member.id=" + team.member.id +"," +
    "team.member.name=" + team.member.name +"," +
    "team.member.team.id=" + team.member.team.id +"," + ,,,,,
}

JSON 직렬화도 위와 비슷한 원리로 무한 순환참조가 발생하게된다.


++ 엔티티반환 금지

컨트롤러에서 엔티티 전체를 반환해서는 안 된다.

예를들어 findMember() 와 같은 컨트롤러 메서드를 정의할 때 절대로 반환타입이 Member 여서는 안된다.

필요한 데이터만을 따로 옮겨담는 DTO 객체를 활용해서

Member -> MemberDTO or MemberResponse

로 옮겨 담은 다음에 이 DTO 를 반환하도록 해야한다.

왜냐하면, 이 또한 순환참조를 발생 시킬 수 있으며 뿐 만아니라 엔티티가 화면에 굉장히 의존적이게 된다.

본 포스트는
김영한의 자바 ORM 표준 JPA프로그래밍 기본 강의 및 도서를 참고하여 정리했습니다.

profile
🍀 개발을 통해 지속 가능한 미래를 만드는데 기여하고 싶습니다 🍀

0개의 댓글