JPA

sangeun·2020년 5월 17일
0

스프링 공부

목록 보기
5/5

면접직전이라 JAVA ORM표준 JPA프로그래밍을 이틀만에 급 읽어서 정리; 집중안돼서 너무 힘들었지만 평소에 알고싶었던 개념들이라 지금이라도 정리해서 다행이다..

기본 내부구조

ORM이란?

  • 데이터베이스와 자바 간 패러다임의 불일치를 해결하기 위한 도구. 개발자들이 비즈니스 로직에 집중할 수 있게 도와준다.
  • 패러다임 불일치 해결
    - 반복되는 SQL 자동 생성
    • 자바에서의 상속을 데이터베이스와 매핑해서 객체지향적인 구조를 유지할 수 있게 도와줌

JPA의 장점?

  • 패러다임의 불일치 해결
  • 연관된 객체 탐색을 보다 간편하게 해줌
  • 동등성 관리에 도움(근데 캐시와 프록시가 끼어드니 더 복잡하게 만드는 것 같아서 참조보다는 값을 통한 비교가 나을 수도 있음)
  • 1차 캐시(영속성 컨텍스트)를 통한 도움
    - 내부적인 트랜잭션 관리 가능
    • 데이터베이스에 다녀오지 않아도 이미 영속성 컨텍스트에서 가지고 있는 데이터는 곧바로 가져올 수 있음

그래도 Mybatis를 사용하는 이유?

  • 지연 로딩같은게 JPA의 장점일 수 있는데, 사실 사용할 객체라면 한번에 join으로 로딩해오는 것이 더 빠를 수 있음
  • 어쩌피 현업에서 매우 단순한 쿼리는 잘 사용하지 않아서 성능도 고려하면 대부분의 쿼리는 개발자가 직접 생성해야 함
  • 제대로 이해하지 못하고 사용하면 추상화되어있기 때문에 문제를 파악하기 어려움 ex)N+1문제

그래도 JPA가...

  • mybatis에서 리턴받은 데이터베이스 오브젝트를 일일히 xml을 통해 자바의 객체와 매핑하는 것이 너무 불편; 패러다임의 불일치 극복 비용이 너무 많이 든다..

엔티티 매니저

  • 엔티티와 관련된 모든 일을 처리(CRUD).
  • 서버 요청당 하나일 수도(OSIV), 트랜잭션당 하나일 수도 있고 다양함.
  • 가장 근본적인 원칙은 앤티티 매니저 팩토리는 스레드 세이프하지만 엔티티 메니저는 스레드 간에 공유가 안됨

영속 컨텍스트

  • 1차 캐시 관리
  • 내부에 있는 엔티티들은 모두 식별자를 가지고 있음 -> 따라서 Identity전략(mysql에서 사용)트랜잭션에서 write을 할 경우, 식별자를 알아야지 영속 컨텍스트에 넣을 수 있으므로 쓰기 지연이 발생하지 않음

엔티티의 생명주기

  • 비영속: 순수한 자바 객체 상태
  • 영속: 영속성 컨텍스트에 들어간 자바 객체.
    - 객체의 변경이 추적되어 자동적으로 데이터베이스에 반영된다.
    • 1차 캐시
    • 동일성 보장
    • 트랜잭션 쓰기 지연: flush전에는 데이터베이스에 직접 락을 걸지 않으므로 장점
    • 지연 로딩
  • 준영속: 트랜잭션의 종료 또는 명시적인 영속성 컨텍스트 종료 등으로 영속성이 끊어진 상태. 거의 비영속 상태인데 차이는 식별자 값을 가진 것
  • 삭제

관련 어노테이션

@PersistenceContext

  • 순수 자바 어플리케이션과는 달리, 스프링에서는 EntityManager를 알아서 관리해주기에, 직접 EntityManagerFactory에서 주입받지 않더라도 주입받을 수 있음

@Repository

  • 해당 클래스가 빈으로 등록

  • JPA전용 에러가 발생하였을 때 추상화된 스프링의 DB에러로 한번 더 감싸서 나옴

    객체의 연관관계

다대일 양방향

  • member * - 1 team일 경우
  • member가 foreign key를 관리할 수 있도록 설정하는 것이 편리. -> @ManyToOne
  • team는 member의 외래키를 이용해서 List< member >를 생성하는 것이므로, 연관관계의 실제 주인인 member의 team속성을 넣어줌-> @ManyToOne(mappedBy="team")
    - 왜넣어주지? 싶었는데 List< member >이 많을 경우 ex) 학교 팀 멤버들, 동네 팀 멤버들... 이렇게 있을 때 구분해주기 위해서인듯
  • 주의사항
    - 한쪽에만 데이터를 넣거나 삭제하면 다른 쪽에는 반영되지 않기 때문에, 양쪽에 넣어주는 함수 만들어 사용하면 편함(단, 영속성 전이를 사용하면 양쪽에 반영할 수 있음)
    • 순환참조

상속관계

  • 부모, 자식들을 각자 다른 테이블로 만듬
    - 장
    - 테이블이 정규화됨
    - 효율적인 데이터 사용
      • 많은 조인이 들어감
        • insert할때 부모, 자식 두번 들어감
  • 부모, 자식에서 필요한 칼럼들을 모아서 하나의 테이블로 만듬
    - 장
    - 성능을 높일 수 있음
    - 조회 쿼리 단순
      • 칼럼들이 nullable해짐
        • 저장공간 낭비
  • 부모에서 필요한 칼럼들을 모두 자식 테이블에 넣음
    - 장
    - 메모리 공간 절약 + 부모 테이블에 조인할 필요성 없음
      • 자식들을 한꺼번에 모아서 처리하기 불편함

식별관계 vs 비식별관계

  • 식별관계: 부모 테이블의 기본 키를 받아서 자식 테이블의 기본키 + 외래키로 사용하는 방식
  • 비식별관계: 부모 테이블의 기본 키를 자식 키에서 외래키로 사용하는 것

로딩

지연 로딩

  • 한번에 모든 데이터들을 가져오는 것은 매우 비효율적이어서 정말 사용할 때 가져오는 방식
  • JPA에서는 바이트 코드를 로딩할때 조작하는 로드타임 위빙 방식과 프록시 방식을 사용

프록시 방식

  • 객체를 getReference로 조회하면 엔티티 그대로의 객체가 오는 것이 아니라, 프록시로 한번 더 감싸진 객체가 온다. (영속성 컨텍스트에 존재하면 이미 로딩되었다는 것이므로 불필요하게 프록시로 감싸져있는게 아닌 실제 엔티티를 가져옴)
  • getReference로 얻은 프록시 객체는 식별자를 가지고 있기에 AcessType이 프로퍼티인 경우에는 getId를 해도 엔티티가 초기화되지 않음(?어떻게.....? Mysql은 디비에 넣어야지 식별자를 알 수 있을텐데... 임시식별자인가?)
  • 실제 데이터를 얻기 위해 get메서드를 사용할 때 초기화를 진행한다.
  • 영속성 컨텍스트의 범위에서 벗어나 있는 경우(ex 컨트롤러단, 영속성 컨텍스트 종료)에 해당 속성에 접근하려고 하면 LazyInitializationException이 뜬다.

Spring Data JPA

사용 이유

  • 별다른 구현 클래스 없이 인터페이스만 만들어놓으면 네이밍 규칙만 맞추면 자동으로 쿼리를 생성해준다 (JpaRepository를 상속받아야 함)
  • 이미 몇가지 findAll같은 인터페이스는 이미 JpaRepository에 들어있음

트랜잭션

영속성 컨텍스트

  • 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용

준영속 상태의 지연 로딩 문제

컨트롤러에서 추가적으로 엔티티를 초기화할 필요가 있을수도 있음. 하지만 컨트롤러는 트랜잭션의 범위를 이미 넘어가서 엔티티가 비영속 상태이고, 더이상 초기화가 불가능해짐.

  • 해결방법1: 페치 전략을 즉시로딩으로 수정
    - 단: N+1문제가 발생, 불필요한 엔티티를 로딩해둠
  • 해결방법2: OSIV를 사용
    - 이전 OSIV: 필터 또는 인터셉터에서 트랜잭션을 시작해 요청당 하나의 영속성 컨텍스트를 사용 -> 컨트롤러에서 뷰에 보여지기 위해 임시로 엔티티의 데이터를 수정하면, 영속성 컨텍스트 내부에 있으므로 결과적으로 요청을 반환할 때 DB에 반영되는 문제
    - 해결책: 엔티티를 읽기 전용으로 전달(getter만 있는 인터페이스 타입으로 전달. 매번 인터페이스를 작성해야 한다는 불편함), DTO로 전달, wrapper로 전달 다 비슷한 단점
    • 현재 OSIV: 필터 또는 인터셉터에서 영속성 컨텍스트를 만들어 영속성 컨텍스트는 요청 전체로 유지를 하되, 트랜잭션은 서비스 계층만으로 국한시킴. -> 서비스 호출이 끝나고 컨트롤러에서 데이터를 수정하여도, flush는 이미 하였으니 디비에는 반영되지 않을 것. flush를 명시적으로 호출하여도 트랜잭션의 범위 밖이라서 데이터 변경 불가 에러 발생(그런가보다..)
      하지만 service -> 컨트롤러에서 수정 -> service하면 결국 flush하니 디비에 반영되는디...? -> 그러지 마라....

롤백 문제

트랜잭션이 롤백되어도, 영속성 상태의 객체는 이미 수정되어서 남아있음. 따라서 명시적으로 em.clear같이 초기화를 해줘야 함
-> 그런데 스프링에서는 내부적으로 롤백발생하면 알아서 초기화해줌

추가적인 문제들

엔티티 동등성

  • 영속성 컨텍스트가 같으면 같은 엔티티는 같은 참조를 가짐
  • 반면 영속성 컨텍스트가 다를 때는 엔티티의 동일성 비교(==)는 실패하므로, 되도록이면 미스하지 않도록 동등성 비교(equals)쓰는게 좋음

프록시 객체 동등성

  • getReference하고 find하면? -> 영속성 컨텍스트에서는 find에다가 이미 찾아놓은 프록시 객체 돌려줌
  • find -> getReferenct하면? -> 영속성 컨텍스트에서는 getReference에다가 이미 로딩해놓은 엔티티 객체 돌려줌
  • 동등성 지켜짐
  • 심화문제있는데 나중에 657 이해해보자

성능 최적화

N+1문제

지연 로딩으로 인해 객체들을 찾아오고, 가지고있는 외부참조 속성들을 하나씩 초기화하게 되면 요청이 매 객체마다 날라가는 문제 발생

낙관적 락

  • 트랜잭션 대부분이 충돌이 발생하지 않는다고 가정
  • JPA가 제공하는 버전 관리 기능 이용
  • 동시에 두 데이터를 트랜잭션에서 변경할 때, 최초 커밋만 인정하도록 구현
  • Version사용: 트랜잭션이 데이터를 변경하거나, 심지어는 조회만 했을 때(경우에 따라 다르게 설정가능) 버전을 하나씩 높임. 칼럼으로 하나 둬서 JPA가 관리. 만약 커밋을 할 때 내가 가지고 있는 버전과 다르면 다른 트랜잭션에서 이미 접근했다는 의미이므로 에러를 발생시키고 반영하지 않음.
    - NONE: 데이터 변경시 버전 높여서 두번의 갱신으로 인해 데이터가 유실되는 것을 방지
    • OPTIMISTIC: 조회시 버전을 높여서 다른 트랜잭션들의 non repeatable read를 방지

비관적 락

  • 트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 검
  • 데이터베이스가 제공하는 락 기능 사용(ex. for update)

2차 캐시

  • 2차 캐시를 추가로 둘 수 있음.
  • 1차 캐시에서 없으면 2차 캐시를 한번 더 찾아서 네트워크 비용을 줄일 수 있도록 함
  • 동시성을 극대화하기 위해 객체를 그대로 반환하지 않고 복사본을 반환
profile
꾸준히

0개의 댓글