내가 JPA 연관 관계 편의 메서드를 작성했던 이유

Kevin·2023년 5월 20일
2

반성합니다.

목록 보기
1/9
post-thumbnail

그 때 당시에는 Spring은 물론 Java에 대한 개념도 매우 약할 때라 지금도 약하지만

레퍼런스의 코드를 가져다 사용하기에 급급했다.

당시의 나는 해당 개념에 대한 정확한 이해보다는 기능 구현이 우선이 되었기에, 내가 코드에 적용했던 다른 코드나 기술들에 대해 정확히 왜 사용하고, 어떤 기능인지에 대한 것들은 모두 미뤄뒀었다.

그래서 지금부터 내가 잘 모르고 작성했던 코드들에 대한 공부를 차근히 진행하고자 한다.

나는 아직도 너무 무지하고, 개념을 확실히 알고 넘어 가야 한다는 습관이 완벽히 자리 잡지 않았기에

이 시리즈는 한동안 쭉 작성하지 않을까 싶다.

이 시리즈의 첫번째 글의 주제는 연관 관계 편의 메서드를 왜 사용했을까라는 주제이다.

나는 JPA를 사용하여서 엔티티간 ManyToOne 양방향 관계를 맺을 때 연관 관계 편의 메서드를 사용해야만 한다고만 이해를 하고 있었다.

정확히 왜 연관 관계 편의 메서드를 작성해야 하는지에 대해서는 이해하지 못하고 있었다.

이제부터 왜 연관 관계 편의 메서드를 작성 해야 하는지 알아보자.

설명은 내가 프로젝트 했던 실제 코드들을 바탕으로 해나갈 것이다.


먼저 양방향 관계에서의 연관 관계에 대해서 알아보자

먼저 Post(게시물)과 User(유저)는 N:1, @ManyToOne 관계를 가지고 있다.

Post의 코드이다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 생성자를 통해서 값 변경 목적으로 접근하는 메시지들 차단
@EntityListeners(AuditingEntityListener.class) 
@Entity
public class Post extends Date {

    @Id
    @Column(name = "post_id") /
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
    private Long id; 

    private String title; 

    private String content;

    private String hashTag; 
    
    @ManyToOne(fetch = FetchType.LAZY) 
    @JoinColumn(name = "user_id") 
    private User user;
}
  • Post는 이 관계에서 주인이다. → 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
  • 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고, 외래키를 관리(등록, 수정, 삭제)할 수 있다.

User의 코드이다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User{
-==
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long id;
    private String password;
    private String email;
    private String name;
    private LocalDate birth;

		@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.REMOVE)
    private List<Post> postList = new ArrayList<>();
}
  • User는 이 관계에서 주인이 아니다. → 주인이 아닌 쪽은 읽기만 할 수 있다.
  • 주인이 아니면 mappedBy를 작성해주고, 반대쪽 매핑의 필드 이름을 인자 값으로 주면 된다.
  • 주인이 아닌 곳의 입력된 값은 외래키에 영향을 주지 않는다.
    user.getPostList().add(user1);        //무시
    user.getPostList().add(user2);        //무시
    
    post1.setUser(user1);                 //연관관계 설정(연관관계의 주인)
    post2.setUser(user2);                 //연관관계 설정(연관관계의 주인)


그러면 주인에서만 값을 저장하면 될까?

답은 아니다. 위와 같이 주인에서만 post1.setUser(user1); 코드와 같이 주인에서만 값을 저장하면 객체 지향적이지 않고, 안전하지 않다. → ORM은 반드시 객체와 데이터베이드 둘 다 모두를 함께 고려해야한다.

양쪽 모두 값을 입력하지 않으면, JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 생길 수 있다.

아래는 JPA를 사용하지 않고, 순순한 객체 상태의 엔티티에 대한 테스트 코드이다.

void test_save(){
		User user = new User();
		user.setId(1L);
		user.setName("케빈");

		Post post = new Post();
		post.setId(1L);
		post.setUser(user);

		List<Post> postList = user.getPostList();
		System.out.println("postList.size() = " + postList.size());
	}
  • 위 결과로 postList.size() = 0이 출력된다.
  • 코드를 보면 Post.User에만 연관 관계를 설정하고, 반대 방향은 연관 관계를 설정하지 않았다.
  • 우리가 예상하는 결과로는 Post에 User의 연관 관계를 설정함으로써 User의 postList의 size 또한 1이 출력되어야 한다.

양방향은 위와 같이 한쪽만 관계를 설정하는 것이 아니라. 이 처럼 게시물 → 유저를 설정하면, 유저→게시물 또한 설정을 해줘야 한다.

아래는 위 코드를 양방향에 맞게 수정한 코드이다.

void test_save(){
		User user = new User();
		user.setId(1L);
		user.setName("케빈");

		Post post = new Post();
		post.setId(1L);
		post.setUser(user); // post -> user 연관 관계 설정 
		user.getPostList().add(post); // user -> post 연관 관계 설정 

		List<Post> postList = user.getPostList(); 
		System.out.println("postList.size() = " + postList.size());
	}
  • 양쪽에 연관 관계를 설정했다. → 이제 순수한 객체 상태에서도 동작하며, 테이블의 외래 키 또한 정상적으로 입력된다.
    • Post.user = 연관 관계의 주인, 이 값으로 외래키를 관리한다.
    • User.postLists = 연관관계의 주인이 아니다. 따라서 저장 시 에 사용되지 않는다.
      • 이 말은 JPA에서 외래키 저장시 사용되지 않는다는 말이다.

💡 데이터베이스 뿐만 아니라 객체까지 고려해서 주인이 아닌 곳에도 값을 입력해주자.
객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자.

이제 위의 양방향 연관 관계를 편하게 맺게 해주는

연관 관계 편의 메서드를 알아보자.

post.setUser(user); // post -> user 연관 관계 설정 
user.getPostList().add(post); // user -> post 연관 관계 설정 

양방향 관계에서 위 코드는 하나인 것 처럼 사용하는 것이 잊지 않고, 안전하다.

이 때 연관 관계 편의 메서드를 사용하는데, 보통 자주 쓰이는 엔티티에 메서드를 정의하는 것이 일반적이다.

보통 Post에서 User를 지정하는 편이 많기에, Post에 연관 관계 편의 메서드를 작성해보자.

public void setUser(User user){
        this.user = user;
        user.getPostList().add(this);
    }
  • 이렇게 한 번에 양방향 관계를 설정하는 메서드를 연관관계 편의 메서드라 한다.

그런데 위 setUser 메서드에는 버그가 존재한다.

만약 2개 이상의 post가 있으면 어떤 경우가 발생할까?

void test_save(){
		User user = new User();
		user.setId(1L);
		user.setName("케빈");

		Post post1 = new Post();
		post1.setId(1L);
		post1.setUser(user); // 양방향 연관 관계

		Post post2 = new Post();
		post2.setId(2L);
		post2.setUser(user); // 양방향 연관 관계

		List<Post> postList = user.getPostList(); 
		System.out.println("postList.size() = " + postList.size());
	}
  • 위의 상황에서 생기는 문제는 아래와 같다.
    • 먼저 post1.setUser(user);의 코드에서 post1과 user는 양방향 관계를 맺는다.

    • 그다음 post2.setUser(user);의 코드에서 post2와 user가 양방향 관계를 맺을 때 기존 post1과 user의 관계가 남아있다는 문제이다.

    • 그래서 post1과 user의 관계를 먼저 끊어주고, post2와 user의 관계를 추가해야한다.

      public void setUser(User user){
              // 기존 user와의 관계를 제거
              if (this.user != null) {
                  this.user.getPostList().remove(this);
              }
              this.user = user;
              user.getPostList().add(this);
          }


내용을 정리하면 다음과 같다.
  • 단방향 매핑만으로도 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
  • 단방향을 양방향으로 만들면 주인의 반대방향으로 객체 그래프 탐색 기능이 추가되는 것 뿐이다.
    • 주인의 반대방향은 여전히 읽기만 가능하다.

      💡 기존 단방향은 아래 코드만 가능하지만 post.getUser()

      양방향을 이용하면 아래 코드 또한 가능하다.
      user.getPostList();

  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.



내가 연관 관계 편의 메서드를 작성했던 이유

답은 바로 객체까지 고려해서 주인이 아닌 곳에도 값을 넣어줘야 하기 때문이었다!!

profile
Hello, World! \n

2개의 댓글

comment-user-thumbnail
2024년 2월 12일

해당 메소드는 실제 Service부분에서는 어떤식으로 쓰이는지 알 수 있을까요?

1개의 답글