관리 메뉴

나구리의 개발공부기록

연관관계 매핑 기초, 단방향 연관관계, 양방향 연관관계와 연관관계 주인(기본/주의점 및 정리), 실전 예제 - 연관관계 매핑 시작 본문

인프런 - 스프링부트와 JPA실무 로드맵/자바 ORM 표준 JPA 프로그래밍 - 기본편

연관관계 매핑 기초, 단방향 연관관계, 양방향 연관관계와 연관관계 주인(기본/주의점 및 정리), 실전 예제 - 연관관계 매핑 시작

소소한나구리 2024. 10. 8. 15:36

출처 : 인프런 - 자바 ORM 표준 JPA 프로그래밍 - 기본편(유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 연관관계 매핑 기초

** 실전예제 전까지는 DB URL 설정을 test로 설정하여 실습

<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>

 

1) 목표

  • 객체와 테이블 연관관계의 차이를 이해
  • 객체의 참조와 테이블의 외래 키의 매핑 방법

2) 용어 이해

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

3) 연관관계가 필요한 이유

  • 객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다 - 조영호(객체지향의 사실과 오해)
  • 테이블 하나로 표현하고자 하는 것을 표현할 수 없기 때문에 여러 테이블이나 객체가 연관관계로 묶어서 표현해야 함
  • 객체지향스럽게 설계하는 것이 무엇인지 근본적으로 공부하는 책 등을 읽어서 공부하는 것을 권장함

4) 예제 시나리오

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

5) 객체를 테이블에 맞추어 모델링

(1) 연관관계가 없는 객체 설계

  • MEMBER 테이블과 TEAM 테이블의 관계에서 외래키인 TEAM_ID가 MEMBER테이블에 있음
  • 일대다 관계에서 외래키를 가지고 있는 테이블이 항상 다에 해당함 즉, 여러명의 MEMBER는 1개의 TEAM에 소속될 수 있다는 뜻

(1) Member

@Entity
public class Member {

    @Id @GeneratedValue // GeneratedValue 전략을 기본값으로 설정(Auto)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String name;

    @Column(name = "TEAM_ID")
    private Long teamId;
    
    // ... getter, setter
}

 

(2) Team

@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;
    
    // ... getter, setter
}

 

(3) JpaMain - 저장, 조회

  • 팀과 회원을 저장해보면 회원 저장시 외래 키 식별자를 직접 다루게 됨
  • 회원이 속한 팀을 조회하고자 할 때에도 테이블간의 연관관계가 없다보니 계속 DB에서 값을 가져와야만 조회가 가능함
package jpabook;

public class JpaMain {
    public static void main(String[] args) {
    
        // ... 기존 코드 동일

        try {
            // 팀 저장
            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);

            tx.commit();
        } catch (Exception e) {
        
        // ... 기존 코드 동일
}

 

(4) 문제점

  • 객체를 테이블에 맞춰 데이터 중심으로 모델링을 하면 협력 관계를 만들 수 없음
  • 테이블은 외래 키로 조인을 해서 연관된 테이블을 찾고 객체는 참조를 사용해서 연관된 객체를 찾는 큰 간격이 있음
  • 객체 지향적 설계의 이점을 활용하지 못하여 추가 쿼리가 발생하게 되면 성능도 저하되고 코드도 많아져서 유지보수하기가 어려워지며 비즈니스 로직 구현의 복잡성이 증가됨
  • 애플리케이션의 규모가 커지먼 커질수록 더욱 복잡해짐

2. 단방향 연관관계

1) 단방향 매핑

  • Member에서 TeamId가 아니라 Team 객체를 매핑함

 

(1) Member 수정

  • @ManyToOne 애노테이션으로 Member객체와 Team의 객체가 다대일 관계임을 명시함
  • @JoinColumn으로 연관관계 매핑을할 외래키를 지정
//    @Column(name = "TEAM_ID")
//    private Long teamId;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    // ... 기존 코드들

 

(2) JpaMain 수정 - 저장, 조회, 수정

  • 연관관계 설정으로 참조를 사용하여 회원 저장시 Team 객체의 참조를 저장
  • 조회시에도 DB에서 값을 조회하는 것이 아니라 조회한 Member에서 바로 Team 정보를 가져올 수 있게 됨
  • 수정을 하고자 할 때에도 팀을 조회할 필요 없이 조회한 회원객체에 새로 입력할 팀 객체의 참조를 입력하면 바로 수정이 됨
  • Member에서 Team을 조회할 때 즉시로딩, 지연로딩 설정을 할 수 있는데 해당 설명은 이후에 자세히 설명함
package jpabook;

public class JpaMain {
    public static void main(String[] args) {
            // ... 기존 코드 동일
            
            // 회원 저장
            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();
            System.out.println("findTeam.getName() = " + findTeam.getName());

            // TeamB를 추가
            Team teamB = new Team();
            teamB.setName("TeamB");
            em.persist(teamB);

            // 조회된 멤버의 팀을 변경
            findMember.setTeam(teamB);
            Team updateTeam = findMember.getTeam();
            System.out.println("findTeam.getName() = " + updateTeam.getName());

            tx.commit();
            
            // ... 기존 코드 동일
}
  • 출력결과를 보면 참조를 사용해서 멤버의 팀을 조회한 결과와, 새로운 팀을 생성하여 TeamB로 수정한 결과과 정상적으로 반영됨

  • Member 조회시 JPA가 영속성 컨텍스트에 저장된 값을 반환하여 DB에서 조회하는 select쿼리가 보이지 않음
  • 만약 DB에서 조회하는 쿼리를 보고싶다면 em.flush()와 em.clear()를 사용하여 commit전에 SQL을 즉시 DB에 전송하고 영속성 컨텍스트를 초기화하여 조회단계에서부터 새로운 영속성 컨텍스트로 시작하면 DB에서 값을 조회하는 쿼리를 볼 수 있음
// 회원 저장 후 flush()와 clear()를 호출하여 SQL을 DB에 반영하고 영속성 컨텍스트를 초기화
em.flush();
em.clear();

// 이후에 조회하는 쿼리를 날리면 DB에서 가져오는 쿼리를 확인할 수 있음

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

1) 양방향 매핑

  • 객체를 양방향으로 매핑하려고 보면 테이블의 구조는 다이어그램을 단방향 매핑을 할때와 비교해봐도 변하는 것이 없음
  • 테이블의 연관관계는 방향이라는 개념이 없고 외래키로 두 테이블을 조회가 가능한데  객체는 각각 연관관계를 매핑해주어야 서로 탐색이 가능함
  • 이 부분이 객체와 테이블의 가장 큰 패러다임의 차이임

(1) Team 수정

  • @OneToMany로 일대다 매핑을 적용하여 @Member의 team 필드와 양방향 매핑관계가 적용됨
  • 관례상 일대다 매핑을 할때 ArrayList를 사용함(add시 NullPointerException 에러가 발생하지 않기 때문)
  • mappedBy로 어떤 필드와 매핑할 것인지 설정 -> 아래에서 자세히 설명
package jpabook.jpashop.domain;

@Entity
public class Team {
    // ... 기존 코드 생략
	
    // Member 객체에 일대다 매핑 추가  
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    // ... 기존 코드 생략
}

 

(2) JpaMain 수정

  • 양방향 매핑이 적용되어 em.find로 조회한 회원에서 팀을 조회하면 회원이 속한 Team의 정보도 가져올 수 있음
package jpabook;

public class JpaMain {
    public static void main(String[] args) {

        try {
            // ... 기존코드 생략

            em.flush();
            em.clear();

            // 연관관계 매핑을 활용하여 회원이 속한 팀의 멤버들을 조회
            Member findMember = em.find(Member.class, member.getId());
            List<Member> members = findMember.getTeam().getMembers();

            for (Member m : members) {
                System.out.println("m = " + m.getName());
            }

            tx.commit();
            
    // ... 기존코드 생략
}

 

** 참고

  • 실제 설계시 가급적이면 단방향 설계가 좋음
  • 양반향 설계는 신경쓸 포인트가 많아져서 구조가 복잡해지고 규모가 커지면 더욱 유지보수하기가 어려움

2) 연관관계의 주인과 mappedBy

(1) mappedBy

  • JPA가 어려운 이유중 하나이기에 단번에 이해하기가 어려움
  • 중요한 포인트는 객체와 테이블간에 연관관계를 맺는 차이를 이해해야 함

(2) 객체와 테이블이 관계를 맺는 차이

  • 객체는 양방향 연관관계를 맺을 때 매핑 포인트가 2개임
  • 연관관계를 맺을 엔터티가 각각 서로 참조를 해야함
  • 객체에서 양방향 연관관계를 맺었다는 것은 사실 단방향 연관관계가 서로 1번씩 2번 매핑한 것
  • 테이블은 외래키 하나로 두테이블의 양방향 연관관계가 성립이 되어 1번의 연관관계로 테이블들의 연관관계가 매핑 됨
  • 즉, 테이블에는 방향이 없이 외래키만 있으면 연관관계가 서로 성립된다는 뜻

좌) 객체의 양방향 매핑은 단방향 매핑을 2번 한것 / 우) 테이블은 외래피 하나로 두테이블이 연관관계를 가짐

(3) 딜레마

  • 객체가 서로를 단방향 매핑하고 있어 각각 참조하고 있기에 어떤 객체가 하나뿐인 외래키를 매핑해야 되는지 딜레마가 생김
  • 일반적으로 접근하면 Member가 Entity고 Entity가 Table로 설정 되었으니 당연히 Member의 team필드만 Team_ID가 매핑되어야 할것같지만 Team객체가 Member의 team을 참조하고 있기 때문에 Member 테이블의 외래키와 매핑이 되어있음
  • 극단적으로 객체를 수정해야 한다고 가정하고 두 객체 중 한쪽에만 값이 있거나 둘다 값이 있을 때 외래키는 계속 매핑 주체가 바뀌거나 어디를 매핑의 주체로 수정을 해야할지 혼란스러워짐
  • 그래서 두 객체의 연관관계 중 하나를 주인으로 지정하여 외래 키를 관리해야함

3) 연관 관계의 주인(Owner)

(1) 양방향 매핑 규칙

  • 객체의 두 관계중 하나를 연관관계를 주인으로 지정해야 함
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정)하며 주인이 아닌쪽은 읽기만 가능함
  • 주인은 mappedBy속성을 사용하지 않고, 주인이 아닌쪽에 mappedBy( ~ 에게 매핑이 되었다는 뜻) 속성으로 주인을 지정하여 해당 필드에 매핑이 되었다고 설정을 해야함

(2) 누구를 주인으로 해야하는가

  • 일대다 관계에서 다(N쪽)를 주인으로 정해야 함, 즉 외래키가 있는 곳을 주인으로 정해야 함
  • 테이블의 일대다 관계에서보면 다(N)쪽이 무조건 외래키를 가지고 있는데 이렇게 설정하면 다양한 문제점들을 해결할 수 있고 헷갈리는 경우를 방지할 수 있음
  • 이미지의 구조에서 Team을 연관관계의 주인으로 설정해버리면, Team의 Entity를 수정했는데 실제 DB는 MEMBER TABLE에 Update 쿼리가 반영되는 상황이 발생되어 일반적으로 JPA를 다루는 개념으로 접근 했을 때 혼란이 발생할 수 있음
  • Member객체가 연관관계의 주인이되면 한번에 MEMBER테이블에 쿼리를 반영하는데 Team객체가 연관관계의 주인이 되면 Team 객체에서 insert 쿼리가 발생한 후 Member 객체에서 Update 쿼리가 생성되어 MEMBER 테이블에 반영되어 추가적인 동작이 발생되어 성능면에서도 문제가 있음
  • Member.team이 연관관계의 주인이 되어 진짜 매핑이라고 표현함
  • Team.members는 team필드에 매핑이된 대상으로 가짜 매핑으로 표현함

** 참고

  • 연관관계의 주인은 표현때문에 비즈니스 적으로 엄청 중요하다고 생각할 수 있는데 비즈니스 적으로는 중요한 것이 아니라 두 테이블, 주 객체와의 관계에서 외래키를 누가 관리할 것이냐가 포인트임
  • 간혹 위와 같은 생각으로 접근하여 외래키가 없지만 비즈니스적으로 중요한 객체라고 판단된 곳에 연관관계의 주인을 설정해버려서 문제가 발생하는 경우가 종종 있음

4. 양방향 연관관계와 연관관계의 주인 - 주의점, 정리

1) 양방향 매핑 시 가장 많이 하는 실수

(1) 연관 관계의 주인에 값을 입력하지 않음 - JpaMain 수정

  • 연관관계의 주인이 아닌 Team의 members에 값을 집어넣고 실행해보면 쿼리는 2번이 실행되지만 실제 DB를 보면 값이 저장되어있지 않음

public class JpaMain {
    public static void main(String[] args) {
       // ... 기존 코드 생략
        try {
            // 저장
            
            // 연관관계 주인
            Member member = new Member();   
            member.setName("member1");
            em.persist(member);

            // 연관관계 주인이 아님
            Team team = new Team();         
            team.setName("team1");
            
            // 연관관계 주인이 아닌곳에 값을 저장
            team.getMembers().add(member);  
            em.persist(team);

            em.flush();
            em.clear();

            tx.commit();
        } catch (Exception e) {
        
    // ... 기존 코드 생략
}
  • 연관관계의 주인에 값을 입력하면 정상적으로 DB에 값이 반영됨
  • JPA의 관점으로 보면 연관관계의 주인인 Member에만 값을 설정해도 DB에는 정상적으로 반영됨

public class JpaMain {
    public static void main(String[] args) {
       // ... 기존 코드 생략
        try {

            // 저장
            Team team = new Team();
            team.setName("team1");
            // 연관관계 주인이 아닌곳에 값을 저장
//            team.getMembers().add(member);
            em.persist(team);

            Member member = new Member();
            member.setName("member1");
            // 연관관계 주인에 값을 저장
            member.setTeam(team);
            em.persist(member);

            em.flush();
            em.clear();

            tx.commit();
        } catch (Exception e) {
        
    // ... 기존 코드 생략
}

2) 객체지향적으로 양방향 연관관계 설계

(1) 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정해야 함

  • JPA의 관점으로는 연관관계의 주인쪽에만 값을 입력하면 DB에 정상 반영이되지만 순수한 객체지향적인 관계를 고려해보면 항상 양쪽 값을 입력해야 객체 그래프 탐색을 온전히 할 수 있음
  • 위 코드에서 em.flush()와 em.clear()를 제거하고 실행해보면 값이 정상적으로 조회되지 않는것을 확인할 수 있는데, em.find()로 조회하는 것은 DB에서 값을 조회하는 것인데 em.persist()만 호출된 상태에서 em.find()를 호출해서 조회했으므로 영속성 컨텍스트의 1차 캐시에만 값들이 저장되어있기 때문임
  • 테스트 케이스 작성 시 JPA없이 작성하는 경우도 많은데 이런 경우에 JPA에서는 정상적으로 값들이 조회되지만 테스트는 통과되지 않는 문제가 발생할 수도 있음

(2) 연관관계 편의 메소드를 생성

  • 이렇게 매번 두 연관관계 객체에 값을 입력하면 실수가 발생할 확률이 매우 높기 때문에 메소드를 만들어서 값을 입력하는데 이를 연관관계 편의 메소드라고 부름
  • 관습적인 getter, setter를 이용하지말고 엔터티에 별도의 메서드를 만들어서 값을 저장,수정,삭제 등이 발생했을 때 해당 메소드만 호출하여 양쪽의 객체의 상태가 변경되도록 하는 것이 좋음
  • Member클래스에 연관관계 편의 메서드를 작성하여 연관관계의 매핑이된 필드가 변경되었을 경우에는 연관관계 편의 메서드를 호출하면 됨
  • 연관관계 편의 메서드는 연관관계의 주인과 달리 꼭 외래키쪽에 꼭 둘 필요는 없어서 애플리케이션 상황과 정해진 룰에 따라서 두 객체중 한곳에 만들어 두고 해당 메서드를 사용하도록 공유하면 됨
  • 만약 양쪽에 모두 만들면 고려해야할 사항이 더욱 많아지고 잘못 사용하면 무한루프에 빠져 애플리케이션이 종료될 수 있으므로 둘 중 한곳을 정해서 만드는 것을 권장함
@Entity
public class Member {
    // ... 기존 코드들 생략
    
    // Member 클래스에 연관관계 편의 메서드 생성
    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

public class JpaMain {
    public static void main(String[] args) {
            // ... 기존 코드들 생략

            Member member = new Member();
            member.setName("member1");
            // 연관관계 편의 메소드를 호출하여 객체의 상태를 양쪽에 한번에 저장
            member.changeTeam(team);
            em.persist(member);

//            em.flush();
//            em.clear();

             // ... 기존 코드들 생략
}
/*
연관관계 편의 메서드는 외래키가 있는 곳에 둘 필요는 없으므로 상황에 따라 원하는 곳에
연관관계 편의 메서드를 만들어서 사용하면 됨
둘다 만들경우 잘못 사용하면 무한 루프에 빠질수도 있고 고민해야할 부분이 많아지므로 한곳에만 만들어서 사용
*/
@Entity
public class Team {
    // ... 기존 코드들 생략    

    public void changeMembers(Member member) {
        member.setTeam(this);
        members.add(member);
    }
}

 

(3) 양방향 매핑시에는 무한 루프를 조심해야함

  • 양방향 매핑이 된 객체들에 lombok 라이브러리 등을 사용하여 @ToString을 사용하거나 IDE의 toString() 생성기능을 활용하여 toString()를 생성하고 해당 객체를 사용하게 되면 서로의 toString에서 양쪽의 객체를 계속 조회하게 되어 무한루프에 빠져 스택오버플로우에러가 발생하고 프로그램이 종료가 됨
  • JSON 생성 라이브러리를 사용할 때도 엔터티를 JSON으로 변환하는 순간 무한 루프에 빠져버리게 되는데, 이를 방지하기 위하여 컨트롤러에서는 절대 엔터티를 직접 반환하지 말고 DTO를 사용해야 함

** 참고

  • 컨트롤러에서 엔터티를 사용하게되면 무한 루프 외에 또다른 문제가 발생함
  • JSON으로 변환하는 이유가 API 통신을 하기 위하여 JSON 스펙을 정의하고 API를 사용하는 쪽에서 해당 스펙에 맞춰서 데이터를 요청함
  • 그러나 비즈니스 요구사항이 변경되는 등의 이유로 엔터티는 변경이 될 수 있는데 이렇게 되면 JSON 스펙이 변경되어 버리기 때문에 서로 통신이 안되는 문제가 발생하기 때문에 컨트롤러에서는 꼭 직접 엔터티를 사용하지말고 DTO를 통해서 필요한 것만 사용해야 함

2) 양방향 매핑 정리

(1) 단방향 매핑만으로도 이미 연관관계 매핑은 완료된 것임

  • 양방향 매핑은 반대방향으로 조회(객체 그래프 탐색) 기능이 추가된 것일 뿐임

(2) 처음 설계시에는 단방향 매핑으로만 설계

  • JPA 모델링을 할때는 가급적 단방향 매핑으로 설계를 끝내는 것을 권장함
  • 실제 실무에 들어가면 테이블이 적으면 20개 많으면 100개 이상이 될 수도 있는데 처음부터 양방향 관계를 고려해서 설계를 하면 객체 관점에서는 고려해야할 부분만 많아지기 때문임
  • JPA를 사용하기로 결정 되었다면 우선 단방향 매핑으로만 테이블설계와 객체설계를 동시에 진행하면 테이블 관계에서 대략적인 외래키가 나오게 되고 일대다, 일대일 관계도 나오게 됨
  •  @ManyToOne으로 단방향 매핑만 적용해두고 초기 설계를 진행

(3) 실무에서는 역방향으로 탐색할 일이 많음 - 양방향 관계 발생

  • 실무에서는 JPQL 쿼리 작성시 역방향으로 탐색하는 경우가 자주 발생함
  • 단방향 매핑을 전부 해두었으니 mappedBy를 자바 코드에 추가만하면 양방향 매핑을 비교적 간단하게 추가할 수 있음
  • 단방향 매핑이 되어있는 곳에서 양방향 매핑을 추가한다고 하여 테이블에 영향을 주는 것이 아니기 때문에 단방향 설계로 먼저 진행 후 양방향 관계 설정이 필요하면 그때 추가하는 것을 권장함

(4) 연관관계의 주인은 외래키의 위치를 기준으로 정해야 하고 연관관계 편의 메서드는 둘 중 아무곳에 정의해도 상관없음


** jdbc.url 설정을 jpashop으로 변경해서 진행

<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/jpashop"/>

5. 실전 예제 - 연관관계 매핑 시작

1) 객체 구조 수정

  • 지난번 글에서 진행한 테이블(데이터)을 기반으로 작성한 코드로 연관관계 매핑을 적용하여 실습
  • 테이블 구조는 동일하며 객체 구조에서 외래키가 아닌 참조를 사용하도록 변경함

2) 단방향으로 먼저 설계

  • 위에서 설명했듯이 테이블과 객체의 구조를 보고 외래키와 일대다 관계를 보고 먼저 단방향으로만 매핑을 진행

(1) Order 수정

  • 기존에 설정했던 MemberId필드를 제거하고 Member객체를 연관관계로 설정
  • Member와 Order는 1대다 관계이고 Order가 외래키를 가지고 있으므로 Order에 @ManyToOne으로 설정하고 @JoinColumn(name = "MEMBER_ID")로 외래키를 지정
package jpabook.jpashop.domain;

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

    // ... memberId를 제외한 기존 코드 동일
    
    // Member를 연관관계로 설정
    @ManyToOne()
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    // ... getter, setter
}

 

(2) OrderItem 수정

  • orderId와 itemId 필드를 제거하고 Order와 Item을 연관관계로 설정
  • 외래키를 모두 OrderItem을 가지고 있고 연관관계로 설정한 객체들과 모두 다대일 관계이므로 @ManyToOne과 @JoinColumn을 설정
package jpabook.jpashop.domain;

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

    // ... orderId, itemId를 제외한 기존 코드 동일
    
    @ManyToOne
    @JoinColumn(name = "ORDER_ID")
    private Order order;

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

    // ... getter, setter
}

3) 양방향 관계 설정

  • 애플리케이션 개발 중 멤버 입장에서 주문 목록이 중요해졌다고 가정

** 참고

  • 지금은 예제이기 때문에 위와 같은 상황을 가정하여 양방향 관계를 설정하지만 대부분의 실무에서는 Member가 orders를 가지도록 설계하지 않고 설계할 필요가 없음
  • 특정 회원의 주문을 보고싶다고 쿼리를 한다고 가정을 하면 Order가 가지고 있는 member_id외래키를 이미 가지고 있기 때문에 바로 해결이 가능함
  • Member를 조회해서 해당 주문정보를 찾도록 설계하는 것은 관심사를 적절하게 끊어내지 못하고 복잡하게 설계가 되어 근본적으로 설계가 잘못 되었다고 볼 수도 있음
  • 회원은 회원만 가지고있고 주문은 주문만 가지도록 객체 관점으로 설계하는 것이 중요함
  • Order에서 OrderItem을 가지고있는 경우는 종종 발생하긴하지만 이 경우도 양방향 연관관계 설정 없이 개발이 가능함

(1) Member - Order 양방향 연관관계 설정

  • Member와 Order는 일대다 관계이므로 @OneToMany로 연관관계를 설정 - readOnly 매핑이 됨
  • 연관관계의 주인을 외래키를 가지고 있는 Order의 member필드로 설정
@Entity
public class Member {
   
    // 기존 코드 생략

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}

 

(2) OrderItem - Order 양방향 연관관계 설정

  • @OneToMany를 활용하여 일대다 연관관계 매핑을 설정하고 연관관계의 주인을 OrderItem의 order필드로 설정
@Entity
@Table(name = "ORDERS") // 주문은 ORDERS로 관례상 많이 씀
public class Order {

    //... 기존 코드 생략
    
    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<>();
    
}

 

(3) 연관관계 메서드 예시코드

  • 새로운 주문이 생성되면 OderItem도 함께 생성되도록 Order에 연관관계 메서드를 작성
  • 실무에서는 연관관계 메서드를 생성할 때 더 고려해야할 상황이 많지만 간단하게 적용할때에는 이정도로도 괜찮으며 외래키 소유 여부와 관계없이 비즈니스 상황등을 고려해서 Order에 연관관계 메서드를 생성한 모습
public class JpaMain {
    public static void main(String[] args) {
            // ... 기존 코드 생략
            
            // 새로운 주문이 생성되었다고 가정
            Order order = new Order();
            order.addOrderItem(new OrderItem());
            tx.commit();
}
            
// OrderItem과 Order의 연관관계 편의 메서드 생성            
public class Order {
    // ... 기존 코드 생략
    
    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }
}

4) 실무에서 양방향 관계 설정

  • 단방향 설계만 잘해두면 대부분의 문제는 코드로 해결할 수 있음
  • 양방향 관계는 개발상의 편의를 위해 설계하는 경우가 많음
  • 실무에서는 JPQL 쿼리를 자주 사용하게 되는데 JPQL 쿼리를 사용하다보면 조회를 좀 더 편하게 하기 위하여 양방향 관계를 고려하는 경우가 발생함
  • 좀 더 객체지향적으로 개발해야 하거나 비즈니스 로직상으로 양방향 관계를 설계해야만 하는 상황일 때 선택해서 양방향 관계를 적용하면 됨