회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
스프링 데이터 JPA를 알려면 먼저 SQL을 몰라도 데이터베이스를 조작할 수 있게 해주는 편리한 도구인 ORM 개념을 알아야 합니다. 그러고 나서 JPA를 알아야 비로소 스프링 데이터 JPA를 알 수 있습니다. ORM은 SQL 공부를 한 적이 있다면 그 편리함을 더 크게 느낄 수 있는 도구인데요, 스프링 부트의 ORM 기술 표준인 스프링 JPA와 이를 구현하기 위한 하이버네이트를 사용해서 차례대로 알아보겠습니다.
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
스프링 데이터 JPA를 알려면 먼저 SQL을 몰라도 데이터베이스를 조작할 수 있게 해주는 편리한 도구인 ORM 개념을 알아야 합니다. 그러고 나서 JPA를 알아야 비로소 스프링 데이터 JPA를 알 수 있습니다. ORM은 SQL 공부를 한 적이 있다면 그 편리함을 더 크게 느낄 수 있는 도구인데요, 스프링 부트의 ORM 기술 표준인 스프링 JPA와 이를 구현하기 위한 하이버네이트를 사용해서 차례대로 알아보겠습니다.
ORM(object-relational mapping)은 자바의 객체와 데이터베이스를 연결하는 프로그래밍 기법입니다. 예를 들어 데이터베이스에 age, name 컬럼에 20, 홍길동이라는 값이 들어있다고 생각해보죠. 이것을 자바에서 사용하려면 어떻게 해야 할까요? 아마 다른 방법이 필요할 겁니다. 보통은 SQL이라는 언어로 데이터를 꺼내 사용하죠. 그러면 SQL을 새로 공부해야 하니 골치가 아픕니다.
하지만 ORM이 있다면 데이터베이스의 값을 마치 객체처럼 사용할 수 있습니다. 쉽게 말해 SQL을 전혀 몰라도 자바 언어로만 데이터베이스에 접근해서 원하는 데이터를 받아올 수 있죠. 즉, 객체와 데이터베이스를 연결해 자바 언어로만 데이터베이스를 다룰 수 있게 하는 도구를 ORM이라고 합니다. ORM은 다음과 같은 장점과 단점이 있습니다. 이 책에서는 ORM의 장점을 최대한 살려 사용해 보겠습니다.
DBMS에도 여러 종류가 있는 것처럼 ORM에도 여러 종류가 있습니다. 자바에서는 JPA(Java Persistence API)를 표준으로 사용합니다. JPA는 자바에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스인데요, 인터페이스이므로 실제 사용을 위해서는 ORM 프레임워크를 추가로 선택해야 합니다. 대표적으로는 하이버네이트(Hibernate)를 많이 사용하죠.
하이버네이트는 JPA 인터페이스를 구현한 구현체이자 자바용 ORM 프레임워크입니다. 내부적으로는 JDBC API를 사용하죠. 하이버네이트의 목표는 자바 객체를 통해 데이터베이스 종류에 상관없이 데이터베이스를 자유자재로 사용할 수 있게 하는 데 있습니다.
JPA와 하이버네이트에 대해서 알아보았으니, JPA의 중요한 컨셉 중 하나인 엔티티 매니저와 영속성 컨텍스트를 알아보겠습니다.
엔티티
엔티티(entity)는 데이터베이스의 테이블과 매핑되는 객체를 의미합니다. 엔티티는 본질적으로는 자바 객체이므로 일반 객체와 다르지 않습니다. 하지만 데이터베이스의 테이블과 직접 연결된다는 아주 특별한 특징이 있어 구분 지어 부릅니다. 즉, 엔티티는 객체이긴 하지만 데이터베이스에 영향을 미치는 쿼리를 실행하는 객체인 것이죠.
엔티티 매니저
엔티티 매니저(entity manager)는 엔티티를 관리해 데이터베이스와 애플리케이션 사이에서 객체를 생성, 수정, 삭제하는 등의 역할을 합니다. 그리고 이런 엔티티 매니저를 만드는 곳이 엔티티 매니저 팩토리(entity manager factory)입니다. 앞서 데이터베이스에 여러 사용자가 접근할 수 있다고 했죠? 예를 들어 회원 2명이 동시에 회원 가입을 하려는 경우 엔티티 매니저는 다음과 같이 업무를 처리합니다. 회원 1의 요청에 대해서 가입 처리를 할 엔티티 매니저를 엔티티 매니저 팩토리가 생성하면 이를 통해 가입 처리해 데이터베이스에 회원 정보를 저장하는 것이죠. 회원 2도 마찬가지입니다. 그리고 회원 1, 2를 위해 생성된 엔티티 매니저는 필요한 시점에 데이터베이스와 연결한 뒤에 쿼리합니다.
그렇다면 스프링 부트에서도 직접 엔티티 매니저 팩토리를 만들어서 관리할까요? 사실 그렇지 않습니다. 스프링 부트는 내부에서 엔티티 매니저 팩토리를 하나만 생성해서 관리하고 @PersistenceContext 또는 @Autowired 애너테이션을 사용해서 엔티티 매니저를 사용합니다.
▼ 스프링 부트가 엔티티 매니저를 사용하는 방법 예
@PersistenceContext
EntityManager em; // 프록시 엔티티 매니저. 필요할 때 진짜 엔티티 매니저 호출
그리고 스프링 부트는 기본적으로 빈은 하나만 생성해서 공유하므로 동시성 문제가 발생할 수 있습니다. 그래서 실제로는 엔티티 매니저가 아닌 실제 엔티티 매니저와 연결하는 프록시(가짜) 엔티티 매니저를 사용합니다. 필요할 때 데이터베이스 트랜잭션과 관련된 실제 엔티티 매니저를 호출하는 겁니다.
* 쉽게 말해 엔티티 매니저는 Spring Data JPA에서 관리하므로 여러분이 직접 생성하거나 관리할 필요가 없습니다.
또한 엔티티 매니저는 엔티티를 영속성 컨텍스트에 저장한다는 특징이 있습니다. 영속성 컨텍스트는 JPA의 중요한 특징 중 하나로, 엔티티를 관리하는 가상의 공간입니다. 이것이 있기 때문에 데이터베이스에서 효과적으로 데이터를 가져올 수 있고, 엔티티를 편하게 사용할 수 있는 것이죠. 영속성 컨텍스트에는 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩이라는 특징이 있는데요. 이 특징들에 대해 간단하게 알아보겠습니다.
기존에는 데이터 조작을 위해 쿼리를 직접 작성해야 했지만, 스프링 부트에서는 이런 쿼리를 자바 코드로 작성하고 이를 JPA가 알아서 쿼리로 변경해 주는 것이 매우 편리합니다. 그래서 어떤 사람들은 JPA의 영속성 컨텍스트를 몰라도 괜찮다고 이야기하기도 합니다. 하지만 필자는 이를 모르고 지나치면 자신이 의도하지 않은 방향으로 프로그램이 결과를 만들 수 있다고 생각합니다. 그래서 이런 상황을 대비해 영속성 컨텍스트의 기본적인 특징들을 알아 두기를 추천합니다.
1차 캐시
영속성 컨텍스트는 내부에 1차 캐시를 가지고 있습니다. 이때 캐시의 키는 엔티티의 @Id 애너테이션이 달린 기본 키 역할을 하는 식별자이며 값은 엔티티입니다. 엔티티를 조회하면 1차 캐시에서 데이터를 조회하고 값이 있으면 반환합니다. 값이 없으면 데이터베이스에서 조회해 1차 캐시에 저장한 다음 반환합니다. 이를 통해 캐시된 데이터를 조회할 때에는 데이터베이스를 거치치 않아도 되므로 매우 빠르게 데이터를 조회할 수 있습니다.
쓰기 지연
쓰기 지연(transactional write-behind)은 트랜잭션을 커밋하기 전까지는 데이터베이스에 실제로 질의문을 보내지 않고 쿼리를 모았다가 트랜잭션을 커밋하면 모았던 쿼리를 한번에 실행하는 것을 의미합니다. 예를 들어 데이터 추가 쿼리가 3개라면 영속성 컨텍스트는 트랜잭션을 커밋하는 시점에 3개의 쿼리를 한꺼번에 쿼리를 전송합니다. 이를 통해 적당한 묶음으로 쿼리를 요청할 수 있어 데이터베이스 시스템의 부담을 줄일 수 있습니다.
변경 감지
트랜잭션을 커밋하면 1차 캐시에 저장되어 있는 엔티티의 값과 현재 엔티티의 값을 비교해서 변경된 값이 있다면 변경 사항을 감지해 변경된 값을 데이터베이스에 자동으로 반영합니다. 이를 통해 쓰기 지연과 마찬가지로 적당한 묶음으로 쿼리를 요청할 수 있고, 데이터베이스 시스템의 부담을 줄일 수 있습니다.
지연 로딩
지연 로딩(lazy loading)은 쿼리로 요청한 데이터를 애플리케이션에 바로 로딩하는 것이 아니라 필요할 때 쿼리를 날려 데이터를 조회하는 것을 의미합니다(반대로 조회할 때 쿼리를 보내 연관된 모든 데이터를 가져오는 즉시 로딩도 있습니다).
이 특징들의 갖는 공통점은 모두 데이터베이스의 접근을 최소화해 성능을 높일 수 있다는 겁니다. 캐시를 하거나, 자주 쓰지 않게 하거나, 변화를 자동 감지해서 미리 준비하거나 하는 등의 방법을 통해 말이죠. 이런 특징을 잘 이해하고 JPA를 사용한다면 문제 상황을 조금 더 잘 이해할 수 있을 겁니다.
엔티티는 4가지의 상태를 가집니다. 영속성 컨텍스트가 관리하고 있지 않은 분리(detached) 상태, 영속성 컨텍스트가 관리하는 관리(managed) 상태, 영속성 컨텍스트와 전혀 관계가 없는 비영속(transient) 상태, 삭제된(removed) 상태로 나눠집니다. 이 상태는 특정 메서드를 호출해 변경할 수 있는데요. 필요에 따라 엔티티의 상태를 조절해 데이터를 올바르게 유지하고 관리할 수 있습니다.
▼ 엔티티 상태 변경 예
public class EntityManagerTest {
@Autowired
EntityManager em;
public void example() {
// ❶ 엔티티 매니저가 엔티티를 관리하지 않는 상태(비영속 상태)
Member member = new Member(1L, "홍길동");
// ❷ 엔티티가 관리 상태가 됩니다(관리 상태).
em.persist(member);
// ❸ 엔티티 객체가 분리된 상태가 됩니다(분리 상태).
em.detach(member);
// ➍ 엔티티 객체가 삭제된 상태가 됩니다(삭제 상태).
em.remove(member);
}
}
❶ 엔티티를 처음 만들면 엔티티는 비영속 상태가 됩니다. ❷ persist( ) 메서드를 사용해 엔티티를 관리 상태로 만들 수 있으며, Member 객체는 영속성 컨텍스트에서 상태가 관리됩니다. ❸ 만약 엔티티를 영속성 컨텍스트에서 관리하고 싶지 않다면 detach( ) 메서드를 사용해 분리 상태로 만들 수 있습니다. ➍ 또한 더 이상 객체가 필요 없다면 remove( ) 메서드를 사용해서 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제할 수 있습니다.
지금까지 배운 개념에서는 엔티티의 상태를 직접 관리하고, 필요한 시점에 커밋을 해야 하는 등의 개발자가 신경 써야 할 부분이 많습니다. 스프링 데이터(Spring Data)는 비즈니스 로직에 더 집중할 수 있게 데이터베이스 사용 기능을 클래스 레벨에서 추상화했습니다. 스프링 데이터에서 제공하는 인터페이스를 통해서 스프링 데이터를 사용할 수 있습니다. 이 인터페이스에서는 CRUD를 포함한 여러 메서드가 포함되어 있으며, 알아서 쿼리를 만들어줍니다.
또한 이외에도 페이징 처리 기능과 메서드 이름으로 자동으로 쿼리를 빌딩하는 기능이 제공되는 등 많은 장점이 있습니다. 추가로 각 데이터베이스의 특성에 맞춰 기능을 확장해 제공하는 기술도 제공합니다. 예를 들어 표준 스펙인 JPA는 스프링에서 구현한 스프링 데이터 JPA(Spring Data JPA)를, 몽고디비는 스프링 데이터 몽고디비(Spring Data MongoDB)를 사용합니다. 이 책에서는 스프링 데이터 JPA를 살펴보겠습니다.
스프링 데이터 JPA는 스프링 데이터의 공통적인 기능에서 JPA의 유용한 기술이 추가된 기술입니다. 스프링 데이터 JPA에서는 스프링 데이터의 인터페이스인 PagingAndSortingRepository를 상속받아 JpaRepository 인터페이스를 만들었으며, JPA를 더 편리하게 사용하는 메서드를 제공합니다. JPA를 사용하지 않을 때는 다음과 같이 메서드 호출로 엔티티 상태를 바꿔야 합니다.
▼ 메서드 호출로 엔티티 상태 변경 예
@PersistenceContext
EntityManager em;
public void join() {
// 기존에 엔티티 상태를 바꾸는 방법(메서드를 호출해서 상태 변경)
Member member = new Member(1L, "홍길동");
em.persist(member);
}
하지만 스프링 데이터 JPA를 사용하면 리포지터리 역할을 하는 인터페이스를 만들어 데이터베이스의 테이블 조회, 수정, 생성, 삭제 같은 작업을 간단히 할 수 있습니다. 다음과 같이 JpaRepository 인터페이스를 우리가 만든 인터페이스에서 상속받고, 제네릭에는 관리할 <엔티티 이름, 엔티티 기본 키의 타입>을 입력하면 기본 CRUD 메서드를 사용할 수 있습니다.
▼ 기본 CRUD 메서드를 사용하기 위한 JpaRepository 상속 예
public interface MemberRepository extends JpaRepository<Member, Long> {
}
그러면 JpaRepository에서 제공하는 메서드의 사용 방법을 살펴보겠습니다. springbootdeveloper 패키지에 MemberService.java를 생성해 코드를 작성하세요.
▼ MemberService.java
@Service
public class MemberService {
@Autowired
MemberRepository memberRepository;
public void test() {
// <span style="font-weight: 400;">❶</span> 생성(Create)
memberRepository.save(new Member(1L, "A"));
// <span style="font-weight: 400;">❷</span> 조회(Read)
Optional<Member> member = memberRepository.findById(1L); // 단건 조회
List<Member> allMembers = memberRepository.findAll(); // 전체 조회
// <span style="font-weight: 400;">❸</span> 삭제(Delete)
memberRepository.deleteById(1L);
}
}
❶ save( ) 메서드를 호출해 데이터 객체를 저장할 수 있습니다. 전달 인수로 엔티티 Member를 넘기면 반환값으로 저장한 엔티티를 반환받을 수 있습니다. ❷ findById( ) 메서드에 id를 지정해 엔티티를 하나 조회할 수 있습니다. findAll( ) 메서드는 전체 엔티티를 조회합니다. ❸ deleteById( ) 메서드에 id를 지정하면 엔티티를 삭제할 수 있습니다.
* delete( ) 메서드를 호출해 엔티티를 전달 인수로 넘겨 삭제할 수도 있습니다.
요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.