본문 바로가기
Web/SpringBoot

3. SpringBoot에서 JPA로 데이터베이스 활용

by SeleniumBindingProtein 2022. 2. 27.
728x90
반응형

1. JPA 소개

    - 개발자는 객체지향적으로 프로그래밍하고, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행함.

    - 개발자는 항상 객체지향적으로 코드를 표현할 수 있으니 SQL에 종속적인 개발을 하지 않아도 됨

 

 1) Spring Data JPA

    - JPA : 인터페이스로서 자바 표준명세서이며, 인터페이스인 JPA를 사용하기 위해서는 구현체가 필요함 

                  ex) Hibernate, Eclipse, Link 등이 있음

    - Spring Data JPA : 구현체들을 보다 쉽게 사용하고자 추상화 시킨 모듈로서, 관계는 아래와 같음.

                                      (JPA <- Hibernate <- Spring Data JPA) 

                                       구현체 교체의 용이성 : Hibernate 외에 다른 구현체로 쉽게 교체하기 위함 

                                       저장소 교체의 용이성 : 관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위함  

                                                                          (Spring Data JPA, Spring Data Redis, Spring Data MongoDB 등등)

 2) 요구사항 분석

게시판 기능 회원 기능
게시글 조회 구글/네이버 로그인
게시글 등록 로그인한 사용자 글 작성 권한
게시글 수정 본인 작성 글에 대한 권한 권리
게시글 삭제  

 

2. Spring Data JPA 적용

 1) build.gradle

    - spring-boot-starter-data-jpa : 스프링 부트용 Spring Data Jpa 추상화 라이브러리이며,

                                                            스프링 부트 버젼에 맞춰 자동으로 JPA관련 라이브러리들의 버젼을 관리해줌

    - h2 : 인메모리 관계형 데이터베이스이며, 별도의 설치가 필요 없이 프로젝트 의존성만으로 관리할 수 있음.

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

 

    - 도메인 : 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역

buildscript {
    ext{   //build.gradle에서 사용하는 전역변수를 설정하겠다는 의미
        springBootVersion = '2.1.7.RELEASE' //spring-boot-gradle-plugin라는
        // 스프링부트그레이들 플러그인의 2.1.7RELEASE를 의존성으로 받겠다.
    }

    repositories {  //각종 의존성(라이브러리)들을 어떤 원격 저장소에서 받을지 정한다.
        mavenCentral()
        jcenter()
    }

    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin : 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'     //스프링부트의 의존성을 관리해주는 플러그인!!!중요!!

group 'com.book'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'
}

test {
    useJUnitPlatform()
}

 

 2) domain/posts/Posts 

    - @Entity : 테이블과 링크될 클래스임을 나타내며, 기본값으로 클래스의 카멜케이스 이름을

                        언더스코어 네이밍(_)으로 테이블 이름을 매칭함

                        JPA의 어노테이션

    - @Id : 해당 테이블의 PK 필드를 나타냄

    - @GeneratedValue : PK의 생성 규칙을 나타냄

    - @Column : 테이블의 칼럼을 나타내며, 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 칼럼이 됨

                           기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용함(사이즈나 타입 변경 등)

    - @NoArgsConstructor : 기본 생성자 자동 추가, public Posts() {} 와 같은 효과, 롬복의 어노테이션

    - @Getter : 클래스 내 모든 필드의 Getter 메소드를 자동생성, 롬복의 어노테이션

    - @Builder : 해당 클래스의 빌드 패턴 클래스를 생성하며, 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함 

    - 이 클래스는 Setter 메소드가 없고, 해당 필드의 값 변경이 필요하면 명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야 함

package com.jojoldu.book.springboot.domain.posts;

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

import javax.persistence.*;

@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.author = author;
        this.content = content;
    }
}

 

 3) domain/posts/PostsRepository

    - MyBatis 에서 DAO라 불리는 DB Layer 접근자이고, JPA에선 Repository라고 부르며, 인터페이스를 생성함 

    - JpaRepository<Entity 클래스, PK 타입>를 상속하면 기본적인 CRUD 메소드가 자동으로 생성됨 

    - @Repository를 추가할 필요 없음

    - Entity 클래스와 기본 Entity Repository는 함께 위치해야 함 

package com.jojoldu.book.springboot.domain.posts;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PostsRepository extends JpaRepository<Posts, Long> {
    //
}

 

 

3. Spring Data JPA 테스트 코드 작성

 1) domain/posts/PostsRepositoryTest

    - @After : junit에서 단위 테스트가 끝날때마다 수행되는 메소드를 지정.

                      보통은 배포 전 전체 테스트를 수행할 때 테스트 간 데이터 침범을 막기 위해 사용함

                      여러 테스트가 동시에 수행되면 테스트용 데이터베이스인 H2에 데이터가 그대로 남아 있어,

                      다음 테스트 실행 시 테스트가 실패할 수 있음

    - @postsRepository.save : 테이블 posts에 insert/update 쿼리를 실행함

                                                  id값이 있다면, update가 없다면 insert 쿼리가 실행됨

    - @posts : 테이블 posts에 있는 모든 데이터를 조회해오는 메소드

//PostsRepositoryTest.java

package com.jojoldu.book.springboot.domain.posts;

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("akfkdhwk@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);
    }
}

[PostsRepositoryTest 테스트 실행 및 결과]

    - 실제로 실행된 쿼리 형태 확인을 위해 src/main/resources/application.properties 파일 생성함

       - spring.jpa.show_sql=true (옵션 추가)

[쿼리 로그 확인]

       - spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect (옵션 추가)

[MySQL 쿼리 로그 확인]

 

4. 등록/수정/조회 API 생성

    - API를 만들기 위해서는 총 3개의 클래스가 필요함 

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

    - Spring 웹 계층 : 비즈니스 로직 처리(Domain)

    - Web Layer 

       => 흔히 사용하는 컨트롤러(@Controller)와 JSP/Freemarker 등의 뷰 템플릿 영역

       => 이외에도 필터(@Filter), 인터셉터, 컨트롤러 어드바이스 등 외부 요청과 응답에 대한 전반적인 영역을 말함

    - Service Layer 

       => @Service에 사용되는 서비스 영역이고, 일반적으로 Controller와 Dao의 중간 영역에서 사용됨

       => @Transaction이 사용되어야 하는 영역이기도 함

    - Repository Layer 

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

    - Dtos

       => Dto(Data Transfer Object)는 계층 간에 데이터 교환을 위한 객체를 말하며, Dtos는 이들의 영역을 얘기함

       => 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등을 말함 

    - Domain Model

       => 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고, 공유할 수 있도록 단순화 시킨 것을 도메인 모델

       => 택시 앱이 있다면, 배차, 탑승, 요금 등이 모두 도메인이 됨

       => @Entity가 사용된 영역 역시 도메인 모델

       => 다만, 무조건 데이터베이스의 테이블과 관계가 있어야만 하는 것은 아님

       => VO처럼 값 객체들도 이 영역에 해당하기 때문

 

 1) 등록

    - web/PostsApiController

package com.jojoldu.book.springboot.web;

import com.jojoldu.book.springboot.service.PostsService;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class PostsApiController {

    private final PostsService postsService;

	//등록
    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto){
        return postsService.save(requestDto);
    }
}

    - service/PostsService

       => @RequireArgsConstructor : final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성해줌 (@Autowired 사용안함)

       => 생성자를 직접 안쓰고 롬복 어노테이션을 사용하는 이유는

             해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속 수정하는 번거로움을 해결하기 위함

package com.jojoldu.book.springboot.service;

import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class PostsService {

    private final PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestDto requestDto){
        return postsRepository.save(requestDto.toEntity()).getId();
    }
}

    - web/dto/PostsSaveRequestDto

       => Entity 클래스를 Request/Response 클래스로 사용해서는 안됨 

       => Entity 클래스는 데이터베이스와 맞닿은 핵심 클래스로, Entity 클래스를 기준으로 테이블이 생성되고, 스키마가 변경됨

       => 화면 변경은 사소한 기능 변경인데, 이를 위해 테이블과 연결된 Entity 클래스 변경은 무리수임

       => View Layer와 DB Layer의 역할 분리를 해야하며,

             실제로 Controller에서 결과값으로 여러 테이블을 조인해서 줘야 할 경우가 빈번하므로

             Entity 클래스만으로 표현하기 어려운 경우가 많음

package com.jojoldu.book.springboot.web.dto;

import com.jojoldu.book.springboot.domain.posts.Posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
    private String title;
    private String content;
    private String author;

    @Builder
    public PostsSaveRequestDto(String title, String content, String author){
        this.title = title;
        this.content = content;
        this.author = author;
    }

    public Posts toEntity(){
        return Posts.builder().title(title).content(content).author(author).build();
    }
}

    - web/PostsApiControllerTest

       => @SpringBootTest와 @TestRestTemplate를 사용하여 JPA기능까지 한번에 테스트함

package com.jojoldu.book.springboot.web;

import com.jojoldu.book.springboot.domain.posts.Posts;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
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.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

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

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

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepsitory;

    @After
    public void tearDown() throws Exception{
        postsRepsitory.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 = postsRepsitory.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(title);
        assertThat(all.get(0).getContent()).isEqualTo(content);
    }
}

[Posts 등록 API 테스트 결과]

 

  2) 수정

    - web/PostsApiController

package com.jojoldu.book.springboot.web;

import com.jojoldu.book.springboot.service.PostsService;
import com.jojoldu.book.springboot.web.dto.PostsResponseDto;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
@RestController
public class PostsApiController {

    private final PostsService postsService;

    //등록
    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto){
        return postsService.save(requestDto);
    }

    //수정
    @PutMapping("/api/v1/posts/{id}")
    public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto){
        return postsService.update(id, requestDto);
    }

    @GetMapping("/api/v1/posts/{id}")
    public PostsResponseDto findById(@PathVariable Long id){
        return postsService.findById(id);
    }
}

    - web/dto/PostsResponseDto

       => PostsResponseDto는 Entity의 필드 일부만 사용하므로 생성자로 Entity를 받아 필드에 값을 넣음

       => 굳이 모든 필드를 가진 생성자가 필요하진 않으므로, Dto는 Entity를 받아 처리함

package com.jojoldu.book.springboot.web.dto;

import com.jojoldu.book.springboot.domain.posts.Posts;
import lombok.Getter;

@Getter
public class PostsResponseDto {
    private Long id;
    private String title;
    private String content;
    private String author;

    public PostsResponseDto(Posts entity){
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.content = entity.getContent();
        this.author = entity.getAuthor();
    }
}

    - web/dto/PostsUpdateRequestDto

package com.jojoldu.book.springboot.web.dto;

import com.jojoldu.book.springboot.domain.posts.Posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {

    private String title;
    private String content;

    @Builder
    public PostsUpdateRequestDto(String title, String content){
        this.title = title;
        this.content = content;
    }
}

    - web/dto/Posts

package com.jojoldu.book.springboot.domain.posts;

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

import javax.persistence.*;

@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.author = author;
        this.content = content;
    }

    //수정
    public void update(String title, String content){
        this.title = title;
        this.content = content;
    }
}

    - service/PostsService

       => update 기능에서 데이터베이스에 쿼리를 날리는 부분이 없고, 이유는 JPA의 영속성 컨텍스트 때문임

       => 영속성 컨텍스트란 엔티티를 영구 저장하는 환경임

       => JPA의 엔티티 메니저가 활성화된 상태로 트랜잭션 안에서 데이터베이스에서 데이터를 가져오면, 

             이 데이터는 영속성 컨텍스트가 유지된 상태임

       => 이 상태에서 해당 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영함

             (Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없음 = dirty checking)

package com.jojoldu.book.springboot.service;

import com.jojoldu.book.springboot.domain.posts.Posts;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.web.dto.PostsResponseDto;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class PostsService {

    private final PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestDto requestDto){
        return postsRepository.save(requestDto.toEntity()).getId();
    }

    @Transactional
    public Long update(Long id, PostsUpdateRequestDto requestDto){
        Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id ="+id));
        posts.update(requestDto.getTitle(), requestDto.getContent());
        return id;
    }

    public PostsResponseDto findById (Long id){
        Posts entity = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id="+id));
        return new PostsResponseDto(entity);
    }
}

    - web/PostsApiControllerTest

package com.jojoldu.book.springboot.web;

import com.jojoldu.book.springboot.domain.posts.Posts;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsUpdateRequestDto;
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.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

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

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

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepsitory;

    @After
    public void tearDown() throws Exception{
        postsRepsitory.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 = postsRepsitory.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(title);
        assertThat(all.get(0).getContent()).isEqualTo(content);
    }

    //수정
    @Test
    public void Posts_수정된다() throws Exception{
        //given
        Posts savedPosts = postsRepsitory.save(Posts.builder().title("title").content("content").author("author").build());

        Long updateId = savedPosts.getId();
        String expectedTitle = "title2";
        String expectedContent = "content2";

        PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder().title(expectedTitle).content(expectedContent).build();

        String url = "http://localhost:"+port+"/api/v1/posts/"+updateId;

        HttpEntity<PostsUpdateRequestDto> requestDtoHttpEntity = new HttpEntity<>(requestDto);

        //when
        ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestDtoHttpEntity, Long.class);

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

        List<Posts> all = postsRepsitory.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
        assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
    }
}

[Posts 수정 API 테스트 결과]

 

 3) 조회

    - 조회 기능은 실제 톰켓을 실행해서 확인 

    - 로컬 환경에선 데이터베이스로 H2를 사용하며, 메모리에서 실행하기 때문에 직접 접근하려면 웹 콘솔을 사용해야 함

    - application.properties 에 'spring.h2.console.enabled=true' 옵션 추가

    - Application 클래스의 main 메서드를 실행

    - http://localhost:8080/h2-console 접속 후, JDBC URL 확인 후에 Connect 버튼 클릭

       => JDBC URL : jdbc:h2:mem:testdb

[h2-console 화면]

    - 현재 프로젝트의 H2를 관리할 수 있는 관리페이지로 이동

       => POSTS 테이블이 정상적으로 노출됨 

       => SELECT * FROM posts;  쿼리문 실행하면 조회가 진행됨

[select 쿼리 실행 결과]

       => insert into posts (author, content, title) values ('author', 'content', 'title'); insert 쿼리 실행

[insert 쿼리 실행 결과]

    - 등록된 데이터를 확인한 후, API를 요청

       => 'http://localhost:8080/api/v1/posts/1' 입력해 API 조회 기능을 테스트 함

 

5. JPA Auditing으로 생성시간/수정시간 자동화    

    - 엔티티에는 해당 데이터의 생성시간과 수정시간을 포함해야 함(언제 만들어지고, 수정되었는지 등은 차후 유지보수에서 중요함)

       => JPA Auditing을 사용함

 

 1) LocalDate 사용

    - domain/posts/BaseTimeEntity 

       => 모든 Entity의 상위클래스가 되어 Entity들의 createDate, modifiedDate를 자동으로 관리하는 역할을 함

       => @MappedSuperclass : JPA Entity 클래스들이 BaseTimeEntity을 상속할 경우 필드들도 컬럼으로 인식하도록 함

       => @EntityListeners(AuditingEntityListener.class) : BaseTimeEntity 클래스에 Auditing 기능을 포함시킴

       => @CreateDate : Entity가 생성되어 저장될 때 시간이 자동 저장됨 

       => @LastModifiedDate : 조회한 Entity의 값을 변경할 때 시간이 자동 저장됨 

package com.jojoldu.book.springboot.domain;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {

    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;
}

    - Posts 클래스가 BaseTimeEntity를 상속받도록 변경함

package com.jojoldu.book.springboot.domain.posts;

import com.jojoldu.book.springboot.domain.BaseTimeEntity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter
@NoArgsConstructor
@Entity
public class Posts extends BaseTimeEntity {

    @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.author = author;
        this.content = content;
    }

    //수정
    public void update(String title, String content){
        this.title = title;
        this.content = content;
    }
}

    - Application 클래스에 활성화 어노테이션 하나 추가함 

package com.jojoldu.book.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing  //JPA Auditing 활성화 
@SpringBootApplication
public class Application {
    public static void main(String[] args){
        SpringApplication.run(Application.class, args);
    }
}

 2) JPA Auditing 테스트 코드 작성

package com.jojoldu.book.springboot.domain.posts;

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.time.LocalDateTime;
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("akfkdhwk@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);
    }

    @Test
    public void BaseTimeEntity_등록(){
        //given
        LocalDateTime now = LocalDateTime.of(2022,02,27,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));
    }
}

    - PostsRepositoryTest 클래스에 테스트 메소드 추가

[JPA Auditing 테스트 코드 결과]

728x90
반응형

댓글