인프런 커뮤니티 질문&답변

고운 코끼리님의 프로필 이미지
고운 코끼리

작성한 질문수

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발

주문 서비스 개발

OrderItem을 DB에서 지우고자 할 때

작성

·

2.9K

2

안녕하세요.

주신 예제로 여러 가지 응용을 해보고 있는데, 한 가지 질문이 있어 질문드립니다.

 

OrderItem을 지우는 상황을 가정을 했을 때, 지우는 방법에 대해 여쭤보고 싶습니다. (parent에 의존하여 영속화 되어있는 객체를 지우는 상황)

Order를 지우면 OrderItem은 CASCADE 옵션 덕분에 잘 지워지지만, 반대로 Order는 두고 OrderItem 하나만 지우기 위해 Order <-> OrderItem 관계를 끊어도 OrderItem은 지워지지 않습니다.

 

아래는 제가 시도했던 부분입니다.

- Parent인 Order의 list에서 OrderItem 삭제

- Child인 OrderItem에서 this.order = null; this.item = null;로 모든 관계 삭제

- @OneToMany 옵션이 있는 Parent쪽에서 orphanRemoval = true 옵션 넣기

- 위 과정 모두 한 뒤, em.persist(Order); 호출

 

위 모두 해보아도 OrderItem에 null로 들어갈뿐 OrderItem이 삭제 되진 않습니다.

구글링을 해봤을 땐 orphanRemoval 옵션 추가하고 연관관계 삭제하라는 말뿐이네요.. 

 

혹시 방법이 있을까요?

 

그리고 추가로,

여기선 OrderItem이라는 다:1 매핑된 객체는 CASCADE를 통해 따로 영속화하지 않았는데, 보통 다:1 매핑은 전부 그러한가요?

제가 느끼기엔 서로 독립적으로 저장해야할 때라고 판단하였는데, 다:1이면 독립적일 수가 없을 것 같더라구요.

어떤 경우에 따로따로 영속화하고, 어떤 경우는 이 예시와 같이 한꺼번에 하는지 궁금합니다.

 

감사합니다.

답변 22

5

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. t님

메일을 받았습니다. 문제를 단순화 하기위해서 제가 새로 가장 단순한 형태의 샘플 코드를 만들었습니다.

궁금하신 질문이 persist 후에 연관관계 맺어 추가된 child에 대해서는 orphanRemoval이 작동하지 않는다. 로 이해하고 제가 샘플 코드를 모두 보여드릴게요. 이 예제를 직접 돌려보시면 이 경우에도 orphanRemoval이 동작하는 것을 확인하실 수 있습니다.

package com.example.demo.entity;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Getter @Setter
public class Parent {

@Id @GeneratedValue
private Long id;

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

public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}

}
package com.example.demo.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Getter
@Setter
public class Child {

@Id
@GeneratedValue
private Long id;

@ManyToOne
@JoinColumn(name = "parent_id")
Parent parent;
}
package com.example.demo.entity;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;


@SpringBootTest
class ParentTest {

@Autowired
EntityManager em;

@Test
@Transactional
void orphanRemoval() {

//child1는 persist 전에 연관관계 추가
Child child1 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
em.persist(parent);
em.flush(); //INSERT 쿼리 2번

//child2는 persist 후에 연관관계 추가
Child child2 = new Child();
parent.addChild(child2);
em.flush(); //INSERT 쿼리 1번

parent.getChildList().clear(); //child1, child2를 컬렉션에서 제거한다. orphanRemoval 용
System.out.println("before ===================");
em.flush();//delete 쿼리 2번
System.out.println("after ===================");

em.clear();//영속성 컨텍스트를 초기화해서 깔끔한 상태로 다시 조회한다.

Parent findParent = em.find(Parent.class, parent.getId());
//child가 orphanRemoval에 의해 모두 삭제된 것을 확인할 수 있다.
System.out.println("getChildList().size = " + findParent.getChildList().size());
}

}

출력결과

Hibernate: call next value for hibernate_sequence

Hibernate: call next value for hibernate_sequence

Hibernate: insert into parent (id) values (?)

Hibernate: insert into child (parent_id, id) values (?, ?)

Hibernate: call next value for hibernate_sequence

Hibernate: insert into child (parent_id, id) values (?, ?)

before ===================

Hibernate: delete from child where id=?

Hibernate: delete from child where id=?

after ===================

Hibernate: select parent0_.id as id1_1_0_ from parent parent0_ where parent0_.id=?

Hibernate: select childlist0_.parent_id as parent_i2_0_0_, childlist0_.id as id1_0_0_, childlist0_.id as id1_0_1_, childlist0_.parent_id as parent_i2_0_1_ from child childlist0_ where childlist0_.parent_id=?

getChildList().size = 0

출력 결과를 보시면 persist전, persist 후에 등록한 Child가 모두 삭제되는 것을 확인하실 수 있습니다.

도움이 되셨길 바래요.

3

김영한님의 프로필 이미지
김영한
지식공유자

추가로 orphanRemoval을 사용할 때는 주의할 점이 있습니다.

orphanRemovel과 cascade=ALL를 사용한다는 것은 해당 엔티티를 저장하고 관리하는 역할이 Repository가 아니라 관리하는 엔티티로 바뀌게 됩니다. 따라서 관리하는 엔티티에서 해당 엔티티를 관리(추가하고 삭제하고) 해야지, Repository를 통해서 em.persist()를 호출하면 순서가 꼬이는 문제가 발생할 수 있습니다.

t님이 보내주신 코드가 정상 동작하지 않는 이유도 방금 말씀드린 이유와 같습니다.

public Long addOrderItem(Long orderId, Long itemId, int count) {
Order order = orderRepository.findOne(orderId);
Item item = itemRepository.findOne(itemId);

OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);

order.addOrderItem(orderItem);
orderRepository.save(order);
return orderItem.getId();
}

보내주신 코드를 보면 다음과 같이 addOrderItem에서 order를 리포지토리를 통해서 저장하고 있는데요. 이렇게 보면 문제가 없어보이지만, 모든 데이터 변경은 항상 persist를 호출하는 것이 아니라, 변경 감지를 통해서 사용해야 합니다.

그리고 이 경우 save를 호출하면 내부에서 em.persist(order)를 호출하게 되는데요. 문제는 order -> orderItem이 cascade 관계로 되어 있기 때문에 orderItem에 em.persist()를 한 것과 같은 효과가 나타압니다.

결국 orderItem에 대해서 persist를 호출했기 때문에 JPA는 리포지토리를 통해서 orderItem을 저장해야 합니다. 그리고 또 orphanRemoval를 통해서 삭제해야 합니다. persist로 데이터베이스에 저장하는 것도 즉시 발생하는 것이 아니라 나중에 플러시가 발생해야 되기 때문에, 둘의 순서가 꼬여버리는 것이지요. 

그래서 이 코드에서 orderRepository.save(order);를 제거하고 사용하는 방법을 찾으셔야 합니다.

물론 하이버네이트가 매우 정밀하게 이런 부분까지 문제없이 다 해결해주면 좋겠지만, 이 부분이 실무에서 이슈가 되지 않는 이유는 위에서 말씀드린 것 처럼, 다음 2가지를 지키면서 개발하기 때문입니다.

1. 데이터 변경은 변경 감지를 사용한다.

2. cascade, orpahnRemoval을 사용하면 관리의 주체가 리포지토리가 아니라 관리하는 엔티티, 여기서는 Parent,  Order가 되고, 여기를 통해서만 관리해야 합니다.

감사합니다.

2

알려주셔서 감사합니다.

이제부터는 정답이 없는 고민을 하게 되는군요.

설계를 생각할 때엔 단순함을 먼저 생각하는 것을 꼭 기억하겠습니다.

개발경험을 통해 알려주신 부분 덕분에 여러 가지 감을 잡을 수 있었던 것 같습니다.

 

많이 도와주신 덕분에 이러한 고민도 할 수 있었던 것 같습니다.

정말 감사드립니다!

1

김영한님의 프로필 이미지
김영한
지식공유자

Delivery의 경우 완전히 개인 소유가 될 수도 있고, 아닐 수도 있습니다.

이 부분은 프로젝트 설계에 따라서 달라지는데, 지금 보기에는 개인 소유가 되는 것 처럼 보이지만, Delivery 같은 객체는 다른 곳에서 향후에 참조 가능성이 높습니다. 이런 부분까지 고려해서 개인 소유를 고민해야 합니다.

그래서 개인 소유는 애매하면 사용하지 않는 것이 더 나은 방법입니다.

추가로 질문 주신 부분은 말씀하신 것 처럼 별도로 조회해서 삭제해도 됩니다. 그런데 개인 소유라는 개념에는 대상의 리포지토리도 만들지 않는다는 설계 컨셉이 있습니다. CASCADE + orpahnRemoval까지 다 해버리면 사실 첨부파일 같은 경우 별도의 리포지토리가 없어도 됩니다. 그런데 이 부분은 설계 컨셉에 대한 것이어서 링크에 드린 것 처럼 도메인 주도 설계의 에그리것 루트 개념을 이해해야 합니다.

감사합니다.

1

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. t님

먼저 cascade는 order엔티티에 뭔가 영향이 있어야 orderItem에도 영향이 있습니다.

예를 들어서 order를 삭제해야 cascadeAll에서 delete가 적용됩니다.

그래서 이런 경우는 orphanRemoval = true를 넣어주셔야 합니다.

다른 방법으로는 OrderItem을 직접 조회해서 em.remove()를 호출해주시면 됩니다.

추가로 질문 주신 부분은 다음 내용을 참고해주세요^^

https://www.inflearn.com/questions/31969

감사합니다.

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. t님^^

정답에 점점 가까워 지시는게 느껴지네요. 무엇보다 방향성에 대해서 고민하시는 부분이 좋네요.

결론부터 말씀드리면 3가지 방법이 있습니다.

1. 부모 엔티티로 자식 엔티티를 관리한다. (cascade + orphanRemoval)

이 방법을 사용하면 부모 엔티티를 통해서 자식 엔티티를 CRUD하는 것이 맞습니다.

2. 자식 엔티티도 하나의 독립적인 개체로 보고, 별도의 리포리토리를 통해서 관리한다. 이렇게 되면 어디에 소속된 엔티티라고 할 수 없다.

이 방법을 사용하면 자유롭게 자식 엔티티를 사용할 수 있습니다.

3. 하이브리드

엔티티를 관리할 때는 1번을 선택하지만 자식을 조회할 때 너무 불편해지니, 실용적인 관점에서 조회를 위해서 자식 엔티티의 조회용 리포지토리를 사용한다.

AggregateRoot

사실 어떤 경우에 부모 엔티티가 자식 엔티티를 관리하는게 좋을까? 많이 궁금하실거에요. 이런 부분을 이미 정의해둔 단어가 있는데 바로 도메인 주도 설계(DDD)에서 이야기하는 AggregateRoot라는 개념입니다. 이 개념에 부합할 때 1번이 합당한 것이고 아니라면 별도의 리포지토리를 구성하는 것이 맞습니다.

설계라는 것이 정답이 있다기 보다는, 현재 나의 도메인과 설계 상황에 따라서 더 적절한 선택지가 있다 생각합니다. 따라서 어떤 경우에는 1번이 더 나은 선택이고, 어떤 경우에는 2번이 더 나은 선택이고, 어떤 경우에는 3번이 더 나은 선택일 수 있습니다.

더 깊이있는 이해를 위해서 도메인 주도 설계(DDD)와 AggregateRoot 개념을 공부해보시길 권장드립니다.

마지막으로

설계는 항상 단순한 것이 가장 좋습니다. 아무리 좋은 설계 원칙들도 적용해서 애플리케이션의 복잡도만 더 높아진다면, 지금 나의 애플리케이션에 맞지 않은 선택일 가능성이 높습니다.

도움이 되셨길 바래요^^

0

궁금하던 부분이 해소되었습니다~!

@Transactional을 벗어나면서 변경감지를 통해 id가 채워져서 나가기 때문에, 객체를 return한 뒤에 id를 사용할 수 있군요.

정말 감사합니다.

 

아마 아래에 드리는 질문이 이 것과 관련된 마지막 질문이 될 것 같습니다.!

위 예시에서는 객체를 찾을 때 이미 객체를 알고 있어서 참조 비교로 찾을 수 있는데, 외부 URI를 통해 들어오는 정보는 id 밖에 없어서  바로 객체 참조값을 가질 수 없어 어찌됐든 id를 통해 객체를 찾아야 하더라구요.

보통은 repository를 통해 findById로 찾을텐데, cascade와 orphanremoval로 완전히 entity에게 책임을 넘긴 상황이라면, entity의 멤버함수로 id를 받아 객체를 찾는 것이 좋은 설계일까요?

  

다시 정리드리면,

상황: URI를 통해 들어오는 정보만으로 객체를 찾아야하므로 id값 밖에 알 수 있는 것이 없음.
(실무에서 사용하는 다른 방법이 있거나, cascade와 orphanremoval을 써서 묶은 entity를 조회하는 일이 있으면 안될 경우 알려주세요.)

해결: id로 객체를 찾기 위해 entity class에 id를 받아 객체를 반환하는 멤버함수를 만들음 (cascade와 orphanremoval로 책임을 entity에게 넘겼기 때문)

위와 같은 상황이 맞는 설계인지가 궁금합니다.

 

오류가 난다면 "오답"이기 때문에 "정답"으로 고치면 되지만,

설계 방식에 있어서는 실무경험이 없는 제겐 맞는 설계 방식인지 아닌지 알 수 있는 방법이 없네요..

 

이에 대한 조언 주시면 감사하겠습니다.

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요 t님

어떤 상황이 궁금하신지 이해가 되었습니다.

1. 변경감지를 사용하면 File의 id가 null로 반환될까 걱정이다.

2. 원래 있던 File을 어떻게 찾아야 할까?

먼저 변경감지 이후에 트랜잭션이 커밋되고, 플러시가 호출되면서 File도 id를 가지게 됩니다.

그리고 두번째 원래 있던 File을 찾는 방법은 index가 아니라 객체를 찾을 때 사용하는 방법으로 찾으시면 됩니다.

문제를 해결할 수 있는 여러가지 아이디어가 있는데요. 참조가 필요하기 때문에 객체를 외부에서 생성해서 넣고, 다시 찾거나 하는 등의 방식이 필요합니다.

상황을 설명하기 위해 다음과 같은 예시를 만들어보았습니다. 약간 억지스러운 예시이니 이렇게 찾는구나 정도로 생각해주세요.

(이 경우는 사실 file에 대한 참조가 이미 있기 때문에 찾지 않아도 됩니다.)

package com.example.demo;

import com.example.demo.entity.File;
import com.example.demo.entity.Post;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class PostController {

private final PostService postService;

@RequestMapping("/test")
public File test() {

File file = new File();
file.setFilename("fileA");

Post post = postService.saveAndReturn(file);

//파일 찾기
List<File> fileList = post.getFileList();
for (File findFile : fileList) {
if (findFile == file) {
return file;
}
}

return null;
}
}

package com.example.demo;

import com.example.demo.entity.File;
import com.example.demo.entity.Post;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;

@Service
@Transactional
@RequiredArgsConstructor
public class PostService {

private final EntityManager em;

public Post saveAndReturn(File file) {
Post post = new Post();
em.persist(post);

post.addFile(file);
return post;
}

}

실행결과

{"id":3,"filename":"fileA"}

그런데 이런 방식이 딱 맞을 때도 있지만 대부분의 경우에는 특정 엔티티를 직접 관리하는게 더 편하고 알맞은 방식을 때가 많습니다. 이럴 때는 엔티티를 통해서 엔티티를 관리하는 cascade, orphanRemoval이 맞지 않는 상황입니다. 이 경우에는 cascade, orphanRemoval을 제거하고, 해당 엔티티를 직접 관리하는 리포지토리를 만들어서(여기서는 File을 관리하는 리포지토리) 관리하는게 더 좋습니다.

도움이 되셨길 바래요^^

0

감사합니다. 다른 부분 모두 이해했습니다.

 

그런데, 마지막 질문은 제가 조금 여쭤보고 싶었던 거랑 다르네요ㅠㅠ

말씀주신 내용은 알고있는 부분인데, 제가 궁금했던 부분은 외부에서 어떻게 child 객체를 찾는지가 질문이었습니다.

변경감지를 통한 insert는 해당 메서드에서 바로 id를 읽을 수 없기 때문에 (아직 메서드가 안 끝나서 commit전이므로 id==null) Service에서 child 객체를  추가하고 id를 마지막에 return할 수 없는 상황입니다. (마치 OrderService에서 order 메서드를 마치고 id를 return해주는 것과 같은 메서드를 만들 수 없는 상황)

(위에서 말씀드렸듯이, child에서도 id를 받기 위해 em.persist를 호출한 것인데, persist를 사용하면 안 된다고 하면 자동으로 id도 null로 return되어서..)

 

아래는 전부 게시물-첨부파일 예시로 들겠습니다.

만약 OrderService처럼 child를 add후 해당 child의 id값을 return을 해주면, view에서 뿌릴 때 id를 내주어서 DELETE /posts/1/files/2 이런식으로 1번 게시물의 2번 첨부파일을 지우라는 행동이 가능한데 반해,

만약 위처럼 변경감지에 의존하면 Service에서 id값을 return해줄 수 없고, 뿌릴 때 해봤자 index로 뿌리게 되는데, 똑같이 DELETE /posts/1/files/2 를 요청하게 되면, 2번이 사라지고 3번인 요소가 2번이 되기 때문에 문제가 생길 것 같아서요. (같은 URI에 다른 behavior가 매핑되어서요)

이 부분을 실무에서는 실제로 Controller에서 어떻게 받아서 요청하는지 궁금합니다.

제가 이해한 바로는, CASCADE로 묶어서 엔티티에게 관리주체를 넘기면, 엔티티에서 관리하기 때문에 repository에서 잘 해오던 DB로부터 id 값을 받는 일이 없을 것 같아, 이럴 때에는 어떻게 해결하는지 궁금합니다.

 

제 질문이 잘 전달되지 않아 아쉬워서 이렇게 조금 더 구체적으로 상황을 적어봅니다.

이렇게 질문드리는 이유는 실무에선 이런 상황일 때 어떤 패턴으로 프로그래밍하는지 궁금하여 질문드립니다.

실무에서 이런 상황이 발생하지 않고 애초에 다른 방향으로 설계한다면, 그 방향을 알려주시면 감사하겠습니다.

도와주셔서 감사합니다.

0

김영한님의 프로필 이미지
김영한
지식공유자

mappedBy로 연결된 list는 강의 때 "mappedBy 단어 그대로 mapping이 되어있을 뿐 연관관계의 주인이 아니기 때문에 이 변수를 변경하여도 DB에 반영되지 않는다."라고 말씀주셨던 것 같은데, 위 테스트코드에선 mappedBy의 변수를 변경함으로써 제거하신 것 같아서요.!

다음 질문을 참고해주세요.

-> https://www.inflearn.com/questions/15855

Parent.childList는 지웠지만 Child.parent는 지우지 않으셨는데, 이 부분은 (DB반영 말고도 실무에서) 문제가 없을까요?

-> 네 원칙적으로 양쪽을 다 null로 설정하는게 맞습니다. 다만 너무 번거로우니 실용적인 관점에서 문제가 되지 않을 때는 그냥 두어도 됩니다.

parent에 연결된 특정 child를 지우기 위해서는 어떻게 해야하나요?

-> 객체의 참조를 이용하시면 됩니다.

예를 들어서 다음과 같이 하면 참조가 사라집니다.

parent.childList.add(new Child());

다음과 같이 참조값이 유지되면 쉽게 제거할 수 있습니다. (orphanRemovel을 사용하는 상황이니까요)

Child child = new Child();

parent.childList.add(child);

parent.childList.remove(child);

t님의 작성한 로직은 이런 방식으로 수정해야 합니다.

도움이 되셨길 바래요.

0

늦게 답변드려 죄송합니다.

해결 도와주셔서 정말 감사합니다.

em.persist() 호출로 인해 저장과 삭제 순서가 꼬여 발생하는 것이었군요.

정말 친절한 답변 감사드립니다!

 

주신 코드에서 궁금한 점이 있는데,

(다른 강의지만,) mappedBy로 연결된 list는 강의 때 "mappedBy 단어 그대로 mapping이 되어있을 뿐 연관관계의 주인이 아니기 때문에 이 변수를 변경하여도 DB에 반영되지 않는다."라고 말씀주셨던 것 같은데, 위 테스트코드에선 mappedBy의 변수를 변경함으로써 제거하신 것 같아서요.!

여기서는 어떻게 변경이 된 것인지, 원래 변경을 해도 되는 변수인지 궁금합니다. 그리고, Parent.childList는 지웠지만 Child.parent는 지우지 않으셨는데, 이 부분은 (DB반영 말고도 실무에서) 문제가 없을까요?

 

그리고 cascade를 사용하면 관리의 주체가 리포지토리가 아닌 엔티티로 해야한다고 말씀주신 부분 이해했습니다. repository에서는 완전히 신경끄고 그 부분을 엔티티에게 넘겨야하군요.

선생님 덕분에 설계에 대해 또 한 번 배웁니다. 감사합니다.

그렇다면 parent에 연결된 특정 child를 지우기 위해서는 어떻게 해야하나요?

저 위에서 제가 em.persist()를 호출했던 이유는 생성된 OrderItem의 id를 넘겨주기 위해서였는데,

만약 save없이 변경감지에 의존하여 엔티티에서 해결해야 한다면 id 부여가 되지 않을텐데, 생성했음을 알릴 때 id를 넘겨줄 수 없기도 하고 특정 OrderItem 삭제를 원할 때 어떤 OrderItem인지 어떻게 찾을 수 있나요?

위에 적어주신 게시물-첨부파일을 예시로 들면, 게시물에 첨부파일 3개를 업로드한 뒤에 2번 첨부파일을 지우기 위한 요청을 보냈다고 하면 DELETE를 id와 함께 보내주어야할 것 같아서요.! 

내부 코드에서는 index로 관리할 수 있지만, 서비스에서 OrderItem 삭제를 보낼 때엔 그 pk id가 있어야한다고 생각을 했었습니다.

 

질문이 길어 질문 부분에 하이라이트 하였습니다.

답변 주시면 감사하겠습니다!

0

아하, 코드를 확인해야 문제를 찾을 수 있군요.

다른 사람 코드 읽는게 굉장히 시간 소모적인 것을 알기에, 제가 말씀드린 상황이 담긴 함수를 강의예시 프로젝트에 얹어서 최소한의 수정으로 설명과 함께 전달드리겠습니다.

강의 예시는 갖고 계실테니 바로 합칠 수 있도록 diff 뜬 것도 따로 전달드리겠습니다.

 

도와주셔서 감사합니다!

주말 내로 보내드리겠습니다.

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. t님

자세히 설명을 해주셨지만, 트랜잭션부터 여러가지 상황들이 겹칠 수 있어서, 정확한 문제의 파악을 위해서 실제로 동작하는 코드가 필요합니다.

전체 프로젝트를 압축해서 올려주세요.

그리고 테스트 할 수 있는 방법과 테스트 코드안에서 주석으로 어떤 부분을 어떻게 기대했는데, 기대한 바와 어떻게 다른지 남겨주세요^^. 테스트 코드는 문제의 핵심을 딱 집어서 최소한으로 작성 부탁드릴게요.

감사합니다.

0

혹시나 해서 연관관계 끊는 함수도 단위테스트 해보았는데, 정상 작동 합니다. 처음은 orphanRemoval이 안 되는 것에 대한 질문이었지만, 알아보다보니 다음 질문으로 바뀐 것 같습니다.

persist 후에 연관관계 맺어 추가된 child에 대해서는 orphanRemoval이 작동하지 않는다.
(Order - OrderItem 예시로 들면, 강의에서는 OrderItem을 미리 만들고 Order에 추가한 후 영속화하였지만, 영속화 후에 OrderItem을 추가할 경우 orphanRemoval이 작동하지 않습니다.)

이렇게 정리가 되네요. 위의 요약본을 더 요약한 한줄요약입니다.

왜 이런지 아직 이해를 못 한 상황입니다..

0

CASCADE 관련 질문이 많았나 보군요 ㅎㅎ

그런데 위 링크는 제 질문과 많이 다른 질문 같습니다.ㅠㅠ

 

제 상황이 잘 전달이 되지 않는 것 같아 짧게 요약드리자면.. 제 상황은 이렇습니다.

1. CascadeType.ALL로 묶여있음.

2. orphanRemoval = true

3. child와 연관관계 매핑 후 persist를 통해 영속화한 parent의 경우, child와 연관관계를 지움으로써 delete query 정상 발생
     // pseudo code
     persist(new Parent(child))

4. child 없이 parent 선 persist 후에 나중에 child와 연관관계를 맺은 후 추가로 parent를 persist를 한 경우엔 (이미 managed entity), child와 연관관계를 지워도 delete query 발생하지 않음 (대신 연관관계를 지워서 fk=null로 바꾸는 update 발생)
     // pseudo code
     persist(new Parent())
     persist(parent.addChild(child))

5. 상황3, 4번에서 모두 child와의 연관관계를 지웠기 때문에 em.remove(child)으로 삭제 가능 [위 링크와 다른 점]. 하지만, orphanRemoval을 통해 지우는 것은 불가능.

6. 4번의 경우 dirty checking으로 update되는 상황이라 생각 (child는 persist하지 않았지만, 실제로 child에 parent fk 잘 받아옴.). 하지만 상황3, 4번이 각각 다른 결과를 내는 것을 보아하니 두 작업이 다른 것을 확인.

 

두 작업이 다른 것은 이제 구분이 되는데, 왜 다른지, 어떻게 다른지가 궁금합니다.

디테일한 상황은 바로 직전 질문을 확인해주시면 감사하겠습니다.. 모두 요약한 내용입니다.

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. t님

다음 주머니 입니다. ㅎㅎ

https://www.inflearn.com/questions/56718

예제 코드까지 있으니 한번 확인해보세요^^

0

링크 속 질문하신 분은, CascadeType.ALL, CascadeType.PERSIST로 하면 되는데 그 밖은 안 되는 상황에서의 질문인데, 저는 상황이 달라 이미 CascadeType.ALL 옵션을 썼지만 되지 않는 상황이었습니다.

코드 흐름이 달라 보이지 않은데 전 CascadeType.ALL로 하였을 때 왜 안 될까 싶어, 코드를 계속 살펴보고 여러 가지 시도 해보면서 상황을 조금 좁혀나가 보았습니다.

결론은, 저는 parent를 영속화한 후 child를 추가하는 상황이 있었고, 그런 상황일 때에 orphanRemoval이 작동을 하지 않았습니다.

 

위 설명주신 게시글-첨부파일 관계가 있다고 가정을 했을 때,

public class Post {
    // ...
    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<AttachedFile> files = new ArrayList<>();
}

public class AttachedFile {
    // ...
    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;
}

Post를 file 없이 생성하고 혼자 영속화한 뒤, 나중에 file을 생성하여 Post와 연관관계를 맺어줄 때 아래와 같이 추가해줬습니다.

// 이렇게 그대로 작성하진 않았지만, 이 두 개 모두 연결했다는 의미로 작성합니다.
post.files.add(attachedFile);
attachedFile.setPost(post);

추가한 후에는 Post쪽에서 save를 (persist를) 해줬습니다.

저는 이렇게 할 경우 CASCADE로 묶여 있어서 dirty checking이 알아서 추가해주리라 생각하며, 영속화한 후에 추가해도 다르지 않을 것이라 생각했는데 orphanRemoval이 되지 않습니다..

이렇게 하니, (post를 처음 persist할 때 추가하지 않은) attachedFile은  fk인 board_id는 잘 받아오지만, orphanRemoval이 되지 않더라구요.

 

오늘 하루종일 이거만 보고있는데, 아직 구글링으로도 답을 못 구했습니다.

혹시 이유를 알 수 있을까요?

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요 t님^^

마침 비슷한 질문을 주신 분이 있네요.

다음 내용을 읽어보시면 답이 나오실거에요.

https://www.inflearn.com/questions/137740

0

친절히 알려주셔서 정말 감사합니다!

 

위 질문은 전부 이해했습니다만, 아직 제가 이해가 안된 부분이 조금 있습니다.

제일 처음에 드린 질문 상황에서 orphanRemoval = true로 하고, 연관관계를 null로 없애도 삭제가 되지 않더라구요. (첫 질문에 리스트 되어있는 4개 모두 적용한 상태입니다.)

위에서 말씀주신대로 em.remove()로 직접 삭제하면 되긴하지만, orphanRemoval을 계속 말씀주시는 것으로 보아 원래 정상적으로 연관관계를 끊으면 삭제되어야 하는 것 같아서요!

OrderItem에 연결되어 있는 연관관계는 Order와 Item 뿐인 것 같은데 (orphanRemoval 옵션이 들어간 Order와의 연관관계만 끊으면 돼야 하는 것 같은데), 어떤 부분이 문제일까요?

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. t님

설계 의도가 cascade + orpahnRemoval까지 모두 제공하게 되면, 부모 엔티티인 게시판에서 모든 것을 컨트롤 하는 것을 의도한 것입니다. 따라서 게시판 엔티티에서 자식인 첨부파일을 빼기만 하면 자동으로 삭제가 됩니다. 이미 이런 의도를 가지고 설계를 했기 때문에 다른 곳에서 삭제 기능을 또 제공하는 것은 잘못되었다기 보다는, 한번은 더 생각해볼 필요가 있습니다.

감사합니다.

0

설계상 Repository가 필요 없을지 잘 생각해보고, 개인 소유되는 것에 한 해 정말 필요없을 때에만 CASCADE를 사용한다는 말씀이군요.

친절한 답변 정말 감사합니다!

그렇다면, 위 답변에서 게시물에 속한 첨부파일를 예시로 들 때, 첨부파일이 완전히 게시물 소유에 Repository도 필요없을 것 같아 CASCADE로 묶어버렸는데, 나중에 게시물 삭제 없이 첨부파일만 삭제하기 위해 게시물Repository 내에 em.remove(첨부파일)과 같은 행동을 하는 함수를 넣는 것은 잘못된 설계인가요?

위 설계가 잘못된 설계인지, em.remove(첨부파일)과 같이 직접 첨부파일을 건드려야 하는 일이 있다면 첨부파일Repository 로 무조건 빼내야하는지 궁금합니다.

0

답변 감사합니다.

CASCADE로 묶여있을 때엔, 부모가 지워지면 자식도 지워질 뿐이지, 자식 연결고리를 끊는다고 더티체킹이 지워주지는 않는다는 말씀이군요. 감사합니다.

 

그리고 주신 링크에서, "완전히 개인 소유"일 때에만 CASCADE로 묶으라고 알려주셨는데, Delivery를 묶지 않은 이유는 이 것은 Order를 통하지 않고도 추가되거나 변경될 수 있어서 인가요?

OrderItem은 묶고 Delivery를 묶지 않은 기준이 다른 곳에서 참조될 수 있는 점 때문이라고 말씀주셨는데, Delivery의 참조가 단순 "reference"의 의미라면 단순 read 작업인데 왜 프로젝트가 확장되면 CASCADE 옵션을 달면 안 되는지 궁금합니다.

 

그리고 예시로 들어주신 게시물 첨부파일도 만약 수정 중에 업로드된 것을 지우고 싶을 때 따로 em.remove(attached)을 호출해야한다면, CASCADE를 사용하는 목적이 무엇인가요? (제가 이해하기로는 완전히 종속된 개념이라 추가/수정/제거가 모두 부모를 따르기 때문이라고 생각했는데, 첨부파일도 부모와 별개로 지울 수 있는 상황이 있어 제가 이해한 개념이 아닌가 싶은 생각에 질문드립니다.)

 

고운 코끼리님의 프로필 이미지
고운 코끼리

작성한 질문수

질문하기