[JPA] 엔티티 연관관계 매핑-1

Noah-wilson·2025년 1월 2일

JPA

목록 보기
6/10

연관관계 필요한 이유?

ERD 중심으로 객체를 모델링하면 OOP의 철학에 따른 객체 간의 동적인 참조 관계와 협력 관계를 자연스럽게 구현하기 어렵다.

ERD 중심 설계 예재

@Entity
class Order {
    @Id @GeneratedValue
    private Long id;
	
    @Column(name="customer_id");
    private Long customerId; 

}
class Main{
	public static void main(String[] args{
		Order findOrder=em.find(Order.class,1L);
        Customer customer = em.find(Customer.class,findOrder.getCustomerId);
    	
    }
}

문제점: Order는 단순히 customerId를 가지고 있을 뿐, Customer의 참조를 가지고 있지 않습니다.
또한, Main 클래스에서 Order에 포함된 member를 찾기 위해 find를 호출한 뒤, 별도로 select 쿼리를 통해 member 객체를 조회하는 방식은 객체지향적인 설계라고 볼 수 없습니다.

객체 지향 설계 예제:

@Entity
class Order {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne 
    @JoinColumn(name = "customer_id")
    private Customer customer;
    
    public Customer getCustomer() {
        return customer;
    }

}

class Main{
	public static void main(String[] args{
			Order findOrder=em.find(Order.class,1L);
        	Customer customer = findOrder.getCustomer();
    	
    
    }
}

Order 객체가 Customer 객체를 참조하고 있으므로, ERD 중심 객체 설계 방법의 문제점을 해결할 수 있다.

방향(Direction)

단방향

@Entity
class Order {
    @Id @GeneratedValue
    private Long id;
	
    //@Column(name="customer_id");
    //private Long customerId; 
    ![](https://velog.velcdn.com/images/noah-wilson0/post/92cffe3c-f09c-4203-8601-484c62426b31/image.png)

    @ManyToOne 
    @JoinColumn(name = "customer_id")
    private Customer customer;

}

위 코드처럼 객체 지향 모델링을 하게 되면 단방향 연관 관계를 설정할 수 있다.

//고객 저장
 Customer customer = new Customer();
 customer.setName("A");
 em.persist(customer);
 //주문 저장
 Order order = new Order();
 order.setCustomer(customer); //단방향 연관관계 설정, 참조 저장
 em.persist(order);
 
 System.out.println("order = " + order.getCustomer(());

이로써 Order 객체에서 참조를 통해 customer 데이터를 참조하여 사용할 수 있게 된다.

양방향

양방향 연관 관계 매핑하는법:

@Entity
class Member {
     @Id @GeneratedValue
 	private Long id;
    
 	@Column(name = "USERNAME")
 	private String name;
    
 	private int age;
    
 	@ManyToOne
 	@JoinColumn(name = "TEAM_ID") //단방향
 	private Team team;


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

 }
class Main{
	public static void main(String[] args{
			//조회
 			Team findTeam = em.find(Team.class, team.getId()); 
			int memberSize = findTeam.getMembers().size(); //역방향 조회
    	
    }
}

이로써 Member에서 Team을 참조할 수 있고,Team에서도 Member를 참조하여 조회할 수 있게 된다.(양방향 관계)

결국 Member ➡️ Team (단방향), Team ➡️ Member (단방향) 으로 단방향을 2번 사용했을뿐인데, 이를 양방향 관계라고 하는 이유가 뭘까?
이것은 RDB의 table간 연관 관계와 객체지향에서 객체간 연관 관계를 생각해보면 이해하기 쉽다.
RDB에서 연관 관계느 단/양방향 연관 관계 매핑은 외래키를 통해 연관 관계가 맺어지기 때문에 차이가 없고, 객체 지향에서 객체간 연관 관계는 Member ➡️ Team, Team ➡️ Member로 참조 관계를 통해 연관 관계가 맺어지기 때문이다.

연관 관계 주인(Owner)

그럼 연관 관계 설계시 항상 두가지 의문을 가질 것이다.
1. Member를 연관 관계 주인으로 한다.
2. Team을 연관 관계 주인으로 한다.

위 의문중 어느것을 선택하던지 다음과 같은 문제를 더 겪을 것이다.
4. Team의 Member가 변경된다면 Team의 Member를 업데이트하는것이 맞을까?
5. Member의 Team을 변경된다면 Member의 Team을 업데이트하는것이 맞을까?
6. 두가지 모두 업데이트하는게 맞을까?

결론은 Team과 Member 하나를 연관 관계의 주인으로 해야한다.
둘 다 주인으로 지정하게 되면 데이터 불일치 문제가 생길 수 있다.
그럼 어떤 객체를 주인으로 해야할까?

양방향 매핑 규칙

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

위 규칙을 준수하면 모든 고민이 해결된다.
연관 관계의 주인만 등록, 수정을 하고 그외는 읽기 전용으로 하여 데이터 불일치 문제가 생기지 않는다.

그럼 남은 고민이 있다. 어떤 객체를 주인으로 해야 될까?

외래키가 있는 곳을 주인으로 정해야 한다.(Member가 연관관계의 주인이 된다)

Team을 연관 관계의 주인으로 못하는 것은 아니다.
하지만 다음과 같은 문제가 있다.

Team(기본키)를 주인인 경우 예제:

Team team = new Team();
team.setName("Team A");

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

Member member2 = new Member();
member2.setName("Member 2");

team.addMember(member1);
team.addMember(member2);

em.persist(team); 

예상되는 sql:

INSERT INTO team (id, name) VALUES (1, 'Team A');

INSERT INTO member (id, name) VALUES (1, 'Member 1');
INSERT INTO member (id, name) VALUES (2, 'Member 2');
UPDATE member SET team_id = 1 WHERE id = 1;
UPDATE member SET team_id = 1 WHERE id = 2;

이렇게 외래키가 없는 곳을 주인으로 정하게 되면
불 필요한 sql query를 더 실행하므로 N+1문제가 생기기 때문에 성능 이슈가 생긴다.

Member(외래키=TEAM_ID)를 주인인 경우 예제:

class Member {
 // ...
 	@ManyToOne
 	@JoinColumn(name = "TEAM_ID")
 	private Team team;
 // ...
}
class Team {
  // ...
  @OneToMany(mappedBy="team")
  private List<Member> members = new ArrayList<>();
  // ...
}

Member를 연관 관계 주인으로 설정함으로써 Member와 Team 간의 양방향 연관 관계 매핑이 가능해지며, 이에 따라 다른 모든 고민을 해결할 수 있다.

양방향 매핑시 주의점

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

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

Member member2 = new Member();
member2.setName("Member 2");

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

위 코드는 team과 member를 만들고 데이터 베이스에 저장하는 코드입니다.
정상적으로 데이터 베이스에 저장이 될 것 같지만, 그렇지 않습니다.
위 코드를 실행하면 데이터 베이스에 저장되는 결과는 다음과 같습니다.

MEMBER_IDUSER_NAMETEAM_ID
1member1null
2member2null

외래 키 TEAM_ID에 null 값이 입력되어 있는데, 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문입니다.

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

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

Member member2 = new Member();
member2.setName("Member 2");

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

연관 관계의 주인인 member에 team값을 설정해주게 되면
외래키(TEAM_ID)에 정상적으로 값이 설정되는 것을 알 수 있습니다.

MEMBER_IDUSER_NAMETEAM_ID
1member11
2member22

정리

• 단방향 매핑만으로도 이미 연관관계 매핑은 완료
• 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추
가된 것 뿐
• JPQL에서 역방향으로 탐색할 일이 많음
• 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨
(테이블에 영향을 주지 않음

0개의 댓글