일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 29 | 30 | 31 |
- 스프링 부트
- 자바의 정석
- MongoDB
- in-memory
- github
- 이벤트루프
- sqld
- 레디스
- 스프링 시큐리티
- 호이스팅
- 영속성 컨텍스트
- 실행 컨텍스트
- SQL
- 분할정복
- spring security
- 다이나믹프로그래밍
- 정보처리기사
- 스프링부트
- Spring Boot
- 캐시
- 정처기
- 동적계획법
- document database
- 게시판
- JPA
- 깃허브
- 가상 면접 사례로 배우는 대규모 시스템 설계 기초
- VMware
- Redis
- NoSQL
- Today
- Total
FreeHand
JPA 연관관계 본문
시작하며
언제인지 기억은 잘 안 나지만 JPA를 사용하여 엔티티를 작성하는데 DB 테이블의 컬럼을 그대로 가져와 작성한 것을 보았다. 물론 그렇게도 할 수는 있지만 내가 강의를 통해 배운 것과는 달랐기에 나름대로 설명을 했었는데, 그 내용을 정리해볼까 한다. 그리고 더 나아가 JPA의 여러 연관관계도 다룬다. JPA를 처음 배울 때만 하는 실수이니 JPA를 이미 좀 아는 사람은 볼 필요 없다.
객체지향스럽게
JPA는 DB 테이블과 자바의 객체를 매핑하는 ORM이다. 다음과 같은 DB 테이블이 있다고 해보자.
그럼 위 테이블을 자바 객체(엔티티)로 표현하려면 어떻게 해야할까
@Entity
public class Player {
@Id
@Column(name = "ID")
private Long id;
@Column(name = "NAME")
private String name;
@Column(name = "TEAM_ID")
private Long teamId;
}
@Entity
public class Team {
@Id
@Column(name = "ID")
private Long id;
@Column(name = "NAME")
private String name;
}
이렇게 작성할 수 있을 것이다. 하지만 위처럼 작성하면 선수의 팀을 알기 위해 teamId로 team을 조회해야 한다.
그런데 이는 객체지향스럽지 못한 방법이다.
실제 축구선수의 프로필을 보면 팀이 나와있지 팀의 ID가 나오지 않는다.
DB 테이블은 ID를 통해 조인을 해서 테이블 간의 연관을 만들지만 엔티티는 객체의 참조를 통해 연관을 만든다.
참고로 연관관계에 대해서는 '클래스 관계', 'UML 연관관계' 등으로 찾아보면 된다. 간단하게 설명하면 다른 객체(클래스)를 필드에 포함하는 것을 연관관계라고 한다.
객체의 연관관계로 엔티티를 다시 작성하면 다음과 같다.
@Entity
public class Player {
@Id
@Column(name = "ID")
private Long id;
@Column(name = "NAME")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team; // Player 객체와 Team 객체가 연관관계임
}
@Entity
public class Team {
@Id
@Column(name = "ID")
private Long id;
@Column(name = "NAME")
private String name;
}
이렇게 작성해야 두 객체 사이에 연관관계가 생기고 Player 객체에서 바로 Team을 조회할 수 있다.
양방향 연관관계와 연관관계의 주인
위에서 작성한 엔티티를 보면 Player에서 Team을 참조할 수 있지만 Team에서 Player를 참조할 수는 없다.
Team에 Player 타입의 필드가 없으니 당연하다. 이처럼 한쪽만 참조할 수 있는 것을 단방향 연관관계라고 한다.
서로가 참조할 수 있는 양방향 연관관계로 만드는 방법은 간단하다. 참조할 수 있도록 필드를 추가하면 된다.
@Entity
public class Player {
@Id
@Column(name = "ID")
private Long id;
@Column(name = "NAME")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team; // Player 객체와 Team 객체가 연관관계임
}
@Entity
public class Team {
@Id
@Column(name = "ID")
private Long id;
@Column(name = "NAME")
private String name;
@OneToMany(mappedBy = "team")
private List<Player> players = new ArrayList<>(); // Team 객체에서 Player 객체를 참조 가능
}
이렇게 단방향 연관관계를 서로 갖고 있으면 양방향 연관관계라고 할 수 있다.
그럼 다시 DB 테이블을 보자.
여기서 김덕배 선수가 레알마드리드로 소속팀을 옮기려면 연결된 TEAM_ID만 레알마드리드의 ID로 변경하면 된다.
자바 코드에서는 어떻게 해야 할까.
Player 객체의 Team을 변경할지 Team 객체의 players를 변경할지 아니면 둘 다 변경해야 할까?
이러한 모호함을 해결하기 위한 것이 '연관관계의 주인'이라는 개념이다.
이 개념을 사용하려면 양방향 매핑을 할 때 다음과 같은 규칙을 따라야 한다.
- 두 객체 중 하나를 연관관계의 주인으로 지정한다.
- 주인만 외래키를 변경할 수 있다.
- 주인이 아닌 객체는 읽기만 할 수 있다.
- 주인이 아닌 객체는 mappedBy 속성으로 주인을 지정한다. (주인 객체는 mappedBy를 사용하지 않는다.)
위 예시 코드에서는 mappedBy에 의해 지정된 Player의 team이 연관관계의 주인이 된다.
내가 본 강의에서는 외래키가 있는 곳을 주인으로 정하라고 한다. PLAYER 테이블에 외래키가 있으므로 Player가 주인이 되면 되는 것이다.
연관관계 주인을 외래키가 있는 곳이 아닌 다른 곳으로 지정하면 예상치 못한 쿼리가 실행되어 성능이 떨어지거나 데이터 무결성에 문제가 생기는 상황이 발생할 수 있다고 한다.
추가로, 이렇게 양방향 연관관계를 설정하면 순환참조 문제도 주의해야 한다.
연관관계 매핑
다대일(N:1)
다대일 관계는 가장 많이 쓰이는 연관관계이다. 앞에서 본 Player와 Team의 관계가 다대일 관계이다.
@Entity
public class Player {
@Id
@Column(name = "PLAYER_ID")
private Long id;
@Column(name = "NAME")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private Long id;
@Column(name = "NAME")
private String name;
}
다음과 같이 @ManyToOne 어노테이션을 사용해서 나타낸다.
일대다(1:N)
일대다 관계는 연관관계의 주인이 Team에 있는 경우이다.
@Entity
public class Player {
@Id
@Column(name = "PLAYER_ID")
private Long id;
@Column(name = "NAME")
private String name;
}
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private Long id;
@Column(name = "NAME")
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Player> players = new ArrayList<>();
}
이렇게 되면 연관관계의 주인과 DB 테이블의 외래키가 서로 다른 엔티티에 존재하게 된다.
따라서 Team에서 players에 변화를 주면 Player의 테이블에 영향을 준다.
이러한 이유로 일대다 관계는 권장하지 않고, 대신 다대일 양방향 관계를 사용한다고 한다.
추가로 @JoinColumn을 사용하지 않으면 Player와 Team 외에 다른 임의의 테이블이 하나 생성되어 테이블을 join 하므로 @JoinColumn 어노테이션을 사용해야 한다.
일대일(1:1)
선수 한 명이 사물함 하나를 갖는 1대1 관계는 다음과 같이 @OneToOne을 사용해서 작성할 수 있다.
@Entity
public class Player {
@Id
@Column(name = "PLAYER_ID")
private Long id;
@Column(name = "NAME")
private String name;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
@Entity
public class Locker {
@Id
@Column(name = "LOCKER_ID")
private Long id;
}
양방향 관계를 만들고 싶으면 Locker에 Player 필드를 추가하고 mappedBy 속성을 통해 연관관계의 주인으로 지정하면 된다.
그리고 마찬가지로 외래키가 있는 엔티티에 @OneToOne을 적용해야 된다.
다대다(N:M)
다대다 관계는 2개의 테이블로 표현할 수 없어서 일대다, 다대일 관계로 작성한다.
그러나 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계 표현이 가능하다.
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "NAME")
private String name;
@ManyToMany
@JoinTable(name = "ORDER")
private List<Product> products = new ArrayList<>();
}
@Entity
public class Product {
@Id
@Column(name = "PRODUCT_ID")
private Long id;
@Column(name = "NAME")
private String name;
}
중간에서 조인되는 테이블을 @JoinTable로 지정해 주고 @ManyToMany를 사용해서 작성한다.
양방향으로는 Product에 members 리스트를 추가하고 @ManyToMany에 mappedBy로 연관관계 주인을 지정하면 된다.
그러나 다대다 관계 역시 권장되지 않는 방식이기 때문에 DB 테이블처럼 일대다, 다대일 관계로 표현하는 것이 좋다.
즉 중간 테이블 역할을 하는 엔티티를 하나 더 만들어야 한다.
테이블 ERD처럼 Order 엔티티를 만들어서 앞에서 본 @ManyToOne, @OneToMany를 사용해서 작성할 수 있다.
마치며
이번에는 엔티티 객체를 모델링할 때 알아야 할 JPA의 연관관계에 대해서 정리해 봤다.
처음부터 양방향으로 설계하는 것은 번거롭고 어렵다. 심지어 양방향으로 참조할 필요가 없는 경우도 있다.
그렇기 때문에 처음 설계에는 단방향으로 진행하는 것이 좋다.
나 역시 프로젝트를 진행하면서 양방향으로 조회가 필요할 때 엔티티를 수정해서 진행했다.
(물론 엔티티를 수정하는 것도 좋아하지 않는다.)
이번 내용은 JPA를 사용하면 기본적인 내용이지만 그만큼 중요하다. 엔티티 설계부터 막히면 답이 없다.
물론 요즘 ChatGPT가 엔티티도 작성해 주지만 GPT의 코드를 검사하고 잘 이용하는 것도 이런 내용을 알아야 가능하니 잘 숙지해 놓는 게 좋겠다.
'Web > JPA' 카테고리의 다른 글
JPA 영속성 컨텍스트 (0) | 2024.03.03 |
---|