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. 양방향 관계는 객체 간 순환 참조를 유발할 가능성이 존재한다.
이러한 이유로 인해 양방향 관계를 맺는 것은 결론적으로 코드가 복잡해질 가능성이 농후해 유지보수가 어려워진다.
그래서 웬만하면 양방향 관계를 지양하고 단방향 관계를 지향하는 게 낫다.
그리고 정말 필요할 때만 양방향 관계를 설정해야한다.
'스프링부트 > JPA' 카테고리의 다른 글
| [Spring] Query DSL 문법 정리 (1) | 2025.08.26 |
|---|---|
| [Spring] JPA에서의 프록시 객체-불필요한 SELECT 쿼리 최소화 방법 (2) | 2025.08.10 |
| [Spring] MyBatis 사용법 - JPA와 차이점을 중심으로 (4) | 2025.07.30 |
| [Spring] JPA환경에서 검색 및 필터 기능 구현 2 - QueryDSL 방법 이용 (2) | 2025.06.30 |
| [Spring] JPA환경에서 검색 및 필터 기능 구현 1 - Specification 방법 이용 (1) | 2025.06.30 |