[JPA] 연관관계 매핑(1) - 객체와 테이블
- [ Backend ]/Spring DB, JPA
- 2022. 1. 4.
0. 연관관계 매핑하기
앞에서 배웠던 내용들을 바탕으로 이번에는 쇼핑몰을 만드는 코드를 작성해보자. 회원이 상품을 주문하는 단순한 기능을 구현하기 위해서 다음과 같이 테이블을 설계하였다.
테이블의 구조를 설계했으면 JPA 구성을 위해 엔티티를 설계하고 매핑해보자. 한개의 Member객체는 여러개의 Order를 만들 수 있고(일대다 매핑), Order와 Item사이에는 다대다 매핑이 가능하도록 설계했다(하나의 order에 여러개의 item이 들어갈수도, 한 item이 여러 order에 들어갈수도 있으므로).
그런데 이렇게 객체의 설계를 테이블 구조에 맞춰서 설계하면, 테이블의 fk를 객체에 그대로 가져오게 되고, Order객체에서 객체를 주문한 객체를 찾고싶다면 memberId를 통해서 Member객체의 Id를 얻어오고, 얻어온 id를 바탕으로 다시 member객체를 찾아야 한다.
Order order = em.find(Order.class, "1L");
Long memberId = order.getMemberId();
Member member = em.find(Member.class, memberId);//얻어온 id로 다시 member객체 정보를 찾음
객체지향적이고 직관적인 설계를 위해서는 Order클래스 내에 Member객체를 바로 찾을수 있어야 한다.
1. 단방향 연관관계
//Member클래스
...
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
...
객체 클래스 내에 다른 객체를 직접 선언하고 column명만 id로 해주면 자연스럽게 직관적인 설계가 가능해진다.
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam(); //Team객체에 대한 정보를 직접 받음.
이렇게 하면 jpa가 member와 team정보를 한번에 join하여 가져온다. 또한 Team객체 자체를 얻었으니 객체를 이용한 정보 수정 역시 가능해진다.
2. 양방향 연관관계
Team과 Member의 예시를 이어서 살펴보자. 테이블 연관관계만 보면 단방향 연관관계와 아무 차이가 없다. 왜냐하면 테이블에서는 기본적으로 양방향 참조가 가능하기 때문이다. 만약 TEAM테이블에서 MEMBER정보를 알고싶다면 TEAM테이블의 기본키와 MEMBER테이블의 TEAM_ID 외래키를 JOIN하면 된다. 반대로 MEMBER테이블에서 소속된 TEAM의 정보를 알고 싶을때도 MEMBER테이블에 존재하는 TEAM_ID외래키와 TEAM테이블의 기본키를 JOIN하면 된다. 즉 테이블 연관관계에서는 외래키만 있으면 서로의 정보를 다 알 수 있다.
하지만 객체끼리의 연관관계에서는, 내가 Member객체에서는 Team객체에 관한 getter를 호출해서 팀에 대한 정보를 알 수 있는 반면에, Team객체에서는 Member에 대한 정보를 알 수 있는 방법이 없다. 단방향 연관관계에서 Team은, team의 id와 name정보만 갖고 있었기 때문이다.
그래서 Team에 연관관계를 갖는 Members에 대한 리스트를 추가하여 상호 저장/접근 가능하게 한 것이 양방향 연관관계 세팅이다.
//<Member 클래스>
...
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
...
//<Team 클래스>
...
@OneToMany(mappedBy = "team") // Member클래스의 Team타입 "team객체"와 매핑됨을 표시. 양방향 매핑!
private List<Member> = new ArrayList<>();
2.1 mappedBy - 객체와 테이블간에 연관관계를 생성하는 차이를 이해하자
테이블에서는 외래키와 기본키만을 이용하여 1개의 연관관계에서 관계를 맺는다. 반대로, 사실 객체끼리는 회원->팀, 팀->회원의 두가지 단방향 연관관계를 사용하여 관계를 맺는 것이다. 다시 말하면, 사실 객체끼리는 양방향 연관관계가 존재하는 것이 아니라 각 객체에 상대 객체멤버를 넣어주어 관리하는, 단방향 연관관계가 두 가지로 존재하는 것이다.
그러면 외래키를 둘 중 무엇으로 외래키를 관리해야 할까?
1. 둘 중에 어떤 하나를 정해야 한다고 단정할 수는 없지만, 일반적으로 외래 키가 있는쪽을 주인으로 정하자. 만약 외래키가 있는쪽이 종관계가 되면, 테이블 매핑관계에서 주인의 정보가 종의 column으로 들어가기 때문에 관리하기가 힘들어진다.
2. 또한, 무조건 객체의 두 관계중 한가지만을 연관관계의 주인으로 설정해주어야 한다. 그것을 '연관관계의 주인'이라고 한다. 주인만이 외래키를 관리(등록,수정..)하며, 반대쪽에서는 읽기만 가능하다.
//잘못된 예시
Team team = new Team();
team.setName("TeamA");
team.getMembers.add(member); //종인 team을 이용해서 member정보를 집어넣은 잘못된 예시
우리는 주인이 아닌쪽에 mappedBy키워드를 달아주어 주종관계를 나타내줄 수 있다.
2.2 jpa의 구조에 따른 순수 객체상태 동작 이해
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();
for(Member m : members) {
System.out.println("m = " + m.getUsername());
}
위와 같은 코드가 있다고 하자. Team클래스 내에 mappedBy로 매핑이 되어있을때, 멤버 리스트를 불러오면 멤버 리스트의 호출이 이루어질 것이라고 기대할 수 있다. 하지만 위의 코드에서 flush, clear코드를 삭제하면, 영속성 컨텍스트의 1차 캐시에만 값이 저장되고 flush나 commit을 통해 flush되지 않은 상태이므로 DB로 SELECT쿼리가 나가지 않을 것이다. 결국 members 리스트에는 Member리스트가 저장되지 않은 상태에서 출력하게 된다.
반면 flush, clear를 하면 캐시에 아무것도 없기 때문에 DB에서 다시 조회해오기 때문에 문제가 발생하지 않는다.
2.3 정리
무조건 외래키가 존재하는 쪽이 주인이 되도록 설정하되, 순수 객체 상태를 고려하여 항상 양쪽에 값을 설정하자. 물론 양쪽에 값을 넣는것이 불편하다면, 한쪽에만 설정값을 넣어도 양쪽에 값이 들어가도록 추가적으로 메소드를 설정해도 된다. 또한 단방향 매핑에 반대방향으로 조회기능이 추가된것이 양방향 매핑임을 명심하고, 항상 단방향 매핑부터 잘 설계하고, 후에 종객체에서 조회가 필요하다면 조회기능을 추가하는 식으로 진행하자.
예시)
//Member객체
..
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
2.4 추가
1. 엔티티에는 final을 사용하지 말자. JPA 구현체들은 엔티티를 내부에서 다양한 방식으로 사용한다. 예를 들어 jpa가 객체를 먼저 생성하고 나중에 값을 필드에 추가하거나, 프록시 기술을 사용할 경우에 문제가 발생할 수 있다.
'[ Backend ] > Spring DB, JPA' 카테고리의 다른 글
[JPA] 연관관계 매핑(3) - 상속관계 매핑 (0) | 2022.01.11 |
---|---|
[JPA] 연관관계 매핑(2) - 다양한 연관관계 (0) | 2022.01.06 |
[JPA] 기본 키 매핑-SEQUENCE 전략, 에러 해결 (0) | 2022.01.02 |
[JPA] 엔티티 매핑 (0) | 2021.12.31 |
[JPA] JPA 구조, 특성 (0) | 2021.12.29 |