[JPA 프로그래밍] Entity Mapping 2편

최동근·2023년 1월 1일
0

JPA

목록 보기
11/13

해당 글은 김영한 님의 ["자바 ORM 표준 JPA 프로그래밍"] 을 스터디 하면서 정리하는 글 입니다 !👨‍💻

📣 이번 글은 Entity Mapping 1편 에 이어 다양한 연관관계 매핑에 대해 알아보겠습니다.

연관관계에는 다양한 다중성이 있습니다.

  • 다대일
  • 일대다
  • 일대일
  • 다대다

연관관계애는 두가지 방향이 존재합니다.

  • 단방향
  • 양방향

📣 해당 글에서는 왼쪽이 연관관계의 주인이라고 가정합니다. 예를 들어 다대일 연관관계인 경우,
다쪽이 연관관계라는 뜻입니다.

💡 다대일 (ManyToOne)

데이터베이스 테이블의 다대일 관계에서 외래키는 항상 다쪽에 있습니다 🙆🏻

1. 단방향

해당 이미지는 회원(Member) 와 팀(Team)의 다대일 연관관계를 표현합니다.
Member 엔티티는 team 필드를 통해 Team 엔티티를 참조합니다.
하지만 단방향이기에 Team 엔티티는 자신을 참조하는 Member 엔티티의 존재를 알지 못합니다.

@Entity
public class Member {

	@Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID") // TEAM_ID 외래 키와 매핑
    private Team team; // Team 엔티티 참조
    
}

@Entity
public class Team {
	
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String teamName;
    
 }

2. 양방향

해당 이미지는 다대일 양방향 연관관계를 표현합니다.
Member 엔티티에서는 team 필드로, Team 엔티티에서는 members 필드로 서로를 참조하고 있습니다.

@Entity
public class Member {

	@Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID") // TEAM_ID 외래 키와 매핑
    private Team team; // Team 엔티티 참조
    
    public void setTeam(Team team) {
    
    	this.team = team;
        
        if(!team.getMembers().contains(this)) {
        	team.getMembers().add(this);
        }
    } // 무한 루프 방지
}

@Entity
public class Team {
	
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String teamName;
    
    @OneToMany(mappedBy = "team)
    private List<Member> members = new ArrayList<>(); // only 읽기
    
    public void addMember(Member member) {
    
    	this.members.add(member);
        
        if(member.getTeam() != this) {
        	member.setTeam(this);
        }
    } // 무한 루프 방지
 }

어떤 다중성이던지 양방향인 경우에는 항상 연관관계의 주인을 설정해야 합니다.
또한 항상 서로를 참조해야 합니다. 이때는 연관관계 편의 메소드를 통해 양방향 연관관계를 맺어주는 것이 좋습니다 ❗️

💡 일대다(OneToMany)

일대다 연관관계는 다대일 연관관계의 반대 방향입니다.
또한 일쪽에서 다쪽을 참조하는 구조임으로, 자바 칼렉션을 사용합니다.

1. 단방향

해당 이미지를 분석해보겠습니다.
앞에서도 명시했지만 데이터베이스 상에서 외래 키는 항상 다쪽에 존재합니다.
이런 특성 때문에 일대다 단방향은 특이한 특성을 가집니다 🧐

일대다 단방향이기 때문에 Team 엔티티에서 Team.members 로 회원 테이블의 외래키인 TEAM_ID 를 관리합니다.
보통 자신에게 속해 있는 외래 키를 관리하는데, 이 매핑은 반대쪽 테이블에 있는 외래 키를 관리하게 됩니다 😖
(연관관계의 주인만이 외래키를 관리 할 수 있습니다.)

@Entity
public class Team {

	@Id@GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
    
 }
 
@Entity
public class Member {

	@Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String userName;
    
}

또한 일대다 단방향 관계에서는 @JoinColumn을 꼭 명시해야 합니다. 그렇지 않으면
조인 테이블을 전략으로 사용해서 매핑합니다.

이처럼 일대다 단방향 매핑은 치명적인 단점이 존재합니다 ⛔️

매핑한 객체가 관리하는 외래 키가 다른 테이블에 존재한다.

왜 이런 특성이 치명적인 단점이 될까요?
JPA 는 개발자가 작성한 코드를 기반으로 데이터베이스에 보낼 쿼리문을 작성합니다.
만약 일대다 단방향 매핑을 통해 연관관계를 표현한다면, 다른 테이블에 있는 외래 키 처리를 위해 UPDATE SQL을 추가로 실행해야 합니다 🥲

예를 들어 다음과 같은 코드의 실행을 통해 일대다 연관관계에 있는 엔티티를 저장 해보겠습니다 👨‍💻


public void test() {

	Member member1 = new Member("member1");
    Member member2 = new Member("member2");
    
    Team team1 = new Team("team1");
    team.getMembers().add(member1);
    team.getMembers().add(member2);
    
    em.persist(member1);
    em.persist(member2);
    em.persist(team1);
    
    transaction.commit();
}

-> 실행되는 쿼리문

insert into Member (MEMBER_ID, username) values (null, ?);
insert into Member (MEMBER_ID, username) values (null, ?);
insert into Team (TEAM_ID,name) values(null, ?);
update Member set TEAM_ID=? where MEMBER_ID=? // 추가의 UPDATE 쿼리 문 실행
update Member set TEAM_ID=? where MEMBER_ID=?

해당 코드를 보면 Team 엔티티를 저장시, Member 엔티티의 외래 키인 TEAM_ID 업데이트를 위해 쿼리문이 추가적으로 실행되는 것을 볼 수 있습니다 🧐

즉, Member 엔티티를 저장할 때는 Member 엔티티의 외래 키인 TEAM_ID 에는 어떠한 값도 저장되지 않습니다. 대신 TEAM 엔티티 저장시 TEAM.members 의 참조값을 확인해서 Member 엔티티의 외래 키를 업데이트 합니다.
이는 성능문제가 있으며 개발자 입장에서 관리도 부담스럽습니다 ⛔️

일대다 단방향 사용은 지양하고 대신 구조가 비슷한 다대일 양방향을 사용할 것을 권장드립니다 🤓

2. 양방향

JPA 에서는 일대다 양방향 매핑을 지원하지 않습니다.
따라서 다대일 양방향 매핑을 사용해야 합니다.

💡 일대일(OneToOne)

일대일 관계는 두 엔티티가 하나의 관계만 가집니다.
따라서 그 반대도 일대일 관계입니다. 일대일 관계에서는 두 테이블을 주 테이블과 대상 테이블로 명시합니다 🧑🏼‍💻

일대일 관계에서는 주 테이블이나 대상 테이블 중에 누가 외래 키를 가질지 선택해야 합니다 🙆🏻
저는 주 테이블에 외래 키가 있는 것을 선호하기 때문에 주 테이블에 외래 키를 두고 단/양방향을 살펴보겠습니다 📚

1. 단뱡향

회원과 사물함의 일대일 단방향 관계를 예로 들어보겠습니다 🧑🏼‍💻
회원은 하나의 사물함만 가질 수 있으며 사물함도 한명의 회원만을 가집니다.

// Member 클래스
@Entity
public class Member {

		...
		@OneToOne
		@JoinColumn(name = "LOCKER_ID")
		private Locker locker
}
// Locker 클래스
@Entity
public class Locker {

		@Id @GeneratedValue
		@Column(name = "LOCKER_ID")
		private Long id;
}

일대일 관계임으로 @OneToOne 어노테이션을 사용했습니다.
또한 Member 클래스는 자신의 Locker 사물함을 참조하고 있습니다 👉

2. 양방향

일대일 양방향은 Member 와 Locker 객체가 서로를 일대일로 참조하고 있는 형태를 가집니다.
해당 예시에서 주테이블은 Member 테이블이고, 대상 테이블은 Locker 테이블입니다.

// Member 클래스
@Entity
public class Member {
		...
		@OneToOne
		@JoinColumn(name = "LOCKER_ID")
		private Locker locker
}
// Locker 클래스
@Entity
public class Locker {
		@Id @GeneratedValue
		@Column(name = "LOCKER_ID")
		private Long id;
		
		private String name;

		@OneToOne(mappedBy = "locker")
		private Member member;
		...
}

앞에서 배운 바와 같이 양방향이기 때문에 연관관계의 주인을 정해야 겠죠?
Member가 주 테이블이고 주 테이블에 외래 키가 존재한다고 설정했으니, 자연스레 연관관계 주인은 Member가 됩니다 🧑🏼‍💻

💡 다대다(ManyToMany)

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없습니다 🥲
이러한 이유 때문에 다대다 연관관계를 가지는 엔티티를 테이블로 매핑할 때는 별도의 '연결 테이블' 이 필요합니다 🧑🏼‍💻

다대다 연관관계의 예시는 회원(Member) - 상품(Product) 로 들겠습니다.

1. 단방향

// Member 엔티티 클래스
@Entity
public class Member { 

	@Id@Column(name = "MEMBER_ID)
    private String id;
    
    private String userName;
    
    ...
    
    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT,
    joinColumns = @JoinColumn(name = "MEMBER_ID"),
    inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID))
    private List<Product> products = new ArrayList<>();
    
// Product 엔티티 클래스
@Entity
public class Product { 

	@Id@Column(name = "MEMBER_ID)
   private String id;
   
   ...
   
}

두 코드는 회원 - 상품의 다대다 연관관계를 표현하는 코드입니다.
여기서 주의깊게 봐야할 부분은 Member 클래스의 '@ManyToMany'와 '@JoinTable'입니다.
해당 어노테이션을 통해 연결 테이블인 '회원_상품(Member_Product)' 를 따로 구현할 필요가 없습니다 ✅

  • joinColumns : 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정합니다.
  • inverseJoinColumns : 반대 방형인 상품과 매핑할 조인 컬럼 정보를 지정합니다.
public void save() { 

	Product productA = new Product();
    ...
    
    Member memberA = new Member();
    ...
    
    memberA.getProducts().add(productA); // 연관관계 설정
    em.persist(memberA);
    
}

해당 코드는 연관관계를 맺은 후, 저장하는 코드입니다.
이때 productA 와 memberA는 연관관계를 맺고 있음으로, 회원1을 저장할 때 연결 테이블에도 값이 자동으로 저장됩니다 ✅

2. 양방향

앞에서 배운바와 같이 양방향인 경우에는 연관관계의 주인을 정해주면 됩니다. 🧑🏼‍💻
해당 예시에서는 Member 엔티티에 연관관계 주인을 가정하겠습니다.

// Product 엔티티 클래스
@Entity
public class Product{


	
 	@Id@Column(name = "MEMBER_ID)
    private String id;
    
    @ManyToMany(mappedBy = "products") // 역방향 추가
    private List<Member> members; 
	
    ...
}
public void save() { 

	Product productA = new Product();
    ...
    
    Member memberA = new Member();
    ...
  
    memberA.getProducts().add(productA); // 연관관계 설정
    em.persist(memberA);
    
    productA.getMembers().add(memberA); // 연관관계 설정
    em.persist(productA);
}

3. 다대다 사용시 주의할 점

이처럼 다대다 연관관계는 @ManyToMany, @JoinTable 어노테이션을 통해 연결 테이블 구현 없이도 손쉽게 만들 수 있습니다.
하지만 실제 실무에서는 연결테이블에 다양한 정보가 들어가기 때문에 이와 같은 방법에는 '한계'가 존재합니다 💡
다시 말해, 연결 테이블을 직접 구현해야 합니다.
그러나 연결 테이블을 직접 구현하는데에는 주의할 점이 있습니다 🧐

  • 복합 기본 키를 사용해야 한다.

회원 상품 엔티티를 별도로 구현시 해당 엔티티의 기본키는 MemberId 와 ProductId 로 이루어진 복합 기본키를 가집니다. JPA에서 복합 키를 사용하기 위해서는 @IdClass어노테이션을 통해 별도의 '식별자 클래스'를 만들어야 합니다.

식별자 클래스를 만드는 것은 개발자 입장에서 메우 번거로울 수 있습니다.

  • 식별관계를 가진다.

회원상품은 회원과 상품의 기본 키를 받아서 자신의 기본 키로 사용합니다.
식별관계란 부모 테이블의 기본키를 받아 자신의 기본키 + 외래키로 사용하는 것을 말합니다.

이처럼, 일반적인 연결 테이블을 구성시, 약간의 제약 조건이 존재합니다 🤔
그러면 어떤 방식으로 이와 같은 제약조건을 해소 할 수 있을까요?

연결 테이블 구성시 데이터베이스에서 자동으로 생성하는 대리 키를 사용하자 🙆🏻

대리키는 자연키와 달리 비즈니스 로직에 의존하지 않으며, 이전에 보았던 식별 관계에 복합 키를 사용하는 것보다 매핑이 단순하고 이해하기 쉽습니다.
따라서 가장 추천되는 기본 키 방식입니다 🙆🏻
연결 테이블의 기본키를 대리키로 구성하게 되면 복합 기본키를 사용할 필요도 없으며 부모 테이블에서 가져온 키를 외래키로만 사용 할 수 있습니다 💡

해당 이미지를 볼까요?
회원상품 테이블은 주문(Order) 테이불로 이름을 변경했습니다.
Order 테이블의 기본키는 Order_ID 입니다. 해당 키는 대리키를 사용한 기본키입니다. 또한 연관관계를 맺는 Member와 Product의 기본키를 외래키로만 사용하고 있습니다 💪

@Entity
public class Order {

	@Id@GeneratedValue
    @Column(name = "ORDER_ID")
    private Long id;
    
    
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
    
    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    priavate Product product;
    
    private int orderAmoun;
    
    ...
    
 }
@Entity
pulic class Member { 
	
   @Id @Column(name = "MEMBER_ID")
   private String id;
   private String userName;
   
   @OneToMany(mappedBy = "member")
   private List<Order> orders = new ArrayList<>(); // 양방향 관계
  
}

@Entity
public class Product {

	@Id @Column(name = "PRODUCT_ID")
   private String id;
   private String name;
   // Product 에서 Order 참조 불가(단방향)
   
}

저장하는 방식도 코드를 통해 알아봅시다 📚

public void save(){

   Member member1 = new Member();
   member1.setId("member1");
   member1.setUserName("회원1");
   em.persist(member1);
   
   
   Product product1 = new Product();
   product1.setId("product1");
   product1.setName("상품1");
   em.persist(product1);
   
   Order order = new Order();
   order.setMember(member1);
   order.setProduct(product1);
   order.setOrderAmount(100);
   em.persist(order);
} 
   

이렇게 다양한 연관관계를 매핑하는 방법을 알아보았습니다.
JPA에서 연관관계를 이해하고 사용하는 것은 가장 핵심이라고 생각합니다 🔥
저 또한 부족한 점을 다시 한번 검토하고 실제 코드에 다양하게 반영할 예정입니다 🧑🏼‍💻

참고

다양한 연관관계 매핑
복합 키와 식별, 비식별 관계
다양한 연관관계
연관관계 매핑이란?

profile
비즈니스가치를추구하는개발자

0개의 댓글