본문 바로가기
Web/Spring

[2] 스프링부트로 웹 서비스 출시하기 - 2. SpringBoot & JPA로 간단 API 만들기

by 수짱수짱 2022. 8. 1.
 
 

출처: https://jojoldu.tistory.com/251?category=635883

spring boot와 jpa를 통해 간단한 API를 만들어보자.

 

spring boot&JPA로 진행하게 되면 집중해야할 비즈니스 로직에만 집중할 수 있다. 

(Express, Django, Rails 못지않게 생산성이 좋음!)

작성일자 2022.07.22


1. 도메인 코드 만들기

 

 

src > main > java > com > webservice 에 domain 패키지를 생성해줍니다.

또, domain 패키지 아래에 posts 패키지를 생성해줍니다.

 

그리고 posts패키지 아래에 Posts 클래스, PostsRepository 인터페이스를 생성해주세요!

그럼 위의 사진처럼 구성되게 됩니다.

 

 

test에도 위의 구성과 똑같이 패키지를 만들어 주고 작성된 코드가 잘 작동하는지 확인하기 위한 PostsRepositoryTest 클래스도 생성해줍니다.

 

package com.jojoldu.webservice.domain.posts;

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

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

@NoArgsConstructor(access = AccessLevel.PROTECTED) // Lombok 라이브러리의 어노테이션, 기본 생성자 자동 추가. access = AccessLevl.PROTECTED로 기본생성자의 접근 권한을 protected로 제한한다. = protected Posts() {}
// Entity 클래스를 프로젝트 코드상에서 기본생성자로 생성하는 것을 막되, JPA에서 Entity 클래스를 생성하는 것은 허용하기 위해 사용한다.!!

@Getter // Lombok 라이브러리의 어노테이션, 클래스내 모든 필드의 Getter 메소드 자동 생성
@Entity // 실제 DB의 테이블과 링크되는 클래스. JPA를 사용할 때 쿼리보다는 해당 Entity 클래스를 통해 수정한다.
        // 대문자가 아니라 _으로 이름을 매칭한다.
public class Posts {

    @Id // 해당 테이블의 PK 필드. 웬만하면 Entity의 PK는 Long타입 Auto_increment를 추천(MySQL 기준으론 bigint타입)
    @GeneratedValue // PK 생성규칙. 기본값은 AUTO로, MySQL의 auto_increment와 같이 자동증가하는 정수형 값. 스프링 부트 2.0에선 옵션을 추가해야만 auto_increment가 된다.
    private Long id;

    // @Column: 테이블의 컬럼을 나타내면 굳이 해당 어노테이션을 선언하지 않아도 해당 클래스의 필드는 모두 컬럼이 된다. 사용하는 이유는 기본값 외에 추가로 변경이 필요할 때 사용한다.
    @Column(length = 500, nullable = false) // 문자열의 경우 VARCHAR(255)가 기본값인데 사이즈를 500으로 늘림.
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false) // VARCHAR 말고 타입을 TEXT로 변경
    private String content;

    private String author;

    @Builder // Lombok 라이브러리의 어노테이션, 해당 클래스의 빌더패턴 클래스 생성.
    // 생성자 상단에 선언시 생성자에 포함된 필드만 빌더에 포함된다. (title, content, author)
    public Posts(String title, String content, String author){
        this.title = title;
        this.content = content;
        this.author = author;
    }
}

Posts.java

 

JPA에서 제공하는 어노테이션과 Lombok 라이브러리 사용을 위한 어노테이션이 Posts.java에 사용되었습니다.

@Entity가 클래스 명 위에 사용되면서 Posts.java는 Entity 클래스가 됩니다.

JPA를 사용하면 DB 데이터에 직접 쿼리를 날리기 보다는 이 Entity 클래스의 수정을 통해 데이터를 작업합니다.

 

JPA에서 제공하는 어노테이션
  • @Entity
    • 테이블과 링크되는 클래스임을 나타낸다.
    • 언더스코어 네이밍을 사용합니다.
      • Ex) SalesManager.java  →  sales_manager.java
  • @Id
    • 해당 테이블의 PK 필드를 나타낸다. (Primary Key, 기본키)
 

웬만하면 Entity의 PK는 Long타입의 Auto_increment를 추천한다. (MySQL 기준으로는 bigint 타입)

주민등록번호와 같은 비지니스상 유니크키나 , 여러키를 조합한 복합키로 PK를 사용할 경우 난감할 수 있다.

(1) FK를 맺을때 다른 테이블에서 복합키를 전부 갖고 있거나, 중간 테이블을 하나 더 둬야하는 상황이 발생

(2) 인덱스에 좋은 영향X

(3) 유니크한 조건이 변경된다면 PK 전체를 수정해야 할 수 있다.

 

따라서 주민등록번호, 복합키 등은 유니크키로 별도로 추가하는 것이 좋습니다.

 

 

  •  @GeneratedValue
    • PK의 생성 규칙을 나타낸다.
    • 기본값은 AUTO로 MySQL의 auto_increment와 같이 자동증가하는 정수형 값
    • Long타입 id에 적용했으므로 id가 자동증가하는 정수형 값이며 id는 기본키가 된다.
    • 스프링 부트 2.0에선 옵션을 추가해야만 auto_increment가 된다.

참고로, 스프링부트의 버전은 build.gradle 파일에서 확인이 가능합니다.

  • @Column 
    • 테이블의 컬럼을 나타내면 굳이 선언하지 않아도 해당 클래스의 필드는 모두 컬럼이 된다.
    • 그럼에도 사용하는 이유는, 기본값 외에 추가로 변경이 필요한 옵션이 있는 경우 사용한다.
    • String타입 title에 length를 500으로 변경하기 위해 사용한다. (기본값은 VARCHAR(255) )
    • String타입 content에 타입을 TEXT로 변경하기 위해 사용한다. (기본 타입은 VARCHAR)

Lombok 라이브러리 사용을 위한 어노테이션

서비스 구축단계에선 테이블 설계(Entity 설계)가 빈번하게 변경되는데, 이때 Lombok 어노테이션들은 코드 변경량을 최소화시켜주기 때문에 강력 추천하는 라이브러리이다.

Tip> Lombok은 의존성만 추가해선 IDE에서 바로 사용할수 없기 때문에 각 IDE 환경(이클립스, 인텔리제이 ...)에 맞게 Lombok 사용환경 구성이 필요하다.

Lombok 라이브러리 주의사항) https://kwonnam.pe.kr/wiki/java/lombok/pitfall

 

  •  @NoArgsConstructor
    • 기본 생성자 자동 추가
    • Entity 클래스를 프로젝트 코드 상에서 기본생성자로 생성하는 것은 막되, JPA에서 Entity 클래스를 생성하는 것은 허용하기 위해 사용한다.
    • @NoArgsConstructor(access = AccessLevel.PROTECTED)
      • 기본생성자의 접근권한을 protected로 제한
      • protected Posts() {} 와 같다.
  • @Builder
    • 해당 클래스의 빌더패턴 클래스 생성
    • 생성자 상단에 선언시 생성자에 포함된 필드만 빌더에 포함된다.
      • Ex) 생성자에 title, content, author이 포함되었으므로 이 3가지만 빌더에 포함된다.
  • @Getter
    • 클래스내 모든 필드의 Getter 메소드를 자동생성

 


Entity 클래스를 생성할 때 주의할 것은 무분별한 setter 메소드 생성이다.

 

무작정 자바빈 규약을 생각하면서 getter/setter를 생성하게 된다면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수가 없어 차후 기능 변경이 어려워진다.

 

따라서, 해당 필드의 값 변경이 필요하다면 명확하게 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야 한다.

 

Ex) 주문 취소 메소드

// 잘못된 getter, setter 사용
public class Order{
	public void setStatus(boolean status){
    	this.status = status;
    }
}

public void 주문서비스_취소메소드(){
	order.setStatus(false);
}

 

// 올바른 getter, setter 사용
public void Order{
	public void cancelOrder(){   // => setStatus(setter)가 cancelOrder로 명확한 목적과 의도를 나타내는 메소드로 변경되었다.
    	this.status = false;
    }
}

public void 주문서비스_취소메소드(){
	order.cancelOrder(); 
}

cancelOrder 메소드로 setter를 대체하였다.

=> 만약 cancelOrder 메소드가 아닌 setter 메소드로 계속 주문을 취소한다면 주문 취소 기능을 변경해야 하는 경우가 생겼을 때 setter에서 주문을 취소하는 부분을 찾아야해서 기능변경이 복잡해질 수 있다.

 

 

기본생성자의 접근지정자를 protected로 막아뒀고(@NoArgsConstructor(access=AccessLevel.PROTECTED) 어노테이션으로.. ) setter 메소드도 없는데 어떻게 값을 채워 DB에 insert 하나요?

 

기본적인 구조는 생성자를 통해 최종 값을 채운 후 DB에 insert 하는 것이며, 값 변경이 필요한 경우는 해당 이벤트에 맞는 public 메소드를 호출하여 변경하는 것을 전제로 합니다.

 

여기서 생성자 대신에 @Builder로 제공되는 빌더 클래스를 사용하여 최종 값을 채웁니다.

 

생성자와 빌더는 생성 시점에 값을 채워주므로 역할은 같습니다.

다만, 생성자의 경우 지금 채워야 할 필드가 무엇인지 명확하게 지정할 수가 없습니다.

반대로 빌더는 지금 채워야 할 필드가 무엇인지 명확하게 알 수 있습니다.

 

에를 들어보겠습니다.

public Example(String a, String b){
	this.a = a;
	this.b = b;
}

위와 같은 생성자가 있는 경우입니다.

만약, 개발자가 new Example(b,a) 처럼 a와 b의 위치를 변경해도 실제로 코드를 시행하기 전까지는 전혀 문제를 찾을 수 없습니다.

 

 

하지만 빌더를 사용하게 된다면 아래와 같이 어느 필드에 어떤 값을 채워야 할지 명확하게 알 수 있습니다.

Example.builder()
	.a(a)
	.b(b)
	.build();
빌더 패턴에 대한 소개와 예제: https://using.tistory.com/71

 


 

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

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

PostsRepository.java

 

위에서 설명한 부분을 코드로 작성한 부분입니다.

 

보통 ibatis/MyBatis 등에서 Dao라고 불리는 DB Layer 접근자입니다.

JPA에선 Repository 라고 부르며 인터페이스로 생성합니다.

단순히 인터페이스를 생성 후, JpaRepository<Entity클래스, PK타입>상속하면 기본적인 CRUD 메소드가 자동생성 됩니다. 특별히 @Repository를 추가 할 필요도 없습니다.


2. 테스트 코드 작성하기

 

위에서 작성한 코드가 잘 작동하는지 확인하기 위해 테스트 코드를 작성해보도록 하겠습니다.

테스트 코드를 작성하기 위해 해당 위치에 PostsRepositoryTest.java를 생성해줍니다.

 


junit을 통해 테스트 코드를 작성해야 하는데 저는 인텔리제이에 Junit이 설정되어 있지 않기 때문에 Junit부터 설정해주겠습니다. Junit 설정은 Gradle을 통해 설정해줍니다.

 

Junit은 자바 프로그래밍 언어용 단위 테스트 프레임워크이다.

Junit은 테스트 주도 개발면(TDD)에서 매우 중요하며 SUnit과 함께 시작된 XUnit이라는 이름의 단위 테스트 프레임워크 계열의 하나이다.

Junit Platform은 JVM 테스트 프레임워크를 실행할 수 있도록 하며 Junit Jupiter는 테스트를 하기 위한 것들을 포함한다.
또한, JUnit Vintage는 Junit3 or JUnit4 기반 테스트를 실행할 수 있도록 도와준다.

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

 

JUnit5 버전을 InteliJ에서 사용하기 위해 build.gradle을 열어줍니다.

(참고: https://docs.gradle.org/current/userguide/java_testing.html#using_junit5

+ https://itbellstone.tistory.com/106)

 

test{
   useJUnitPlatform()
}

buidl.gradle에  JUnit Plaform 사용으로 테스트를 실행할 수 있도록 추가해줍니다.

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.1.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.1.0'

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.1.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.1.0'
testCompileOnly 'junit:junit:4.12'
testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.1.0'

 

그리고 dependencies(의존성) 부분에 위의 내용을 추가해줍니다. 

1~2번째 줄은 JUnit5을 사용하기 위한 의존성 부분입니다.

3~6번째 줄은 JUnit5 이전 버전을 사용하거나 함께 사용하기 위한 의존성 부분입니다.!

 

위의 내용을 전부 입력해준 다음 gradle sync을 실행해줍시다.


다시 본론으로 돌아와 테스트 코드를 작성해보도록 하겠습니다.

package com.jojoldu.webservice.domain;

import com.jojoldu.webservice.domain.posts.Posts;
import com.jojoldu.webservice.domain.posts.PostsRepository;
import org.aspectj.lang.annotation.After;
import org.junit.jupiter.api.AfterEach;
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.context.junit4.SpringRunner;


import java.util.List;


import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

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

    @Autowired
    PostsRepository postsRepository;

    @AfterEach   // Junit 5에서는 헷갈림 방지를 위해 @After가 @AfterEach로 변경되었다.
    public void cleanup(){
        // 이후 테스트 코드에 영향을 미치지 않기 위해 테스트 메소드가 끝날 때 마다 repository를 비우는 코드
        postsRepository.deleteAll();
    }

    @Test
    public void 게시글저장_불러오기(){
        // given : 테스트 기반 환경을 구축, @Builder 사용법도 같이 확인
        postsRepository.save(Posts.builder()    // 생성자 대신 빌더 사용
                .title("테스트 게시글")
                .content("테스트 본문")
                .author("jojoldu@gamil.com")
                .build());

        // when : 테스트 하고자 하는 행위 선언, Posts가 DB에 insert 되는 것을 확인하기 위함
        List<Posts> postsList = postsRepository.findAll();

        // then : 테스트 결과 검증, 실제로 DB에 insert 되었는지 확인하기 위해 조회 후, 입력된 값 확인
        Posts posts = postsList.get(0);
        assertThat(posts.getTitle(),is("테스트 게시글"));
        assertThat(posts.getContent(), is("테스트 본문"));

    }

}

PostsRepositoryTest.java

 

해당 테스트 코드를 실행시키고 결과가 위와 같이 출력된다면 테스트를 성공적으로 통과했음을 알 수 있습니다!

 

 

테스트 코드를 작성할 때는 given, when, then 순서로 작성해주는 것이 좋습니다!

BDD(Behavior-Driven Development) 에서 사용하는 용어인 given, when, then

JUnit에선 이를 명시적으로 지원해주지 않아 주석으로 표현한다.
전문 BDD 프레임워크로는 Groovy 기반의 spock을 많이 사용하고 있다.
(spock 참고: https://jojoldu.tistory.com/228)

 

  • given
    • 테스트 기반 환경을 구축
    • @builder 사용법도 같이 확인
  • when
    • 테스트 하고자 하는 행위 선언
  • then
    • 테스트 결과 검증

 

 

Q. DB가 설치되어 있지 않은데 Repository를 사용할 수 있는 이유?

A> Spring boot의 테스트 코드는 메모리 DB인 H2를 기본적으로 사용하기 때문이다.

테스트 코드를 실행하는 시점에 H2 DB를 실행시키고 테스트가 끝나면 H2 DB도 같이 종료된다.

 

그럼 눈에 보이지 않는 DB에 어떻게 데이터가 들어갔는지 확인할 수 있는지 궁금하지 않나요?


3. Controller & DTO 구현

package com.jojoldu.webservice.web;

import com.jojoldu.webservice.domain.posts.PostsRepository;
import com.jojoldu.webservice.dto.PostsSaveRequestDto;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@AllArgsConstructor // 추가, 모든 필드를 인자값으로 하는 생성자를 Lombok 라이브러리의 @AllArgsConstructor 어노테이션이 대신 생성해준다.
                    // 생성자를 직접 안 쓰고 Lombok 어노테이션을 사용한 이유는 해당 클래스의 의존성 관게가 변경될 때 마다 생성자 코드를 계속해서 수정해야 하는 번거로움을 해결하기 위함.

public class WebRestController {

    private PostsRepository postsRepository;  // 추가

    @GetMapping("/hello")
    public String hello() {
        return "HelloWorld";
    }

    @PostMapping("/posts")
    public void savePosts(@RequestBody PostsSaveRequestDto dto){
        postsRepository.save(dto.toEntity());
    }
}

WebRestController.java를 위와 같이 수정합니다.

 

postsRepository 필드에 @Autowired가 없습니다.

스프링 프레임 워크에서 Bean을 주입받는 방식은 아래 3가지가 있습니다.

 

  1. @Autowired
  2. setter
  3. 생성자

이중 3번째인 생성자로 주입받는 방식이 가장 권장하는 방식이며 Autowired는 비권장방식입니다.

즉, 생성자로 Bean 객체를 받도록 하면 @Autowired와 동일한 효과를 볼 수 있습니다.

 

생성자로 주입받기 위해 @AllArgsConstructor 어노테이션을 사용합니다.

모든 필드를 인자값으로 하는 생성자를 Lombok의 @AllArgsConstructor 어노테이션이 만들어줍니다.

	private PostsRepository postsRepository;
	
	public WebRestController(PostsRepository postsRepository) {
		this.postsRepository = postsRepository;
	}

실제론 이러한 코드의 형태인 것이죠.

생성자를 직접 쓰지 않고 Lombok 어노테이션을 사용한 이유는 해당 클래스의 의존성 관계가 변경될 때 마다 생성자 코드를 계속해서 수정해야 하는 번거로움을 해결하기 위함입니다.

* Lombok 어노테이션을 사용한다면 해당 컨트롤러에 새로운 서비스를 추가하거나 기존 컴포넌트를 제거하는 등이 발생해도 생성자 코드는 전혀 손대지 않아도 됩니다.

 

 

webservice 폴더 아래 dto 패키지를 생성해주고 dto 패키지에 PostsSaveRequestDto.java를 생성해줍니다.

package com.jojoldu.webservice.dto;

import com.jojoldu.webservice.domain.posts.Posts;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class PostsSaveRequestDto {

    private String title;
    private String content;
    private String author;

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

}

PostsSaveRequestDto.java

 

여기서는 @Setter를 사용한 이유가 뭘까요?

컨트롤러에서 @RequestBody로 외부에서 데이터를 받는 경우에는 기본생성자 + set메소드를 통해서만 값이 할당됩니다.

그래서 이때만 setter를 허용합니다.

 

 

Entity 클래스와 거의 유사한 형태임에도 DTO 클래스를 추가로 생성한 이유는

절대로 테이블과 매핑되는 Entity 클래스를 Request / Response 클래스로 사용해서는 안되기 때문이다.

 

Entity 클래스는 가장 Core한 클래스로 봐야한다. 수많은 서비스 클래스나 비지니스 로직들이 Entity 클래스를 기준으로 동작한다.

 

즉, Entity 클래스가 변경되면 여러 클래스에 영향을 끼치게 되어 변경이 잦으면 안되지만

반대로, Request와 Response용 DTO는 View를 위한 클래스이므로 변경이 아주 잦다.

 

그러므로 View Layer와 DB Layer를 철저하게 역할 분리하는 것이 좋다.

 

실제로 컨트롤러에서 결과값으로 여러 테이블을 조인해서 줘야할 경우가 빈번하기에 Entity 클래스만으로 표현하기 어려운 경우가 많다.

 

※ 꼭 Entity 클래스와 컨트롤러에서 쓸 DTO는 분리해서 사용하도록 해라. ※

 

4. Postsman + 웹 콘솔로 검증

 

아직 별도로 입력화면이 없기때문에 Postsman을 통해 Post로 데이터를 전송하여 검증해보도록 하겠습니다.

 

 

 

Local 환경에선 DB로 H2를 사용하기 때문에 스프링 부트에서 H2를 활성화 시키도록 옵션을 추가하겠습니다.

 

이를위해 application.propertites 파일을 application.yml 파일로 변경시켜주어야 합니다.

(참고: https://gocoder.tistory.com/2493)

저는 Rename을 통해 확장자를 yml로 바꾸어 주는 방법을 사용했습니다.

spring:
    h2:
        console:
            enabled: true

yml로 바꾼 해당 파일에 위와 같이 입력해주면 스프링 부트에서 H2를 활성화 시킬 수 있게 됩니다.

 

▣ yml로 바꾼 이유는 properties에 비해 상대적으로 유연한 구조를 가졌기 때문입니다.
yml은 상위 계층에 대한 표현, List등을 완전하게 표현할 수 있습니다.
최근의 많은 도구들이 yml 설정을 지원합니다

 

yml 작성까지 끝내고 나면 다시 Application.java를 실행시키고 브라우저를 켜줍니다.

 

 

브라우저 주소창에 localhost:8080/h2-console을 입력하여 H2 DB를 관리할 수 있는 웹 클라이언트에 접속할 수 있습니다. connect 버튼을 클릭하여 접속해줍니다!


이런 connect 버튼을 클릭하니깐 이런 오류가 뜨네요.. 해결해줍시다. 

 

참고하고 있는 블로그의 댓글에 천사분이 남겨주신 댓글로 해결했습니다!

사랑합니당!


 

해당 조회 쿼리를 입력해준 결과로 데이터가 하나도 없는 것이 확인 되었습니다.

이제 포스트맨을 통해 데이터를 전송해보도록 하겠습니다.

메소드는 POST로! 주소는 로컬호스트의 posts 메소드에 매핑되도록!

Body로 전달하고 JSON 형태로 전달!

해당 본문과 같이 입력해주고 난 이후로 Send해주면 성공!

 

 

다시 조회해보면 포스트맨으로 전달한 데이터가 조회되는 것을 확인할 수 있습니다!

재밌쬬 *ㅅ* ?

 

포스트맨을 대체하여 .http를 사용하자 !
https://jojoldu.tistory.com/266

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

 

보통 Entity에는 해당 데이터의 생성시간, 수정시간을 포함시킵니다.

언제 만들어졌는지 언제 수정되었는지 등은 차후 유지보수에 있어 굉장히 중요한 정보이기 때문입니다.

그렇다보니 매번 DB에 insert하기 전, update 하기 전 날짜 데이터를 등록./수정 하는 코드가 들어가게 됩니다.

// 생성일 추가 코드 예제
public void savePosts(){
    ...
    posts.setCreateDate(new LocalDate());
    postsRepository.save(posts);
    ...
}

이런 단순하고 반복적인 코드가 모든 테이블과 서비스 메소드에 포함된다면 귀찮고 코드가 더러워집니다.

 

이를 해결하기 위해 JPA Auditing을 사용합니다!

 

 

LocalData 사용

 

자바의 날짜 타입을 사용합니다.

Java8부터 LocalData와 LocalDataTime이 등장했고 이는 자바의 기본 날짜 타입인 Data의 문제점을 고친 타입이므로 Java8 이상일 경우 무조건 LocalData, LocalDataTime을 사용하도록 합니다.

 

LocalData, LocalDatTIme이 DB에 저장 시 제대로 전환이 안되는 이슈를 Hibernate core 5.2.10부터는 해결되므로 이를 적용 시켜줍니다. => 현재 작성일자를 기준으로는 딱히 설정해주지 않아도 5.2.10 버전 이상입니다!

 

src > main > java > com > 패키지이름 > webservice > domain에 BaseTimeEntity.java를 생성해줍니다.

 

package com.jojoldu.webservice.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.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@MappedSuperclass  // JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우 필드들(createdDate, modifiedDate)도 컬럼으로 인식하도록 한다.
@EntityListeners(AuditingEntityListener.class) // BaseTimeEntity 클래스에 Auditing 기능을 포함시킨다.
public class BaseTimeEntity {

    @CreatedDate  // Entity가 생성되어 저장될 때 시간이 자동 저장된다.
    private LocalDateTime createdDate;

    @LastModifiedDate // 조회한 Entity의 값을 변경할 때 시간이 자동 저장된다.
    private LocalDateTime modifiedDate;
}

BaseTimeEntity.java

 

해당 클래스는 모든 Entity들의 상위 클래스가되어 Entity들의 createdDate, modifiedDate를 자동으로 관리합니다.

 

  • @MappedSuperclass
    • JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우 필드(createdDate, modifiedDate)들을 컬럼으로 인식하도록 함.
  • @EntityListeners(AuditingEntityListener.class)
    • BaseTimeEntity 클래스에 Auditing 기능을 포함시킴.
  • @CreatedDate
    • Entity가 생성되어 저장될 때 시간이 자동 저장됨.
  • @LastModifiedDate
    • 조회한 Entity의 값을 변경할 때 시간이 자동 저장됨.

 

이렇게 만든 BaseTimeEntity를 사용하기 위해서는 다른 Entity 클래스들이 BaseTimeEntity를 상속받도록 해줍니다!

이렇게 Posts Entity 클래스에 BaseTimeEntity를 상속받도록 한다.

 

위와 같이 JPA Auditing 어노테이션들을 모두 활성화 시키기 위해 Application 클래스에 활성화 어노테이션(@EnableJpaAuditing)을 추가해줍니다!

 

 

JPA Auditing 테스트 코드 작성하기

 

@Test
public void BaseTimeEntity_등록() {
    // given
    LocalDateTime now = LocalDateTime.now();
    postsRepository.save(Posts.builder()
                    .title("테스트 게시글")
                    .content("테스트 본문")
                    .author("jojoldu@gamil.com")
                    .build());

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

    // then
    Posts posts = postsList.get(0);
    assertTrue(posts.getCreatedDate().isAfter(now));
    assertTrue(posts.getModifiedDate().isAfter(now));
}

PostsRepositoryTest 클래스에 해당 테스트 메소드를 추가시켜 줍니다.

해당 코드는 Posts를 저장한 뒤, 해당 Posts에 createdDate와 modifiedDate가 있는지 확인하는 코드입니다.

 

해당 테스트 코드가 성공적으로 실행되었으니 실제로 포스트맨으로 데이터를 입력했을 때도 날짜가 잘 입력되는지 확인해봅시다!

 

그전에 Application을 다시 실행시켜주는 거 잊지마세요 !!

 

포스트맨으로 데이터를 입력해주고 조회쿼리를 실행시켜주니 createdDate와 modifiedDate가 성공적으로 DB에 isnert 된 것을 확인할 수 있습니다 !! 왕 ~

 

앞으로 추가될 Entity 클래스들은 더이상 등록일/수정일로 고민 할 필요가 없습니다!

BaseTimeEntity만 상속받으면 되기 때문이에요!!