이번 글은 막상 JPA를 통해 상관관계를 정의하려고 할 때 바로 바로 생각이 안나서 정리하는 글입니다 ㅎㅎ
DataBase에서는 우리는 Foreign key를 통해서 테이블 관의 연관 관계를 정의 합니다. 그렇다면 JPA에서는 어떻게 정의하고 동작하는지 짚어보고 갑시다!!
어노테이션이 뜻하는 것처럼 1:1 관계를 뜻하며 주관계와 부관계로 이뤄집니다. 예를 들면, User 한명당 프로필, Locker, 마이페이지 정보 등이 있을 수 있습니다.
필자는 @OneToOne의 어노테이션을 써본 적이 없어서 이것을 왜 관계를 맺는지 의문을 가지게 되었다...
그냥 주 관계 테이블의 컬럼으로 추가하면 되는 거 아닌가???
- 물론 Table의 복잡하지 않다면 사실 테이블 컬럼으로 귀속 시켜버리는게 나을 것이다. 쿼리를 짤 때 복잡하지 않고, N+1 문제를 고려하지 않아도 괜찮고!!
- 하지만 Table의 스키마가 복잡해지거나, Table의 스키마가 변경 될 수 있는 점, 향후 확장성을 고려한다면 어느 경우에는 분리해버리는 것이 옳을 수 도 있습니다!!
아마 아직 큰 서비스를 개발해 본 경험이 없어서인가...ㅎㅎ
이제 @OneToOne을 어떻게 쓰는지 살펴 봅시다.
@OneToOne @JoinColumn(name = "post_id") private Post post;
위의 코드는 Comment가 Post와 1대1 매칭일 때 외래키를 설정해주는 컬럼이라 생각하시면 됩니다. @OneToOne을 선언하고 Post의 post_id를 외래키로 참조하겠다고 선언한 것이라고 생각하시면 편합니다!!
여기서 끝나면 좋겠지만...
JPA는 RDBMS가 아니기에 관계형 DB처럼 관계를 알아서 갖지 않습니다. Entity마다 각 객체이기에 우리는 이 객체간의 관계를 좀 더 명확히 정의해줘야합니다!!!
저렇게만 하면 단방향 관계를 갖게 됩니다!!
- Post에서 Comment를 참조할 수 있지만, Comment는 Post를 참조할 수 없습니다.
- 그래서 우리는 주관계를 갖는 엔티티에서 Mapped By를 선언해줘야합니다.
아래는 예시 코드 입니다.@OneToOne(mappedBy = "post", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE) @OrderBy("created_at asc") private List<Comment> commentList;
- 주엔티티에서 mappedBy = "자신의 테이블"를 선언하면 주엔티티가 되며,양방향의 관계를 가지게 됩니다. 이때부터 Comment도 Post를 참조할 수 있게 됩니다.
@OneToOne보다 저희가 DB설계 및 엔티티끼리의 관계를 정의 할때 제일 많이 쓰는 구조입니다.
어노테이션 그대로 해석하면, 하나에 많은 것들의 관계를 맺을 것이죠!!
위에 예시를 1대1을 댓글과 Post로 들어서 혼돈 하지마시고!!!
원래 대부분 게시글 하나에 많은 댓글들이 달립니다. 그리고 하나의 팀에 여러 선수가 존재하듯이!!
그러면 이번에도 코드를 보면서 이해하러 가시죠!!
@OneToMany(mappedBy = "post", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE) @OrderBy("created_at asc") private List<Comment> commentList;
아까 @OneToOne이랑 매우 유사합니다!!
- 여기서 차이점은 @OneToOne은 둘중의 누구든 주관계를 가질 수 있었습니다. 개발자가 선택한다면!
- 하지만 @OneToMany는 One인 Entity가 주관계를 가지게 됩니다!!
@OneToMany의 반대를 뜻하며 @OneToMany와 짝지어서 씁니다!
이번에도 코드를 보시죠!
@ManyToOne @JoinColumn(name = "post_id") private Post post;
@OneToOne에서 부관계에 있는 어노테이션과 일치합니다!
JPA에서는 RDBMS가 아니기에 객체관의 연관관계를 정의해줘야합니다!!
주인과 그에 종속되는 부관계로 나눌 수 있습니다.
주인 엔티티는 @OneToMany,@OneToOne에 mappedBy를 통해 자기 자신이 주인임을 명시합니다.
종속되는 부 관계는 @ManyToOne @OneToOne이면 @JoinColumn을 통해 주인의 특정 컬럼을 외래키로 참조합니다!!
상관관계를 정의하다보면 우리는 꼭 Fetch Type을 정의 해줍니다.Cascade도 같이 등장하고요.
필자는 그래서 이 둘에 대해 정리를 하고 글을 마치고자 합니다.
하지만!!이전에 우리는 꼭 이해하고 넘어가야한 JPA Proxy가 있기에 JPA Proxy로 이번 문단을 시작하고자 합니다 ㅎㅎ
프록시라는 말은 자주 들어봤을 것입니다. 대신 해주는 객체? 이런식으로 이해하고 계실 껍니다. JPA에서도 Proxy를 쓰는데 언제 왜 쓰는지 살펴 봅시다.
우리는 여태까지 상관관계를 정리했습니다,근데 만약 주인관계를 가진 Entity를 조회할 때마다, 종속관계인 Entity를 조회를 같이 해야한다????
결론
- JPA는 이런 불필요한 쿼리를 생성하는 것을 방지하고자 Proxy를 이용합니다. Proxy는 여기서 가짜 객체를 의미하게 됩니다. 즉 만약 Post를 조회할 때 Comment의 엔티티를 Proxy를 통해 가짜객체로 둡니다. 클래스 형태만 같을 뿐 실제 데이터는 들어 있지 않는 것이죠!!
- 실제로 그래서 Post 조회쿼리를 날려도 Comment의 조회 쿼리는 같이 나가지 않습니다.
- 실제 Comment를 조회하는 쿼리를 날릴때 날라가게끔 만들어버리는 것이죠!!
- 근데 여기서 또 의문이 생기죠 proxy는 어떻게 가짜 엔티티 역할을 하는 것이지??
해답 : Proxy는 실제 Entity를 상속받기 때문입니다. 그래서 우리는 Entity를 선언할 때 private로 선언하지 못하는 것입니다!!!!- 참고로 이 Proxy는 Entity객체가 생성될 때 같이 생성되며, Entity에 대한 참조값을 가지고 있습니다.
- 그리고 초기화는 Proxy가 실제 객체의 메서드를 호출할 필요가 있을 때 데이터베이스를 조회해서 참조 값을 채우게 되는데요, 이를 프록시 객체를 초기화한다고 합니다.
그림으로 보며 한번 더 이해하고 넘어가시길 바랍니다!!
이제 드디어 Fetch Type으로 넘어오셨습니다!!
Fetch Type에는 Eager와 Lazy이 두 개가 존재합니다. 단어 뜻 대로 해석하시면 불러오는 시점을 정해주는 것 입니다!
의문점
- 그러면 둘중에 무엇을 써야할지 어떻게 판단해야할까?
해답 : Eager는 쿼리가 동시에 두 개가 날리기 때문에 Lazy보다 성능이 떨어집니다. 하지만 관련이 깊거나 자주 사용할 Entity라면 Eager가 더 옳을 수 도 있습니다.
☠️ 하지만 Lazy를 쓰실 때 우리는 주의해야합니다!!
Lazy는 JPA Proxy를 활용합니다. 그런데 가짜 객체인 Proxy를 고치거나 조회하는 쿼리를 받게 되면 Error가 발생합니다!!!
💩 Eager는 문제가 없을까?
Eager는 JPA의 단골 문제인 N+1의 문제가 발생하게 됩니다. 그래서 실무에서는 Eager를 많이 안쓴다고 하죠ㅎㅎ N+1에 대한 글은 곧 따로 올려보겠습니다 ㅎㅎ필자도 공부하면서 복습해야하니까여!!
우리는 상관관계를 정의하면선 JPA는 주인과 종속 관계를 가진다고 했습니다. 관계가 생기면 우리는 신경써줘야할 것이 많습니다!!
즉 부모의 영속성 상태를 전이 해줄 것인지 말 것인지 정하는 것입니다.
이런 문제를 해결하기 위해 JPA는 Cascade Option을 제공합니다!!
- ALL
- PERSIST
- REMOVE
- MERGE
- REFERESH
- DETACH
하지만 정작 주로 사용하게 되는 것은 ALL, PESIST, Remove입니다. ALL의 경우의 위의 해당하는 모든 영속성이 전이되는 경우이고 Persist의 경우 엔티티가 저장될때만 연쇄적으로 저장되게 하는 옵션이고, Remove는 같이 삭제되게 하는 옵셥입니다!!
예시 코드를 보면서 이해하고 마무리 합시다.
@OneToMany(mappedBy = "post", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE) @OrderBy("created_at asc") private List<Comment> commentList;
위의 코드는 Post가 삭제될 때 Comment 삭제 되게끔 cascade = CascadeType.REMOVE로 선언했습니다.
필자도 마지막 플젝을 더 발전시켜가며 현재 다시 복습하는 과정을 글을 쓰고 있는데, 많은 것을 다시 알아가고 몰랐던 것을 이해해가고 있습니다!!!