[스프링부트 JPA 활용] JPA 동작확인

[스프링부트 JPA 활용] JPA 동작확인

JPA 동작확인


JPA와 DB설정#


여태까지 application.properties 를 이용해왔는데, 간혹 yml을 사용하는 것을 봐왔는데 이번에 사용하게 되었습니다.

resources/application.properties를 확장자를 변경하여 백업(또는 제거) 하고 같은 resources/ 위치에 application.yml 생성

contact

resources/application.yml

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/jpashop; # MVCC=true H2 1.4.200 버전부터 MVCC 옵션이 제거되었습니다. 
    username: sa
    password:
    driver-class-name: org.h2.Driver

  jpa:
    hibernate:
      ddl-auto: create # 애플리케이션 동작 시점에 엔티티 재생성
    properties:
#      show_sql: true #sysout을 통해 남기는 sql
      format_sql: true
    database: h2
     
  devtools:
    livereload:
      enabled: true # livereload 사용시 활성화
    restart:
      enabled: false #운영 에서는 제거.

  thymeleaf:
    cache: false


logging:
  level:
    org.hibernate.SQL: debug

이렇게만 세팅하여도 SpringBoot에서 커넥션풀(HikariCP) 설정을 해줍니다.

이러한 설정에 대한 메뉴얼은 스프링부트의 LEARN의 버전별 Reference Doc.에 있습니다.

https://docs.spring.io/spring-boot/docs/current/reference/html/

contact

contact

MVCC=true 옵션 설명

MVCC=true#

  • 다중 버전 동시성 제어 (Multi-Version Concurrency Control)
  • 동시성을 제어하기 위해 사용하는 매커니즘 중 하나

동시성이란 데이터베이스에 동시 접근하는 것을 의미

일관성과 동시성은 반비례관계 동시성↑일관성↓ / 동시성↓일관성↑

동시에 DB에 접근하는 사람이 많으면 데이터가 일관적이지 않을 수 있기 때문에 동시성을 낮출수 밖에 없다.

‘동시성 제어’란 동시에 실행되는 트랜잭션을 최소화하며, 일관성을 최대화 하여 데이터 무결성 유지 되도록 하는것이 목표

읽기와 쓰기가 서로의 작업을 방해해 동시성 저하와 리소스 Lock을 사용함에도 데이터 일관성이 훼손될 수 있는 문제를 해결하기 위해 MVCC 매커니즘을 사용

MVCC 매커니즘#

  • 데이터를 변경 할 때 변경사항을 Undo 영역에 저장.
  • 데이터를 읽다가 트랜잭션 시작한 시점 이후 변경된 값을 발견하면 Undo 영역에 저장된 정보를 이용해 버전을 생성하고 그것을 읽는다.

장점#

  • Lock을 기다릴 필요가 없어 일반적인 RDBMS보다 빠르다.
  • 데이터를 읽을 때 다른 사용자의 CRUD에 영향을 받지 않는다.

단점#

  • 데이터의 버전 충돌이 있을 수 있다.
    • 애플리케이션 영역에서 문제를 해결해야함.
  • 사용하지 않는 버전들에 대한 정리가 필요하다.

JPA 동작확인#


엔티티 생성#


엔티티 패키지 java/jpabook/jpashop/entity 패키지 생성

java/jpabook/jpashop/entity/Member.java

package jpabook.jpashop.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
@Getter @Setter // Lombok으로 Getter Setter 사용
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String username;

}

엔티티 패키지 java/jpabook/jpashop/repository 패키지 생성

@PersistenceContext#

JPA를 쓰기 때문에 EntityManger가 있어야 하는데 SpringBoot를 사용하면 스프링컨텍스트 위에서 동작하기 때문에, ‘@PersistenceContext’가 붙어진 EntityManger에 EntityManger 객체를 스프링에서 자동으로 주입해줍니다.

java/jpabook/jpashop/repository/MemberRepository.java

package jpabook.jpashop.repository;

import jpabook.jpashop.entity.Member;
import org.springframework.stereotype.Repository;

import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Repository
public class MemberRepository {

    @PersistenceContext
    private EntityManager em;

    public Long save(Member member){
        em.persist(member);
        return member.getId();
    }

    public Member find(Long id){
        return em.find(Member.class, id);
    }
}


return member.getId(); member를 직접 반환하지 않고 member.getId()를 반환하는 이유는

command와 query를 분리해라 라는 원칙에 따라서

저장을 하고나온 member는 바로 사용하게 되면 사이드이펙트가 발생할 여지가 있기 때문에, 최소한의 id만 리턴하여 다시 사용할때는 다시 조회해서 사용하는게 안전합니다.

테스트 코드#

테스트 코드를 작성할 클래스명을 블록잡아서 Alt + Enter > 테스트 생성

contact

JUnit5로 선택하고 확인

Alt + Insert로 빈 테스트코드 생성

테스트코드 라이브템플릿 추가#

Ctrl + Alt + S

contact

추가를 눌러 tdd로 새로운 템플릿을 작성합니다.

@Test
public void $NAME$() throws Exception{
    //given
    $END$
    //when
    
    //then
}

*Test.java 에디터에서 tdd를 입력하고 tab을 눌러 tdd 템플릿을 사용할 수 있습니다.

java/jpabook/jpashop/repository/MemberRepositoryTest.java

package jpabook.jpashop.repository;

import jpabook.jpashop.entity.Member;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class MemberRepositoryTest {

    @Autowired MemberRepository memberRepository;

    @Test
    public void memberTest() throws Exception{
        //given
        Member member = new Member();
        member.setUsername("MemberA");

        //when
        Long saveId = memberRepository.save(member);
        Member findMember = memberRepository.find(saveId);

        //then
        Assertions.assertEquals(findMember.getId(), member.getId());
        Assertions.assertEquals(findMember.getUsername(), member.getUsername());

    }
}

테스트를 실행하면, 오류가 발생합니다.

No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call; nested exception is javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call

현재 스레드에 사용할 수 있는 실제 트랜잭션이 있는 EntityManager가 없습니다. '영속' 호출을 안정적으로 처리할 수 없습니다. 중첩 예외는 javax.persistence.TransactionRequiredException입니다: 현재 스레드에 사용할 수 있는 실제 트랜잭션이 있는 EntityManager가 없습니다. '영속' 호출을 안정적으로 처리할 수 없습니다.

EntityManager는 트랜잭션 안에서 동작하여야 안정적인 처리를 할 수 있다는 오류를 뱉어냅니다.

MemberRepositoryTest.java

package jpabook.jpashop.repository;

import jpabook.jpashop.entity.Member;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;


import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional 
/* org.springframework.transaction.annotation.Transactional 권장 */
class MemberRepositoryTest {

    @Autowired MemberRepository memberRepository;

    @Test
    public void memberTest() throws Exception{
        //given
        Member member = new Member();
        member.setUsername("MemberA");

        //when
        Long saveId = memberRepository.save(member);
        Member findMember = memberRepository.find(saveId);

        //then
        Assertions.assertEquals(findMember.getId(), member.getId());
        Assertions.assertEquals(findMember.getUsername(), member.getUsername());

    }
}

다시 테스트를 실행하면 테스트가 성공한 것을 확인 할 수 있습니다.

contact

Test를 진행하면 DB에 쌓이지 않고 Rollback을 하게 되는데 @Rollback(false) 어노테이션을 추가하면 데이터가 쌓이는 것을 확인 할 수 있습니다.

여기까지 확인을 하게되면 JPA가 잘 세팅 되어있는 것을 확인 할 수있습니다.

Junit5#

Junit4의 assertThat() 과 isEqualTo 사용하기 위해서는 Hamcrest 라이브러리의 도움을 받아햐 합니다.

gradle.build

dependencies{
	implementation 'org.hamcrest:hamcrest-core:2.2'
}
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

 assertThat(findMember, equalTo(member));
 assertThat(findMember, is(member));
 

생성한 member와 조회한 findMember는 같은 객체일까 ?

package jpabook.jpashop.repository;

import jpabook.jpashop.entity.Member;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;


import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
/*@Rollback(false) 테스트 시 데이터 쌓임 */
class MemberRepositoryTest {

    @Autowired MemberRepository memberRepository;

    @Test
    public void memberTest() throws Exception{
        //given
        Member member = new Member();
        member.setUsername("MemberA");

        //when
        Long saveId = memberRepository.save(member);
        Member findMember = memberRepository.find(saveId);

        //then
        Assertions.assertEquals(findMember.getId(), member.getId());
        Assertions.assertEquals(findMember.getUsername(), member.getUsername());
        assertThat(findMember, equalTo(member));
    }
}

contact

같은 영속성 컨텍스트 안에서는 생성한 객체와 조회한 객체가 같습니다.

1차 캐시에 값이 있기 때문에, 굳이 재조회를 하지 않고 값을 찾아 넣어줍니다.

Jar 빌드해서 동작 확인#

Gradle 빌드를 합니다.

윈도우 cmd 창 프로젝트 경로에서

.\gradlew.bat // 배치 실행 자동으로 .\이 붙네요

.\gradlew build //윈도우 11에서는 이렇게도 실행되네요 

contact

실행을 마치고 나면, ~~\jpashop\build\libs 프로젝트 빌드 라이브러리 안에 Jar 파일이 생성됩니다.

contact

생성된 Jar를 실행해 봅니다.

java -jar .\jpashop-0.0.1-SNAPSHOT.jar

contact

스프링부트에서는 Jar기동만 해도 스프링부트가 톰캣을 실행 시키는군요 이런 방식으로는 처음 구동해 보네요

스프링부트의 자동설정#

스프링부트를 통해 persistence.xml이나 localContainerEntityFactoryBean 등 번거러운 설정을 자동으로 해준다.

쿼리 파라미터 로그 남기기#

https://github.com/gavlyukovskiy/spring-boot-data-source-decorator

contact

오픈소스로 사용할 수 있는 라이브러리 spring-boot-data-source-decorator의 P6Spy를 추가해 보도록 하겠습니다.

P6Spy - 대부분의 Connection, Statement 및 ResultSet 메소드 호출을 가로채는 것을 포함하여 SQL 쿼리를 가로채고 기록하는 기능을 추가합니다.

contact

Quick Start를 보고 p6spy를 build.gradle에 추가합니다.

dependencies {
	implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.0'
}

이후 설정 리로딩 후 라이브러리를 받은 후 바로 적용이 됩니다.

테스트를 돌려보면, p6spy 가 포함된 로그를 볼 수 있습니다.

contact

p6spy 설정 application.yml에 필요한 옵션을 추가하면 된다.

# JDBC 이벤트를 기록하기 위해 P6LogFactory 등록
decorator.datasource.p6spy.enable-logging=true
# com.p6spy.engine.spy.appender.SingleLineFormat 대신 com.p6spy.engine.spy.appender.MultiLineFormat 사용
decorator.datasource.p6spy.multiline=true
# 기본 리스너 [slf4j, sysout, file, custom]에 대한 로깅 사용
decorator.datasource.p6spy.logging=slf4j
# 사용할 로그 파일(logging=file일 때만)
decorator.datasource.p6spy.log-file=spy.log
# 사용할 클래스 파일(logging=custom인 경우에만). 클래스는 com.p6spy.engine.spy.appender.FormattedLogger를 구현해야 합니다.
decorator.datasource.p6spy.custom-appender-class=my.custom.LoggerClass
# 사용자 지정 로그 형식(지정된 경우 com.p6spy.engine.spy.appender.CustomLineFormat이 이 로그 형식과 함께 사용됨)
decorator.datasource.p6spy.log 형식=
# 정규식 패턴을 사용하여 로그 메시지를 필터링합니다. 지정된 경우 일치하는 메시지만 기록됩니다.
decorator.datasource.p6spy.log-filter.pattern=
# 추적 시스템에 유효한 SQL 문자열(실제 값으로 대체된 '?')을 보고합니다.
# 참고 이 설정은 로깅 메시지에 영향을 주지 않습니다.
decorator.datasource.p6spy.tracing.include-parameter-values=true

이런 기능들은 개발시 편리와 도움을 주지만, 운영에 배포할 때에는 성능에 이슈가 있을 수 있어서 성능 테스트 이후 제거하거나 수정해서 배포를 하도록해야합니다.

참고#