릴레이션(관계형 데이터베이스)를 객체(도메인 모델)로 매핑 하려는 이유?
- 객체 지향 프로그래밍의 장점을 활용할 수 있다.
- 이를 통해, 비즈니스 로직 구현 및 테스트 구현이 편리함
- 각종 디자인 패턴 사용하여 성능 개선 가능
- 코드 재사용
-> 하지만 이렇게 사용하기 위해서는 해결해야하는 문제점들이 다수 존재한다.
ORM이 해결해야하는 문제점과 해결책
상속의 문제
- 객체 : 객체간에 멤버변수나 상속관계를 맺을 수 있다.
- RDB : 테이블들은 상속관계가 없고 모두 독립적으로 존재한다.
- 해결방법 : 매핑정보에 상속정보를 넣어준다. (@OneToMany, @ManyToOne)
관계 문제
- 객체 : 참조를 통해 관계를 가지며 방향을 가진다. (다대다 관계도 있음)
- RDB : 외래키(FK)를 설정하여 Join 으로 조회시에만 참조가 가능하다. (즉, 다대다는 매핑 테이블 필요)
- 해결방법 : 매핑정보에 방향정보를 넣어준다. (@JoinColumn, @MappedBy)
-> 양방향 참조인지, 단방향 참조인지를 구분해주는 것
-> 단방향으로 등록하면 다른 필드에서 조회를 하지 못하도록 할 수 있다.
탐색 문제
- 객체 : 참조를 통해 다른 객체로 순차적 탐색이 가능하며 콜렉션도 순회한다.
- RDB : 탐색시 참조하는 만큼 추가 쿼리나, Join 이 발생하여 비효율적이다.
- 해결방법 : 매핑/조회 정보로 참조탐색 시점을 관리한다.(@FetchType, fetchJoin())
-> lazy냐 eager이냐를 구분해주어서 바로 연관된 객체를 불러오느냐, 아니면 필요할 때 불러오느냐를 구분해줄 수 있어서, 불필요한 시간, 리소스 낭비를 막을 수 있다.
밀도 문제
- 객체 : 멤버 객체크기가 매우 클 수 있다.
- RDB : 기본 데이터 타입만 존재한다.
- 해결방법 : 크기가 큰 멤버 객체는 테이블을 분리하여 상속으로 처리한다. (@embedded)
-> 다른 Type (UserRole 같이 유저가 만들어둔) 을 사용할 경우
식별성 문제
- 객체 : 객체의 hashCode 또는 정의한 equals() 메소드를 통해 식별
- RDB : PK 로만 식별
- 해결방법 : PK 를 객체 Id로 설정하고 EntityManager는 해당 값으로 객체를 식별하여 관리 한다.(@Id, @GeneratedValue)
-> RDB는 PK로만 식별하니까 Id를 설정하고 그것이 GeneratedValue 같이 자동적으로 증가하도록 하여 구분할 수 있다.
ORM이 얻은 최적화 방법
1차, 2차 캐시

-
1차 캐시
- 영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소가 있는데 이를 1차 캐시라고 한다.
- 일반적으로 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다.
- 1차 캐시는 한 트랜잭션 계속해서 원본 객체를 넘겨준다.
-
2차 캐시
- 애플리케이션 범위의 캐시로, 공유 캐시라고도 하며, 애플리케이션을 종료할 때 까지 캐시가 유지된다.
- 2차 캐시는 캐시 한 객체 원본을 넘겨주지 않고 복사본을 만들어서 넘겨준다.
- 복사본을 주는 이유는 여러 트랜잭션에서 동일한 원본객체를 수정하는일이 없도록 하기 위해서이다.
- 2차캐시 적용방법
영속성 컨텍스트(1차 캐시)를 활용한 쓰기지연
- 영속성이란? 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성을 말한다. 영속성을 갖지 않으면 데이터는 메모리에서만 존재하게 되고 프로그램이 종료되면 해당 데이터는 모두 사라지게 된다. 그래서 우리는 데이터를 파일이나 DB에 영구 저장함으로써 데이터에 영속성을 부여한다.
-> 정리하자면 , 일반적으로 프로그램을 실행하고 데이터를 저장하면 메모리내에서만 저장이 되기 때문에 프로그램이 종료되면 사라짐 (휘발성). 근데 이를 영구적으로 데이터베이스에 저장하게 되면 프로그램이 종료되어도 남아있다. (비휘발성)

- 영속성 4가지 상태 (
비영속 > 영속 > 준영속 | 삭제)
- 비영속(new/transient) - 엔티티 객체가 만들어져서 아직 저장되지 않은 상태로, 영속성컨텍스트와 전혀 관계가 없는 상태
- 영속(managed) - 엔티티가 영속성 컨텍스트에 저장되어, 영속성 컨텍스트가 관리할 수 있는 상태
- 준영속(detached) - 엔티티가 영속성 컨텍스트에 저장되어 있다가 분리된 상태로, 영속성컨텍스트가 더 이상 관리하지 않는 상태
- 삭제(removed) - 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제하겠다고 표시한 상태
- Raw JPA 관점
- persist(), merge() >
(영속성 컨텍스트에 저장된 상태) > flush() > (DB에 쿼리가 전송된 상태) > commit() > (DB에 쿼리가 반영된 상태)
-> 예전에 flush랑 commit의 차이점을 정리했을 때에 commit이 flush를 담고 있어서 더 큰 범위라고만 했었는데, flush는 db에 쿼리를 전송하였고 그게 db의 transaction에 아직 있는 상태이며 그 transaction에서 db로 쿼리를 반영하는 것이 commit이다.
- 쓰기 지연이 발생하는 시점
- flush() 동작이 발생하기 전까지 최적화한다.
- flush() 동작으로 전송된 쿼리는 더이상 쿼리 최적화는 되지 않고, 이후 commit()으로 반영만 가능하다.
-> flush 동작이 실행된다면 애플리케이션쪽이 아니라 db 쪽의 transaction으로 쿼리를 전달한 상태이기 때문이다.
- 쓰기 지연 효과
- 여러개의 객체를 생성할 경우 모아서 한번에 쿼리를 전송한다.
- 영속성 상태의 객체가 생성 및 수정이 여러번 일어나더라도 해당 트랜잭션 종료시 쿼리는 1번만 전송될 수 있다.
- 영속성 상태에서 객체가 생성되었다 삭제되었다면 실제 DB에는 아무 동작이 전송되지 않을 수 있다.
- 즉, 여러가지 동작이 많이 발생하더라도 쿼리는 트랜잭션당 최적화 되어 최소쿼리만 날라가게된다.
-> db에 접근을 최소화해서 시간을 절약하려는 의도가 있는 듯...
-> but, 키 생성전략이 generationType.IDENTITY 로 설정 되어있는 경우 생성쿼리는 쓰기지연이 발생하지 못한다.
-> why? 단일 쿼리로 수행함으로써 외부 트랜잭션에 의한 중복키 생성을 방지하여 단일키를 보장한다.
-> 쓰기 지연을 사용하는 이유는 외부 트랙잭션에서 똑같은 값에 접근 할 경우, 데이터 혼동이 일어날 수 있기 때문인데, 단일키를 보장한다면 중복 데이터에 접근할 수 없기 때문으로 추측...