기존에 데이터베이스에서 A테이블과 B테이블이 연관관계를 맺는다면 FK를 사용해서 이루어졌다.
JPA를 사용하면서도 마찬가지의 방식으로 동작하게 할 수 있다.
그러나 그게 JPA를 제대로 사용하는 것이라고 말하긴 힘들 것이다.
JPA의 여러 특징과 효율을 가장 높은 수준으로 뽑아내고 있는 건 아니기 때문이다.
매핑
위에서 얘기한 것처럼, DB는 외래키를 통해 관계를 맺는다고 표현할 수 있다.
매핑이라는 건 관계를 맺는다는 표현이므로, 결국 DB는 FK를 가지고 매핑한다.
그러나 객체는 다르다.
객체는 참조를 통해서 매핑한다.
별 거 아니라는 느낌이 들지만, 이게 막상 해보면 생각보다 어렵다.
방향
A에서 B로 향할지 그 방향도 정해야하는데, DB는 기본적으로 양방향이고 객체는 단방향이라서 한 쪽을 정해야한다.
어떤 방법을 활용해서 양방향처럼 활용할 수도 있는데 중요한건 객체는 Default가 양방향이 아니기 때문에 연관관계의 주인을 정해야한다.
다중성
1:N 또는 1:1 과 같은 관계를 말하는 용어다.
내가 하는 사이드 프로젝트에서는 책이 있고 그 안에 챕터를 각각의 스토리로 구분하는데, 이 경우 하나의 책이 여러 스토리를 가질 수 있어서 책:스토리 == 1:N 의 구조를 갖는다.
예전에 책으로 볼 때는 Team-Member 테이블로 설명을 읽고, 들었던 기억이 있는데 지금은 내가 하는 사이드 프젝의 테이블을 가지고 얘기해보려고 한다.
Book 과 Story 는 1:N 관계로 FK는 Story가 가지고 있다.
테이블 구조는 위와 비슷하게 구현되어 있다.
테이블에서는 간단히 book_id 라는 FK를 가지고 양방향으로 소통, 즉 쿼리할 수 있다.
SELECT b.title FROM Book b JOIN Story s ON b.id = s.book_id
SELECT b.title FROM Story s JOIN Book b ON s.book_id = b.id
구분을 위해 class를 붙여줬다.
보다시피 Book의 id를 갖고 있는게 아니라 book를 가지고 있다.
즉, 객체지향에서의 참조를 하고 있는 것이다.
만약 코드를 이렇게 작성한다면 에러가 난다.
Story.class
{
Long id;
Book book;
...
}
객체지향으로 해야한다고 해서 Long 타입으로 Book의 id를 가지고 오지 않고
Book 객체 자체를 타입으로 가져왔는데 왜 에러가 나지? 싶을 수 있다.
에러 코드를 읽어보면 Collection... 이라는 말이 등장한다.
조금 더 이전으로 돌아가보면 의아한 점이 있다.
객체라서 참조를 해야되는데 참조는 단방향이면,, 어떡하지?
단방향으로만 쿼리를 할 수 있다면 너무 좋겠지만, 하다보면 그럴 수 없는 경우도 있었다.
그래서 대부분 양방향이 필요한데 이걸 셀프로 하면 코드가 괴상해진다.
양방향처럼 보이게만드는 행위에 그치기 때문에, 양쪽에서 참조하도록 해서 단방향 2개를 가진 형태가 되기 때문이다.
양방향을 좀 더 수려하게 가능하도록 하려면 JPA를 제대로 써서 JPA에서 제공하는 어노테이션을 추가해야하고
컬렉션을 사용해야 한다.
먼저 Story 테이블에 Book 타입의 필드를 넣어서 에러가 나는 건 당연한거다.
실제 테이블에는 Long 타입의 book_id를 가지고 있기 때문에 타입이 다르니 말이다.
이를 해결(매핑)하기 위해 어노테이션을 활용한다.
@Entity
public class Book {
@Id
private Long id;
private String title;
// 그외 컬럼 생략..
}
@Entity
public class Story {
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "book_id")
private Book book;
// 그외 컬럼 생략..
}
@JoinColumn() 을 사용해서 실제 테이블상에 어떤 컬럼과 매핑할 것인지 지정한다.
사실 이 어노테이션은 생략할 수 있다.
하지만 나는 오히려 있는게 가독성에 좋다고 생각해서 써주고 있다.
아무튼 양방향은 필요하니까 억지로 만든거 말고 JPA답게 양방향을 만들어보자.
위에서 Collection.. 어쩌구 하면서 에러가 뜬다고 말했었는데 그게 바로 여기서 나온다.
객체니까 양방향이 될려면 간단히 생각해서 1:N 이라면 1을 가지고 있는 곳에는 그 객체 1개만 적어주고
N을 가지고 있는 곳에는 LIST 로 객체 N개를 가지면 된다.
@Entity
public class Book {
@Id
private Long id;
private String title;
// 아래는 추가된 컬럼
@OneToMany(mappedBy = "book")
private List<Story> stories;
}
@Entity
public class Story {
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "book_id")
private Book book;
// 그외 컬럼 생략..
}
@ManyToOne 과 @OneToMany 모두 연관관계에서 다중성을 정해주는 역할을 한다.
따라서 반드시 있어야할 어노테이션이다.
파라미터로 여러가지 조건을 줄 수 있는데 예를 들면 페치 타입이나 cascade 같은 것들이다.
이 어노테이션들이 결국 방향에 따른 참조를 모두 관리하고 있다고 보면 된다.
실제로 테이블에는 List와 매핑되는 컬럼이 없다.
그저 객체상에서 쓰이는 애들일 뿐이다.
그리고 하나 더, MappedBy 도 중요하다.
연관관계의 주인을 정해주는 파라미터인데 왜 사용해야 하는지, 그리고 어떻게 정해야하는지, 이 2개를 알아야한다.
왜 사용하는가
지금 JPA를 사용해서 양방향으로 만들었다고 하지만, 사실 억지로 단방향 2개를 끼워 준 것임에는 달라진 점이 없다.
왜냐면 어차피 객체는 한쪽으로 밖에 참조할 수 없는 한계가 존재하기 때문이다.
다만 JPA의 어노테이션 덕분에 양방향처럼 보일 뿐이다.
그러면 구체적인 상황에서는 문제가 발생할 수 밖에 없다.
예를 들어 값을 변경해야하는 경우 어느쪽에서 참조해 들어가 값을 건드려야 하는지?
실제 디비는 어차피 양방향이라서 상관없지만, 객체는 단방향 2개로 이루어졌으니까 한쪽을 건드렸다가 한쪽에서는 업데이트가 안된다면 서로 가진 데이터의 무결성이 깨진다.
이런 이유 때문에 어느쪽을 주인으로 할지 정해줘야한다.
어떻게 정해주는가
이건 사실 선택의 문제라고 생각한다.
나도 많은 경험을 가진 개발자가 아니지만 교재에서 추천하는 방식인 FK가 있는 곳에 넣어두는 걸 추천한다.
그 이유는 간단하다.
실제로 사이드 프젝을 하면서 쿼리를 던질때마다 그게 옳았기 때문이다. 즉, 실제 경험에서 느낀 답이다.
그리고 사용방법은 주인-노예 관계로 표현하면 노예 쪽에다가 주인에서 갖고 있는 FK 대상의 필드 명을 새겨준다.
여기서는 Story가 주인이었으니까 Book 쪽에서 MappedBy를 하고 주인의 book 이라는 필드명을 써준 것이다.
특징
그럼 연관관계의 주인이 되면 객체 상에서 뭐가 달라지는지 알아야 할 것이다.
권한이 달라진다고 할 수 있겠다.
주인은 외래키를 관리할 수 있게 되면서 모든 역할을 수행할 수 있지만, 반대의 입장에서는 읽기만 가능하다.
한번 더 강조하지만, 어차피 단방향 2개로 이루어졌기 때문에 양쪽에서 데이터를 변경하는 일에 민감하게 생각해야한다.
그래서 관계의 주인이라고 해서 그 쪽만 신경쓸 게 아니라 반대쪽도 로직이 필요하다.
그런데 이 로직이 자주 바뀌고 비즈니스 로직마다 매번 들어간다면 문제가 생길 수 있다.
때문에 JPA에서는 관계가 객체라는 것을 고려해 양방향으로 사용할 때 데이터 변경에 대한 견고한 로직을 필요로 한다.
이렇게 견고한 로직을 각각의 Entity 가 갖고 있어주면 편하다.
예를 들어
public void setBook(Book book){
this.book = book;
book.getStories.add(this);
}
이런식으로 book 데이터를 건드릴 때는 반대쪽도 자연스럽게 추가면 추가, 삭제면 삭제, 같은 행위가 일어날 수 있도록 해주는 것이 좋다.