2. 엔티티 매핑: 연관관계 매핑

xellos·2022년 6월 19일
0

JPA

목록 보기
5/7

엔티티들은 대부분 다른 엔티티와 연관관계가 있다. 그런데 객체는 참조를 사용해서 관계를 맺고 테이블은 외래키를 사용해서 관계를 맺는다. 기본개념에서 보았듯 이 둘은 완전히 다른 특징을 가진다.

객체 관계 매핑(ORM)에서 가장 어려운 부분이 바로 이 객체 연관관계와 테이블 연관관계를 매핑하는 일이다.

이번장의 핵심 키워드는 다음과 같다.

  • 방향: [ 단방향, 양방향 ] 이있다. 서로 참조하면 양방향, 한 쪽만 참조하면 단방향이라고 한다. (객체의 관점)
  • 다중성: [ 다대일, 일대다, 일대일, 다대다 ]
  • 연관관계의 주인: 객체를 양방향으로 연관관계를 만들려면 연관관계의 주인을 정해야 한다.

단방향 연관관계

  • 객체 연관관계: 회원 객체는 필드로 팀 객체와 연관관계를 가진다. 회원 객체와 팀 객체는 단방향 관계이다.
  • 테이블 연관관계: 회원 테이블은 TEAM_ID 외캐키로 팀 테이블과 연관관계를 맺는다. 회원 테이블과 팀 테이블은 양방향 관계다.

1) 객체 연관관계와 테이블 연관관계의 가장 큰 차이

객체의 참조를 통한 연관관계는 언제나 단방향이다. 객체 간에 연관관계를 양방향으로 가지고 싶으면 서로 참조를 가져야 한다. 그러나 이는 사실 양방향 관계가 아니아 서로 다른 단방향 관계 2개다.
또한 객체는 연관관계를 탐색할 때 member.getTeam()과 같은 참조를 이용하며 테이블은 조인을 이용한다.

2) 객체 관계 매핑

위의 그림대로 엔티티를 매핑하고 싶다면 다음과 같이 하면된다.

Member 엔티티

  • @ManyToOne : 이름 그대로 다대일 관계라는 매핑정보다. 연관관계를 매핑할 때 이렇게 다중성을 나타내는 애노테이션을 필수로 사용해야 한다.
  • @JoinColumn : 조인 컬럼은 외래키를 매핑할 때 사용된다. name 속성에는 매핑할 외래키 이름을 지정한다.
    • 해당 이름을 가지는 FK 컬럼이 생성된다.
    • 이 애노테이션은 생략할 수도 있다. (기본값: 필드명 + "_" + 찹조하는 테이블 PK 컬럼명)
@Entity
@Getter @Settter
public class Member {
	
    @Id @Column(name="MEMBER_ID")
    private String id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;
    
    ...
}

Team 엔티티

@Entity
@Getter @Setter
public class Team {
	
    @Id @Column(name="TEAM_ID")
    private String id;
    
    private String name;
    
    ...
}

연관관계 사용

1) 저장

JPA 저장에서 엔티티를 저장할 때 모든 엔티티는 영속 상태여야 한다.

  • 다음과 같이 일반 객체의 참조와 같이 설정을 하면 JPA는 참조한 팀의 식별자를 외래키로 사용해서 적절한 등록 쿼리를 생성한다.
Team t1 = new Team("team1", "팀");
em.persist(t1);

Member m1 = new Member("member1", "A");
m1.setTeam(t1);	//연관관계 설정
em.persist(m1);

Member m2 = new Member("member2", "B");
m2.setTeam(t1);	//연관관계 설정
em.persist(m2);

2) 조회

연관관계가 있는 엔티티를 조회하는 방법은 크게 두가지다.

객체 그래프 탐색

Member m1 = em.find(Member.find, "member1");
Team t1 = m1.getTeam();

객체 지향 쿼리 사용(JPQL)

참고로 :teamName 과 같이 : 로 시작하는 것은 파라미터를 바인딩받는 문법이다.

String jpql = "SELECT m FROM Member m JOIN m.team WHERE t.name = :teamName";

List<Member> resultList = em.createQuery(jpql, Member.class)
							.setParameter("teamName", "팀1")
                            .getResultList();

3) 수정

Team t2 = new Team("team2", "팀2");
em.persist(t2);

Member m1 = em.find(Member.class, "member1");
m1.setTeam(t2);

4) 연관관계 제거

Member m1 = em.find(Member.class, "member1");
m1.setTeam(null);	//연관관계 제거

5) 연관된 엔티티 삭제

연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다. 그렇지 않으면 외래키 제약 조건으로 인해, DB 오류가 발생한다.

m1.setTeam(null);	//연관관계 제거
m2.setTeam(null);	//연관관계 제거
em.remove(t1);		//팀 삭제

양방향 연관관계

객체는 서로의 참조를 가지고 있지만 DB 테이블은 한쪽만 참조를 가지고 있어도 서로 조회가 가능하다.

1) 양방향 연관관계 매핑

Member 엔티티

@Entity
@Getter @Setter
public class Member {
	...
    
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;
    
}

Team 엔티티

팀과 회원은 일대다 관계다. 따라서 팀 엔티티에서 컬렉션인 List<Member> members를 추가했다. 그리고 일대다 관계를 매핑하기 위해 @OneToMay 매핑 정보를 사용했다.

  • mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑 필드의 이름을 이름값으로 주면된다.
@Entity
public class Team {
	
    @OneToMany(mappedBy="team")
    private List<Member> members = new ArrayList<>();
    
}

2) 일대다 컬렉션 조회

Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers();

연관관계의 주인

@OneToMany는 직관적으로 이해가 될것이다. 문제는 mappedBy 속성이다. 단순히 @OneToMany만 있으면 될거 같은데 왜일까?

엄밀히 말하면 객체에는 양방향 연관관계라는 것이 없다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 앙뱡향인 것 처럼 보이게 할 뿐이다. 그러나 DB는 테이블 외래키 하나를 이용해서 양쪽이 서로 조인한다.

여기서 차이가 발생하는데 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래키는 하나로 둘의 차이가 발생한다. → 이 차이로 JPA 에서는 두 객체중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이를 연관관계의 주인이라고 한다.

1) 양방향 매핑의 규칙: 연관관계의 주인

연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아니면 읽기만 할 수 있다. 어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 된다.

  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
  • 연관관계의 주인을 정한다는 것은 사실 외래키 관리자를 정하는 것이다.

2) 연관관계의 주인은 외래 키가 있는 곳

연관관계의 주인은 테이블에 외래키가 있는 곳으로 정해야 한다. 위에서는 회원 테이블이 외래키를 가지고 있으므로 Member.team 이 주인이 된다. 주인이 아닌쪽은 mappedBy를 사용하여 주인이 아님을 설정한다.

정리하면 연관관계의 주인만 DB 연관관계와 매핑되고 외래키를 관리할 수 있다. 주인이 아닌 반대편은 읽기만 가능하다. DB 테이블의 다대일, 일대다 관계에서는 항상 다쪽이 외래키를 가진다. → 따라서 @ManyToOne 에는 mappedBy 속성이 없다.


양방향 연관관계 저장

  • 양방향 연관관계는 연관관계의 주인이 외래키를 관리한다.
  • 따라서 주인이 아닌 방향은 값을 설정하지 않아도 DB에 외래키 값이 정상 입력된다.
Team t1 = new Team("team1", "팀");
em.persist(t1);

Member m1 = new Member("member1", "A");
m1.setTeam(t1);	//연관관계 설정
em.persist(m1);

Member m2 = new Member("member2", "B");
m2.setTeam(t1);	//연관관계 설정
em.persist(m2);

1) 주의사항

주인이 아니므로 다음과 같은 코드는 무시된다.

t1.getMembers().add(m1);
t1.getMembers().add(m2);

2) 순수한 객체까지 고려한 양방향 연관관계

그렇다면 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않는것은 어떨까?
→ 객체 관점에서 양쪽 모두 값을 입력해주는 것이 안전하다. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체상태에서 심각한 문제가 발생할 수 있다.
→ 객체까지 고려하여 주인이 아닌 곳에도 값을 입력하자.

연관관계 편의 메서드

양방향 연관관계는 결국 양쪽이 다 신경써야 한다. 서로 다른 멤버를 각각 호출하다보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있다.

member.setTeam(team);
team.getMembers().add(member);

양방향 관계에서 위의 두 코드를 하나인것 처럼 사용하는 것이 안전하다.

  • setTeam() 메서드 하나로 양방향 관계를 모두 설정하도록 변경했다. 이렇게 하면 연관관계의 주인인 Member가 Team을 가질 때 객체지향적으로 Team 역시 Member를 가진다.
  • 이러한 메서드를 연관관계 편의 메서드라고 한다.
public class Member {
	private Team team;
    
    public void setTeam(Team team) {
    	//기존 팀과 관계를 제거
       	if(this.team != null) {
        	this.team.getMembers().remove(this);
        }
        
        this.team = team;
        team.getMembers().add(this);
    }
|

앙뱡향 매핑시에는 무한 루프에 빠지지 않도록 조심해야 한다. 예를 들어 Member.toString() 에서 getTeam()을 호출하고 Team.toString() 에서 getMember()를 호출하면 무한 루프에 빠질 수 있다.

0개의 댓글