연관관계 매핑 기초

김민우·2022년 9월 3일
0

JPA

목록 보기
4/10

저번 시간에 했던 테이블에 맞춰서 FK키를 그대로 가져오면서 설계하는 방식에서 벗어나 연관관계를 맺어서 어떤식으로 설계하는지 (좀 더 객체지향적으로 설계하는) 알아보자.

궁극적인 목표는 객체와 RDB의 패러다임 차이를 극복하는데에 있다. 이 부분이 굉장히 중요하다.


연관관계가 필요한 이유

객체는 객체끼리의 연관관계(필드 객체 활용)가 존재한다. 그러나 이를 활용하여 객체지향적으로 설계하는 것이 뭔지 근본적으로 아는 것이 매우 어렵다.

예제 시나리오를 보자.

  • 회원과 팀이 있다.
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 회원과 팀은 다대일 관계다.
    • 하나의 팀에 여러 회원이 소속될 수 있다. (회원 : N, 팀 : 1)

일단, 연관관계가 없는 객체를 테이블에 맞추어 모델링해보자.

위의 설계에 맞는 객체 코드

@Entity
public class Member {

	@Id @GeneratedValue
	private Long id;
    
	@Column(name = "USERNAME")
    private String name;
    
    @Column(name = "TEAM_ID")
	private Long teamId; ...
    
	...
}

@Entity
public class Team {

	@Id @GeneratedValue
    private Long id;
    private String name;
	
    ...
}

객체의 저장 및 조회

// 팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);

// 회원 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId());
em.persist(member);
// 조회
Member member = em.find(Member.class, member.getId());

// 연관관계가 없음
Team team = em.find(Team.class, team.getId());
  • 저장 및 조회 시, 연관관계가 없음을 알 수 있다.
  • 이는 객체지향스럽지가 않다.

이렇듯, 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다. 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는 반면 객체는 참조를 사용해서 연관된 객체를 찾는다.

테이블과 객체 사이에는 이런 큰 간격이 있다.


연관관계

1. 양방향 연관관계

가장 중요하고 기본이 되는 단방향 연관관계이다.

  • 객체에서 Team -> Member 는 당연히 안된다.
  • 테이블 연관관계는 된다.

객체의 참조와 테이블의 외래 키를 매핑

@Entity
public class Member {

	@Id @GeneratedValue
	private Long id;
    
	@Column(name = "USERNAME")
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
	private Long teamId; ...
    
	...
}
  • @ManyToOne
    • JPA에게 이 둘의 관계가 1대1인지 다대일인지 알려준다.
  • @JoinColumn(name = "TEAM_ID")
    • Member 객체의 Team 객체 필드와 RDB의 TEAM_ID를 조인

ORM 매핑

객체의 저장, 조회 및 수정

//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);

//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); //단방향 연관관계 설정, 참조 저장
em.persist(member);
//조회
Member findMember = em.find(Member.class, member.getId());

//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();
// 새로운 팀B
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);

// 회원1에 새로운 팀B 설정
member.setTeam(teamB);
  • 이제 참조를 사용하여 로직을 수행한다.

2. 양방향 연관관계

  • Team 객체에 별도로 Member 컬렉션을 필드로 사용하면 양방향 연관관계 구현이 가능하다.

사실 테이블 연관관계는 단방향 양방향이라는 개념이 없다. 당연히 양방향이다.
그러나 객체는 이 개념이 존재한다.

이것이 객체의 참조와 테이블의 외래키의 가장 큰 차이점이다.
그러나, 객체는 가급적이면 단방향이 좋다 (양방향이면 신경쓸게 많아진다.)
객체에서 양방향 연관관계는 단방향 연관관계가 2개 있는 것이다. 억지로 만들었다고 생각하면 된다.

테이블 연관관계에서는 FK 하나로 연관관계가 끝난다. 이 차이점을 반드시 알아두자.

Member 엔티티

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "USERNAME")
    private String username;
    private int age;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    ...
  • 단방향과 동일하다.

Team 엔티티

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
  • Member 컬렉션이 추가됬다.

정방향, 역방향 조회

//조회
Team findTeam = em.find(Team.class, team.getId());

int memberSize = findTeam.getMembers().size(); //역방향 조회

연관관계의 주인

매우 어려운 개념이다. JPA계의 포인터 느낌이다. 이를 제대로 이해하기 위해선 객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.

예제를 활용하여 알아보자.

객체의 양방향 관계

객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단뱡향 관계 2개다.
객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

테이블의 양방향 연관관계

테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계 가진다. (양쪽으로 조인할 수 있다.)

객체와 테이블이 관계를 맺는 차이

객체 연관관계 : 2개

  • 회원 -> 팀 연관관계 1개(단방향)
  • 팀 -> 회원 연관관계 1개(단방향)

테이블 연관관계 : 1개

  • 회원 <-> 팀의 연관관계 1개(양방향)

.
.
.
.

양방향이 되면 즉, 둘 중 하나로 외래 키를 관리해야 하는 딜레마가 생긴다.
(Memberteam 을 바꿔야 하는지 아니면 Teammembers 를 바꿔야하는지...)

이럴 때 사용하는 것이 연관관계의 주인(Owner)이다.

연관관계의 주인(Owner)

양방향 매핑 규칙

  • 객체의 두 관계중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정)
  • 주인이 아닌쪽은 읽기만 가능
  • 주인은 mappedBy 속성을 사용 X
  • 주인이 아니면 mappedBy 속성으로 주인 지정
  • 즉, mappedBy 속성은 주인을 나타낸다.

그럼 과연 누구를 주인으로 해야할까? 외래 키가 있는 곳을 주인으로 정하면 된다.

PK를 주인으로 하면 업데이트 쿼리가 이상하게 나가는 현상이 발생한다.
(Team 객체를 수정했는데 Member 객체에서 Update Query가 나간다거나...)

DB의 FK가 있는 쪽(N)이 무조건 연관관계 주인이 된다.
그러나 연관관계 주인이라고 이게 비즈니스적으로 결정적인 역할을 하는 것은 아니다.

예를 들어, 자동차와 자동차 바퀴가 있다고 생각해보자. 자동차가 당연히 중요하지만 연관관계 주인은 자동차 바퀴로 해야한다!

이렇게 해야 설계가 깔끔해지고 성능 이슈도 없고 Entity와 테이블이 매핑되있는 연관관계에서 관리가 된다. (엉뚱하게 쿼리가 나가는 것을 방지)
연관관계 매핑 설계는 이 기준만 명심하면 된다!

위의 예제에서는 Member.team이 연관관계의 주인이다.

양방향 매핑시 가장 많이 하는 실수

1. 연관관계의 주인에 값을 입력하지 않는 경우

Team team = new Team(); 
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");


//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);

em.persist(member);

결과

  • 당연히 값이 입력되지 않는다.

2. 순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 한다

Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");


team.getMembers().add(member); //연관관계의 주인에 값 설정
member.setTeam(team); //**

em.persist(member);

결과
업로드중..

3. 그 외

순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자.

양방향 연관관계는 양쪽에서 매핑을 해줘야한다. 이를 연관관계 편의 메소드라 한다.

연관관계 편의 메소드나 JPA의 상태를 변경하는 메소드는 getter, setter 관례 때문에setXXX을 잘 사용하지 않는다.

이러한 연관관계 편의 메소드를 사용할 때는 중복 사용을 조심하자.

양방향 매핑에서는 무한 루프를 조심하자. (서로를 반복적으로 호출해버리면서 stack overflow 발생한다.)

참고
컨트롤러에서 Entity 를 절대 반환하지말자. 이유는 다음과 같다.

  1. 무한 루프 발생
  2. Entity 는 변경될 수 있다. Entity 를 API 반환해버리면 API를 변경하는 순간 API 스펙이 바뀌어버린다.

따라서, 컨트롤러에서는 DTO로 반환하는 것을 권장한다.


정리

단방향 매핑만으로도 이미 연관관계 매핑은 완료된다.
양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.

실무에서는 객체만으로는 설계가 힘들다.
테이블 설계를 고려하면서 객체 설계를 해야한다.

그 시점에 연관관계나 PK, FK가 어느정도 결정된다.

처음에는 단방향 매핑으로 하고 양방향 매핑이 필요한 경우 추가하자!(양방향은 단방향 2개니간, 테이블에 영향을 주지 않으므로)

물론, 계속 이야기하지만 객체입장에서 양방향으로 추가하면 별로 좋진 않다.
그렇지만 역방향 참조할일은 생각보다 많다. (JPQL에서 자주 사용된다.)

0개의 댓글