JPA와 Spring Data JPA

이신영·2024년 8월 2일
1

Spring

목록 보기
15/16
post-thumbnail

예로부터 사람이 불편해지는 일은 점점 편해지도록 발전한다. 도구가 처리해주고 기계가 처리해주고 나중에는 기계가 알아서 처리해주며 발전했다.

자바가 그렇다. 메모리처리를 쉽게해주기 위해(개발자가 편해지기위해) QC를 사용했고 매번 똑같은 메서드를 반복해서 적기싫어 AOP개념을 대입하거나 API들을 사용해서 개발자의 편의성을 향상시켜왔다.

이제 다음차례는 DB다. DB쿼리짜는게 너무 귀찮다? API를 사용하자! 해서 나온게? JPA다. 근데, 자동화를 한다는건 우리(개발자)가 실행해주는 과정에서 디폴트로 적용되어있는 옵션들이 있다는것이고 이 디폴트로 이루어지는 동작과정 발생하는 문제들이 있을 것 이다.

딱 이정도만 알고 보자!


JPA?

JPA(Java Persistence API)는 자바의 ORM을 자동화해주는 API이자 인터페이스다. 즉, 내가 생성한 엔티티를 DB의 어떤 테이블과 연결해줘야하는데 이걸 JPA를 통해 구현한 ORM 프레임워크 ORM 프레임워크(예: Hibernate, EclipseLink, OpenJPA 등)로 JDBC를 통해서 해준다.

JPA를 잘 활용한다면 우린 쿼리를 직접 짤 필요가 없어진다! = 비즈니스 로직에 집중할 수 있다 = 한가지의 책임에 집중할 수 있다!!

ORM(Object-Relational Mapping) : 객체와 DB 테이블이 매핑되도록 연결시켜줌.
JDBC(Java Database Connectivity) : 자바와 DB를 연동시켜주는 자바 API
Hibernate : 흔히 쓰이는 JPA의 구현체 대부분 얘로 JPA를 구현하는걸 써봤을듯 하다

동작 과정

일단 그냥 코드부터보자

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class MemberService {

    private EntityManagerFactory emf = Persistence.createEntityManagerFactory("myPersistenceUnit");

    public void manageMemberLifecycle() {
        // 비영속 상태: 엔티티 객체를 생성했지만 영속성 컨텍스트에 저장되지 않은 상태
        Member member = new Member("member1", "회원1");

		// 엔티티 매니저 생성 및 트랜잭션 시작
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();

        // 영속 상태: 엔티티를 영속성 컨텍스트에 저장
        // 트랜잭션이 커밋될 때 데이터베이스에 INSERT 쿼리를 자동으로 생성하고 실행
        em.persist(member);

        // 준영속 상태: 엔티티를 영속성 컨텍스트에서 분리
        em.detach(member);

        // 영속 상태로 복귀
        // 변경된 데이터가 감지되면, JPA는 UPDATE 쿼리를 생성
        member.setUsername("회원1_수정");
        em.merge(member);

        // 삭제 상태: 엔티티 삭제
        // DELETE 쿼리가 실행될 준비를 하고 커밋을 할 때 반영
        em.remove(member);

        // 트랜잭션 커밋
        // commit()을 호출하면, 영속성 컨텍스트의 변경 사항이 데이터베이스에 반영됨. 
        // 이때 flush()가 자동으로 호출되어 데이터베이스에 실제 변경 사항을 적용한다.
        em.getTransaction().commit();

        // 엔티티 매니저 닫기
        em.close();
    }
}

EntityManager를 통해 Entity(Member)를 관리하고 commit을 진행한다.

과정은 이렇다.

  1. 명시한 DB(일반적으로는 .xml이나 .properties 같은 설정파일)에 연결
  2. EntityManagerFactory 생성
  3. EntityManager 생성
  4. (트랜잭션 시작)사용자가 엔티티에 대한 CRUD작업을 진행
  5. (트랜잭션 커밋)적절한 쿼리를 자동으로 생성해서 JDBC를 통해 DB로 보내준다.

자동 쿼리 예상

persist 호출 시 예상되는 쿼리:

INSERT INTO members (id, username) VALUES ('member1', '회원1');

merge 호출 시 예상되는 쿼리 (변경된 경우):

UPDATE members SET username = '회원1_수정' WHERE id = 'member1';

remove 호출 시 예상되는 쿼리:

DELETE FROM members WHERE id = 'member1';

Entity : DB의 테이블과 연동되는 자바 객체

영속성 컨텍스트(Persistence Context)

트랜잭션 범위 내에서 엔티티를 캐시하여 성능을 최적화하고 변경된 데이터를 자동으로 감지하여 데이터베이스에 반영해주는 DB와 JVM간의 중간저장소이다.

영속성 컨텍스트의 기능

  1. 동일성 보장
    같은 트랜잭션 내에서 동일한 ID의 엔티티는 같은 객체로 취급한다는 점이다.
  2. 1차 캐시
    영속성 컨텍스트의 EntityManager는 1차 캐시로 엔티티 식별자를 key로 사용하여 객체를 저장하여 성능을 최적화한다.
  3. 지연로딩
    실제로 필요할 때까지 연관된 엔티티나 컬렉션의 데이터를 로드하지 않는 방식이다. 불필요한 DB호출을 방지하는 데 유리하지만 N+1 문제를 야기할 수 있다.
  4. 변경감지
    엔티티의 변경 사항을 자동으로 감지하고 트랜잭션 커밋 시 데이터베이스에 반영하는 기능이다. = 개발자가 엔티티를 명시적으로 업데이트하지 않아도 됨
  5. 트랜잭션 지원
    영속성 컨텍스트는 트랜잭션을 지원하며, 트랜잭션이 활성화될 때 시작되고 종료될 때 정리된다.

동작 예시 코드

아래는 실제 작동하는 영속성 컨텍스트 코드가 아닌, 그냥 이런 플로우다~ 로 이해하면 된다. 확인해보자

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.EntityTransaction;

public class JpaExample {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("myPersistence");
        EntityManager em = emf.createEntityManager();

        try {
            EntityTransaction tx = em.getTransaction();
            // 5. 트랜잭션 지원
            tx.begin();

            // 1. 동일성 보장
            // ID가 1인 Member 엔티티를 처음으로 조회 (DB 조회 발생)
            Member member1 = em.find(Member.class, 1L);
            System.out.println("첫 번째 조회: " + member1.getUsername());

            // ID가 1인 Member 엔티티를 두 번째로 조회 (1차 캐시 사용, DB 조회 없음)
            Member member2 = em.find(Member.class, 1L);
            System.out.println("두 번째 조회: " + member2.getUsername());

            // 2. 1차 캐시
            // 동일한 ID로 조회된 엔티티는 같은 인스턴스
            System.out.println("동일성 보장: " + (member1 == member2)); // true

            // 3. 지연 로딩
            Member lazyMember = em.getReference(Member.class, 1L);
            System.out.println("지연 로딩 확인 전: " + lazyMember.getClass().getName());
            System.out.println("지연 로딩 확인 후: " + lazyMember.getUsername());

            // 4. 변경 감지
            // 새로운 Member 엔티티 생성 및 영속화
            Member newMember = new Member();
            newMember.setId(2L);
            newMember.setUsername("회원2");
            em.persist(newMember);

            // 새로운 Member 엔티티가 1차 캐시에 저장됨
            Member member3 = em.find(Member.class, 2L);
            System.out.println("새로운 멤버 조회: " + member3.getUsername());

            // 기존 엔티티의 속성 변경
            member1.setUsername("수정된 회원1");
            System.out.println("변경 감지 확인: " + member1.getUsername());

            tx.commit();
        } finally {
            em.close();
            emf.close();
        }
    }
}

2차 캐시?

1차 캐시가 있다는말은 즉 2차캐시도 존재한다는 것이다. 1차는 Entity Manager가 엔티티의 동일성(==)을 보장한 채 성능을 최적화해주었다면 2차 캐시는 애플리케이션 전체에 대한 캐시이다.

보통 전역적으로 설정하기 위해 xml과 properties를 이용한다.

비교 요약

특성1차 캐시2차 캐시
관리 주체EntityManagerJPA 구현체 및 캐시 제공자
범위트랜잭션 범위 (EntityManager 단위)애플리케이션 전체
활성화 여부항상 활성화선택적 (설정 필요)
목적동일 트랜잭션 내 데이터베이스 접근 최소화데이터베이스 접근 최소화 및 성능 향상
일관성항상 데이터베이스와 동기화데이터베이스 변경에 따라 갱신/무효화
캐시 전략엔티티의 ID를 기준으로 캐싱사용자 정의 설정 가능 (유효기간, 만료 정책 등)
객체 동일성같은 트랜잭션 내에서만 보장여러 트랜잭션 간에 객체 동일성 보장하지 않음

Spring Data JPA (with Spring Boot)

JPA만 사용하더라도 많은 기능을 편리하게 해주지만? Spring Boot를 쓴다면? 더더욱 편리하고 더 다양한 기능을 추가해주는 JPA의 구현체인 hibernate를 사용하는 Spring Data JPA를 사용할 수 있다.

Hibernate는 기본 JPA 구현체를 사용하여 더더욱 DB와의 상호작용을 간소화한 극한의 편리기술이다.

import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
    // 기본 CRUD 메서드는 JpaRepository에서 제공
    // 필요 시 추가 메서드를 정의할 수 있음
    Member findByUsername(String username);
}

위처럼 JPA를 extends 한 뒤 추가적으로 필요한 메서드들을 정의하면?

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class MemberService {

    @Autowired
    private MemberRepository memberRepository;

    @Transactional
    public List<Member> findAll() {
        return memberRepository.findAll();
    }

    @Transactional
    public Member findByUsername(String username) {
        return memberRepository.findByUsername(username);
    }

    @Transactional
    public Member save(Member member) {
        return memberRepository.save(member);
    }
}

기본적인 findAll() save() 외에도 관례를 따라 정의한 메서드명을 분석하여 쿼리를 알아서 생성해준다!

관련공식문서

물론 이걸 외우는거보단 IDE 자동완성기능을 사용하면 수월하게 개발할 수 있다.

이정도면 진짜 대부분 자동화된거 아님? 이라고 생각이 든다. 다만?
위에서 말했듯 언제나 강한힘에는 책임이 따르는법.. 여러가지 문제가 발생 할 수 있다는걸 알고있도록하자.

이 또한 추후에 글을 작성하겠다..!


마지막으로

최근에 이 글을 보고 많은 생각이 들더라

풀스택 개발자는 다재다능한 개발자이지만 유능한 개발자일까? 소규모 프로젝트에서 매우 유용할 수 있지만 다재다능은 유능함의 척도는 아니라고 생각한다. 개발자는 책임을 최대한 분산해야한다는 생각이기 때문이다.

근데 회사는 우리한테 SQL, CI/CD, 클라우드를 요구하니깐... 모든 프로젝트가 다 대규모도 아닐뿐더러 하라면 해야지 뭐 😂

JPA를 써보면 문득 드는생각이 최적화랑 별개지만 너무편하고 배울게 별로 없다. 차세대 기술이라는 생각이 들 수밖에 없다.

특히나 인터넷강의에서는 김영한 강사님이 JPA를 많은 홍보를 하시는데 아무래도 한국시장에 JPA를 쓸 줄 아는 주니어들을 양성해서 언젠간 다음기술을 받아들일 준비를 하는 큰 그림을 그리는중이 아닐까?

끝으로 김영한 강사님의 말씀을 보고가자.

반복적인 CRUD SQL을 작성하고 객체를 SQL에 매핑하는데 시간을 보내기에는 우리의 시간이 너무아깝다.

이미 많은 자바 개발자들이 오랫동안 비슷한 고민을 해왔고 문제를 해결하려고 많은 노력을 기울여왔다. 그리고 그 노력의 결정체가 바로 JPA다.

JPA는 표준 명세만 570페이지에 달하고, JPA를 구현한 하이버네이트는 이미 10년 이상 지속해서 개발되고 있으며, 핵심 모듈의 코드 수가 이미 십만 라인을 넘어섰다.

귀찮은 문제들은 이제 JPA에게 맡기고 더 좋은 객체 모델링과 더 많은 테스트를 작성하는데 우리의 시간을 보내자. 개발자는 SQL Mapper가 아니다. - 김영한

profile
후회하지 않는 사람이 되자 🔥

0개의 댓글