본문 바로가기

FastCampus - 한번에 끝내는 Java|Spring 웹 개발/04 JPA

JPA Ch 7 영속성 - 개요 & 리얼 DB - 패스트캠퍼스 챌린지 29일차

 

한 개 강의가 48분에 육박하는 긴 강의라 2배속으로 봐도 시간소요가 컸습니다. 중간중간 일시정지한 것도 있구요. 4배속 진짜 필요하다니까...

오늘 강의의 주된 내용은 두 가지입니다.

  • JPA 를 깊게 이해하기 위한 개념설명: "Persistence Context" 의 개념만 가볍게
    (Spring Boot를 쓰면 이 짓을 수동으로 할 필요가 없으니까)
  • H2 에서 리얼 디비로 변경 (MySQL) & 트러블슈팅 및 추가 설명

Persistence Context (영속맥락?)

여기에서는 다음 강의부터 자세하게 배울 개념적인 부분을 살짝 공부하고 넘어갑니다.

Persistence Context가 뭔데?

강의에서 설명은 "Spring이 Bean을 관리하는 것처럼" 이라는 비유 방식이었습니다. JPA에서 DB/파일로 저장되는 객체를 관리하는 문맥이 Persistence Context 라는 거죠. 다시 설명하자면, Spring DI Container 가 Bean 을 관리한다면, JPA Persistence Context의 Container 는 DB에 저장될 객체, 즉 엔티티Entity 를 관리합니다. 관리하는 대상이 다르지만 비슷한 개념인 셈입니다.

소스 / 문서 보여줘

interface EntityManager (Jakarta Persistence API)

오... 이 작명방식은 흔한 ~Manager... 각설하고,

EntityManager가 Persistence 컨테이너입니다. 인터페이스니까 hibernate 같은 실 구현체가 구현하겠죠. 엔티티를 저장하고 불러오고 하는 API들이 기술되어 있습니다. 강의를 일시정지하고 거기에 있는 문서를 읽어봤는데 영어가 좀 어렵더라구요...

Interface used to interact with the persistence context.
(EM은) Persistence Context 와 상호작용하기 위한 인터페이스입니다.
An EntityManager instance is associated with a persistence context.
"EM 인스턴스"Persistence Context와 연결되어 있습니다. (연관, 묶여있다, 연동, 아무튼 1:1 대응이라는 뜻)

A persistence context is a set of entity instances / in which / for any persistent entity identity / there is a unique entity instance.
- Persistence Context는 엔티티 인스턴스의 집합입니다.
- 그런데 그 (엔티티 인스턴스의 집합)은 말이죠,
- 모든 영속되는 엔티티의 ID마다 (Persistent Entity Identity) 각각의 고유한 인스턴스가 있어요.
  => (역은 성립하지 않는 문장이겠지만, 굳이 역을 취해보자면 "영속할 인스턴스마다 고유 ID가 있다"는 게 되겠습니다.)

Within the persistence context, the entity instances and their lifecycle are managed.
엔티티 인스턴스가 Persistence Context 안에 있다면 엔티티 인스턴스와 엔티티 인스턴스의 라이프 사이클이 (개발자가 수동으로 관리해야 하는 것이 아니라 시스템?EM?에 의해) 관리됩니다.

The EntityManager API is used to create and remove persistent entity instances, to find entities by their primary key, and to query over entities.
EntityManager API 는 영속하는 엔티티 객체(Persistent entity instance)를 만들거나 없애거나 P. Key 로 찾거나 쿼리를 때리는 용도입니다.
The set of entities that can be managed by a given EntityManager instance is defined by a persistence unit.
Persistence Unit 하고 묶여있는 EntityManager가 있다면, 이게 엔티티 집합을 관리(manage)할 수 있습니다.

A persistence unit defines the set of all classes that are related or grouped by the application, and which must be colocated in their mapping to a single database.
Persistence Unit 은 클래스 집합을 정의합니다. 이 클래스 집합은...
- 앱하고 관계가 있거나, 앱이 집합으로 묶어놨어요. 그리고
- 반드시 대응하는 DB의 특정 위치에 있어야 해요.

아따 영어 피동 표현 겁나 많네

뭐 아무튼 그렇습니다. EntityManager 가 PersistenceContext를 관리하는 놈입니다. 실제 API Surface 라는 말이죠.

PersistenceContext 라는 어노테이션도 찾아보니까 있던데 이건 강의에서 다루지 않으셨으니 일단 링크만 걸어놓고 패스

META-INF/persistence.xml == EntityManger의 세팅
: 스프링 Data JPA를 쓰지 않았더라면 했어야 할 일

EntityManager의 설정은 기본값 위치가 META-INF/persistence.xml 인 XML로 합니다. 딱 봐도 완전 번거로워보이는 이런 작업은 Spring Data JPA 가 대충 알아서 해주고 있기에 우린 별로 신경을 쓰지 않아도 됩니다. "XML이라니... 이게 바로 ㅌㄸ 냄새인가?"

관련 코드는 LocalContainerEntityManagerFactoryBean 의 함수 setPersistenceXmlLocation (Spring 메서드) 이 있습니다. (이걸 잘도 필기했다...)

내용은 강의에서는 스킵했는데, s/o 를 대충 보니 JDBC 커넥션 URI 같은 게 들어있나봅니다.


이미 500자는 진작에 넘은 거 같은데... 내용은 절반밖에 안 왔는데...


리얼 DB 세팅

드디어 H2 데이터베이스를 떠나 "진짜 데이터베이스" 를 사용합니다. (명칭이야 제가 맘대로 그렇게 부른거구요...)

내용은

  • DB 세팅 (강의에서는 설치 방법은 스킵, IDE에서 만지는 법만 확인)
  • 테스트를 고치면서 여러가지 수정 (전체 테스트 실행)
    • DB 관련 application.yml 추가
    • 그 외 이것저것 트러블슈팅 (@WebMvcTest 에는 (당연히도) 없는 DB 컨텍스트 대응하기라던가)

정도입니다.

강의에서는 MySQL을 쓰는데, DB 설치하는 게 무엇보다도 귀찮다는 걸 진작에 알고있기에 저는 기존에 개인 프로젝트에서 쓰던 개발용 docker-compose 파일을 가져와서 대충 wsl 에 postgresql 을 올리기로 했습니다. 여기 올려두면 나중에 내가 참고하겠지??

# docker/docker-copmpose.dev.yml
version: '3.8'
services:
  postgres:
    image: postgres:12
    restart: always
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    ports:
      - 5432:5432
#!/bin/bash

# scripts/setup_dev.sh 파일입니다.

PWD_BACK=`pwd`
cd "$(dirname "$0")"
. ../.env

COMPOSE="docker-compose -f ../docker/docker-compose.dev.yml --env-file ../.env"

$COMPOSE down
$COMPOSE up -d
# wait for pgsql
until $COMPOSE exec postgres pg_isready --username=${POSTGRES_USER} --dbname=${POSTGRES_DB} --timeout=10 --quiet;
    do sleep 1;
done
sleep 1;

cd $PWD_BACK
# .env 파일 내용입니다. 이 줄은 지우시고.
POSTGRES_USER="jpasandbox_role"
POSTGRES_DB="jpasandbox_dev"
POSTGRES_PASSWORD="jpasandbox_db_password"
DATABASE_URL=postgres://jpasandbox_role:jpasandbox_db_password@localhost/jpasandbox_dev

wsl ./scripts/setup_dev.sh

원래는 DB 초기화 같은 거도 들어있었는데... 지금은 ㅎㅎ

IntelliJ IDEA 에서 연결하기 (Ultimate)

강의에서는 이게 얼티밋용 기능이라는 얘기가 빠져있는데요, n년차 올프로덕트팩 구독자(...)인 저는 사용 가능합니다. 연결이 뭐가 잘 안 되네요.

성공한 스샷

application.yml 설정 / 디펜던시 추가

강의에서 트러블슈팅을 하면서 application.yml 이 조금조금씩 수정되었습니다. 기본적인 커넥션 뿐만이 아니라 다른 것들도요.

아 그러고보니 그 전에 pgsql 커넥션 라이브러리 추가해야지... 이것만큼은 검색;
아 이거 Azure 문서 보니까 Spring Initializr 에서 들고오는걸로 되어있네요. 이니셜라이Zㅓ 에 explore 기능이 있어서 살짝 들여다봤는데 이겁니다.

  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  implementation 'org.springframework.boot:spring-boot-starter-web'
  runtimeOnly 'org.postgresql:postgresql' << 이거

위에 두 개는 이미 있지만 다른게 뭐가 추가될까 싶어서 혹시 몰라서 넣어봄

yml에는 아래와 같은 내용을 추가합니다.

spring:
  jpa:
    generate-ddl: true # << 프로덕션에는 안 돼요!
    hibernate:
      ddl-auto: create-drop # << 프로덕션에는 안 돼요!
  datasource:
    driver-class-name: org.postgresql.Driver # 이거 필요한지 모르겠음
    url: jdbc:postgresql://localhost:5432/jpasandbox_dev
    username: jpasandbox_role
    password: jpasandbox_db_password
    initialization-mode: always # deprecated 입니다. 아래걸로 바꿉시다.
  sql:
    init:
      mode: always # 위에꺼 대신 씁니다. 마찬가지로 프로덕션엔 안 돼요!

generate-ddl vs ddl-auto with initialization-mode

  • spring.jpa.generate-ddl 은 JPA 쪽 옵션인데 (구현체 무관), 앱 시작시 DDL을 만들어서 초기화할지 여부입니다.
  • spring.jpa.hibernate.ddl-auto 는 hibernate 쪽 옵션인데, create-drop 을 쓰면 (시작할 때 테이블을 만들고) (끝날 때 테이블을 드롭합니다.)
    • 몇 가지 옵션이 더 있습니다.
      • create : 시작할 때 드롭하고 새로 만듭니다.
      • update: 갱신
      • validate: 검증만 (실패하면 오류)
      • none <- 프로덕션용 (DDL 갱신은 수동!)

보통 이 두가지는 프로젝트 처음에만 세팅하니까 잘 모르는 경우가 많다네요. 친절하게 설명해주셨는데, spring 문서에 따르면 hibernate의 ddl-auto 쪽이 우선된다네요. 하긴 그게 더 상세하니까...

  • spring.datasource.initialization-mode (deprecated, 쓰지 마시오)
    -> 변경 ->
    spring.sql.init.mode
    는 앱 시작시에 DB를 초기화할지 여부입니다.
    • always : 항상 초기화합니다.
    • embedded : h2 같은 임베디드 DB일때만 초기화합니다. (이래서 memory 에서 파일로 바꾸면 안 됐던거)
    • never : 초기화가 뭐죠? (프로덕션용)

이것과 관련해서는 resources/{ data.sql, scheme.sql } 에 대한 언급이 있었습니다.

scheme.sql 의 경우 위의 값이 "always" 이면 실행되고, 내용은 DB의 스키마 초기화입니다. 즉, ddl-auto 와 의미가 겹칩니다. 같은 짓인데 자동이냐 수동이냐가 되겠습니다. 이 경우, 역시나 더 상세한 "수동지정"에 해당하는 always 이고 scheme.sql 이 있으면 의 경우가 우선되고 ddl-auto 는 무시된다고 합니다.

다 됐냐 테스트 돌리자

짜잔

당연히 gradle 에서 test 를 골라서 돌리면 됩니다. 많이 해본거라.

다만 저는 앞에서 했던 실습을 전부 따라하지는 않았어서 양이 좀 적습니다. 뭐 비슷하겠죠?

두 번째 돌리면 안 되잖아...?

(1) pgsql 에 쓰면 안 되는 이름들이 있었습니다. user를 빠르게 Account 로 개명해줬습니다. 혹시나 해서.

https://stackoverflow.com/a/39055151

(2) data.sql 에 문제가 있습니다. ` 를 " 로 바꿔줬습니다. 사실은 이 쪽이 원인이었음.

이 문제는 강의에서는 없었습니다. DB를 다른 걸 써서...

테스트 "class"에 @Transactional 을 붙이자

전체 테스트를 돌리면 실패하는데 단일 테스트를 실행하면 성공한다, 이 경우는 ID를 직접 확인한 경우였습니다. 이전 테스트에서 실행한 내용이 영향을 미치기 때문인데, 실행 후 바로 롤백을 하기 위해서 클래스에 @Transactional 을 붙여줍니다.

사실 Transactional 이 이런 의미는 아닐텐데 (commit이 있겠죠... commit이 안 되니 사이드 이펙트로 롤백) 나중 강의에 나온다네요.

@WebMvcTest 에서 JPA metamodel 을 찾으려는 문제

저어...는 실습 때 WebMVC 테스트를 하는 게 너무 뻔해보여서 안 했었는데, 강의에서는 이 테스트가 추가되어있어서 문제가 되었습니다.

원인은 @WebMvcTest 가 앱의 컨텍스트(bean들)를 전부 로드하지 않는다는 거고, 그 중에는 Audit 에 필요한... @EnableJpaAuditing 도 있다는 게 문제입니다. auditing 을 하려면 DB가 있어야 하고, DB는 WebMvcTest에는 기본으로 로드되지 않으니까요.

에러 내용: "JPA metamodel must not be empty"

해결 방법은 3가지입니다.

  1. @MockBean(JpaMetamodelMappingContext.class) 를 테스트 클래스 위에 추가
    (kotlin 이라면 @MockBean(classes = [JpaMetamodelMappingContext::class]))
    => JPA가 필요하지 않을 때 씁니다. mocking 하는거니까 가짜 JPA 매핑 컨텍스트가 만들어지겠죠...
  2. @EnableJpaAuditing 을 Application 클래스에서 별도의 @Configuration 클래스로 옮깁니다.
    이러면 테스트에서 로드가 안 된대요. 앱용 설정이니까... 이건 처음 알았습니다.
    @Configuration
    @EnableJpaAuditing
    public class JpaSandboxConfiguration {
    }​

    강사님 권장 방법인데 살짝 미묘함... 이러면 테스트에서는 Auditing 이 안 된다는 말이잖아요? 권장 사유는 위아래의 두 가지보다 실수할 가능성이 적다는 것.
  3. 테스트를 컨텍스트를 잘라서 일부만 로드하는 @WebMvcTest 에서 풀 테스트인 @SpringBootTest로 변경.
    다만, 이렇게 하면 mockMvcTest 를 스프링 컨텍스트에서 직접 로드해와야 합니다.
    // 필기한 내용에서만 옮깁니다. 직접 에디터에 넣어보긴 귀찮음
    @SpringBootTest
    class MyWebMvcTest {
        // @Autowired // <- 이거 주석처리
        MockMvc mockMvc;
    
        @Autowired
        WebApplicationContext wac;
        
        @BeforeEach // 이거 JUnit 4? 5? 아 강의에서 5 쓴댔지
        void before() {
            mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
        }
        
        // 테스트 코드가 이어져야 합니다.
    }​

어... 사실 이 뒤에 트러블슈팅을 마저 해서 강의처럼 모든 테스트가 통과되게 만들어야 하는데요, 오늘 시간을 너무 많이 썼어요. 일단 이대로 가야할 것 같습니다. 주말에 해결하던지 내일 해결하던지...

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online

잠도 자야 하고 금주 링크도 제출해야해서... 해피 먼데이!