본문 바로가기

Spring

Spring - JPA

꼭 제대로 정리하고 싶었던 Spring 에서의 JPA (Java Persistence API)에 대한 포스팅을 해보겠습니다. 그럼 시작!

 


1. JPA란?

JPA 는 자바 애플리케이션에서 객체(Object)와 관계형 데이터베이스(RDB)를 매핑하기 위한 표준 API 입니다. JPA 는 DB 테이블과 자바 클래스를 일일이 SQL 로 연결하여 쿼리 등을 보내 DB와의 연락을 주고 받는 것이 아니라, 자바 객체만 다뤄도 자동으로 SQL 이 실행되는 ORM 기반 기술입니다.

 

이게 무슨말인지 정의에 사용된 용어부터 차근차근 알아보겠습니다.

 

우선 ORM 은 Object-Relational Mapping 의 줄임말로, 객체와 관계형 데이터베이스를 연결하는 프로그래밍 기술을 의미합니다.

 

이 ORM 을 이해하기 쉽게 풀어보겠습니다.

원래는 유저 정보중 '전화번호'라는 데이터가 있으면, DB 테이블의 전화번호 부분을 조회하는

 

'SELECT 전화번호 FROM 유저'

 

와 같은 방식으로 쿼리를 요청하고 데이터를 조회할 수 있습니다. 같은 방식으로 수정이나 삭제, 등록 등을 할 수 있습니다.

 

하지만 ORM은 같은 목적으로 행동을 하지만, Java 의 객체 자체를 DB 테이블과 매핑 하여 객체를 다루기만 해도 DB에 Create, Read, Update, Delete 등을 수행할 수 있는 기술입니다. 다음으로 JPA 를 보겠습니다.

 

JPA 의 P 는 Persistence(영속성)를 의미합니다. '영속성' 이라는 말을 봐도 일상생활에서 잘 사용하지 않으니 직관적으로 이해되지 않을 수 있습니다. 하지만 어느정도 느낌으로 유추가 가능합니다.

영원히 유지된다, 지속된다

 

정도의 느낌인 것 같습니다.

 

이 말이 어떻게 JPA 라는 개념을 완성하는지 문장을 만들어 보면, Java에서 데이터를 메모리가 아닌 저장소(디스크 등)에 지속적으로 남아있게 하는 API 라고 정의할 수 있습니다.

 

윗 문장에서 '데이터를 ~ 남아있게' 하는 이라는 부분은 제가 이해하기 쉽게 문장으로 표현해본 것인데 JPA 라는 단어만 봤을 땐 너무 설명이 불친절한 용어이지 않나 싶습니다.

 

위의 내용을 종합하면 JPA 는 데이터를 DB에 영구적으로 Create, Read, Update, Delete 하게 하는 기술인데 그 방식이 객체 그 자체를 DB 와 매핑하여 자동으로 쿼리를 보내게 하는 API 이다 라고 생각할 수 있겠습니다.

(설명을 위해 영구적이라는 단어를 넣었지만 사라지지 않는다라는 개념이 아니라 메모리에 잠깐남아있는 개념이 아니라는 뜻으로 이해해주시면 감사하겠습니다!)

 

코드로 간단한 예시를 보면 아래와 같습니다.

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;
}

 

Member 라는 클래스를 정의하고 `@Entity` 어노테이션을 붙이게 되면 이 자바 클래스가 관계형 DB 테이블과 매핑 됩니다. 이떄 Table 에 들어가야하는 `@Id` 부분은 꼭 선언해주어야 합니다.

위의 코드를 저장 후 실행하면 DB에 아무런 테이블이 존재하지 않아서 JPA의 구현체인 Hibernate 가 `INSERT INTO MEMBER ~` 라는 SQL을 만들어서 DB에 보내게 됩니다.


2. JPA 의 등장 배경

JPA 가 등장하기 전, 전통 방식으로 Java 애플리케이션과 DB 를 연결하던 JDBC가 있습니다.

JDBC 는 SQL 쿼리문을 직접 작성해야하고, 로직에 따라 중복된 코드가 많이 나오기도 하고 String 문자열로 쿼리를 작성하다 보니 런타임 시점에 에러가 발생하면 유지보수가 어렵다는 단점이 있었습니다.

 

이후 MyBatis 가 등장하면서 매핑 자체는 편하게 되었지만 여전히 SQL 을 직접 작성해야하는 단점이 있었고, 결과적으로 객체 지향적 코드와 관계형 DB 간의 간극이 존재하는 것은 어쩔 수 없었습니다. 이것을 해결하기위해 JPA 가 등장하게 되었습니다.

 

JPA 자체는 인터페이스로 개발되었고, 실제로 구현하는 구현체는 Hibernate 를 주로 사용합니다. 

 


3. JPA 의 동작 흐름

JPA 를 사용하여 CRUD 작업을 할 때, 동작의 흐름이 어떤지 자세히 살펴보겠습니다.

Member member = new Member("홍길동");
memberRepository.save(member);

 

위와같은 코드를 작성하여 member 라는 Member 타입 객체를 선언하여 이름을 초기화 해주고, '홍길동'이라는 새 이름으로 생성된 이 객체를 memberRepository 를 호출하여 save() 해주면 DB에 객체 자체의 정보를 보고 데이터를 저장하게 됩니다.

흐름을 순서대로 보겠습니다.

[개발자]
 ↓
Member 객체 생성
 ↓
EntityManager.persist(member)
 ↓
JPA가 SQL 생성 (INSERT INTO ...)
 ↓
JDBC API를 통해 DB에 SQL 전달
 ↓
DB에 데이터 저장

 

실제로는 이런 흐름으로 JPA 가 동작하는데, 여기서 앞서 설명했던 persist 라는 메서드가 눈에 들어오는군요. 이 부분에 대해서 설명하면, EntityManager 라는 데이터 관리자가 메서드를 사용하여 생성한 member 객체에 대한 데이터를 DB에 영속화 한다 라고 말할 수 있겠습니다.

 

이 부분에서 JPA 에서 상당히 중요한 부분인 '영속성 컨텍스트' 에 대해서 알아보겠습니다.


4. 영속성 컨텍스트?

영속성 컨텍스트란 JPA 에서 엔티티 객체를 저장하고 관리하는 일시적인 메모리 공간입니다. 쉽게 말하면, DB와 애플리케이션 사이에 있는 "중간 캐시" 역할을 하는 가상의 저장소입니다.

 

JPA 는 단순히 자바 객체 자체를 DB에 바로 보내버리는 것이 아니라, 그 객체를 트랜잭션 동안 메모리에서 일관성있게 관리하는 기능을 제공합니다. 영속성 컨텍스트가 하는 기능 덕분에 객체가 가지고 있는 데이터의 정합성을 보장할 수 있게 됩니다.

 

영속성 컨텍스트의 역할

  • 1차 캐시
  • 변경 감지
  • 쓰기 지연
  • 트랜잭션 단위의 정합성

위 네가지가 대표적인 역할입니다.

 

우선 간단한 코드로 예시를 보겠습니다.

@Transactional
public void updateMember(Long id) {
    Member member = em.find(Member.class, id); // 1차 캐시에 저장됨
    member.setName("바뀐이름");                // 변경 감지
    // 커밋 시점에 update SQL 자동 생성
}

 

여기서 `em.find()` 한 엔티티는 영속성 컨텍스트에 저장되고, 트랜잭션이 커밋될 때 `setName()` 으로 바뀐 값이 자동으로 DB에 반영됩니다.

1차 캐시

1차 캐시는 무엇이냐? 1차 캐시가 있으면 2차 캐시도 있고 3차캐시도 있는건가? 라는 생각이 드실수도 있을 것 같습니다. 왜냐하면 제가 처음 학습할 때 그랬었거든요..!

 

자꾸 이해를 위해 설명이 돌아가는 것 같지만 캐시를 위해 JVM 에 대한 이야기 부터 하겠습니다.

 

우리가 하나의 자바 애플리케이션을 작성하고 프로그램을 가동하면, 운영체제에서는 JVM(자바 가상 머신)이라는 프로세스를 하나 만들어서 실행하게 됩니다. 프로세스는 여러분도 아시다시피 `Ctrl + Alt + Delete` 버튼을 누르고 작업관리자를 키면 컴퓨터에서 실행되고 있는 프로그램의 단위 하나를 의미합니다. 크롬 브라우저가 켜있으면 크롬 프로세스가 있고, 카카오톡 앱이 켜져있으면 카카오톡도 프로세스를 차지하고 있습니다.


이런 방식으로 운영체제는 JVM 을 프로세스 하나로 잡아 생성하게 되고, 이때 운영체제는 JVM 내부에 다양한 메모리를 할당해 줍니다. 학습하셨다면 들어보신 적 있을 Heap 영역, Stack 영역, Metaspace 영역 등 다양하게 분리하여 할당해줍니다.

 

자바에서는 `new` 키워드로 객체를 생성하면 Heap 영역이라는 메모리에 이 객체가 저장되게 됩니다. 앞서 언급했던EntityManger 나 그 안의 Persistence Context(영속성 컨텍스트) 도 JVM 객체이므로 Heap 메모리에 저장되게 됩니다.

 

또한 영속성 컨텍스트는 1차 캐시에 저장된다고 표현 하지만, 이것은 Heap 영역 내부에 따로 물리적으로 구분되어 있는 공간이라기 보다는 '영속성 컨텍스트가 사용하는 전용 1차 캐시 공간' 과 일반 캐시인 '애플리케이션 전체 범용 2차 캐시'와 같은 개념으로 논리적으로 구분되어 있습니다.

 

1차 캐시는 트랜잭션 단위로 활용하는 부분이고, 2차 캐시는 전역 또는 분산 캐시 메모리라고 생각하시면 됩니다.

 

설명이 돌아갔지만 다시 영속성 컨텍스트의 1차 캐시에 대한 설명을 이어가보도록 하겠습니다.

 

1차 캐시는 영속성 컨텍스트가 활성화 되어있는 트랜잭션 환경에서 동일한 엔티티를 반복 조회 할 때, DB 에 다시 접근하게 하는 것이 아닌 캐시에 저장해둔 데이터를 활용할 수 있도록 하는 메모리 공간입니다.

 

맨 처음 DB 테이블에서 가져온 값을 JVM 의 Heap 메모리에 생성되어 있는 영속성 컨텍스트의 트랜잭션 공간에 임시로 저장해 두었다가, 같은 정보를 조회해야한다면 멀리있는 DB 까지 다시 달려가는 것이 아니라 1차 캐시에 저장되어있는 데이터를 확인합니다. 이렇게 때문에 속도적인 측면에서도 매우 뛰어나고, I/O 비용적인 측면에서도 좋습니다.

 

실제 저장되는 구조는 엔티티의 ID 값을 기준으로 Map 구조로 저장합니다.

[1차 캐시 구조]
{
    "Member:1" : Member(id=1, name="홍길동"),
    "Team:10"  : Team(id=10, name="백엔드팀")
}

 

이렇게 저장되므로, 같은 ID의 엔티티를 다시 조회하면 DB에 재접속 하지않고 캐시에서 바로 반환합니다.

 

변경 감지 (Dirty Checking)

영속성 컨텍스트가 생성되어 있는 상태에서 객체의 필드 값이 바뀌면 자동으로 update 쿼리문을 생성합니다. 이 다음 키워드를 위해 update 쿼리문을 '실행함' 이 아닌 '생성함' 임을 잘 기억해주세요!

member.setEmail("new@email.com"); // 새로운 이메일로 수정했음

 

위의 코드가 실행되고 트랜잭션이 커밋 되면

UPDATE member SET email = 'new@email.com' WHERE id = 1;

 

이와 같은 생성되어 있던 쿼리문이 DB에 날아가게 됩니다. JPA 는 개발자가 SQL 을 직접 작성하지 않아도 엔티티 객체의 변화를 감지해 자동으로 반영해줍니다.

 

쓰기 지연 (Write-Behind)

엔티티 매니저가 영속성 컨텍스트 내부를 관리하는 도중 persist() 메서드를 호출한다고 곧바로  SQL 이 나가지는 않습니다. JPA 는 쿼리를 영속성 컨텍스트 내부의 SQL 저장소에 쌓아 두었다가, 트랜잭션 커밋 시점에 한번에 DB에 반영합니다.

 

왜 굳이 이렇게 할까 고민을 해보았는데, 억지로 다음과 같은 상황의 비즈니스 로직을 상상해보겠습니다.

 

A 라는 유저의 이메일이 aaa@example.com 이었는데 이것을 bbb@example.com 으로 변경하여 A 유저 객체에 새로운 값이 설정되었다고 가정하겠습니다.

이때 쓰기 지연이 없다면 UPDATE 쿼리문을 DB에 보내게 되겠고, 이어서 이메일 중복 방지를 위해 bbb@example.com 이라는 이메일 존재유무를 조회하는 쿼리를 다시 보내 DB에서 데이터를 가져오게 했을 때, B 유저의 이메일이 이미 bbb@example.com 이었다면 다시 A 유저 객체에 원래 이메일인 aaa@example.com 으로 설정하고 DB에 UPDATE 쿼리문을 보내게 될 것입니다.

 

물론 이런 데이터 정합성에 대한 내용은 특정 예외가 던져졌을 때 트랜잭션 환경에서 처리될 일입니다.

하지만 데이터를 x -> y 로 바꿨다가 y -> x 로 되돌리는 비즈니스 로직이 있다면 어차피 원래의 데이터대로 DB의 값이 돌아와야되기 때문에 불필요한 쿼리를 보내지 않게하여 성능 최적화를 하는 것이 낫다고 생각듭니다. 이런 부분을 위해 쓰기 지연 기능을 구현해두었다고 이해하시면 될 것 같습니다.

트랜잭션 단위의 정합성

에 대한 내용은, 같은 트랜잭션에서는 항상 동일한 엔티티 인스턴스가 사용되기 때문에 동일성이 보장된다는 개념입니다.

 

추가적으로 영속성 컨텍스트와 관련한 용어를 살펴보겠습니다.

 

  • 영속(Persistent) : 영속성 컨텍스트가 관리하고 있는 상태
  • 비영속(Transient) : 새로 생성된 객체, 아직 관리되지 않은 상태
  • 준영속(Detached) : 더 이상 영속성 컨텍스트에 의해 관리되지 않는 상태
  • 삭제(Removed) : 삭제로 표시되었지만 DB 에는 아직 반영되지 않은 상태

이런 용어들이 있다라고 생각하시고 가볍게 알아두셔도 될 것 같습니다. JPA 에 대한 내용을 이야기하다가 영속성 컨텍스트에 대한 내용으로 너무 깊게 빠진 것 같습니다.

 

하지만 개인적으로 영속성 컨텍스트는 JPA 의 심장과 같은 핵심 개념이라고 생각합니다. 단순히 객체를 저장하는 데서 끝나는 것이 아니라, 데이터의 정합성, 성능, 생산성 등을 책임지는 JPA 핵심 메커니즘이기 때문이죠. 이제 이어서 JPA 에 대한 내용을 조금 더 다뤄보고 포스팅을 마무리하겠습니다.


5. JPA 핵심 요소 및 장단점

JPA 로 매핑을 구현하면, 특정 테이블에 대한 세부사항을 정의할 Entity 를 작성하게 됩니다. 핵심 구성요소는 다음과 같습니다.

구성요소 역할
`@Entity` DB 테이블과 매핑할 클래스임을 선언
`@Id` 엔티티의 기본 키(PK) 지정
`@GeneratedValue` ID 자동 생성 전략 설정
`@OneToMany`, `@ManyToOne` 테이블 간 관계 설정
`@Transactional` 트랜잭션 범위 내에서 영속성 컨텍스트 유지

 

여기서 `@OneToMany`, `@ManyToOne` 는 다른 테이블간의 관계를 설정하는 키워드입니다. @ManyToMany 도 존재하는데, 이 테이블 간 관계설정에 대한 내용은 추후에 `N+1 문제` 키워드에 대해 다룰 때 더 자세히 이야기하도록 하겠습니다.

 

✅ JPA의 장점

JPA 의 장점으로는 반복적인 SQL 작성이 불필요하기 때문에, 생산성이 뛰어납니다. 또한 도메인 모델 중심 개발을 통해 유지보수하기도 쉽고, 1차 캐시나 쓰기 지연 등으로 성능 최적화에도 이점이 있고, 영속성 컨텍스트 + 트랜잭션을 통한 데이터 정합성 유지에도 좋다고 볼 수 있습니다.

 

⚠️ JPA의 단점

JPA 의 단점 중 가장 크다고 느끼는 부분은, JOIN 을 하거나 복잡한 조건의 쿼리를 구현하기 어렵습니다. 이럴 때 복잡한 쿼리는 QueryDSL 이나 native query 등으로 보완할 수 있습니다.

 

또한 JPA 를 그냥 아무런 지식없이 배우기엔 조금 어려운 러닝 커브를 가지고 있는 것 같습니다. 트랜잭션이나 지연로딩, 객체지향 설계에 대한 개념이 기본적으로 필요하기 때문에 이 부분에 대한 개념을 탄탄히 하고 학습 후 적용한다면 좋을 것 같습니다. 또는 JDBC 를 사용하여 DB를 관리하는 방식의 학습을 선행으로 진행해보고 프로그램이 어떻게 동작하는지 내부적으로 자세히 알고 나서 JPA 를 학습한다면 많은 도움이 될 것 같습니다.

 

마지막 단점으로는 쿼리를 자동으로 생성하다 보니 어떤 쿼리가 나갈지 헷갈릴 수가 있습니다. 구현체인 Hibernate 를 통해서 쿼리가 날아가는 것을 로그로 확인할 수 있지만, 이것을 간과하고 자동으로 보내는 쿼리에 익숙해져 있다보면 N + 1 문제 등 많은 함정에 빠져 성능저하를 일으킬 수 있습니다. 꼭 Hibernate 의 설정에서 어떤 쿼리가 날아가는지 확인하시고 개발하시길 바랍니다.


6. 마무리

JPA 는 단순한 DB 매핑 기술이라고 생각하는 것 보다는 객체지향적인 사고방식으로 DB를 다루는 방식이라고 생각하는 것이 맞는 것 같습니다.

코드가 SQL 보다 중요해진 시대에서 JPA 를 활용하고 이해할 줄 아는 개발자가 지속적으로 필요할 것이라고 생각합니다.

긴글 읽어주셔서 감사합니다! 다음에는 N+1 문제, QueryDSL, 트랜잭션 중에 하나의 내용을 가지고 돌아오겠습니다!!

728x90
반응형

'Spring' 카테고리의 다른 글

JPA 와 N+1 문제  (2) 2025.04.19
Java Spring 에서의 Bean 이란?  (4) 2025.04.12
Spring Framework 와 Spring Boot (2)  (0) 2025.04.05
Spring Framework 와 Spring Boot (1)  (0) 2025.03.30
Spring - Redis의 Pub/Sub 및 WebSocket 구현하기  (4) 2024.12.17