Home 연관관계 기본
Post
Cancel

연관관계 기본

연관관계 기본

  • Registry(게시글)와 Comment(댓글)가 있다.



객체를 테이블에 맞춰 모델링

image


객체를 테이블에 맞춰 모델링할 경우 참조 대신에 외래 키를 그대로 사용하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Comment.java

@Entity
public class Comment {
    @Id @GeneratedValue
    @Column(name = "COMMENT_ID")
    private Long id;

    private String comment;

    @Column(name = "REGISTRY_ID")
    private Long registryId;
}
1
2
3
4
5
6
7
// Main.java

Comment findComment = em.find(Comment.class, comment.getId());

// Comment가 어느 게시글에서 작성된 것인지 알고 싶어서 조회함
Long findRegistryId = findComment.getRegistryId();;
Team findRegistry = em.find(Registry.class, findRegistryId);


이 경우 식별자로 다시 조회

→ 객체지향 X





단방향 연관관계

image

registry의 id가 아니라 참조 값을 그대로 가져왔다.



image

위와 같이 적으면 에러가 뜬다.

이제 JPA에 이 둘의 관계가 무슨 관계인지(ex. 1:N, N:1) 알려줘야 한다.

Registry와 Comment는 1:N에서 누가 1이고 누가 N인지 매우 중요하다.

→ DB 관점으로 매우 중요하다.


@Column과 같은 어노테이션들은 db와 매핑하는 어노테이션이다.





@ManyToOne

여기서 Comment가 N이고 Registry가 1이다. (하나의 게시글에 여러 개의 댓글)

그래서 Comment 입장에서는 @ManyToOne이라는 어노테이션으로 매핑을 해야한다.

image





@JoinColumn

image

Registry²의 reference와 Comment 테이블에 있는 REGISTRY_ID(FK)¹와 매핑을 해야한다.

@JoinColumn(name = "REGISTRY_ID") 이렇게 하면 매핑이 끝난다.

1
2
3
4
5
// Comment.java

@ManyToOne
@JoinColumn(name = "REGISTRY_ID")
private Registry registry;





code

*getter, setter 생략

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Comment.java

@Entity
public class Comment {
    @Id @GeneratedValue
    @Column(name = "COMMENT_ID")
    private Long id;

    private String comment;

    @ManyToOne
    @JoinColumn(name = "REGISTRY_ID")
    private Registry registry;
}
1
2
3
4
5
6
7
8
9
10
// Registry.java

@Entity
public class Registry {
    @Id @GeneratedValue
    @Column(name = "REGISTRY_ID")
    private Long id;

    private String title;
}





조회

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Main.java

try {
    Registry registry = new Registry();
    registry.setTitle("title");
    em.persist(registry);

    Comment comment = new Comment();
    comment.setComment("comment");
    comment.setRegistry(registry);
    em.persist(comment);

    Comment findComment = em.find(Comment.class, comment.getId());

    Registry findRegistry = findComment.getRegistry();
    System.out.println("findRegistry.getTitle() = " + findRegistry.getTitle());
            
    tx.commit();
}

image



insert 쿼리가 2번 나간 것을 볼 수 있다.

image



참고로 영속성 컨텍스트 말고 db에서 가져오는 쿼리를 보고 싶다면 아래 코드를 추가하면 된다.

1
2
3
// Main.java
em.flush(); // 강제 호출 (영속성 컨텍스트에 있는 것들을 db에 쿼리를 날려버려서 싱크를 맞춤)
em.clear(); // 영속성 컨텍스트 초기화





point

관계가 무엇인지 @ManyToOne

이 관계를 할 때 join 하는 column은 무엇인지 @JoinColumn






양방향 연관관계

Comment에서 Registry로 갈 수 있는데 반대로 Registry에서 getComment는 안된다.

reference만 넣어두면 Comment ↔ Registry로 왔다갔다 할 수 있다.

→ 양방향 연관관계라고 한다. (양쪽으로 참조해서 갈수있게 함)



테이블 연관관계는 차이가 하나도 없다.

why? REGISTRY_ID(FK) 와 REGISTRY_ID(PK)랑 JOIN하면 되기 때문에

테이블의 연관관계는 외래키 하나로 양방향이 다 있는 것이다.

사실상 테이블의 연관관계는 방향이라는 개념 자체가 없다.

FK만 집어넣으면 양쪽으로 다 알 수 있다.



그러나 문제는 객체다.

이 전에 Comment에서 Registry로 갈 수 있는데 Registry에서 Comment로 갈 수 있는 방법은 없다.

그래서 Registry에 comments라는 List를 넣어줘야 양쪽으로 갈 수 있는 것이다.

image





code

*Comment 엔티티는 단방향과 동일하다.

Registry 엔티티는 컬렉션을 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Registry.java

@Entity
public class Registry {
    @Id @GeneratedValue
    @Column(name = "REGISTRY_ID")
    private Long id;

    private String title;

    @OneToMany(mappedBy = "registry")
    private List<Comment> comments = new ArrayList<>();
}

private List<Comment> comments = new ArrayList<>();

관례로 ArrayList<>()로 초기화 해준다. (그래야 add할때 nullpoint가 안뜨므로)


Registry에서 Comment로 가는 것은 일대 다 이므로 @OneToMany를 작성하고

mappedBy를 적어준다. (mappedBy = "registry")

image

mappedBy는 일대 다 매핑에서 어떤 거랑 연결되어있는지 적는 곳인데

Registry의 변수명 registry랑 매핑이 되어있다라는 얘기다.





객체와 테이블이 관계를 맺는 차이

  • 객체 연관관계 = 2개

    • 회원 → 팀 연관관계 1개(단방향)

    • 팀 → 회원 연관관계 1개(단방향)

회원에서 팀으로 가려면 참조 값 하나 넣어줘야 하고

팀에서 회원으로 가려면 참조 값을 하나 넣어놔야 한다.

→ 단방향 연관관계가 2개가 있는 것이다.


  • 테이블 연관관계 = 1개

    • 회원 ↔ 팀의 연관관계 1개(양방향)

양방향이라고 적었지만 사실은 방향이 없는 것이다.

하나만 있으면 양쪽으로 왔다갔다 할 수 있다.





연관관계의 주인(Owner)

Comment를 바꾸고 싶거나 새로운 Registry에 작성하려고 한다고 가정

그럴때 Comment의 registry 값을 변경해야할지 Registry의 comments를 바꿔야 할지 모름(둘다 맞기 때문에)

하지만 db 입장에서는 COMMENT에 있는 REGISTRY_ID(FK) 외래키 값만 업데이트 하면 된다.

그래서 rule이 생긴다. → 둘 중 하나로 외래 키를 관리해야한다.

→ 주인을 정해야한다.



“연관관계의 주인”이라는 개념은 양방향 매핑에서 나오는 것이다.

• 연관관계의 주인만이 외래 키를 관리(등록, 수정)

주인이 아닌쪽은 읽기만 가능

• 주인은 mappedBy 속성 사용X

• 주인이 아니면 mappedBy 속성으로 주인 지정

mappedBy 라는 뜻 자체가 저것에 의해 내가 매핑이 되었어 라는 뜻





누구를 주인으로?

image

DB 입장에서 보면 외래키(FK)가 있는 곳이 무조건 N(다)이다.

외래키가 없는 곳이 무조건 1이다.

그 말인 즉슨, DB의 N 쪽이 연관관계의 주인이 된다. → @ManyToOne



이미 코드를 보면 답이 다 나와있다.

@OneToMany(mappedBy = "registry")mappedBy로 나는 registry에 의해서 관리가 되고 있다.

이 registry는 아래 코드를 말한다.

1
2
3
4
5
6
7
8
// Comment.java

@Entity
public class Comment {
    @ManyToOne
    @JoinColumn(name = "REGISTRY_ID")
    private Registry registry;
}


mappedBy가 적힌 곳은 읽기만 된다.

1
2
3
4
5
6
7
// Registry.java

@Entity
public class Registry {
    @OneToMany(mappedBy = "registry")
    private List<Comment> comments = new ArrayList<>();
}

comments에 값을 넣어봤자 아무 일도 일어나지 않는다.(대신 조회는 가능)


연관관계의 주인은 Comment에 있는 Registry가 연관관계의 주인이다.

그래서 연관관계 주인에 값을 넣어야 한다.





주의할 점

양방향 매핑시 주의할 점으로는 3가지가 있다.


1. 연관관계의 주인에 값을 입력하지 않음

1
2
3
4
5
6
7
8
9
10
11
12
13
// Main.java

Registry registry = new Registry();
registry.setTitle("게시글 제목");
em.persist(registry);

Comment comment = new Comment();
comment.setComment("댓글");

// 역방향(주인이 아닌 방향)만 연관관계 설정
registry.getComments().add(comment);

em.persist(comment);

실행하면 COMMENT 테이블에 REGISTRY_ID가 null이다.

왜 그럴까? 연관관계의 주인은 Comment에 있는 registry가 연관관계의 주인이다.

Registry에 있는 comments는 mappedBy 읽기 전용이다.(가짜 매핑)

그래서 연관관계 주인에 값을 넣어야 한다.

1
2
3
4
5
6
7
8
9
10
11
// Main.java

Registry registry = new Registry();
registry.setTitle("게시글 제목");
//registry.getComments().add(comment); ←
em.persist(registry);

Comment comment = new Comment();
comment.setComment("댓글");
comment.setRegistry(registry);  // ⭐⭐⭐⭐⭐
em.persist(comment); 





2. 연관관계 편의 메소드 생성하기

JPA 입장에서는 아래 코드가 맞는 코드이다.

1
2
3
4
5
6
7
8
9
10
// Main.java

Registry registry = new Registry();
registry.setTitle("게시글 제목");
em.persist(registry);

Comment comment = new Comment();
comment.setComment("댓글");
comment.setRegistry(registry);  // ⭐⭐⭐⭐⭐
em.persist(comment); 

그런데 객체지향적으로 생각하면 양쪽에 다 값을 걸어야 한다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Main.java

Registry registry = new Registry();
registry.setTitle("게시글 제목");
em.persist(registry);

Comment comment = new Comment();
comment.setComment("댓글");
comment.setRegistry(registry);
em.persist(comment);

em.flush(); // 강제 호출 (영속성 컨텍스트에 있는 것들을 db에 쿼리를 날려버려서 싱크를 맞춤)
em.clear(); // 영속성 컨텍스트 초기화

Registry findRegistry = em.find(Registry.class, registry.getId()); // 1)

List<Comment> comments = findRegistry.getComments();  // 2)
for (Comment c: comments) { // 2)
    System.out.println("c.getComment() = " + c.getComment()); // 2)
}
tx.commit();

위 코드를 실행하면 문제 없이 동작한다.

image

실행하면 두개의 select 쿼리가 나오는데

1) Registry를 조회했을 때 나오는 쿼리

2) 실제 Comment 데이터를 로딩했을 때 (Registry의 comments를 사용하는 시점에 쿼리를 날린다.)

flush(), clear()를 해버리기 때문에

registry.getComments().add(comment); 라고 코드를 안넣어줘도 동작하지만

flush(), clear()를 주석처리하고 실행시키면

1) 은 1차 캐시에 있지만 2)에서 컬렉션에는 값이 없다.



1
2
3
4
5
6
7
8
9
Registry findRegistry = em.find(Registry.class, registry.getId()); // 1)

List<Comment> comments = findRegistry.getComments();  // 2)
System.out.println("= = = = = = = = =");
for (Comment c: comments) { // 2)
    System.out.println("c.getComment() = " + c.getComment()); // 2)
}
System.out.println("= = = = = = = = =");
tx.commit();

image

insert 쿼리만 나가는 것을 볼 수 있다.

Registry는 순수한 객체 상태이기 때문에 컬렉션에는 값이 없다.


저장한 상태 그대로 영속성 컨텍스트에 들어가 있기 때문에 db에서 select 쿼리가 날라가지 않는다.

따라서 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정한다.

*참고로 테스트 케이스에서도 jpa 없이 작성해 줄 수 있기 때문에 둘 다 값을 세팅해줘야한다.



여기서 주의할 점은

registry.getComments().add(comment);comment.setRegistry(registry)를 같이 넣어야 한다는 것이다.

1
2
3
4
5
6
7
8
9
10
Registry registry = new Registry();
registry.setTitle("게시글 제목");
em.persist(registry);

Comment comment = new Comment();
comment.setComment("댓글");
comment.setRegistry(registry); //  👈🏻
em.persist(comment);

registry.getComments().add(comment); //  👈🏻

이 부분을 까먹을 수도 있기 때문에 연관관계 편의 메소드를 생성하는 것을 추천한다.




연관관계 편의 메소드

1
2
3
4
5
6
// Comment.java

public void setRegistry(Registry registry) {
    this.registry = registry;
    registry.getComments().add(this);
}

참고로 연관관계 편의 메소드나 jpa 상태를 변경하는 것은 set을 잘 안쓴다.

getter, setter 관례 때문에 로직이 추가로 들어가면 이름을 바꿔준다.

🔽

1
2
3
4
5
6
// Comment.java

public void changeRegistry(Registry registry) {
    this.registry = registry;
    registry.getComments().add(this);
}
1
2
3
4
5
6
7
8
9
10
// Main.java

Registry registry = new Registry();
registry.setTitle("게시글 제목");
em.persist(registry);

Comment comment = new Comment();
comment.setComment("댓글");
comment.changeRegistry(registry);
em.persist(comment);

image



만약 Registry를 기준으로 comment를 넣는다면 아래와 같이 작성하면 된다.

1
2
3
4
5
6
// Registry.java

public void addComment(Comment comment){
    comment.setRegistry(this);
    comments.add(comment);
}
1
2
3
4
5
6
7
8
9
10
11
// Main.java

Registry registry = new Registry();
registry.setTitle("게시글 제목");
em.persist(registry);

Comment comment = new Comment();
comment.setComment("댓글");
em.persist(comment);
            
registry.addComment(comment);

여기서 조심해야할 것은 연관관계 편의 메소드가 양쪽에 다 있으면 문제를 일으킬 수 있다.

그래서 한쪽은 지워준다. (둘 중에 하나만 정해주면 된다.)



정리하자면 연관관계의 주인은 Comment에 있는 Registry가 주인이고

값을 세팅하는 것은 본인 마음이다. (단, 연관관계 편의 메소드는 둘 중에 하나만 세팅한다.)





3. 양방향 매핑시에 무한 루프를 조심한다.

Comment에서 toString()을 생성해본다.

1
2
3
4
5
6
7
8
9
10
// Comment.java

@Override
public String toString() {
    return "Comment{" +
        "id=" + id +
        ", comment='" + comment + '\'' +
        ", registry=" + registry +
        '}';
}

여기서 registry는 registry.toString()을 또 호출한다는 얘기가 된다.

1
2
3
4
5
6
7
8
9
10
// Comment.java

@Override
public String toString() {
    return "Comment{" +
        "id=" + id +
        ", comment='" + comment + '\'' +
        ", registry=" + registry.toString() +
        '}';
}



또 Registry에서 toString()을 생성하면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
// Registry.java

@Override
public String toString() {
    return "Registry{" +
        "id=" + id +
        ", title='" + title + '\'' +
        ", comments=" + comments +
        '}';
}

comments는 컬렉션 하나하나 안에 있는 toString()을 다 호출한다.

그래서 양쪽으로 toString()을 무한 호출하게 된다.



1
2
3
// Main.java

System.out.println(findRegistry);

→ 실행하면 StackOverflowError가 뜸(양쪽으로 계속 호출하기 때문이다.)





정리

• 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.

• 객체는 참조를 사용해서 연관된 객체를 찾는다.

• 테이블과 객체 사이에는 이런 큰 차이 있다.

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