ORM과 SQL Mapper - Hibernate, JPA
ORM
ORM이란?
객체와 RDB 테이블을 매핑시켜 객체와 관계형 모델의 불일치 문제를 해결하고 RDB 테이블을 객체 지향적으로 사용하게 해주는 기술
객체 필드 <-(Mapping)-> 테이블 데이터
객체와 DB가 연결되어 있기 때문에 개발자가 직접 쿼리를 작성하지 않아도 표준 인터페이스를 기반으로 처리를 할 수 있다.
ORM 장단점
- 장점
- 쿼리 작성 없이 메서드를 통해 객체 중심으로 DB를 조작할 수 있다.
- ORM이 DB 스키마 변경을 자동으로 처리해 주기 때문에 개발자는 Java 코드에 더 집중할 수 있다.
- 단점
- 세밀한 제어가 힘들기 때문에 복잡한 쿼리는 직접 QueryDSL을 통해 작성해야 한다.
- 성능 문제가 발생할 수 있다.
- JPA N+1 문제 등
JPA N+1 문제
게시글을 조회할 때 연관된 댓글도 함께 조회하는 쿼리를 생각해보자
-- 이상적인 쿼리 (게시글 ID가 1인 게시글과 댓글을 함께 가져옴)
SELECT *
FROM board b
LEFT JOIN comment c ON b.board_id = c.board_id
WHERE b.board_id = 1;
위처럼 한 쿼리로 게시글과 관련 댓글을 가져올 수 있지만 JPA를 사용하면 다음과 같이 댓글 개수(N개)의 쿼리가 추가로 생성되는 문제가 있다.
즉, 한 번에 가져올 수 있는 댓글을 댓글의 개수 만큼큼 쿼리를 발생 시켜 성능 저하를 발생시키는 문제를 말한다.
-- 게시글 조회 쿼리
SELECT * FROM board WHERE board_id = 1;
-- 댓글 조회 쿼리 (N번 실행)
SELECT * FROM comment WHERE board_id = 1; -- 첫 번째 댓글
SELECT * FROM comment WHERE board_id = 1; -- 두 번째 댓글
...
SELECT * FROM comment WHERE board_id = 1; -- N 번째 댓글
Hibernate - Spring Data JPA
Java에 대표적인 ORM 프레임워크로는 Hibernate가 있다.
Hibernate란
JPA는 규칙을 정의한 표준 인터페이스이고 Hibernate는 JPA가 정의한 규칙에 맞게 구현한 구현체이다.
즉, JPA가 정의한 엔티티 매니저를 구현하며, JDBC를 사용하여 데이터베이스 연결, SQL 쿼리 생성 및 실행, 객체-테이블 매핑하는 등 실제 동작을 수행하는구현체이다.
아래 코드를 보며 이해해보자.
EntityManager
,@Entity
,@Table
,@Id
,GeneratedValue
등은 모두 JPA 표준 인터페이스/어노테이션이다.EntityManager
의find()
메소드도 JPA가 정의한 메소드이다.find()
메소드를 실행하면 Hibernate가 구현한find()
메소드를 실행하여 실제 동작(User 엔티티에서 id가 1L인 사용자 조회)를 수행한다.- 이때 Hibernate가 구현한
find()
메소드는 DB와 JAVA와 DB 간에 통신을 JDBC를 통해 구현한다.
// JPA 인터페이스 (javax.persistence)
EntityManager em = ...; // EntityManager는 JPA 인터페이스
// 엔티티 (JPA 어노테이션 사용)
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// ...
}
// JPA를 사용한 데이터 조회
User user = em.find(User.class, 1L); // find()는 JPA EntityManager 인터페이스의 메서드
SQL Mapper
SQL Mapper란?
객체의 필드와 SQL을 매핑하여 데이터를 객체화하는 기술이다.
ORM과 다르게 객체 중심이 아닌 쿼리 중심으로써 개발자가 직접 쿼리를 작성한다.
SQL Mapper 장단점
- 장점
- ORM에서 발생하는 성능 문제(JPA N+1 문제 등)를 방지할 수 있다.
- 쿼리를 직접 작성하기 때문에 복잡한 쿼리(조인, 서브 쿼리 등)을 효율적으로 사용할 수 있다.
- JDBC를 사용했을 때 발생하는 불필요한 코드들(객체 매핑, 파라미터 바인딩, 동적 쿼리 등)을 줄일 수 있다.
- 단점
- 쿼리를 개발자가 직접 작성해야 한다.
- 특정 DBMS(MySQL, Oracle 등)에 종속된다.
- 쿼리 중심이기 때문에 ORM의 객체 지향적인 설계의 장점을 누리지 못할 수 있다.
JDBC 대비 장점
- 객체 매핑
JDBC는 객체를 직접 추출하여 매핑해야 한다.
// JDBC 예시 (ResultSet에서 데이터를 수동으로 추출)
ResultSet rs = preparedStatement.executeQuery();
List<User> users = new ArrayList<>();
while (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
users.add(user);
}
하지만 MyBatis는 XML에 객체를 명시하여 자동으로 매핑할 수 있다.
<!-- MyBatis XML 예시 (자동 매핑) -->
<select id="selectUsers" resultType="com.example.User">
SELECT id, name, email FROM users
</select>
- 파라미터 바인딩
JDBC는 코드를 통해 직접 바인딩해야 한다.
// JDBC 예시 (PreparedStatement 사용)
PreparedStatement pstmt = connection.prepareStatement(
"SELECT * FROM users WHERE id = ? AND name = ?"
);
pstmt.setLong(1, userId); // 첫 번째 ?에 userId 값 바인딩
pstmt.setString(2, userName); // 두 번째 ?에 userName 값 바인딩
MyBatis는 명시한 객체의 파라미터 이름을 통해 바인딩할 수 있다.
<!-- MyBatis XML 예시 (파라미터 바인딩) -->
<select id="selectUserByIdAndName" resultType="com.example.User">
SELECT * FROM users WHERE id = #{id} AND name = #{name}
</select>
- 동적 SQL(쿼리)
Mybatis는 조건문, 반복문 등을 사용하여 쿼리를 동적으로 생성할 수 있다.
<!-- MyBatis XML 예시 (동적 SQL) -->
<select id="selectUsers" resultType="com.example.User">
SELECT * FROM users
<where>
<if test="name != null">
AND name = #{name}
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
</select>
JPA란?
엔티티 매니저 팩토리(EntityManagerFactory)와 엔티티 매니저(EntityManager)
- Entity
DB 테이블과 매핑되는 객체 - EntityManagerFactory
EntityManager를 생성하고 관리하는 interface
EntityManager가 생성되면 1:1로 persistent context가 생성됨@PersistenceContext
: 엔티티 팩토리 매니저 주입 - EntityManager
엔티티를 관리하는 역할@PersistenceContext
: 엔티티 매니저 주입
영속성 컨텍스트란?
어플리케이션과 DB 사이에 객체를 영구적으로 보관하는 가상 DB 역할(캐시 서버 역할)
트랜잭션 커밋 시점에 영속성 컨텍스트에 저장된 데이터를 DB에 한번에 저장(EntitiManager.flush()
)한다.
- 영속성 컨텍스트(persist context)
영속성 컨텍스트 역할
- 1차 캐시
- 영속성 컨텍스트는 내부에 엔티티를 보관하는 1차 캐시를 가지고 있다.
- 엔티티를 조회할 때 먼저 1차 캐시를 확인하고, 없으면 데이터베이스에서 조회하여 1차 캐시에 저장한다.
- 이후 동일한 엔티티를 다시 조회하면 데이터베이스에 접근하지 않고 1차 캐시에서 가져온다.
- 쓰기 지연 (Transactional Write-behind)
- 엔티티를 변경(persist, remove 등)해도 바로 데이터베이스에 반영되지 않고, 영속성 컨텍스트 내부에 SQL 쿼리를 쌓아둔다.
- 트랜잭션 커밋 시점에 쌓아둔 쿼리를 한 번에 데이터베이스에 보내어 (flush) 데이터베이스 접근 횟수를 줄인다.
- 변경 감지 (Dirty Checking)
- 영속성 컨텍스트는 엔티티의 스냅샷을 보관하고 있다가, 트랜잭션 커밋 시점에 엔티티의 변경 사항을 감지하여 변경된 내용만 UPDATE 쿼리를 생성한다.
- 지연 로딩 (Lazy Loading) - 특정 객체만 가져옴?
- 연관된 엔티티를 즉시 로딩하지 않고, 실제로 사용되는 시점에 로딩한다.
- 지연 로딩 설정을 하면 프록시 객체를 생성하고 실제 사용되는 시점에 로딩하여 불필요한 데이터베이스 조회를 줄여 성능을 향상시킨다.
// @PersistenceContext : Spring이 EntityManager를 생성해서 스프링 빈에 주입
@PersistenceContext
private EntityManager em;
영속성 컨텍스트 저장 & 조회
- 영속성 컨텍스트에 엔티티 저장 :
EntityManager.persist(entity)
key:value 형태로 데이터가 저장되며 key == 객체의 id 값
private EntityManager em;
public void save(Member member) {
em.persist(member); // 영속성 컨텍스트에 member 엔티티 저장
}
- 영속성 컨텍스트에서 엔티티 조회 :
EntityManager.find(entity)
private EntityManager em;
public Member findOne(Long id) {
return em.find(Member.class, id); // 영속성 컨텍스트에서 member를 찾아서 반환
}
- 영속성 컨텍스트에 엔티티가 있는 경우
영속성 컨텍스트에서 엔티티 반환
- 영속성 컨텍스트에 엔티티가 없는 경우
DB에서 엔티티 조회해서 영속성 컨텍스트에 저장한 후 영속성 컨텍스트에서 반환
- 출처
출처
https://colevelup.tistory.com/21
https://velog.io/@kimdy0915/Transactional-%EC%82%AC%EC%9A%A9%EA%B3%BC-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8persistence-context
https://makemepositive.tistory.com/49
'Back-end' 카테고리의 다른 글
객체 지향 설계의 5가지 원칙(SOLID) (4) | 2024.09.13 |
---|