@OneToMany / @ManyToOne 양방향 지양

춤인형의 개발일지·2025년 3월 9일

이것저것

목록 보기
13/13

ManyToOne vs OneToMany

JPA에서 @ManyToOne과 @OneToMany는 서로 연관된 개념이다. @ManyToOne을 사용하면 당연히 반대쪽에는 @OneToMany가 있을 것 같지만, 실제로는 반드시 그런건 아니라고 한다. 보통 언제 하냐면 아래와 같다.

단방향

1. ManyToOne (N:1 관계)

관계가 필요하다 하면 "@ManyToOne"를 주로 사용한다. 필요시에는 항상 사용한다고 생각하면 된다.
예를 들어, 게시글과 작성자(User) 관계를 생각해보면
하나의 사용자(User)가 여러 개의 게시글(Post)이 속할 수 있기 때문에 유저:게시글 = N:1 관계이다.
즉, Post 엔티티에는 User에 대한 참조가 필요하므로 @ManyToOne을 사용해야한다.
외래키는 N쪽에 생기기 때문에 항상 붙여준다고 생각하면 된다.

@Entity
public class Post {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id")  // 외래키
    private User user;
}

2. OneToMany (1:N 관계)

@OneToMany를 사용하는 경우는 보통 양방향 참조일 때인데 이는 부모가 자식을 "읽기" 위한 수단이 필요할 때만 사용한다.
이게 뭔 말이냐면 User 엔티티에서 자신이 작성한 모든 Post 목록을 가져와야 한다면 @OneToMany가 필요하다.

OneToMany는 단방향으로는 거의 사용하지 않는 비효율적인 방식이다. 1(하나)의 부모가 N(여러 개)의 자식을 가리키는 관계인데 외래 키가 자식 테이블이 아니라, 부모 테이블에서 관리되기 때문에 (JPA는 이를 해결하기 위해 중간 테이블을 생성함) OneToMany만 매핑하는 경우는 극히 드물다.

중간테이블이란 한명의 유저가 여러 포스터를 남길 수 있기 때문에 OneToMany를 달아주면, JPA는 외래 키를 post 테이블에 저장할 수 없으므로 중간 테이블을 자동 생성한다.

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;

    @OneToMany  // 일(1) → 다(N) 관계
    @JoinColumn(name = "user_id")  // 외래 키를 User 테이블에서 관리
    private List<Post> posts = new ArrayList<>();
}

이런 엔티티를 가지고 아래와 같이 중간 테이블을 만들게 된다.

CREATE TABLE user_post (
    user_id BIGINT,
    post_id BIGINT,
    FOREIGN KEY (user_id) REFERENCES user(id),
    FOREIGN KEY (post_id) REFERENCES post(id)
);

양방향 참조 (ManyToOne + OneToMany)

OneToMany가 있는 경우는 양방향 참조로 이루어질 때 보통 사용한다.
예를 들어 User에서 자신의 Post 목록을 항상! 조회해야 할 때 OneToMany를 사용한다.

양방향 참조는
연관된 엔티티를 양쪽에서 모두 조회할 수 있다. 즉, Post에서 User를 찾을 수도 있고, User에서 자신의 Post 목록을 가져올 수도 있습니다. 양방향 관계로 직접참조를 통해 연관관계 객체(즉 내가 찾고자 하는 관계들을)를 바로 찾을 수 있게 되고, Join Query없이 Join이 가능하며, 유지보수에 용이하다는 장점을 가지고 있다.

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

양방향 관계는 서로 매핑해준 관계를 실질적으로 추가해주는 로직이 굉장히 중요하다! 이를 연관관계 편의 메서드 라고 부른다.

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "user")
    private List<Post> posts = new ArrayList<>();

    // 연관관계 편의 메서드
    public void addPost(Post post) {
        posts.add(post);
        post.setUser(this);
    }
}

부모의 입장에서는 List를 가지고 있을 텐데 꼭! new ArrayList<>();로 초기화를 해줘야 한다!!!

@Entity
public class Post {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    public void setUser(User user) {
        this.user = user;
        if (!user.getPosts().contains(this)) {
            user.getPosts().add(this);
        }
    }
}

자식에서는 setter을 이용하여 관계에 추가할 수 있도록 도와준다.
이렇게 관계를 직접적으로 맺어줘야 양방향 관계가 직접적으로 생성되기 때문에 해줘야한다. (OneToMany / ManyToOne은 그냥 그거인거고 실질적인 효과는 추가해주는 .add가 있어야한다.)

양방향의 문제

하지만, 양방향관계는 성능상 문제가 많다

N+1 문제

@OneToMany는 기본적으로 fetch = FetchType.LAZY이므로, 연관된 엔티티를 조회할 때 추가적인 쿼리가 발생한다. 이는 부모 엔티티(User)를 조회한 후, 자식 엔티티(Post)를 N번 추가 조회하는 문제로 인한 성능 문제가 발생한다.

예를 들면 User 1명을 가져오는데 관련된 Post가 10개라면, 총 11개의 쿼리가 실행됨

  • fetch = FetchType.LAZY :연관된 엔티티를 실제로 사용할 때까지 로딩 지연(불필요한 sql쿼리문 실행x - 성능 good)

LAZY를 해결하기 위해 사용하기 전에 미리 다 로딩을 하는 코드가 나왔다. 그게 EAGER(즉시 로딩)이다. 하지만 이 코드는 항상 연관된 엔티티를 함께 조회한다. 그러면 연관된 엔티티가 많으면 많을 수록 불필요한 데이터는 많아지고, 그렇게 되면 방대한 데이터를 전부 다 로딩해야하니 성능의 문제가 생긴다.

update 추가 sql실행

아까 연관관계 편의 메서드를 사용하면서 양방향 관계를 맺게 되면 sql문으로 확인하면 추가적인 UPDATE 쿼리가 계속 발행된다. 계속 .add로 추가하기 때문에 update쿼리가 실행될 수밖에 없다.

또한 데이터를 변경할 때 연관된 모든 데이터를 다시 UPDATE 해야 해서 성능 저하가 된다.

delete 추가 sql실행

@OneToMany(cascade = CascadeType.ALL)을 설정하면 부모 엔티티를 삭제할 때 자식 엔티티도 자동 삭제된다. 이것도 불필요한 자식의 데이터들까지 계속 불러와서 삭제하니까 성능 저하가 발생한다.


이렇게 양방향으로 했을 떄 여러가지 문제가 발생하게 된다. 따라서 단방향(ManyToOne) 관계만 두고, 필요한 경우에만 별도 쿼리로 조회하는 것이 성능 면에서 더 유리할 수 있다.

예를 들어, User에서 자신이 작성한 모든 Post를 조회해야 할 때 양방향 참조 없이 Repository를 이용하여 하는 방법도 있다. 쿼리메서드를 만들어서 찾고싶은 대상을 만들어서 양방향 참조를 막고 단방향 참조를 하는 방향으로 만들어야한다.

0개의 댓글