velog에서 이전한 글 입니다.

2023.07.13 - [spring] - 스프링) JPA Entity 연관관계 - 외래 키 주인 (23-07-11)

지난 포스팅에 이어

JPA Entity Option

FetchType EAGER/LAZY (지연 로딩)

public @interface ManyToOne{
...
    FetchType fetch() default FetchType.EAGER;
...
}

public @interface OneToMany{
...
    FetchType fetch() default FetchType.LAZY;
...
}

EAGER는 첫 select와 동시에 join으로 참조 항목을 가져온다.
LAZY는 첫 select에서 join을 하지않고 참조가 필요할 때 추가로 select를 요청하고 가져온다.

ManyToOne의 default FetchType은 EAGER
OneToMany의 default FetchType은 LAZY 이다.
몇 개일지 모를 다수를 기본적으로 join하는 건 성능에 방해될 수 있어서 LAZY일 것이다.

public class Post extends Timestamped {
...

    @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE)
    private List<Comment> commentList = new ArrayList<>();

...
}

public class Comment extends Timestamped {
    ....
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;

    @ManyToOne()
    @JoinColumn(name = "user_id")
    private User user;
}

위와 같은 entity 관계에 Post를 findAll할 경우 sql은 아래와 같다.

Hibernate: 
    /* <criteria> */ select
        ...
        p1_0.subject,
        p1_0.username 
    from
        post p1_0
Hibernate: 
    select
        ...
        c1_0.created_at,
        c1_0.modified_at,
        u1_0.id,
        u1_0.password,
        u1_0.username 
    from
        comment c1_0 
    left join
        users u1_0 
            on u1_0.id=c1_0.user_id 
    where
        c1_0.post_id=?

post에서 commentList는 default가 LAZY 이다. post만 select한 후 commentList를 사용할 때 추가로 comment를 select하는 것을 볼 수 있다.

 

여기서 comment는 user와 post 필드가 있다. default는 EAGER 이고 post는 LAZY로 설정했다.


comment는 user를 바로 join하는 것을 볼 수 있고 post는 사용하지 않아 추가 select query가 실행되지 않은 것을 볼 수 있다.

CascadeType PERSIST/REMOVE (영속성 전이)

영속성 전이 : 영속 상태의 Entity에서 수행되는 작업이 연관된 Entity까지 전파되는 상황

public class User {
    ...
    (1)
    @OneToMany(mappedBy = "user")
    (2)
    @OneToMany(mappedBy = "user", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    private List<Food> foodList = new ArrayList<>();

    public void addFoodList(Food food) {
        this.foodList.add(food);
        food.setUser(this);// 외래 키(연관 관계) 설정
        }
}
    User user = new User();
    user.setName("Robbie");
    ...
    user.addFoodList(food1);
    user.addFoodList(food2);

    userRepository.save(user);

(1)의 설정은 userRepo의 save가 foodRepo의 save까지 이어지진 않는다.
(2)의 설정은 CascadeType.PERSIST등록(insert)에 대해 영속성 전이가 적용되었고 userRepo save로 foodRepo에 food1, food2를 저장한 것을 볼 수 있다.

    User user = userRepository.findByName("Robbie");
    userRepository.delete(user);

(1)의 설정은 userRepo의 delete가 foodRepo의 delete까지 이어지진 않는다.
(2)의 설정은 CascadeType.REMOVE삭제(delete)에 대해 영속성 전이가 적용되었고 userRepo delete로 food1, food2가 삭제된 것을 볼 수 있다.

orphanRemoval (고아 Entity 삭제)

    (@Transactional)
    User user = userRepository.findByName("Robbie");

    Food chicken = null;
    for (Food food : user.getFoodList()) {
        if(food.getName().equals("후라이드 치킨")) {
            chicken = food;
        }
    }
    if(chicken != null) {
        user.getFoodList().remove(chicken);
    }

위와 같이 연관 관계 데이터를 삭제할 때CascadeType.REMOVE는 Delete 쿼리를 수행하지 않는다.

public class User {
    ...
    (1)
    @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true)
    private List<Food> foodList = new ArrayList<>();
    ...
}

여기서 orphanRemoval 설정은 연관 관계 데이터의 부분 삭제를 쿼리로 이어지게 해준다.

 

cascade remove는 본인 entity 삭제시 연관관계 데이터를 모두 삭제해주었고
orphanRemoval 는 cascade remove의 기능을 포함하고 추가로 연관관계 데이터 부분 삭제시 삭제 쿼리를 날려준다.

 

orphanRemoval는 ManyToOne에는 없는 옵션이다.
반대쪽 연관관계 데이터를 삭제하는 기능인데 외래키를 가지고 있는 Many에서 One을 죽이는 건 말도 안되고 반대가 One이므로 부분 삭제 기능이 필요없다.

주의

orphanRemovalCascadeType.REMOVE 는 신중히 사용할 필요가 있다.
option을 걸기전에 '나'뿐만 아니라 다른 누군가도 '나'의 연관 Entity를 참조하고 있는지 잘 확인해야 한다.