[JPA] @OneToMany 단방향을 사용하면 안되는 이유
Spring

[JPA] @OneToMany 단방향을 사용하면 안되는 이유

728x90

[JPA] @ManyToOne, @OneToMany 뭘 써야할까? 편에서 단방향, 양방향 맵핑에 대해서 다뤘었죠!

 

그리고 분명 단방향만으로 충분한 경우에는 굳이 양방향 맵핑을 안하는게 좋다고도 했는데... 사실 단방향 중에 @OneToMany 맵핑으로만 이루어진 단방향 맵핑은 사용하지 않는게 좋습니다.

 

뭐야?!? 단방향 쓰라더니 왜 또 쓰지 말라는거야 ㅡㅡ  라고 하실 수도 있지만.. 여기에는 당연히 이유가 있습니다. 어떤 문제가 있는지, 해결방법은 없는지 지금부터 한번 알아보겠습니다!


@OneToMany 단방향 세팅

우선 Classroom, Student로 @OneToMany 단방향 맵핑을 구현해보겠습니다.

// Classroom 엔티티
@NoArgsConstructor
@Getter
@Entity
public class Classroom {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String classroomName;

  @OneToMany(cascade = CascadeType.ALL)
  @JoinColumn(name = "classroom_id")
  private List<Student> studentList = new ArrayList<>();

  public Classroom(String classroomName) {
    this.classroomName = classroomName;
  }
}
// Student 엔티티
@NoArgsConstructor
@Getter
@Entity
public class Student {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String studentName;

  public Student(String studentName) {
    this.studentName = studentName;
  }
}

@OneToMany 단방향 저장

// 비즈니스 로직
@Service
@RequiredArgsConstructor
public class SampleService {

  private final ClassroomRepository classroomRepository;

  public Classroom saveClassroom(){
  
    // 교실 1개에 학생 10명 맵핑하여 저장
    Classroom classroom = new Classroom("교실1");
    for (int i=1; i<=10; i++) {
      classroom.getStudentList().add(new Student("학생" + i));
    }
    return classroomRepository.save(classroom);
  }
}

꽤 직관적인 로직이죠! 여기서 saveClassroom() 메소드를 호출하면 과연 몇번의 쿼리가 발생할까요?

음.. 교실이 1개 저장돼야하고, 학생이 10개 저장돼야 하니까.. insert 쿼리가 1 + 10 = 11개 생기겠네요! 자 그럼 설레는 마음으로 Hibernate가 만들어주는 쿼리를 봐볼까요??

Hibernate: 응~ 쿼리 21개 날릴게~~^^ 어쩔JPA~ 단방향티비~ 쿠쿠OneToMany삥뽕^^

생성된 쿼리를 자세히 보시면 Student 객체를 저장할 때 insert후에 update 쿼리가 추가적으로 생성되는 보실 수 있습니다.

 

왜 이런 일이 발생할까요?

바로 실제 외래키는 Student 테이블에 존재하지만, 외래키가 관리되는 엔티티는 Classroom이기 때문입니다!

 

JPA repository 인터페이스를 상속받아 메소드를 사용해서 DB로 저장되는 과정을 혹시 기억하시나요?? 영속성 컨텍스트에 엔티티가 영속되고 flush를 통해 모든 SQL 쿼리가 한번에 실행되게 되죠.

**혹시 위 문장을 보고 네..? flush요? 라는 생각이 드시는 분은 잠깐 멈추고 여기로 가셔서 영속성 컨텍스트와 flush에 대해 이해하고 오시는 것을 추천합니다!

 

JPA DB 저장과정 요약

간단하게 다시한번 요약하면 jpa로 DB에 CRUD를 수행하려면, 먼저 repository를 상속받아서 메소드를 통해 영속성 컨텍스트에 엔티티를 영속시키게 됩니다. 그리고 쓰기지연을 통해 현재 transaction내 로직에서 필요한 모든 SQL명령어가 계속 모이고, flush가 일어나면 한번에 모았던 SQL 쿼리를 파바박 보내서 DB에 실질적인 변화를 주게 되죠!

 

따라서 위의 방식대로 저장을 하게 되면 Classroom이 영속성 컨텍스트를 통해 저장되고, Student도 영속성 컨텍스트를 통해 저장되는데, Student 엔티티에는 외래키가 담길 공간이 없습니다 ㅠㅠ 따라서 먼저 Student 엔티티가 저장되고, 후에 외래키를 업데이트 해주는 것이죠.


해결방법 -> 양방향으로!! 그리고 연관관계의 주인을 외래키가 있는곳으로!!

가장 간단한 해결방법은 연관관계를 양방향으로 바꾸고, 더 중요한것은 연관관계의 주인을 외래키가 실제로 있는 테이블(엔티티)로 지정하는 것입니다!

 

따라서 다음과 같이 바꾸게되면

// Classroom 엔티티
@NoArgsConstructor
@Getter
@Entity
public class Classroom {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String classroomName;

  @OneToMany(mappedBy = "classroom", cascade = CascadeType.ALL)
  private List<Student> studentList = new ArrayList<>();

  public Classroom(String classroomName) {
    this.classroomName = classroomName;
  }
}
// Student 엔티티
@NoArgsConstructor
@Getter
@Entity
public class Student {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String studentName;
  
  @ManyToOne
  @JoinColumn(name = "classroom_id")
  private Classroom classroom;

  public Student(String studentName) {
    this.studentName = studentName;
  }
}

저장시에 아래와 같이 필요한 쿼리 11개만 생성이 된것을 볼 수 있습니다.

위에서 생성된 Hibernate 쿼리와 비교해보세요!


@OneToMany, @ManyToOne 맵핑은 평소에 자주보지만 경우에 따라 효율에 큰차이가 생길 수 있으니 다시한번 정확히 알고 쓰는게 중요하다는 것이 느껴집니다!

 

출처:
- The best way to map a @OneToMany relationship with JPA and Hibernate
- OneToMany-단방향-매핑의-단점
728x90