1. 패러다임의 혁명과 JPA의 핵심 구조
JPA(Java Persistence API)는 객체지향 패러다임(자바)과 관계형 데이터베이스(SQL) 사이의 거대한 벽을 허물기 위해 탄생한 ORM 기술입니다. 그 중심에는 데이터베이스와 애플리케이션 사이의 완충 지대인 영속성 컨텍스트(Persistence Context)가 있습니다.
JPA 캐시 레이어의 이중 구조
JPA는 메모리 효율과 데이터 조회 성능을 극대화하기 위해 캐시 시스템을 1차 캐시와 2차 캐시라는 두 개의 레이어로 쪼개어 관리합니다.
- 1차 캐시 (L1 Cache - 개인 바구니)
- 소속: 영속성 컨텍스트 내부.
- 생명 주기: 하나의 트랜잭션(혹은 하나의 웹 요청)이 시작될 때 생성되고, 끝나면 즉시 파괴됨.
- 구성품: 1. 1차 캐시 방: 엔티티의 최신 상태 객체를 보관.
- 스냅샷 방: DB에서 처음 조회한 순간의 원본 상태를 찍어둔 사진첩 (변경 감지용).
- 2차 캐시 (L2 Cache - 공유 보관소)
- 소속: 애플리케이션 전체 (스프링 컨테이너 레이어).
- 생명 주기: 서버가 켜질 때 같이 가동되며, 서버가 꺼질 때까지 영구 유지됨.
- 특징: 모든 사용자와 모든 트랜잭션이 이 공간을 공유함. 1차 캐시가 파괴되어도 2차 캐시에 데이터가 남아있으면 DB까지 네트워크 통신을 하러 가지 않고 메모리에서 데이터를 고속 복사해올 수 있음.
2. @Transactional의 프록시 구동 메커니즘
우리가 서비스나 컨트롤러 메서드 위에 적는 @Transactional은 자바 고유 기능이 아닌, 스프링 AOP(프록시 기술)의 결정체입니다.
① ApplicationContext의 빈(Bean) 생성 공정 라인
- 순수 객체 생성:
ApplicationContext가 구동되면 개발자가 짠 순수한 자바 객체를 먼저new합니다. - 의존성 주입: 필드에 필요한 의존성(DI)을 채워 넣습니다.
- 빈 후처리기 발동 (BeanPostProcessor): * 최종 검수원이 객체를 훑다가
@Transactional을 발견하면, CGLIB 라이브러리(도구)를 가동합니다.- 원본 객체를 내부에 쏙 품고 있는 거대한 가짜 프록시 클래스 전체를 통째로 복사해서 생성합니다.
- 최종 빈 등록: 이 껍데기가 씌워진 가짜 프록시 객체가
ApplicationContext바구니에 진짜 ‘빈(Bean)’으로 최종 등록됩니다.
② @Transactional 환경에서의 생명 주기
[ @Transactional 메서드 호출 ] ➔ 1. 가짜 프록시 문지기가 요청을 가로챔
│
▼
2. 트랜잭션 매니저 가동 ➔ DB 커넥션 풀(HikariCP)에서 커넥션을 획득하고 setAutoCommit(false) 선언
│
▼
3. 영속성 컨텍스트(L1 캐시) 개방 ➔ 획득한 DB 커넥션을 1차 캐시 상자와 결합(바인딩)
│
▼
4. 비즈니스 로직 수행
- findById() 실행 시: 2차 캐시를 먼저 훑고 없으면 DB에 가서 데이터를 긁어와 '1차 캐시 방'에 올리고 '스냅샷' 촬영.
- user.changeName() 실행: 레포지토리를 안 부르고, 오직 자바 메모리 상의 '1차 캐시 객체' 값만 변경.
│
▼
[ 메서드 정상 종료 ] ➔ 제어권이 다시 바깥의 '가짜 프록시 객체'에게 반환됨
│
▼
5. JPA Flush(플러시) 발동 ➔ 1차 캐시 객체와 처음 찍은 스냅샷 사진을 전수 대조 (변경 감지 - Dirty Checking)
- 이름이 바뀐 것을 포착하면 즉시 UPDATE SQL을 조립해 DB 네트워크 망으로 밀어 넣음.
│
▼
6. 최종 물리 커밋 (connection.commit()) ➔ DB에 물리적으로 완전 저장 후 1차 캐시 바구니 폭파 및 커넥션 반납
3.트랜잭션 없이 .save()를 호출할 때의 메커니즘
① 메커니즘 흐름
- 임시 바구니 개방:
save()메서드가 실행되는 그 찰나의 순간에만 임시 영속성 컨텍스트(1차 캐시)가 아주 잠깐 만들어집니다. - 2차 캐시(L2) 체킹: 임시 1차 캐시는 이 데이터가 애플리케이션 공유 보관소(2차 캐시)에 있는지 확인합니다. 있으면 복사해오고, 없으면 진짜 물리 DB 네트워크 망을 타고 데이터베이스로 내려갑니다.
- 쿼리 발송: 데이터를 가져와 임시 1차 캐시에 올린 뒤, 변경 사항을 확인하고 즉시 DB에
INSERT또는UPDATE쿼리를 쏘아 보냅니다 (Flush & Commit). - 2차 캐시 저장: 물리 DB에 저장이 완료되면, 이 최신 데이터를 애플리케이션 공유 보관소(2차 캐시)에도 따끈따끈하게 저장해 둡니다.
- 임시 바구니 파괴:
save()메서드의 중괄호(})를 탈출하는 순간, 임시로 만들었던 1차 캐시 바구니는 흔적도 없이 파괴됩니다.
② 최종 결론 및 트랜잭션의 유무 차이
- 트랜잭션이 없을 때 (
save()수동 호출): * 1차 캐시를 경유하긴 하지만, 메서드가 실행될 때마다 매번 임시 바구니를 새로 만들고 파괴합니다. -
따라서 여러 번 수정하면 매번 물리 DB와 2차 캐시를 찔러야 하므로 네트워크 비용이 비대해집니다. 반환된 객체는 즉시 감시자가 없는 준영속 상태가 되어 그 밑줄부터는 자바 값을 바꿔도 DB에 전혀 반영되지 않습니다.
- 트랜잭션이 있을 때 (
@Transactional울타리): - 거대한 1차 캐시 바구니가 메서드 시작부터 끝까지 쭉 유지됩니다.
- 자바 코드로 백번 천번 값을 바꾸더라도 DB에 네트워크 통신을 하지 않다가, 마지막 종료 시점에 딱 한 번만 2차 캐시와 물리 DB를 갱신하므로 최적화와 성능 면에서 압도적으로 유리합니다.