05. 연관관계 매핑 기초

zwundzwzig·2023년 2월 12일
0
post-thumbnail

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

엔티티들은 대부분 다른 엔티티와 연관관계를 갖는다. 그런데 엔티티 객체는 참조(주소)를 사용해 관계를 맺고 테이블은 외래키로 관계 맺는다.

다음은 객체의 참조와 테이블의 외래 키를 매핑하기 위한 핵심 키워드이다.

  • 방향 : 단방향, 양방향이 있다. 이는 객체에만 존재하고 테이블은 항상 양방향이다.
  • 다중성 : 흔히 말하는 '몇 대 몇' 관계이다.
  • 연관관계 주인 : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.

단방향 연관관계

연관관계의 시작이 다대일 단방향 관계이다.

객체 연관관계
회원 객체는 Member.team 참조로 팀 객체와 연관관계를 맺는다. 멤버에서 팀으로만 조회가 가능한 단방향 관계이다.
테이블 연관관계
회원 테이블은 TEAM_ID 외래키로 팀 테이블과 연관관계를 맺는다. 외래키를 통해 서로 조회가 가능한 양방향 관계이다.

순수한 객체 연관관계 & 테이블 연관관계

다음은 JPA를 사용하지 않은 순수한 회원과 팀 클래스의 코드다.

public class Member {
	private String id;
    private String name;
    private Team team; // 팀의 참조를 보관
    public void setTeam(Team team) { this.team = team; }
    ...
}

public class Team {
	private String id;
    private String name;
    ...
}

pulblic void main(String[] args) {
	Member mem1 = new Member("member1", "회원1");
	Member mem2 = new Member("member2", "회원1");
    Team team1 = new Team("team1", "팀1");
    
    member1.setTeam(team1);
    member2.setTeam(team1);
    
    Team findTeam = member1.getTeam();
}

객체는 참조를 통해 연관관계를 탐색할 수 있는데, 이를 객체 그래프 탐색이라 한다.

반면, DB는 외래키를 사용해 연관관계를 탐색하고 이는 JOIN이다.

객체 관계 매핑

@Entity
public class Member {
	@Id @Column(name = "MEMBER_ID")
    private String id;
    
    private String name;
    
    @ManyToOne // 연관관계 매핑
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    // 연관관계 설정
    public void setTeam(Team team) { this.team = team; }
    ...
}

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

회원 객체의 Member.team 필드로 객체 연관관계를 매핑하고, 테이블의 MEMBER.TEAM_ID 외래키를 통해 테이블 연관관계를 맺었다.

@JoinColumn

생략시 기본 전략을 사용한다.

속성기능기본값
name매핑할 외래키 이름필드명 + _ + 참조하는 테이블 기본 키 컬럼명
referencedColumnName외래키가 참조하는 대상 테이블의 컬럼명참조하는 테이블의 기본키 컬럼명
foreignKey(DDL)외래키 제약 조건을 직접 지정. 테이블 생성할 때만.
나머지 속성들@Column의 속성과 같다.

@ManyToOne

다대일 관계에서 사용된다.

속성기능기본값
optional매핑할 외래키 이름true
fetch글로벌 페치 전략 설정다대일 : EAGER, 일대다 : LAZY
cascade영속성 전이 기능 사용.
targetEntity연관된 엔티티의 타입 정보를 설정. 컬렉션으로 대체돼 거의 사용 안 함

연관관계의 사용

저장

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

조회

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

  • 객체 그래프 탐색
  • 객체 지향 쿼리 JPQL 사용

이중 JPQL로 조회하는 코드를 보자.

private static void queryLogicJoin(EntityManager em) {
	String jpql1 = "select m from Member m join m.team t where t.name=:teamName";
    // :teamName 같이 :로 시작하는 것은 파라미터를 바인딩 바든 문법이다.
    List<Member> resultList = em.createQuery(jpql, Member.class)
    	.setParameter("teamName", "팀1")
    	.getResultList();
        
    for(Member member : resultList) {
    	System.out.println("[query] member.name= member.getName());
    }
}

from Member m join m.team t 부분을 보면 회원이 팀과 관계를 갖는 필드(m.team)를 통해 멤버와 팀을 조인했고, 조인한 t.name을 검색 조건으로 사용해 팀1에 속한 회원만 검색했다.

JPQL은 엔티티를 대상으로 하고 SQL보다 간결하다.

수정

// 새로운 팀B
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);

// 회원1에 새로운 팀B 설정
member.setTeam(teamB);

단순히 불러온 엔티티의 값만 변경하면 트랜잭션을 커밋할 때 플러시가 일어나 변경 감지 기능이 작동.

변경 사항을 DB에 자동으로 반영

연관관계 제거

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

연관된 엔티티 삭제

member1.setTeam(null); //회원1 연관관계 제거
member2.setTeam(null); //회원2 연관관계 제거
em.remove(team); //팀 삭제

양방향 연관관계

지금까지 회원에서 팀으로만 접근하는 다대일 단방향 매핑을 알아보았다. 이번엔 반대 방향인 팀에서 회원으로 접근하는 관계를 추가하자. 그래서 회원에서 팀으로 접근하고 반대 방향인 팀에서도 회원으로 접근할 수 있도록 양방향 연관관계로 매핑해보자.

JPA는 Collection, Set, Map 같은 다양한 컬렉션을 지원한다.

양방향 연관관계 매핑

@Entity
public class Team {
	@Id @Column(name = "TEAM_ID")
    private String id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
    ...
}

일대다 컬렉션 조회

public void biDirection() {
	Team team = em.find(Team.class, "team1");
    List<Member> members = team.getMembers(); // 팀 -> 회원, 객체 그래프 탐색
    for(Member member : members) {
    	System.out.println("member.name = " + member.getName());
	}
}

연관관계의 주인

객체에는 엄밀히 양방향 연관관계라는 게 없다. 단방향 연관관계 2개를 양방향인 것처럼 보이게 할 뿐이다.

엔티티를 양방향으로 매핑하면 두 곳에서 서로를 참조해 연관관계를 관리하는 포인트는 2곳으로 늘어난다.

엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 테이블의 외래 키는 하나이다. 따라서 둘 사이에 차이가 발생한다. 그렇다면 둘 중 어떤 관계를 사용해 외래키를 관리할까?

이런 차이로 인해 JPA는 두 객체 연관관계 중 하나를 정해 테이블의 외래 키를 관리해야 하는데, 이를 연관관계의 주인이라 한다.

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

양방향 연관관계 매핑 시 둘 중 하나를 연관관계의 주인으로 정해야 한다.

주인만이 DB 연관관계와 매핑되고 외래 키 관리 및 CRUD할 수 있다. 반면 주인이 아닌 쪽은 읽기만 가능하고 mapped by로 주인을 가리켜야 한다.

사실 연관관계의 주인을 정한다는 건, 외래 키 관리자를 선택하는 것이다.

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

연관관계의 주인은 외래키가 있는 테이블에 정해야 한다.

DB 테이블의 다대일, 일대다 관계에선 항상 많은 쪽이 외래 키를 갖는다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이므로, @ManyToOne에선 mappedBy 속성이 없다.

양방향 연관관계 저장

public void testSave() {
	
    Team team1 = new Team("team1", "팀1");
    em.persist(team1);
    
    Member member = new Member("member1", "회원1");
    member1.setTeam(team1); // 연관 관계 설정 member1 => team1
    em.persist(member1);
    
    Member member = new Member("member2", "회원2");
    member2.setTeam(team1); // 연관 관계 설정 member2 => team1
    em.persist(member2);
}

팀1을 저장하고 회원 1, 2에 연관관계 주인인 Member.team 필드를 통해 회원과 팀의 연관관계를 설정하고 저장했다.

참고로 이 코드는 단방향 연관관계에서 살펴본, 회원과 팀을 저장하는 코드와 완전히 같다.

양방향 연관관계는 주인이 외래 키를 관리하기 때문에 주인이 아닌 방향은 값을 설정하지 않아도 DB에 외래 키 값이 정상 입력된다. 주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않는다.

양방향 연관관계의 주의점

가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고 주인이 아닌 곳에만 입력하는 것이다.

public void testSaveNonOwner() {
	Member member1 = new Member("member1", "회원1");
    em.persist(member1);
    
	Member member2 = new Member("member2", "회원2");
    em.persist(member2);
    
    Team team1 = new Team("team1", "팀1");
    team1.getMembers().add(member1);
    team1.getMembers().add(member2);
    
    em.persist(team1);
}

위 코드의 결과는 외래 키 TEAM_ID에 값이 null로 입력될 것이다. 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문이다.

연관관계 주인만이 외래 키의 값을 변경할 수 있다. 위 코드에선 Member.team에 아무런 값도 입력하지 않았기 때문에 null이 나온 것이다.

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

정말 연관관계의 주인에게만 값을 주고 나머지는 값을 저장하면 안되나?

객체 관점에선 양쪽 모두 값을 입력하는 게 안전하다. JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수도 있기 때문이다.

JPA를 사용하지 않고 엔티티에 대한 테스트 코드를 작성한다고 가정해보자. ORM은 객체와 RDBMS 둘 다 중요하다.

public void test_순수한_객체_양방향() {

	Team team1 = new Team("team1", "팀1");
	Member member1 = new Member("member1", "회원1");
	Member member2 = new Member("member2", "회원2");
	
    member1.setTeam(team1); // 연관관계 설정, 연관관계의 주인, 이 값으로 외래 키를 관리한다.
    member2.setTeam(team1); // 연관관계 설정, 연관관계의 주인, 이 값으로 외래 키를 관리한다.
    
    List<Member> members = team1.getMembers();
    System.out.println("members.size = " + members.size());
    // members.size = 0
    
}

코드만 보면 Member.team에만 연관관계를 설정하고 반대편엔 하지 않았다. team1.getMembers().add(member1) 따위의 코드가 필요할 것이다. 물론 Team.member는 주인이 아니기 때문에 저장 시 사용되지 않는다.

연관관계 편의 메소드 작성 시 주의사항

양방향 연관관곈는 결국 양쪽 모두 신경써야 한다. 위에서 양방향 연관관계 저장하는 로직이 바로 편의 메소드인 setter 메소드이다.

사실 이 메소드도 문제가 있다. 만약 연관관계를 설정할 때 member1이 teamA와 연결했다가 teamB로 바꿀 때 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.

public void setTeam(Team team) {
	if(this.team != null) this.team.getMembers().remove(this);
    this.team = team;
    team.getMembers().add(this);
}

이처럼 서로 다른 단방향 연관관계 2개를 양방향인 것처럼 보이게 하려고 얼마나 많은 고민과 수고가 필요한지 보여준다. 그만큼 로직을 견고히 해야 양방향 연관관계를 잘 다룰 수 있다.

🧷 참조 교재

  • 김영한, 『자바 ORM 표준 JPA 프로그래밍』 에이콘(2015)
profile
개발이란?

0개의 댓글