백엔드 개발직 면접 예상 질문

6. JPA(Java Persistence API)

계란💕 2023. 1. 25. 16:45

JPA(Java Persistence API)

 

  • JPA란?
    • Java 진영의 ORM 기술 표준을 말한다.
      • ORM: 직접 SQL구문을 사용하지 않고 객체데이터베이스의 스키마를 매핑시킨다.
    • 객체는 객체대로 설계하고 RDBMS는 RDBMS대로 설계한다.
    • ex) Hibernate(JPA를 구현한 오픈소스)
    • https://oranthy.tistory.com/321

 

 

  • JPA 등장 배경
    • 패러다임의 불일치: Java(속성기능캡슐화해서 사용하는 게 목표) vs RDBMS (데이터 정교화 및 보관)

 

 

  • JPA 장단점
    • 장점
      • 쿼리를 작성할 필요 없어서 코드가 줄어든다.
      • 가독성 좋다.
      • 간편한 수정 가능
      • 동일한 쿼리에 대한 캐시 기능을 사용해서 성능이 좋다.
    • 단점
      • 매핑 설계를 잘못하면 성능 저하
      • JPA를 제대로 사용하려면 알아야할 것이 많아서 학습하는데 시간이 오래 걸린다.
      • 다수의 테이블 조인 시 신경써야 할 게 많다. ⇒ 아주 큰 단점

 

 

  • JPA 영속성 컨텍스트의 장점
    • Def) 영속성 컨텍스트: 엔티티영구 저장하는 환경을 의미한다. 애플리케이션 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다. 
    • 쓰는 이유는 1차 캐시, 동일성 보장, 쓰기 지연, 변경 감지, 지연 로딩이 있다.
      • 1차 캐시: 조회가 가능하며 1차 캐시에 없으면 데이터베이스에서 조회해서 1차 캐시에 올린다.
      • 동일성(identity) 보장: 동일성 비교 가능하다
      • 쓰기 지연: 트랜잭션을 지원하는 쓰기 지연이 가능하며 트랜잭션 커밋하기 전까지 SQL을 바로 보내지 않고 모아서 한 번에 보낸다.
      • 변경 감지(Dirty checking)
      • 지연 로딩

 

 

  • Propagation 전파 단계
    • Def) Propagation은 트랜잭션이 동작하고 있을 때, 다른 트랜잭션을 호출(실행)하는 상황에서 선택 가능한 옵션을 말한다.
    • @Transactional 의 propagation 속성을 통해 피호출 트랜잭션의 입장에서는 호출한 쪽의 트랜잭션을 그대로 사용할 수도 있고 새롭게 트랜잭션을 생성할 수도 있다.
    1. required(디폴트): 부모 트랜잭션 내에서 실행하며 부모 트랜잭션이 없으면 새로운 트랜잭션을 생성한다.
    2. requires_new: 부모 트랜잭션을 무시하고 무조건 새로운 트랜잭션을 생성한다.
    3. support: 부모 트랜잭션 내에서 실행하며 부모 트랜잭션이 없을 경우, nontransactionally로 실행된다.
    4. mandatory: 부모 트랜잭션 내에서 실행되며 부모 트랜잰션이 없을 경우 예외 발생
    5. not_support: nontransactionally로 실행되며 부모 트랜잭션이 내에서 실행될 경우 일시 정지
    6. never: nontransactionally로 실행되며 부모 트랜잭션이 존재하면 예외 발생
    7. nested: 해당 메서드가 부모 트랜잭션 내에서 진행될 경우, 별개로 커밋되거나 롤백될 수 있다. 둘러싼 트랜잭션이 없을 경우 required와 동일하게 작동한다.
    8. 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은 엔티티 객체를 대상으로 쿼리를 실행한다.
      • QueryDSL
        • 네이티브 SQL
        • 쿼리를 Java 코드로 작성할 수 있도록 도와주는 기술이다.
        • JDBC API 직접 사용, MyBatis, SpringJdbcTemplate을 함께 쓴다

 

 

  • 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
        1. read uncommitted: 어떤 트랜잭션의 변경 내용이 커밋, 롤백되지 않아도 다른 트랜잭션에서 보인다. (더티리드 발생)
        2. read committed: 어떤 트랜잭션의 내용이 변경되서 커밋이 완료된 데이터만 다른 트랜잭션에서 조회가능하다. (더티리드 없으나 부정합의 문제 ‘NON-REPEATABLE READ’)
          • 하나의 트랜잭션에서 똑같은 select 쿼리를 실행했을 때 똑같은 결과를 가져와야한다는 “repeatable read”라는 정합성에 어긋난다.
        3. repeatable read: 언두 영역에 백업된 이전 데이터를 이용해서 동일 트랜잭션 내에서 같은 결과를 보여주도록 보장한다.
          • MVCC(Multi Version Concurrency Control)를 위해 언두 영역에 백업된 이전 데이터를 이용해서 동일 트랜잭션, 동일 결과를 보여주도록 보장한다.
          • PHANTOM READ 라는 부정합 문제가 발생 가능 (다른 트랜잭션의 변경 작업에 의해 레코드가 보였다 안 보였다 하는 현상)
          • 언두 영역: UPDATE, DELETE를 실행하기 전의 데이터를 보관하는 곳
        4. serializable: 어떤 트랜잭션이 읽고 있는 레코드에 대해 다른 트랜잭션접근 불가능
          • 공유 잠금(읽기 잠금)을 획득해야 읽기 작업이 가능하다.
        5. uncommitted
      • (아래로 갈수록 격리 수준이 높고 동시 처리 성능도 낮아진다.)
      • 예를 들어, 결제 서비스에서는 입금, 출금할 때는 높은 격리 수준을 쓰고 , 전파 레벨도 새로운 전파를 타도록한다. 

 

 

  • 지연로딩(lazy loading)과 즉시로딩(eager loading)이란
    • 지연 로딩(lazy loading): 객체가 실제 사용될 때, 로딩한다.
      • 문제점: 쿼리가 두 번 실행된다.
    • 즉시 로딩(eager loading): JOIN을 사용해서 연관된 객체를 미리 조회한다.

 

 

  • 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()의 실행 원리 

 

 

 

참고) 아래 깃허브의 질문을 참고하여 내용을 보충해서 작성했습니다. 틀린 부분있다면 댓글 부탁드립니다 💖💛💚

백엔드 면접 질문 1

백엔드 면접 질문 2

 

 

'백엔드 개발직 면접 예상 질문' 카테고리의 다른 글

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