JPA 톺아보기 - 관계 매핑

Janek·2023년 1월 10일
0

JPA 톺아보기

목록 보기
3/10
post-thumbnail
post-custom-banner

해당 포스팅은 인프런에서 제공하는 김영한 님의 '자바 ORM 표준 JPA 프로그래밍 - 기본편'을 수강한 후 정리한 글입니다. 유료 강의를 정리한 내용이기에 제공되는 예제나 몇몇 내용들은 제외하였고, 정리한 내용을 바탕으로 글 작성자인 저의 언어로 다시 작성한 글이기에 서술이 부족하거나 잘못된 내용이 있을 수 있습니다. 그렇기에 해당 글은 개념에 대한 참고 정도만 해주시고, 강의를 통해 학습하시기를 추천합니다.

연관 관계

JPA의 목적은 객체 지향 프로그래밍과 데이터 베이스 사이의 패러다임 불일치를 해결하는 것이다. 따라서 객체와 관계형 데이터 베이스의 테이블이 정확하게 매핑되어야 한다.

public class Member {
	private Long id;
    private Long teamId;
    private String username;
}

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

위의 예시는 테이블 연관 관계에 맞춰 객체를 모델링한 것이다. RDBMS에서는 외래 키를 통해 테이블 간에 연관 관계를 맺기 때문에 이에 맞춰 객체를 모델링하게될 경우 객체가 객체를 참조하는 것이 아닌 외래 키(teamId)를 통해서 두 객체간에 데이터를 조회하게 된다. 이는 전혀 객체 지향적인 방법이 아니며, 두 객체간 협력 관계를 만들 수도 없다.

연관관계 매핑을 이해하기 위해서는 다음 키워드들에 대한 이해가 선행되어야 한다.

  • 방향(Direction) : 단방향양방향이 있다. 두 객체간 관계를 맺을 때 한 쪽만 참조할 수 있는 상태를 단방향 관계라 하며, 서로 참조할 수 있는 상태를 양방향 관계라 한다. 이러한 방향은 객체관계에서만 성립하며, 테이블은 항상 양방향이다.
  • 다중성 (Multiplicity) : 다대일(N : 1), 일대다(1 : N), 일대일(1 : 1), 다대다(N : N) 다중성이 있다.
  • 연관관계의 주인(owner) : 객체를 양방향 연관관계로 만들기 위해서는 연관관계의 주인을 정해야 한다.

단방향 연관 관계

단방향 연관 관계는 엔티티의 관계가 한 쪽에서만 참조되는 관계를 의미한다. 간단한 예시를 통해 살펴 보자.

@Entity
public class Member {

	@Id	@GeneratedValue
	private Long id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

@Entity
public class Team {

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

위의 코드는 처음의 예시를 ORM 매핑을 통해 단방향으로 아래와 같이 객체 지향 모델링한 모습이다.

// 저장
Team team = new Team();
...
엔티티메니저.persist(team);

Member member = new Member();
...
member.setTeam(team);
엔티티메니저.persist(member);
// 조회
Member findMember = em.find(Member.class, member.getId);

Team findTeam = findMember.getTeam();
// 수정
Team anotherTeam = new Team();
...
엔티티메니저.persist(anotherTeam);

member.setTeam(anotherTeam);

두 객체간 연관 관계를 지정해줌으로써 참조를 통한 객체 그래프 탐색이 가능해진것을 알 수 있다. 그러나 이와 반대로 Team 객체에서 Member 객체를 탐색하는 것은 불가능하다. Team 객체에서 Member 객체를 참조하고 있지 않기 때문이다. 그러나 객체 지향 프로그래밍에서는 이러한 상황이 필요할 때가 존재하고, 이를 위해 JPA는 양방항 연관 관계를 지원해준다.

양방향 연관 관계

데이터 베이스는 외래 키를 통해 두 객체간의 데이터를 참조하게 된다.

SELECT * FROM team t JOIN member m ON t.id=m.team_id

SELECT * FROM member m JOIN team t ON m.team_id=t.id

위의 SQL문을 통해 알 수 있듯이 외래 키(FK)로 관계를 맺은 두 테이블은 JOIN을 통해 데이터를 양방향으로 조회할 수 있다. 그에 반해 객체에서는 다른 객체를 멤버 객체로 가지고 있는 객체만이 객체 그래프 탐색을 할 수 있다.

그러나 양방향으로 참조할 수 있는 필요는 분명 존재하고, 이를 위해 JPA는 양방향 매핑을 제공해준다.

위의 그림과 같이 두 객체 모두 내부에 참조할 수 있도록 선언해줌으로써 양방향으로 객체 그래프 탐색이 가능해진다. 하지만 이는 엄밀히 말하자면 서로 다른 단방향 관계가 두 개 생성되는 것이다. 그렇기에 양방향으로 참조하기 위해서는 단방향 연관 관계를 두 개 만들어주어야 한다.

@Entity
public class Team {

	@Id	@GeneratedValue
	private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
    // Member 객체를 참조하기 위한 단방향 매핑
}

위의 코드는 Team 객체에서 Member 객체를 참조하기 위한 단방향 매핑을 추가해줌으로써 양방향 매핑한 것이다. 이러한 양방향 매핑이 제대로 동작하기 위해서는 다음과 같은 규칙을 숙지해야 한다.

  • 객체의 두 관계 중 하나를 연관 관계의 주인으로 지정해야 하며, 연관 관계의 주인은 외래 키(FK)의 위치를 기준으로 정해야 한다.
  • 연관 관계의 주인만이 외래 키를 관리(등록, 수정)해야 하며, 주인이 아닌 쪽은 읽기만 가능하다.
  • 연관 관계의 주인이 아닌 쪽은 mappedBy 속성을 사용해 주인을 지정한다.
  • 순수한 객체 관계를 고려한다면 데이터 등록 시 편의 메서드 등을 사용해서 양쪽 모두 값을 지정해주어야 한다.
Team team = new Team();
엔티티메니저.persist(team);

Member member = new Member();
엔티티메니저.persist(member);

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

이렇듯 양방향 매핑을 위해서는 고려해야할 상황이 많다. 그렇기에 단방향 매핑을 잘 하고, 양방향은 필요할 때 추가하는 방식을 지향해야 한다.

다중성

다대일(N : 1)

다대일 관계에서 그 반대 방향은 항상 일대다 관계이다. 데이터베이스 테이블의 일대 다 관계에선 항상 다쪽에 외래 키(FK)가 존재한다. 그렇기에 객체의 양방향 관계에서 연관관계의 주인은 항상 다쪽이다.

다대일 관계 매핑시 고려할 부분은 다음과 같다.

양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.

일대다와 다대일 관계에서 외래 키는 항상 다쪽에 있다. JPA는 외래 키를 관리할 때 연관관계의 주인 객체만을 사용한다. 주인이 아닌 객체는 조회를 위한 JPQL이나 개체 그래프를 탐색할 때 사용한다.

양방향 연관관계는 항상 서로를 참조해야 한다.

두 객체 중 어느 한 쪽만 참조하면 양방향 관계가 성립되지 않는다. 항상 서로 참조하게 해야하며, 아래와 같은 편의 메서드를 사용하는 것이 좋다.

@Entity
public class Many {
	...
	public void setOne(One one) {
    	this.one = one;
        if (!one.getManyList().contains(this)) {
        // 양쪽에 모두 편의 메서드가 존재할 경우 무한 루프에 빠지지 않도록 체크
        	this.one.getManyList().add(this);
        }
    }
    ...
}

편의 메서드는 한쪽 혹은 양쪽에 작성할 수 있는데, 양쪽에서 모두 사용시 무한루프에 빠질 수 있으므로 주의해야 한다.

일대다(1 : N)

다대일 관계의 반대 방향으로, 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션 타입인 Collection, List, Set, Map 중 하나를 사용해야 한다.

일대다 단방향 관계일 경우 일쪽에서 외래 키를 관리한다. 이때 보통 자신이 매핑한 테이블의 외래 키를 관리하며, 데이터베이스에서는 반대쪽 테이블에 외래 키가 존재한다.

@Entity
public class One {
	...
    @OneToMany
    @JoinColumn(name = "ONE_ID")	// Many 테이블의 ONE_ID (FK)
    private List<Many> manyList = new ArrayList<Many>();
    ...
}

일대다 단방향 관계를 매핑할 때 위의 예제와 같이 @JoinColumn을 명시해줘야 한다. 그렇지 않을 경우 JPA는 중간에 연결 테이블을 생성하여 관리하는 조인 테이블(Join Table) 전략을 사용하여 매핑하게 된다.

일대다 단방향 매핑의 단점

일대다 단방향 매핑의 단점은 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 것으로, 엔티티의 저장과 연관관계 처리를 위한 쿼리를 각각 보내야 한다는 것이다.

일대다 양방향 매핑은 존재하지 않으며, 다대일 양방향 매핑을 읽기 전용으로 추가해서 일대다 양방향 처럼 보이도록 하는 방버은 존재하지만, 일대다 단방향 매핑이 가지는 단점을 그대로 가진다.

따라서 일대다 단방향 매핑 사용을 지양하고 필요할 경우 다대일 양방향 매핑을 사용하는 것이 좋다. 단순히 성능 문제만이 아닌 관리의 어려움도 존재하기 때문이다.

일대일(1 : 1)

양쪽이 서로 하나의 관계만 가지는 것을 의미한다. 일대다 관계에서 항상 다쪽이 외래 키를 가지는 것에 비해 일대일 관계는 주 테이블과 대상 테이블 둘 중 어느 곳이든 외래 키를 가질 수 있다. 양방향 관계시 대상 테이블에 mappedBy를 명시해줘야 한다.

주 테이블에 외래 키

외래 키를 객체 참조와 비슷하게 사용할 수 있어서 객체지향 개발자들이 선호한다. 주 테이블이 외래 키를 가지고 있으르모 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다는 장점이 있다.

대상 테이블에 외래 키

전통적인 데이터베이스 개발에서 보통 대상 테이블에 외래 키를 두는 것을 선호한다. 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다는 장점이 있다. 양방향 관계시 주 테이블에 mappedBy를 명시해줘야 한다.

다대다(N : N)

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그렇기 때문에 다대다 관계를 표현하기 위해서 연결 테이블을 사용한다.

이와 달리 객체는 객체 2개로 다대다 관계를 만들 수 있다. 또한 @JoinTable을 사용하여 추가 엔티티 없이 두 객체를 다대다 매핑할 수 있다.

@Entity
public class ManyA {
	...
    @ManyToMany
    @JoinTable(name = "MANY_MANY",
    		   joinColumns = @JoinColumn(name = "MANY_A_ID"),
               inverseJoinColumns = @JoinColumn(name = "MANY_B_ID"))
    private List<ManyB> manyBList = new ArrayList<ManyB>();
    ...
}

@Entity
public class ManyB {
	...
}

위의 예제는 다대다 단방향 매핑이다. @ManyToMany@JoinTable을 통해 연결 테이블을 추가 엔티티 없이 바로 매핑한 것을 확인할 수 있다. 연결 테이블을 매핑하는 @JoinTable의 속성은 다음과 같다.

  • @JoinTable.name : 연결 테이블을 지정한다.
  • @JoinTable.joinColumns : 현재 방향인 엔티티와 조인할 컬럼 정보를 지정한다.
  • @JoinTable.inverseJoinColumns : 반대 방향의 엔티티를 매핑할 조인 컬럼 정보를 지정한다.
@Entity
public class ManyB {
	...
    @ManyToMany(mappedBy = "MANY_B_ID")
    private List<ManyA> manyAList = new ArrayList<ManyA>();
    ...
}

@Entity
public class ManyA {
	...
    public void addManyB(ManyB manyB) {
    	manyBList.add(manyB);
        manyB.getManyAList.add(this);
    }
}

양방향 연관관계시 위와 같이 mappedBy로 연관관계의 주인을 지정하고, 편의 메서드를 추가하면 된다.

다대다 매핑의 한계와 연결 엔티티 사용

실무에서 연결 테이블 사용시 단순히 연결 정보만 담고 끝나지 않는다. 추가적인 컬럼이 필요하게 되는데, 이러한 경우 해당 컬럼들을 매핑할 수 없기 때문에 더이상 @ManyToMany를 사용할 수 없다.

결국 연결 테이블을 매핑하는 연결 엔티티를 만들고 추가한 컬럼들을 매핑해야 하며, 엔티티의 관계 또한 다대다에서 일대다, 다대일 의 관계로 풀어야 한다.

@Entity
public class ManyA {
	...
    @OneToMany(mappedBy = "MANY_A_ID")
    private List<ManyConnection> manyConnections = new ArrayList<ManyConnection>();
    ...
}

@Entity
public class ManyB {
	...
    @OneToMany(mappedBy = "MANY_B_ID")
    private List<ManyConnection> manyConnections = new ArrayList<ManyConnection>();
    ...
}

@Entity
@IdClass(ManyConnectionId.class)
public class ManyConnection {
	@Id
   	@ManyToOne
    @JoinColumn(name = "MANY_A_ID")
    private ManyA manyA;
    
	@Id
   	@ManyToOne
    @JoinColumn(name = "MANY_B_ID")
    private ManyB manyB;
    
    ...추가 컬럼 매핑
}

위의 예제를 보면 기존 객체들은 일대다로 매핑하고, 연결 엔티티를 생성하여 다대일 매핑한 것과 @Id@JoinColumn을 동시에 사용하여 기본 키와 외래 키를 한 번에 매핑한 것을 알 수 있다. 또한 @IdClass를 사용하여 복합 키본키를 매핑했으며, 이러한 연관관계를 위해서는 아래 ManyConnectionId와 같은 식별자 클래스가 필요하다.

복합 기본 키와 식별자 클래스

public class ManyConnectionId implements Serializable {
	private String manyA;	// ManyConnection.manyA와 연결
    private String manyB;	// ManyConnection.manyB와 연결
    
    // hashCode and equals
    
    @Override
    public boolean equals(Object p) {...}
    
}

연결 엔티티는 기본 키가 두 객체의 기본키로 이루어진 복합 기본키다. JPA에서 복합 기본키를 사용하려면 별도의 식별자 클래스가 필요하며, 연결 엔티티에 @IdClass를 사용해서 식별자 클래스를 지정해야 한다. 복합 키를 위한 식별자 클래스는 다음과 같은 특징이 있다.

  • 복합 키는 별도의 식별자 클래스로 만들어야 한다.
  • Serializable을 구현해야 한다.
  • equalshashCode 메서드를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.
  • @IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 존재한다.

식별 관계

연결 엔티티는 객체들의 기본 키를 받아서 자신의 기본 키로 사용하며, 이렇게 부모 테이블의 기본 키를 받아 자신의 기본 키 + 외래 키로 사용하는 것을 데이터베이스 용어로 식별 관계(Identifying Relationship)이라고 한다.

새로운 기본 키 사용

복합 키를 사용하는 방법은 복잡하다. 그렇기에 복합 키를 사용하지 않고 데이터베이스에서 자동으로 생성해주는 대리 키를 새로운 기본 키로 사용하여 다대다 관계를 구현하는 방법이 있다.

@Entity
@IdClass(ManyConnectionId.class)
public class ManyConnection {
	@Id	@GeneratedValue
    @Column(name = "CONNECTION_ID")
    private Long id;
    
   	@ManyToOne
    @JoinColumn(name = "MANY_A_ID")
    private ManyA manyA;
    
   	@ManyToOne
    @JoinColumn(name = "MANY_B_ID")
    private ManyB manyB;
    
    ...추가 컬럼 매핑
}

대리 키를 사용함으로써 식별 관계에 복합 키를 사용하는 것보다 매핑이 단순하고 이해하기 쉬워진 것을 알 수 있다. 또한 기존의 연결 대상이 되는 객체들은 변경사항이 없다. 그렇기에 새로운 기본 키를 사용해서 다대다 관계를 풀어내는 것도 좋은 방법이다.

profile
만들고 나누며, 세상을 이롭게 하고 싶습니다.
post-custom-banner

0개의 댓글