자바 ORM 표준 JPA 프로그래밍에 대한 후기를 오늘 적어보고자 한다, 이번 책을 기준으로 총 5권의 책을 완독했는데 앞으로 남은 책들 중 Real MySQL을 제외한 Docker, K8S 및 코드 컨벤션 같은 책들은 비교적 가볍게 읽을 수 있을 거 같아서 이제 마지막 한 계단이 남은 기분이다(착각일 수 있다), 실제로 책의 두께도 기존은 평균 800페이지에서 400페이지, 절반 분량으로 줄어들었기 때문에 비교적 쉽게 읽지 않을까 싶긴하다
스프링이 아닌 책
지난 6월달 처음 책을 읽기 시작하면서부터 총 5권의 책을 완독했고, 여태까지의 책들은 모두다 스프링과 관련된 책들이었다, 하지만 이번에 처음으로 스프링에서 벗어나 JPA와 관련된 책을 읽으면서 또 한번 나의 무지함을 느낄 수 있었던 시간이었다, 처음 토비의 스프링
을 읽었을 때도 현재 내가 만들고 있는 서비스에 대해서 더 깊게 생각해볼 수 있었고 실제로 여러 부분을 체크할 필요를 느꼈다,
그리고 토비의 스프링 VOl.2
와 실전 스프링 부트
를 읽었을 때는 처음 스프링관련 서적을 읽었을 때보다는 중복된 내용이 많았고, 덕분에 이 기술에 대한 나의 주관이 조금은 더 형성될 수 있었지만, 당장에 문제점을 찾기에는 다소 아쉬운 점이 있었다,

하지만, 스프링에서 벗어나 다른 기술에 대한 서적을 읽자마자 다시 한번 나의 문제점을 아주 많이 체크할 수 있었다, 위에 사진을 보면 알 수 있듯이 책을 읽으면서 고쳐야할 점들을 노션에 따로 정리하고 있는데, 아래 사진과 같이 어느순간 감당못할 정도로 체크리스트가 늘어나는 모습을 볼 수 있었고, 잡다하게 체크해야하는걸 다 합치면 200개 가량을 체크해야하는 상황이 되었다.

이번 자바 ORM 표준은 총 4.5만자 가량 정리를 했는데, 이번에 자바 ORM을 읽으면서 DB에 대한 지식이 정말 부족하다는걸 느낄 수 있어서 다음 책인 Real MySQL이 정말 많이 기대되고 있는 상황이다, 하지만 그만큼 다음책은 얼마나 필기를 해야할지를 생각해보면 벌써부터 아득한 거 같다,

후기
요약
이번 자바 ORM 책은 16장으로 구성되어 있어서 기존에 책 후기를 적던 방식대로 모든 챕터마다 느낀 점을 자세히 적다보면 오늘 포스팅이 매우 길어질 것이다, 때문에 모든 챕터를 요약해서 적을 예정이며, 여기서 내가 더 적고 싶은 부분들만 더 자세히 적을 예정이다, 나중에 블로그를 다시 만들 경우에 지금 기록한 내용들이 책 → 필기 → 디스코드 → 노션 → 블로그
로 엄청 여러차례 요약되는데 그냥 필기한 내용들을 하나도 빠짐없이 다시 한번 정리만 해서 포스팅해도 좋을 거 같다는 생각이 든다,
여튼, 이번에도 잡설이 많았다 항상 후기만 적고자 하지만 뜻대로는 안되는 거 같아서 (어차피 나중에 PR하기 위한 포스팅은 다 따로 할 예정이다), 그냥 이 책을 한번 읽어보고자 하는 사람한테 초점을 맞추어서 적는게 좋을 거 같기도하다, 이번 자바 ORM 책은 요약하자면 아래와 같다.
JPA의 토비의 스프링..?
이렇게 표현하는게 매우 조심스러운 부분이 있지만, 나에게는 이렇게 다가온 거 같다, 토비의 스프링과는 명확하게 다른 부분도 있지만, 어느정도 비슷한 부분도 있었던 거 같다, 예를 들어서 JPA란 무엇인가?
에 대해서 다시 한번 제대로 정립할 수 있었던 시간이었고, 내가 제대로 알지 못했던 부분들에 대해서 점검해주는 책이었으며, 기본적인 개념에 대해서 확실하게 외우는게 아닌 이해를 하고 넘어간다는 표현이 맞는 책이었다, 여튼 그랬다.
1장: JPA 소개
1장이다, 개발 서적은 항상 1장에서 해당 기술에 대한 소개를 하는 거 같다, 하지만 그 만큼 더욱 자세히 봐야하는 챕터다, 이 챕터를 집중해야지 앞으로의 여러 챕터에서 설명하는 내용들에 대해서 확실하게 이해를 할 수 있다, 왜냐하면 개발을 하다보면 여럿 느끼는거지만 개발자들마다 똑같은 거를 다른 명칭으로 얘기하곤 하는 거 같다, 예를 들어 변수를 필드, 멤버 변수, 멤버 어트리뷰트 등등.. 때문에 1장부터 제대로 집중하고 봐야지 오해를 하지 않고 내용을 이해할 수 있을 거다, 1장의 핵심은 아래와 같다.
- 패러다임 불일치
- ORM(Obejct-Relational Mapping)
- JPA(Java Persistence API)
애플리케이션은 발전하면서 그 내부의 복잡성도 점점 커진다, 만약 부모 객체를 상속받거나 다른 객체를 참고하고 있을 경우 객체의 상태를 저장하기 쉽지 않다, 자바는 이런 문제까지 고려해서 객체를 파일로 저장하는 직렬화 기능과 파일을 객체를 복구하는 역 직렬화 기능을 제공한다, 하지만 이 방법은 직렬화 된 객체를 검색하기 어렵다는 문제 떄문에 현실성이 없다,
현실적인 대안은 관계형 DB에 객체를 저장하는 것인데 관계형 DB는 데이터 중심으로 구조화 되어 있고 집합적인 사고를 요구한다, 그리고 OOP에서 이야기하는 추상화나 상속, 다형성 같은 개념도 없다, 객체와 관계형 DB는 지향하는 목적이 다르므로 둘의 기능과 표현 방법도 다르다,
이것을 객체와 관계형 DB의 패러다임 불일치 문제라고한다, 그리고 이 문제를 개발자가 해결하기 위해서는 많은 자원이 필요하고 떄문에 ORM과 JPA같은 기술들을 통해 해결이 가능하다,
- ORM (Object-Relational Mapping)
- 객체와 관계형 데이터베이스를 매핑하는 기술
- 객체지향 프로그래밍과 관계형 데이터베이스 간의 패러다임 불일치 문제 해결
- 생산성 향상과 유지보수성 개선
- JPA (Java Persistence API)
- 자바 진영의 ORM 기술 표준
- 애플리케이션과 JDBC 사이에서 동작하며 개발 생산성과 유지보수성 향상
- 특정 데이터베이스에 종속되지 않는 방언(Dialect) 기능 제공
2장: JPA 시작
2장은 JPA에서 사용하는 주된 기술들을 설명한다, 예를 들어 스프링 IoC 컨테이너는 엔티티 매니저, 스프링 컨텍스트는 영속성 컨텍스트와 비슷한 느낌이고 스프링의 빈은 JPA에서는 엔티티라고 명칭하는 느낌이다, 2장에서 핵심되는 내용은 아래와 같다
- 엔티티 매니저 팩토리 (EntityManagerFactory)
- 엔티티 매니저 (EntityManager)
- JPQL (Java Persistence Query Language)
엔티티 매니저 팩토리는 이름 그대로 엔티티 매니저를 만드는 공장이다, 공장을 만드는 비용을 상당히 크기 떄문에 한 개만 만들어서 애플리케이션 전체에 공유 되도록 설계되어 있다, 반면에 공장에서 엔티티 매니저를 생성하는 비용은 거의 들지 않는다, 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드간에 공유해도 되며 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하기에 절대 스레드간 공유를 하지 않도록 주의하자.
- JPQL (Java Persistence Query Language)
- 엔티티 객체를 대상으로 쿼리하는 객체지향 쿼리 언어
- SQL을 추상화해서 특정 데이터베이스에 의존하지 않음
- 엔티티 객체를 대상으로 하므로 대소문자를 구분해야 함
3장: 영속성 관리
3장은 좀 많이 중요한 개념들이 들어가 있다, 핵심 키워드는 아래와 같다.
영속성 컨텍스트는 엔티티를 영구 저장하는 환경이라는 의미다, 엔티티 매니저로 저장하거나 조회하면 에닡티 매니저는 영속성 컨텍스트에 엔티티를 보관 관리한다, 여러 엔티티 매니저가 같은 영속성 컨텍스트에 접근할 수 있다, 즉 영속성 컨텍스트는 엔티티를 저장하는 환경인데 이때 여러 기능(1차 캐시, 동일성 보장, 변경 감지, 지연 로딩 등)을 제공하는데 이 기능들이 핵심이므로 잘 이해하자,
그리고 책을 읽다보면 비영속, 영속, 준영속, 삭제 상태가 있는데 삭제가 있는데 준영속이 굳이 왜 있는건가 하는 생각이 들었는데
, 유저를 DB에서 지우는게 아닌 Active 상태를 비활성화해서 관리하는 경우가 있는데 이와 비슷하게 준영속 상태를 사용하면 자원을 효율적으로 관리하는 전략 중 하나이기에 이 또한 성능을 최적화하기 위해서 만든 것이다, 그러면 또 의문이 들 것이다 삭제는 그럼 왜 있는거지?
아예 필요없어지는 경우 DB에서 가지고 있는 것도 적절한 대처는 아닐 것이다,
그리고 법적인 요구사항 등 여러 문제로 아예 데이터를 지워야 하는 이유는 아주 많다.
- 플러시 (Flush)
- 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 과정
- 뜻 그대로 반영한다는 거다, 1차 캐시에 저장된 엔티티가 쓰기 지연 SQL 저장소에 등록되고 그 쿼리를 데이터베이스에 전송하게 되는데 이때 SQL 저장소에 등록된 내용이 지워지는게 아닌 동기화
되는 거다, 생각보다 중요한 개념이니 기억하는게 좋을 거 같다
- 변경 감지, 수정된 엔티티 쓰기 지연 SQL 저장소에 등록, 쿼리를 데이터베이스에 전송
- 트랜잭션 커밋 시 자동으로 호출되거나 개발자가 직접 호출 가능
4장: 엔티티 매핑
4장은 기본적으로 쓰이는 매핑 어노테이션에 대한 설명이 주된 내용이니 간단히 요약하겠다,
- @Entity
- JPA가 관리할 엔티티 클래스임을 지정
- 기본 생성자 필수, final 클래스, enum, interface, inner 클래스에는 사용 불가
- 저장할 필드에 final 사용 불가
- @Table
- 엔티티와 매핑할 테이블 지정
- 생략 시 엔티티 이름을 테이블 이름으로 사용
- schema, catalog, uniqueConstraints 등의 속성 제공
- @Id
- 엔티티의 기본 키를 지정
- 자동 생성 전략(@GeneratedValue)과 함께 사용 가능
- 복합 키의 경우 @IdClass나 @EmbeddedId 사용
5장: 연관관계 매핑 기초
5장부터는 내가 실제로 겪은 문제나 의문들에 대한 내용을 주로 다루는게 더 좋아보인다,
객체에는 양방향 연관관계라는 것이 없다, 서로 다른 단방향 연관관계 2개를 애플리케이션에서 잘 묶어서 양방향인 것처럼 하는거다, 반면 DB 테이블은 외래키 하나로 양쪽이 서로 조인할 수 있다, 따라서 테이블은 외래 키 하나만으로 양방향 연관관게를 맺는다
- 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래키는 하나라서 둘 사이에 차이가 발생한다
- DB 동기화 문제, 일관성 유지에 어려움, 개발자 혼란, 연관관계 주인과 비주인의 역할같은 문제가 발생한다
- 때문에 JPA는 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리한다, 그리고 이것이 연관관계 주인이다
- 연관관계의 주인만이 DB 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제) 할 수 있다, 반면에 주인이 아닌 쪽은 읽기만 할 수 있다 (주인은 mappedBy 속성을 사용하지 않는다)
- 연관관계 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다
- DB 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다
- 연관관계의 주인은 테이블의 외래 키가 있는 곳으로 정해야한다
- 만약 Team과 Member가 있을 경우 Team_id는 member가 가지고 있고 이때는 Team_id는 외래키다
- member는 여러개이기에 Team이 외래키를 가지고 있어도 여러 member에 join이 안된다
- 때문에 주인은 member다 -> 보통 OneToMany 관계면 다 쪽이 연관관계의 주인이다
- 연관관계의 주인만이 외래 키의 값을 변경할 수 있다
- 그리고 객체관점에서 양쪽 방향 모두 값을 입력해주는게 안전하다
- JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생한다
그리고 나는 이번 챕터를 참고해서, 현재 내 코드의 연관관계의 주인이 제대로 잘 이루어지고 있는지 다시 한번 점검할 수 있는 시간을 가졌다,
6장: 다양한 연관관계 매핑
Q. 간단한게 ManyToMany를 매핑하는 방법이 있는데 왜 굳이 복합 키를 써서 매핑하는거지?
자동으로 만들어진 연결 테이블은 실무에 적절하지 않아서 그런건가?
A. 복합 키를 사용하는 이유는 관계의 유니크함을 보장하고, 데이터 무결성을 유지하며, 실무에서 필요로 하는 다양한 추가 속성을 관리하기 위함이다. 자동으로 생성된 연결 테이블은 간단한 경우에는 유용하지만, 실무에서는 이를 대체하는 명시적인 설계가 필요할 때가 많다. 따라서 복합 키를 사용하거나 대리 키와 연결 엔티티를 사용하는 방법을 선택하게 된다,
Q. 그럼 그냥 연결 엔티티를 만들어도 되지 않는건가? 이게 너무 무식한 방법이라서 그런가?
(231 Page) 대리키, 엔티티를 만들어서 관리하는 거 이거와 관련된 내용이다
A. 오히려 연결 엔티티를 그냥 만드는걸 더 선호하는 경우도 있다, 하지만 데이터의 일관성을 더 중요시 하거나 저장 공간에 효율성을 더 따지거나 특정 쿼리에 대한 성능이 향상되는 등 복합키를 쓰는게 더 좋은 부분도 있지만 명확한 단점이 복잡하다는거다.
7장: 고급 매핑
이장에서 다룰 고급 매핑은 다음과 같다
- 상속관계 매핑
: 객체의 상속 관계를 DB에 어떻게 매핑하는지 다룬다
- @MappedSuperClass
: 등록일, 수정일 같이 여러 엔티티에서 공통으로 사용하는 매핑 정보만 상속받을 때 쓴다
- 복합키와 식별 관계 매핑
: DB의 식별자가 하나 이상일 때 매핑하는 방법을 다룬다, 그리고 DB 설계에서 이야기하는 식별관계와 비식별 관계에 대해서도 다룬다
- 조인 테이블
: 테이블은 외래 키 하나로 연관관계를 맺을 수 있지만 연관관계를 관리하는 연결 테이블을 두는 방법도 있다, 여기서는 이 연결 테이블을 매핑하는 방법을 다룬다
- 엔티티 하나에 여러 테이블 매핑하기
: 보통 에니티 하나에 테이블 하나를 매핑하지만 엔티티 하나에 여러 테이블을 매핑하는 방법도 있다, 여기서는 이 매핑방법을 다룬다
DB 테이블의 관계는 외래 키가 기본 키에 포함되어 있는지에 따라 식별 관계와 비식별관계로 구분한다
- 식별 관계는 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계다
- 비식별 관계는 부모 테이블의 기본 키를 받아서 자식 테이블의 외래키로만 사용하는 관계다
- 외래 키가 Null을 허용하지 않으면 필수적 비식별관계이며, Null을 허용하면 선택적 비식별 관계다
- 객체 관계 관점에서 식별 관계의 단점(유연하지 않은 테이블 구조, 늘어나느 기본 키 컬럼 등) 때문에 비식별 관계를 선호한다
- 기본키 인덱스를 활용하기 좋고 특정 상황에선 하위 테이블로만 검색이 가능하다는 식별 관계의 장점이 있긴 있다
그리고 이번 챕터를 통해서 현재 내 코드의 관계를 한번 더 점검했는데, 대부분 비식별 관계로 연관관계를 설정하고 요구사항 역시 비식별 관계이지만 식별 관계로 해야하는 경우가 있는지 점검하는 시간을 가졌다,
8장: 프록시와 연관관계 관리
프록시와 즉시로딩, 지연로딩
- 객체는 객체 그래프로 연관된 객체들을 탐색한다, 하지만 객체가 DB에 저장되어 있기에 연관된 객체를 탐색하기 어렵다, 때문에 JPA 구현체들은 이 문제를 해결하기 위해서 프록시라는 기술을 쓴다, 프록시는 연관된 객체를 처음부터 DB에서 조회하는 것이 아니라 실제 사용하는 시점에 DB에서 조회할 수 있다
프록시의 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화 된다
- 프록시 객체를 초기화 한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다, 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다
- 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크시에 주의해서 사용해야 한다
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다
- 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다, 준영속 상태의 프록시를 초기화하면 문제가 발생한다
Q. 근데 프록시를 왜 쓰는거지? JPA이든 스프링이든
A.
- JPA
: 지연 로딩, 성능 최적화, 의존성 관리 및 트랜잭션 관리
- 스프링
: AOP, 트랜잭션 관리, 의존성 주입
- 즉, 프록시를 사용하면 연관된 객체를 처음부터 DB에서 조회하는 것이 아니라 실제 사용하는 시점에 DB에서 조회할 수 있다
- 메모리 최적화, 실제 객체의 접근을 제한하고 추가적인 로직 적용(로깅, 접근 권한, 스프링이랑 비슷한 이유), 일관된 방식으로 처리가 가능하다 (인터페이스 느낌)
Q. CascadeType.all + orphanRemoval = true를 동시에 사용하면 어떻게 될까?
A. 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다
자식을 저장하려면 부모에 등록만 하면 된다, 자식을 삭제하려면 부모에서 제거하면 된다.
9장: 값 타입
JPA의 데이터 타입을 가장 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있다
- 엔티티 타입은 @Entity로 정의하는 객체이고
- 값 타입은 Int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 말한다 그리고 값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다, 따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다
- 그리고 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다
- 임베디드로 타입을 정의하면 타입이 자바의 기본 타입이 아니라 객체 타입이다
- 자바는 기본 타입에 값을 대입하면 값을 복사하지만 객체에 값은 대입하면 항상 참조값을 보낸다
10장: 객체지향 쿼리 언어
JPQL은 엔티티 객체를 조회하는 객체지향 쿼리다, 문법은 SQL과 비슷하고 ANSI 표준 SQL이 제공하는 기능을 유사하게 지원한다
- JPQL은 SQL을 추상화해서 특정 DB에 의존하지 않는다
- 그리고 데이터베이스 방언만 변경하면 DB 변경을 쉽게 할 수 있다
- JPQL은 SQL보다 간결하다, 엔티티 직접 조회, 묵시적 조인, 다형성 자원으로 SQL 보다 코드가 간결하다
- Criteria 쿼리 소개
- Criteria는 JPQL을 생성하는 빌더 클래스다
- Criteria의 장점은 문자가 아닌 query.select(m).where() 처럼 프로그래밍 코드로 작성한다
- 하지만 문자기반 쿼리가 아닌 장점도 있지만 단점도 있다, 아래는 장점이다
- 컴파일 시점에 오류를 발견할 수 있다, 동적 쿼리 작성이 편하다, IDE 로 코드 자동 완성을 지원한다
- 하지만 복잡하고 장황해서 사용이 어렵고 가독성도 많이 떨어진다
- QueryDSL 소개
- Criteria 처럼 JPQL 빌더 역할을 한다
- 코드 기반이면서 단순하고 사용하기 쉽다는 장점이 있다
- 작성한 코드도 JPQL과 비슷해서 한 눈에 들어온다
- 네이티브 SQL 소개
- JPA는 SQL을 직접사용할 수 있는 기능을 지원하는데, 이것을 네이티브 SQL 이라 한다
- 오라클의 Connect By과 같이 표준화 되어있지 않은 기능을 네이티브 SQL을 쓰는거다
- 단점은 특정 DB에 의존하는 SQL을 작성해야 한다는 것이다, 따라서 DB 변경 시 네이티브 SQL도 변경해야한다
- JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용
- 잘 안 쓰니깐 적당히 알자, DB와 영속성 컨텍스트 동기화 문제가 플러시를 안해서 발생할 수 있다
페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회해서 SQL 호출 횟수를 줄여 성능을 최적화 할 수 있다 @OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략
- 위 처럼 엔티티에 직접 적용하는 전략은 애플리케이션 전체에 영향을 미치므로 글로벌 로딩 전략이라 부른다
- 그리고 페치 조인은 글로벌 로딩 전략보다 우선된다
- 글로벌 로딩 전략은 될 수 있으며 지연로딩은 사용하고 최적화가 필요하면 페치 조인 적용이 효과적이다
- 또한 페치 조인을 사용하면 연관된 엔티티를 쿼리 시점에 조회하므로 지연로딩이 발생하지 않는다, 따라서 준영속 상태에서도 객체 그래프를 탐색할 수 있다
Q. 지연 로딩이 발생하지 않아야지 객체 그래프 탐색이 가능한건가?
A. 객체 그래프 탐색을 효과적으로 수행하려면 지연 로딩을 피하고 필요한 연관 객체들을 미리 로딩해야 한다.
11장: 웹 애플리케이션 제작
온라인 강의에 있는 내용과 동일한 내용이다, 여태까지 학습한 내용을 토대로 웹 애플리케이션을 제작하는 챕터이므로 생략.
12장: 스프링 데이터 JPA
스프링 데이터 JPA가 제공하는 공통 인터페이스는 SimpleJPARepository 클래스가 구현한다
- @Repository 적용
: JPA 예외를 스프링이 추상화한 예외로 변환한다
- @Transactional 트랜잭션 적용
: JPA의 모든 변경을 트랜잭션안에서 이루어져야함
- 스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용하면 메소드에 @Transactional로 트랜잭션 처리
- 서비스 계층에서 트랜잭션을 시작했으면 리포지토리로 해당 트랜잭션을 전파받아서 그대로 사용
- @Transactional(readOnly = true)
: 데이터를 조회하는 메소드에는 readOnly = true 옵션 적용
- readOnly = true 옵션 사용 시 플러시를 생략해서 약간의 성능 향상이 있다
- save() 메소드
: 저장할 엔티티가 새로운 엔티티면 저장하고 이미 있는 엔티티면 병합한다
- 새로운 엔티티를 판단하는 기본 전략은 엔티티의 식별자로 판단하는데 식별자가 객체일 때 NUll, 자바 기본 타입일 땐 숫자 0값이면 새로운 엔티티로 판단한다
- 필요하면 엔티티에 Persistable 인터페이스를 구현해서 판단 로직을 변경할 수 있다
13장: 웹 애플리케이션과 영속성 관리
스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다
- 이 전략은 이름 그대로 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다는 뜻이다
- 즉, 트랜잭션을 시작할 때 영속성 컨텍스트의 생존 범위가 같다는 뜻이다
- 그리고 같은 트랜잭션 안에서 항상 같은 영속성 컨텍스트에 접근한다

- 스프링 프레임워크를 사용하면 보통 비즈니스 로직을 시작하는 서비스 계층에서 @Transactional 애노테이션을 선언해서 트랜잭션을 시작한다
- 이 어노테이션이 있으면 호출할 메소드를 실행하기 직전에 스프링의 트랜잭션 AOP가 먼저 동작한다
- 스프링 트랜잭션 AOP는 대상 메소드를 호출하기 직전에 트랜잭션을 시작하고, 대상 메소드가 정상 종료되면 트랜잭션을 커밋하면서 종료한다
- 이떄 중요한 일이 일어나는데 트랜잭션을 커밋하면 JPA는 먼저 영속성 컨텍스트를 플러시해서 변경 내용을 DB에 반영한 후 DB 트랜잭션을 커밋한다
- 따라서 영속성 컨텍스트의 변경 내용이 DB에 정상 반영된다
- 만약 예외가 발생하면 트랜잭션을 롤백하고 종료하는데 이때는 플러시를 호출하지 않는다

OSIV (open Session In View)는 영속성 컨텍스트를 뷰까지 열어둔다는 의미이다
- 영속성 컨텍스트가 살아 있으면 엔티티는 영속 상태로 유지된다, 그럼 뷰에서도 지연 로딩을 쓸 수 있다
- OSIV는 하이버네이트에서 사용하는 용어다, JPA에서는 (Open EntityManager In View)인데 관례상 OSVI이라고 하기로 했다
- 가장 단순한 OSIV 구현 방법은 클라이언트의 요청을 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝나는 것이다 (요청 당 트랜잭션)

- 요청당 트랜잭션 방식의 OSIV 문제점
- 컨트롤러나 뷰 같은 프레젠테이션 계층이 엔티티를 변경할 수 있다
- 뷰로 렌더링 한 후(만약 고객 이름을 XXX로 변경해서 뷰에 렌더링 해야할 경우) 트랜잭션을 커밋한다, 그리고 영속성 컨텍스트를 플러시하고 변경 감지 기능 때문에 DB에 XXX이름이 동기화 되는 문제가 발생한다
- 프레젠테이션 계층에서 엔티티를 수정하지 못하게 막는 방법은 다음과 같다
- 엔티티를 읽기 전용 인터페이스로 제공
- 엔티티 래핑
- DTO만 반환
Q. 스프링에서 OSIV를 사용해야 하는 실무적인 경우는 어떤게 있을까?
A.
- 뷰 렌더링 시 지연 로딩
: 컨트롤러나 뷰에서 연관 엔티티의 지연 로딩이 필요한 경우
예: 상품 목록에서 각 상품의 카테고리 정보를 표시할 때
- REST API에서의 연관 데이터 처리
: API 응답에 연관 엔티티 정보를 포함해야 할 때
예: 주문 정보 조회 시 관련된 고객 정보도 함께 반환
- 복잡한 도메인 모델
: 여러 계층의 연관 관계가 있는 복잡한 도메인 모델을 다룰 때
예: 대학 시스템에서 학생-수업-교수 관계를 탐색할 때
- 성능 최적화가 필요하지 않은 간단한 애플리케이션:
데이터베이스 연결 비용보다 개발 생산성이 더 중요한 경우
- 레거시 시스템 유지보수
: OSIV를 사용하는 기존 시스템을 큰 변경 없이 유지해야 할 때
14장: 컬렉션과 부가 기능
이 장에서 다루는 내용은
- 컬렉션
: 다양한 컬렉션과 특징을 설명한다
- 컨버터
: 엔티티의 데이터를 변환해서 DB에 저장한다
- 리스너
: 엔티티에서 발생한 이벤트를 처리한다
- 엔티티 그래프
: 엔티티를 조회할 떄 연관된 엔티티들을 선택해서 함께 조회한다
JPA는 자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원하고 다음 경우에 이 컬렉션을 사용할 수 있다
- @OneToMany, @ManyToMany를 사용해서 일대다나 다대다 엔티티 관계를 매핑할 때
- @ElementCollection을 사용해서 값 타입을 하나 이상 보관할 떄
컨버터를 사용하면 엔티티의 데이터를 변환해서 DB에 저장할 수 있다
- 컨버터 클래스는 @Converter 애노테이션을 사용하고 AttributeConverter 인터페이스를 구현해야한다, 그리고 제네릭에 현재 타입과 변환할 타입을 지정해야한다, 여기서는 <Boolean, String>을 지정해서 Boolean 타입을 String 타입으로 변환한다 (621page)
모든 엔티티를 대상으로 언제 어떤 사용자가 삭제를 요청했는지 모든 로그를 남겨야하는 요구사항이 있을 경우에 삭제 로직을 하나씩 찾아서 로그를 남기는 것은 매우 비효율적이다
- 그래서 JPA 리스너 기능을 사용하면 손 쉽게 엔티티의 생명주기에 따른 이벤트 처리가 가능하다

- 이벤트 적용 위치
- 엔티티에 직접 적용
- 별도의 리스너 등록
- 기본 리스너 사용
- 여러 리스너를 등록했을 떄 이벤트 호출 순서는 다음과 같다
- 기본 리스너
- 부모 클래스 리스너
- 리스너
- 엔티티
15장: 고급 주제와 성능 최적화
- 예외처리 : JPA 사용 시 발생하는 다양한 예외아 예외에 따른 주의점을 설명할 예정
- 엔티티 비교 : 엔티티를 비교할 떄 주의점과 해결 방법을 설명한다
- 프록시 심화 주제 : 프록시로 인해 발생하는 다양한 문제점과 해결 방법을 다룬다
- 성능 최적화:
- N + 1 문제
: 한 번의 쿼리가 아닌 상당히 많은 쿼리가 발생하는 문제
- 읽기 전용 쿼리의 성능 최적화
: 엔티티를 단순히 조회만 할 경우 영속성 컨텍스트에 스냅샷을 유지할 필요도 없고, 플러시할 필요도 없다, 엔티티를 읽기 전용으로 쿼리할 때 성능 최적화 방안을 다룬다
- 배치 처리
: 수백만 건의 데이터를 처리해야하는 배치 처리 상황에 JPA 에서 어떻게 다루는지 알아본다
- SQL 쿼리 힌트 사용
: 하이버네이트를 통해 SQL 쿼리 힌트를 사용하는 방법을 다룬다
- 트랜잭션을 지원하는 쓰기 지연과 성능 최적화
즉시 로딩은 사용하지 않고 지연로딩을 사용하여 N + 1 문제를 해결하는걸 권장한다
- 즉시 로딩은 생각보다 필요하지 않은 데이터를 로딩하는 등 성능 최적화가 어렵다는 문제가 있다
- 따라서 모두 지연로딩으로 설정하고 성능 최적화가 꼭 필요한 곳에는 JPQL 페치 조인을 사용하는게 좋다
- JPA의 글로벌 페치 전략 기본값은 다음과 같다
- @OneToOne, @ManyToOne : 기본 페치 전략은 즉시 로딩이다
- @OneToMany, @ManyToMany : 기본 페치 전략은 지연 로딩이다
- 따라서 기본 값이 즉시 로딩인 @OneToOne과 @ManyToOne은 Fetch = FetchType.Lazy로 설정해서 지연로딩 전략을 사용하도록 변경하자
Q. 근데 이부분에서 기본 값이 지연로딩인 일대다 다대다인 엔티티에도 명시적으로 애노테이션을 부과했는데, 명시적으로 적어두는게 나쁜건가?
A. 기본 전략이라도 페치 전략을 명시적으로 적는 것은 코드의 가독성, 유지보수성, 일관성을 높이는 좋은 실천이라고 할 수 있다.
16장: 트랜잭션과 락, 2차 캐시
- 트랜잭션 락 : JPA가 제공하는 트랜잭션과 락 기능을 다룬다
- 2차 캐시 : JPA가 제공하는 애플리케이션 범위의 캐시를 다룬다
애플리케이션에서 공유하는 캐시를 JPA는 공유 캐시라하는데 일반적으로 2차 캐시라고도 부른다,
- 2차 캐시는 애플리케이션 범위의 캐시다, 따라서 애플리케이션을 종료할 떄까지 캐시가 유지된다,
- 분산 캐시나 클러스터링 환경의 캐시는 애플리케이션보다 더 오래 유지될 수도 있다,
- 2차 캐시를 적용하면 엔티티 매니저를 통해 데이터를 조회할 때 우선 2차 캐시에서 찾고 없으면 데이터베이스에서 찾는다,
- 2차 캐시를 적절히 활용하면 데이터베이스 조회 횟수를 획기적으로 줄일 수 있다

- 영속성 컨텍스트는 엔티티가 필요하면 2차 캐시를 조회한다
- 2차 캐시에 엔티티가 없으면 DB를 조회해서
- 결과를 2차 캐시에 보관한다
- 2차 캐시는 자신이 보관하고 있는 엔티티를 복사해서 반환한다
- 2차 캐시에 저장되어 있는 엔티티를 조회하면 복사본을 만들어 반환한다
- 2차 캐시는 동시성을 극대화하려고 캐시한 객체를 직접 반환하지 않고 복사본을 만들어서 반환한다
- 만약 캐시한 객체를 그대로 반환하면 여러 곳에서 같은 객체를 동시에 수정하는 문제가 발생할 수 있다
- 이 문제를 해결하기 위해선 객체에 락을 걸어야 하는데 이렇게 하면 동시성이 떨어질 수도 있다
- 락에 비하여 객체를 복사하는 비용은 아주 저렴하다, 따라서 2차 캐시는 원본 대신에 복사본을 반환한다
- 2차 캐시의 특징은 다음과 같다
- 2차 캐시는 영속성 유닛 범위의 캐시다
- 2차 캐시는 조회한 객체를 그대로 반환하는 것이 아니라 복사본을 만들어서 반환한다
- 2차 캐시는 데이터베이스 기본 키를 기준으로 캐시하지만 영속성 컨텍스트가 다르면 객체 동일성 (a == b)를 보장하지 않는다
끝으로
12장부터는 조금 심화되는 주제들이 나왔는데, 이 부분부터는 실전 스프링 부트
와 비슷한 느낌으로 "이 문제를 해결 하기 위해서 이 기술을 써도 된다" 라는 느낌이기에 당장에 내 코드를 점검해야 하는 부분도 당연히 있었지만 그보다는 이런게 있다는 사실을 인지하는 정도를 목표로 읽었다.
그리고 포스팅을 하면서 매번 겪는 고민 중 하나가 내가 책을 단순히 읽는거에서 끝이 아닌 실제로 코드에 적용하고자 노력했다는 걸 PR 하고 싶지만, 이렇게 책 후기에서는 그걸 담기에는 포스팅도 길어질 뿐더러 이후에 포스팅을 수정하는 과정을 거쳐야지만 가능한 거 같아서 다소 아쉽다는 느낌이 들었다,
하지만 이번 포스팅을 통해서 알게된 점은 이 글을 읽는 사람들에게 저가 이 만큼 열심히 책을 읽었습니다 !
라는 느낌으로 글을 쓰고 있다는 사실인데, 이게 무슨 의미가 있는건가 싶다는 생각이 들었다, 왜냐하면 포스팅을 통해서 다시 한번 내가 적어놓은 것들을 정독하고 아 맞다 이런게 있었지
라는 느낌으로 복기하고 그걸 실제로 코드에 계속해서 적용하고 점진적으로 발전하면 굳이 내가 이런 학습을 했다는걸 알릴 필요가 없어지는날이 오지 않을까 하는 생각이다,
여튼, 빨리 Real MySQL도 완독하고 싶다, 기대된다 아직 챕터1밖에 안 읽긴했지만.

감사합니다