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을 생성해 실행한다.
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
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
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();
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가 실행될 수 있다.
}
현재 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);
}
중간 작업 하나라도 실패하면 앞선 저장도 함께 롤백되어야 한다.
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()로 감싸서 호출자가 없을 수 있음을 처리한다.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"인 회원
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 같은 마이그레이션 도구로 관리한다.
@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도 분리하는 편이 안전하다.
| 구분 | JdbcTemplate | JPA |
|---|---|---|
| 중심 대상 | SQL | Java 엔티티 객체 |
| INSERT | SQL 또는 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 위에서 객체 중심으로 개발하게 해 주는 기술이다.
@Entity는 Java 클래스를 DB 테이블과 연결한다.EntityManager는 엔티티를 저장·조회·관리하는 JPA의 핵심 객체다.em.persist()는 새 엔티티를 저장 대상으로 등록하고, 커밋 시 INSERT가 반영된다.@Transactional은 서비스의 여러 DB 작업을 하나의 성공/실패 단위로 묶는다.em.find()는 ID로 한 건을 조회한다.show-sql로 Hibernate가 실행하는 SQL을 확인할 수 있다.