안녕하세요. 이번 게시글에선 JPA 영속성 전이에 대해 분석해보고
특히 복합키로 구성된 DB구조에서 어떻게 영속성 전이를 처리할지 알아보겠습니다.
“영속성 콘텍스트”는 무엇일까요?
영속성 컨텍스트(Persistence Context)는 JPA에서 엔티티(Entity) 인스턴스를 관리하는 환경을 말합니다.
이 환경은 영속성을 관리하고 엔티티의 생명주기를 추적하는데 사용됩니다.
“영속성”이라는 개념을 실현시키기 위해 어플리케이션 안에서 구체화된 것이 “영속성 컨텍스트”입니다.
엔티티의 생명주기를 추적하고, 엔티티의 상태를 데이터베이스와 일치시키는 영속성을 관리하는 기술적 “환경“입니다.
어플리케이션 환경에서 구체적으로 표현해보면 위와 같이 그려볼 수 있습니다.
정리하자면 영속성 컨텍스트는 크게 두가지 작업을 한다고 볼 수 있겠습니다.
- 엔티티의 생명주기를 추적
- 다양한 기능으로 엔티티의 상태를 데이터베이스와 일치시킴
또한 주의해야할 점으로 어플리케이션 안에는
영속성 컨텍스트의 관리를 받는 엔티티와
영속성 컨텍스트의 관리를 받지 않는 엔티티가 별개로 존재할 수 있다는 점입니다.
이는 이번 게시글을 작성하게 된 주요 이유인데요. 이에 대해 추후 설명드리도록 하겠습니다.
먼저, 엔티티의 생명주기에 대해 좀 더 알아보겠습니다.
엔티티 생명주기 개념에 대해선 검색하면 많은 정보가 존재하므로.. 짧게 정리만 하겠습니다.
- 엔티티 생명주기 - 엔티티가 PK를 알고 있는지가 주요 구분 사항!!
- 비영속 (new)
- 엔티티가 막 생성되어 아직 PK를 모르는 상태
- PK 없음, 관리 안받음
- 영속 (managed)
- 엔티티가 PK를 새로 부여받거나 조회해와서 PK를 알고 있는 상태
- PK 있음, 관리 받음
- 준영속 (detached)
- 엔티티가 PK를 알고 있어서 영속 상태였지만, 영속성 컨텍스트의 관리가 끝난 상태
- PK 있음, 관리 안받음
- 삭제 (removed)
- 엔티티가 삭제된 상태
- PK 없음, 관리 안받음
- 비영속 (new)
엔티티의 생명주기를 통해 알 수 있는점은
오직 “영속“ 상태의 엔티티만 영속성 컨텍스트의 관리를 받게된다는 점입니다.
이는 영속 상태가 아닌 상태(비영속, 준영속, 삭제)에서는
영속성 컨텍스트의 다양한 기능(1차캐시, 쓰기지연, 더티체킹, 연관관계 탐색, 영속성 전이.. 등등)들을 적용 받을 수 없다는 것입니다.
그럼 가장 중요한 엔티티가 영속 상태가 되는 상황에 대해 그림와 예시코드를 보면서 알아보실까요?
일반적으로 Spring Data Jpa를 사용하는 경우에서
- find…()메소드를 호출하는 경우
- save()메소드를 호출하는 경우
조회 시, 저장 시 각각 엔티티가 영속화 되어집니다.
먼저 예시 엔티티를 보여드리겠습니다.
엔티티가 영속 상태가 되는 상황
1. find() 혹은 jpql을 호출하는 경우
- find()
2. jqpl 호출
find()는 영속성 컨텍스트에 먼저 엔티티 존재 여부를 판단 한 뒤, 없으면 엔티티를 조회해와서 영속화 시킵니다.
jpql의 경우, 영속성 컨텍스트를 건너 뛴 후, 엔티티를 조회해오고, 이후 영속성 컨텍스트에 존재여부를 판단합니다.
없다면 조회해온 엔티티를 영속화 시키지만, 이미 존재하는 엔티티라면 조회해온 엔티티를 버립니다.
이렇게 조회 과정에 대한 내부 작동 과정엔 약간의 차이가 있지만 jpa를 통해 “조회된 엔티티는 영속화되어 있다”라고 정리해볼 수 있습니다.
2. save() 호출하는 경우
parent 변수는 생성자로 생성되었을 당시 비영속(new)상태입니다.
아직 데이터베이스와 통신하지 않아 PK를 할당받지 않았기 때문입니다.
save()를 호출하는 순간 엔티티는 DB에서 PK를 할당받게 되는데
(여기엔 물리적인 예외가 있습니다. PK전략이 아이덴티티인 경우와 시퀀스를 사용하는 경우 작동방식이 다른데 이건 검색해보시는 것을 추천드립니다...)
그렇게 되면 위의 parent 변수를 사용중인 엔티티는 영속화되어 영속성 컨텍스트의 관리를 받게 되는 것입니다.
save()가 호출되기 전 parent변수의 pk인 id값은 null이므로 비영속 상태입니다.
save() 변수가 호출된 직후 parent 변수는 id값을 할당받아 영속성 컨텍스트에 관리를 받게 되었습니다.
parent변수의 해시코드 @12189로 같은 것을 볼 때 새로운 인스턴스가 생성되거나 하는 것이 아닌
우리가 new로 생성된 기존 비영속 엔티티를 영속 엔티티로 영속화 시킨 것임을 알 수가 있습니다.
그럼 아래의 코드는 어떻게 작동할까요? 우리는 save()를 통해 반환 값을 받을 수 있습니다.
parent와 save는 다를까요? 같을까요?
정답은 같다 입니다. 다른 참조변수를 사용하는 같은 엔티티입니다.
그럼 parent 변수와 save변수 중 아무거나 사용해도 영속성 컨텍스트의 기능을 사용할 수 있겠군요.
한 번 영속성 컨텍스트의 기능 중 영속성 전이 (Persistence Cascade)를 사용해볼까요?
영속성 전이는 부모 엔티티의 영속성을 자식 엔티티로 전파시키는 것을 의미합니다.
부모가 영속화 되었다면, 영속성 전이를 통해 별다른 작업을 하지 않고도 자식 또한 영속화가 되는 것이죠.
그럼 Parent엔티티의 영속성을 Child 엔티티로 전이시켜보겠습니다.
먼저 테스트가 실패하는 케이스를 보여드리겠습니다.
위의 테스트를 보시면 child 조회 시 실패를 하고 있네요.
하지만 parent는 찾아와서 동등성 판단도 통과하였습니다. 무엇이 다른 것일까요?
parent는 save()시점에 pk를 할당 받아 영속성 컨텍스트에 올려진 상태입니다.
또한 setParent()메소드를 통하여 자식 엔티티인 child 또한 영속성 전이가 발생하여야합니다.
하지만! 영속성 전이가 발생하기 위해선 flush가 발생하여야합니다. 위의 코드에선 아직 flush가 발생하지 않아 영속성 전이가 아직 발생하지 않았습니다..!
따라서 child의 pk 할당되지 않은 채로 findById()로 요청되어 null값을 요청하고 있다는 에러가 발생하였습니다.
그렇다면 이번엔 flush를 강제로 발생시키고 결과를 보겠습니다..!
테스트가 성공적으로 통과 되었습니다!
위의 코드에서 자식 엔티티를 영속화 시키는 작업을 명시적으로 호출하지 않았지만
영속성 전이 (Persistence Cascade)를 통하여 부모 엔티티의 상태를 자식 엔티티에 자동적으로 전파하였습니다.
위의 예시에서 부모 엔티티는 save()호출로 persist 상태가 되었으므로 자식 엔티티 또한 flush 발생시 persist 상태가 되어 insert 쿼리가 발생한 것입니다.
그럼 다른 환경의 예시로 복합키 구조인 경우를 봐볼까요?
@Entity
@Getter
@Table(name = "employee")
@NoArgsConstructor
@IdClass(EmployeeId.class)
public class Employee {
@Id
@Column(name = "nick_name")
private String nickName;
@Id
@Column(name = "name")
private String name;
public Employee(String nickName, String name) {
this.nickName = nickName;
this.name = name;
}
@OneToMany(fetch = FetchType.LAZY, mappedBy = "employee", cascade = CascadeType.PERSIST)
List<EmployeeDetail> employeeDetails = new ArrayList<>();
}
@Entity
@Getter
@Table(name = "employee_detail")
@NoArgsConstructor
@IdClass(EmployeeDetailId.class)
public class EmployeeDetail {
@Id
@Column(name = "nick_name")
private String nickName;
@Id
@Column(name = "name")
private String name;
@Id
@Column(name = "email")
private String email;
@Column(name = "content")
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumns({
@JoinColumn(name = "nick_name", insertable = false, updatable = false),
@JoinColumn(name = "name", insertable = false, updatable = false)
})
private Employee employee;
public EmployeeDetail(String nickName, String name, String email) {
this.nickName = nickName;
this.name = name;
this.email = email;
}
//연관관계 편의 메소드
public void setEmployee(Employee employee) {
if (this.employee != null) {
this.employee.getEmployeeDetails().remove(this);
}
this.employee = employee;
employee.getEmployeeDetails().add(this);
}
}
위의 상황처럼 영속성 전이 테스트가 복합키 환경에서도 동일하게 작동할까요?
이전에 보았던 성공 예시와 동일하게
트랜잭션 중간 flush()를 호출하여 영속성 전이가 발생할 것을 예상해보았습니다.
테스트가 통과할까요?
이런 ! 테스트가 통과하지 않았습니다.
무엇이 문제였길래 영속성이 전이되지 않았던 것일까요?
직접 Jpa 내부 소스코드를 분석해보겠습니다..!
SimplaJpaRepository.save()를 보시게 되면,
엔티티의 상태를 판단하여 파라미터로 받은 엔티티를 그대로 리턴하거나
새로운 엔티티를 별개로 생성하여 리턴하는 두가지 리턴 방식이 존재합니다.
Jpa는 엔티티의 상태를 어떻게 판단하고 있을까요?
AbstractEntityInfomation.isNew(T entity) 를 보시면 해당 엔티티의 PK값 존재 유무로
엔티티의 상태가 new 인지, detach인지 구분하고 있는 것을 볼 수가 있습니다..!!
detach는 원래대로라면 영속성 컨텍스트에 올려졌다가 영속 상태가 끝난 엔티티의 상태인데
이를 판단하기 위해 Jpa는 엔티티의 PK유무를 판단하는 군요!!
복합키 기반 엔티티 예시에선, 엔티티가 영속성 컨텍스트에 올려지지 않았는대도 불구하고
PK값을 임의로 주입하였기 때문에 Jpa는 엔티티를 준영속 상태로 판한다고, 새로운 영속 엔티티를 생성하여 리턴하였습니다!
그렇다면 파라미터에 넣은 엔티티와 리턴 받은 엔티티는 다른 엔티티이고
리턴 받은 엔티티를 통하여 영속성 전이를 시도하면 되겠군요!!
예상한대로 테스트가 통과되었습니다!!
save()를 통해 영속화된 엔티티를 가지고 영속성 전이를 시도한 결과 테스트가 통과된 것을 확인해볼 수 있었습니다.
자, 이렇게 영속성 컨텍스트가 무엇인지에 대해서 알아보았고
엔티티의 생명주기에 대해서도 알아보았습니다.
또한 연관관계에 있는 엔티티의 생명주기가 전파되는 과정을 영속성 전이 테스르를 진행하면서 예시코드로 살펴보았습니다.
특히 Jpa가 save() 메소드 호출 시 엔티티의 상태에 따라 리턴값의 인스턴스가 달라지는 점,
엔티티의 상태를 판단하는 isNew() 메소드는 엔티티의 PK보유 유무를 판단한다는 점 내부 소스를 뜯어보며 알아보는 재밌는 시간이었습니다 !!
참고) 복합키 엔티티 설정 시 insertable=false, updatable=false 에 관한 궁금증
흐음! 복합키 상황에서 좀 더 알아보고 싶은 점이 생겼는데요
insertable=false, updatable=false 설정. 이것은 무엇일까요??
아래는 자식엔티티인 EmployeeDetail에서의 부모 관계 필드인데요.
무엇인가 insertable=false, updatable=false라는 표현이 연관관계 인서트, 업데이트를 막는 설정인가..?
싶을 수도 있습니다. 하지만 우리는 방금 위에서 연관관계를 통한 영속성 전이를 성공시켰습니다.
그렇다면 이 설정은 무엇일까요?
위의 공식문서를 참고해보면
이 설정은 단순히 JPA가 쿼리를 생성할 때 해당 필드를 SQL 칼럼으로 추가할지 안할지에 대한 여부입니다
INSERT 쿼리 생성 시, UPDATE 쿼리 생성 시 해당 칼럼에 대한 쿼리는 생성하지 않겠다는 명시입니다.
그런데 조금 헷갈립니다.
우리는 연관관계를 사용하므로 nick_name 칼럼과 name 칼럼에 대한 정보를 insert 및 update 시키는데 어떻게 된 일 일까요?
복합키를 사용하는 구조에서 헷갈릴 수 있는 부분입니다.
하지만 해당 설정은 단순히 sql쿼리를 생성시킨다는 개념으로 접근하면 이해가 쉽습니다.
insertable=false, updatable=false로 해당 필드를 통한 쿼리 생성은 막혔지만
복합키 선언을 한 곳의 동일한 필드는 아직 남아있기 때문이죠!
//..
public class EmployeeDetail {
@Id
@Column(name = "nick_name")
private String nickName;
@Id
@Column(name = "name")
private String name;
//..
애초에 @IdClass 방식의 복합키 설정 방식에서는 엔티티 필드로 설언할 칼럼이 중복하여 선언됩니다.
따라서 해당 insertable=false, updatable=false 이라는 설정은
쿼리 생성 시 연관관계에 있는 외래키를 칼럼을 SQL문에 포함 시킬 것인지 아닌지에 대한 설명이므로
영속성 전이 설정과는 별개의 설정으로 정리해볼 수 있습니다!!
참조)
https://velog.io/@yundleyundle/JPA-%EC%A4%80%EC%98%81%EC%86%8D
https://sjiwon-dev.tistory.com/14
https://arahansa.github.io/docs_spring/jpa.html#jpa.entity-persistence.saving-entites
'백엔드 > JPA' 카테고리의 다른 글
JPA 양방향 연관관계 매핑 시, 순수 객체와 엔티티 작동 차이점 (0) | 2024.03.16 |
---|---|
복합키 JPA 사용 시 연관관계 및 1차 캐시 주의 사항 - 2 (0) | 2024.01.13 |
복합키 JPA 사용 시 연관관계 및 1차 캐시 주의 사항 (2) | 2024.01.06 |