안녕하세요. 이 포스트는 JAVA 및 스프링 프로그램에서 데이터베이스 접근을 위해 제공하는 JDBC, Java JPA, 그리고 Spring Data JPA 의 개념을 대략적으로 정리한 글입니다. 개념을 위주로 설명하는 글인 만큼 각 API의 자세한 사용법은 공식 문서나 다른 블로그를 참고해주세요.
JDBC, Java JPA, Spring Data JPA는 모두 자바 프로그램 내에서 DB에 접속하기 위해 만들어진 API 입니다. 어떤 데이터베이스를 사용하든지 간에 프로그램에서는 동일한 API를 사용할 수 있어서 서버의 비즈니스 로직과 DB 접근 로직을 분리할 수 있다는 장점이 있습니다.
JDBC
가장 먼저 JDBC에 대해서 알아보겠습니다. JDBC란 Java Database Connectivity 의 약어로 말 그대로 자바가 데이터베이스와 연결될 수 있도록 제공하는 API라는 의미를 담고 있습니다.
JDBC를 사용한 자바 프로그램은 다음과 같은 구조로 이루어져 있습니다. 먼저 Java Web Application 이란 개발자가 작성한 자바 프로그램을 말합니다. 이 프로그램 내에서 JDBC API를 임포트하고 있습니다. 그리고 JDBC API는 위에서 설명한 API이고, JDBC Driver는 데이터베이스로의 접근, 클라이언트의 쿼리를 DB로 전달, 쿼리 결과를 클라이언트에 반환하는 프로토콜의 구현체입니다. 데이터베이스마다 차이가 있는 부분이므로, JDBC Driver는 데이터베이스마다 각각 존재합니다. 벤더사에서는 프로그래머들을 위해 드라이버 프로그램을 배포하고 있으니, 자신이 사용하는 DBMS의 드라이버를 다운받아 사용할 수 있습니다.
아래 코드는 프로그램 내에서 드라이버를 연동하는 코드 예제입니다(출처 : 위키백과). 미리 프로그램의 build path에 드라이버를 연동해 놓으면 다음의 코드를 통해 jdbc driver를 찾아서 커넥션을 맺게 됩니다.
Connection conn = DriverManager.getConnection(
"jdbc:somejdbcvendor:other data needed by some jdbc vendor",
"myLogin",
"myPassword" );
try {
/* you use the connection here */
} finally {
//It's important to close the connection when you are done with it
try { conn.close(); } catch (Throwable e) { /* Propagate the original exception
instead of this one that you want just logged */ logger.warn("Could not close JDBC Connection",e); }
}
커넥션을 생성한 다음 데이터베이스에 쿼리를 전달하는 과정은 다음과 같습니다.(코드 출처) 첫번째 주석에서는 Statement 객체를 생성하는데요, 이 객체가 SQL문에 해당합니다. 다음으로는 SQL문에 실행할 쿼리를 파라미터로 넣어주고 executeQuery() 를 호출하면 DB의 쿼리가 실행되고, 그 결과값이 ResultSet 객체로 반환됩니다. ResultSet 객체에서 데이터를 꺼낼 때에는 getXXX() 를 사용합니다.
public static void viewTable(Connection con) throws SQLException {
// 1. Statement 객체 생성
String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";
try (Statement stmt = con.createStatement()) {
// 2. 쿼리 실행
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
String coffeeName = rs.getString("COF_NAME");
int supplierID = rs.getInt("SUP_ID");
float price = rs.getFloat("PRICE");
int sales = rs.getInt("SALES");
int total = rs.getInt("TOTAL");
System.out.println(coffeeName + ", " + supplierID + ", " + price +
", " + sales + ", " + total);
}
} catch (SQLException e) {
JDBCTutorialUtilities.printSQLException(e);
}
}
이처럼 JDBC를 활용한 프로그램에서는 쿼리문을 개발자가 직접 작성하고, JDBC는 쿼리 수행을 전달해 줄 뿐입니다. 이러한 번거로운 과정을 제거하고 DBMS - 프로그램 간 의존성을 걷어내기 위해 등장한 것이 다음에 소개할 JPA 입니다.
Java JPA(Java Persistence API)
Object Relational Mapping
JPA를 이해하기 위해서는 먼저 ORM(Object Relation Mapping) 개념을 이해해야 합니다. ORM은 지속성(persistence)을 유지하기 위해 도입된 개념으로, POJO 자바 클래스를 관계형 데이터베이스에 맵핑하는 기술을 의미합니다. 쉽게 표현하자면, 순수 자바 클래스와 데이터베이스의 데이터가 서로 대응되게끔 만든다는 의미입니다.
persistence : 일반적으로 프로그램이 종료되면 메모리에 적재된 데이터는 사라진다. 지속성(persistence) 이란 프로그램 종료 후에도 데이터가 유지되는 성질을 의미한다.
한편 ORM은 Object-Relational Middleware 의 머릿글자라고도 불리는데, 이 명칭은 설계 관점에서 ORM의 장점을 보여줍니다. 프로그램과 DBMS 사이의 미들웨어 역할을 통해 DB 접근을 추상화함으로써 프로그래머가 비즈니스 로직에만 집중할 수 있도록 하는 것입니다. JDBC API 를 사용하는 프로그램이라면 DB 접근을 위해서 쿼리문을 작성해야 하고, 데이터베이스에서 데이터를 가져온 다음, 해당 데이터를 POJO 클래스에 넣는 작업을 모두 프로그래머가 수행합니다. ORM이 목표로 하는 것은 이러한 작업을 JPA가 내부에서 수행하고 프로그래머는 그 결과로 생성된 POJO 클래스를 곧바로 이용할 수 있게 만드는 것입니다.
JPA는 자바 프로그램 내에서 ORM을 사용하도록 지원하는 API 입니다. JPA는 javax.persistence 에 정의되어 있는 인터페이스로, 실제로 프로그래밍을 할 때에는 해당 JPA를 구현한 구현체를 임포트해서 사용합니다. 대표적인 구현체로는 Hibernate가 있습니다.
중요 개념
entity
entity 클래스는 관계형 데이터베이스의 하나의 테이블에 대응됩니다. 그리고 각각의 엔티티 인스턴스는 테이블의 각 행에 대응됩니다. @Entity
어노테이션을 사용하면 엔티티 클래스로 등록됩니다.
create table Contact (
id integer not null,
first varchar(255),
last varchar(255),
middle varchar(255),
notes varchar(255),
starred boolean not null,
website varchar(255),
primary key (id)
)
위와 같은 DB 테이블이 있으면, 아래와 같이 각 컬럼을 멤버 변수와 대응시킬 수 있습니다. id 컬럼은 멤버변수 id와 대응되는 것을 보실 수 있습니다(각각 대응시켜보세요!).
@Entity(name = "Contact")
public static class Contact {
@Id
private Integer id;
private Name name;
private String notes;
private URL website;
private boolean starred;
//Getters and setters are omitted for brevity
}
@Embeddable
public class Name {
private String first;
private String middle;
private String last;
// getters and setters omitted
}
한편 테이블 내 컬럼 중에서 의미상 서로 의존 관계에 있는 컬럼이 존재할 수 있습니다. 이 경우 @Embeddable
어노테이션을 생성한 객체를 만들어서 의존 관계에 있는 컬럼을 클래스로 묶고, 엔티티에서 해당 객체를 참조할 수 있도록 만들 수 있습니다. 아래 예제에서는 Name 클래스는 테이블의 first, middle, last 컬럼을 묶어서 표현하였고, Contact 클래스에서는 해당 객체를 멤버 변수로 선언했습니다.
entity manager
Entity Manager 는 entity instance를 생성/삭제하거나 탐색하기 위한 매니저 객체입니다. 이렇게 객체의 persistency를 관리하는 객체를 Persistence context라고 부릅니다. entity manager 의 사용법은 아래 CRUD 항목에서 자세히 설명하겠습니다.
@PersistenceContext
private EntityManager em;
entity transaction
JPA에서 트랜잭션을 관리하기 위해서 사용하는 객체입니다. em.getTransaction()
으로 트랜잭션을 가져올 수 있습니다.
기본적인 CRUD
다음으로 JPA를 활용한 기본적인 CRUD 에 대해 설명드리겠습니다.
Create
Part part = new Part(partNumber,
revision,
description,
revisionDate,
specification,
drawing);
em.persist(part); // JPA 의 persistence context에 현재 데이터를 저장
em.getTransaction().commit(); // 주의할 점. 실제 DB에 반영되는 시점은 커밋 시점!
// em.flush(); - flush 라는 함수를 사용할 수도 있음
create 는 위와 같이, entity 인스턴스를 생성한 다음 persist 함수를 호출하는 것으로 수행됩니다. 한편 주의할 점은 persist 함수는 단순히 persistence context, 즉 entity manager 에 생성한 엔티티 인스턴스를 저장하는 과정일 뿐, 실제 DB에 데이터를 저장하는 것은 아니라는 점입니다. 실제로 DB에 반영되는 시점은 commit 을 호출한 시점입니다.
read
CustomerOrder order = em.find(CustomerOrder.class, orderId);
List<Customer> customers = em.createQuery("select customer from Customer customer")
.getResultList(); //JPQL
다음은 read 입니다. 하나의 데이터를 찾을 경우 find 함수를 호출할 수 있지만, 전체 데이터를 조회할 경우 아래 코드와 같이 createQuery 함수로 쿼리문을 직접 입력해야 합니다(후술하겠지만, 이러한 점이 Spring Data JPA의 탄생 배경이 되었습니다). 여기에서 사용되는 쿼리문은 SQL문과 비슷하지만 약간 다른, JPQL 이라는 문법입니다.
위의 JDBC 예제와 비교해보면 훨씬 과정이 단순해진 것을 확인할 수 있습니다. Statement를 생성하고, try문을 씌워서 쿼리를 실행하고, 각각의 컬럼을 ResultSet이라는 것을 이용해 조회를 해야 했던 JDBC 코드와 달리 한 줄의 코드로 read 작업이 수행되고 있습니다.
update
CustomerOrder order = em.find(CustomerOrder.class, orderId);
order.setMenu("chicken"); // setter로 데이터 입력 후 commit 하면 자동으로 업데이트됨
update는 update를 할 데이터를 find로 찾은 다음, setter 메서드를 호출하면 자동으로 이루어집니다. 이는 entity manager가 해당 데이터를 관리하고 있기 때문입니다.
delete
CustomerOrder order = em.find(CustomerOrder.class, orderId);
em.remove(order);
삭제는 remove 함수를 호출합니다.
더 자세한 예제는 다음 사이트를 참고해 주세요.
https://www.javaguides.net/2018/12/jpa-crud-example.html
참고 : JPQL
- entity의 persistent state를 관리하기 위한 쿼리
- SQL이랑 유사한 문법을 사용함
- JPA 에서 복잡한 쿼리를 사용하려면 JPQL을 사용해야 함 → Spring Data JPA를 사용하는 이유 중 하나
creating query
- dynamic query : 쿼리가 비즈니스 로직에 따라 동적으로 결정되는 것
public List findWithName(String name) {
return em.createQuery(
"SELECT c FROM Customer c WHERE c.name LIKE :custName")
.setParameter("custName", name)
.setMaxResults(10)
.getResultList();
}
- static query : 쿼리를 사전에 미리 정의해놓는 것
// @NamedQuery 어노테이션을 사용
@NamedQuery(
name="findAllCustomersWithName",
query="SELECT c FROM Customer c WHERE c.name LIKE :custName"
)
/*...*/
@PersistenceContext
public EntityManager em;
...
// createNamedQuery 함수 사용
customers = em.createNamedQuery("findAllCustomersWithName")
.setParameter("custName", "Smith")
.getResultList();
spring data JPA
Spring data JPA는 위에서 설명한 JPA를 조금 더 사용하기 쉽게 만든 API 입니다. Spring data JPA 사이트에서는 등장 배경을 다음과 같이 설명하고 있는데요,
Implementing a data access layer of an application has been cumbersome for quite a while. Too much boilerplate code has to be written to execute simple queries as well as perform pagination, and auditing. Spring Data JPA aims to significantly improve the implementation of data access layers by reducing the effort to the amount that’s actually needed. As a developer you write your repository interfaces, including custom finder methods, and Spring will provide the implementation automatically.
즉 Spring Data JPA는 JPA를 한 단계 더 추상화시킨 버전이라고 할 수 있습니다. Hibernate 아키텍처와 비교해서, 아래의 Java Persistence API를 한 번 더 랩핑한 것이 Spring Data API 라고 이해하시면 될 것 같습니다.
JPA를 설명할 때 전체 데이터 조회 시 JPQL을 사용해야 한다고 말씀드렸습니다. 이처럼 JPA는 조금만 쿼리가 복잡해져도 결국 JPQL 이란 것을 사용해야 된다는 불편함이 존재합니다. Spring Data JPA는 JPQL 쿼리를 자동으로 생성해서 이러한 불편함을 해소하였습니다.
repository
Spring Data JPA가 JPA를 추상화하기 위해 사용하는 핵심 개념은 Repository
입니다. Repository
는 marker interface로서 내부에 아무것도 구현되어 있지 않고, Repositorty
를 상속한 CrudRepository
에 핵심 기능이 정의되어 있습니다.
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
Optional<T> findById(ID primaryKey);
Iterable<T> findAll(); // 전체 데이터 가져오기
long count();
void delete(T entity);
boolean existsById(ID primaryKey);
// … more functionality omitted.
}
위의 CrudRepository 는 이름과 함수명에서 알 수 있듯, CRUD 기능을 제공하는 인터페이스입니다. 주의할 점은 클래스가 아니라 인터페이스라는 점인데요, 실제 스프링 프로그램이 동작하면서 Repository로 등록한 인터페이스를 읽어서 자동으로 repository 객체를 생성해 줍니다.
프로그래머는 Repository
인터페이스를 상속해서 자신만의 Repository
인터페이스를 만들 수 있습니다. Repository
인터페이스를 만든 다음에는 JavaConfig나 xml 설정 파일에 만든 Repository
를 등록하면 됩니다.
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
// 1. JavaConfig 사용하는 방법 - @EnableJpaRepositories 어노테이션 이용
@EnableJpaRepositories
public class Config {}
// 2. xml 설정 파일에 추가하는 방법
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/jpa
http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
// base-package로 설정된 위치를 뒤져서 repository를 등록한다.
<jpa:repositories base-package="com.acme.repositories"/>
</beans>
스프링이 구동될 때, 자동으로 적절한 FactoryBean 을 찾아서 컨테이너에 등록합니다. 그 후 각각의 빈이 컨테이너에 등록되는데, 이 때 빈 이름의 생성은 인터페이스 이름에서 유도됩니다. 예를 들어 UserRepository
라는 인터페이스가 있었다면, userRepository
라는 이름의 빈이 생성됩니다. 이렇게 인터페이스로부터 스프링 빈이 생성되고, 프로그램 내에서 이 객체를 사용할 수 있습니다.
저는 이 부분을 공부하면서 "어떻게 인터페이스에서 객체가 곧바로 생성될 수 있지?" 라는 질문이 머릿속을 떠나지 않았는데요, 알고 보니 이는 JDK의 dynamic proxy 와 연관되어 있었습니다. 원리가 궁금하신 분들은 구글에 검색해 보시면 자세한 내용을 확인하실 수 있습니다. 저는 아직 잘 이해가 안되더라구요 @.@
query method
Spring Data JPA의 Repository
에는 자동 쿼리 생성 기능이 있습니다. 공식 문서를 보면 여러 가지 쿼리 생성 방식이 존재하지만, 메서드 이름을 이용해서 쿼리를 생성하는 방법이 가장 특징적입니다. 다음의 예를 보면 메서드 이름이 findBy
, find ... by
등으로 정형화되어 있음을 확인할 수 있는데요, 언뜻 보면 코딩 컨벤션이라고 생각하기 쉽지만, 이는 Spring Data JPA에서 쿼리를 생성하기 위한 정해진 규약입니다. 예를 들면 findByEmailAddressAndLastname
은 규칙에 따라서 select u from User u where u.emailAddress = ?1 and u.lastname = ?2
로 변환됩니다. findBy 는 select 로, EmailAddresss 와 Lastname은 각각 where절의 조건문으로 변환된 것을 확인하실 수 있습니다.
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// Enables the distinct flag for the query
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
// Enabling ignoring case for an individual property
List<Person> findByLastnameIgnoreCase(String lastname);
// Enabling ignoring case for all suitable properties
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// Enabling static ORDER BY for a query
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
이 외에는 JPA의 named query 를 사용하는 방법, @Query 어노테이션을 사용하는 방법이 있습니다. @Query 어노테이션에서 사용하는 문법은 JPQL, SQL 문 둘 중 어느것이라도 상관 없다고 합니다.
public interface UserRepository extends JpaRepository<User, Long> {
// 이런식으로...
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);
}
- 참고 : 좀 더 구체적인 Spring Data JPA의 동작 방식
참고
https://www.oracle.com/java/technologies/javase/javase-tech-database.html
https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/
http://hibernate.org/orm/what-is-an-orm/
https://www.slideshare.net/brmeyer/orm-jpa-hibernate-overvie
- JPA tutorial : https://www.javaguides.net/p/jpa-tutorial-java-persistence-api.html
- JPA interview questions : https://www.javaguides.net/2018/12/jpa-interview-questions-and-answers-for-beginners-and-experienced.html
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#preface
댓글