1. JPA란 무엇인가?
1.1. JPA의 등장 배경과 정의
JPA(Java Persistence API)는 자바 진영의 ORM(Object-Relational Mapping) 기술 표준이다. JPA가 등장하게 된 근본적인 이유는 기존 데이터 접근 방식의 여러 한계를 해결하기 위해서다.
기존 방식의 문제점 (JDBC/JdbcTemplate/MyBatis):
// 전통적인 JDBC 방식의 코드
public class MemberRepository {
public Member findById(Long id) throws SQLException {
String sql = "SELECT id, name, age FROM member WHERE id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
rs = pstmt.executeQuery();
if (rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
member.setAge(rs.getInt("age"));
// 객체 그래프를 완성하려면 추가 쿼리 필요
// member.setTeam(findTeamByMemberId(id));
return member;
}
return null;
} finally {
close(conn, pstmt, rs);
}
}
public void update(Member member) throws SQLException {
String sql = "UPDATE member SET name = ?, age = ? WHERE id = ?";
// Connection, PreparedStatement, executeUpdate...
// 변경된 필드만 업데이트하는 게 아니라 모든 필드 업데이트
}
}
기존 기술들의 근본적인 문제점:
- 반복적인 보일러플레이트 코드 - 매번 Connection, Statement, ResultSet을 열고 닫아야 함
- SQL 의존적인 개발 - 비즈니스 로직이 SQL문과 강하게 결합됨
- 패러다임 불일치 문제 - 객체지향과 관계형 데이터베이스의 근본적인 차이
- 데이터베이스 종속성 - 데이터베이스 벤더 변경 시 SQL 수정 필요
- 객체 그래프 탐색의 어려움 - 연관된 객체를 조회하려면 매번 추가 쿼리 실행
1.2. ORM(Object-Relational Mapping) 기술
ORM은 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑해주는 기술이다. 객체지향 프로그래밍은 클래스와 객체를 사용하고, 관계형 데이터베이스는 테이블과 행을 사용하는데, 이 두 패러다임 간의 간극을 메워준다.
// 객체지향 세계의 클래스 구조
class Member {
private Long id;
private String name;
private int age;
private Team team; // 객체 참조
private List<Order> orders; // 객체 컬렉션
}
class Team {
private Long id;
private String teamName;
private List<Member> members;
}
// 관계형 데이터베이스의 테이블 구조
-- MEMBER 테이블
CREATE TABLE member (
id BIGINT PRIMARY KEY,
name VARCHAR(255),
age INT,
team_id BIGINT, -- 외래키
FOREIGN KEY (team_id) REFERENCES team(id)
);
-- TEAM 테이블
CREATE TABLE team (
id BIGINT PRIMARY KEY,
team_name VARCHAR(255)
);
패러다임 불일치의 구체적 문제들:
- 상속 문제 - 객체는 상속이 있지만, 테이블은 상속이 없음
- 연관관계 문제 - 객체는 참조를 사용하지만, 테이블은 외래키를 사용
- 데이터 타입 문제 - 객체는 다양한 데이터 타입을 가질 수 있지만, 테이블은 제한적
- 식별자 문제 - 객체는 동일성(identity)과 동등성(equality)을 구분하지만, 테이블은 기본키로 식별
1.3. JPA vs SQL Mapper(JdbcTemplate, MyBatis)
| 특징 | JPA (ORM) | SQL Mapper (JdbcTemplate, MyBatis) |
| 접근 방식 | 객체 중심 | SQL 중심 |
| 생산성 | 높음 (반복 코드 최소화) | 중간 |
| 유지보수 | 용이 (객체 수정 시 자동 반영) | SQL 직접 관리 필요 |
| 패러다임 불일치 해결 | 자동 해결 | 개발자가 수동 해결 |
| 데이터베이스 독립성 | 높음 (Dialect 사용) | 낮음 (SQL 직접 작성) |
| 학습 곡선 | 가파름 | 완만함 |
| 복잡한 쿼리 | JPQL 또는 네이티브 SQL 필요 | SQL 직접 작성 가능 |
JPA의 핵심 가치:
- 객체와 테이블을 매핑하는 설정만으로 SQL을 자동 생성
- 객체 중심의 개발로 패러다임 불일치 해결
- 데이터베이스 변경에 대한 추상화 제공
// JPA를 사용한 코드
@Entity
public class Member {
@Id
private Long id;
private String name;
private int age;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
// 저장
public void save(Member member) {
entityManager.persist(member); // INSERT SQL 자동 생성
}
// 조회
public Member findById(Long id) {
return entityManager.find(Member.class, id); // SELECT SQL 자동 생성
}
// 연관 객체 조회 (지연 로딩)
public void printMemberTeam(Long memberId) {
Member member = entityManager.find(Member.class, memberId);
System.out.println(member.getName());
System.out.println(member.getTeam().getName()); // 필요할 때만 Team 조회
}
2. JPA 구동 방식
2.1. JPA 아키텍처와 구성 요소
JPA는 계층형 아키텍처로 구성되어 있으며, 주요 컴포넌트들은 다음과 같은 관계를 가진다.

2.2. 구동 단계별 상세 설명
1단계: persistence.xml 설정 파일 로드
JPA는 META-INF/persistence.xml 파일을 통해 설정을 읽는다. 이 파일은 JPA의 모든 설정을 포함하는 핵심 파일이다.
<!-- META-INF/persistence.xml -->
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<!-- 영속성 유닛 정의 -->
<persistence-unit name="hello">
<!-- JPA 구현체 설정 -->
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<!-- 엔티티 클래스 등록 -->
<class>com.example.Member</class>
<class>com.example.Team</class>
<!-- 데이터베이스 연결 속성 -->
<properties>
<!-- 필수 속성 -->
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<!-- 옵션 속성 -->
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="create"/>
</properties>
</persistence-unit>
</persistence>
2단계: EntityManagerFactory 생성
EntityManagerFactory는 JPA의 진입점이자 데이터베이스당 하나 생성되는 무거운 객체다. 이 객체를 생성하는 데는 상당한 비용이 든다.
// EntityManagerFactory 생성 코드
public class JpaMain {
public static void main(String[] args) {
// persistence.xml의 persistence-unit 이름으로 팩토리 생성
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("hello");
try {
// 애플리케이션 로직 실행
runApplication(emf);
} finally {
// 애플리케이션 종료 시 팩토리 닫기
emf.close();
}
}
private static void runApplication(EntityManagerFactory emf) {
// 비즈니스 로직 실행
}
}
EntityManagerFactory의 특징:
- 데이터베이스 연결 정보를 기반으로 생성
- 스레드 안전(thread-safe)하므로 여러 스레드가 동시에 접근 가능
- 애플리케이션 전체에서 한 번만 생성하고 공유해야 함
- 생성 비용이 크므로 필요할 때만 생성하고 재사용
3단계: EntityManager 생성
EntityManagerFactory로부터 EntityManager를 생성한다. EntityManager는 실제 데이터베이스 작업을 수행하는 핵심 객체다.
public class MemberRepository {
private final EntityManagerFactory emf;
public MemberRepository(EntityManagerFactory emf) {
this.emf = emf;
}
public void save(Member member) {
// 요청마다 새로운 EntityManager 생성
EntityManager em = emf.createEntityManager();
// 트랜잭션 시작
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
// 엔티티 저장
em.persist(member);
// 트랜잭션 커밋
tx.commit();
} catch (Exception e) {
// 예외 발생 시 롤백
tx.rollback();
throw e;
} finally {
// 작업 완료 후 EntityManager 닫기
em.close();
}
}
}
EntityManager의 특징:
- 데이터베이스 커넥션을 내부적으로 관리
- 스레드에 안전하지 않음(not thread-safe)
- 요청마다 생성하고 사용 후 반드시 닫아야 함
- 1차 캐시, 쓰기 지연 SQL 저장소, 변경 감지 등의 기능 제공
2.3. 중요 주의사항
2.3.1. 엔티티 매니저 팩토리는 하나만 생성해서 애플리케이션 전체에서 공유
// ❌ 잘못된 예시 - 매번 새로 생성
public class WrongExample {
public void method1() {
EntityManagerFactory emf1 =
Persistence.createEntityManagerFactory("hello");
// ... 작업
emf1.close();
}
public void method2() {
EntityManagerFactory emf2 =
Persistence.createEntityManagerFactory("hello");
// ... 작업
emf2.close();
}
}
// ✅ 올바른 예시 - 싱글톤으로 관리
@Component
public class JpaConfig {
private static EntityManagerFactory emf;
@PostConstruct
public void init() {
if (emf == null) {
emf = Persistence.createEntityManagerFactory("hello");
}
}
@PreDestroy
public void destroy() {
if (emf != null && emf.isOpen()) {
emf.close();
}
}
public EntityManager createEntityManager() {
return emf.createEntityManager();
}
}
2.3.2. 엔티티 매니저는 쓰레드간에 공유하지 않음 (사용하고 버려야 함)
// ❌ 위험한 예시 - EntityManager 공유
public class UnsafeMemberService {
private EntityManager em; // 인스턴스 변수로 공유
public UnsafeMemberService(EntityManagerFactory emf) {
this.em = emf.createEntityManager(); // 생성자에서 한 번만 생성
}
public void processConcurrentRequests() {
// 여러 스레드가 동시에 이 em을 사용하면 문제 발생!
}
}
// ✅ 안전한 예시 - 요청마다 생성
@Service
public class SafeMemberService {
private final EntityManagerFactory emf;
public SafeMemberService(EntityManagerFactory emf) {
this.emf = emf;
}
@Transactional
public void saveMember(Member member) {
// Spring이 트랜잭션마다 적절한 EntityManager 생성/관리
EntityManager em = getCurrentEntityManager(); // Spring이 관리
em.persist(member);
}
}
// Spring 없는 순수 JPA 사용 시
public class PureJpaService {
private final EntityManagerFactory emf;
public void businessMethod() {
EntityManager em = emf.createEntityManager(); // 메서드마다 생성
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
// 비즈니스 로직
tx.commit();
} catch (Exception e) {
tx.rollback();
throw e;
} finally {
em.close(); // 반드시 닫기
}
}
}
2.3.3. JPA의 모든 데이터 변경은 트랜잭션 안에서 실행
JPA는 트랜잭션을 기반으로 동작한다. 데이터 변경 작업(INSERT, UPDATE, DELETE)은 반드시 트랜잭션 안에서 실행해야 한다.
// ❌ 잘못된 예시 - 트랜잭션 없이 데이터 변경
public void saveWithoutTransaction(Member member) {
EntityManager em = emf.createEntityManager();
// 트랜잭션 없이 저장 시도
em.persist(member); // 예외 발생 또는 데이터 무결성 문제
em.close();
}
// ✅ 올바른 예시 - 트랜잭션 내에서 데이터 변경
public void saveWithTransaction(Member member) {
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin(); // 트랜잭션 시작
// 트랜잭션 내에서 데이터 변경 작업
em.persist(member);
tx.commit(); // 변경 내용을 데이터베이스에 반영
} catch (Exception e) {
if (tx.isActive()) {
tx.rollback(); // 예외 발생 시 롤백
}
throw e;
} finally {
em.close();
}
}
// 조회 작업은 트랜잭션 없이도 가능 (단, 영속성 컨텍스트 필요 시 트랜잭션 필요)
public Member findById(Long id) {
EntityManager em = emf.createEntityManager();
try {
// 조회만 하는 경우 트랜잭션 없이도 가능
Member member = em.find(Member.class, id);
return member;
} finally {
em.close();
}
}
트랜잭션의 중요성:
- 원자성(Atomicity): 모든 작업이 성공하거나 모두 실패
- 일관성(Consistency): 데이터 무결성 유지
- 격리성(Isolation): 동시 접근 제어
- 지속성(Durability): 커밋된 변경 사항 영구 저장
4. 결론
JPA는 객체지향 프로그래밍과 관계형 데이터베이스 사이의 간극을 메우기 위해 등장한 ORM 기술의 표준이다. JDBC나 SQL Mapper와 비교했을 때 JPA의 가장 큰 차이는 객체 중심의 사고방식과 생산성 향상에 있다.
JPA의 핵심 가치:
- 객체와 테이블의 매핑 설정만으로 SQL 자동 생성
- 패러다임 불일치 문제 해결 (상속, 연관관계, 객체 그래프 탐색)
- 데이터베이스 독립성 제공
- 생산성 향상과 유지보수성 개선
적용 전략:
- 단순 CRUD: JPA의 기본 기능으로 충분
- 복잡한 조회: JPQL 또는 Criteria API 사용
- 매우 복잡한 통계: 네이티브 SQL 활용
- 동적 쿼리: Criteria API 또는 QueryDSL 사용
'Spring > JPA' 카테고리의 다른 글
| [Basic-3] 엔터티 매핑 (Entity Mapping) (0) | 2026.01.06 |
|---|---|
| [Basic-2] 영속성 컨텍스트 (Persistence Context) (0) | 2026.01.06 |
| [Optimization-7] JPA: 벌크 연산 모니터링 (0) | 2025.12.27 |
| [Optimization-6] JPA: 벌크 연산과 성능 최적화 (0) | 2025.12.27 |
| [Optimization-5] JPA: 벌크 연산의 이해와 ID 생성 전략의 상관관계 (0) | 2025.12.26 |
