엔티티 필드 중 컬렉션 타입도 프록시의 일종은 컬렉션 래퍼(PersistentBag)로 교체된다.
final 키워드도 의미가 없다. 컬렉션 래퍼인 프록시로 교체되어야 하기 때문에 사용 안 하는 것이 좋다. 그리고 JPA 철학이 엔터티를 그대로 영속화한다는 개념이기 때문에. final로 선언된 필드가 테이블의 컬럼이라고 생각하면 final 은 더욱 어울리지 않는다.
JPA에서 지원하는 컬렉션은 Collection, Set, List, Map이 있다.
Collection: 최상위 컬렉션. 하이버네이트는 중복 허용, 순서 보장 x
Set: 중복 X, 순서 X
List: 중복 O, 순서 O
Map: Key, Value 형태. Map은 복잡한 매핑에 비해 활용도가 떨어지고 다른 컬렉션을 사용해도 충분하다.
고아 객체 Orphan
JPA에서 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 Orphan 고아라고 부른다.
orphanRemoval 편의 기능: 부모에서 자식과의 연관관계를 끊으면 자식이 DB에서 까지 삭제하는 편의기능. 그리고 부모 엔티티가 삭제되면 자식 엔티티들도 다 같이 삭제된다. (현재 테스트해보니 컬렉션에서 삭제해도 자식 객체들이 삭제가 안 되는 버그가 있다. Cascade와 함께 써야 삭제가 되는 것 같다)
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY, orphanRemoval = true)
private final List<Student> students = new ArrayList<>();
@Test
void 부모_삭제로_고아_삭제2() {
// Given
teamRepository.deleteById(축구팀.getId());
// When
List<Long> 축구팀_학생들_아이디들 = 축구팀.getStudents().stream().map(Student::getId).collect(Collectors.toList());
List<Student> students = studentRepository.findAllById(축구팀_학생들_아이디들);
// Then
assertThat(students).hasSize(0);
}
PersistContext와 트랜잭션
트랜잭션에 따라 PersistContext 라이프사이클이 결정된다.
트랜잭션 A는 독립된 PersistContext A를 가진다.
EntityManager A,B가 있더라도, 한 트랜잭션이면 PersistContext가 하나이다.
EntityManager가 1개가 있고 쓰레드가 2개이고 각각 트랜잭션이 있다면, PersistContext 2개이다.
이렇게 만들어진 이유는 트랜잭션을 확실히 보장하기 위함이다. 만약 EntityManager에 따라 PersistContext가 된다면, 여러 EM을 사용하는 비즈니스 로직 같은 경우에 트랜잭션을 보장하기 힘들 것이다.
준영속성 상태
트랜잭션이 종료된 엔티티는 영속 상태에서 비영속 상태로 변환된다.
비영속 상태에서는 지연 로딩 Fetch Lazy가 작동을 하지 않는다. 영속 상태가 아니라서 PersistContext가 존재하지 않기 때문.
트랜잭션이 끝나도 페치만 가능하도록 readOnly만 가능하도록 스프링에서 제공하는 OSIV(Open Session In View) 기능을 제공한다.
다른 방법으로는 Controller와 Service Layer 사이에 View에 필요한 페치만 하도록 Facade Layer 를 추가한다. 단점은 Layer를 하나 더 개발 및 관리해야 하기 때문에 큰 규모가 아니면 효율이 떨어진다.
Facade Layer 역할과 특징
프리젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성 분리
프리젠테이이션 계층에서 필요한 프록시 객체 초기화
리포지토리를 직접 호출해서 뷰가 요구하는 엔티티 조회
Facade Layer
스프링 OSIV
기존 OSIV 는 요청 당 트랜잭션을 생성해서 요청이 끝날 때 트랜잭션도 끝나는 방식이다. 하지만 Controller에서 엔티티 수정이 가능하다는 위험성이 있다.
그래서 스프링 프레임워크가 제공하는 OSIV는 Service 레이어까지만 트랜잭션을 해두고, PersistContext는 소멸시키지 않고 남겨둬서 해당 엔티티들은 영속 상태로 유지한다. 이 PersistContext는 ReadOnly로. 엔티티 수정은 트랜잭션에서만 가능하다.
주의할 점은 트랜잭션 보다 PersistContext 범위가 넓게 되면, 여러 트랜잭션이 PersistContext를 사용할 수 있게 된다. 그러면 그 중 한 개의 트랜잭션이 롤백 시, PersistContext 가 같이 clear 되는지 확인해야 한다. 다행히 스프링에서는 한 개의 트랜잭션이라도 롤백하면 PersistContext를 clear 하도록 한다.
프레젠테이션 계층에서 엔티티를 수정되고 엔티티 그대로 비즈니스 계층으로 그대로 넘어가면 DB에 반영될 위험이 있다.
통계와 같은 복잡한 View 는 DTO와 JPQL을 쓰는 것이 좋다.
스프링 OSIV
API 전략
외부 API: 외부에 노출. 한 번 정의하면 변경이 어렵다. 서버와 클라이언트를 동시에 수정하기 어렵다. 예시) 타 팀과 협업하기 위한 API, 타 기업과 협업하는 API.
내부 변경사항이 외부에 반영되지 않도록(의존성을 낮추도록) DTO를 사용하는 것이 좋다.
내부 API: 외부에서 노출하지 않는다. 언제든 변경할 수 있다. 서버와 클라이언트를 동시에 수정할 수 있다. 예시) 같은 프로젝트에 있는 화면을 구성하기 위한 AJAX 호출
엔티티와 클라이언트를 함꺼번에 같이 수정할 수 있기 때문에, 엔티티를 직접 노출해도 괜찮다고 생각.