엔티티 매핑 - 연관관계

김종하·2021년 3월 16일
0

JPA

목록 보기
6/10

엔티티와 엔티티의 필드들이 어떤식으로 매핑되는지에 대해 알아보았다.
하지만 앞선 포스팅에서 다루지 않은 부분이 있는데 그것은 객체가 연관관계를 다루는 법과, RDB가 연관관계를 다루는 방법의 차이다.

객체와 연관관계

객체에서는 연관관계를 어떻게 표현하는가?
바로, 참조를 이용해서 연관관계를 표현한다.
예시와 코드를 통해 무슨 말인지 이해해보도록 하자

우선, '학생' 객체와 '동아리' 객체가 있다고 생각해보자.
'jaden' 이라는 학생이 'jpaLover' 이라는 동아리에 가입되어 있음을 코드로 작성해보자

// 학생 클래스
public class Student {
    private String name;
    
    private Circle circle;
    
    (getter & setter)
}
// 동아리 클래스 
public class Circle {

    private String name;

    (getter & setter)
}



Student jaden = new Student();
jaden.setName("jaden"); // jaden 이라는 학생객체를 만들었다 

Circle jpaLover = new Circle(); 
jpaLover.setName("jpaLover"); // jpaLover 라는 동아리객체를 만들었다 

jaden.setCircle(jpaLover); // jaden 이 jpaLover 에 소속되어있음을 참조로 표현했다. 

// jaden이 소속된 동아리가 어떤 동아린지 궁금하다면?
Circle jadenCircle = jaden.getCircle(); 

이처럼 우리는 객체세상에서 연관관계를 나타내고 싶을 때, 참조를 사용해 표현한다.

객체 세상의 연관관계는 참조로 표현할 수 있다.

RDB의 연관관계

데이터베이스에서 연관관계는 어떻게 표현하는가?
바로, 외래키(FK)를 활용하여 연관관계를 표현할 수 있다.
마찬가지로 jaden 이라는 학생이 jpaLover 라는 동아리에 가입되어있음을 데이터베이스에서 어떻게 표현할지 이미지로 살펴보자

테이블을 보면 jaden 이라는 학생이 동아리 ID 가 1번인 동아리와 연관관계가 있음이 표현되어있는 것을 알 수 있다.

데이터베이스 에서는 연관관계를 외래키를 사용하여 표현할 수 있다

그렇다면 JPA 에서는..?

우선 코드를 먼저 보면서 알아보도록 하자.

Entitiy 객체들은 다음과 같이 설계할 수 있다.

@Entity
public class Student {

    @Id @GeneratedValue
    @Column(name = "student_id")
    private Integer id;

    private String name;

    /*
    학생이 최대 하나의 동아리에만 가입할 수 있다고 가정하자.
    ManyToOne 이란 학생(1) - 동아리(다) 1:다 관계를 표현한다. 
    */
    @ManyToOne	
    @JoinColumn(name = "circle_id") // Circle 의 어떤 필드가 FK가 될지 명확히 보인다.
    private Circle circle;
}

@Entity
public class Circle {

    @Id @GeneratedValue
    @Column(name = "circle_id")
    private Integer id;

    private String name;

}

Entity 들을 사용해 영속화를 진행해 보자

public static void main(String[] args) {

        // EntityMangerFactory 생성
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // EntityManger 생성
        EntityManager em = emf.createEntityManager();

        // 트랜잭션 단위로 실행한다.
        EntityTransaction transaction = em.getTransaction();
        transaction.begin();

        try{
            Student jaden = new Student();
            jaden.setName("jaden");
            em.persist(jaden);

            Circle jpaLover = new Circle();
            jpaLover.setName("japLover");
            em.persist(jpaLover);
            
           
            em.flush();
            em.clear();
            
            transaction.commit();
        } catch (Exception e){
            transaction.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

코드를 진행한 후 실제 DB를 조회해보면 다음과 같은 결과가 나온다.

이를통해 JPA를 활용하여 코드에서는 참조를 통해 표현한 연관관계가 DB에서는 외래키를 사용한 연관관계로 잘 표현된 것을 확인할 수 있다.
하지만 과연 이것으로 모든 문제가 해결된 것일까?

만약 jpaLover 동아리에 가입한 학생들을 알고싶다면 어떻게 할 것인가?

우선, 엔티티객체에 가입한 학생들을 참조할 수 있는 필드가 필요할 것 같으니 추가해보도록 하자.

@Entity
public class Circle {

    @Id @GeneratedValue
    @Column(name = "circle_id")
    private Integer id;

    private String name;
    
    @OneToMany
    private List<Student> studentList = new ArrayList<>();
}

이쯤에서 궁금증이 하나 생긴다. 데이터베이스 입장에서 보면

  1. 학생이 가입한 동아리를 확인하는 작업
  2. 동아리에 가입된 학생들을 확인하는 작업

두 가지 작업 모두가 CIRCLE_ID 를 이용해 조인해오면 된다.

그렇다면 학생테이블의 CIRCLE_ID 을 설정하는 작업을 어디서 해야할까?
우선 가능한 방법을 모두 고려해 보자.

  1. student.setCircle(circle);
  2. circle.getStudentList.add(student);

바로 이부분에서 JPA는 문제가 생긴다. 도대체 CIRCLE_ID 를 언제 업데이트 해주어야하는 것인가에 대한 문제이다.
학생객체에서 동아리를 업데이트할 때? 동아리에서 학생들을 업데이트할 때?
이러한 문제를 해결하기 위해서 '연관관계의 주인' 이라는 개념을 만들었다.
연관관계의 주인에서만 DB의 업데이트 작업을 수행할 수 있게 하고, 주인이 아닌쪽은 조회만 가능하게 설정하는 것이다.
그리고, 주인관계는 주인이 아닌쪽에 mappedBy 속성을 사용하여 표현한다.

@Entity
public class Circle {

    @Id @GeneratedValue
    @Column(name = "circle_id")
    private Integer id;

    private String name;

    @OneToMany(mappedBy = "circle") // 연관관계의 주인이 아님. 
    private List<Student> studentList = new ArrayList<>();
}

연관관계의 주인은 ..?

연관관계의 주인은 둘 중 아무쪽에서 주인이 되어도 상관없다. 하지만, 테이블 구조를 생각해보면 FK 가 있는쪽이 주인이 되는게 가장 직관적인 이해를 돕는다. 동아리 관련 코드를 만졌는데 학생테이블의 CIRCLE_ID 가 업데이트가 되는 것은 직관적이지 않기 때문이다. 따라서 관례적으로 FK 가 있는 쪽 ( 예시에서는 학생테이블에 동아리의 FK 를 가짐으로 학생쪽 ) 을 연관관계의 주인으로 설정한다.

  • 연관관계의 주인만 연관관계에 대한 업데이트를 수행하고, 반대쪽은 조회만 가능하다.
  • 연관관계의 주인은 FK 를 가진 쪽으로 하는게 바람직하다.

주의점과 고려할 사항

연관관계의 주인이 아닌쪽에서 업데이트를 한 경우, DB에 업데이트가 일어나지 않는다.
즉, circle.getStudentList.add(student); 를 사용하면 데이터베이스의 학생테이블의 CIRCLE_ID 에는 아무런 값이 들어가지 않는다. 동아리 객체가 연관관계의 주인이 아니기 때문에, 조회만 가능하기 때문이다.
즉, student.setCircle(circle); 를 사용해야만 데이터베이스의 학생테이블에 CIRCLE_ID 가 들어가게 되는 것이다.

연관관계의 주인이 되는 객체에서 DB값을 변경할 수 있다. 반대쪽은 조회만 가능

student.setCircle(circle); 를 써서 연관관계에 대한 정보를 입력하면 JPA 는 문제없이 연관관계를 테이블에 표현해준다. 하지만 1차캐시와 관련된 문제가 발생할 수 있다. 다음의 상황을 생각해보자

            Student jaden = new Student();
            jaden.setName("jaden");
            em.persist(jaden);
            // 학생이 생성되고 1차캐시에 들어감

            Circle jpaLover = new Circle();
            jpaLover.setName("japLover");
            em.persist(jpaLover);
            // 동아리가 생성되고 1차캐시에 들어감 

            jaden.setCircle(jpaLover);
            // 학생(주인) 쪽에서 연관관계 설정 

            Circle circle = em.find(Circle.class, jpaLover.getId());
            // 동아리는 DB에서 가져오는 데이터가 아니라, 1차캐시에 있는 동아리를 가져옴 

            List<Student> studentList = circle.getStudentList();
            for (Student student : studentList) {
                System.out.println("=========");
                System.out.println(student.getName());
                System.out.println("=========");
            }
            // 1차 캐시의 동아리에는 아무런 연관관계를 찾을 수 없음으로 로그가 찍히지 않음.

            transaction.commit();

이를 해결하기 위해선

           Student jaden = new Student();
            jaden.setName("jaden");
            em.persist(jaden);

            Circle jpaLover = new Circle();
            jpaLover.setName("japLover");
            em.persist(jpaLover);

            jaden.setCircle(jpaLover);

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

            Circle circle = em.find(Circle.class, jpaLover.getId());

            List<Student> studentList = circle.getStudentList();
            for (Student student : studentList) {
                System.out.println("=========");
                System.out.println(student.getName());
                System.out.println("=========");
            }


            transaction.commit();

연관관계의 설정이 되면 flush() 와 clear()를 통해 데이터베이스에 반영하고 1차캐시를 지운 후 연관관계가 반영된 객체를 1차캐시에 넣어주는 작업이 필요하다. 불편하기도 하고 혼란을 야기할 수 있는 이런 문제를 어떻게 해결할 수 있을까?
우선 학생 클래스를 다음과 같이 수정해보자

public class Student {

    @Id @GeneratedValue
    @Column(name = "student_id")
    private Integer id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "circle_id")
    private Circle circle;
    
    // 추가 부분 
    public void joinCircle(Circle circle) {
        this.circle = circle;
        circle.getStudentList().add(this);
    }
}

바로, joinCircle 을 활용해 연관관계를 설정하면 되는 것이다.

            Student jaden = new Student();
            jaden.setName("jaden");
            em.persist(jaden);

            Circle jpaLover = new Circle();
            jpaLover.setName("japLover");
            em.persist(jpaLover);

            jaden.joinCircle(jpaLover);


            Circle circle = em.find(Circle.class, jpaLover.getId());

            List<Student> studentList = circle.getStudentList();
            for (Student student : studentList) {
                System.out.println("=========");
                System.out.println(student.getName());
                System.out.println("=========");
            }


            transaction.commit();

굳이, 1차캐시의 특성을 고려하지 않더라도 자연스럽게 코드를 작성해 나갈 수 있다

해당 포스팅은 김영한님의 '자바 ORM 표준 JPA 프로그래밍' 강의를 참고하여 작성된 내용입니다.

0개의 댓글