일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 멀티스레드 싱글톤
- ArrayList 길이 확장
- thread safety
- PostgreSQL
- docker
- ArrayList 가변
- ArrayList 소스코드
- 컨테이너
- 싱글톤 동시성
- 로드밸런서
- JPA란
- 데이터베이스
- Container
- heap
- 트랜잭션
- java
- load balancer
- 도커
- create-drop
- github
- 권장 PK 전략
- transaction
- JPA 장점
- acid
- JPA
- 자바 동시성
- 스키마 자동 생성
- postgres
- Database
- index
- Today
- Total
JS
[JPA #5] 연관관계 매핑 (Relationship Mapping) 본문
"객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다." - 조영호(객체지향의 사실과 오해)
Member(회원)과 팀(Team) 이 있고, 한 팀에 여러명의 멤버가 속하게 된다고 가정해봅시다.
데이터베이스 테이블의 경우 멤버 테이블에 팀ID를 가지고 있게 될텐데, 같은 형태로 객체 연관관계를 구성하면 데이터를 조회하고 추가하는데 계속 ID를 이용해 참조대상을 찾아야하는 불편함이 생깁니다.
이번 포스팅에서는 그 불편함을 연관관계 매핑을 통해 해결하면서 객체지향적으로 연관관계를 설정하는 방법에 대해 알아보겠습니다.
1. 단방향 매핑
각 멤버는 하나의 팀에 소속될 수 있기 때문에 팀과 멤버의 관계는 1:N 으로 표현할 수 있습니다.
그러므로 멤버 객체에서 소속된 팀 객체를 참조하게끔 연관관계를 설정해줘야 합니다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column
private String username;
@ManyToOne // <--
@JoinColumn(name = "team_id");
private Team team;
}
@ManyToOne
어노테이션은 현재 클래스가 Many, 참조 대상이 One 이라고 설정해주는 역할을 합니다.
@JoinColumn(name = "team_id")
는 참조 대상인 Team 객체와 실제 테이블에 있는 FK 컬럼을 매핑해주는 역할을 합니다. JOIN 해야하는 컬럼이 이 컬럼이다 라고 생각하면 편합니다.
2. 양방향 연관관계와 연관관계의 주인
데이터베이스 테이블에서 참조관계를 구성해놓은 경우 FK만 있으면 양쪽으로 원하는 데이터를 가져올 수 있습니다.
FK 로 물고 있는 ID 를 가지고 JOIN 해서 팀에 속한 멤버들, 해당 멤버가 속한 팀을 양방향으로 가져올 수 있어 딱히 방향이라는 개념이 없습니다.
하지만 엔티티 모델(객체)에서는 참조 대상을 필드로 가지고 있는 객체에서만 접근이 가능합니다.
위의 예시에서는 멤버는 Team 을 참조하고 있으니 해당 멤버가 속한 팀을 가져올 수 있지만, 팀 객체에서는 해당 팀에 속한 멤버를 가져올 수가 없습니다.
이 문제를 해결하려면 Team 모델에 members 라는 리스트를 필드로 추가해주면 됩니다.
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
@Column
private String name;
@OneToMany(mappedBy = "team") // <--
private List<Member> members = new ArrayList<>();
}
Member
에서는 Team
을 @ManyToOne
으로 연관관계를 설정해주었으니 Team
에서는 반대로 @OneToMany
로 묶어줍니다.
그리고 @OneToMany
에 mappedBy
라는 속성을 추가하여 나를 참조하고 있는 엔티티에서 어떤 필드에 나를 매핑해두었는지 명시해줍니다.
Member
테이블에서 team
이라는 변수명으로 Team
객체를 참조하고 있으니 mappedBy
의 값으로 team
을 주는 것이죠.
2-1. 연관관계의 주인
위에서 양방향 연관관계를 설정해주었는데 여기서 생각해봐야할 포인트가 있습니다.
데이터베이스에서는 연관관계를 FK 하나로 관리하기 때문에 참조 대상을 변경할 때 FK 값만 변경하면 됩니다.
반면에 위의 Member 와 Team 엔티티 객체를 이용해서 특정 멤버가 속한 팀을 변경해주고 싶을 때는 아래와 같이 두 가지 방법 중 어떤 옵션을 선택해야할지 헷갈립니다.
- Member 객체에서 setTeam() 해서 Team 을 변경
- Team 객체에서 members 리스트를 수정하는 방법도 있습니다.
둘다 건드렸을때 생기는 문제점도 있겠죠.
JPA 에서는 이 문제를 연관관계의 주인을 설정해주는 방식으로 해결합니다.
연관관계의 주인을 지정하여 DB 테이블에서 FK 하나만 수정하듯 값을 수정할 수 있는 객체 하나를 지정해주는 것입니다.
오직 연관관계의 주인으로 지정된 객체를 통해서만 외래키를 등록, 수정할 수 있고 주인이 아닌 반대편 객체는 읽기만 가능합니다.
연관관계 주인을 지정하고 식별하기 위해 mappedBy
를 사용합니다.
2-2. 양방향 매핑 규칙
양방향 매핑 규칙을 아래와 같이 정리해볼 수 있습니다.
- 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
- 연관관계의 주인만이 외래 키를 관리(등록, 수정)
- 주인이 아닌쪽은 읽기만 가능
- 주인은
mappedBy
속성 사용 X (내가 무언가에 의해서 매핑이 되었어 라는 뜻 - 주인이 아니다) - 주인이 아니면
mappedBy
속성으로 지정 - 결국
mappedBy
걸린쪽은 읽기만 가능 - 외래키가 있는 곳을 주인으로 정해라 (1:N 에서 N쪽을 연관관계 주인으로)
2-3. 양방향 매핑시 자주 하는 실수
양방향 매핑을 구성하다 보면 연관관계의 주인에 값을 입력하지 않는 실수를 하는 경우가 많습니다.
mappedBy
쪽은 읽기 전용이라 JPA가 변경사항을 적용하지 않기 때문에 값을 추가해도 데이터베이스를 확인하면 FK가 null
로 지정 될 수 있습니다.
꼭 연관관계 주인을 통해 값을 설정할 수 있도록 해야합니다.
2-4. 양방향 매핑시 주의할 점
- 주의할 점연관관계 주인에서만 값을 세팅하고
mappedBy
쪽에서는 추가하지 않는 경우
사실 연관관계 주인에서 값을 세팅하면 정상적으로 FK가 지정되어 연관관계 설정이 되지만, 역방향에서도 추가해주지 않으면 JPA 동작 특성 때문에 문제가 발생할 수도 있습니다.
값을 세팅한 뒤 데이터베이스 커밋 시점에 발생하는 flush, clear 까지 도달해야 역방향에서 조회시 값을 데이터베이스에서 가져오게 되는데, flush, clear 까지 도달하지 않은 상태에서 조회시 1차 캐시에만 저장되어 있는 최초 생성된 상태 그대로 가져오게 됩니다.
이런 경우 주인쪽에서 값을 넣어준다 하더라도 데이터가 안들어올 수 있게 됩니다.
그래서 항상 양쪽에 값을 설정해줘야 정확합니다.
하지만 매번 일일히 두 가지 작업을 직접하면 실수를 할 가능성이 있습니다.
따라서 연관관계 편의 메서드를 제공해서 메서드 호출 하나로 한번에 처리되게끔 해주면 실수를 줄일 수 있습니다.
Team team = new Team();
team setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
team.getMembers().add(member); // <- 이렇게 하지말고
// Member 엔티티 클래스에 편의 메서드 제공
// 아니면 Team 쪽에 만들어도 무관
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
- 무한 루프를 조심하자
양방향 매핑시 lombok 라이브러리 등을 활용하거나 IDE의 도움을 받아 toString
메서드를 추가해놓은 경우 toString
이 호출될 때 양쪽 toString
을 전부 호출해서 무한 루프가 발생하게 됩니다.
이는 JSON 생성 라이브러리를 사용하는 경우에도 마찬가지로 발생할 수 있습니다.
스프링 컨트롤러에서 엔티티를 직접 리턴하는 경우 JSON 으로 변환하는 과정에서 참조 대상을 또 JSON 으로 변환하면서 무한루프가 발생하게 됩니다.
이런 문제를 방지하기 위해서 신경쓰면 좋을 몇 가지가 있습니다.
- lombok 이용시
@ToString
을 왠만하면 쓰지 말자. (양방향 매핑 관계가 아닌 경우 굳이 필요하다면 사용해도 무방) - 컨트롤러에서 엔티티를 직접 반환하지말고 DTO 사용해서 반환하자.
컨트롤러에서 엔티티를 직접 반환하는 것은 무한 루프 문제 외에도 엔티티에 변경사항이 생겼을 때 API 스펙에도 변경사항이 생기는 문제가 있기 때문에 절대 엔티티를 직접 반환하지 않는게 좋습니다.
2-5. 실무 Best Practices
위에서 정리한 내용들을 바탕으로 실무에서 지키면 좋을 best practices 를 소개하겠습니다.
- 최초 JPA 설계시 우선 단방향 매핑으로 설계를 완료해라
최초 연관관계 설계시 양방향을 고려하지 말고 단방향 매핑으로 설계를 끝내는 것을 권장합니다.
단방향 매핑만 잘 해놔도 이미 테이블 구조는 완벽하게 설계가 되어 연관관계 매핑이 완료됩니다.
양방향 매핑은 결국 반대쪽에서 조회해야될 소요가 생겼을시 테이블 구조에 영향을 주지 않으면서 추가해주면 되는 부가 기능이기 때문에 처음부터 고려할 필요가 없습니다.
- 비즈니스 로직을 기준으로 연관관계의 주인을 선택하지 마라
연관관계의 주인은 FK의 위치를 기준으로 정해야 헷갈리지도 않고 고민거리가 사라지게 됩니다.
객체를 사용하면서 발생하는 불편함 때문에 고민이 된다면 연관관계 편의 메서드를 추가해서 해결해주면 됩니다.
'JPA' 카테고리의 다른 글
[JPA #4] 엔티티 매핑 (Entity Mapping) (0) | 2022.10.28 |
---|---|
[JPA #3] 데이터베이스 스키마 자동 생성 기능 (0) | 2022.10.28 |
[JPA #2] 영속성 컨텍스트 (Persistence Context) (0) | 2022.10.27 |
[JPA #1] JPA란? JPA를 왜 사용할까? JPA 장점은? (0) | 2022.10.27 |