주저리

이번 장은 엔티티의 상호 관계에 관한 부분이 주요하게 다루어졌다.
일대일, 일대다, 다대일, 다대다.
말로만 들으면 알 거 같으면서도,
실제로 강의에서 이건 일대다에요.
이건 다대일로 할 필요가 없겠네요.

이런 이야기가 자연스럽게 흘러 나오는데
해당 개념이 뭐지...? 하는 당혹스러움을 느꼈었기에
해당 부분을 조금 더 꼼꼼히 살펴 보면서 조금 감을 잡았던 거 같다.

이번 장의 경우 실습이 위주이고 텍스트가 엄청 많은 건 아니다.
적은 것도 아니지만 해당 개념을 나름의 이해한 방향대로
최대한 정리하는 형태가 될 거 같다.
핵심의 개념은

크게 보면 4가지

  • 일대일(@OneToOne)

  • 일대다(@OneToMany)

  • 다대일(@ManyToOne)

  • 다대다(@ManyToMany)
    그리고 방향성 두 가지를 각각이 가지는 경우에 따라서

  • 단방향 : 두 엔티티의 관계에서 한쪽의 엔티티만 참조하는 형식

  • 양방향 : 두 엔티티의 관계에서 각 엔티티가 서로의 엔티티를 참조하는 형식
    위와 같이 2가지가 나뉜다.

위와 같은 내용이 나름 데이터베이스에 자료를 구축하는 부분에서
이번에 새로 주어진 예약 시스템을 구축하는 프로그램 작성 부분에서
요긴하게 쓰일 것으로 생각하고 있다.

두 테이블이 연관관계를 갖게 되면 주종의 관계를 가진다.
외래키를 가지고 있는 테이블이 주인(Owner)가 된다고 한다.
하지만 이 경우 권한이 외래키를 활용해
해당 값을 읽는 형태만 할 수 있다.
즉, 수정의 권한은 없고 말 그대로 참조만 하며

단방향은 한쪽만 참조가 가능한 것.
양방향은 서로가 참조가 가능한 것.

위와 같이 기본적인 부분을 설계하고
간단하게
@JoinColumn 어노테이션을 이야기 할 필요가 있다.
해당 어노테이션을 통해 외래키를 조정할 수 있다.
기본적으로
@Id로 선언된 PK가 위의 값으로 쓰이는데
명시적으로 작성해 외래키를 변경할 수도 있다.

위와 같은 내용을 정리했다면
이제 기본적인 연관관계의 개념만 정리하면 된다!

이 경우는 현실세계를 상상하면서 이야기 할 필요가 있다.

1. 일대일 매핑(@OneToOne)

기본적인 개념 단어로 먼저 상상해 보자. 한 개에 한 개만 매핑이 되는 경우
서로의 값이 유일하게 쓰이는 경우를 말할 것이다.

현재 개발하고 있는 예약 시스템을 예로 들어서 이야기 해 보자면,

예약 시스템의 경우
유저와 예약이라는 두 개의 테이블이 있다.

유저의 아이디를 예약의 외래키로 가지며.
이렇게 되면 이 유저는 하나의 예약만 가능하다는 뜻이 된다.
한 예약을 할 수 있는 경우의 관계가 OneToOne으로 설정되었기 때문이다.

하나의 매핑을 위하고, 기본 외래키를 썼기 때문에 JoinColumn은 명시하지 않아도 될 것이다.

  • 단뱡향의 경우 외래키를 가질 곳에만
    @OneToOne(optional = false)//nullr값을 허용하지 않겠다는 선언
    JoinColumn(name = "user_id")
    private User User;
    //Book에서 user의id를 외래키로 가져오겠다고 하는 경우
  • 양방향의 경우 서로의 테이블에 OneToOne을 붙이게 된다.
    위를 조금 변용해서

@OneToOne(mappedBy = "user") 주종관계 설정 여기서 주는 book, 종이 user가 된다.
@ToString.Exclude//아래에 설명
private Book book; 과 같은 형태를 만들어 주면 된다.
//User에서 BookId를 가지는 경우
여기서 양방햐향의 경우에 중의해야 할 부분이 있는데
ToString이 두 번 발생해 예외가 발생할 수 있다는 것이다.
(물론, ToString이 실제 개발에서 잘 쓰이지는 않아 왔지만,
서적에서 개념적으로 소개하고 있으며, 또 어떤 상황이 있을지 모르니!)

양방향 매핑의 경우 한쪽에서만 값을 가지고 올 수 있게 하기 위해
ToString.Exclude라는 어노테이션을 추가해준다.
mappedBy의 경우, 실제 db에서 한쪽만 값을 가지는 주인으로 명시적으로 선언해 표시한 것이다.

2. 일대다/다대일매핑(@OneTOMany/@ManyToOne)

위의 개념에서는 fetch 전략도 나와서 주요하게 다룰 수 있는 내용이 있다.

일대다와 다대일의 경우는 서로가 가지는 값의 유형에 따라 상대적으로 사용된다.

우선 일대다의 경우 특히 하게도 단방향 매핑만을 서적에서는 소개한다.
그렇게 하는 이유는 일대다의 양방향 매핑은 주인을 설정할 수 없기 때문인데
그 이유는 일대다의 경우
외래키가 선언을 한 테이블이 가지고 오는 게 아니라,
선언 당한 곳이 외래키를 가지기 때문이다.

즉 위의 예시에서
@OneToMany(fetch = FetchType.EAGER) //1,2개 더 보고 뒤에서 한 번에 정리
JoinColumn(name = "book_id")
private List users = new ArrayList<>();이라고 하게 되면
User 테이블에 외래키 book_id가 들어오는 것이다.

이처럼 다른 매핑과 다르게 자신의 테이블에서 다른 테이블에
외래키를 주는 형태기 때문에 양방향 형태에서 주종 관계가 없다.

그렇다면 다대일매핑은 어떨까?

위의 경우는 단방향과 양방향이 있으며 일대일처럼 양방향의 경우 2개를 합쳐주는데,
이 때 두 개를 합치는 방식이
@ManyToOne과 @OneToMany를 사용하는 방식이라는 점이 꽤나 재미있다.
앞서 말했듯이 OneToMany는 상대에게 외래키를 준다는 특징이 있기 때문이다!
반면 @ManyToOne의 경우 일대일처럼 상대방에게 외래키를 준다.

즉 위의 user 와 book의 유형을 예에서
단방향을 쓰고, 아래에 하나를 더 쓰면 양방향이 되는 것이다.

Book 클래스 안에
@ManyToOne
@JoinColumn(name ="user_id")
private User user;
위와 같은 상황은 한 명의 유저가
여러가지의 예약을 할 수 있는 것을 뜻한다.
위와 같이만 작성하면 단방향으로 북에 외래키가 주입된 형태이고

@OneToMany(@mappedBy = "book",fetch = FetchType.EAGER)
privateList bookList = new ArrayList<>();
위와 같이 작성되면 양방향으로 설계가 되는 것인데,
book이 주인이며
심지어 book만이 외래키를 가지는 구조라는 것을
앞선 일대다 매핑의 특징을 통해 이해할 수 있다.

여기서 계속 fetch 부분이 쓰여 왔는데
해당 부분은 Tip에서 다루고 있는 부분을 살펴 보도록 하자.

Tip 지연로딩과 즉시로딩
JPA에서
즉시 로딩(eager loading_FetchType.EAGER)
지연로딩(lazy loading_FetchType.LAZY)
엔티티라는 '객체의 개념'으로 데이터베이스를 구현!
연관관계를 가진 각 엔티티 클래스에는
연관관계가 있는 객체들이 필드에 존재
연관관계와 상관없이
즉각 '해당 엔티티의 값만 조회'하거나 (== 즉시 로딩)
'연관관계를 가진 테이블의 값도 조회' (== 지연 로딩)
등 여러 조건들을 만족하기 위해 등장한 개념
단순 조회만 할 건지, 주변의 값도 함께 조회할 건지의 개념으로
즉시 로딩이 단순 조회에서 성능이 더 우수하다고 한다.

3. 다대다매핑(@ManyToMany)

마지막으로 ManyToMany을 볼 텐데,
실무에서 거의 사용되지 않는 구성이라고 한다.

처음 예약의 개념을 통해 이야기를 해 보자면.
한 명이 예약을 했지만 실제 환경에서는
예약을 한 명이 했지만 해당 예약자가 아닌 일행이
해당 예약을 다루는 경우가 있을 것이다.
가령, 누나의 이름으로 예약했지만, 누나의 일행입니다.
와 같은 형태로 해당 예약을 내가 조정하는 것과 같은 상황이
이에 해당할 수 있겠다.
또한 처음 다대일의 경우처럼 한 명이 여러가지
예약을 하는 경우가 있겠다. 이 경우는
상상하기 어렵지 않을 것이다. 특히
시간표 개념처럼 한 명이
시간표를 여러가지로 분할해서 사용하는 경우가 있듯이.
M:N으로 표현되는 ManyToMany는
깃허브를 활용한 개발에서도 다양하게 쓰이지만
실제로 굉장히 복잡하다는 것을 쉽게 이해할 수 있을 것이다.

또한 앞서 봤던 단방향매핑과 양방향매핑의
특성을 그대로 갖기 때문에
한쪽에만 선언을 하면 단방향
둘 다 선언하면 양방향

이렇게 쉽게 떠올릴 수 있다.
단, 위의 다대일 예시에서 쓰였듯,
단순 객체가 아닌 리스트 형태의 객체를 활용해서만 만들 수 있다. :)

끝으로 영속성 전이에 관한 속성을 알아 보고자 한다!

영속성 전이(@OneToMany cascade = CascadeType.~)

영속성 전이란,
@OneToMany의 값으로 활용되며,
엔티티와 연관된 속성을 바꿀 수 있는 기능입니다.
6가지 기능에 관해 다루고 있는데
아래의 설정표를 통해 어떤 속성들이 있는지 확인해 볼 수 있습니다.

종류설명
ALL모든 영속 상태 변경에 대해 영속성 전이를 적용
PERSIST엔티티 영속화할 때 연관된 엔타ㅣ티도 함께 영속화
MERGE엔티티를 영속성 컨텍스트에 병합할 때 연관된 엔티티도 병합
REMOVE엔티티를 제거할 때 연관된 엔티티도 제거
REFRESH엔티티를 새로고침할 때 연관된 엔티티도 새로고침
DETACH엔티티 영속성 컨택스트에서 제외하면 연관된 엔티티도 제외

위의 표에서 처음 보고 이해가 안 되는 건 영속 상태란 무엇인가였습니다.
쉽게 말하면 저장이라고 볼 수 있는데 굳이 이렇게까지 부르는 이유는
구분하기 위해서라고만 일단은 생각합니다... save가 있으니까..?
쉽게 말해서 저장 또는 삭제의 범위라고 생각합니다.
데이터를 업데이트 할 때, 새로 저장할 때 어떤 범위까지 저장할 꺼냐,
어느 범위까지 삭제할 거냐,
어느 범위만큼 업데이트 할 꺼냐. 이런 개념으로 보면 될 것 같습니다.
(잘못 이해하고 있다면 알려주시면 감사히 내용을 수렴하겠습니다.)

관련해서 고아객체라고 엔티티와 관계가 끊어진 엔티티가 발생하는데
이를 JPA에서는 자동으로 제거하는 기능이 있어 크게 신경 쓸 건 없지만,
주종 관계가 형성되어 있는 경우라면
이러한 설정값 즉, 삭제가 발생하지 않도록 하라는 것이 서적,
저자의 권장 사항입니다. :)
해당 고아 객체의 삭제도 명시적으로 아래와 같은 형식으로 작성하면 됩니다.

@OneToMany(@mappedBy = "book",cascade = CascadeType.PERSIST, orphanRemoval =true)
JoinColumn(name = "book_id")
private List users = new ArrayList<>();

orphanRemoval =true 왼쪽의 형태를 추가해 주면
고아 엔티티를 삭제할 수 있습니다. :)

이로써 연관관계 설정과/영속성전이에 대해서
최대한 제 기준으로 짧고 간결하게 정리해 보았습니다!

profile
하루 하루 즐겁게

2개의 댓글

comment-user-thumbnail
2023년 4월 8일

👍👍👍👍👍
"단방향 : 두 엔티티의 관계ㅒ에서 한쪽의 엔티티만 참조하는 형식" 해당부분에 typo 가 있네요.

1개의 답글