스프링부트/JPA

[Spring] JPA에서 양방향 관계를 지양해야 하는 이유

삼록이 2025. 7. 12. 01:26

DB에서 테이블 간의 관계는 FK(외래키)를 참조함으로써 맺어진다.

그리고 양 테이블 어느 쪽에서나 JOIN을 걸어 원하는 데이터를 얻을 수 있다.

DB에서는 단방향, 양방향이란 개념은 없다.

 

JPA는 ORM 언어다. DB의 테이블을 객체로 매핑시켜 관계형 데이터베이스를 객체지향형 언어로 변환해주는 기술이란 뜻이다.

그렇다 보니 DB의 테이블간의 관계를 객체간의 관계로 나타내야하는데 DB에서는 이 관계를 외래키를 통해 표현되지만,

자바 객체에서는 '객체 간의 참조 관계'로 나타낸다.

User(유저)와 Post(게시글) 두 클래스가 있을 때, 포스트 클래스에 User타입의 writer라는 필드를 두면 포스트->유저 단방향 연관관계가 맺어진다.

이 때, 유저 클래스에 List<Post> 타입의 postList 필드를 두면 유저->포스트 연관관계도 생겨, 양방향 관계가 된다.

이처럼, JPA에서는 객체간의 참조를 통해 관계를 나타내기 때문에 단방향,양방향이라는 개념이 존재한다. 

 


 

그래서 유저와 포스트간의 관계를 설정 할 때, 나는 당연히 둘 간의 관계를 양방향으로 설정해왔다.

현재 내가 작업 중인 코드인데 이것도 마찬가지로 게시글, 댓글, 좋아요를 당연하게 양방향으로 설정했다.

public class User extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    //로그인 아이디(임시)
    private String loginId;
    //비밀번호(임시)
    private String password;
    //이름
    @NotBlank
    private String name;
    //생년월일
    @NotBlank
    @Size(min=8)
    private String birthday;
    //회사이름
    private String companyName;
    //직책
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "role_id")
    private JobRole jobRole;
    //내가 쓴 게시글
    @OneToMany(mappedBy = "user")
    @Builder.Default
    private List<Post> posts = new ArrayList<>();
    //내가 쓴 댓글
    @OneToMany(mappedBy = "user")
    @Builder.Default
    private List<Comment> comments = new ArrayList<>();
    //좋아요 한 목록
    @OneToMany(mappedBy = "user")
    @Builder.Default
    private List<Likes> likes = new ArrayList<>();
    //유저역할
    @OneToMany(mappedBy = "user")
    @Builder.Default
    private List<UserRoleMap> roles = new ArrayList<>();
    //탈퇴여부
    @Builder.Default
    @Enumerated(EnumType.STRING)
    private DelYN delYN = DelYN.N;
}
public class Post extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    //게시판 카테고리
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id" )
    private PostCategory postCategory;
    //도메인 업계(필터링 걸 기준)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "field_id")
    private Field field;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
    // 글 본문
    private String content;
    //첨부사진
    @OneToMany(mappedBy = "post")
    @Builder.Default
    private List<Attachment> pictures = new ArrayList<>();
    //댓글
    @OneToMany(mappedBy = "post")
    @Builder.Default
    private List<Comment> comments = new ArrayList<>();
    //글 삭제여부
    @Builder.Default
    @Enumerated(EnumType.STRING)
    private DelYN delYN = DelYN.N;
}

 

유저와 포스트만 보았을 때, 유저쪽에서도 포스트를 참조하고 /  포스트에서도 유저를 참조하는 것을 알 수 있다.

이렇게 한 이유는 마이페이지에서 내가 쓴 게시글 기능을 만들려고 하는데 그때 편하게 user.getPosts()를 하면 되니까 말이다.

하지만 생각해보면 내가 쓴 게시글 기능은 postRepository 에서 user칼럼을 기준으로 조회하면 된다.

그럼 이럴 땐 굳이 양방향 관계를 할 필요 없이 Post에서 User쪽에만 참조하는 단방향 관계를 가지면 되지 않을까?

 

실제로 JPA에서 객체 간 관계는 모두 단방향으로 먼저 설계하는 것이 권장된다고 한다. 그리고 정말 필요할 때만 양방향 연관관계를

맺으면 된다고 하는데 그 이유는 뭘까?


1. 양방향 관계를 맺으면 연관관계의 주인/비주인 개념이 생기는데 이게 은근 혼란을 유발한다.

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

이건 User쪽 클래스다. 양방향 관계를 맺을 때 mappedBy로 User와 Post 연관관계의 주인은 Post의 user칼럼에 있다고 알려주는 어노테이션이다. 즉 실질적으로 db에서 두 관계간의 외래키는 post테이블이 가지고 있다고 알려주는 것이다. 

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

여긴 Post 클래스.

 

그러면 아래와 같이 post 객체를 만들어 DB에 저장한다고 할 때.

Post post = Post.builder.user(user).build(); 
postRepository.save(post)

이때 DB는 양방향, 단방향 관계가 없으므로 문제 없이 잘 저장되는데.

사실 지금 코드는 문제가 있다. user.posts.add(post); 이 코드를 빼먹은 것이다.

(*mappedBy는 DB에 어떠한 영향도 주지않기 때문에 저 코드를 수동으로 항상 추가해야하는 것인데 빼먹은 것 )

자바에서는 양방향으로 설정했는데 지금은 post객체에서 user만 참조한 단방향으로 코드가 진행된 것이다.

이 때 DB는 문제가 없는데 객체의 데이터는 문제가 있다. 즉, DB와 객체 간 데이터의 정합성이 깨졌다.

2. List 컬렉션은 항상 전체를 가지고 온다. 페이징 /정렬 /필터링이 불가하다.

마이페이지에서 내가 쓴  글을 조회하려고 할 때 그냥 편하게 user.getPosts()해서 List를 가지고 오고 그걸 프론트에 리턴하려한다는 것 자체가 사실은 비효율적이다.

예를 들어, 어떤 한 유저가  100,000개의 게시글을 썼다치자. 그리고 user.getPosts()를 하면 내부적으로 SELECT * 

FROM post WHERE user_id =1; 쿼리를 날리고 리스트로 그 100,000개의 게시글이 전부 메모리에 적재한다. 메모리에 부하가 발생하는 것이다.

그냥 postRepository.~을 사용해서 쿼리를 날리더라도 페이징을 사용함으로써 메모리 부하를 막을 수 있다.

 

3. 양방향 관계는 객체 간 순환 참조를 유발할 가능성이 존재한다.


이러한 이유로 인해 양방향 관계를 맺는 것은 결론적으로 코드가 복잡해질 가능성이 농후해 유지보수가 어려워진다.

그래서 웬만하면 양방향 관계를 지양하고 단방향 관계를 지향하는 게 낫다.

그리고 정말 필요할 때만 양방향 관계를 설정해야한다.