[새배내] level3 프로젝트 하면서 배운 내용

Junseo Kim·2021년 6월 29일
6

[우아한테크코스3기]

목록 보기
24/27
post-thumbnail

학습로그로 작성한 내용

1차 데모데이

2차 데모데이

3차 데모데이

4차 데모데이


JPA

  • SQL 중심에서 벗어나기 위해 만들어 짐.
  • 자바 진영의 ORM 기술 표준이다.
  • 인터페이스의 모음이다.
  • 하이버네이트는 JPA의 구현체이다.
  • collection(ex. list)를 쓰듯이 사용하기 위함이다.(Repository를 하나의 컬렉션으로 생각가능)

영속성 컨텍스트(엔티티 매니저)

  • 하나의 요청 당 하나가 생성
  • 한 번의 요청에 대해 캐싱이 동작.(서로 다른 요청에 대해서는 캐싱된 정보를 사용할 수 없다. 트랜잭션이 다르기 때문에 서로 다른 엔티티 매니저를 사용하기 때문)
  • 트랜잭션 커밋 되는 순간 영속성 컨텍스트(내부의 쓰기 지연 sql 저장소 - 물리적 개념이 아닌 논리적 개념!)에 담겨있는 정보를 DB에 flush 한다.
  • 변경 감지(더티 체크)
    - 1차 캐시에는 스냅샷이라는 게 존재한다. 처음에 영속화 된 상태를 기록해두고, 엔티티와 스냅샷을 비교해서 값이 달라진 경우 update 쿼리를 생성하고, flush & commit한다.(변경되었다가 다시 원래 상태로 돌아가있다면, update 쿼리를 생성하지 않는다. )
  • 기본적으로 서비스 코드에 트랜잭션이 있어야한다.(단 건 쿼리는 DB 내부적으로 트랜잭션을 처리하는 로직이 존재한다(?))
  • Persistence 클래스에서 persistence.xml 설정 정보를 읽어서 EntityManagerFactory 를 만들고, 필요할 때마다 EntityManager를 생성해서 사용.
  • EntityManagerFactory는 어플리케이션 전체에서 하나만 만들어 공유해서 사용한다.(싱글톤)
  • EntityManager는 스레드 간에 공유하면 안되기 때문에, 쓰고 버린다.

엔티티 생명주기

  • 비영속(new/transient) : 방금 생성한 경우. 메모리 상에서는 관리 중이지만, 영속성 컨텍스트나 DB 상에서는 관리를 안 하는 상태. 영속성 컨텍스트와 전혀 상관 없는 상태.
  • 영속(managed) : save, findById, findByName 과 같은 메서드를 호출하여 실제로 영속성 컨텍스트에 의해 관리되고 있는 상태. DB에는 실제로 존재하는지 안 하는지 모름.
  • 준영속(detached) : 영속성 컨텍스트가 관리중이다가, 강제로 관리하지 말라고 분리된 상태. 강제로 분리시키거나, 트랜잭션 범위를 벗어난 경우.
  • 삭제(removed) : 영속성 컨텍스트에서 삭제된 상태.

연관 관계 매핑

  • 연관된 모든 엔티티는 영속 상태여야한다.

MappedBy : 누가 fk를 가지고 있나.(mappedBy 옵션을 주지 않으면 mapper 테이블이 생성되고, mappedBy 옵션을 주면 alter 테이블로 처리된다.)

연관 관계의 주인 == 외래키 관리자 == fk를 가지고 있는 녀석.

(추가하기)

JPA 사용 이유

  • 생산성 : 이미 다 만들어져 있다. 메서드 호출만 하면 된다.
    • 저장 : jpa.persist(member)
    • 조회 : Member member = jpa.find(memberId);
    • 수정 : member.setName(“변경 이름”);
    • 삭제 : jpa.remove(member);
  • 유지 보수
    • 필드가 추가되더라도, 객체에만 추가해주면된다. sql은 jpa가 알아서 해줌
    • h2를 쓰던, Mysql을 쓰던 dialect 설정만 변경해주면, 알아서 처리해준다.
  • 패러다임 불일치 해결
  • 성능 최적화
    • 1차 캐시와 동일성 보장
      • 같은 트랜잭션 안에서는 같은 엔티티 반환
    • 트랜잭션을 지원하는 쓰기 지연
    • 지연 로딩과 즉시 로딩
      • 지연 로딩 : 객체가 실제 사용될 때 로딩
      • 즉시 로딩 : Join sql로 한번에 연관된 객체까지 미리 조회

ORM

  • 객체는 객체대로 설계
  • 관계형 테이블은 관계형 테이블대로 설계
  • 객체와 관계형 테이블의 차이는 ORM이 중간에서 매핑

객체와 관계형 DB의 차이

  1. 상속
  2. 연관 관계
  3. 데이터 타입
  4. 데이터 식별 방법

저장 & 조회 동작

  • [저장]
    • Entity 분석
    • Insert Sql 생성
    • JDBC API 사용 -> DB와 연결
    • 패러다임 불일치 해결
  • [조회]
    • Select sql 생성
    • Jdbc api 사용
    • ResultSet 매핑
    • 패러다임 불일치 해결

JPQL

SQL을 추상화한 객체 지향 쿼리.(특정 DB에 의존적이지 않다) 테이블을 기준으로 쿼리를 날리는 것이 아닌 엔티티 객체를 기준으로 쿼리를 날림

ex. select m from Member as m

동일성 보장

영속성 컨텍스트가 메모리 상에 캐싱하기 때문. 조회 시 영속성 컨텍스트의 1차 캐시를 찾아보고 없으면 DB에 접근해서 찾는다. DB에 접근하게 되면, 1차 캐시에도 저장해준다.

Spring data JPA

findById & findByName

1차 캐시는 키 값을 id로 가진 객체들이 저장된다.

따라서 findById는 1차 캐시에서 찾아볼 수 있다. 하지만 findByName은 name을 키 값으로 가진 객체가 없기 때문에 DB에서 조회한다. findByName은 JPQL을 사용하는 것이다.

JPQL, ID Generator

JPQL, ID Generator가 사용되면 내부적으로 flush가 일어난다.

@Test
void update() {
    // 트랜잭션 락
    Station station1 = stationRepository.save(new Station("잠실역"));
    station1.changeName("몽촌토성역");
    // 트랜잭션 flush(JPQL, ID Generator이 동작할 때 ‘플러시(flush)’ 일어남)
    Station station2 = stationRepository.findByName("몽촌토성역"); // JPQL
    assertThat(station2).isNotNull();
    // 트랜잭션 커밋
    // 트랜잭션 언락(언락 되기 전까지는 DB반영이 아니라, 특정 공간에 로그가 쌓임. 중간에 오류 발생시 로그 다 날려서 롤백)
}

아래 예시에는 findByName이 사용되기 전 flush가 먼저 일어난다.

@NoRepositoryBean

Jpa를 사용할 때, JpaRepository<엔티티, ID타입> 를 상속해서 Repository를 만든다.

JpaRepository 내부에 보면 클래스 레벨에 @NoRepositoryBean가 붙어져 있다. JpaRepository 뿐만 아니라, 그 부모 클래스인 PagingAndSortingRepository, CrudRepository에도 @NoRepositoryBean가 존재한다.

공식 문서에 보면 이렇게 나와있다.

Annotation to exclude repository interfaces from being picked up and thus in consequence getting an instance being created.

This will typically be used when providing an extended base interface for all repositories in combination with a custom repository base class to implement methods declared in that intermediate interface. In this case you typically derive your concrete repository interfaces from the intermediate one but don't want to create a Spring bean for the intermediate interface.

이 어노테이션이 붙어있으면, 실제 프록시 빈으로 등록하지 않게 해주는 어노테이션이라고 한다. 그래서 중간 단계의 Repository 들을 빈으로 등록하지 않으려고 붙여져있다.

JpaRepository는 인터페이스 타입인데 어떻게 동작하는가?

JpaRepository를 상속하면, Spring Data JPA가 인터페이스에 대해서 프록시 구현체를 만든 뒤, 구현체를 주입해준다.(의존성 주입)

JpaRepository 상속시 @Repository를 붙여주지 않아도 되는 이유

spring data JPA는 스프링이 만들어서 제공하는 컴포넌트이므로, @Repository를 생략해도 JPA 관련 예외를 스프링 예외로 변환해서 제공해준다.

또 JpaRepository를 상속하면 @Repository가 없어도 빈으로 등록되는데, 이는 컴포넌트 스캔에 의해 빈으로 동작하는 것이 아니라, spring data에서 해당 인터페이스를 구현한 클래스를 찾아서 사용한다. 실제로는 인터페이스를 구현한 클래스를 바로 사용하는게 아니라, 스프링이 동적으로 임의의 구현 클래스를 생성하고, 내가 구현한 클래스를 연결해준다.

참고
JpaRepository나 CrudRepository 같은 인터페이스에 @Repository 어노테이션이 안붙는 이유에 대해서 궁금합니다.

OSIV

[JPA] OSIV(Open Session In View)

하나의 트랜잭션 내부에서 JPQL, Identity 타입으로 id 생성시

@Transactional이 붙은 하나의 서비스 메서드가 있다고 하자. 이런 경우 트랜잭션이 시작되면 락이 걸리고, 언락이 되기 전에는 db에 반영되는 것이 아니라 특정 공간에 로그를 쌓는다. 트랜잭션 메서드가 성공적으로 끝나면 언락이 되면서 DB에 반영된다.

하지만 메서드 중간에 JPQL(ex. findByName)이나, 저장시 Identity 타입으로 Id를 생성하는 경우는 로그를 쌓는 것만으로는 부족하지 않을까 라는 의문이 들었다. 왜냐하면 이렇게 중간에 flush가 일어나야하는 경우 (Identity 타입으로 ID를 생성하는 경우를 예로 들면) ID를 생성하기 위해서는 DB에 반영이 되어야하기 때문이다.

제이슨에게 물어봤는데 db마다 처리 방식이 다르고, undo 영역을 공부해야한다고 하셨다.

일단 jpa 공부할 때는 그냥 flush 되면 실제 db에 반영된다고 생각하면 된다고한다.(그래서 실제 DB에 반영되기 때문에 auto increment는 롤백 되지 않는 것이다.)

플러시와 롤백

Flush : db에 반영, 롤백 가능
Commit : 롤백 불가능

JPA 양방향 매핑시 롬북 주의점

(추가하기)

JPQL의 변환 위치는?

(추가하기)

양방향 매핑

1:다 && 다:1 양방향 매핑을 해줄때 OneToMany(slave) 쪽에 mappedBy로 주인을 표기해줘야 쓸모없는 테이블이 생기지 않는다. 이때 convenience method 주의 하기. 주인쪽엔 무조건 추가 or 삭제를 해줘야하고(테이블에 반영하기 위해), 객체에서 동기화를 위해 slave 쪽에도 직접 추가, 삭제 해주는 것이 좋음.

도메인 이벤트

(추가하기)

@Embedded & @embbedable

(추가하기)

@where

(추가하기)

casacade

(추가하기)

fetch

(추가하기)

스프링

빈 생명 주기

(추가하기)

기타

세션의 확장성

세션은 확장하기 불편하다. 서버가 늘어나면, 서버마다 세션 정보를 관리하는 저장소를 가지거나, 공통으로 세션 정보를 관리하는 저장소가 필요한데, 서버마다 관리하면 동기화에 있어 비용이 들고, 공통으로 관리해주면 별도의 저장소를 만들어야하기 때문에 비용이 든다. 따라서 세션은 확장성이 좋지 않다.

record 클래스

(추가하기)

Spring.datasource.url vs spring.datasource.hikari.jdbc-url 차이점?

-> https://jojoldu.tistory.com/296
(추가하기)

H2를 mysql처럼 사용하기

h2 url을 적어줄때 MODE=MySQL을 추가해주고, dialect 옵션을 준다.

spring.datasource.url=jdbc:h2:~/test;MODE=MySQL;

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect

but, 이렇게 해주면 h2 1.4.200 버전에서는 테스트 코드에서 에러가 발생한다. 프로덕션 실행은 문제가 없었다.. 스프링부트가 알아서 1.4.200버전을 넣어준 것인데, 왜 이런 문제가 발생할까.. 1.4.199 버전으로 낮추니까 테스트 코드도 문제없이 돌아간다..

제이슨은 이 경우, 다운그레이드할 필요 없이, @AutoConfigureTestDatabase(replace = Replace.NONE) 설정을 해주면 된다고 하셨다.

junit @Nested 롤백?

junit의 @Nested 클래스는 롤백이 보장되지 않는 것 같다.. (?) 아닌 것 같기도 하다.
(추가하기)

Optional의 orElse와 orElseGet의 차이

Oauth 로그인을 구현하면서 findByOauthId를 통해 유저를 조회해오고 없으면 새로 유저를 save하는 기능이 있었다.

그 기능을 아래와 같이 처리했다.

Member member = memberRepository.findByOauthId(userProfile.getOauthId())
                                .orElse(memberRepository.save(userProfile.toMember()));

그러나 DB를 살펴보니, 중복 저장도 되고, 에러도 발생했다...

이유는 orElse에 있었다. orElse는 empty든 아니든 무조건 실행하는 녀석이었고, orElseGet은 empty일 때만 실행되는 녀석이었다.

따라서 아래와 같이 orElseGet으로 바꿔주니 잘 동작했다.

Member member = memberRepository.findByOauthId(userProfile.getOauthId())
                                .orElseGet(() -> memberRepository.save(userProfile.toMember()));

CQS

(추가하기)

2개의 댓글

comment-user-thumbnail
2022년 6월 29일

JPA 관련해서 공부하고 있는데, 너무 잘 정리되어있네요:)
공유 감사합니다😎

답글 달기
comment-user-thumbnail
2024년 2월 9일

좋은글 잘봤습니다

답글 달기