본문 바로가기

JPA/양방향연관관계

N:M(다 대 다) 매핑 4: 양방향 연관관계 편의 메서드

전체코드 github 주소[branch: ManyToManyWithMappingMethod]

참고 강의: 배달의 민족 개발팀장 김영한 강사님의 JPA 강의

 

2021.05.22 - [JPA/양방향연관관계] - N:M(다 대 다) 매핑 3: 매핑테이블을 엔티티로 만들기

 

N:M(다 대 다) 매핑 3: 매핑테이블을 엔티티로 만들기

N:M 관계를 풀어낼 때 두 테이블을 엮기 위해 중간에 매핑테이블을 만들어야 한다. 그런데 이 때 매핑테이블을 @JoinTable과 @ManyToMany 어노테이션을 사용하여 만들면, 개발 도중에 발생하는 상황들

code-mania.tistory.com

해당 글은 위 글과 이어지는 내용입니다!


package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();

        try {
            // 수학 과목 등록
            Subject math = new Subject();
            math.setName("수학");
            em.persist(math);

            // 한국사 과목 등록
            Subject history = new Subject();
            history.setName("한국사");
            em.persist(history);

            // codeMania 학생 등록
            Student codeMania = new Student();
            codeMania.setName("code-mania");
            codeMania.setAge(21);
            em.persist(codeMania);

            // codeLover 학생 등록
            Student codeLover = new Student();
            codeLover.setName("code-lover");
            codeLover.setAge(21);
            em.persist(codeLover);

            // codeMania 학생 수학 과목 수강
            StudentSubject studentSubject = new StudentSubject();
            studentSubject.setStudent(codeMania);
            studentSubject.setSubject(math);
            em.persist(studentSubject);

            // codeMania 학생 한국사 과목 수강
            StudentSubject studentSubject2 = new StudentSubject();
            studentSubject2.setStudent(codeMania);
            studentSubject2.setSubject(history);
            em.persist(studentSubject2);

            // codeLover 학생 한국사 과목 수강
            StudentSubject studentSubject3 = new StudentSubject();
            studentSubject3.setStudent(codeLover);
            studentSubject3.setSubject(history);
            em.persist(studentSubject3);

            tx.commit();
            System.out.println("codeMania가 수강 중인 과목 수 = " + codeMania.getStudentSubjects().size());
            System.out.println("수학을 수강 중인 학생 수 = " + math.getStudentSubjects().size());
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        em.close();
        emf.close();
    }
}

 

이 코드에서 System.out.println 메서드를 통해 찍은 codeMania의 과목 개수와 수학을 수강 중인 학생의 수가 0이 나오고 있다.

이는 DB에서는 codeMania가 2개의 과목을 수강하고 있고, 객체에서는 0개의 과목을 수강하고 있다고 볼 수 있다.
또한 DB에서는 Math를 1명이 수강하고 있고, 객체에서는 0명이 수강하고 있다.

 

현재 코드에서 DB와 객체가 차이 나는 이유는 Entity의 연관관계는 양방향인데, 값 설정은 한 곳에서만 해주었기 때문이다.

Student → StudentSubject로 참조가 가능하고, StudentSubject → Subject로 참조가 가능하다.
하지만 값 설정은 StudentSubject 객체의 setStudent와 setSubject 메서드를 통해서만 이뤄지고 있다.

현재 코드에서 Student → StudentSubject로 참조는 가능하나 잘못된 값이 설정되어 있는 상태라고 볼 수 있다.

해결하러 가보자~~~~


양방향 연관관계 편의 메서드

양방향 연관관계 편의 메서드는 양방향 연관관계를 맺고 있는 Entity들을 위해 필요한 메서드이다.
한쪽에 값을 설정할 때 나머지 한쪽도 같이 설정해주는 역할을 한다.

그러면 양방향 연관관계 편의 메서드를 어디에 만들어야 할까?

A Entity와 B Entity가 양방향 연관관계를 맺고 있다면,

A Entity 혹은 B Entity 어디든 만들어도 된다!!

이제 코드를 통해 살펴보자~~~

('양방향 연관관계 편의 메서드'가 공식 명칭은 아니다.)

Student.java

package hellojpa;

import lombok.Data;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Data
public class Student {

    @Id @GeneratedValue
    private Long id;

    private String name;

    private int age;

    @ManyToOne
    @JoinColumn(name = "CLUB_ID", insertable = false, updatable = false)
    private Club club;

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    Locker locker;

    @OneToMany(mappedBy = "student")
    private List<StudentSubject> studentSubjects = new ArrayList<>();

    void addStudentSubject(StudentSubject studentSubject) {
        studentSubject.setStudent(this);
        this.studentSubjects.add(studentSubject);
    }
}

 

Student.java에 `addStudentSubject`라는 양방향 연관관계 편의 메서드를 추가했다.
addStudentSubject에서는 인자로 전달받은 studentSubject의 student값을 설정하고,
Student 객체의 studentSubjects에 해당 studentSubject를 추가하는 것을 볼 수 있다.

Student와 StudentSubject 객체에 모두 값을 설정해주고 있는 것이다.

StudentSubject.java

package hellojpa;

import lombok.AccessLevel;
import lombok.Data;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;

import javax.persistence.*;
import java.time.LocalDateTime;

@Data
@Entity
public class StudentSubject {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "STUDENT_ID")
    private Student student;

    @ManyToOne
    @Setter(AccessLevel.NONE)
    @JoinColumn(name = "SUBJECT_ID")
    private Subject subject;

    @CreationTimestamp
    private LocalDateTime registrationDate;

    public void takeSubject(Subject subject) {
        this.subject = subject;
        subject.getStudentSubjects().add(this);
    }
}

 

StudentSubject.java에 `takeSubject`라는 Subject와 StudentSubject 간의 양방향 연관관계 편의 메서드를 만들었다.
subject를 인자로 받아 StudentSubject 객체의 subject를 설정해주고,
Subject 객체의 studentSubjects 필드에 StudentSubject 객체를 아이템으로 추가하는 코드이다.

Subject와 StudentSubject 객체에 모두 값을 설정해주고 있다.

주의점
Subject 필드에 `@Setter(AccessLevel.NONE)`이라는 어노테이션이 달렸다.
이는 Subject의 Setter를 잠가버린 것이다.
(더 쉽게 말해 setSubject 메서드를 만들지 않은 것이다.)

`takeSubject`라는 Subject 및 StudentSubject 양쪽 객체에 모두 값을 설정해주는 메서드를 만들었다.
그런데 setSubject를 사용하면 StudentSubject에만 값이 설정된다.
실수로라도 setSubject 사용을 막기 위해 Setter를 잠그는 것이다.

JpaMain.java

이제 JpaMain에서 StudentSubject의 값을 설정할 때 만들어놓은 양방향 연관관계 편의 메서드를 사용하면 DB와 객체의 차이가 생기는 문제를 해결할 수 있다.

        try {
            // 수학 과목 등록
            Subject math = new Subject();
            math.setName("수학");
            em.persist(math);

            // 한국사 과목 등록
            Subject history = new Subject();
            history.setName("한국사");
            em.persist(history);

            // codeMania 학생 등록
            Student codeMania = new Student();
            codeMania.setName("code-mania");
            codeMania.setAge(21);
            em.persist(codeMania);

            // codeLover 학생 등록
            Student codeLover = new Student();
            codeLover.setName("code-lover");
            codeLover.setAge(21);
            em.persist(codeLover);

            // codeMania 학생 수학 과목 수강
            StudentSubject studentSubject = new StudentSubject();
            codeMania.addStudentSubject(studentSubject);
            studentSubject.takeSubject(math);
            em.persist(studentSubject);

            // codeMania 학생 한국사 과목 수강
            StudentSubject studentSubject2 = new StudentSubject();
            codeMania.addStudentSubject(studentSubject2);
            studentSubject2.takeSubject(history);
            em.persist(studentSubject2);

            // codeLover 학생 한국사 과목 수강
            StudentSubject studentSubject3 = new StudentSubject();
            codeLover.addStudentSubject(studentSubject3);
            studentSubject3.takeSubject(history);
            em.persist(studentSubject3);

            tx.commit();
            System.out.println("codeMania가 수강 중인 과목 수 = " + codeMania.getStudentSubjects().size());
            System.out.println("수학을 수강 중인 학생 수 = " + math.getStudentSubjects().size());
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

실행결과

실행결과를 살펴보면 정상적으로 과목 수와 학생 수가 출력된 것을 확인할 수 있다.