JPA(Java Persistent API)
JPA(Java Persistence API) : ORM 기술의 표준 인터페이스
ORM은 객체와 관계형 데이터베이스를 매핑해 주는 기술이며, JPA는 이를 표준화한 것이다.
그러니까, 자바 애플리케이션에서 관계형 데이터베이스를 사용할 때 이를 객체와 매핑해 주며 사용하기 위한 인터페이스라는 것이다.
따라서 사용하기 위해서는 구현체가 필요하고, JPA를 구현한 대표적인 프레임워크로는 Hibernate, EclipseLink, DataNucleus 등이 있으며, 이 중 Hibernate가 가장 널리 사용된다.
객체지향 프로그래밍은 다형성, 상속 등의 특성을 활용해 유연하고 확장 가능한 코드를 작성할 수 있게 해 준다.
하지만 이러한 객체들을 관계형 데이터베이스에 저장하려고 하면, 객체들의 관계를 테이블에 맞춰 저장하고 관리해줘야 하기 때문에 복잡하고 어려워진다.
JPA를 사용하면, 객체와 데이터베이스 테이블 간의 매핑 설정만으로도 객체의 상태를 데이터베이스에 맞춰 저장하고 관리할 수 있다. 이로 인해 개발자는 객체 지향적인 코드 작성에 집중할 수 있으며, 데이터베이스와의 패러다임 불일치 문제를 걱정하지 않아도 된다.
JPA 사용 예시
사용자 정보를 데이터베이스에 저장하고 조회하는 간단한 작업을 수행한다고 가정해 보자. JPA 를 사용하지 않고 이를 수행하기 위해서는 아래처럼 작성해야 한다.
public class UserDao {
private DataSource dataSource;
public UserDao(DataSource dataSource) {
this.dataSource = dataSource;
}
public void addUser(User user) throws SQLException {
String sql = "INSERT INTO users (username, email) VALUES (?, ?)";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, user.getUsername());
pstmt.setString(2, user.getEmail());
pstmt.executeUpdate();
}
}
public User getUser(String username) throws SQLException {
String sql = "SELECT * FROM users WHERE username = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, username);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
return new User(rs.getString("username"), rs.getString("email"));
}
return null;
}
}
}
위의 코드에서 볼 수 있듯이, 각각의 데이터베이스 연산을 위해 SQL 쿼리를 직접 작성해야 한다. 이는 시간이 많이 걸릴 뿐 아니라 오류가 발생하기도 쉽다.
이때 JPA를 사용해 엔티티 클래스를 정의하면(위에서 서술한 대로 JPA를 사용했다는 것은 구현했다는 뜻이니, 정확히 말하면 Hibernate를 사용한 것이다.)
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username")
private String username;
@Column(name = "email")
private String email;
// Constructor, Getter, Setter 생략
public class UserDao {
@PersistenceContext
private EntityManager em;
public void addUser(User user) {
em.persist(user);
}
public User getUser(String username) {
return em.createQuery("SELECT u FROM User u WHERE u.username = :username", User.class)
.setParameter("username", username)
.getSingleResult();
}
}
SQL 쿼리를 직접 작성하는 대신 객체와 데이터베이스 테이블 간의 매핑을 정의하고, 간단한 메서드 호출로 데이터베이스 작업을 수행할 수 있다. 이는 편리할 뿐만 아니라, 코드의 가독성과 유지보수성도 높일 수 있다.
JPA의 장점
위 예시를 토대로 장점을 정리해 보면,
- 객체와 데이터베이스 간 패러다임 불일치 해결
- 테이블 간의 매핑을 설정하는 것만으로 JPA가 객체의 상태를 데이터베이스에 맞춰 저장하고 관리해 준다.
- 생산성 향상
- CRUD 작업을 위한 기본적인 SQL 쿼리를 자동으로 생성 -> 복잡한 SQL 쿼리를 직접 작성할 필요 X
- 간단하게 메서드를 호출하는 것만으로 데이터베이스 작업을 수행할 수 있다.
- 👉 개발 시간 단축, 생산성 향상
- 유지보수성 향상
- SQL 쿼리가 여러 곳에 흩어져 있다면 데이터베이스 구조가 변경될 때마다 모든 관련 SQL 쿼리를 찾아 수정해야 한다.
- 반면에 JPA는 데이터베이스 구조가 변경되어도 대부분의 경우 알아서 처리 -> 관련 코드를 일일이 수정할 필요 X
- 객체 중심의 코드 -> 가독성이 좋고 이해하기 쉽다.
- 👉 유지보수성 향상
- 데이터베이스 독립성
- 데이터베이스를 변경하려고 할 때, 기존에 작성된 SQL 쿼리가 새로운 데이터베이스에서 작동하지 않을 수 있어 수정이 필요한 경우도 있다.
- 반면에 JPA는 데이터베이스에 독립적이므로 데이터베이스 변경 시 코드 수정이 최소화된다.
- 데이터베이스를 변경하려고 할 때, 기존에 작성된 SQL 쿼리가 새로운 데이터베이스에서 작동하지 않을 수 있어 수정이 필요한 경우도 있다.
- 성능 향상
- 개발자가 직접 작성한 SQL 쿼리는 최적화되지 않을 수 있음 -> 애플리케이션 성능 저하 가능성 ▲
- JPA는 지연 로딩, 쓰기 지연, 1차 캐시 등 다양한 성능 최적화 기능을 제공
- 👉 불필요한 데이터베이스 접근▼, 성능▲
- 지연 로딩(Lazy Loading)
- 필요할 때까지 관련된 엔티티를 로딩하지 않음
- ex) 사용자 정보 조회 시 - 해당 사용자의 모든 게시물을 바로 로딩하지 않고, 실제로 게시물 정보가 필요할 때만 데이터베이스에서 조회 가능.
- 초기 로딩 시간을 줄이고 리소스 사용을 최적화
- 1차 캐시(First-Level Cache)
- 영속성 컨텍스트 내에 1차 캐시를 유지
- -> 한 트랜잭션 내에서 같은 엔티티에 대한 반복적인 조회 요청이 있을 때 데이터베이스에 여러 번 접근하는 것을 방지
- -> 데이터베이스 부하▼, 성능▲
- 쓰기 지연(Write Behind)
- 트랜잭션이 커밋되기 전까지 변경된 엔티티를 메모리에 보관
- 커밋 시점에 변경 사항을 데이터베이스에 반영
- -> 데이터베이스와의 불필요한 연결과 데이터 전송 최소화
- 배치 처리(Batch Processing)
- 여러 개의 삽입, 업데이트, 삭제 등의 쿼리를 하나의 배치 작업으로 묶어 처리 가능
- 네트워크 비용 감소, 데이터베이스 처리 성능 향상
- 쿼리 캐시(Query Cache)
- 자주 사용되는 쿼리의 결과를 캐시에 저장 -> 동일한 쿼리 요청이 있을 때 캐시에서 결과를 바로 가져옴(데이터베이스 접근 X)
JPA의 동작 원리
- 애플리케이션과 JDBC 사이에서 동작 : JDBC API를 사용해 데이터베이스와 통신
- Entity 객체 분석 및 SQL 생성
- 애플리케이션에서 JPA의 persist() 메서드 호출
- JPA : Entity 객체 분석 -> 적절한 SQL 생성
- 생성된 SQL -> JDBC API를 통해 데이터베이스에 전송되어 실행
- 객체-관계 매핑
하이버네이트(Hibernate)
하이버네이트는 Java 언어를 위한 ORM 프레임워크이며, JPA 인터페이스를 구현한 구현체이다.
따라서 이도 JDBC API를 내부적으로 사용하고, JPA의 특징, 장점을 그대로 가지고 우리가 직접 사용할 수 있게 해 준다.
Hibernate 주요 특징
JPA의 구현체이기 때문에 당연히 JPA 특징과 같겠지만, 다시 정리해 보면 다음과 같다.
- ORM 기능 : 객체와 관계형 데이터베이스 간의 매핑
- JDBC 추상화
- JPQL(Java Persistence Query Language), 네이티브 SQL, Querydsl 등 다양한 쿼리 언어 지원
- 캐싱 기능
- 데이터베이스 독립성
Hibernate 사용 방법
위 JPA에서 간단하게 알아본 사용 방법을 좀 더 자세히 알아보자.
1. 엔티티 클래스 정의
JPA 어노테이션을 사용해 데이터베이스 테이블과 매핑되는 엔티티 클래스를 정의한다. 대표적인 어노테이션으로는 @Entity,
@Table
, @Id
, @Column
등이 있다.
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "email")
private String email;
// Getter, Setter, Constructor
}
2. EntityManager 생성
EntityManagerFactory
를 통해 EntityManager
를 생성한다. 데이터베이스 연결, 트랜잭션 관리를 수행한다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("myPersistenceUnit");
EntityManager em = emf.createEntityManager();
3. CRUD 작업 수행
EntityManager
를 구현한 메서드(예시에선 em
)를 사용하여 엔티티에 대한 CRUD 작업을 수행하면 된다.
위에서 만든 user
테이블에 대한 CRUD를 구현한다고 하면,
// Create
em.getTransaction().begin();
User user = new User("Chaeyami", "chaeyami@example.com");
em.persist(user);
em.getTransaction().commit();
// Read
User foundUser = em.find(User.class, user.getId());
// Update
em.getTransaction().begin();
foundUser.setEmail("chaeyami02@example.com");
em.merge(foundUser);
em.getTransaction().commit();
// Delete
em.getTransaction().begin();
em.remove(foundUser);
em.getTransaction().commit();
4. 쿼리 작성
위에서 말한 대로 JPQL, 네이티브 SQL, Querydsl 등을 사용할 수 있다. 만약 JPQL이나 네이티브 SQL을 사용해 user
테이블에 있는 모든 컬럼을 가져오고 싶다면,
// JPQL
List<User> users = em.createQuery("SELECT u FROM User u", User.class).getResultList();
// Native SQL
List<User> users = em.createNativeQuery("SELECT * FROM users", User.class).getResultList();
이렇게 작성해서 실행할 수 있다.
참고 : Spring Data JPA
https://chaeyami.tistory.com/257
GitHub 댓글