Post

연관관계 적용

연관관계 적용

연관관계 매핑(1 : N)

실제 프로젝트에 적용시켜본다.


연관관계 목차


요약

image

1. @Column(name = "registry_id") 로 Registry id 컬럼에 이름을 명시해준다.

2. Comment 의 Registry에 @JoinColumn으로 참조할 컬럼명을 지정해 준다.

3. Comment : Registry = N : 1 관계이기 때문에

  • Comment의 Registry에는 @ManyToOne, Registry의 Comment에는 @OneToMany

4. mappedBy : 주인이 아닌 것을 지정 (양방향 시 키를 관리할 주인을 지정)

  • mappedBy="주인쪽에 자신이 매핑되어 있는 필드명"

5. fetch = FetchType.LAZY : 지연 로딩으로 필요할 때만 쿼리를 날려 조회





매핑하기

매핑할 객체는 RegistryComment다.


연관관계 매핑 전까지는 직접 값을 넣어서 DB에 저장했다.

기존 코드에서 연관관계 매핑을 하면서 수정한 부분이 몇 군데 있다.

(아래 코드는 수정 이후의 코드다.)


Wrapper class로 수정

기존에 idx를 제외한 정수 타입을 int로 사용했다.

이 경우 null 값이 들어오면 기본값(0)이 들어가면서 오류인지 모르는 문제가 발생한다.


Wrapper 클래스(Integer, Long)를 사용하면 null 그대로 유지되어 오류를 바로 확인할 수 있다.

*MySQL에서 LongBIGINT로 변경


GenerationType 수정

AUTO를 사용했으나 auto_increment 환경에서 문제가 발생해 IDENTITY로 변경했다.

자세한 내용은 Generationtype을 참고한다.





Registry.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@Entity
@ToString
public class Registry {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "registry_id")
    private Long idx;

    @Column(nullable = false)
    private String nickname;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String main;

    public Registry(RegistryDto registryDto) {
        this.title = registryDto.getTitle();
        this.main = registryDto.getMain();
        this.nickname = registryDto.getNickname();
    }
}



Comment.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Setter
@Getter
@NoArgsConstructor
@Entity
public class Comment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "comment_id")
    private Long idx;

    @Column(nullable = false)
    private String nickname;

    @Column(nullable = false)
    private String comment;

    @Column(nullable = false)
    private Long registryId;

    @Column(nullable = false)
    private String registryNickname;

    public Comment(CommentDto commentDto) {
        this.nickname = commentDto.getNickname();
        this.comment = commentDto.getComment();
        this.registryId = commentDto.getRegistryId();
        this.registryNickname = commentDto.getRegistryNickname();
    }
}



관계

Registry와 Comment는 일대다 관계다.

(게시글 하나당 여러 개의 댓글)



매핑 방향

Registry와 Comment가 서로 참조하는 양방향 매핑으로 진행한다.



주인

1 : N 관계에서는 N이 주인이다.

따라서 Comment가 주인이며 @ManyToOne@JoinColumn을 작성한다.

1
2
3
@ManyToOne // 주인
@JoinColumn(name = "registry_id")
private Registry registry;

🐣 1:N 관계에서는 N이 주인일 수 밖에 없는 이유는 RDBMS에서 여러 개의 데이터가 들어갈 수 없기 때문이다.



fetch 옵션

  • EAGER : 즉시 로딩
  • LAZY : 지연 로딩
    • @ManyToOne(fetch = FetchType.LAZY)


기본값

  • @ManyToOne, @OneToOne → EAGER
  • @OneToMany, @ManyToMany → LAZY

🐣 실무에서는 대부분 LAZY 사용을 권장



mappedBy

주인이 아닌 클래스(Registry)에 mappedBy를 지정한다.

일대다 관계에서는 컬렉션 타입 필드를 사용하며 반드시 초기화해야 한다.

1
2
3
4
// Registry
@OneToMany(mappedBy = "registry")
@JsonIgnore
private List<Comment> comments = new ArrayList<>();

mappedBy = "registry"는 Comment의 private Registry registry 필드를 의미한다.



컬렉션 초기화 이유

하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 내부 컬렉션으로 감싸서 관리한다.

초기화하지 않으면 NullPointerException이 발생할 수 있다.



양방향 매핑 시 주의사항

  • toString()
  • Lombok
  • JSON 직렬화

양방향 매핑 시 순환 참조가 발생할 수 있다.

따라서 다음 처리가 필요하다.

  • @ToString.Exclude
  • @JsonIgnore





연관관계 편의 메서드

Comment.java

1
2
3
4
5
6
7
8
9
10
public void setRegistry(Registry registry) {
    if (this.registry != null) {
        this.registry.getComments().remove(this);
    }
    this.registry = registry;

    if (!registry.getComments().contains(this)) {
        registry.addComment(this);
    }
}

registry.addComment(this); : Registry에서 addComment()를 작성해 줬기 때문에 이를 활용하여 작성한 것

만약 작성하지 않았다면 registry.getComments().add(this); 로 작성해야 할 것


Registry.java

1
2
3
4
5
6
7
public void addComment(Comment comment) {
    this.comments.add(comment);

    if (comment.getRegistry() != this) {
        comment.setRegistry(this);
    }
}

comments는 리스트 형태이므로 내장 함수인 add()를 활용해 더해준 것



비즈니스 로직 적용

1
2
3
4
5
6
7
8
9
10
11
12
@Transactional
public Comment setComment(CommentDto commentDto) {
    Comment comment = new Comment(commentDto);

    Registry registry =
        registryRepository.findById(commentDto.getRegistryId()).get();

    comment.setRegistry(registry);
    commentRepository.save(comment);

    return comment;
}



Test 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@DisplayName("H2를 이용한 TEST")
@DataJpaTest
public class CommentAndRegistryTest {

    @Autowired
    RegistryRepository registryRepository;

    @Autowired
    CommentRepository commentRepository;

    @Test
    void comment_save_test() {

        Registry registry = new Registry();
        registry.setNickname("coco");
        registry.setTitle("안녕하세요");
        registry.setMain("hi");
        registryRepository.save(registry);

        Comment comment = new Comment();
        comment.setNickname("우헤헤");
        comment.setComment("❤️🧡💛💚💙💜");
        comment.setRegistry(registry);
        commentRepository.save(comment);

        Comment savedComment = commentRepository.findById(1L).get();

        Assertions.assertThat(savedComment.getRegistry().getNickname())
                  .isEqualTo("coco");
    }
}





super 와 부모 생성자

기존에 작성했던 Registry test 코드에서 매핑한 Comment 값이 없어 에러가 떴다.

그래서 생성자 오버로딩을 통해서 기존 값이 오류가 나지 않게 했다.

1
2
3
public Registry(long idx, String nickname, String title, String main) {
    super();
}



1
2
3
4
5
public class Parent {
    public Parent() {
        System.out.println("Parent 생성자 호출");
    }
}
1
2
3
4
5
6
public class Child extends Parent {
    public Child() {
        super();
        System.out.println("Child 생성자 호출");
    }
}
1
2
3
4
5
public class Main {
    public static void main(String[] args) {
        Child child = new Child();
    }
}
  • 객체 생성 시 부모 생성자가 먼저 호출된다.
  • super()는 부모 생성자를 의미한다. (자신을 가리키는 키워드는 this)
  • 부모 생성자는 한 번만 호출된다.




reference
super와 부모생성자

This post is licensed under CC BY 4.0 by the author.