Home 연관관계_proxy
Post
Cancel

연관관계_proxy

Proxy

em.find() vs em.getReference()

em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회

em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회

→ DB 쿼리는 안나가는데 객체는 조회되는 걸 말한다.




아래는 기본 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
EntityManagerFactory emf = Persistence.createEntityManagerFactory("dal");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();

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

    em.persist(comment);

    em.flush();
    em.clear();

    // 코드 추가할 곳
    
    tx.commit();
}catch (Exception e){
    tx.rollback();
    e.printStackTrace();
}finally {
    em.close();
}
emf.close(); 



1
2
3
Comment findComment = em.getReference(Comment.class, comment.getId());
System.out.println("findComment.getId() = " + findComment.getId());
System.out.println("findComment.getComment() = " + findComment.getComment());

image

.getReference()를 호출하는 시점에는 select 쿼리가 안나간다.

findComment.getId()로 파라미터에 id값을 넣어줬기 때문에 db에서 안가지고 와도 안다.

getUsername()은 db에 있기 때문에 JPA가 DB에 쿼리를 날린다.




1
System.out.println("findComment = " + findComment.getClass());

image

Comment를 조회해보면 이름이 Comment가 아니라 hibernate가 강제로 만든 가짜 클래스로 출력된다.

→ Proxy class라는 것이다.




getReference()라고 하면 진짜 Comment 객체를 주는 것이 아니라

hibernate가 자기 내부의 어떤 라이브러리를 써가지고 속칭 proxy 라고 하는 가짜 엔티티 객체를 준다.

껍데기는 똑같은데 안이 텅텅 비어있고 내부에는 target이라는게 있다. (target이 진짜 reference를 가리킴)

*em.find()를 하면 진짜 객체를 준다.







Proxy 특징

• 실제 클래스를 상속 받아서 만들어진다.

• 실제 클래스와 겉 모양이 같다.

• 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)




• 프록시 객체는 실제 객체의 참조(target)를 보관

• 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출

image

Proxy의 getComment()을 호출하면 target에 있는 getComment()(Entity의 getComment을 말함)을 대신 호출해준다.

하지만 실제 db에서 조회한 적이 없기 때문에 처음에는 target이 없을 것이다.




1
2
Comment comment = em.getReference(Comment.class, comment.getId());
comment.getComment();

image

프록시 객체를 가져와서 comment.getComment()을 호출하면 Comment의 target에 값이 처음에 없다.

그러면 JPA가 영속성 컨텍스트에 요청을 한다. (⭐영속성 컨텍스트를 통해서 초기화를 요청한다.⭐)

영속성 컨텍스트에서는 DB를 조회해서 실제 Entity를 생성해서 준다.

그리고 target에 있는 것에다가 연결을 시켜준다.


그래서 getName(MemberProxy)을 했을 때

target의 getComment()(Comment)을 통해서 Comment에 있는 getComment()이 반환된다.



한번 초기화 되면 이제 Member target(MemberProxy)에 걸리기 때문에 다시 db 조회할 일은 없다.

1
2
System.out.println("findComment.getComment() = " + findComment.getComment());
System.out.println("findComment.getComment() = " + findComment.getComment());

image




• 프록시 객체는 처음 사용할 때 한 번만 초기화

• 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니며,

초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능하다.







Comment를 조회할 때 Registry도 함께 조회해야할까?

단순히 Comment 정보만 사용하는 비즈니스 로직일 경우 같이 조회하는 것은 손해다.




지연 로딩 LAZY을 사용해서 프록시로 조회

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
public class Comment {
    @Id
    @GeneratedValue
    @Column(name = "COMMENT_ID")
    private Long id;

    private String comment;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "REGISTRY_ID")
    private Registry registry;
}

이렇게 하면 Registry를 프록시 객체로 조회한다. → Comment 클래스만 db에서 조회




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

Comment만 가져온 것을 볼 수 있다.

image




registry를 조회해보면 proxy로 나온 것을 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Registry registry = new Registry();
registry.setTitle("title");
em.persist(registry);

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

em.persist(comment);

em.flush();
em.clear();

Comment findComment = em.find(Comment.class, comment.getId());
System.out.println("findComment.getRegistry().getClass() = " + findComment.getRegistry().getClass());

tx.commit();

image




이제 registry에 무언가를 가지고 오려고 하면 이 시점에 쿼리가 나간다.

실제 registry를 사용하는 시점에 proxy 객체가 초기화 되면서 값을 가지고 온다.

1
2
3
4
5
6
Comment findComment = em.find(Comment.class, comment.getId());
System.out.println("findComment.getRegistry().getClass() = " + findComment.getRegistry().getClass());

System.out.println("= = = = = = = = = = = = = ");
findComment.getRegistry().getTitle();
System.out.println("= = = = = = = = = = = = = ");

image


그래서 지연로딩으로 세팅하면 연관된 것을 프록시로 가져온다.





지연로딩

comment를 로딩을 할 때 Registry 인스턴스는 지연로딩으로 세팅되어있기 때문에 프록시로 가지고 온다.

이것을 LAZY, 지연로딩이라고 한다.




지연 로딩 LAZY을 사용해서 프록시로 조회

Comment comment = em.find(Comment.class, 1L);

em.find로 comment를 가지고 왔을 때 지연로딩으로 세팅되어있으면

가짜 프록시 객체를 받아서 넣는다.




1
2
Registry registry = comment.getRegistry();
registry.getTitle(); // 실제 registry를 사용하는 시점에 초기화(DB 조회)

실제 registry를 사용하는 시점에 쿼리가 나간다.

⭐ registry를 사용할 때가 아니라 registry에 있는 뭔가를 실제 사용할 때 초기화가 된다.



findComment.getRegistry()는 프록시로 가져오기 때문에 쿼리가 나가지 않고

프록시를 가져와서 어떤 메소드를 사용할 때 초기화가 일어난다. (ex. findComment.getRegistry().getTitle();)



만약 Comment와 Registry가 자주 함께 사용한다면 즉시 로딩 EAGER를 사용해서 함께 조회한다.



참고로 LAZY 설정할 때 static import할 수 있다.

image

1
2
3
4
5
6
7
8
9
10
11
12
13
import static javax.persistence.FetchType.LAZY;

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

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








영속성 전이(CASCADE)와 고아 객체

영속성 전이

• 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들도 싶을 때

• 예: 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장

image




영속성 전이: 저장

@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)

image

parent를 저장할 때 연관된 얘도 같이 저장하는게 cascade다.




코드로 살펴보기

1
2
3
4
5
6
7
8
9
10
11
12
Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);
em.persist(child1);
em.persist(child2);

tx.commit();

Parent를 중심으로 코드를 작성하고 있는데 persist를 child까지 총 3개를 작성해야 한다.

parent를 persist할 때 자동으로 child도 persist 해줬으면 좋을 때 cascade를 사용한다.

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)



1
2
3
4
5
6
7
8
9
10
Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);

tx.commit();

parent만 persist 했는데 Child도 persist 된 것을 볼 수 있다.

image


image

name 값은 넣지 않아서 null으로 나오는게 맞고

PARENT의 ID와 CHILD의 ID 값인 34, 35가 PARENT의 33으로 잘 들어가 있는 것을 볼 수 있다.



이 전에 했던 연관관계와는 전혀 관련이 없고 심플하게 Parent를 persist할 때

cascade로 선언한 컬렉션 안에 있는 얘들(Child)을 전부 persist 날려주는 것이 cascade다.


연관관계를 매핑하는 것과 아무관련 없다라는 뜻은 연관관계를 맺는 과정에 영속성 전이는 필요없다라는 뜻이다.

예를 들어, 1:N 연관관계를 맺을 때 CASCADE 속성 없이 @OneToMany만 있어도 사용하여 연관관계를 맺을 수 있다.

즉, 1:N 연관관계를 맺는다는 관점에서 CASCADE는 아무런 역할을 하지 않는다.

CASCADE를 추가하는 이유는 연관관계 매핑이 아닌 순전히 영속성 전이를 위함이기 때문이다.





주의할 점

• 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음

• 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐





종류

참고로 bold 처리된 3개만 쓰게 됨

ALL: 모두 적용

PERSIST: 영속

REMOVE: 삭제

• MERGE: 병합

• REFRESH: REFRESH

• DETACH: DETACH





CASCADE는 언제 쓸까?

1대 다에는 무조건 걸어야 할까? ❌ NO!!

하나의 부모가 자식들을 관리할 때 CASCADE가 의미가 있다.





CASCADE를 쓰면 안되는 case

파일을 여러 군데에서 관리하고 다른 엔티티에서 관리하는 경우 사용하면 안된다.

Parent만 Child를 관리하고 연관관계가 있으면 상관없는데

다른 객체랑 Child랑 관계가 있으면 사용하면 안된다.

소유자가 하나일 때만 CASCADE를 사용한다.


Child에서 다른 객체로 나가는 것은 상관이 없는데

다른 객체가 Child를 알게 되면 CASCADE를 사용하면 안된다. (운영이 힘들어진다.)


다른 객체가 Child를 안다는 것은 Locker → Child 이런식으로 Locker가 Child를 필드에 가지고 있는 것을 말한다.

Child에서 다른 부분으로 나간다는 것은 Child → Door 이런식으로 Child가 Door를 필드에 가지고 있는 것을 말한다.

결과적으로 영속성 전이와 고아객체는 다른 곳에서 Child를 참고하지 않을 때 사용할 수 있다.


Life Cycle이 동일할 때 (Parent와 Child의 LifeCycle이 유사할 때 - 등록, 삭제)

단일 소유자 (소유자가 하나일 때)

CASCADE를 사용하면 된다.

cascade는 부모 엔티티의 특정 동작(예를 들어, 삭제)을 자식 엔티티에도 전파하는 기능이다.






고아 객체

  • 고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제

  • orphanRemoval = true

  • Parent parent1 = em.find(Parent.class, id);

    parent1.getChildren().remove(0); // 자식 엔티티를 컬렉션에서 제거

  • DELETE FROM CHILD WHERE ID=?



코드로 보기 #46

1
2
3
4
5
em.flush();
em.clear();

Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);

아래와 같이 하나가 지워졌다.

image


id가 2번인 child가 지워졌다.

image



1
2
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();

orphanRemoval은 컬렉션에서 빠진 것은 삭제 된다.





고아 객체 - 주의

• 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능

참조하는 곳이 하나일 때 사용해야함! (ex. 게시판의 첨부파일 개념일 때)

특정 엔티티가 개인 소유할 때 사용

@OneToOne, @OneToMany만 가능

• 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다.
따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다.
이것은 CascadeType.REMOVE처럼 동작한다.


이 말은 아래와 같이 설명할 수 있다.



코드로 보기 #46

코드를 보면 CascadeType.ALL을 지우고 child를 persist 코드로 다시 바꾼 후에

1
2
3
// Parent.java
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> childList = new ArrayList<>();


아래와 같이 parent를 지우게되면 Parent 엔티티가 지워지게 된다.

1
2
3
4
5
6
7
8
9
10
// JpaMain.java
em.persist(parent);
em.persist(child1);
em.persist(child2);

em.flush();
em.clear();

Parent findParent = em.find(Parent.class, parent.getId());
em.remove(findParent);

그러면 orphanRemoval 입장에서 해당 컬렉션은 다 날라가게 된다.


실행시키면 Child 2개 있는게 지워지는 것을 확인할 수 있다.

image


참고로 @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) 라고만 해도 똑같은 결과가 나온다.

image



orphanRemoval는 조심히 사용해야한다.

이것만 기억한다. 특정 엔티티가 개인 소유할 때 사용한다.






영속성 전이 + 고아 객체, 생명주기

영속성 전이 + 고아 객체, 생명주기

CascadeType.ALL + orphanRemoval=true


• 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거

→ 라이프 사이클을 JPA 영속성 컨텍스트(엔티티 매니저)를 통해서 한다.

• 위 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.

parent만 persist하고 parent만 remove를 했다.

Parent는 JPA를 통해서 생명주기를 관리하고 있고 Child는 Parent가 관리한다.


• 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용하다.

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