당니의 개발자 스토리
단방향 연관관계 본문
단방향 연관관계
이번 시간에는 연관관계 맵핑에 대해서 알아보겠습니다.

연관관계 맵핑 기초 시간인데요. 이전 시간에 알아봤던 것처럼 뭔가 테이블에 맞춰서 Foreign Key를 그대로 가져오면서 설계하는 방식이 아니라, 정말 member.getItem이나, 누가 주문했는지 member.getMember 이런 식으로 연관관계를 쭉쭉 맺어서, 좀 더 객체지향스럽게 어떻게 설계하는지 알아보는 첫 번째 시간이고요. 객체랑 관계형 DB랑의 어떤 패러다임의 차이에서 오는 것 중에서 이게 제일 어려운 내용입니다. 이전에 기본 맵핑 이런거 보면 사실 되게 쉽잖아요. 그냥 일대일로 맵핑하면 되니까.
근데 여기서는 객체가 지향하는 패러다임과 관계형 DB가 지향하는 패러다임이 다르기 때문에 이 둘 간에서의 차이에서 오는 좀 극심한 어려움이 있습니다. 한 번만 잘 배워두시면 돼요.
자 그래서 이제 목표를 보시면,

우선 객체와 테이블 연관관계의 차이를 이해를 해야 합니다. 왜냐하면 객체는 레퍼런스로 가잖아요. 지금 객체의 참조와 테이블의 외래 키를 맵핑하는 것을 배우는 거거든요.
그래서 생각해보시면 객체는 레퍼런스란 말이에요. memer.getTeam 이런 식으로 가거나 member.Team 이렇게 레퍼런스로 쭉쭉 따라갈 수 있는데 테이블은 '나랑 연관된 애가 뭐지?'라고 할 때 외래 키 값을 이용합니다. 뭐 예를 들어서 주문 테이블에 있던, 이전 강의에서 보셨던 것처럼 주문 테이블 안에 memberId라던가 이런 것들이죠.
자 그래서 사실상 이번 시간에 배우는 건 뭐냐면 객체의 참조와 테이블의 외래 키를 어떻게 맵핑하는지 배우는 겁니다.
그 다음에 중요한 용어들이 있는데 방향이라는 게 있습니다. 방향(Direction)은 단방향, 양방향 이라는 게 있고 이제 뒤에 넘어가면서 자세한 내용을 설명 드리겠습니다.
그리고 다중성은 뭐냐면 다대일, 일대다, 일대일, 다대다 같이 우리가 보통 관계형 DB에서 설계할 때 나오는 게 이 다중성이라고 보시면 됩니다.
그 다음에 이게 이제 진짜 어려운 내용인데요. 연관관계의 주인. 이건 뒤에서 설명드리겠습니다. JPA에서 사실 이게 제일 어렵습니다. 제가 정말 쉽게 설명해드리겠습니다.

연관관계 목차는 이렇게 쭉 있는데요.

이제 연관관계가 필요한 이유.

객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다. 사실 테이블 하나로, 또는 객체 하나로 살아갈 수 있는 세상이 아니잖아요. 객체 같은 게 다 뭔가 연관관계에 걸려 있어 가지고 그것들을 뭐 이렇게 다 활용을 하는 거죠. 또 여기까지 왔으니


여러분이 이 책 두가지 모두 강추드립니다.

왜 이런 책들을 봐야 되냐면 JPA가 사실 문제가 아니에요. 뭔가 내가 객체지향스럽게 코드를 짜고 싶고 객체지향스러운 게 뭔지에 대해서 알게 되면 자연스럽게 ORM에 손이 가게 되는 거거든요. ORM이란 그냥 맵핑만 배우고 객체와 관계형 DB에서의 차이에서 오는 패러다임의 차이나 이런 것들을 배워두면 사실 이 강의가 그렇게 어렵지 않아요. 오히려 어려운 게 뭐냐면 객체지향스럽게 설계하는 게 뭔지에 대해서 약간 근본적으로 아는 것 자체가 살짝 어려운 것 같아요.
그래서 아까 소개해드린 책들을 보시면 크게 도움을 받으실 수 있을 거예요.
자, 이제 예제 시나리오

제가 구체적인 예제로 설명을 드릴 거에요. 회원과 Team이 있고 회원은 하나의 Team에만 소속될 수 있다. 회원과 Team은 다대일 관계다. 이건 무슨 말이냐면 반대로 얘기하면 하나의 Team에 여러 회원이 소속될 수 있고, 즉 회원이 N이고 Team이 1이라고 보시면 됩니다.

자 이러한 객체를 정말 단순하게 테이블에 맞추어서 모델링 한다고 해볼게요. 연관관계가 없는 객체란 말이에요. 그러면 이전 시간에 했던 것처럼 이렇게 설계가 됩니다. Member 테이블 먼저 볼게요. 보시면 Member의 pk가 Member_id, 그리고 여기 이제 Member가 소속되는 Team을 알아야 되니까 Tema_id. 외래 키 값이 들어가고요. 그 다음 username. 여기 TEAM 테이블을 보시면 Team_id, name이 있고, 잘 보시면 외래 키가 MEMBER 테이블에 있죠. 이 관계형 DB에서 지금 설명하는 내용에 대해서 잘 이해가 안 되시면 관계형 DB에 대해서 어느 정도 공부를 하셔야 됩니다.

그러니까 Team이 1이고 Member가 n이란 뜻이에요. FK가 Member에 있단 말은 뭐냐면, 이 Team과 Member의 관계에서는 쭉쭉쭉 Member를 Insert할 때 Team_id에 값을 막 넣으면서 여러 개의 Member가 어떤 Team에 소속되었는지를 여러 개 넣을 수 있다는 거고요.
반대로 여러 명의 회원이 하나의 Team에 소속될 수 있고, 다른 말로 하면 하나의 Team에 여러 명의 회원이 소속될 수 있다는 뜻입니다.

자 그러면 객체를 테이블에 맞춰서 딱 모델링 해보면 이렇게 나오는 거에요. 객체를 보시면 Team은 지금 id, name 그대로 하면 되죠. 근데 Member는 id, teamId, 이 외래 키 값을 그대로 가지고 오는 거죠.

자 이렇게 하면 어떻게 되는지 코드를 작성해 보겠습니다. jpashop 말고 처음에 만들었던 프로젝트에다가 작성하겠습니다.

이렇게 작성하면 됩니다. 왜냐하면 Member는 Member랑 Team을 레퍼런스로 가져가야 되는데, 그게 아니라 지금 DB에 맞춰서 딱 모델링을 한 거죠.

이번에는 Team을 만들겠습니다.

이렇게 @Colum으로 Team_id 라고 해줍니다.

Member에다가도 Member_id 해주고

이제 돌려보기 전에 필요없는 건 다 지우고 실행해보면,

여기 Member가 생성됐고 Team도 생성 됐죠.

보시면 Member 테이블에서 Team_id 값을 그대로 가지고 있죠. 자 그러면 이제 무슨 문제가 있냐면 지금 이건 객체의 참조가 아니라, 테이블에 맞춰 가지고 외래 키 값을 그대로 가지고 있는 거죠.
자 그러면 이제 객체를 테이블에 맞춰서 딱 모델링하면 뭐가 문제인지 보기 위해서,

이 코드를 작성해볼게요.

먼저 Team과 Member에 Getter, Setter를 만들어주고

Team을 저장하고 name까지 세팅할게요.

그 다음에 Team을 persist 해야 되겠죠.
그 다음에 회원을 저장해 보겠습니다.

이 member1을 TeamA에 소속 시키고 싶어요. 그럼 이렇게 해야 되죠. member.setTeamId() 해서 team의 id를 줘야되겠죠. 지금 같은 경우에는 '이 id 를 어떻게 얻지?'

이게 em.persist 하면 항상

이 id에 값이 들어간다고 그랬죠.

영속상태가 되면 무조건 pk 값이 세팅되고 영속상태가 됩니다.

자 그러면 Team.getId 해서 꺼내면 됩니다.

그 다음에 em.persist로 member를 딱 저장을 하면 되는데,

이게 좀 객체지향스럽지 않죠. setTeam 이라고 해야 될 것 같은데 지금까지 우리가 배운 거에서 아직 연관관계 맵핑을 안 배웠기 때문에 이렇게 하고 돌려보겠습니다.

지금 mode가 create구요. 보시면 Team 쿼리 나가고 Member 쿼리 나가죠.
그러면 db를 한번 보겠습니다.

콘솔 제일 왼쪽 위에 있는 이 버튼이 연결 끊기거든요.

끊고 다시 test로 돌아왔습니다.

Member가 잘 나왔고, Team도 보시면 id가 1번에 name이 잘 세팅 됐죠. Memeber도 보시면 됐죠. 이 id가 1이고 2인 것은

사실 이게 지금 내부적으로 h2db가 이 시퀀스를 써서 그래요. 이걸 공용으로 써서 그랬고 이걸 따로따로 id 관리를 하고 싶으면 따로 식별자 맵핑을 해야 되죠. 지금 귀찮으니까 그냥 공용 키에서 끌고 온다고 가정을 할게요.

자 다시 보시면 teamId 1번 PK가 1번 이었죠?

그러니까 Member 입장에서는 Foreign Key 값에 1이 들어가야 되겠죠. 그러면 나중에 필요하면 조인해서 데이터를 가져올 수 있겠죠.

보시면 이 스타일을 처음 보시는 분들도 있을 거에요. 이게 ANSI 표준 조인 문법이거든요. 가급적이면 이 ANSI 표준 문법을 쓰셔야 다른 데이터베이스로 바꾸셔도 무리가 없어요. 이렇게 조인해서 DB를 가져오면 Member랑 Team을 조인할 수가 있죠.

그러니까 객체는 이렇게 했는데 지금 보시면 이게 애매한 거에요. 이건 외래 키 식별자를 직접 다루는 거죠. 그리고 지금 상황에서 조회할 때도 이슈가 있는데

조회할 때 em.find 해서 예를 들어 제가 Member 클래스를 가지고 온단 말이에요.

이걸 findMember 라고 할게요. 그러면 '내가 찾아온 Member가 어느 Team 소속인지 알고 싶어' 그러면 이게 번잡하죠.

여기서 Team을 바로 못 가져오니까 findTeamId 라고 teamId를 가져오는데 또 어떻게 해야합니까?

em.find 해서 Team을 찾은 다음에 findTeamId를 넣어서 Team을 꺼내야 되죠.

이런식으로 계속 그때마다 계속 JPA한테 물어봐야 되죠. 아니 뭐 어쨌든 DB를 통해서 계속 끄집어내야 되죠.

왜냐면 연관관계라는 게 없기 때문에 그래요. 이게 뭔가 객체지향스럽지 않은 방식인 거죠.

자 그래서 객체를 테이블에 맞춰 데이터 중심으로 모델링하면 협력관계를 만들 수가 없어요. 테이블은 외래 키로 조인을 해서 연관된 테이블을 찾아요. 방금 제가 조인 보여드렸죠. 그리고 객체는 참조를 사용해서 연관된 객체를 찾는단 말이에요. 테이블과 객체 사이에는 이런 큰 간격이 있어요. 외래 키 라는 거랑 참조는 완전히 다른 거죠.

자 이제 이런 차이를 인식을 했고, 그러면 제일 먼저 가장 중요하고 가장 기본이 되는 연관관계에서의 단방향 연관관계에 대해서 알아보겠습니다. 뭔가 단방향이 있다는 건 양방향도 있다는 거겠죠.
자 그럼 객체지향스럽게 모델링 하는 건 뭐냐면,

이렇게 하는 겁니다. 자 보시면 team의 Id가 아니고 Team 참조 값을 그대로 가져왔죠.

코드를 바로 보시죠.

여기 보시면 여기를 이제 주석처리 하고요. 이렇게 객체지향스럽게 참조를 하겠다는 거예요.

이제 Getter, Setter가 바뀌었으니까 다시 불러와주고,

그러면 Team 보시면 빨간줄이 납니다. 왜냐하면 이게 이제 JPA한테 알려줘야 돼요. '이 둘의 관계가 무슨 관계지?' 일대다 인지, 다대일 인지 알려줘야 돼요.
자 그래서 이 회원과 Team은 일대다에서 누가 1이고 누가 다인지가 되게 중요한데 이게 DB 관점으로 중요한 거예요.

@Column 같은 이 어노테이션들은 다 데이터베이스랑 맵핑하는 어노테이션이거든요.

그러면 자 Member랑 Team이 있어요. 누가 N이고 누가 1이죠? Member가 N이고 Team 1이죠. 하나의 Team에 여러개의 Member가 소속되니까.

그래서 Member 입장에서는 @ManyToOne 이라는 어노테이션으로 맵핑을 해야 됩니다.

그 다음에 여기서 객체 Member의 team 레퍼런스랑 Member 테이블에 있는 team_id Foreign Key랑 맵핑을 해야되는 거예요.

그래서 @JoinColumn 이러면 아주 명확하죠? join 해야 되는 column이 뭐냐.

name을 그래서 TEAM_ID로 주시면 빨간줄이 생기는데

이게 그 IntelliJ 에서는 데이터 소스를 직접 연동해 가지고 db에 실제 값이 있어야 이게 지금 되는 건데 그것 때문에 이러는 거니까 무시하셔도 됩니다.

아무튼 이렇게 하면 맵핑이 끝난겁니다. @ManyToOne, '관계가 뭔지랑 이 관계를 할 때 조인하는 컬럼은 뭐야' 라고 적어주시면 맵핑이 끝납니다.
그래서 이렇게 하면,

이렇게 Team과 TEAM_ID Foreign Key를 연관관계 맵핑을 하는 겁니다. 이러면 사실 맵핑이 끝납니다. 그럼 이제 내가 마음껏 쓸 수가 있는 거에요.

자 그럼 이제 한번 써볼까요. 제일 먼저 저장하는 거를 해볼게요.

저장하는 코드에 보시면 여기에 빨간불이 들어오겠죠?

이거를 member.setTeam이라고 하시면 돼요. 그리고 재미난 게 이 team을 바로 넣어주시면 됩니다. 그러면 jpa가 알아서 Team에서 pk 값을 꺼내서 Foreign Key 값에 Insert할 때 Foreign Key 값을 사용합니다.

그 다음에 조회할 때도 이전에는 Member를 찾았단 말이에요. 이제는 member.getTeamId를 할 필요가 없어요.

getTeam 해서 바로 끄집어내시면 돼요.

findTeam.getName을 출력해보겠습니다.

실행을 해보면 Insert 두 개가 제대로 나간걸 보실 수 있구요. 조회도 제대로 됩니다. 이런 식으로 해서 객체지향스럽게 바로 레퍼런스들을 가지고 오고 할 수 있다는 걸 확인해 보셨을 거에요. 지금 예제에서 보시면 실행했을 때 '어? select 쿼리가 왜 안나가지?' 이건 영속성 컨텍스트를 열심히 공부하시는 분들은 아시겠죠.

em.persist에서 이미 영속성 컨텍스트에 member가 들어가 있잖아요.

그럼 find 하면 1차 캐시에서 가져왔기 때문에 그렇죠. 이런 거를 테스트할 때 '아 영속성 컨텍스트 말고 DB에서 가져오는 쿼리를 직접 보고 싶어' 하시면,

이렇게 하면 됩니다. em.flush를 강제 호출 하시고요. em.clear 하면 됩니다. em.flush를 해서 현재 영속성 컨텍스트에 있는 쿼리를 db에다가 다 날려버려서 싱크를 딱 맞춘 다음에, em.clear()를 딱 하시면 영속성 컨텍스트를 완전 초기화시켜 버리는 거죠. 그러면 여기서부터는 완전히 깔끔한 데서 가져오겠죠.

그러면 여기 쿼리가 두방 나가고 그 다음에 select 쿼리가 쭉 나가는 거 보실 수가 있습니다.

뭔지 모르겠지만 Member랑 Team을 뭔가 조인을 해서 가지고 오네요. 그 말은 뭔지 모르겠지만 JPA가 Member랑 Team을 한 번에 다 땡겨 왔다는 거겠죠.
이제 이거를 한 방에 땡겨오고 분리해서 가져오고 할 수 있는데, 그거는 뒤에서 다루겠습니다. 잠깐 말씀드리면,

fetch 라고 있는데요. @ManyToOne은 EAGER가 디폴트인데 이걸 LAZY로 바꾸면 쿼리가 분리돼서 나갑니다.

보시면 Member랑 Team이랑 select 쿼리가 분리돼서 나가죠. 이거는 뒤에서 이 부분을 지연 로딩 전략이라고 그러는데 따로 자세히 설명 드리겠습니다. 지금은 뭐 대략 이런게 있다 정도면 아시면 될 것 같구요.
암튼 핵심 뭐냐면 이제 jpa 에서는 이렇게 객체지향스럽게 한 거를, 그러니까 객체 참조와 DB에 외래 키를 맵핑을 해서 연관관계 맵핑을 할 수 있구나 라는 정도만 이해하시면 됩니다.

이제 연관관계 수정을 해보면,

예를 들어서 TeamA를 다른 Team으로 바꾸고 싶어요. 뭐 완전 다른 코드가 있다고 가정을 하고 em.find 해서 PK가 100번인 Team이 db에 있다고 가정을 할게요. 그러면 찾은 Member에서 Team을 바꾸고 싶어 라고 한다면,

이렇게 하시면 돼요. 그러면 물론 이거는 DB에 100번 Team이 있다고 가정을 한 거구요. setTeam에서 TeamA만 바꿔주시면 update 쿼리를 통해서 업데이트가 됩니다. 그 Foreign Key가 업데이트 됩니다.

다시 이쪽 로직만 봐주세요. 그러니까 100번 Team이 지금 DB에 있다고 가정을 하고, Team을 가져온 다음에 Member에다가 Team 소속을 change 해주는 거죠. change만 하면 DB의 외래 키 값이 update 됩니다. 이렇게 연관관계를 수정할 수 있고요.
이번 시간에는 단방향 연관관계를 알아봤습니다.

다음 시간에는 양방향 연관관계와 연관관계의 주인에 대해서 알아보겠습니다.
'스프링 > 자바 ORM 표준 JPA 프로그래밍 - 기본편' 카테고리의 다른 글
| 양방향 연관관계와 연관관계의 주인 2 - 주의점, 정리 (0) | 2024.06.16 |
|---|---|
| 양방향 연관관계와 연관관계의 주인 1- 기본 (0) | 2024.06.10 |
| 실전 예제 1 - 요구사항 분석과 기본 매핑 (0) | 2024.06.08 |
| 기본 키 매핑 (0) | 2024.06.08 |
| 데이터베이스 스키마 자동 생성 (0) | 2024.05.27 |