JPA(Java Persistence API)
- JPA란?
- Java 진영의 ORM 기술 표준을 말한다.
- ORM: 직접 SQL구문을 사용하지 않고 객체와 데이터베이스의 스키마를 매핑시킨다.
- 객체는 객체대로 설계하고 RDBMS는 RDBMS대로 설계한다.
- ex) Hibernate(JPA를 구현한 오픈소스)
- https://oranthy.tistory.com/321
- Java 진영의 ORM 기술 표준을 말한다.
- JPA 등장 배경
- 패러다임의 불일치: Java(속성과 기능을 캡슐화해서 사용하는 게 목표) vs RDBMS (데이터 정교화 및 보관)
- JPA 장단점
- 장점
- 쿼리를 작성할 필요 없어서 코드가 줄어든다.
- 가독성 좋다.
- 간편한 수정 가능
- 동일한 쿼리에 대한 캐시 기능을 사용해서 성능이 좋다.
- 단점
- 매핑 설계를 잘못하면 성능 저하
- JPA를 제대로 사용하려면 알아야할 것이 많아서 학습하는데 시간이 오래 걸린다.
- 다수의 테이블 조인 시 신경써야 할 게 많다. ⇒ 아주 큰 단점
- 장점
- JPA 영속성 컨텍스트의 장점
- Def) 영속성 컨텍스트: 엔티티를 영구 저장하는 환경을 의미한다. 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다.
- 쓰는 이유는 1차 캐시, 동일성 보장, 쓰기 지연, 변경 감지, 지연 로딩이 있다.
- 1차 캐시: 조회가 가능하며 1차 캐시에 없으면 데이터베이스에서 조회해서 1차 캐시에 올린다.
- 동일성(identity) 보장: 동일성 비교 가능하다
- 쓰기 지연: 트랜잭션을 지원하는 쓰기 지연이 가능하며 트랜잭션 커밋하기 전까지 SQL을 바로 보내지 않고 모아서 한 번에 보낸다.
- 변경 감지(Dirty checking)
- 지연 로딩
- Propagation 전파 단계
- Def) Propagation은 트랜잭션이 동작하고 있을 때, 다른 트랜잭션을 호출(실행)하는 상황에서 선택 가능한 옵션을 말한다.
- @Transactional 의 propagation 속성을 통해 피호출 트랜잭션의 입장에서는 호출한 쪽의 트랜잭션을 그대로 사용할 수도 있고 새롭게 트랜잭션을 생성할 수도 있다.
- required(디폴트): 부모 트랜잭션 내에서 실행하며 부모 트랜잭션이 없으면 새로운 트랜잭션을 생성한다.
- requires_new:
부모 트랜잭션을 무시하고무조건 새로운 트랜잭션을 생성한다. - support: 부모 트랜잭션 내에서 실행하며 부모 트랜잭션이 없을 경우, nontransactionally로 실행된다.
- mandatory: 부모 트랜잭션 내에서 실행되며 부모 트랜잰션이 없을 경우 예외 발생
- not_support: nontransactionally로 실행되며 부모 트랜잭션이 내에서 실행될 경우 일시 정지
- never: nontransactionally로 실행되며 부모 트랜잭션이 존재하면 예외 발생
- nested: 해당 메서드가 부모 트랜잭션 내에서 진행될 경우, 별개로 커밋되거나 롤백될 수 있다. 둘러싼 트랜잭션이 없을 경우 required와 동일하게 작동한다.
- https://taetaetae.github.io/2016/10/08/20161008/
- JPA 쓰는 이유
- 객체 지향 프레임워크이기 때문이다.
- JPA를 사용하면 비즈니스 로직이 RDBMS에 의존하는 게 아니라 자바 코드로 표현 가능하기 때문이다. ⇒ 생산성이 높아진다.
- JPA은 JPQL로 SQL을 추상화하기 때문에 동일한 쿼리를 작성하더라도 RDBMS 벤더에 관계없이 같은 동작을 기대할 수 있다. database dialect를 지원하기 때문이다.
- JPQL(Java Persistence Query Language)
- JPA Criteria(객체 지향 쿼리 빌더): Java 코드를 짜서 JPQL을 빌드해주는 모음
- (JPQL의 작성을 도와주는 빌더 클래스이다.)
- 기본으로 JPQL을 쓰고 안 되는 경우에 네이티브 SQL 사용
- https://oranthy.tistory.com/332?category=964280
- 객체 지향 쿼리
- SQL을 추상화한 객체 지향 쿼리 언어를 의미한다.
- SQL는 테이블 대상으로 쿼리, JPQL은 엔티티 객체를 대상으로 쿼리를 실행한다.
- JPA Criteria(객체 지향 쿼리 빌더): Java 코드를 짜서 JPQL을 빌드해주는 모음
- QueryDSL
- 네이티브 SQL
- 쿼리를 Java 코드로 작성할 수 있도록 도와주는 기술이다.
- JDBC API 직접 사용, MyBatis, SpringJdbcTemplate을 함께 쓴다
- JPQL(Java Persistence Query Language)
- N + 1문제와 해결 방법
- 최초 쿼리가 하나 실행되고 그에 따라서 N개의 쿼리가 추가적으로 나가는 것을 말한다.
- 연관 관계에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회활 경우에 조회된 데이터 개수만큼 연관 관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오게 된다.
- n + 1 문제는 즉시 로딩과 지연 로딩 전략 각각의 상황에서 발생 가능하다.
- 하위 엔티티가
존재하는 경우, 하나의 쿼리에서 모두 가져오는 게 아니라 필요한 곳에서 각각 쿼리가 발생하는 것을 말한다. - 즉시 로딩에서 발생하는 이유는 JPQL을 사용하는 경우 전체 조회했을 때, 영속성 컨텍스트가 아닌 데이터베이스에서 직접 데이터를 조회한 다음 즉시로딩 전략이 동작하기 때문이다
- 지연 로딩에서 발생하는 이유는 지연로딩 전략을 사용한 하위 엔티티를 로드할 때 JPA에서 프록시 엔티티를 언프록시할 때 해당 엔티티를 조회하기 위한 추가적인 쿼리가 실행되어 발생한다.
- 해결 방법
- Fetch join이라고 불리는 JPQL의 fetch join을 사용한다. 조인문을 이용해서 하나의 쿼리로 이용한다. (로딩 전략을 LAZY로 설정해도 즉시 로딩으로 적용되는 기능? )
- 모든 연관 관계를 지연 로딩으로 세팅한다.
- @EntityGraph
- @BatchSize
- 전역적인 batch 사이즈를 설정한다.
- https://oranthy.tistory.com/329
- @Transactional, 트랜잭션 격리 수준
- 참고)
- isolation (트랜잭션 격리 수준)
- https://oranthy.tistory.com/434
- read uncommitted: 어떤 트랜잭션의 변경 내용이 커밋, 롤백되지 않아도 다른 트랜잭션에서 보인다. (더티리드 발생)
- read committed: 어떤 트랜잭션의 내용이 변경되서 커밋이 완료된 데이터만 다른 트랜잭션에서 조회가능하다. (더티리드 없으나 부정합의 문제 ‘NON-REPEATABLE READ’)
- 하나의 트랜잭션에서 똑같은 select 쿼리를 실행했을 때 똑같은 결과를 가져와야한다는 “repeatable read”라는 정합성에 어긋난다.
- repeatable read: 언두 영역에 백업된 이전 데이터를 이용해서 동일 트랜잭션 내에서 같은 결과를 보여주도록 보장한다.
- MVCC(Multi Version Concurrency Control)를 위해 언두 영역에 백업된 이전 데이터를 이용해서 동일 트랜잭션, 동일 결과를 보여주도록 보장한다.
- PHANTOM READ 라는 부정합 문제가 발생 가능 (다른 트랜잭션의 변경 작업에 의해 레코드가 보였다 안 보였다 하는 현상)
- 언두 영역: UPDATE, DELETE를 실행하기 전의 데이터를 보관하는 곳
- serializable: 어떤 트랜잭션이 읽고 있는 레코드에 대해 다른 트랜잭션은 접근 불가능
- 공유 잠금(읽기 잠금)을 획득해야 읽기 작업이 가능하다.
uncommitted
- (아래로 갈수록 격리 수준이 높고 동시 처리 성능도 낮아진다.)
- 예를 들어, 결제 서비스에서는 입금, 출금할 때는 높은 격리 수준을 쓰고 , 전파 레벨도 새로운 전파를 타도록한다.
- https://oranthy.tistory.com/434
- 지연로딩(lazy loading)과 즉시로딩(eager loading)이란
- 지연 로딩(lazy loading): 객체가 실제 사용될 때, 로딩한다.
- 문제점: 쿼리가 두 번 실행된다.
- 즉시 로딩(eager loading): JOIN을 사용해서 연관된 객체를 미리 조회한다.
- 지연 로딩(lazy loading): 객체가 실제 사용될 때, 로딩한다.
- Repository의 메서드 save() 는 어떻게 실행되는가?
- Spring Data JPA 가 제공한다.
- repository.save()
- repository 인터페이스에 정의된 메서드
- save()의 선언부가 정의되어 있다.
<hide/>
package org.springframework.data.repository;
import java.util.Optional;
@NoRepositoryBean
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();
}
- SimpleJpaRepository 은 JpaRepositoryImplementation를 implements 한다.
- save()가 정의되어 있다.
- findById(ID id): id가 존재하지 않으면 예외가 터진다.
- save(): entity manage에 merge() 하거나 persist()
- persist(): 새로운 엔티티를 영속성 컨텍스트에서 관리하기 위해 사용한다.
- merge(): 분리된 (detached) 엔티티에 대해서 상태를 변경해서 database에 반영하고 싶은 경우 사용한다.
<hide/>
@Repository
@Transactional(
readOnly = true
)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
...
@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (this.entityInformation.isNew(entity)) {
this.em.persist(entity);
return entity;
} else {
return this.em.merge(entity);
}
}
...
@Transactional
public void delete(T entity) {
Assert.notNull(entity, "Entity must not be null!");
if (!this.entityInformation.isNew(entity)) {
Class<?> type = ProxyUtils.getUserClass(entity);
T existing = this.em.find(type, this.entityInformation.getId(entity));
if (existing != null) {
this.em.remove(this.em.contains(entity) ? entity : this.em.merge(entity));
}
}
}
@Transactional
public void deleteAllById(Iterable<? extends ID> ids) {
Assert.notNull(ids, "Ids must not be null!");
Iterator var2 = ids.iterator();
while(var2.hasNext()) {
ID id = var2.next();
this.deleteById(id);
}
}
public Optional<T> findById(ID id) {
Assert.notNull(id, "The given id must not be null!");
Class<T> domainType = this.getDomainClass();
if (this.metadata == null) {
return Optional.ofNullable(this.em.find(domainType, id));
} else {
LockModeType type = this.metadata.getLockModeType();
Map<String, Object> hints = new HashMap();
this.getQueryHints().withFetchGraphs(this.em).forEach(hints::put);
return Optional.ofNullable(type == null ? this.em.find(domainType, id, hints) : this.em.find(domainType, id, type, hints));
}
}
}
회고록
- JPA를 쓰는 장점으로 쿼리를 작성하지 않아도 된다고 답했는데 쿼리 작성하지 않는 게 장점이라고 할 수 있냐는 질문을 들었다. 코드량이 줄어드는 건 맞는데 다른 면도 생각해보게 되는 질문이었다.
- 빈출 질문: JPA란? JPA의 장단점, JPA를 쓰면서 발생한 문제점과 해결 방법, save()의 실행 원리
참고) 아래 깃허브의 질문을 참고하여 내용을 보충해서 작성했습니다. 틀린 부분있다면 댓글 부탁드립니다 💖💛💚
'백엔드 개발직 면접 예상 질문' 카테고리의 다른 글
7. 팀 프로젝트 관련 및 인성 질문 (3) | 2023.01.26 |
---|---|
5. Spring (2) | 2023.01.24 |
4. Java (0) | 2023.01.24 |
3. 인프라 및 클라우드 (0) | 2023.01.23 |
2. 운영체제, 네트워크, 보안과 암호학 (2) | 2023.01.22 |