들어가기 앞서
- 본 글은 김영한님의 인프런 강의 자바 ORM 표준 JPA 프로그래밍 - 기본편을 공부하고 공부한 내용을 정리한 글입니다.
- 강의의 내용을 '공부하며' 정리한 것이기에 부족한 부분이 있을 수 있습니다. 보다 정확한 학습을 원하신다면 위 강의를 참고하실 것을 권합니다.
- 강의에서 발췌한 사진 두 장이 첨부되어 있습니다. 문제의 여지가 있다면 댓글로 알려주시기 바랍니다.
들어가기 앞서 2
- 백엔드 개발자로 입사한 지 4개월 차, JPA를 사용하는 새로운 프로젝트에 투입되게 되었다. 취업 준비를 하며 JPA를 학습했던 경험이 있으나 입사를 한 이후 3개월 동안 JPA를 한 번도 사용해 본적이 없었다.
- 새로운 프로젝트를 준비하며 JPA 관련된 공부를 다시 시작했는데 모두 까먹은 것인지 기억이 하나도 나지 않았다.
인간의(사실 나의) 기억력 부족함에 대한 통감을 느끼며 먼 훗날 또다시 JPA를 까먹는 순간을 대비해 이 글을 쓴다.
목표
- 객체와 테이블 연관 관계의 차이를 이해
- 객체의 참조와 테이블의 외래 키를 매핑
- 중요 용어 이해
- 방향(Direction): 단방향, 양방향
- 다중성(Multiplicity): 다대일, 일대다, 일대일, 다대다
- 연관관계의 주인(Owner)
목차
- 연관관계 기초 : 단방향 연관관계
- 양방향 연관관계와 연관관계의 주인
- 결론
연관관계 기초 : 단방향 연관관계
객체를 테이블에 맞추어 모델링 하는 경우 (연관관계 매핑을 사용하지 않는 경우)
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
private Long teamId;
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
}
// sql
create table member (
member_id bigint not null,
team_id bigint,
username varchar(255),
primary key (member_id)
) engine=InnoDB
create table team (
team_id bigint not null,
name varchar(255),
primary key (team_id)
) engine=InnoDB
문제점 : 외래키 식별자를 직접 다룬다? 객체지향적이지 않고 뭔가 애매함..
EntityManager em = ...
// 메소드 내부
...
Team team = new Team();
team.setName("TeamA");
em.persist(team); // team을 엔티티 메니저가 관리할 수 있게 영속
Member member = new Member();
member.setUserName("memberA");
member.setTeamId(team.getId()); // persist 상태가 되면 무조건 pk가 생성된다. 위에 team에 id 값이 지정되지 않았지만 em.persist()를 했을 때 자동으로 pk, 즉 id가 생성되어 있는 상태인 것.
// member.setTeamId(team.getId()). 뭔가 객체지향스럽지 않다. member.setTeam()을 했어야 객체지향스러울 것이다.
em.persist(member);
// 만약 member와 연관된 Team 객체를 가져오고 싶은 경우, 번잡한 작업이 필요하다.
Member foundMember = em.find(Member.class, member.getId()); // Member 객체를 찾고
Long foundTeamId = foundMember.getTeamId(); // foundMember가 들고 있는 Team 아이디를 가져온 후
Team targetTeam = em.find(team.class, foundTeamId); // foundTeamId를 pk로 들고 있는 Team 객체를 조회해야 함
// 위처럼 객체를 가져오는 건 상당히 번거롭기 때문에, 결국에는 JPA라는 orm을 사용하면서도 sql 종속적인 join문을 이용하게 되는 웃지 못할 상활을 마주할 지도 모른다.
- 왜 위와 같은 방식이 애매한걸까?
- 테이블은 외래 키로 조인을 사용해 연관된 테이블을 찾는다.
- 객체는 참조를 사용해서 연관된 객체를 찾는다.
- 테이블과 객체 사이에는 이러한 큰 간격이 있기 때문.
객체를 객체 지향스럽게 모델링 하는 경우 (단방향 연관관계)
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
//private Long teamId;
}
사용상의 개선점
EntityManager em = ...
// 메소드 내부
...
Team team = new Team();
team.setName("TeamA");
em.persist(team); // team을 엔티티 메니저가 관리할 수 있게 영속
Member member = new Member();
member.setUserName("memberA");
member.setTeam(team); // team을 그대로 넣을 수 있다.
em.persist(member);
// 만약 member와 연관된 Team 객체를 가져오고 싶은 경우, 아래처럼 편하게 조회가 가능하다.
Member foundMember = em.find(Member.class, member.getId()); // Member 객체를 찾고
Team targetTeam = foundMember.getTeam();
양방향 연관관계와 연관관계의 주인
- 연관관계의 주인은 JPA 학습에서 가장 어려운 주제 중 하나이다.
- 왜 연관관계의 주인이 필요할까?
- 테이블과 객체에 차이가 있기 때문
- 객체 <- 객체는 그래프 탐색을 통해 다른 객체를 참조한다.
- 테이블 <- 테이블은 FK를 이용한 join을 통해 다른 테이블을 참조한다.
- 연관관계의 주인이라는 개념은 테이블과 객체 간의 이러한 차이 때문에 사용된다.
- 연관관계의 주인은 어려운 주제이기 때문에 '왜' 이러한 개념을 사용하는지 명확한 이해가 필요하다.
테이블과 객체는 어떤 차이가 있을까?
- 단방향 연관관계의 문제점
- 테이블은 fk로 두 테이블을 매핑해 놓으면 어느 테이블에서든 상대 테이블로의 접근이 가능하다.
- 그러나 객체에서의 단방향 연관관계는 연관관계를 매핑한 곳에서만 상대에 접근이 가능하다. (위의 경우 Member에서 Team으로 접근이 가능하지만 Team에서 Member로의 접근은 불가능함)
- 실무에서는 타겟팅한 객체들간에 자유로운 조회가 필요한 상황이 왕왕 있는데 단방향 연관관계의 경우 이러한 요구를 반영하기 어려울 수 있음
단방향 연관관계에서 양방향 연관관계로의 전환
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team") // mappedBy는 '내가 누구랑 연결되어 있지?' <- Member.team)
private List<Member> members = new ArrayList<>();
}
EntityManager em = ...
// 메소드 내부
...
Team team = new Team();
team.setName("TeamA");
em.persist(team); // team을 엔티티 메니저가 관리할 수 있게 영속
Member member = new Member();
member.setUserName("memberA");
member.setTeam(team); // team을 그대로 넣을 수 있다.
em.persist(member);
// 만약 member와 연관된 Team의 모든 Member를 조회하고 싶다면
Member foundMember = em.find(Member.class, member.getId()); // Member 객체를 찾고
List<Member> targetMembers = foundMember.getTeam().getMembers();
- 왜 MappedBy가 필요할까? (MappedBy는 사실 JPA에서 멘탈붕괴를 유발하는 극악무도한 난이도를 가진 요소이다.)
- MappedBy를 이해하려면 객체와 테이블간 연관관계를 맺는 차이를 이해해야 한다.
객체와 테이블이 관계를 맺는 차이
- 객체에서의 양방향 연관관계는 사실 단방향 연관관계 두 개가 묶여 있는 것
- Member -> Team
- Team -> Member
- 반면 테이블의 연관관계는 fk 하나로 양방향 연관관계가 묶여 있다. (사실 테이블에서는 ‘방향’이라는 개념 자체가 없다.)
- 즉, 객체 세상은 테이블의 세상보다 조금 더 복잡하고 어지럽다.
그렇다면 객체적인 관점에서 어떤 단방향 매핑 값으로 테이블의 fk를 제어하고 통제해야 할까? 즉 만약, 어떤 회원의 소속팀이 바뀌었을 때 Member에 있는 Team을 바꿔야 fk가 바뀔까, Team에 있는 List<Member>를 바꿔야 fk가 바뀔까?
만약 Member에 있는 Team에는 새로운 값을 넣었는데, 해당 Team에는 Member 정보를 반영하지 않는다거나, Team에 새로운 Member를 넣었는데 해당 Member에는 Team 정보를 반영하지 않는다면 db의 테이블은 어떤 것을 따라야 할까?
양방향 연관관계의 경우 이렇게 혼란스러운 상황이 발생하기 때문에 ‘연관관계의 주인’이라는 개념을 만들어 연관관계의 주인만이 외래 키를 관리할 수 있도록 하는 것이다. 즉, 연관관계의 주인이라는 개념은 양방향 연관관계에서만 사용되는 개념인 것.
연관관계의 주인과 MappedBy
- 연관관계의 주인은 외래 키를 관리한다. (등록, 수정, 삭제 가능)
- 주인이 아닌 쪽은 only-read만 (읽기만 가능)
- mappedBy는 주인이 아닌 쪽에서 사용
그럼 누구를 주인으로 해야 할까? 이에 대해서는 명확한 답이 있다.
- 외래키가 있는 쪽을 주인으로 정해라. (이렇게 사용하는게 헷갈리지 않고 성능 이슈를 방지할 수 있다.)
- 외래키가 없는 쪽을 주인으로 정할 경우에는 A 객체를 수정했는데 B 객체에 대한 update 쿼리문이 날아가는 매우.. 헷갈리는 상황을 마주할 수 있다.
양방향 매핑 시 가장 많이 하는 실수 (연관관계의 주인에 값을 입력하지 않음)
EntityManager em = ...
// 메소드 내부
...
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUserName("memberA");
em.persist(member);
team.getMembers().add(member); // 연관관계의 주인이 아닌 team쪽에서 값을 입력함
// 연관관계의 주인이 아니면 읽기전용이기 때문에 db에 반영되지 않음
// 헷갈리니 양방향 연관관계일 때는 그냥 둘 다 넣자.
결론
양방향 연관관계는 매우 헷갈린다. 가능하면 단방향 연관관계를 사용하고 필요한 경우 아래의 사항을 명심하자.
- 양방향 연관관계를 매핑할 때 연관관계의 주인을 반드시 정하라. (주인의 기준은 fk를 가지고 있는 쪽. 일대다일 경우 무조건 ‘다’쪽)
- 양방향 연관관계를 구성할 때는 연관관계 메소드를 만들어서 객체의 삽입이 양쪽으로 모두 들어갈 수 있도록 하자. (논리상 주인쪽에만 값을 넣어도 되기는 하지만 헷갈리는 상황이 많이 발생할 수 있고, 엄밀하게 말해 객체지향적이지도 않다. 그냥 복잡하게 생각하지 말고 ‘둘 다’ 넣자.) ← 김영한님은 이를 ‘연관관계 편의 메소드’라고 부르심
- 연관관계 편의 메소드는 한쪽에만 만들자. 순환참조가 발생할 여지가 있다. ← 순환참조가 발생하면 stack over flow를 뱉으며 프로그램이 뻗는다! (+ lombok의 toString을 웬만하면 사용하지 말라고 한다! 순환참조 관련해서 취약할 수 있다고 한다.)
양방향 매핑은 단방향 매핑에서 반대 방향으로의 조회 기능이 추가된 것일 뿐이다. 양방향 매핑은 사용하는데 주의할 점이 많고 무엇보다 헷갈리기 때문에 다시 한번 언급하지만 웬만하면 단방향 매핑으로 프로그램을 설계하고 정말 필요한 경우에만 양방향 매핑을 고민하도록 하자.
'JAVA' 카테고리의 다른 글
java 리플렉션 적용해보기 (2) | 2022.11.25 |
---|---|
[디자인 패턴 - 생성 패턴] 팩토리 메소드 패턴 (0) | 2022.11.03 |
[번역] cron4j quickstart (0) | 2022.04.01 |
Java의 날짜/시간 API. Date와 Calendar는 왜 사용하면 안될까? (0) | 2022.03.03 |
JVM(Java Virtual Machine)이란? (0) | 2022.02.19 |