연관관계에 대한 고찰

개인적으로 진행하던 백엔드 서버 스터디에서 엔티티 연관관계에 대한 얘기가 나왔었는데, 정확히 어떤 상황에서 양방향을 사용해야 하는지, 어떤 연관 관계를 사용해야 하는지에 대해 답변하지 못했다. 이번 기회에 연관 관계에 대한 내용을 복습하고 정리해 본다.

일대일(1:1) 관계

우선 mappedBy라는 속성을 이해하기 위해서는 두 가지 핵심을 알아야 한다.

첫째는 “외래키를 가지는 엔티티가 그 관계의 주인이다.” 라는 것이고, 둘째는 mappedBy의 의미는 “외래키를 대상 엔티티에 위임하겠다.”라는 의미라는 것이다. 그래서 mappedBy 속성을 설정한 대상 엔티티에는 @JoinColumn 어노테이션으로 외래키를 지정해야 한다. (지정하지 않을 경우, Default로 mappedBy 에 의해서 자동으로 생성된다. ) 이 mappedBy속성의 대상이 되는 엔티티는 관계에서 주인이 되어 외래키가 부여됩니다.

만약, 일대일 양방향 관계에서 어느쪽에도 mappedBy 를 사용하지 않으면 양쪽 엔티티 모두 @JoinColumn이 있든 없든 외래키를 가지게 되고 그로 인해 각 외래키를 기준으로 조인 쿼리가 각각 2번 실행되어 비효율적이게 된다. 때문에 반드시 일대일 양방향 관계에서는 쓸데 없는 join이 없도록 mappedBy라는 속성을 사용하여 관계의 주인에만 @JoinColumn을 추가해 외래키를 부여하도록 해야 한다.

다대일(N:1) 관계

그리고 다대일 관계에서의 양방향이 좋은가? 단방향이 좋은가? 에 대한 해답이다. 정답은 설계에 정답이 없듯이, “기본적으로 단방향으로 하고 필요가 있으면 양방향을 하라” 이다. 양방향 관계는 단방향에 비해 비용이 많이 든다. 양방향 관계는 양쪽 객체 모두 다뤄줘야 하고 삭제되지 않은 관계가 없도록 로직을 관리해야 하며, 엔티티에 연관관계 필드가 늘어나기 때문에 코드를 복잡하게 만드는 일로 이어지기도 한다. 때문에 그렇게 되지 않도록 최대한 양방향 관계를 결정하는 일에 대해서는 소극적인 자세로 대하여야 하는것이 바람직해 보인다. 그럼 언제 양방향 관계를 맺어야 할까?

양방향 관계는 데이터베이스 관계와 무관하다. 순전히 객체의 참조를 편하게 사용하기 위함이기 때문에 양방향의 필요성은 객체를 사용함에 있어의 필요성과 같다. 위와 같은 문제가 있더라도 객체를 사용함에 있어 외래키가 없는 반대쪽 엔티티에서 참조할 일이 많아 오히려 단방향만으로는 가독성이 떨어지고 유지보수에 좋지 않다고 판단 된다면 양방향으로 구현하면 되는 것이다.

양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다.

일다대(1:N) 관계

그리고 다대일, 일대다의 기준은 외래키가 어느쪽에 있느냐 이다. 다(N)쪽에 엔티티에 외래키가 있다면 다대일, 일(1)쪽 엔티티에 외래키가 있다면 일대다 연관관계인것인데, 일대다 관계에는 2가지 문제가 존재 한다. 이유는 데이터베이스의 세계에서는 외래키가 항상 다(N)쪽에 있지만 일대다 연관관계는 외래키가 일(1)쪽에 있어, 데이터베이스와 엔티티 두 형상이 일치하지 않아서 발생하는 문제이다. 자세히 알아보면,

첫 째, “일대다 단방향 연관관계에는 불필요한 update가 실행되는 문제가 있다.” 이고, 둘 째는 “일대다 양방향 관계는 공식적으로 없다.” 이다. 전자에 대해 설명 하자면, 단방향이기 때문에 mappedBy 속성을 부여할 상대 엔티티가 없고 일(1)쪽 엔티티에는 @JoinColumn으로 다(N)쪽 엔티티에 존재하는 외래키를 억지로 부여하여 주인이된 상태일 것이다.(실제로 DB 테이블엔 다(N)쪽에 외래키가 존재하지만 엔티티에선 일(1)쪽에 존재함)이런 특이한 구조로 인해 다(N)쪽에 하나의 로우를 insert 하기 위해서는 이 연관관계를 해결하려 update 쿼리가 추가로 발생하는 문제가 발생 한다. 때문에 이런 경우에는 일대다 관계가 아닌 다대일 관계를 사용해야 한다.

후자에 대해서는 일반적인 DB 스키마 다(N)쪽에서 외래키를 관리하는 관계형 DB 특성상 논리적으로 이루어질 수 없는 연관 관계이다. 그래서 JPA에서는 공식적으로 지원하지 않는다.

다대다(N:M) 관계

다대다 관계(@ManyToMany) 같은 경우는 양쪽 모두 @JoinColumn 을 사용하기 때문에 각 엔티티에 외래키가 존재하기 때문에 엔티티를 다루는 코드가 그만큼 복잡하고 불편해진다. 때문에 실무에서 잘 사용되지 않는 어노테이션이고 이러한 문제를 해결하기 위해 다대일, 일대다 관계로 풀어 중간 테이블을 만들어서 사용하는 것이 좋습니다. 중간 테이블을 만드는 방법은 @JoinColumn을 생략하면 된다.

요약

  1. 일대일 양방향 관계에서는 쓸데 없는 join이 없도록 mappedBy라는 속성을 사용하여 관계의 주인에만 @JoinColumn 추가해 외래키를 부여하자.
  2. 기본은 단방향, 필요에 의해 양방향 고려하자.
  3. 웬만한건 모두 다대일로 쓴다.
  4. 일대다 단방향은 비효율적인 update쿼리가 발생해 굳이 사용할 필요가 없고 다대일 관계 사용하자. 일대다 양방향은 없다.
  5. 다대다도 웬만해선 쓰지 않도록 하고, 써야 한다면 다대일-일대다로 중간 엔티티를 만들어서 커버하자.

위 내용 별개로 팀원 중 한분이 연관관계 설정으로 인해 엔티티 클래스에 추가되는 필드가 클래스의 성격에 맞지 않는것 같다 라는 의문을 제기하셨고 그에 대해 생각해 보았다. 엔티티 클래스는 데이터베이스의 스키마를 표현하기 위한 특별한 목적을 가진 클래스이기 때문에 일반적인 클래스를 바라보는 시각으로 보는것은 약간 무리가 있다. 이 목적을 만족시키기 위해 불가피하게 추가되는 연관관계 필드들은 JPA가 그렇게 사용하라고 설계되어 있고 그렇게 사용해야하기 때문에 어쩔 수 없이 용인을 해야 한다.

다만, 엔티티의 컬럼 필드에 대해서는 약간 이야기가 다른데, 컬럼 같은 경우에는 JPA와 무관하게 데이터베이스 테이블에 속한 속성이다. 일반적으로 우리가 테이블의 컬럼을 정할 땐, 클래스의 필드를 정할 때와 동일한 맥락의 사고방식으로 설계하곤 한다. 이 말은 “엔티티의 컬럼 필드에 대해서는 클래스의 성격과 맞지 않다” 라는 말을 할 수 있다는 것이고 만약, 이 말을 하게 되는 상황이라면 테이블 설계가 잘못되었다는 뜻으로 생각할 수 있다.

DevThink 카테고리 내 다른 글 보러가기

댓글남기기