객체와 관계형 데이터베이스의 패러다임 차이
객체는 참조를 통해 연관관계를 같는다
class Member {
private Long id;
private String name;
private String age;
private Team team; // 참조를 통한 연관관계
}
class Team {
private Long id;
private String name;
}
데이터베이스는 PK와 FK를 통해 연관관계를 같는다.
예시 - 객체형 데이터베이스를 기준으로 설계
객체를 테이블에 맞추어 데이터 중심으로 모델링하면 협력 관계를 만들 수 없다.
class 생성
// * Member class
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ"
)
class Member {
@Id @GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "MEMBER_SEQ_GENERATOR"
)
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "TEAM_ID")
private Long teamId;
private String name;
// setter, getter
}
// * Team class
@Entity
@SequenceGenerator(
name = "TEAM_SEQ_GENERATOR",
sequenceName = "TEAM_SEQ"
)
public class Team {
@Id @GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "TEAM_SEQ_GENERATOR"
)
@Column(name = "TEAM_ID")
private Long id;
private String name;
// setter, getter
}
main
// * main method
public string void main(String[] args) {
// EntityManager setting
// insert
// team
Team team = new Team();
team.setName("TeamA");
em.persist(team);
// member
Member member = new Member();
member.setName("MemberA");
member.setTeamId(team.getId());
em.persist(member);
// select
// member
Member findMember = em.find(Member.class, member.getId());
// team
Team findTeam = em.find(Team.class, findMember.getTeamId());
// EntityManager cloas
}
Member 클래스의 getTeam, setTime 메소드를 사용할 수 없다.
테이블은 왜래키로 조인을 통한 연관된 테이블을 찾지만, 객체는 참조를 사용하여 연관된 객체를 찾으므로 이런 패러다임이 발생한다.
단방향, 양방향 연관관계를 통해 이 패러다임을 해결한다.
단방향 연관관계
Member class
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ"
)
class Member {
@Id @GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "MEMBER_SEQ_GENERATOR"
)
@Column(name = "MEMBER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
Entity 객체는 참조가 발생한 경우 두 객체가 어떤 관계인지를 알려줘야한다.
DB에서 하나의 팀은 여러 맴버를 같기 때문에 Member는 여러개가 입력된다.
여기서는 다대일의 @ManyToOne 어노테이션을 작성하였다.
또한 DB에서 PK와 FK처럼 연관관계를 해주기 위해 @JoinColumn 어노테이션을 붙인다.
main method
// * main method
// insert
// team
Team team = new Team();
team.setName("TeamA");
em.persist(team);
// member
Member member = new Member();
member.setName("MemberA");
member.setTeam(team);
em.persist(member);
// select
// member
Member findMember = em.find(Member.class, member.getId());
// team
Team findTeam = findMember.getTeam();
System.out.println();
System.out.println("findMember.getId() : " + findMember.getId());
System.out.println("findMember.getName() : " + findMember.getName());
System.out.println("findTeam.getId() : " + findTeam.getId());
System.out.println("findTeam.getName() : " + findTeam.getName());
System.out.println();
현재는 영속성 컨텍스트의 1차 캐시에서 데이터를 가져오는데, DB에서 조회하도록 영속성 컨텍스트를 초기화 시키면 조회 쿼리를 볼 수 있다.
EntityManager.flush();
EntityManager.clear();
양방향 연관관계와 연관관계의 주인
우선 중요한 개념이라는 것을 강조하는 내용이다.
단방향 연관관계에서 Member는 Team을 참조하고, Member.getTeam()을 통해 Team의 필드를 읽어올 수 있었다.
- Member -> Team : @ManyToOne, @JoinColumn을 통해 설정하였다.
그렇다면 "Team에서는 자신에게 포함된 Member를 조회할 수 있는 방법이 없을까?"에 대한 답은 무엇일까.
양방향 연관관계의 사용 시점
- 단방향 매핑만으로 연관관계 설계는 완료된 것이다.
- 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.
- 단뱡향 매핑으로 설계를 끝난 후에 정말 필요한 경우에 양방향 매핑을 추가하면 된다 (테이블에 영향을 주는게 아니기 때문)
- 객체를 생각하면 양방향 매핑의 이득은 없다. 오히려 양측에 순수 객체 상태를 고려하여 설정해야 하는 부분이 늘어날 뿐이다.
관계형 데이터베이스의 양방향 연관관계
관계형 데이터베이스에서는 FK를 통해 양방향 연관관계를 갖는다.
MEMBER 테이블에서는 가지고 있는 TEAM_ID(FK)를 통해 TEAM을 JOIN하여 TEAM의 데이터를 같이 불러올 수 있고,
SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID;
TEAM 테이블에서는 TEAM_ID(PK)를 사용하고 있는 MEMBER의 TEAM_ID(FK)를 통해 데이터를 가져올 수 있다.
SELECT * FROM TEAM T JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID;
즉 관계형 데이터베이스에서는 FK만 존재하면 양방향으로 데이터를 가져올 수 있다는 점이다.
그래서 객체에서 양방향 연관관계를 맺는다고 하여 데이터베이스의 설계가 바뀌는 점은 없다.
객체의 양방향 연관관계
Member 객체에서 Team을 갖고 있고, Team에서는 Members를 갖고 있다고 생각해본다.
Member에서는 자신이 참조하고 있는 Team을 통해 데이터를 가져올 수 있다.
Member member = new Member();
// ...
member.getTeam().getName();
Team에서는 자신이 참조하고 있는 List<Member>를 통해 데이터를 가져와야 한다.
Team team = new Team();
...
for(Member m : team.getMembers()) {
m.getName();
}
즉 객체에서는 관계형 데이터베이스처럼 하나의 정의된 키를 통해 데이터를 읽어올 수 없고,
단방향 관계를 두개 맺어야 한다.
- Member -> Team
- Team -> Member
예시
Team class
@Entity
@SequenceGenerator(
name = "TEAM_SEQ_GENERATOR",
sequenceName = "TEAM_SEQ"
)
public class Team {
@Id @GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "TEAM_SEQ_GENERATOR"
)
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
TEAM 테이블 하나의 데이터(PK)는 여러개의 MEMBER 테이블에서 가지고 있을 수 있다.(FK를 통해)
그래서 Team 객체에서는 @OneToMany 어노테이션을 붙인다.
mappedBy가 없으면 정상적으로 조회가 안되고, hibernate.hbm2ddl.auto = "create" 일 경우 추가적인 테이블이 생성된다.
Team과 Member 객체간에 외래키를 관리할 주체를 찾지 못해서 TEAM_MEMBER 테이블 생성된다.
mappedBy란 연관관계의 주인(외래키를 관리할 주체)을 참조하는 객체에 주는 것이다.
- Member 필드 중 team으로 선언된 변수를 참조하겠다는 뜻이다.
- 즉 mappedBy는 Member의 team으로부터 매핑 되었다는 의미이다.
- 그래서 Member와 Team의 연관관계에서의 주인은 Member가 된다.
- mappedBy는 "연관관계의 주인이 아니다"라고 설정하는 것이다.
- 데이터가 변경되더라도 List<Member> members는 영속성 컨텍스트에서 변경을 감지하지 않는다.
- 연관관계의 주인이 아니기 때문에 입력, 삭제, 수정이 불가능하고, 읽기만을 지원한다.
연관관계의 주인
이 개념이 필요한 이유를 예를 들어 설명하면,
데이터베이스는 FK만으로 관계를 맺고, 이 FK만 관리된다면 관계에 문제가 생기지 않는다.
MEMBER 테이블은 TEAM_ID(FK)를 가지고 있는데, 이 데이터는 언제 업데이트가 되어야 할까?
- Member 객체의 team이 변경될 때
- Team 객체의 List<Member> members가 변경될 때
위의 이유 때문에 둘 중 하나로 외래키를 관리해야 한다.
Member와 Team 객체에서 FK를 관리할 주체가 연관관계의 주인이고,
객체가 매핑되는 테이블에 FK를 사용한다면 그 객체를 연관관계의 주인으로 설정해야 성능 및 관리가 용이하다.
main method
// insert
// team
Team team = new Team();
team.setName("TeamA");
em.persist(team);
// member
Member member = new Member();
member.setName("MemberA");
member.setTeam(team);
em.persist(member);
// member2
Member member2 = new Member();
member2.setName("MemberB");
member2.setTeam(team);
em.persist(member2);
//
em.flush();
em.clear();
// select
Team findTeam = em.find(Team.class, team.getId());
for(Member m : findTeam.getMembers()) {
System.out.println("m.getName() : " + m.getName());
}
양방향 매핑시 주의점 1
// * Main method
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setName("memberA");
member.setTeam(team);
em.persist(member);
System.out.println("member.getName() : " + member.getName());
System.out.println("for sout before");
for(Member m : team.getMembers()) {
System.out.println("m.getName() : " + m.getName());
}
System.out.println("for sout after");
위의 코드에서 m.getName()은 값이 없다.
영속성 컨텍스트에서 Team과 Member를 Team의 List<Member>에는 Member를 추가하지 않았기 때문이다.
이런 문제를 해결하기 위해서는 EntityManager의 flush, clear 메소드를 조회전에 실행하거나,
Member 클래스에 설정해주는 메소드가 필요하다.
private void teamChange() {
this.team = team;
this.team.getMembers().add(this);
}
순수 객체 상태를 고려하여 항상 양쪽에 값을 설정해야 한다.
또는 member객체가 변경되었을 경우, 삭제되었을 경우 또한 필요하다면 설정해야 한다.
양방향 매핑시 주의점 2
주의점 1 처럼 설정하는 메소드를 추가할 때 양쪽(Team, Member) 객체에 동시에 존재하면 버그가 발생할 수 있으므로 한 객체에만 사용하는 걸 권장한다.
양방향 매핑시 주의점 3
// * main method
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setName("memberA");
em.persist(member);
team.getMembers().add(member);
위의 코드를 실행하면 Insert 쿼리는 Team, Member 각각 실행되지만, 정상적인 값이 들어가지 않는다.
mappedBy를 사용한 필드에 값을 변경하여도 영속성 컨텍스트에서 관리되지 않으므로 무시되었다.
정상적인 코드를 실행하려면 연관관계의 주인인 Member 객체에 team필드를 변경하여야 한다.
양방향 매핑시 주의점 4
무한 루프를 주의해야 한다.
예를 들면 toString()과 toString을 자동적으로 생성해주는 lombok이 있다.
Member와 Team에 toString 메소드를 만들고, 조회 후 print를 찍으면 stackoverflow가 동작된다.
// * main method
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setName("memberA");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println(findMember);
원인은 findMember를 toString으로 변환하면서 그 내부에 있는 Team을 호출하고,
Team에서는 List<Member> 내의 member를 호출하면서 무한루프에 빠진다.
양방향 매핑시 주의점 5
Controller에서 Entity 객체를 그대로 리턴하면 JSON 생성 라이브러리가 toString을 호출하여 4번과 같은 문제가 발생할 여지가 있고,
Entity 객체의 설계를 수정한다면, Response의 명세가 바뀌기 때문이다
- Entity 객체의 설계는 데이터베이스의 설계를 따르므로
계층간의 이동 객체(DTO)를 통해 Response를 보내야 한다.
Reference
자바 ORM 표준 JPA 프로그래밍(인프런, 김영한)
'framework > jpa' 카테고리의 다른 글
[JPA] 상속관계 매핑, mappedSuperclass (0) | 2023.04.23 |
---|---|
[JPA] 엔티티 매핑 3 (0) | 2023.04.23 |
[JPA] 엔티티 매핑 1 (0) | 2023.04.08 |
[JPA] 기본 사용 방법 (0) | 2023.04.07 |
[JPA] JPA 사용 이유 및 개념 정리 (0) | 2023.04.07 |