Spring/SpringBoot

프로젝트 환경설정 - JPA와 DB 설정, 동작확인

느리지만 꾸준하게 2022. 3. 30. 01:34

 

  • 참고: 스프링부트를 통해 복잡한 설정이 다 자동화되었다. persistence.xml 도없고,

 

  • LocalContainerEntityManagerFactoryBean 도 없다.

 

  • 스프링 부트를 통한 추가 설정은 스프링 부트 메뉴얼을 참고하고,

 

  • 스프링 부트를 사용하지 않고 순수 스프링과 JPA 설정 방법은 자바 ORM 표준 JPA 프로그래밍 책을 참고하자.

 

  • application.properties파일을 지우고 application.yml파일을 생성해준다.

 

 

 

 

 

세팅을 한번 해주자.

// application.yml

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/jpashop #4칸 // MBCC=TRUE라는 옵션은 빼준다.
    username: sa
    password:
    driver-class-name: org.h2.Driver
    // 데이터베이스 커넥션과 관련된 데이터설정이 완료가 된다.
    


// jpa와 관련된 세팅
  jpa:
    hibernate:
      ddl-auto: create // 자동으로 테이블을 만들어 준다. 즉 어플리케이션 가지고 있는 테이블을 다 지우고(엔티티를 확인한 후에)
      // 테이블을 다시 생성한다.
    properties:
      hibernate:
#        show_sql: true // systemout에 출력
        format_sql: true

logging.level:
  org.hibernate.SQL: debug // hibernate.SQL모드를 debugger로 쓴다는건데
  // 즉 hibernate나 jpa가 생성하는 sql이 다 보인다.
  org.hibernate.type: trace

 

이제 실제 동작하는지 확인하기 위해 회원 엔티티를 만들어준다. Member class

Entity를 해주고 Id를 매핑을 해준다음에 

// Member class

package jpabook.jpashop;


import lombok.Getter;
import lombok.Setter;

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

// lombok을 쓰니까 Getter과 Setter을 설정을 @로 해준다.
@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue // GeneratedValue는 자동생성 해준다.
    private Long id;
    private String username;
}

 

이제 Repository를 만들어주면 (엔티티를 찾아주는 역할)

지금하고 있는 작업이 SpringBoot를 쓰고 있기 때문에 Spring Container위에서  다 동작하게 된다.

package jpabook.jpashop;

import org.springframework.stereotype.Repository;

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

@Repository
public class MemberRepository {


	// 엔티티 매니저는 PersistenceContext로 설정해줌.
    // 스프링 부트가 아래 어노테이션에 있으면 EntityManager를 주입을 해준다. 그냥쓰자.
    @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);
    }

}

 

아래꺼 들어가면서 entity 매니저가 생성하는게 자동으로 들어가고 

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

 

아래 설정파일을 다 읽어서 알아서 생성해주는 코드가 만들어진다.

// application.yml

  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
#        show_sql: true
        format_sql: true

MemberRepository 클래스에서 command + shitf + t를 눌러서 테스트를 만들어준다.

아래는 완성된 코드이고

// MemberRepositoryTest

package jpabook.jpashop;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MemberRepositoryTest {

    @Autowired MemberRepository memberRepository;

    @Test
    @Transactional
    @Rollback(false)
    public void testMember() throws Exception{
        // given
        Member member = new Member();
        member.setUsername("memberA");

        // when
        // command + option + v 단축키로 extract해서 변수 뽑아주는 단축키
        // member를 가지고 save를 하면 잘 저장됐는지 
        Long savedId = memberRepository.save(member);
        // 확인해준다.
        Member findMember = memberRepository.find(savedId);

        // then
        Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
        Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername());

    }
}

 

아래 코드를 실행시켰을 때 에러가 나야하는데

// MemberRepositoryTest

package jpabook.jpashop;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MemberRepositoryTest {

    @Autowired MemberRepository memberRepository;

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

        // when
        // command + option + v 단축키로 extract해서 변수 뽑아주는 단축키
        // member를 가지고 save를 하면 잘 저장됐는지 
        Long savedId = memberRepository.save(member);
        // 확인해준다.
        Member findMember = memberRepository.find(savedId);

        // then
        Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
        Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername());

    }
}

왜냐. transaction이 없다는 이야기인데 entity manager를 통한 모든 데이터변경은 항상 트랜잭션 안에서 이루어져야 한다.

 

memberRepository에다가 직접 트랜잭션을 걸어도 되는데

test케이스에 직접 트랜잭션을 걸어줘보자.

트랜잭션 어노테이션은 스프링프레임워크를 쓰고있으니까 첫번째꺼를 선택해주자.(쓸 수 있는 옵션이 많다.)

MemberRepositoryTest 코드를 돌려주게 되면 정상적으로 실행이 되고

package jpabook.jpashop;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MemberRepositoryTest {

    @Autowired MemberRepository memberRepository;

    @Test
    // 트랜잭션 어노테이션 추가
    @Transactional
    public void testMember() throws Exception{
        // given
        Member member = new Member();
        member.setUsername("memberA");

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

        // then
        Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
        Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername());

    }
}

jdbc url을 yml에 있는 아래걸로 설정을 해주고 들어가준다.

url: jdbc:h2:tcp://localhost/~/jpashop #4칸

테이블 생성된거는 id username primary key로 되어있는데 매핑한대로 세팅이 된다.

application.yml애서 ddl-auto: create를 해주었기 때문

@Transactional 어노테이션이 테스트케이스에 있으면 테스트가 끝난다음에 바로 롤백을 해버린다.(db로 롤백을 함)

데이터가 들어가 있으면 반복적인 테스트를 못하기 때문에 다 롤백을 한다.

 

@Rollback 어노테이션을 false로 해서 돌려주고 h2 db를 실행하면 데이터가 들어있는걸 볼 수 있다.(롤백안하고 커밋해줌)

=> JPA 동작확인 

@Test
@Transactional
@Rollback(false)
public void testMember() throws Exception{
    // given
    Member member = new Member();
    member.setUsername("memberA");

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

    // then
    Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
    Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername());

 

재밌는 테스트를 한번 더 해보자. 저장한거랑 조회한거랑 같은지 다른지

Assertions.assertThat(findMember).isEqualTo(member);

true가 나온다. findmember랑 member랑 같은 트랜잭션 안에서 저장하고 조회하면 영속성 컨테스트가 똑같다. 

같은 영속성 context 안에서는 id 값이 같으면 같은 식별자로 인식하기 때문이다.

(영속성 context에서 식별자가 같으면 같은 엔티티로 인식한다.)

package jpabook.jpashop;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MemberRepositoryTest {

    @Autowired MemberRepository memberRepository;

    @Test
    @Transactional
    @Rollback(false)
    public void testMember() throws Exception{
        // given
        Member member = new Member();
        member.setUsername("memberA");

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

        // then
        Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
        Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
        Assertions.assertThat(findMember).isEqualTo(member);
        System.out.println("findMember == member " + (findMember == member));

    }
}

그래서 build결과를 보면 insert쿼리만 있게된다.

 

jar 빌드해서 동작 확인 해보자.

./gradlew clean build
Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details

> Task :test
2022-03-29 21:52:36.972  INFO 3935 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2022-03-29 21:52:36.972  INFO 3935 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2022-03-29 21:52:36.977  INFO 3935 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

 

 

터미널에서 jar로 build하고 8080 port로 가면 바꿨던 것들을 다 확인할 수 있다.

milaju@MacBook-Pro libs % ls
jpashop-0.0.1-SNAPSHOT-plain.jar	jpashop-0.0.1-SNAPSHOT.jar

milaju@MacBook-Pro libs % ls -al
total 79208
drwxr-xr-x   4 milaju  staff       128  3 29 21:52 .
drwxr-xr-x  10 milaju  staff       320  3 29 21:52 ..
-rw-r--r--   1 milaju  staff      4508  3 29 21:52 jpashop-0.0.1-SNAPSHOT-plain.jar
-rw-r--r--   1 milaju  staff  40546114  3 29 21:52 jpashop-0.0.1-SNAPSHOT.jar

// 쭉 build하게 된다.
milaju@MacBook-Pro libs % java -jar jpashop-0.0.1-SNAPSHOT.jar

 

참고파일을 보자.

스프링부트를통해복잡한설정이다자동화되었다.persistence.xml 도없고,

LocalContainerEntityManagerFactoryBean 도 없다. 

스프링 부트를 통한 추가 설정은 스프링 부트 메뉴얼을 참고하고, 

스프링 부트를 사용하지 않고 순수 스프링과 JPA 설정 방법은 자바 ORM 표준 JPA 프

로그래밍 책을 참고하자.

 

  • 쿼리 파라미터 로그 남기기(개발단계에서는 ok)
  • (운영단계 배포단계에서는 성능을 저하시킬수도 있어서 병목이 될 수 있어서
  • 성능테스트를 해보고 쓰자.

그리고 마지막에 결과 Insert 쿼리를 보면 ? ? 로 남게 되었다. 

application.yml파일에다가 구문을 추가해보자. 로깅할 때 타입을 trace로 주면 파라미터 로그를 찍어준다.

logging.level:
  org.hibernate.SQL: debug
  org.hibernate.type: trace

근데 아래와 같이 values의 값도 알고싶다 그러면 외부 라이브러리를 쓰면 된다.

데이터베이스 커넥션을 래핑해서 sql statement을 이해해서 출력 로그를 보여주는 라이브러리이다.

P6Spy을 써보자. 버전은 1.5.6으로 가져와서 넣어준다.

// build.gradle

// denpendencies에다 추가해준다.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-devtools'

// SpringBoot가 버전정보를 세팅 안해줬기 때문에 1.5.6으로 세팅해준다.
	implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.6'

다시 run해보면

첫번째는 원본이 출력되고 두번째는 parameter bind된게 출력된다.(로그를 어떻게 찍을지 옵션은 여기서 한번 찾아보고 수정해서 쓰자.

커넥션을 얻어온거도 다 모니터링이 된다.

 

 

 

 

 

 

 

자료참고

 

JPA & ddl-auto 검색 후 application.yml파일 작성법 확인

 

jpa 검색

참고:https://docs.spring.io/spring-boot/docs/2.1.7.RELEASE/reference/html/

 

Spring Boot Reference Guide

Authors Phillip Webb, Dave Syer, Josh Long, Stéphane Nicoll, Rob Winch, Andy Wilkinson, Marcel Overdijk, Christian Dupuis, Sébastien Deleuze, Michael Simons, Vedran Pavić, Jay Bryant, Madhura Bhave

docs.spring.io

 

 

ddl-auto검색

https://docs.spring.io/spring-boot/docs/2.1.7.RELEASE/reference/html/howto-data-access.html#howto-configure-jpa-properties

 

84. Data Access

Spring Boot includes a number of starters for working with data sources. This section answers questions related to doing so. 84.1 Configure a Custom DataSource To configure your own DataSource, define a @Bean of that type in your configuration. Spring Boo

docs.spring.io

 

번외 - 에러로 인해서 힘들었던 구간을 참고하면서 다시 생각해보자.

 

 

 

<출처 김영한: 실전! 스프링 부트와 JPA 활용1 - 웹 어플리케이션 개발 >

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-%ED%99%9C%EC%9A%A9-1/dashboard

 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., - 강

www.inflearn.com