[Spring] JPA란?

artp·2025년 5월 23일

spring

목록 보기
9/11
post-thumbnail

JPA(Java Persistence API)는 자바에서 ORM(Object-Relational Mapping)을 표준화한 인터페이스입니다.

쉽게 말해, JPA는 자바 객체와 데이터베이스 테이블을 자동으로 매핑해주는 기술입니다.

JPA 자체는 인터페이스이며, 실제 구현체로는 대표적으로 Hibernate가 있습니다. Hibernate는 사실상의 표준으로 Spring Boot에서 사용하는 JPA는 대부분 Hibernate 기반입니다.

ORM이란?

ORM은 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑해주는 기술입니다.

전통적인 방식 (JDBC)

String sql = "SELECT id, name, email FROM users WHERE id = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setLong(1, userId);
ResultSet rs = pstmt.executeQuery();

User user = null;
if (rs.next()) {
	user = new User();
    user.setId(rs.getLong("id"));
    user.setName(rs.getString("name"));
    user.setEmail(rs.getString("email"));
}

JPA 방식

// 같은 기능을 JPA로 구현
User user = entityManager.find(User.class, userId);
  • 같은 기능을 JPA로 구현하면 코드가 훨씬 간단해집니다.

JPA 사용 이유 - JDBC의 한계

기존 JDBC 방식은 다음과 같은 문제점이 있었습니다.

  • SQL을 직접 작성해야 함 - 반복적이고 실수하기 쉬움
  • ResultSet을 자바 객체로 직접 변환 필요 - 번거로운 매핑 작업
  • DB 스키마 변경 시 자바 코드와 SQL을 일일이 수정해야 함 - 유지보수의 악몽
  • 트랜잭션, 커넥션 등 부수적인 자원 관리 코드가 많음 - 비즈니스 로직에 집중하기 어려움

JPA는 이러한 반복적이고 불편한 작업을 대신 처리해 줍니다.

JPA의 장점

항목설명
생산성SQL 작성 줄어듦, 반복 코드 감소
유지보수성엔티티 클래스 위주로 개발 → 변경 추적 쉬움
객체지향적 개발객체 중심의 도메인 설계 가능
트랜잭션 처리EntityManager가 내부적으로 처리 지원
캐싱1차 캐시(Persistence Context)로 성능 최적화

비교 예시: JDBC로 사용자 생성

public void createUser(User user) {
    Connection conn = null;
    PreparedStatement pstmt = null;
    
    try {
        conn = dataSource.getConnection();
        conn.setAutoCommit(false); // 트랜잭션 시작
        
        String sql = "INSERT INTO users (name, email, created_at) VALUES (?, ?, ?)";
        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, user.getName());
        pstmt.setString(2, user.getEmail());
        pstmt.setTimestamp(3, new Timestamp(System.currentTimeMillis()));
        
        pstmt.executeUpdate();
        conn.commit(); // 트랜잭션 커밋
        
    } catch (SQLException e) {
        if (conn != null) {
            try {
                conn.rollback(); // 롤백
            } catch (SQLException ex) {
                // 로그 처리
            }
        }
        throw new RuntimeException(e);
    } finally {
        // 자원 정리
        if (pstmt != null) try { pstmt.close(); } catch (SQLException e) {}
        if (conn != null) try { conn.close(); } catch (SQLException e) {}
    }
}

비교 예시: JPA로 사용자 생성

@Transactional
public void createUser(User user) {
	entityManager.persist(user);
}
  • JPA를 사용할 때의 코드 양이 현저히 적습니다.

JPA의 장점과 단점

장점

항목설명실제 효과
생산성SQL 작성 줄어듦, 반복 코드 감소개발 시간 단축
유지보수성엔티티 클래스 위주로 개발 → 변경 추적 쉬움스키마 변경 시 자동 반영
객체지향적 개발객체 중심의 도메인 설계 가능비즈니스 로직에 집중
트랜잭션 처리EntityManager가 내부적으로 처리 지원트랜잭션 관리 자동화
캐싱1차 캐시(Persistence Context)로 성능 최적화동일 엔티티 중복 조회 방지
지연 로딩필요할 때만 연관 데이터 로딩메모리 사용량 최적화

단점

  • 복잡한 SQL 작성의 어려움: 복잡한 통계 쿼리나 대용량 처리에는 한계
  • 성능 튜닝의 어려움: 자동 생성된 SQL을 최적화하기 어려움
  • 학습 곡선 (러닝 커브): 영속성 컨테스트, 지연 로딩 등 개념 이해 필요
  • 디버깅의 복잡성: 실제 실행되는 SQL을 예측하기 어려움

JPA 핵심 개념들

1. 엔티티 (Entity)

엔티티는 데이터베이스 테이블과 매핑되는 자바 객체입니다.

@Entity
@Table(name = "users") // 테이블 이름 지정 (생략 시 클래스명 사용)
public class User {
	@Id // 기본키 지정 (*필수)
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 증가
    private Long id;
    
    @Column(name = "user_name", nullable = false, length = 50) // 컬럼 매핑
    private String name;
    
    @Column(unique = true) // 유니크 제약조건
    private String email;
    
    @Temporal(TemporalType.TIMESTAMP) // 날짜/시간 타입 지정
    @Column(name = "created_at)
    private Date createdAt;
    
    // 기본 생성자 필수 (JPA 요구사항)
    public User() {}
    
    // 생성자, getter, setter ...
}
  • 엔티티는 데이터베이스 테이블과 매핑되는 자바 클래스입니다.
  • @Entity@Id는 필수 애노테이션이며, 컬럼은 @Column으로 세부 설정 가능합니다.
  • JPA는 이 엔티티를 기준으로 데이터베이스 연산을 수행합니다.

2. 영속성 컨테스트 (Persistence Context)

영속성 컨텍스트는 엔티티를 관리하는 환경입니다. 1차 캐시 역할을 하며, 동일한 엔티티는 한 번만 DB에서 조회합니다.

// 동일한 ID로 두 번 조회해도 SQL은 한 번만 실행됨 (캐싱)
user user1 = entityManager.find(User.class, 1L); // SQL 실행
user user2 = entityManager.find(User.class, 1L); // 캐시에서 반환

System.out.println(user1 == user2); // true (동일한 객체)
  • EntityManager 내부의 1차 캐시로, 영속 상태의 엔티티를 관리합니다.
  • 같은 ID로 조회하면 DB 쿼리를 생략하고 캐시에서 반환합니다.
  • 엔티티의 동일성을 보장하고, 성능도 향상됩니다.

3. 엔티티의 생명주기

엔티티는 JPA 기준으로 비영속 → 영속 → 준영속 → 삭제 상태를 가집니다.

// 1. 비영속 (new/transient) - 새로운 객체, JPA와 관계없음
User user = new User();
user.setName("홍길동");

// 2. 영속 (managed) - 영속성 컨텍스트에 관리됨
entityManager.persist(user);

// 3. 준영속 (detached) - 영속성 컨텍스트에서 분리됨
entityManager.detach(user);

// 4. 삭제 (removed) - 삭제 예정 상태
entityManager.remove(user);
  • JPA는 영속 상태의 에닡티만 추적하며, 변경 시 자동으로 UPDATE 쿼리를 생성합니다.
  • 상태마다 persist(), detach(), remove() 같은 메서드로 조작됩니다.

4. 연관관계 매핑

객체 간의 관계를 DB 외래 키처럼 표현하는 것이 연관관계 매핑입니다.

일대다 관계 (One-to-Many)

@Entity
public class Team {
	@Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team", cascade = CascadeType.ALL)
    private List<User> users = new ArrayList<>();
}

@Entity
public class user {
	@id @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
    @JoinColumn(name = "team_id")
    private Team team;
}
  • @ManyToOne, @OneToMany, @JoinColumn 등을 이용해 관계를 설정합니다.
  • 실무에서는 단방향 매핑을 우선적으로 고려하고, 필요 시 양방향을 설계합니다.

지연 로딩과 즉시 로딩

JPA는 객체 관계를 언제 로딩할지 전략을 설정할 수 있습니다.

// 지연 로딩 (LAZY) - 실제 사용할 때 조회
User user = entityManager.find(User.class, 1L);
System.out.println(user.getName()); // User만 조회
System.out.println(user.getTeam().getName()); // 이때 Team 조회

// 즉시 로딩 (EAGER) - 처음부터 함께 조회
// @ManyToOne(fetch = FetchType.EAGER)
  • LAZY는 실제 사용하는 시점에 로딩(성능 유리), EAGER는 즉시 로딩합니다.
  • 실무에서는 LAZY가 기본이며, EAGER는 주의해서 사용해야 합니다.

JPA 상태 전환 메서드

JPA의 persist(), detach(), remove(), merger()는 모두 EntityManager가 제공하는 엔티티 상태 전환용 메서드입니다.
각 메서드는 엔티티의 생명주기 상태를 바꾸는 역할을 하며, JPA 동작의 핵심 중 하나입니다.

1. persist(entity)

  • 상태 전환: 비영속 → 영속
  • 기능: 새로운 엔티티 객체를 영속성 컨텍스트에 등록
  • 이때부터 1차 캐시에서 관리되며, 트랜잭션 커밋 시 INSERT SQL이 실행됨
User user = new User();
user.setName("홍길동");
entityManager.persist(user); // 이제부터 영속 상태

2. detach(entity)

  • 상태 전환: 영속 → 준영속
  • 기능: 영속성 컨텍스트로부터 엔티티를 분리(detach)
  • 이후 JPA는 해당 객체의 변경 사항을 추적하지 않음
User user = entityManager.find(User.class, 1L); // 영속 상태
entityManager.detach(user); // 더 이상 관리되지 않음

3. remove(entity)

  • 상태 전환: 영속 → 삭제(removed)
  • 기능: 영속성 컨텍스트에서 삭제 대상으로 마킹
  • 트랜젝션 커밋 시 DELETE SQL이 실행됨
User user = entityManager.find(User.class, 1L);
entityManager.remove(user); // 삭제 예약 (DB에서 제거됨)

4. merge(entity)

  • 상태 전환: 준영속 → 영속
  • 기능: 준영속 상태의 객체나 새 객체를 영속성 컨텍스트에 병합(merge)하여 새로운 영속 객체를 반환
  • 기존 DB의 값과 병합하여 트랜잭션 커밋 시 UPDATE SQL이 실행됨
User detachedUser = new User();
detachedUser.setId(1L); // 기존에 DB에 있던 ID
detachedUser.setName("업데이트된 이름");

// 병합: 새로운 영속 객체를 반환
User mergedUser = entityManager.merge(detachedUser);
- merge()는 원본 객체(detachedUser)를 그대로 영속화하지 않고, 복사한 새로운 객체(mergedUser)를 만들어 영속화합니다.
- 따라서 이후 작업은 반드시 mergedUser를 사용해야 합니다.

JPA의 동작 방식

1. EntityManagerFactory 생성

EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpatest");
  • JPA 설정(persistence.xml 또는 application.yml)을 기반으로 전체 애플리케이션에서 공통으로 사용할 팩토리 객체 생성
  • 싱글톤처럼 사용하며, EntityManager를 만들어내는 공장 역할을 수행

2. EntityManager 생성 (트랜잭션 단위)

EntityManager em = emf.createEntityManager();
  • DB 작업을 수행하는 핵심 객체이며, 트랜잭션 범위에서 사용되는 단기 생명주기 객체
  • 내부에 1차 캐시(영속성 컨텍스트)를 가지며, 엔티티를 관리함

3. 트랜잭션 시작

EntityTransaction tx = em.getTransaction();
tx.begin(); // 트랜잭션 시작
  • 트랜잭션을 명시적으로 시작해야 JPA가 변경사항을 추적하고 SQL을 실행함
  • EntityManager는 반드시 트랜잭션 안에서 동작해야 정상적으로 persist() 등이 작동함

4. 엔티티 저장

em.persist(new User("kim")); // 영속화
  • User 객체는 비영속 상태 → 영속 상태로 전환됨
  • 아직 SQL은 실행되지 않고, 영속성 컨텍스트(1차 캐시)에만 등록됨
  • 트랜잭션 커밋 시점에 INSERT 쿼리가 생성됨

5. 트랜잭션 커밋 → DB 반영

tx.commit(); // DB 반영
  • 커밋 순간 JPA는 변경사항을 감지(Dirty Checking)하고 필요한 SQL을 자동 생성
  • 이때 DB에 INSERT/UPDATE/DELETE 쿼리가 전송됨

실제 로그 확인

// JPA 실행 시 다음과 같은 SQL이 자동 생성됨
Hibernate: 
    insert 
    into
        users
        (created_at, email, user_name) 
    values
        (?, ?, ?)
  • JPA는 직접 SQL을 작성하지 않아도, 객체의 상태 변화에 따라 SQL을 자동 생성해줌
  • 개발자는 객체만 다루면 되고, SQL은 Hibernate가 대신 처리함 (ORM의 핵심)

Spring에서는 @Transactional 사용

Spring Boot 환경에서는 @Transactional 애노테이션으로 트랜잭션을 자동으로 시작하고 종료할 수 있습니다.
직접 begin()/commit()을 호출하지 않아도 되며, 메서드 단위로 트랜잭션 범위를 지정할 수 있습니다. 메서드가 시작될 때 트랜잭션을 열고, 정상 종료되면 자동으로 커밋, 예외 발생 시 롤백됩니다.

@Service
public class UserService {

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    public void saveUser(User user) {
        entityManager.persist(user); // 트랜잭션 자동 시작
        // 예외가 없으면 자동 커밋됨
    }
}
  • 메서드 실행 시 Spring AOP(프록시)가 동작하여 트랜잭션을 자동 시작
  • persist()와 같은 JPA 명령이 트랜잭션 내에서 수행됨
  • 메서드가 예외 없이 종료되면 commit() 호출
  • RuntimeException 또는 Error가 발생하면 자동으로 rollback() 처리

주의할 점

  • @Transactionalpublic 메서드에서만 제대로 동작합니다 (Spring AOP의 제약).
  • 기본적으로는 RuntimeException에만 rollback되며, 체크 예외(예: IOException 등)는 rollback되지 않습니다.
profile
donggyun_ee

0개의 댓글