일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- ReactHooks
- golang
- 웹애플리케이션서버
- typescript
- react-hook-form
- Chakra
- Spring
- java
- Gin
- RTK
- satisfiles
- JPA
- go
- component
- Redux
- tanstackquery
- hook
- storybook
- 티스토리챌린지
- designpatterns
- backend
- 오블완
- javascript
- css
- springboot
- test
- React
- frontend
- JavaSpring
- Today
- Total
bkdragon's log
JDBC 부터 Spring Data JPA 까지 본문
Spring Boot의 Data Access layer에 대해 알아보자.
JDBC
자바 애플리케이션이 관계형 데이터베이스와 상호작용할 수 있도록 해주는 표준 API이다.
JDBC는 3가지 기능을 표준 인터페이스로 정의하여 제공한다.
- java.sql.Connection - 연결
- java.sql.Statement, PreparedStatement - SQL을 담은 내용
- java.sql.ResultSet - SQL 요청 응답
JDBC 드라이버는 특정 데이터베이스에 맞게 JDBC API 의 인터페이스를 구현한 구현체이다.
JDBC 드라이버 덕분에 개발 과정에선 JDBC 표준만으로 코드를 작성할 수 있다.
JDBC의 동작 흐름은 다음과 같다.
- JDBC 드라이버 로딩 : 사용하고자 하는 JDBC 드라이버를 로딩한다. JDBC 드라이버는 DriverManager 클래스를 통해 로딩된다. (자동 가능)
- Connection 객체 생성 : JDBC 드라이버가 정상적으로 로딩되면 DriverManager를 통해 데이터베이스와 연결되는 세션(Session)인 Connection 객체를 생성한다.
- Statement 객체 생성 : Statement 객체는 작성된 SQL 쿼리문을 실행하기 위한 객체로 정적 SQL 쿼리 문자열을 입력으로 가진다.
- Query 실행 : 생성된 Statement 객체를 이용하여 입력한 SQL 쿼리를 실행한다.
- ResultSet 객체로부터 데이터 조회 : 실행된 SQL 쿼리문에 대한 결과 데이터 셋이다.
- ResultSet, Statement, Connection 객체들의 Close : JDBC API를 통해 사용된 객체들은 생성된 객체들을 사용한 순서의 역순으로 Close 한다.
아래는 데이터를 추가하는 예제이다. 위 동작 흐름과 비교하며 코드를 보자.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class PostgresJdbcInsertExample {
// JDBC URL, 사용자 이름, 비밀번호 설정
private static final String URL = "jdbc:postgresql://localhost:5432/mydb"; // 데이터베이스 이름은 "mydb"로 가정
private static final String USER = "postgres";
private static final String PASSWORD = "password";
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
// 1. PostgreSQL JDBC 드라이버 로딩, 자동으로도 됨.
Class.forName("org.postgresql.Driver");
// 2. 데이터베이스 연결
Connection connection = DriverManager.getConnection(URL, USER, PASSWORD);
// 3. SQL 삽입 쿼리 작성
String insertQuery = "INSERT INTO users (name, email) VALUES (?, ?)";
// 4. PreparedStatement 생성
PreparedStatement preparedStatement = connection.prepareStatement(insertQuery);
// 5. 파라미터 설정
preparedStatement.setString(1, "John Doe");
preparedStatement.setString(2, "johndoe@example.com");
// 6. 쿼리 실행
int rowsAffected = preparedStatement.executeUpdate();
// 7. 실행 결과 확인
if (rowsAffected > 0) {
System.out.println("데이터가 성공적으로 삽입되었습니다.");
}
} catch (ClassNotFoundException e) {
System.out.println("PostgreSQL JDBC 드라이버를 찾을 수 없습니다.");
e.printStackTrace();
} catch (SQLException e) {
System.out.println("데이터베이스 연결 또는 쿼리 실행 중 오류가 발생했습니다.");
e.printStackTrace();
} finally {
// 8. 자원 해제
try {
if (preparedStatement != null) preparedStatement.close();
if (connection != null) connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
코드 자체는 어렵지 않으나 리소스 해제, 쿼리 작성 등의 보일러 플레이트가 많아 보인다.
JDBC Template
JDBC Template 은 JDBC 의 복잡한 리소스 관리와 반복적인 쿼리 작성을 단순화해주는 추상화 레이어이다.
위에 JDBC 예제와 같은 유저를 추가하는 코드가 아래와 같이 바뀐다.
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class UserRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public int addUser(String name, String email) {
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
return jdbcTemplate.update(sql, name, email);
}
}
보일러 플레이트는 많이 줄일 수 있었지만 결국 쿼리를 직접 작성해야하고 자바 객체와 데이터베이스 테이블 간 매핑과 객체지향적으로 코드를 작성하기는 어렵다.
JPA
이제 ORM의 등장이다. JPA 는 자바 객체와 관계형 데이터베이스 테이블 간의 매핑을 지원하는 자바 표준 ORM 이다. JPA는 표준 인터페이스이고 Hibernate 등의 구현체가 실제로 동작을 한다.
그리고 JPA 는 JDBC 위에서 동작한다. JPA 내부적으로 JDBC 를 사용해 쿼리를 실행한다.
기술 | 역할 |
---|---|
JPA | 자바 객체와 데이터베이스 테이블 간의 매핑을 처리하는 고수준의 API. SQL을 자동으로 생성하고, 객체 중심의 데이터베이스 접근을 가능하게 함. |
JDBC | 데이터베이스와 직접 통신하는 저수준의 API. SQL 쿼리를 실행하고 데이터베이스와 상호작용. |
JPA를 사용하려면 Entity, Entity Manager, 영속성 컨텍스트 등의 주요 개념을 알아야한다.
Entity 는 JPA 에서 데이터베이스 테이블에 대응하는 자바 클래스이다. 테이블의 구조를 나타내고 각 인스턴스가 행(row)이 된다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters and setters...
}
Entity Mnager는 Entity의 CRUD 작업과 트랜잭션을 관리하는 핵심 인터페이스이다.
private final EntityManager entityManager;
public void saveUser(User user) {
entityManager.persist(user); // 새로운 엔티티 저장
}
public User findUser(Long id) {
return entityManager.find(User.class, id); // 엔티티 조회
}
@Transactional // 트랜잭션 관리
public void updateUser(User user) {
entityManager.merge(user); // 엔티티 업데이트
}
영속성 컨텍스트는 Entity Manager 에 의해 관리되는 환경이다. 데이터베이스와 자바 어플리케이션 사이의 저장 공간이라고 이해하면 된다. 영속성 컨텍스트에 저장된 Entity는 영속, 비영속, 준영속 등의 상태를 가지는데 이 상태를 이용해서 데이터베이스와의 효율적인 통신을 가능하게 해준다.
영속성 컨텍스트의 자세한 내용과 JPQL, 관계 매핑 등의 다른 개념들은 다른 글에서 정리해보겠다.
Spring Data JPA
Spring Data JPA 는 JPA를 더 쉽게 사용할 수 있게 해주는 추상화 계층이다. 기본적인 CRUD의 자동화와 복잡한 쿼리도 쉽게 구현을 할 수 있다.
이를 가능캐하는 핵심이 JpaRepository Interface 이다. JpaRepository는 여러 Interface를 상속 받아서 만들어진다.
Repository<T, ID>
public interface Repository<T, ID> {
// 아무런 메서드도 정의되지 않은 마커 인터페이스
}
CrudRepository<T, ID>
모든 엔티티에 대한 기본 CRUD 작업을 처리할 수 있는 메서드를 제공한다.
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAllById(Iterable<? extends ID> ids);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
}
앞에 List가 붙은 인터페이스도 있는데 이는 List로 엔티티를 다룰 수 있다.
public interface ListCrudRepository<T, ID> extends CrudRepository<T, ID> {
<S extends T> List<S> saveAll(Iterable<S> entities);
List<T> findAll();
List<T> findAllById(Iterable<ID> ids);
}
PagingAndSortingRepository<T, ID>
Pageable 객체와 Sort 객체를 사용해서 페이징 처리와 정렬 작업을 쉽게 도와주는 Interface
public interface PagingAndSortingRepository<T, ID> extends Repository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
QueryByExampleExecutor
QueryByExampleExecutor은 Example 객체를 사용해 동적 쿼리를 쉽게 작성하게 해주는 기능이다. Example 문자 그래도 예시라는 의미로 받아들이면 된다. 예시 엔티티를 만들어서 그거와 같은 모양의 데이터를 뽑아 올 수 있는 것이다.
public interface QueryByExampleExecutor<T> {
<S extends T> Optional<S> findOne(Example<S> example);
<S extends T> Iterable<S> findAll(Example<S> example);
<S extends T> Iterable<S> findAll(Example<S> example, Sort sort);
<S extends T> Page<S> findAll(Example<S> example, Pageable pageable);
<S extends T> long count(Example<S> example);
<S extends T> boolean exists(Example<S> example);
<S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
}
이 모든 인터페이스를 상속한 것이 JpaRespository를 이다.
JpaRespository를 상속하는 UserRepository를 만들면 User 테이블에 접근하는 계층이 만들어진다.
public interface UserRepository extends JpaRepository<User, Int> {
}
이제 이 유저 레포지토리를 사용하여 위에서 본 다양한 데이터베이스 접근을 할 수 있다. Example 관련 예제를 하나 살펴보자.
public List<User> findUsersByExample(String name, String email) {
// 검색 조건이 되는 User 객체를 생성 (비어있지 않은 필드만 검색에 사용됨)
User userProbe = new User();
userProbe.setName(name);
userProbe.setEmail(email);
// Example 객체 생성
Example<User> example = Example.of(userProbe);
// Example 객체를 사용해 검색
return userRepository.findAll(example);
}
SELECT
user.id,
user.name,
user.email,
user.age
FROM
user
WHERE
user.name = 'test'
AND user.email = 'test@example.com';
이런 쿼리가 날아가게 된다.
하나 신기한것은 직접 작성한 UserRepository 또한 Interface 라는 부분이다. 구현체가 없다.
Spring Data JPA는 JpaRepository 인터페이스를 상속받는 인터페이스를 만나면 이를 구현한 클래스를 동적으로 생성하고 이 클래스를 빈으로 등록해준다.
SimpleJpaRepository 라고 하는 클래스를 기반으로 생성이 된다.
@Repository
@Transactional(
readOnly = true
)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
//... 생략
private final JpaEntityInformation<T, ?> entityInformation;
private final EntityManager entityManager;
//... 생략
@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (this.entityInformation.isNew(entity)) {
this.entityManager.persist(entity);
return entity;
} else {
return this.entityManager.merge(entity);
}
}
//... 생략
save 메서드만 가져와봤는데, 내부적으로 엔티티 매니저를 사용하는 것을 볼 수 있다. (JPA 기반)
JpaRespository 는 쿼리 메서드라고 강력한 기능을 제공하는데 이 기능은 메서드의 이름을 파싱하고 분석해서 쿼리를 직접 생성하는 기능이다.
public interface UserRepository extends JpaRepository<User, Int> {
Optional<User> findByEmail(String email)
}
이렇게 메서드를 추가하면 위 인터페이스의 구현체를 생성할 때 이름을 파싱해서 JPQL 또는 SQL 쿼리로 변환한다.
SELECT u FROM User u WHERE u.email = :email
간단한 쿼리부터 복잡한 쿼리까지 개발자가 쿼리를 직접 작성하지 않고 데이터베이스와 통신이 가능해졌다.
그림을 통해 요약하면 아래와 같은 형태가 될 것이다.
'Java Spring' 카테고리의 다른 글
웹 기술의 발전과정 (0) | 2024.11.12 |
---|---|
Specification 과 Criteria API (0) | 2024.11.08 |
JPA와 하이버네이트 (2) | 2024.10.24 |
Transactional 원리 (0) | 2024.09.24 |