S E P H ' S

[Spring] JPA 본문

Programing & Coding/Spring

[Spring] JPA

yoseph0310 2024. 1. 16. 16:43

Spring 애플리케이션을 개발하면서 JPA를 선택하는 이유가 무엇일까? MyBatis로도 DB를 쉽게 다루고 동적 쿼리를 작성할 수 있는데 말이다. 이에 대해서는 먼저 ORM에 대해 이해하는 것이 필요하다.

 

ORM

ORM(Object Relational Mapping)을 직역하면 객체 관계 매핑이다. 말그대로 객체의 관계를 매핑한다는 말이다. 무엇을 매핑하는 것일까? 우리는 Spring, 즉 Java 코드를 작성할때, 객체 지향 프로그래밍으로 코드를 작성한다. (최대한..) 객체 지향 프로그래밍에서는 하나의 객체가 자신의 역할을 준수하며 책임을 다하고 메시지를 기반으로 여러 객체들과의 협력을 이뤄내면서 작동한다.

 

관계형 데이터베이스는 테이블을 사용한다. 객체 지향 프로그래밍의 객체 모델과 관계형 데이터베이스의 테이블 즉, 관계형 모델 간에는 불일치가 존재한다. 따라서 객체 간의 관계를 바탕으로 SQL을 자동으로 생성하여 이 불일치를 해결하고 객체를 통해 간접적으로 데이터베이스 데이터를 다루는 것이 ORM이다.

 

장점

  • 객체 지향적인 코드로 인해서 더 직관적이고 로직에 집중할 수 있도록 한다.
  • SQL문이 아닌 메소드를 통해 데이터베이스를 조작할 수 있으므로 개발자가 객체 모델만 이용하여 프로그래밍을 하는데 더 집중할 수 있다.
  • 객체마다 별도로 코드를 작성하여 가독성이 높아진다. 이로 인해 생산성도 증가한다.
  • 객체 지향적이기 때문에 재사용 및 유지보수성 또한 우수하다.
  • DBMS에 대한 종속성이 줄어든다. 객체 자체에 집중함으로써 DBMS를 교체하더라도 비교적 적은 리스크와 시간이 소요된다.
  • 또한 Java에서 가공할 경우 equals(), hashCode()의 재정의 또한 편리하게 가능하다.

단점

  • DBMS의 고유 기능을 이용하기 어렵다. ORM만으로는 완벽히 서비스를 구현하기는 어렵다. (그러나 DBMS의 고유기능을 이용하면 이식성이 저하된다. 단점으로만 보기는 어렵다.)
  • 설계를 신중히 해야한다.
  • 프로젝트의 복잡도가 커질 경우, 복잡한 쿼리의 경우 구현 난이도 또한 어려워질 수 있다.
  • 프로시저가 많은 시스템에서는 ORM의 객체지향적 장점을 활용하기 어렵다.

이와 같이 장단점이 존재하기 때문에 상황에 맞게 판단하여 사용하는 기준이 중요할 것이다. 

 

JPA/Hibernate

JPA(Java Persistence API)는 Java에서 사용하는 ORM 기술 표준이고 인터페이스의 모음이다. 이러한 JPA 표준 명세를 구현한 구현체가 바로 Hibernate이다. JPA를 사용하다보면 단점에서 언급했던 것 처럼 복잡한 쿼리를 작성하는 것이 어려워질 수 있다. 그러나 이를 위해 JPA는 Native Query라는 기능을 제공한다. 관계 매핑이 어렵거나 성능에 대한 이슈가 우려되는 경우 SQL을 직접 작성하여 사용할 수 있다.

 

결국 복잡한 쿼리를 위해 SQL을 직접 작성하는 Native Query를 사용한다면 굳이 JPA를 써야할 필요가 있을까? MyBatis를 사용하면 되지 않을까? 왜 JPA를 사용할까? JPA의 특징을 조금 더 알아보도록 하자.

 

영속성 컨텍스트(Persistence Context)

JPA에서는 테이블과 매핑되는 엔티티 객체 정보를 영속성 컨텍스트를 통해 애플리케이션 내에서 오래 지속되도록 보관한다. 영속성 컨텍스트란 엔티티를 영구 저장하는 환경이라는 뜻이다. 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다. 엔티티 매니저를 통해 엔티티를 저장, 조회하면 엔티티 메니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다. 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어진다. 이를 통해서 영속성 컨텍스트에 접근하고 관리할 수 있다.

 

엔티티의 생명주기

 

  • 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
User user = new User();
user.setId("1");
user.setName("James");

 

  • 영속(managed) : 영속성 컨텍스트에 저장된 상태
EntityManager em = EntityManagerFactory.createEntityManager();
em.getTransaction().begin();

User user = new User();
user.setId("1");
user.setName("James");

// 객체를 영속성 컨텍스트에 저장(영속)
em.persist(user);

 

  • 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
// user 엔티티를 영속성 컨텍스트에서 분리(준영속)
em.detach(user);

 

  • 삭제(removed) : 삭제된 상태
// 객체를 삭제한 상태
em.remove(user);

 

영속성 상태의 장점

  • 1차 캐시 : 애플리케이션에서 데이터를 조회하기 위해 데이터베이스에서 조회해 오는 것은 많은 과정을 거치게 된다.(= 비용이 많이든다.) 영속성 컨텍스트에 최초 저장한 데이터를 1차 캐시에 저장함으로써 동일한 객체를 조회하는 경우 1차 캐시에서 조회함으로써 데이터베이스를 한번 더 조회하는 값비싼 행위를 하지 않게 된다.
  • 동일성(identify) 보장 : 같은 데이터를 조회하여 캐시에 저장하면서 동일성을 보장한다.
  • 트랜잭션을 지원하는 쓰기 지연 : 엔티티 매니저는 데이터 변경 시 반드시 트랜잭션을 시작해야한다. 
EntityManager em = EntityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();	// 트랜잭션

// 트랜잭션 시작
tx.begin();

// 비영속
User user = new User();
user.setId("1");
user.setName("James");

// 영속
em.persist(user);

// 엔티티 등록
tx.commit();

여러개의 엔티티를 생성하고 persist를 하더라도 commit 하기 전에는 데이터베이스에 저장되지 않는다. 이를 쓰기 지연이라 하며 영속성 컨텍스트의 장점이다.

 

  • 변경 감지
EntityManager em = EntityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();	// 트랜잭션

// 트랜잭션 시작
tx.begin();

// 비영속
User user = em.find(User.class, "user");
user.setName("Jacop");

// 엔티티 등록
tx.commit();

 

  • 엔티티 수정은 set 메소드를 통해 변경하고 별다른 로직 없이 트랜잭션 커밋을 하면 업데이트 된다. 이것이 가능한 이유는 변경 감지 (Dirty Check)기능을 제공하기 때문이다.
  • 영속성 컨텍스트의 1차 캐시에는 user의 초기 데이터가 저장되어 있다. 이후 set 메소드를 통해 데이터를 변경하고 트랜잭션 커밋 시 flush()가 발생하며 1차 캐시에서 엔티티와 스냅샷을 비교하여 변경에 대한 감지를 한다.
  • 이후 Udpate SQL을 생성하여 쓰기 지연 저장소에서 쿼리를 보낸다.
  • 이렇게 데이터베이스에 저장된 데이터를 수정하게 된다.
  • 지연 로딩

 

영속성 컨텍스트의 장점들을 살펴보면 1차 캐시로 인해 성능상 이점을 얻을 수 있다는 점을 알 수 있다. 위에서도 설명했듯, 데이터베이스에 직접 쿼리를 실행하는 과정은 큰 비용이 발생한다. 만약 영속성 컨텍스트를 사용하지 않는다면 동일한 데이터를 조회할때도 데이터베이스에 지속적으로 조회 쿼리를 실행시킬 것이다. 하나의 쿼리라면 그다지 큰 차이가 나지 않겠지만 수없이 많은 조회를 요청받게 될 경우에는 큰 차이가 날 것이다. 이를 1차 캐시에 한번 조회한 것은 저장해둠으로써 데이터베이스에 가지 않더라도 조회할 수 있도록 하여 성능상 이점을 얻을 수 있다.

 

위에서 말한 복잡한 쿼리를 사용하는 것에 대해서도 답변이 될 수 있다. JPA가 제공하는 Native Query는 복잡한 쿼리를 작성하면서도 영속성 컨텍스트를 활용할 수 있다. 물론 객체지향 프로그래밍의 장점을 살린 JPA를 사용하는 만큼 Native Query를 작성하여 로직을 작성하는 것은 그리 바람직하지는 않다. 하지만 영속성 컨텍스트의 장점을 활용할 여지가 있느냐 아니냐의 차이는 꽤 큰 차이라고 생각한다.