JPA란
JPA(Java Persistence API)는 자바 ORM 기술에 대한 API표준 입니다.
ORM이란 Object Relational Mapping의 약자로 객체와 관계형 데이터 베이스를 매핑해 주는 것을 의미합니다.
JPA는 인터페이스이고 이를 구현한 대표적은 구현체로는 Hibernate, EclipseLink, DataNucleus, OpenJpa, TopLink등이 있습니다.
SQL 중심 개발의 문제점은 개발자가 CRUD라고 불리는 INSERT, UPDATE, SELECT, DELETE 문을 작생해서 객체를 관계형 데이터베이스에 넣어주고 가져오는 작업입니다.
자바 객체를 SQL를 통해 데이터베이스에 관리하게 하고 데이터베이스에 저장된 데이터를 자바 애플리케이션에 사용하려면 SQL를 통해 다시 자바 객체로 변환하는 반복적인 작업을 해야 합니다.
즉, 개발자가 SQL을 매핑하는 역할을 반복해야 합니다.
또한 객체와 관계형 데이터베이스의 패러다임의 불일치가 가장 큰 문제입니다.
자바는 객체 지향 패러다임으로 만들어졌고, 관계형 데이터베이스는 데이터를 정규화해서 잘 보관하는 것을 목표로 합니다.
따라서 객체를 데이터베이스에 넣기 위해서는 SQL문을 통해 변환해서 저장해야 하고,
데이터베이스에서 객체를 다시 꺼내오기 위해서는 복잡한 SQL문을 작성해야 합니다.
결국 객체를 단순히 데이터 전달 목적으로 사용할 뿐 객체지향적으로 프로그래밍을 할 수 없습니다.
이는 객체지향과 관계형 데이터베이스 간의 패러다임이 불일치하기 때문입니다.
이를 해결하기 위해 나온 기술이 ORM 입니다.
JPA의 장점
- 특정 데이터베이스에 종속되지 않음
- JPA는 추상화한 데이터 접근 계층을 제공합니다. 따라서 설정 파일에 어떤 데이터 베이스를 사용하는지 알려주면 얼마든지 데이터베이스를 변경할 수 있습니다.
- 객체지향적 프로그래밍
- JPA를 사용하면 데이터베이스 설계 중심의 패러다임에서 객체지향적으로 설계가 가능합니다. 이를 통해서 좀 더 직관적이고 비지니스 로직에 집중할 수 있습니다.
- 생산성 향상
- 데이터베이스 테이블에 새로운 컬럼이 추가되었을 경우 테이블의 컬럼을 사용하는 DTO 클래스 필드를 모두 변경해야 합니다. JPA에서는 테이블과 매핑된 클래스에 필드만 추가한다면 쉽게 관리가 가능합니다. 또한 SQL문을 직접 사용하지 않고 객체를 사용하여 동작하기 때문에 유지보수 측변에서 좋고 재사용성도 증가합니다.
JPA의 단점
- 복잡한 쿼리 처리
- 통계 처리 같은 복잡한 쿼리를 사용할 경우는 SQL문을 처리 하는 것이 좋습니다. JPA에서는 Native SQL을 통해 기존의 SQL문을 사용할 수 있지만 그러면 특정 데이터베이스에 종속된다는 단점이 생깁니다. 이를 보완하기 위해 SQL과 유사한 기술인 JPQL을 지원합니다.
- 성능 저하 위험
- 객체 간의 매핑 설계를 잘못했을 때 성능 저하가 발생할 수 있으며, 자동으로 생성되는 쿼리가 많기 때문에 개발자가 의도하지 않는 쿼리로 인해 성능이 저하되기도 합니다.
- 어려움
- JPA를 제대로 사용하려면 알아야 할 것이 많아 학습하는데 시간이 오래 걸립니다.
JPA동작 방식
엔티티
데이터 베이스의 테이블에 대응하는 클래스 입니다.
@Entity가 붙은 클래스는 JPA에서 관리하며 엔티티라고 합니다.
예를 들어서 item 테이블을 만들고, 이에 대응되는 Item.java 클래스를 만들어서 @Entity 어노테이션을 붙이면 이 클래스가 엔티티가 되는 것입니다.
클래스 자체나 생성한 인스턴스도 엔티티라고 부릅니다.
엔티티 매니저 팩토리(Entity Manager Factory)
엔티티 매니저 팩토리는 인스턴스를 관리하는 주체입니다.
애플리케이션 실행 시 한 개만 만들어지며 사용자로부터 요청이 오면 엔티티 매니저 팩토리로부터 앤티티 매니저를 생성합니다.
엔티티 매니저(Entity Manager)
엔티티 매니저란 영속성 컨텍스트에 접근하여 엔티티에 대한 데이터베이스 작업을 제공합니다.
내부적으로 데이터베이스 커넥션을 사용해서 데이터 베이스에 접근 합니다.
대표적인 메소드
- find( ): 영속성 컨텍스트에서 엔티티를 검색하고 영속성 컨택스트에 없을 경우 데이터베이스에서 데이터를 찾아 영속성 컨텍스트에 저장합니다.
- persist( ): 엔티티를 영속성 컨텍스트에 저장합니다.
- remove( ): 엔티티 클래스를 영속성 컨텍스트에서 삭제합니다.
- flush( ): 영속성 컨텍스트에 저장된 내용을 데이터베이스에 반영합니다.
영속성 컨텍스트
엔티티를 영구적으로 저장하는 환경으로 엔티티 메니저를 통해 영속성 컨텍스트에 접근합니다.
엔티티 생명주기
- 비영속(new): new키워드를 통해 생성된 상태로 영속성 컨텍스트와 관련이 없는 상태
- 영속(managed): 엔티티가 영속성 컨텍스트에 저장된 상태로 영속성 컨텍스트에 의해 관리되는 상태, 영속 상태에서 데이터베이스에 저장되지 않으며, 트랜잭션 커밋 시점에 데이터베이스에 반영
- 준영속 상태(detached): 영속성 컨텍스트에 엔티티가 저장되었다가 분리된 상태
- 삭제 상태(removed): 영속성 컨텍스트와 데이터베이스에서 삭제된 상태
코드 예제
//영속성 컨텍스트에 저장할 상품 엔티티를 하나 생성합니다.
//new 키워드를 통해 생성했으므로 영속성 컨텍스트와 관련이 없는 상태입니다.
Item item=new Item();
item.setItemNm("테스트 상품");
//엔티티 매니저 팩토리로부터 엔티티 매니저를 생성합니다.
EntityManager em=entityManagerFactory.createEntityManager();
//엔티티 매니저는 데이터 변경 시 데이터의 무결성을 위해 반드시 트랜잭션을 시작해야 합니다.
//여기서의 트랜잭션도 데이터베이스의 트랜잭션과 같은 의미로 생각하며 됩니다.
EntityTransaction transaction=em.getTransaction();
transaction.begin();
//생성한 상품 엔티티가 영속성 컨텍스트에 저장된 상태입니다.
//여기까지 데이터베이스에 INSERT SQL을 보내지 않은 단계입니다.
em.persiste(item);
//트랜잭션을 데이터베이스에 반영합니다.
//이 때 영속성 컨텍스트에 저장된 상품 정보가 데이터베이스 INSERT 되면서 반영됩니다.
transaction.commit();
//엔티티 매니저와 엔티티 매니저 팩토리의 close()를 호출해 사용한 자원을 반환합니다.
em.close();
emf.close();
영속성 컨택스트 사용시 이점
애플리케이션과 데이터 베이스 사이에 영속성 컨텍스트리라는 중간 계층을 만들기 때문이다.
이렇게 중간 계층을 만들면 버퍼링, 캐싱 등을 할 수 있다는 장점이 있다.
1차 캐시
영속성 컨텍스트에는 1차 캐시가 존재하며 MAP(KEY, VALUE)로 작성됩니다.
entityManager.find( ) 메소드 호출 시 영속성 컨텍스트의 1차 캐시를 조회합니다.
엔티티가 존재할 경우, 해당 엔티티를 반환하고, 엔티티가 없으면 데이터 베이스에서 조회 후 1차 캐시에 저장 및 반환합니다.
동일성 보장
하나의 트랜잭션에서 같은 키값으로 영속성 컨택스트에 저장된 엔티티 조회 시 같은 엔티티 조회를 보장합니다.
바로 1차 캐시에 저장된 엔티티를 조회하기 때문에 가능합니다.
트랜잭션을 지원하는 쓰기 지연
영속성 컨텍스트에는 쓰기 지연 SQL이 존재합니다.
entityManager.persist( )를 호출하면 1차 캐시에 저장되는 것과 동시에 쓰기 지연 SQL 저장소에 SQL문이 저장됩니다.
이렇게 SQL을 쌓아두고 트랜잭션을 커밋하는 시점에 저장된 SQL문들이 flush되면서 데이터베이스에 반영됩니다.
이렇게 모아서 보내기 때문에 성능에서 이점을 볼 수 있습니다.
변경 감지
JPA는 1차 캐시에 데이터베이스에서 처음 불러온 엔티티의 스냅샷 값을 가지고 있습니다.
1차 캐시에 저장된 엔티티와 스냅샷을 비교 후 변경 내용이 있다면 UPDATE SQL 문을 쓰기 지연 SQL 저장소에 담아둡니다.
데이터베이스 커밋에 변경 내용을 자동으로 반영합니다. 즉 따로 update를 호출할 필요가 없습니다.
Spring Data JPA
@Query어노테이션
Spring Data JPA @Query어노테이션을 사용하면 SQL과 유사한 JPQL(Java Persistence Query Language)이라는 객체지향 쿼리 언어를 통한 복잡한 쿼리도 처리가 가능합니다.
SQL의 경우 데이터베이스의 테이블을 대상으로 쿼리를 수행하고, JPQL은 엔티티 객체를 대상으로 쿼리를 수행합니다.
테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리입니다.
JPQL은 SQL을 추상화해서 사용하기 때문에 특정 데이터베이스 SQL에 의존하지 않습니다.
즉, JPQL로 작성을 했다면 데이터베이스가 변경되어도 애플리케이션이 영향을 받지 않습니다.
단점
@Query 어노테이션 안에 JPQL문법으로 문자열을 입력하기 때문에 잘못 입력하면 컴파일 시점에 에러를 발견할 수 없습니다.
이를 보완하는 것이 Querydsl 입니다.
Spring Data JPA 에서는 Auditing 기능을 제공하여 엔티티가 저장 또는 수정 될 때 자동으로 등록일, 수정일, 등록자를 입력해 줍니다.
Spring DATA JPA Querydsl
Querydsl 은 JPQL을 코드로 작성할 수 있도록 도움을 주는 API입니다.
Querydsl은 소스코드로 SQL문을 문자열이 아닌 코드로 작성하기 때문에 컴파일러의 도움을 받을 수 있습니다.
소스 코드 작성시 오타가 발생하면 개발자에게 오타가 있음을 바로 알려줍니다.
또한 동적으로 쿼리를 생성합니다.
장점
- 고정된 SQL문이 아닌 조건에 맞게 동적으로 쿼리를 생성할 수 있다.
- 비슷한 쿼리를 재사용 할 수 있으며 제약 조건 조립 및 가독성을 향상 시킬 수 있다.
- 문자열이 아닌 자바 소스코드로 작성하기 때문에 컴파일 시점에 오류를 발견할 수 있다.
- IDE의 도움을 받아서 자도오안성 기능을 이용하기 때문에 생산성을 향상시킬 수 있다.
Querydsl을 Spring Data Jpa와 함께 사용하기 위해서는 사용자 정의 리포지토리를 정의해야한다.
- 사용자 정의 인터페이스 작성
- 사용자 정의 인터페이스 구현
- spring data jpa 리포지토리에서 사용자 정의 인터페이스 상속
Querydasl의 조회결과 메소드 종류
- QueryResults<t> fetchResults(): 조회 대상 리스트 및 전체 개수를 포함하는 QueryResults 반환
- List<T> fetch(): 조회 대상 리스트 반환
- T fetchOne(): 조회 대상 1건이면 해당 타입 반환, 1건 이상이면 에러 발생
- T fetchFirst(): 조회 대상이 1건 또는 1건 이상이면 1건만 반환
- long fetchCount(): 해당 데이터 전체 개수 반환, count 쿼리 실행
QueryDslPredicateExcutor 인터페이스 상속
long count(Predicate): 조건에 맞는 데이터의 총 개수 반환
boolean exists(Predicate): 조건에 맞는 데이터 존재 여부 반환
Iterable findAll(Predicate): 조건에 맞는 모든 데이터 반환
Page<A> findAll(Predicate, Pageable): 조건에 맞는 페이지 데이터 반환
Iterable findAll(Predicate, Sort): 조건에 맞는 정렬된 데이터 반환
T findOne(Predicate): 조건에 맞는 데이터 1개 반환