연관관계의 주인을 바꾸면 무슨일이 발생할까?

반응형
반응형

JPA에서 공부하면서 곰곰히 생각해봤다.
내가 과연 어떻게 테이블을 작성했는지... 또, 마이바티스를 어떻게 이용했는지 생각해봤다.
1년전에 보이지 않는 내용들이 점차 눈에 들어오기 시작했다.
가장 눈에 띄이는건 JOIN관계다. 애초에 테이블을 만들때 JOIN을 사용하지 않는다.
SQL은 반드시 외래키를 사용해야 된다는 제약 사항이 전혀 없다.
오히려 외래키를 사용하지 않고 DB를 구축하기도 한다. 
그래서 JOIN문을 사용할때 외래키를 사용하지 않고 쿼리 작성하는것이 가능하다.
신기하게도? JPA는 JOIN문을 쿼리 생성하기 전에 미리 만들어 둔다.

무조건 JOIN문을 사용해야 되는것은 아니지만, 
이러한 점이 SQL을 작성할때와의 차이점이라 생각이 든다.
그렇다면 어째서 JPA는 이러한 전략을 택했을까?
아마도 객체지향과 유사하게 작성하기 위한 전략이 아닐까?

이걸 생각하기 위해서는 일단 객체지향과 테이블이 어떤 차이가 있는지 생각해봐야 한다.
애초에 테이블은 액셀처럼 데이터만 쌓여있는 형태다.
예를들어, 데이터 테이블이 2개가 있다고 가정하자,
만약 데이터를 묶어서 보여줘야 한다면, 조인을 걸어서 새로운 테이블을 만들어야 된다.
방향이 달라져도 보여주는 데이터는 같다는 사실을 알 수 있다.
물론, 외부 조인을 걸게 되면 방향에 따라 보여주는게 차이가 있지만 
여기에서 말하는 조인은 내부 조인을 뜻한다.

대충 쿼리를 작성하면 다음과 같다.

SELECT * FROM A a
INNER JOIN B b
ON a.id = b.id;

또, 방향이란 테이블의 위치를 바꾸는 것을 의미한다.

쿼리상에서는 이게 1:N조합인지 1:1조합인지 알 방법이 전혀 없다.
테이블상에서 직접적으로 이것을 알 수 있는 방법은 딱 한가지 UML확인하는 수밖에 없다.
물론, 테이블의 데이터를 보고 대략적으로 짐작은 할 수 있겠지만 테이블은 데이터를 쌓고 그것을 확인할 수 밖에 없다.

그러면 객체지향에서는 어떨까?
객체지향에서도 결과를 봐야 이것이 어떤 조합인지 아는 걸까?
애초에 객체 지향이라는 말이 객체가 중점이 되야 된다는 이야기다.
즉, 학교안에 학생이라면
학교는 하나고 학생은 여러명일 수도 있다는 뜻이다.
즉 여기서 알 수 있는 점은

학교 : 학생 = 1 : N

으로 정리할 수 있다.

이것으로 미뤄봤을 때, JPA로 쿼리를 작성한다는 의미는 객체지향적으로 설계한다는 의미다.
그러니까 이것에 대한 정보가 뚜렷하게 보여야 된다는 뜻이된다.
어디가 1이고 어디가 N인지 확실한 정보가 필요하다.
엔티티를 만들때 각 객체간의 관계가 어떤 관계인지 확실하게 알 필요가 있다는 뜻이다.
그래서 create table를 날리기전에 이미 객체간의 관계가 이미 성립이 되었다는 뜻이 되버린다.

위에서 학생과 학교에 대한 예시를 뒀기 때문에 간단한 JPA 엔티티를 만들어보자.
간단한 엔티티를 만들었다.

@Entity
public class Student {
  @Id @GeneratedValue
  private Long id;
  private String name;

  @JoinColumn
  @ManyToOne
  private School school;
}

학생코드

@Entity
public class School {
  @Id @GeneratedValue
  private Long id;
  private String name;

}

학교 코드

일반적으로 학생은 학교를 하나만 다니기 때문에 이렇게 작성을 했다.
자세히 보면 JoinColumn에 아무런 값이 존재하지 않는다는 것을 알 수 있는데
이는 school의 id값이 default값으로 정해져있기 때문이다.
만약 다른 이름으로 지정하고 싶다면, name 키워드를 사용하면 변경이 된다.
즉, 이는 테이블에 영향을 주는거지 객체에는 아무런 해약이 없다고 할 수 있다.

이제 데이터를 넣어보자.


데이터는 정상적으로 들어왔다.
이제 데이터를 출력해보자.
이유는 모르겠지만 em.flush() em.close 를 하는순간 에러가 발생한다.
em.close가 아니라 em.clear입니다. 실수로 close를 작성했네요. 어쩐지 오류가 발생하더라구요.


이걸 하게 되면 1차 캐시를 지우고 실제 DB에서 데이터를 가져오는 걸로 알고 있는데 이상하게도 데이터가 추가 되지 않았다.
아무튼 이걸 해결하는게 이 공부에 목적이 아니기 때문에 과감히 넘어갈예정이다.
그냥 select문 없이 이해하는 것이 빠를 것 같다.
상상속으로만 생각해야지

아무튼 flush의 유무에 따라 1차캐시에서 가져오는지 DB에서 가져오는지 알 수 있다.
근데 반대로 학교에서 어떤 특정한 학생을 찾고 싶어한다고 가정하자.
그래서 코드를 이렇게 변경하였다.

  @JoinColumn
  @OneToMany
  private List<Student> students;


데이터가 아무것도 추가되지 않았다.
롤백이 되었다는 것이 확인되었다. 아마도 양쪽 방향으로 열려있는데 어디로 추가할지 모르기 때문에 이러한 경과가 나타나지 않았나 추측해본다.

아 일단 학생 -> 학교의 연관관계는 제거해두자 그래야 단방향이 되니까
지금은 양방향이 아니기 때문이다.
이상한 쿼리가 발생했다.

Hibernate: 
    /* create one-to-many row helloJava.School.students */ update
        Student 
    set
        students_id=? 
    where
        id=?

업데이트 쿼리가 발생했다.
나는 단순히 insert만 했을뿐인데 update라니 개인적으로 말이 되지 않는다고 생각이 든다.
다행스럽게도 데이터는 정상적으로 들어왔다.

근데 insert만 해도 될 일을 굳이 update쿼리를 날리면서 insert를 해야할까?
학생 -> 학교의 흐름은 자연스럽게 insert쿼리만 사용해도 되었지만,
학교 -> 학생의 흐름으로는 insert뿐만 아니라 update쿼리까지 발생해야 된다는 문제가 발생했다.
이게 의미하는 바는 자연스럽지 않다는 것도 알 수 있다.

따라서 단방향은 학생 -> 학교의 관계일때만 사용하는것이 좋다고 생각이 든다.
근데 이게 어떤것을 의미하는지 생각해야 된다.

  @JoinColumn
  @ManyToOne
  private School school;

잘보면 학생 -> 학교의 관계는 이런식으로 객체만 작성하는것에 반면에

@JoinColumn
@OneToMany
private List<Student> students;

학교 -> 학생의 관계에는 이런식으로 작성되는데 객체뿐만아니라 List도 추가된다는 것을 알 수 있다.
이것은 다시 테이블을 생각해보면 이해할 수 있는데
애초에 테이블은 List를 사용되지 않고 데이터가 쌓여지는 형태다.
따라서 School이 외래키라는 것이라는 짐작할 수 있다.

왜냐하면 애초에 테이블에 존재하지도 않는 LIst를 외래키로 지정한다는 것은 말이 되지 않는다.
이렇게 해서 양방향을 만들었다.
근데 위에서 언급했듯이 데이터가 추가 되지 않았다.
이는 데이터 충돌때문에 발생한거라 생각이 된다.
양쪽에서 데이터를 넣으려고 하니 문제가 발생한게 아닌가 추측해본다.
그래서 특정한곳은 읽기만 가능하게 하는 것이 좋다고 생각이 든다.
그러면 양쪽에서 데이터를 넣는일은 없기 때문이다.
그러면 어디로 데이터를 넣는냐가 중요해지는데 최대한 insert는 insert만 되어지는게 낫다고 생각이 든다.

그래서 Studnet를 주인으로 두는것이 더 나을 것 같다.
코드를 이렇게 수정했다.

  @OneToMany(mappedBy="school")
  private List<Student> students;

 즉 이것을 해석해보면 반대편, 즉 Studnet에 존재하는 school에 매핑된다는 사실을 알 수 있다.
원래 Stduent에 넣을려고 했는데 JPA자체에서 막은 것 같다.

그래서 연관관계의 주인을 바꾸면 어떤일이 발생할지 궁금했었는데 그건 하지 못하게 되었다.
이제 이런식으로 데이터를 뽑아낼 수 있다.
아쉽게도 nullPoint가 발생했다. 이를 방지하기 위해 이런식으로 코드를 변경했다.
아 어쩐지 안되더라 clear를 실수로 close로 작성해서 select가 나오지 않던거 였다.
아무튼 다시 넘어가서

근데 내가 이런식으로 조회를 한다면?

for (Student findStudent : school.getStudents()) {
        System.out.println(findStudent.getName());
}

이것도 같은 결과가 나오는것이 맞다.
근데 이상하게도 데이터가 전혀 나오고 있지 않는다.
그 이유는 직접적으로 데이터를 넣지 않았기 때문이다. 그래서 school쪽은 읽기 전용이라는 이야기다.
근데 나는 위도 같은 결과를 도출하고 싶어졌다.
그래서 다음과 같이 코드를 수정했다.

  public void setSchool(School school) {
    this.school = school;
    school.getStudents().add(this);
  }

원래는 메소드명도 변경하는것이 맞지만 귀찮아서 바꾸지 않았다.

=======================
1번 학생
2번 학생
=======================

정상적으로 데이터가 들어온다는 사실을 알게 되었다.
결국은 mappedby 속성이 있는 곳은 직접적으로 데이터를 넣을 수 없다는 사실을 알게 되었다.

반응형

'JPA' 카테고리의 다른 글

JPQL 기본 문법  (0) 2021.12.18
값 타입 작성 방법  (0) 2021.12.12
Proxy?  (0) 2021.12.04
상속하는 방법?  (0) 2021.11.27
Entity와 Table  (0) 2021.10.16

댓글

Designed by JB FACTORY