[SpringBoot] #3 스프링부트에서 JPA로 데이터베이스 다뤄보자

✨New Wisdom✨·2020년 11월 15일
0

📕 SpringBoot 📕

목록 보기
2/8
post-thumbnail

2020/05/18 에 네이버 블로그에서 작성한 내용 이사 🚗

🚩 JPA 소개

현대 웹 애플리케이션에서는 대부분 관계형 DB를 사용하니 객체를 관계형 DB에서 관리하는 것은 중요하다.

관계형 DB

어떻게 데이터를 저장할지에만 초점

객체지향 프로그래밍 언어

기능과 속성을 한 곳에서 관리하는 기술

SQL을 사용하고자 했을 때 문제점?

  • DB가 SQL만 인식할 수 있으니 각 테이블마다 기본적 CRUD를 매번 생성해야한다.
  • 패러다임 불일치

JPA는 이러한 패러다임 불일치를 중간에서 일치시켜준다!

JPA? 자바 표준명세서 (인터페이스)
인터페이스인 JPA를 사용위해서는 구현체가 필요한데 Spring에서는 Hibernate가 아닌 Spring Data JPA 모듈 사용해보자!

Spring Data JPA

  • 구현체 교체의 용이성
  • 저장소 교체의 용이성 : 관계형 DB 외에 다른 저장소로 쉽게 교체하기 위함(의존성만 교체하면 됨)

🚩 이제부터 게시판 만들기 프로젝트 할거임!

의존성 추가 : Spring Data Jpa 적용


spring-boot-starter-data-jpa

h2

  • 인메모리 RDB
  • 메모리에서 실행되서 애플리케이션 재시작할 때마다 초기화 되는 점을 이용해 테스트 용도로 많이 사용됨

Q. 왜 내장 WAS를 쓰나?

A. 언제 어디서나 같은 환경에서 스프링 부트 배포 가능

간단한 API 만들어 보기-!

domain/posts/Posts.class

package com.org.example.springboot.domain.posts;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

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

@Getter
@NoArgsConstructor
@Entity
public class Posts {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(length = 500,nullable = false)
    private String title;
    
    @Column(columnDefinition = "TEXT",nullable = false)
    private String content;
    
    private String author;
    
    @Builder
    public Posts(String title, String content,String author){
        this.title=title;
        this.content=content;
        this.author=author;
    }
}

Posts 클래스

  • 실제 DB 테이블과 매칭될 클래스(=Entity 클래스)
  • JPA 사용하면 DB 데이터 작업할 경우 쿼리 날리는게 아니라 이 Entity 클래스 수정하면 됨~

@Entity

  • 테이블과 링크될 클래스임을 나타냄

@Id

  • 해당 테이블의 PK 필드를 나타냄

@GeneratedValue

  • PK의 생성 규칙을 나타냄

@Column

  • 테이블의 칼럼, 사실 굳이 선언 안해도 해당 클래스의 필드는 모두 칼럼이 됨
  • 여기선 사이즈를 500으로 늘리고 타입을 "TEXT"로 변경함

@NoArgsConstructor

  • 기본 생성자 자동 추가

롬복의 어노테이션@들은 코드 변경량을 최소화 시켜준다.
Entity 클래스에서는 절대 Setter 메소드를 만들지 않는다.
그럼 어떻게 값을 채워 DB에 삽입하냐?
기본적으로는 생성자 통해, 값 변경 필요하면 해당 이벤트에 맞는 public 메소드로
여기서는 생성자 대신 @Builder를 통해 제공되는 빌더 클래스 사용할거임
왜냐면 빌더 사용하면 어느 필드에 어떤 값 채워야할지 명확히 인지 가능!

domain/posts/PostsRepository.Interface

DB Layer 접근자

JPA에서는 Repository라고 부름. 인터페이스로 생성함
인터페이스 생성 후 JpaRepository<Entity 클래스, PK 타입> 상속하면 기본적 CRUD 메소드 자동으로 생성됨

Entity 클래스와 기본 Entity Repository는 함께 위치해야 함!
Entity 클래스는 기본 레파지토리 없이 제대로 역할 못함


그래서 이렇게 domain패키지 안에 함께^^

🚩 Spring Data JPA 테스트 코드 작성하기

PostsRepositoryTest.class

package com.org.example.springboot.web.domain.posts;

import com.org.example.springboot.domain.posts.Posts;
import com.org.example.springboot.domain.posts.PostsRepository;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {
    
    @Autowired
    PostsRepository postsRepository;
    
    @After
    public void cleanup(){
        postsRepository.deleteAll();
    }
    @Test
    public void 게시글저장_불러오기(){
        //given
        String title="테스트 게시글";
        String content="테스트 본문";
        
        postsRepository.save(Posts.builder()
                                .title(title)
                                .content(content)
                                .author("wlgp2500@gmail.com")
                                .build());
        //when
        List<Posts> postsList=postsRepository.findAll();
        
        //then
        Posts posts=postsList.get(0);
        assertThat(posts.getTitle()).isEqualTo(title);
        assertThat(posts.getContent()).isEqualTo(content);
    }
}

@After

  • 단위 테스트가 끝날 때마다 수행되는 메소드
  • 여러 테스트가 동시에 수행되면 테스트용 DB인 H2에 데이터가 그대로 남으니 deleteAll()해주는거임

@postsRepository.save

  • 테이블 posts에 insert 또는 update 쿼리 실행

@postsRepository.findAll

  • 테이블 posts에 있는 모든 데이터 조회

Q. 근데 실제로 실행되는 쿼리 못보나😦?

A. 스프링부트에서는 application.properties, application.yml 등의 파일로 한줄의 코드로 설정 가능 & H2 쿼리 문법을 MySQL 버전으로 바꿔 출력

main/resources/application.propertiies

spring.jpa.show_sql=true
dialect.MySQL5InnoDBDialect

그럼 이제 테스트할 때 콘솔에 쿼리 로그 나옴~

+ gradle 버전 낮추기

중간에 이 책의 깃허브가서 이슈 봤는데 그레이들 버전때문에 오류났다는 글보고
나도 후다닥 gradle 4.10.2버전으로 다운그레이드 함
터미널에다가 밑에 코드 입력하면 된다.

gradlew wrapper --gradle-version 4.10.2 

🚩 등록 / 수정 / 조회 API 만들기

  • Request 데이터 받을 Dto
  • API 요청 받을 Controller
  • 트랜잭션, 도메인 기능 간의 순서 보장하는 Sevice

이 세가지 클래스가 필요함!

Spring 웹 계층

Web Layer

  • 컨트롤러와 JSP/Freemarker 등의 뷰 템플릿 영역
  • 외부 요청과 응답에 대한 전반적인 영역

Service Layer

  • @Service에 사용되는 서비스 영역
  • 일반적으로 Controller와 DAO(Data Access Object)의 중간 영역에서 사용됨
  • @Transactional이 사용되어야 하는 영역
  • 트랜잭션과 도메인 간의 순서만 보장

Repository Layer

  • Database와 같이 데이터 저장소에 접근하는 영역

Dtos

  • Dto : 계층 간에 데이터 교환을 위한 객체, Dtos : 이들의 영역
  • ex) 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등

Domain Model

  • 도메인이라 불리는 개발 대상을 모두가 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화 시킨 것
  • 비즈니스 처리를 담당하는 곳!

web/PostsApiController.class

service/PostsService.class

web/dto/PostsSaveRequestDto.class

여기서 Entity 클래스인 Posts 클래스를 또 정의한 이유는
Entity 클래스를 Request/Response 클래스로 사용하면 안되서!!!

Entity 클래스는 DB와 맞닿아 테이블 생성하고 스키마 변경되고 그러는데 화면 변경같은 빈번한 변경을
테이블과 연결된 Entity 클래스를 변경하는 건 너무 큰 변경이다.

Entity 클래스와 Controller에서 쓸 Dto는 분리해서 사용하기^^

PostsApiControllerTest.java

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @After
    public void tearDown() throws Exception{
        postsRepository.deleteAll();
    }

    @Test
    public void Posts_등록된다() throws Exception{
        //given
        String title="title";
        String content="content";
        PostsSaveRequestDto requestDto=PostsSaveRequestDto.builder()
                                                        .title(title)
                                                        .content(content)
                                                        .author("author")
                                                        .build();
        String url="http://localhost:"+port+"/api/v1/posts";

        //when
        ResponseEntity<Long> responseEntity=restTemplate.postForEntity(url,requestDto,Long.class);

        //then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Posts> all=postsRepository.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(title);
        assertThat(all.get(0).getContent()).isEqualTo(content);
    }
}

Q. 여기서 @WebMvcTest를 쓰지않고 @SpringBootTest와 TestRestTemplate을 사용한 이유?

A. @WebMvcTest는 JPA기능이 작동되지 않기 때문이다!

업데이트/수정 기능 추가 코드 중


update에는 쿼리를 날리는 부분이 없는데 JPA의 영속성 컨텍스트 때문이다.
JPA의 영속성 컨텍스트? 엔티티를 영구 저장하는 환경
트랜잭션 안에서 DB에서 데이터 가져오면 이 데이터는 영속성 컨텍스트가 된다.
이 상태에서 해당 데이터 값 변경하면 트랜잭션 끝나는 시점에 해당 테이블에 변경분 반영. = 더티체크

🚩 JPA Auditing으로 생성시간 / 수정시간 자동화 하기

엔팉티에는 해당 데이터 생성 / 수정 시간을 포함한다.
매번 DB 삽입하기 전 갱신하는 코드 작성하기 귀찮으니 JPA Auditing을 사용하자! (LocalDate 사용)

domain/BaseTimeEntity

모든 Entity의 상위 클래스가 되어 Entity들의 생성 수정 날짜 관리

@MappedSuperclass

  • JPA Entity 클래스들이 BaseTimeEntity을 상속할 경우 필드들도 칼럼으로 인식하도록

@EntityListeners(AuditingEntityListener.class)

  • BaseTimeEntity 클래스에 Auditing 기능 포함

@CreatedDate

  • Entity가 생성되어 저장될 때 시간 자동 저장

@LastModifiedDate

  • 값을 변경할 때 시간 자동 저장

JPA Auditing 테스트 코드 작성

@Test
    public void BaseTimeEntity_등록(){
        //given
        LocalDateTime now= LocalDateTime.of(2020,5,20,0,0,0);
        postsRepository.save(Posts.builder()
                                .title("title")
                                .content("content")
                                .author("author")
                                .build());

        //when
        List<Posts> postsList=postsRepository.findAll();

        //then
        Posts posts=postsList.get(0);

        System.out.println(">>>>>>>>>>>> createDate="+posts.getCreatedDate()+", modifiedDate="+posts.getModifiedDate());

        assertThat(posts.getCreatedDate()).isAfter(now);
        assertThat(posts.getModifiedDate()).isAfter(now);
    }

3번째 글도 이사 완료 ^~^

profile
🚛 블로그 이사합니다 https://newwisdom.tistory.com/

0개의 댓글