당니의 개발자 스토리

양방향 연관관계와 연관관계의 주인 1- 기본 본문

스프링/자바 ORM 표준 JPA 프로그래밍 - 기본편

양방향 연관관계와 연관관계의 주인 1- 기본

clainy 2024. 6. 10. 21:55

양방향 연관관계와 연관관계의 주인 1- 기본

자 이번에는 이제부터가 진짜입니다.

아까 말씀드린 jpa계의 포인터라고 할 수 있는 양방향 연관관계랑 연관관계 주인에 대해서 설명을 드릴 건데요. 한번 잘 들으시면 어렵진 않은데 진짜 잘 들어야 합니다.

JPA에서 나머지 부분은 사실 제가 생각할 때 그렇게 어렵지 않은데 딱 어려운 두 가지가 앞 전에 설명드린 영속성 컨텍스트에 대해서 메커니즘을 이해하는 거고, 또 하나가 양방향 연관관계와 연관관계 주인. 특히 연관관계 주인 이 부분이 다른 책들로 공부하기 힘들어요.

왜냐하면 객체랑 테이블 이 두 개가 패러다임 차이가 있거든요. 연관된 애들을 찾을 때 객체는 참조라는 걸 사용을 하고, 테이블은 외래키를 가지고 조인을 활용하는데 이 둘 간에 차이가 뭔지랑 차이에서 오는 거를 이해를 해야 돼요. 그래야 '아 뭔가 연관관계 주인이라는 개념이 있구나' 라는 걸 아시게 되거든요. 암튼 이 개념이 왜 필요한지 알게 돼요. 왜 필요한지 모르면 막 메뉴얼에 기능만 적혀 있는 경우가 있는데 나중에 보시면 뭐 mappedBy 나오는데 이게 완전 멘붕이거든요.

자 제가 정말 최대한 쉽게 잘 설명해 드리겠습니다.

이전 시간에 했던 단방향 맵핑에서 양방향 맵핑으로 한 단계 올려 보겠습니다.

지금 이 코드를 보시면 Member에서 Team으로 갈 수 있죠. member.getTeam 으로 갈 수 있는데

반대로 이 Team에서 getMember는 될까요? 안될까요? 당연히 안되겠죠.

보시면 안에 아무것도 없단 말이에요.

근데 사실 보면 Member랑 Team은 서로 왔다갔다 할 수 있잖아요. 레퍼런스만 넣어두면 가능하잖아요.

자 이런거를 이제 양방향 연관관계라고 그러고요. 양쪽으로 참조해서 갈 수 있게 만들려면 그럼 어떻게 하면 되냐?

지금 이 테이블 연관관계 그림을 보시면 일단 테이블 연관관계는 이전 시간과 했던 거랑 차이가 하나도 없어요.

앞에서 했던 단방향 예제거든요. 양방향 예제와 바뀐 게 하나도 없습니다.

내가 지금 양방향으로 객체 참조할 때도 테이블은 전혀 변화가 없어요. 왜냐? 이게 진짜 중요한데요. 테이블을 생각해보시면 이 TEAM_ID라는 외래키 있죠? 이걸로 여기 조인하면 돼요. 반대로 Member에서 내가 소속된 Team을 알고 싶으면 Member에 있는 TEAM_ID(FK)랑 Team에 있는 TEAM_ID(PK)를 조인하면 알 수 있어요. 또 Team의 입장에서 우리 Team에 누가 어떤 Member들이 소속되어 있지 라고 알고 싶으면 나의 PK랑 Member에 있는 FK랑 조인하면 돼요.

여기서 뭐가 중요하냐면 테이블의 연관관계는 외래키 하나로 양방향이 다 있는 거예요. 사실상 테이블의 연관관계에는 방향이라는 개념 자체가 없어요. 그냥 포린키 하나 집어넣으면 양쪽으로 서로의 연관관계를 다 알 수 있는 거예요.

근데 문제는 객체예요. 객체는 보시면 이전 예제에서는 Member가 Team을 가졌잖아요. 그래서 Member에서 Team으로 갈 수 있는데, Team에서는 Member로 갈 수 있는 방법이 없었어요. 그래서 지금 Team에다가 Member스라는 리스트를 넣어줘야 양쪽으로 갈 수가 있는 거예요. 완전 다르죠.

객체에서는 Member에서도 Team을 세팅히고, Team도 Member를 세팅했죠. 근데 테이블은 외래키 하나만 넣어주면 양쪽으로 다 볼 수 있다는 차이가 있습니다. 이게 이제 객체 참조와 테이블의 외래키의 가장 큰 차이입니다.

자 일단 한번 해볼게요.

양방향 연관관계를 하기로 했으니까 Team에다가 List를 넣어 주시고요. 이름은 members 라고 할게요.

그리고 이렇게 ArrayList로 초기화 해두는 게 관례입니다. 그러면 add 할 때 NullPoint가 안 뜨니까요. 그리고 여기에다가

아까 Member 엔티티를 보시면 다대일 이잖아요. 그러면 Member에서 Team으로 가는 게 다대일 이면 Team에서 Member로 가는건 일대다겠죠?

자 그래서 여기에다가 @OneToMany 라고 적어주시면 됩니다.

그리고 뭘 하나 더 적어야 되냐면 mappedBy 라고 하나 적어 주셔야 됩니다. 이 mappedBy는 뭐냐면 지금 일대다 맵핑에서 '나는 뭐랑 연결되어 있지?' 라는 건데

여기 보시면 지금 Member에 있는 이 team, 이 변수명 이거랑 같이 걸려있다 라고 '나의 반대편 사이드에는 이게 걸려있어' 라고 적어주는 겁니다.

그래서 mappedBy = "team"을 굳이 해석을 하자면 '나는 이 Team에 맵핑이 돼 있는 애야' 라는 뜻이죠.

자 이렇게 하면 이제 반대로 탐색을 할 수가 있습니다.

이전에 했던 건 다 지우고 지금 Member를 찾았단 말이에요. 일단 Team으로 가서 아까 추가한 members의 getter, setter를 만들고,

Team에 가서 찾은 member에서 getTeam으로 찾고 Team에서 getMembers 호출하면 됩니다.

그 다음에 이터레이터 돌려가지고 getUsername 하고 돌려보면,

지금 하나밖에 없으니까 하나만 나올 거에요. member1 나오죠.

여기서 flush 하고 clear 해준 게 아주 중요합니다.

그래야 이렇게 find 할 때 db에서 깔끔하게 값을 가지고 옵니다. 지금 보면 Member에서 Team으로, Team에서 다시 Member로 왔다갔다 하고 있죠. 이게 양방향 연관관계라는 겁니다.

그래서 이제 반대 방향으로도 객체 그래프 탐색을 할 수가 있는 거죠.

그러면 양방향 맵핑이 과연 좋냐에 대해서는 사실 객체는 가급적이면 단방향이 좋아요. 양방향으로 하면 신경쓸게 점점 많아져요.

그러면 여기서 이제 이런 의문을 가지셔야 됩니다.

여기는 그냥 @JoinColumn만 해서 적어 줬는데,

mappedBy 얘 정체는 뭐야? mappedBy 없어도 되는 거 아니야? 등등 이제 고민이 되게 많이 되실 거에요. 이 포인트가 이제 진짜 중요합니다.

자 이 mappedBy의 정체가 뭐냐 하면 지금부터 잘 설명 드리겠습니다.

일단 이 mappedBy, JPA의 멘탈 붕괴 난이도, 이거에 대해서 이론적인 이유를 모르고 근본적으로 이걸 왜 이런 식으로 JPA에서 스펙 설계를 했는지 모르고 들어가면 이걸 어느 타이밍에 써야 될지 감이 안 오거든요. mappedBy는 객체와 테이블 간의 연관관계를 맺는 차이를 이해하시면 돼요.

객체와 테이블의 관계를 맺는 차이는 뭐냐?

객체는 연관관계가 되는 Key Point가 두 가지가 있습니다. 회원에서 Team으로 가는 연관관계 하나랑 Team에서 회원으로 가는 연관관계 하나.

보세요. 객체를 보시면 Member는 Team team으로 가고, Team은 List를 통해서 Member한테 가죠. 지금 보시면은 단방향 연관관계가 두 개가 있는 거에요. 이걸 억지로 그냥 양방향 연관관계라고 우리가 부르는 거에요.

그런데 테이블의 연관관계의 Key Point는 딱 하나에요.

여기 테이블을 보시면 이 Member에 있는 이 TEAM_ID 외래키 값, 외래키 제약 조건을 얘기하는 게 아니에요. 이 TEAM_ID로 TEAM의 PK랑 조인을 하면 이 Member가 어느 Team 소속인지 알 수가 있죠.

그런데 반대로 Team 입장에서도 TEAM_ID PK에다가 외래키를 조인을 하면 우리 Team에 누가 들어있는지 알 수가 있죠. 양쪽을 다 알 수가 있는 거예요. 그냥 이 TEAM_ID Foreign Key 하나로 양쪽에 서로 연관 관계가 다 그냥 끝이 나는 거예요.

그러니까 테이블의 연관관계에서는 이 Foreign Key 값 하나로 모든 연관관계가 끝이 나는 거예요. 근데 반대로 객체세상은 좀 더 어지러워서 참조가 Member에도 있고 Team에도 있어야 돼요.

자 그래서 일단은 이 두 가지는 차이가 있다는 걸 이해하셔야 돼요. 이게 출발점이구요. 그래서 뭐 사실 제가 회원과 Team의 연관관계 테이블은 양방향으로 적어놨는데 사실은 방향이 없는 거에요. 그냥 하나만 있으면 양쪽으로 다 왔다 갔다 할 수 있는 거에요. 근데 객체는 회원에서 Team으로 가려면 참조 값을 하나 넣어놔야 되고, 반대로 Team에서 회원 들어가려고 할 때에도 참조 값을 하나 넣어놔야 됩니다. 그러니까 단방향이 그냥 두 개가 있는 겁니다.

그래서 객체의 양방향 관계는 사실 양방향 관계가 아니라, 서로 다른 단방향 관계 두 개입니다. 이걸 우리가 그냥 억지로 양방향이라고 부르는 거예요. 그래서 객체를 양방향 참조로 만들려면 단방향 연관관계를 두 개 만들어야 됩니다. 여기 보시면 제가 A, B로 예를 적어 놨는데 Class A가 B를 가지고 있고 B가 A를 가지고 있으면 A에서 B로 가려면 a.b나 a.getB, 반대로 B에서 A로 가려면 b.a나 b.getA 이렇게 참조를 각각 만들어 놔야 됩니다.

그런데 테이블의 양방향 관계는 외래키 하나로 두 테이블의 연관관계를 관리한단 말이에요.

아까 계속 말씀드렸듯이 여기 보시면 Member에 있는 TEAM_ID 외래키 값 하나만 있으면 이쪽으로 갈 수 있고 이쪽으로도 서로 알 수가 있어요.

그래서 Member에 있는 TEAM_ID 외래 키 하나로 양방향 연관관계를 가집니다. 양쪽으로 다 조인할 수 있는 거에요. 보시면 query가 select from member join team 으로 해석해도 되고 반대로 select from team join member 할 수도 있습니다.

자 그럼 여기서 이제 딜레마가 와요.

지금 객체를 두 방향으로 만들어 놨단 말이에요. 참조가 두 개죠. Member에서 Team으로 가는 Team 참조 값이랑 Team에서 Member로 가는 참조 값이 있어요.

그러면 '나는 도대체 이 둘 중에 뭘로 TEAM_ID를 맵핑을 해야 돼? Member의 Team 값을 바꿨을 때 이 Member 테이블 외래키 값이 update가 돼야 돼? 아니면 Team에 있는 Members를 update 했을 때 이 Member 테이블 외래키 값이 update 돼야 돼?' 하는 딜레마가 오는 거예요.

DB에 입장에서 MEMBER 라는 테이블이 있는데, 뭔가 내가 Member를 바꾸고 싶거나, 아니면 어느 새로운 Team에 들어가고 싶어 그럴 때 Member의 Team 값을 보고 바꿔야 될지 아니면 Team에 있는 리스트 Member스 값을 보고 바꿔야 될지 이상하단 말이에요.

이론상으로만 보면 둘 다 맞는데 DB 입장에서는 이 MEMBER에 있는 TEAM_ID 외래키 값만 어떻게든 update 되면 돼요. 그래서 연관관계 맵핑을 할 때 저희가 이전에는 단방향일 땐 신경 쓸 필요가 없었죠. Member의 team만 딱 챙기면 되니까 참조 값인 team만 연관관계 맵핑을 하면 됐는데 문제는 지금 양방향이 되면서 Team에 있는 Member스도 신경을 써야 돼요. members 값으로도 맵핑을 해야 되나? 혼란스러워집니다.

예를 들어서 극단적인 시나리오로 Member의 Team에는 값을 넣고, Team의 Member스에는 값을 안 넣어요. 반대로 Member의 Team에는 값을 안 넣고 Team에 있는 Member스에는 값을 넣어요. 아님 둘 다 값이 있어요. 이런 시나리오에서는 이걸 도대체 어떻게 update를 해야 되지?

자 그래서 객체와 테이블 사이에 명확한 차이가 있는 거예요. 그래서 룰이 생깁니다.

둘 중 하나로 외래키를 관리해야 됩니다. Member에 있는 Team으로 외래키를 관리할지, 아니면 Team에 있는 리스트 members로 외래키를 관리할지 둘 중에 하나를 주인을 정해야 됩니다.

이게 바로 연관관계의 주인입니다. 연관관계의 주인이라는 개념은 사실 양방향 맵핑에서 나오는 거고요. 객체의 두 관계 중 하나를 연관관계의 주인으로 지정해야 됩니다.

그러니까 여기서 객체 Member에 있는 Team이 주인이 될래? 아니면 Team에 있는 Member스가 주인이 될래? 라고 정해야 된다는 겁니다.

그러니까 연관관계의 주인만이 외래키를 관리할 수 있습니다. 관리라는 것은 등록이나 수정할 수 있다는 거예요. 즉 변경할 수 있다는 거죠. 주인이 아닌 쪽은 읽기만 가능합니다. 이게 엄청 중요합니다. 그리고 주인은 mappedBy 속성을 사용하면 안되고요. 이 mappedBy 라는 뜻 자체가 뭔가 내가 저거에 의해서 맵핑이 되었어라는 뜻이잖아요. 그러니까 주인이 아니라는 거죠.

그래서 주인이 아니면 mappedBy 속성으로 주인을 지정을 해줘야 됩니다.

그러면 누구를 주인으로 해야 되지? 자 여기에는 답이 있습니다.

여기 Member를 보면 지금 @JoinColumn을 해서 이미 TEAM_ID랑 맵핑을 해버렸죠? 지금 이게 지금 관리한다는 뜻이에요.

그런데 Team을 가보시면 members에 mappedBy로 '나는 team에 의해서 관리가 돼" 라고 되어있죠. 이 Team에 가보면,

바로 이겁니다. 얘가 연관관계의 주인이라는 거에요.

그래서 이 mappedBy가 적힌 데는 딱 읽기만 되는 겁니다. 결론적으로 여기 members에 값을 넣어봐야 아무 일도 벌어지지 않아요. 대신에 members로 조회는 할 수 있어요. 조회를 하면 JPA가 team.getMembers 하면 연관된 Team을 조회하는 건 되는데,

값을 변경해서 DB에 update하거나 넣을 때는 이것만 참조합니다. 그래서 이것 때문에 진짜 개념을 모르고 쓰다가

여기에 값을 집어넣고 왜 DB에 값이 안들어가! 합니다.

조금 더 설명드리고 예제를 해볼게요.

누구를 주인으로 할거냐 하면 외래키가 있는 곳을 주인으로 정해야 합니다. 이거는 그냥 제가 정해드리는 가이드에요. 지금 객체 Member의 Team을 맵핑으로 하시구요. 그 다음에 mappedBy 거는 거는 Team 쪽에다 겁니다. 1에다가 거는거죠.

team.member를 이제 가짜 맵핑이라 그러거든요. 가짜 맵핑 읽기만 할 수 있는 거죠. 기준은 뭐냐면 외래키가 있는 곳을 주인으로 정하는 거에요.

지금 Member 엔티티랑 MEMBER 테이블을 맵핑을 했죠. 그러면 사실 Foreign Key가 지금 MEMBER에 있단 말이에요. 그러면 진짜 맵핑을 둘 중에 선택을 하면 Member로 선택을 하셔야 돼요. 그렇게 해야 헷갈리지 않아요. 이게 굉장히 많은 이유가 있는데 어떤 관계에서는 Table을 연관관계 주인으로 정할 수도 있어요. 근데 그렇게 하면 뭐가 문제냐면,

내가 Team의 members의 값을 바꿨어 그런데 다른 테이블에 update 쿼리가 나가는 거예요. 그러니까 Team에다가 어떤 행위를 했는데 Member에 대한 update 쿼리가 나가는거에요. 그러니까 헷갈리겠죠.

그리고 또 하나는 약간 성능 이슈도 조금 있어요. TEAM을 Insert 해야 되는데, MEMBER를 Insert 할 때는 Foreign Key를 바로 챙길 수 있잖아요. 그래서 Insert가 query 한방으로 딱 되는데 뭔가 Team을 해버리면 TEAM에는 Insert query가 나고 MEMBER에는 update query가 나고 그렇게 관계가 되어있어요.

그래서 제가 딱 기준을 정해드린 겁니다. 그냥 외래키가 있는 곳을 주인으로 딱 정하시면 돼요. 그러면 헷갈릴 게 없어요. 그래서 아무튼 진짜 맵핑은 무조건 외래키가 있는 곳을 주인으로 정해라 입니다.

이게 또 왜 기준이 나오냐면 이렇게 외래키가 있는 곳을 주인으로 정해라 하면 사실 많은 것들이 해결이 되는데, 어떤 고민이 해결이 되냐면

여기 보시면 DB 입장에서 보면 외래키가 있는 것이 무조건 다예요. 외래키가 없는 곳이 무조건 1이고요. 1 대 n이 되는 거예요. 그 말인 즉슨 db의 n 쪽이 무조건 연관관계의 주인이 되는 거예요. 다 쪽이 무조건 연관관계 주인이 됩니다. 이렇게 정하셔서 이제 설계를 하셔야 이게 깔끔하게 나와요.

이거에 대해서 이제 좀 할 얘기가 많긴 한데 연관관계 주인이라고 하니까 이게 뭔가 비즈니스적으로 되게 중요한 거 같잖아요. 사실 이거는 비즈니스적으로 중요한 게 핵심이 아니에요. 연관관계 주인은 그냥 정말 단순하게 얘기해서 DB 테이블로 따져서 N쪽인 곳이 연관관계 주인이 되면 돼요.

객체를 보시면 Team과 비교했을 때는 Member의 Team 참조 값이 연관관계 주인이 되면 돼요.

자동차랑 자동차 바퀴가 있으면 비즈니스적으로 따지면 자동차가 훨씬 중요하죠. 그런데 연관관계 주인은 바퀴로 잡아야 되는 거예요. 그렇게 해야 이제 뒤에 나올 성능 이슈도 없고 이 설계가 다 깔끔하게 돌어가요.

그래야 이 외래키가 있는 같은 테이블에서 관리가 돼요. Member 엔티티랑 MEMBER 테이블이 맵핑이 돼 있는 여기에서 연관관계가 관리가 돼요.

그렇게 해야 내가 Member를 바꿨더니 Member의 update 쿼리가 나가는구나. 이렇게 직관적으로 인식이 딱딱돼요. 그래서 외래 키가 있는 곳을 주인으로 정하시면 됩니다.

그러면 이제 연관관계 맵핑은 딱 이 기준 하나로 그냥 쭉 푸시면 돼요. 기준만 잘 잡으시면 사실 JPA의 연관관계 맵핑 설계가 그렇게 어렵지 않아요.

 

이번 강의는 여기까지 하겠습니다.