Home 연관관계 적용
Post
Cancel

연관관계 적용

연관관계 매핑(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로 들어오면 원시 자료형은 기본 값이 들어와버린다. (ex. int = 0)
그렇게 되면 이게 null인지 모르므로 Wrapper 클래스인 Integer로 설정하게 되면
null 값으로 떠서 에러가 뜨므로 이러한 대비를 하는게 좋다.

  • mysql에서 long은 bigint로 설정하면 된다.


GenerationType 수정
auto_increment를 적용하기 위해 자연스럽게 계속 AUTO만 사용하고 있었는데
AUTO를 생성하면서 문제가 생겼고 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
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@Entity
@ToString
public class Registry extends Timestamped {
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    @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
@Setter
@Getter
@NoArgsConstructor
@Entity
public class Comment extends Timestamped {
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    @Column(name = "COMMENT_ID")
    private Long idx;

    @Column(nullable = false)
    private String nickname;

    @Column(nullable = false)
    private String comment;

    @Column(nullable = false)
    private Long registryIdd;

    @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가 주인이므로 매핑종류와 @JoinColumn을 작성해준다.

@JoinColumn의 name은 pk를 fk로 설정해서 저장해둘 이름을 작성한다.

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


fetch : 연관관계가 있는 도메인 로딩 전략 설정 옵션
  • EAGER[이거 로딩] : 즉시 로딩
  • LAZY[레이지 로딩] : 지연 로딩
    • ex) @ManyToOne(fetch = FetchType.LAZY)

Comment를 불러올 때 연관관계인 Registry도 같이 즉시 조회하고 싶으면 EAGER를 사용하고 LAZY는 필요로 할 때만 쓰인다.


default값

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

🐣 EAGER를 사용해 많은 데이터가 로딩되면 부하가 심해서 실무에서는 LAZY로 사용하는 것을 추천한다.

🐣 연관관계 맺은것이 많이 써먹는 메소드가 많다면 EAGER를 사용하고 연관관계 맺은 것이 써먹는 게 없으면 LAZY를 사용한다.


ex) User와 Registry의 1:N 관계에서 EAGERLAZY 쿼리문 차이

image image






















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

자바에서 객체를 불러올 때는 가능하다.



주인이 아닌 클래스(Registry.java)에는 mappedBy 속성으로 주인을 지정한다.

일대다 혹은 다대일 매핑에서 일인 클래스에 다인 클래스를 컬렉션으로 적어준다.

또한 일대다 컬렉션 타입 필드는 “초기화” 해줘야 한다.

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

mappedBy에서는 반대쪽 필드 명을 적는 것이다.

Comment.java의 private Registry registry;에서 registry를 적은 것이다.




초기화

1
2
3
4
5
하이버네이트는 엔티티를 영속 상태로 만들 때, 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 사용한다.

컬렉션을 효율적으로 관리하기 위해서이며, 이런 특징 때문에 컬렉션을 사용할 때 다음처럼 즉시 초기화해서 사용하는 것을 권장한다.

초기화를 시키지 않으면 NullPointerException이 발생한다.



Collection

Collection은 List, Set, Map이 있다.

  • Collection : 자바가 제공하는 최상위 컬렉션이다.
  • List : 순서가 있고, 중복을 허용한다.
  • Set : 순서가 없고, 중복은 허용하지 않는다
  • Map : Key, Value로 되어있으며 키는 중복을 불허한다.




양방향 매핑시 조심해야 할 것들

  • Ex: toString(), lombk , JSON 생성 라이브러리


@ToString

Comment를 문자열로 표현하기 위해 그 안에 Registry의 toString 을 실행한다.

Registry안에 Comment가 있기때문에 Comment의 toString 실행 → Comment안에 Registry가 있기때문에 Profile의 toString 실행….

이러한 무한 루프에 빠지게 되는데 이 무한 루프를 순환참조라고 한다.

따라서, 양방향 매핑 시 @ToString에서 필드를 제외 시켜줘야한다. → @ToString.Exclude


@RestController의 경우 @ResponseBody로 객체를 반환해 줄 때 JSON 형태로 변환해주는데

이때도 마찬가지로 매핑된 필드를 제외해줘야한다. → @JsonIgnore




매핑 적용하기

CommentRegistry를 가져올수는 있는데 값을 설정해주지 않아서 가져오려면 로직 작성이 필요하다.


연관관계를 맺을 때 Registry에서는 Comment가, Comment에는 Registry 로직이 들어가야 하는데

setter 어노테이션이 해줄 수 있지만 기본 setter만 해주기 때문에 연관관계 매핑 로직이 포함 되지 않아서 작성해줘야 한다.

메소드명은 상관없지만 setter 로직이니깐 setRegistry로 작성했다.


Comment.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@ManyToOne // 주이이인
@JoinColumn(name = "registry_id")
private Registry registry;
    
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
8
9
10
11
12
@OneToMany(mappedBy = "registry")
@JsonIgnore
private List<Comment> comments = new ArrayList<>();

public void addComment(Comment comment) {
    this.comments.add(comment);

    // 무한 루프 안걸리게 하기
    if(comment.getRegistry() != this) {
        comment.setRegistry(this);
    }
}

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




🐣 기본 setter와 getter

1
2
3
4
5
6
7
8
9
10
11
private 타입 fieldName

// Getter
public 리턴타입 getFieldName() {
    return fieldName;
}

// Setter
public void setFieldName(타입 fieldName) {
    this.fieldName = fieldName;
}



MySQL 같은 RDBMS에 여러 개 데이터가 들어갈 수 없어서 N이 주인이 될 수밖에 없다.

같은 이유로 여러 개 데이터가 들어갈 수 없기 때문에 add와 set 메소드 작성이 필요하다.


만약 add와 set 메소드를 작성안했다면 값을 설정할 수 없다.

ORM이 db 레코드를 객체로 바꿔주거나 객체를 레코드로 바꾸는 과정에서

add와 set과 같이 값을 지정해주는 메소드가 없다면 여러 개의 데이터가 들어갈 수 없기 때문에 null로 저장된다.

여러 개 데이터 저장 형식에는 List, Set, Map 등이 있다.




코드 작성하기

실제 비즈니스 로직에 연관관계 매핑 코드를 작성한다.

1
2
3
4
5
6
   @Transactional
   public Comment setComment(CommentDto commentDto) {
      Comment comment = new Comment(commentDto);
      commentRepository.save(comment);
      return comment;
   }


1
2
3
4
5
6
7
8
9
10
11
   @Transactional
   public Comment setComment(CommentDto commentDto) {
      Comment comment = new Comment(commentDto);
    
      // 연관관계 매핑
      Registry registry = registryRepository.findById(comment.getRegistry().getIdx()).get();
      comment.setRegistry(registry);
    
      commentRepository.save(comment);
      return comment;
   }




test 코드 작성하기

*참고로 객체 GenerationType이 AUTO이면 아래 test가 에러가 날 것이다.(IDENTITY로 수정한다.)
자세한 내용은 아래 글을 참고한다.
Generationtype


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
32
33
34
35
36
@DisplayName("H2를 이용한 TEST")
@DataJpaTest
public class CommentandRegistryTest {
    @Autowired
    RegistryRepository registryRepository;

    @Autowired
    CommentRepository commentRepository;


    @Test
    void commentSave_Identity() {

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

        registryRepository.save(registry);

        Comment comment = new Comment();
        comment.setComment("❤️🧡💛💚💙💜🤎🖤");
        comment.setNickname("우헤헤");
        comment.setRegistryId(5L);
        comment.setRegistryNickname("pop");

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

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

        Assertions.assertThat("coco").isEqualTo(savedRegistry.getNickname());
        Assertions.assertThat("❤️🧡💛💚💙💜🤎🖤").isEqualTo(savedComment.getComment());
    }
}




참고

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

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

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



super와 부모생성자
class가 인스턴스화 될때 생성자가 실행되면서 객체의 초기화를 한다.
그 때 자신의 생성자만 실행이 되는것이 아니고, 부모의 생성자부터 실행된다.

1
2
3
4
5
6
7
8
9
10
11
12
    public class Car{
        public Car(){
            System.out.println("Car의 기본생성자입니다.");
        }
    }

    public class Bus extends Car{
        public Bus(){
            System.out.println("Bus의 기본생성자입니다.");
        }

    }
1
2
3
4
5
    public class BusExam{
        public static void main(String args[]){
            Bus b = new Bus();
        }
    }
  • new 연산자로 Bus객체를 생성하면, Bus객체가 메모리에 올라갈때 부모인 Car도 함께 메모리에 올라간다.
  • 생성자는 객체를 초기화 하는 일을한다.
  • 생성자가 호출될 때 자동으로 부모의 생성자가 호출되면서 부모객체를 초기화 하게된다.



super 자신을 가리키는 키워드가 this 라면, 부모들 가리키는 키워드는 super
super() 는 부모의 생성자를 의미한다.
부모의 생성자를 임의로 호출하지 않으면, 부모 class의 기본 생성자가 자동으로 호출된다.
아래처럼 호출해보면 위에서 super()를 호출하지 않을 때와 결과가 같다.

1
2
3
4
    public Bus(){
        super();
        System.out.println("Bus의 기본생성자입니다.");
    }
  • super 키워드는 자식에서 부모의 메소드나 필드를 사용할 때도 사용한다.
  • 부모 클래스의 생성자는 한 번만 호출할 수 있다.




출처 super와 부모생성자

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