[Spring] JPA

윤여송·2026년 9월 19일

Spring

목록 보기
5/12

JPA

1. JPA는 무엇인가?

JPA(Java Persistence API)는 Java 객체와 관계형 DB 테이블을 연결해 주는 표준 규칙이다.

JPA 자체는 인터페이스와 규칙이고, 현재 Spring Boot에서는 보통 Hibernate가 JPA의 실제 구현체로 동작한다.

내 코드
  ↓
JPA (표준 API)
  ↓
Hibernate (JPA 구현체)
  ↓
JDBC
  ↓
H2 DB

JdbcTemplate에서는 SQL과 RowMapper를 직접 작성했다. JPA에서는 Java 객체인 엔티티를 중심으로 저장·조회하며, Hibernate가 필요한 SQL을 생성해 실행한다.

2. 현재 Member는 JPA 엔티티다

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}
  • @Entity: 이 클래스는 DB에 저장·관리할 JPA 엔티티라는 표시다. 기본 규칙상 Member 클래스는 member 테이블과 연결된다.
  • @Id: 테이블의 기본 키(Primary Key) 필드라는 표시다.
  • @GeneratedValue(strategy = GenerationType.IDENTITY): ID 값은 DB가 자동 생성한다. 새 회원을 만들 때 Java에서 ID를 직접 지정하지 않아도 된다.
Member Java 객체                MEMBER 테이블
----------------                ------------
private Long id         ↔       ID (PK)
private String name     ↔       NAME

3. EntityManager (em)

EntityManager는 JPA에서 엔티티를 저장·조회·관리하는 핵심 객체다.

현재 SpringConfig.java에서 Spring Boot가 만든 EntityManager를 받아 Repository에 전달한다.

private final EntityManager em;

public SpringConfig(EntityManager em) {
    this.em = em;
}

@Bean
public MemberRepository memberRepository() {
    return new JpaMemberRepository(em);
}

이제 MemberService가 의존하는 MemberRepository의 실제 구현체는 JpaMemberRepository다.

MemberController
  ↓
MemberService
  ↓ MemberRepository 인터페이스
JpaMemberRepository
  ↓
EntityManager
  ↓
Hibernate → JDBC → H2

4. em.persist(member): 새 엔티티 저장

현재 JpaMemberRepository.save() 코드다.

@Override
public Member save(Member member) {
    em.persist(member);
    return member;
}

em.persist(member)는 새 Member 객체를 JPA가 관리하도록 등록하고, DB에 INSERT할 대상으로 만든다.

new Member()
  ↓
member.setName("spring")
  ↓
em.persist(member)
  ↓
JPA의 관리 대상(영속 상태)이 됨
  ↓
트랜잭션 커밋 시 INSERT SQL 실행
  ↓
MEMBER 테이블에 저장

IDENTITY 전략은 DB가 ID를 생성하므로, 저장 후 member.getId()로 생성된 ID를 사용할 수 있다.

memberRepository.save(member);
return member.getId();

5. 영속성 컨텍스트와 변경 감지

EntityManager는 엔티티를 영속성 컨텍스트라는 관리 공간에서 추적한다.

JPA로 조회한 엔티티는 보통 영속 상태가 된다. 트랜잭션 안에서 그 객체의 필드를 바꾸면 JPA가 이전 상태와 비교해 변경을 감지하고 UPDATE SQL을 만든다.

@Transactional
public void changeName(Long id, String newName) {
    Member member = em.find(Member.class, id);
    member.setName(newName);
    // 별도의 update SQL이나 save() 호출 없이도
    // 트랜잭션 커밋 시 UPDATE가 실행될 수 있다.
}

6. 트랜잭션, 커밋, 롤백

현재 MemberService 클래스에 붙은 @Transactional은 서비스 메서드의 DB 작업을 하나의 묶음으로 처리한다.

@Transactional
public class MemberService {
    // join(), 조회 메서드 등
}

회원가입의 흐름은 다음과 같다.

join() 호출 직전: 트랜잭션 시작
  ↓
중복 회원 조회
  ↓
em.persist(member)
  ↓
join() 정상 종료: 커밋 → INSERT 최종 반영

중간에 예외 발생: 롤백 → 지금 트랜잭션의 DB 변경 취소

트랜잭션을 Repository가 아니라 Service에 두는 이유는, DB 접근 한 번이 아니라 회원가입·주문·결제 같은 비즈니스 작업 전체를 성공 또는 실패로 묶기 위해서다.

@Transactional
public void order(...) {
    orderRepository.save(order);
    stockService.decreaseStock();
    paymentRepository.save(payment);
}

중간 작업 하나라도 실패하면 앞선 저장도 함께 롤백되어야 한다.

7. ID로 한 건 조회: em.find()

@Override
public Optional<Member> findById(Long id) {
    Member member = em.find(Member.class, id);
    return Optional.ofNullable(member);
}

em.find(Member.class, id)는 기본 키 ID로 엔티티 한 건을 조회한다.

  • 찾으면 Member 반환
  • 없으면 null 반환
  • Optional.ofNullable()로 감싸서 호출자가 없을 수 있음을 처리한다.

8. JPQL: 엔티티 기준 조회 문법

ID 이외의 조건 조회나 전체 목록 조회에는 JPQL(Java Persistence Query Language)을 사용한다.

전체 회원 조회

@Override
public List<Member> findAll() {
    return em.createQuery(
            "select m from Member m",
            Member.class
    ).getResultList();
}
select m from Member m
  • Member: DB 테이블명이 아니라 Java 엔티티 클래스명 기준이다.
  • m: Member 엔티티의 별칭(alias)이다.
  • select m: 모든 Member 엔티티를 조회한다.
  • getResultList(): 조회를 실제 실행하고 List<Member>를 반환한다.

Hibernate는 이 JPQL을 DB에 맞는 SQL로 바꾼다.

-- Hibernate가 내부적으로 만드는 SQL의 형태
select m.id, m.name
from member m

이름으로 회원 조회

@Override
public Optional<Member> findByName(String name) {
    List<Member> result = em.createQuery(
            "select m from Member m where m.name = :name",
            Member.class
    ).setParameter("name", name)
     .getResultList();

    return result.stream().findAny();
}

:name은 JPQL의 이름 기반 파라미터다.

.setParameter("name", name)

첫 번째 "name"은 JPQL의 :name과 연결되고, 두 번째 name은 Java 메서드 파라미터 값이다.

예를 들어 메서드에 "spring"이 전달되면 개념적으로 다음 조건을 조회한다.

Member 엔티티 중 name이 "spring"인 회원

9. show-sql, ddl-auto

현재 application.yaml의 JPA 설정이다.

spring:
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: none
  • show-sql: true: Hibernate/JPA가 실행하는 SQL을 콘솔 로그에 보여 준다. JPQL이 어떤 SQL로 바뀌는지 확인할 때 유용하다.
  • ddl-auto: none: Hibernate가 테이블 생성·수정·삭제를 자동으로 하지 않는다. 현재 H2에 직접 만든 MEMBER 테이블 구조를 그대로 사용한다.

ddl-auto의 값에 따라 Hibernate의 테이블 관리 방식이 달라진다.

값의미
none테이블 구조를 건드리지 않음
validate엔티티와 테이블 구조가 맞는지 검증
update필요한 변경을 자동 반영 시도
create시작 시 테이블을 새로 생성
create-drop시작 시 생성, 종료 시 삭제

운영 DB에서는 실수로 테이블 구조가 바뀌지 않도록 보통 validate 또는 none을 사용하고, 변경 이력은 Flyway 같은 마이그레이션 도구로 관리한다.

10. 통합 테스트의 @Transactional

@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
}
  • @SpringBootTest: 실제 Spring Boot 컨테이너를 띄우고 Bean을 주입받는 통합 테스트다.
  • 테스트의 @Transactional: 각 테스트가 끝난 뒤 DB 변경을 기본적으로 롤백한다.
  • @Commit: 해당 테스트만 롤백하지 않고 실제 DB에 커밋한다.

현재 앱과 테스트는 같은 H2 주소(jdbc:h2:tcp://localhost/~/test)를 사용한다. 따라서 @Commit을 붙이면 테스트가 실제 콘솔 DB에 데이터를 남긴다.

@Test
@Commit
void 회원가입() {
    // 테스트 종료 후 데이터가 DB에 남는다.
}

일반적인 테스트에서는 @Commit을 제거하고, 앱 DB와 테스트 DB도 분리하는 편이 안전하다.

11. JdbcTemplate과 JPA 비교

구분JdbcTemplateJPA
중심 대상SQLJava 엔티티 객체
INSERTSQL 또는 SimpleJdbcInsert 작성em.persist(member)
ID 조회SQL + RowMapper 작성em.find(Member.class, id)
조건 조회SQL + RowMapper 작성JPQL 또는 이후 Spring Data JPA 메서드
결과 변환RowMapper를 직접 작성JPA가 엔티티로 매핑
수정UPDATE SQL 직접 작성영속 상태 엔티티 변경 감지

JPA도 내부적으로는 JDBC를 사용한다. JPA는 JDBC 위에서 객체 중심으로 개발하게 해 주는 기술이다.

핵심 요약

  1. @Entity는 Java 클래스를 DB 테이블과 연결한다.
  2. EntityManager는 엔티티를 저장·조회·관리하는 JPA의 핵심 객체다.
  3. em.persist()는 새 엔티티를 저장 대상으로 등록하고, 커밋 시 INSERT가 반영된다.
  4. @Transactional은 서비스의 여러 DB 작업을 하나의 성공/실패 단위로 묶는다.
  5. em.find()는 ID로 한 건을 조회한다.
  6. JPQL은 테이블명이 아니라 엔티티와 필드명을 기준으로 작성한다.
  7. show-sql로 Hibernate가 실행하는 SQL을 확인할 수 있다.
  8. JPA가 CRUD SQL을 많이 대신해 주지만, 복잡한 조회에는 JPQL·QueryDSL·네이티브 SQL 등을 사용할 수 있다.
profile
y_some__velog

0개의 댓글