ORM 표준 JPA 프로그래밍 - 연관관계 매핑 기초

링딩·2022년 9월 4일
0

ORM 표준 JPA

목록 보기
4/6

연관관계 매핑 기초

용어

방향(Direction): 단방향, 양방향
다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
연관관계의 주인(Owner): 객체 양방향 연관관계는 '관리 주인'이 필요

사실상 객체지향스럽게 설계하는 것에 근본적으로 이해하는 것이 중요하단 것을 잊지말고 시작하자.

예제 시나리오

• 회원과 팀이 있다.
• 회원은 하나의 팀에만 소속될 수 있다.
• 회원과 팀은 다대일 관계다.

객체를 '테이블'에 맞추어 모델링 하면?

객체는 테이블의 외래키 값을 그대로 가져와서 쓴다.

@Entity
 public class Member { 
 	@Id @GeneratedValue
 	private Long id;
 
 	@Column(name = "USERNAME")
 	private String name;
 	
    @Column(name = "TEAM_ID")
 	private Long teamId; 
 	… 
 }
 
 @Entity
 public class Team {
 	
    @Id @GeneratedValue
 	private Long id;
 	
    private String name; 
 … 
 }

직접적으로 전달해준건데

@Column(name = "TEAM_ID")
 	private Long teamId;

문제: MEMBER랑 TEAM을 레퍼런스로 참조를 주는게 아니라 DB를 기준으로 맞추어 외래키만 가져와 넣어주었다.

객체를 테이블에 맞춰서 모델링하는게 도대체 왜 문제일까?

//팀 저장
 Team team = new Team();
 team.setName("TeamA");
 
 em.persist(team);
 
 //회원 저장
 Member member = new Member();
 member.setName("member1");
 member.setTeamId(team.getId());
 
 em.persist(member);


//조회
 Member findMember = em.find(Member.class, member.getId()); 
 
//연관관계가 없어서 발생되는 문제
 Long findTeamId = findMember.getTeamId();
 Team findTeam = em.find(Team.class, findTeamId);

연관관계 매핑을 쓰지 않았기 때문에
계속 db를 통해서 끄집어내야 되고 이러다 보니 객체지향스럽지가 않음.

정리하면

  • 테이블은 '외래 키'로 조인을 사용해서 연관된 테이블을 찾는다.
  • 객체는 참조를 사용해서 연관된 객체를 찾는다.
  • 테이블과 객체 사이에는 이런 큰 간격이 존재해서 매끄럽지 못한 매핑이 이뤄진다..

그래서 연관관계가 나왔다.



단방향 연관관계

이전처럼 테이블에 맞추어 모델링하지 않고 '객체지향'스러운 모델링을 만들어 보자.

Member 클ㄹ스

@Entity
 public class Member { 
 	@Id @GeneratedValue
 	private Long id;
 
 	@Column(name = "USERNAME")
 	private String name;
 
 	private int age;

	// @Column(name = "TEAM_ID")
	// private Long teamId;

	@ManyToOne
 	@JoinColumn(name = "TEAM_ID")
 	private Team team;
  • 이전처럼 FK를 직접 때려 넣어주는 코드가 아닌, 객체를 넣고 이것과 매핑되는 관계
    -> 즉, 연관관계에 대해 구체적으로 적어준다.

[단방향 연관관계]

이 Member 클래스는 Team 클래스는 1개의 팀에 여러 명의 회원이 소속되기 때문에 '다 대 일'의 관계가 된다.

  • 단뱡향, 다대일의 관계로
    Member 클래스에서 TEAM을 @ManyToOne으로 관계를 적어주고.
  • @JoinColumn(name = "상대 클래스에 매핑되는 필드명")


이렇게 매핑해주면 관계는 객체지향스러운 관계르 변화한다.

[바뀐 실행로직]

//팀 저장
 Team team = new Team();
 team.setName("TeamA");
 em.persist(team);

//회원 저장
 Member member = new Member();
 member.setName("member1");
 member.setTeam(team); //단방향 연관관계 설정, 참조 저장
 em.persist(member);
 
 //조회
 Member findMember = em.find(Member.class, member.getId()); 
 
//참조를 사용해서 연관관계 조회
 Team findTeam = findMember.getTeam()

코드도 전보다 더 간결해졌다.
findMember.getTeam() 으로 바로 꺼내서 쓸 수 있따.
=> 한 방에 다 땡겨왔다



양방향 연관관계와 연관관계의 주인 1 - 기본

Member findMEmber = em.find(MEmber.class, member.getId());

Team findTeam = findMember.getTeam();
//(문제) : Team에서 Member를 바로 가져올 수 없음
//findTeam.getMember();

Team에서 Member로 갈 수 없었다. (단방향이라)

-> (양방향이였따면...) 레퍼런스만 넣어두면 둘 다 와리가리 왔다갔다가 가능했을 것이다


양방향을 바라볼 때 객체와 테이블은 차이가 있다.

우리가 들어가기 전 봐야할게 테이블과 객체는 둘이 다르다..

  • 테이블은 딱히 더 변경될게 없다. FK로 이미 둘은 양방향을 왔다갔다 할 수 있따. 그리고 애초에 이들은 방향이란 개념이 없다. => 곧 문제 x
  • 객체는 테이블과 달리 Team에서 Member로 갈 수가 없었다. => 문제 ㅇ

양방향을 어떻게 만들어야 하는걸까?🤷‍♂️

[Member 클래스에 Team 매핑관계(아까와 같음)]

[Team 클래스에 Member의 매핑관계를 추가]

  • @OneToMany : Team(1) 기준에서 Member(다)와의 관계
  • @OneToMany(mappedBy = "연관관계의 주인의 필드")

객체와 테이블은 관계를 맺는 것이 이렇게 차이가 난다.

• 객체 연관관계 = 2개

• 회원 -> 팀 연관관계 1개(단방향) 
• 팀 -> 회원 연관관계 1개(단방향) 

• 테이블 연관관계 = 1개

• 회원 <-> 팀의 연관관계 1개(양방향)

객체의 양방향 관계는 사실 서로 다른 관계 2개가 합쳐진 것이다.

양방향이라는 것은 이렇게 2가지의 관계가 합쳐져 나온 것이다.

곧 이 말은 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

  • 객체는 참조를 2개를 만들어 주었다.
  • [문제]
    그렇다면 둘 중 Member의 Team 값을 업데이트 했을 때 DB를 업데이트 해야 하는걸까?
    혹은 Team의 List members를 바꿔야 할지 어려워진다.

DB 입장에서는 참조가 어찌되든 간에 FK의 업데이트만 되면 되는 것이다.

우리는 이 FK 업데이트가 될 기준을 이 2개의 참조 중 무엇으로 두어야 하는지 생각해보아야 한다.



연관관계 주인

양방향 매핑 규칙

객체의 두 참조 관계 중 하나를 연관관계의 주인으로 지정
• 연관관계의 주인만이 외래 키를 관리(등록, 수정)
주인이 아닌 쪽은 읽기만 가능
=> 곧 mappedBy가 써져있는 쪽은 읽기만 됨
• 주인은 mappedBy 속성 사용해선 X
• 주인이 아니면, mappedBy 속성으로 주인 지정


그렇다면 누구를 주인으로 삼아야 할까?

정답 : '외래키'가 있는 곳

@OneToMany(mappedBy = "team")
 List<Member> members = new ArrayList<Member>();

Member가 주인이 되어야 한다.
그러니 Team 클래스에 mappedBy가 설정된다.



양방향 매핑시 실수를 조심하자

연관관계의 주인만이 '수정','등록'이 가능하다.

Team team = new Team();
 team.setName("TeamA");
 em.persist(team);
 
 Member member = new Member();
 member.setName("member1");
 
 //연관관계의 주인은 Member인데 team에서 설정?
 team.getMembers().add(member);
 
 em.persist(member);

결과는

(해결) 연관관계의 주인에서 등록해주자

team.getMembers().add(member); 

 //Member(연관관계의 주인)에 값 설정
 member.setTeam(team); //추가해줌

em.persist(member);

결론: 양방향 매핑시, 양쪽 모두에 값을 다 세팅해주자.

왜?

  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자

방법 : 근데 값을 매번 적용하면서 헷갈리니까 하나의 메소드인 곧 '편의 메소드'를 만들어주자

//기존에
Team team = new Team();
 team.setName("TeamA");
 em.persist(team);
 
 Member member = new Member();
 member.setName("member1");
 member.setTeam(team); //** 세팅에 필요
 em.persist(member);
 
 //team.getMembers().add(member);//** 세팅에 필요
 
 em.flush();
 em.clear();
.
.


//세팅에 필요한 부분을 연관 메소드로 만들어 줌
//Member 클래스 편의 메소드
public void changeTeam(Team team){
	this.team = team;
    team.getMembers().add(this);//this는 자기자신
}

//혹은 Team 클래스에 편의 메소드
public void addMember(Member member){
	member.setTeam(this);
    members.add(member);
}
  • 연관메소드도 둘 중 한 곳에만 정해서 써줘야 한다. => 값을 어디에 세팅할지는 본인 맘

양방향 매핑시에, 무한 루프를 조심하자

ex) toString(), lombok, JSON 생성등

양쪽에 toString() 등으로 무한호출을 해버림
(보통 이럴 때)Controller를 Entity로 바로 Response를 반환할 때 Entity가 양방향 연관관계인 경우에 생긴다.

  • (원인) 엔티티를 JSON으로 바꿔주면서 장애가 남

  • (해결) lombok에서 toString()은 웬만해서 쓰지마라,

  • JSON 라이브러리는 걱정 ㄴㄴ
    -> Controller는 Entity로 반환 안해주면 됨. (무한루프, 엔티티 중간변경 위험)

[이를 실무에서는.. ]

차라리 Entity를 DTO(값만 있는애)로 변환해서 반환하는 것을 추천



양방향 매핑 정리

'단방향 매핑'만으로도 이미 연관관계 매핑은 완료
-> 객체 입장에선 양방향으로 만들면 오히려 고민이 더 많아질 뿐
• '양방향 매핑'은 그저 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
• JPQL에서 역방향으로 탐색할 일이 많음
• 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨(테이블에 영향을 주지 않음)
-> 사실상 자바코드 몇 줄 넣는 것은 크게 어렵지 않다.

결론 => 단방향으로 설계를 다 끝내고, 양방향 매핑을 그때 고민해도 늦지 않다는 점


실전 예제 2. 연관관계 매핑을 이용한 시작

테이블 구조와 객체 구조

테이블 구조 (이전과 똑같음)

객체 구조 (참조 사용)

업로드중..

Member 클래스

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;
    private String city;
    private String street;
    private String zipcode;

    //얘는 굳이 싶긴하다 꼭 안 넣어줘도 되는데
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

	//getter,setter 생략
}

Order 클래스


@Entity
@Table(name = "ORDERS")
public class Order {

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

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID") //연관관계 주
    private Member member;

    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<OrderItem>();

    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    //편의메소드
    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

//getter, setter 코드


}

OrderItem 클래스


@Entity
@Table(name="ORDER_ITEM")
public class OrderItem {

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

    @ManyToOne
    @JoinColumn(name="ORDER_ID")
    private Order order;

    @ManyToOne
    @JoinColumn(name="ITEM_ID")
    private Item item;


    private int orderPrice;
    private int count;
 //getter,setter 생략
 }

Item 클래스


@Entity
public class Item {

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

    private String name;

    private int price;
    private int stockQuantity;
  
  //getter,setter 생략
  }
profile
초짜 백엔드 개린이

0개의 댓글