본문 바로가기

JPA - One To Many 단방향의 문제점

주변에서 One To Many 단방향에 관해서 물어볼 때마다 저는 항상 이렇게 대답했습니다.

김영한 님의 인프런 강의에서 봤는데~ One To Many 단방향은 좋지 않다. 차라리 양방향을 해라. 이유는

~

때문이다..

이렇게 대답했습니다. 하지만 직접 문제를 겪어 본 적이 없어서 말에 설득력이 부족했습니다.
그래서 직접 한 번 실험해보고 문제점을 정리했습니다.

먼저 김영한님은 일대다 단방향 매핑은 이러한 단점이 있다고 하셨습니다.

  • 엔티티가 관리하는 외래 키가 다른 테이블에 있음 (Many에 외래키 존재)
  • 연관관계 관리를 위해 추가로 update sql 실행 (성능상 큰 차이는 없다)
  • 개발을 하다 보면 B를 만졌는데 A도 update sql문이 나가니 헷갈린다.
  • 그래서 필요하다면 일대다 보다는 양방향 관계로 한다. ( B는 A가 필요 없더라도, 객체 지향적으로 손해를 보는 거 같지만) - 트레이드 오프

@JoinTable을 사용한 단방향 @OneToMany

Article과 Image 엔티티

    @Entity
    public class Article {

        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;

        @Column
        private String content;

        @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) 
        private List<Image> images = new ArrayList<>();

        public void addImage(final Image image) {
            images.add(image);
        }
    }
    @Entity
    public class Image {

        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;

        @Column
        private String url;

    }

@OneToMany 단방향에서 따로 조인 설정을 넣어주지 않으면 단방향 @JoinTable이 적용됩니다.

Article과 Image를 저장하는 코드

    Article article = new Article("foo");

    article.addImage(new Image("foo 1"));
    article.addImage(new Image("foo 2"));
    article.addImage(new Image("foo 3"));
    article.addImage(new Image("foo 4"));

    articleRepository.saveAndFlush(article);

저장 sql

    Hibernate: insert into article (id, content) values (null, ?)

    Hibernate: insert into image (id, url) values (null, ?)
    Hibernate: insert into image (id, url) values (null, ?)
    Hibernate: insert into image (id, url) values (null, ?)
    Hibernate: insert into image (id, url) values (null, ?)

    Hibernate: insert into article_images (article_id, images_id) values (?, ?)
    Hibernate: insert into article_images (article_id, images_id) values (?, ?)
    Hibernate: insert into article_images (article_id, images_id) values (?, ?)
    Hibernate: insert into article_images (article_id, images_id) values (?, ?)

OneToMany에서 JoinTable을 사용하면 Article과 Image를 저장한 후에 매핑테이블에 한 번 더 저장해줍니다. 하나의 외래키가 아닌 두 개의 외래키가 저장되는데요. 이 경우 1:N 관계 보다는 N:N 연관 처럼 보이며 매우 효율적이지 않습니다. 또 한 세 개의 테이블이 사용되므로 필요한 것보다 더 많은 공간을 사용하고 있습니다.

삭제

Image 엔티티를 삭제 하려면 어떻게 해야할까요?
ImageRepository.delete() 를 통해서 삭제해보겠습니다.

코드

    Image image = imageRepository.findById(1L).get();
    imageRepository.delete(image);

삭제 결과

    PUBLIC.ARTICLE_IMAGES FOREIGN KEY(IMAGES_ID) REFERENCES PUBLIC.IMAGE(ID) (1)"; SQL statement:
    delete from image where id=? [23503-199]

이런 에러가 납니다. article_images 테이블에서 Image의 id를 외래키로 가지고 있기 때문에 제거가 불가능합니다.
Image를 삭제하는 방법은 Article의 Image List에서 remove 해줘야 합니다.

    Image image = imageRepository.findById(1L).get();
    article.getImages().remove(image);
    testEntityManager.flush();

삭제 sql

    Hibernate: delete from article_images where article_id=?
    Hibernate: insert into article_images (article_id, images_id) values (?, ?)
    Hibernate: insert into article_images (article_id, images_id) values (?, ?)
    Hibernate: insert into article_images (article_id, images_id) values (?, ?)
    Hibernate: delete from image where id=?

생각대로라면 article_images에서 1번, image에서 1번 이렇게 총 2번 삭제될 줄 알았는데 결과는 의외였습니다.

  1. article_images 테이블 에서 article_id를 통해 모두 지운다.
  2. 지우려는 image를 제외한 나머지 image들을 article_images에 다시 저장한다.
  3. 지우려는 image를 테이블에서 삭제한다.

자세한 이유는 JPA-일대다-단방향-매핑-잘못-사용하면-벌어지는-일


@JoinColumn을 사용한 단방향 @OneToMany

public class Article{
    ....

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name="article_id")
    private List<Image> images = new ArrayList<>();

    ....

저장 sql

    Hibernate: insert into article (id, content) values (null, ?)

    Hibernate: insert into image (id, url) values (null, ?)
    Hibernate: insert into image (id, url) values (null, ?)
    Hibernate: insert into image (id, url) values (null, ?)
    Hibernate: insert into image (id, url) values (null, ?)

    Hibernate: update image set article_id=? where id=?
    Hibernate: update image set article_id=? where id=?
    Hibernate: update image set article_id=? where id=?
    Hibernate: update image set article_id=? where id=?

@JoinColumn을 사용하면 image를 DB에 저장할 때, article_id를 모르기 때문에 먼저 저장한 후에 update문을 통해서 article_id를 한 번 더 실행합니다.

삭제 sql

    Hibernate: update image set article_id=null where article_id=? and id=?
    Hibernate: delete from image where id=?

OneToMany 양방향

그렇다면 양방향은 어떨까요?

Article, Image

@Entity
public class Article{

    @OneToMany(mappedBy = "article",cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Image> images = new ArrayList<>();

    public void addImage(final Image image) {
        images.add(image);
        image.setArticle(this);
    }

    public void removeImage(final Image image){
        images.remove(image);
        image.setArticle(null);
    }
}

@Entity
public class Image {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "article_id")
    private Article article;

      ...
}

저장 sql

Hibernate: insert into article (id, content) values (null, ?)
Hibernate: insert into image (id, article_id, url) values (null, ?, ?)
Hibernate: insert into image (id, article_id, url) values (null, ?, ?)
Hibernate: insert into image (id, article_id, url) values (null, ?, ?)
Hibernate: insert into image (id, article_id, url) values (null, ?, ?)

삭제 sql

Hibernate: delete from image where id=?

딱 한 번씩, 간단하게 실행됩니다.
양방향을 하면 이렇게 편하고 사용함에 있어서도 편한데 왜 양방향을 사용하지 않고 @OneToMany 단방향을 생각했을까요?
객체는 가급적이면 단방향으로 해주는 게 좋습니다. 양방향으로 하면 신경써줘야 할 부분이 많죠.

의존성? A가 변경될 때 B도 함께 변경될 수 있다.
즉, 양방향은 관리가 어렵고 논리적으로 서로가 계속 변경합니다.
(A 변경 -> B 변경 -> A 변경...)

이런 문제가 있다 보니 @OneToMany 단방향을 생각했는데, 어느 날 양방향에 대해서 질문을 했다가 좋은 대답을 들었습니다.
제가 양방향은 위험하지 않느냐? 사용을 자제하는 게 좋지 않을까요? 라고 물었습니다.
'위험한 거'랑 '하면 안 되는 것'은 다르다. 자동차 타는 것은 위험하지 않느냐? 그러면 타면 안 되냐? 사용에 주의를 하라는 거다.라는 좋은 대답을 들었습니다.

정리하자면,

저장할 때

삭제할 때