본문 바로가기

Spring

[Spring] JPA 시작하기

JPA란?

JPA(Java Persistence API)는 Java와 관계형 데이터베이스 간의 패러다임 불일치 문제를 해결하고,
데이터베이스 작업을 객체 지향적으로 처리할 수 있게 해주는 ORM 기술의 표준 인터페이스다.

 

※ ORM : 객체지향(Object) 을 관계형 데이터베이스(Relation) 에 매핑(Mapping)

 

JPA 특징

  1. ORM 기술의 표준 인터페이스
    : JPA는 자바 진영(Java EE/Jakarta EE)에서 공식 정의한 ORM 표준이다.
      Hibernate, EclipseLink, OpenJPA 등이 대표적인 구현체이며, 그중 Hibernate가 가장 널리 사용된다.
  2. 객체 중심의 데이터 조작
    : 마치 Java 컬렉션에 객체를 저장하듯, 데이터를 객체 형태로 다룰 수 있음
  3. SQL 자동 생성 및 실행
    : 객체의 필드가 변경되면, JPA가 알아서 적절한 SQL을 생성하여 실행
      덕분에 유지보수성이 높고 생산성도 뛰어남
  4. 패러다임 불일치 문제 해결
    : 객체와 테이블의 구조 차이, 연관관계, 라이프사이클 등의 문제를 JPA가 중간에서 처리
  5. 성능 최적화 기능 제공
    : 1차 캐시, 지연 로딩(Lazy Loading), 변경 감지(Dirty Checking) 등 다양한 성능 관련 기능 제공

 

영속성 컨텍스트란?

JPA의 영속성 컨텍스트는 엔티티 객체를 영속 상태로 관리하는 캐시 역할을 한다.
여기에 저장된 엔티티는 DB와 자동으로 동기화되고, 같은 트랜잭션 내에서는 항상 동일한 객체로 유지된다.

 

주요 기능

  1. 1차 캐시
    : 같은 엔티티를 여러 번 조회해도 DB에 다시 가지 않고, 영속성 컨텍스트에 저장된 객체를 재사용
  2. 동일성 보장
    : 같은 트랜잭션 내에서는 같은 ID의 엔티티는 항상 같은 객체로 유지 (== 비교 가능)
  3. 쓰기 지연 (Write-behind)
    : save()나 persist()를 해도 바로 DB에 반영하지 않고, 트랜잭션이 커밋될 때 한꺼번에 처리함 → 성능 향상
  4. 변경 감지 (Dirty Checking)
    : 엔티티 객체의 값을 바꾸기만 해도, 트랜잭션이 끝날 때 자동으로 SQL UPDATE가 실행

 

Entity

JPA에서는 엔티티 클래스를 통해 Java 객체와 DB 테이블을 매핑한다.
기본적으로 다음과 같은 어노테이션을 사용한다.

@Entity(name = "User")
@Table(name = "user")
public class User {

    // pk
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 기본 키 자동 생성
    private Long id;

    // 필드
    @Column(nullable = false, unique = true)
    private String name;

    // 기본 생성자
    protected User() {}

    // 생성자
    public User(String name) {
        this.name = name;
    }
}
  • @Entity : JPA를 사용하여 객체를 테이블과 매핑할 때 사용 (필수)
  • @Table : 엔티티가 매핑될 DB 테이블 지정 (생략 가능하며 클래스명이 테이블명으로 사용)
  • @Id : 기본 키 필드 지정 (필수)
  • @GeneratedValue : 주키의 생성 방법을 맵핑하는 애노테이션
  • @Column : 필드를 DB 컬럼과 매핑 (생략 가능하며 기본 규칙에 따라 컬럼명 자동 생성)

 

연관관계 설정

  • @OneToOne : 일대일 관계를 나타내는 매핑 정보
  • @OneToMany : 일대다 관계를 나타내는 매핑 정보
  • @ManyToOne : 다대일 관계를 나타내는 매핑 정보
  • @ManyToMany : 다대다 관계를 나타내는 매핑 정보
                               (다대다 설정을 하게되면 중간 매핑테이블(JoinTable)이 자동으로 생성된다.)

단방향

한쪽 엔티티만 관계를 알고 있는 상태 (Post만 User를 알고 있음)

@Entity
public class User {

    @Id @GeneratedValue
    private Long id;

    private String name;
}
@Entity
public class Post {

    @Id @GeneratedValue
    private Long id;

    private String content;

    // Post만 User를 참조 (단방향)
    @ManyToOne
    @JoinColumn(name = "user_id") // 실제 DB 외래 키 이름 지정
    private User user;
}
  • @JoinColumn : 실제 DB에서 외래 키로 사용할 컬럼명 지정

 

양방향

양쪽 엔티티가 서로를 참조하는 관계 ( Post가 User를 알고 있으며, User도 Post 목록을 알고 있음)

@Entity
public class User {

    @Id @GeneratedValue
    private Long id;

    private String name;

    // User -> Post 양방향
    @OneToMany(mappedBy = "user")
    private List<Post> posts = new ArrayList<>();
}
@Entity
public class Post {

    @Id @GeneratedValue
    private Long id;

    private String content;

    // Post -> User 양방향
    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}
  • @OneToMany : mappedBy는 연관관계의 주인이 아님을 의미 (외래 키를 가진 쪽이 주인)

 

Repository 인터페이스

Spring Data JPA에서 리포지토리는 인터페이스로 정의하며,
JpaRepository<엔티티, ID>를 상속하기만 해도 기본 CRUD 기능이 자동으로 구현된다.

public interface UserRepository extends JpaRepository<User, Long> {

}

 

JPA의 장점으로는 직접 SQL을 작성하지 않아도 명명 규칙을 지켜 메서드를 선언하면 JPA가 쿼리를 자동 생성해 실행한다.

public interface UserRepository extends JpaRepository<User, Long> {

    // 메서드 이름 기반
    Optional<User> findByName(String name);
}

 

※ 프로그래밍되어 제공되는 쿼리명 규칙

: 리턴타입 {접두어}{도입부}By{프로퍼티 표현식}(조건식)[(And|Or){프로퍼티 표현식}(조건식)](OrderBy{프로퍼티}Asc|Desc) (매개변수...)

접두어 Find, Get, Query, Count, ...
도입부 Distinct, First(N), Top(N)
프로퍼티 표현식 Person.Address.ZipCode => find(Person)ByAddress_ZipCode(...)
조건식 IgnoreCase, Between, LessThan, GreaterThan, Like, Contains, ...
정렬 조건 OrderBy{프로퍼티}Asc|Desc
리턴 타입 E, Optional<E>, List<E>, Page<E>, Slice<E>, Stream<E>
매개변수 Pageable, Sort

 

JPQL

@Query 어노테이션으로 JPQL 기반 커스텀 쿼리를 직접 작성할 수 있다.

  • 수정/삭제 쿼리는 @Modifying과 함께 사용하며, 트랜잭션 내에서 실행해야 한다.
  • 페이징 및 정렬과도 함께 사용할 수 있다.
public interface UserRepository extends JpaRepository<User, Long> {

    //  @Query로 커스텀 쿼리 작성
    @Query("SELECT u FROM User u WHERE u.name LIKE %:keyword%")
    List<User> searchByNameContaining(@Param("keyword") String keyword);
    
    @Modifying
    @Query("UPDATE User u SET u.status = 'inactive' WHERE u.lastLogin < :date")
    int deactivateUsers(@Param("date") LocalDate date);

    @Query("SELECT u FROM User u WHERE u.status = :status")
    Page<User> findByStatus(@Param("status") String status, Pageable pageable);
}