Spring

[스프링 부트와 AWS로 혼자 구현하는 웹 서비스] 3. 프로젝트에 Spring Data JPA 적용하기

주니어주니 2023. 8. 11. 19:00

 

요구사항 분석

* 게시판 기능
- 게시글 조회, 등록, 수정, 삭제 

* 회원 기능
- 구글/네이버 로그인
- 로그인한 사용자 글 작성 권한
- 본인 작성 글에 대한 권한 관리 

 

 

1. 의존성 등록

 

* build.gradle

// spring data JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// h2 데이터베이스
runtimeOnly 'com.h2database:h2'
  • spring-boot-starter-data-jpa
    • 스프링 부트용 Spring Data Jpa 추상화 라이브러리 
    • 스프링 부트 버전에 맞춰 자동으로 JPA 관련 라이브러리들의 버전 관리해줌

  • h2
    • 인메모리 관계형 데이터베이스
    • 별도의 설치 X, 프로젝트 의존성만으로 관리
    • 메모리에서 실행 -> 애플리케이션을 재시작할 때마다 초기화 -> 테스트용으로 많이 사용함

 

 

2. 도메인

 

* Posts

package com.example.studyspringbootwebservice.domain.posts;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@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;
    }
}

 

(1) JPA 어노테이션

  • @Entity
    • 테이블과 링크될 클래스
    • 클래스의 카멜케이스 이름 -> 언더스코어 이름으로 테이블 매칭

  • @Id
    • PK 필드

  • @GeneratedValue(strategy = GenerationType.IDENTITY) 
    • PK 생성 규칙 - 데이터베이스에서 생성해줌 (auto-increment)
    • 웬만하면 Auto-increment 추천

  • @Column
    • 선언하지 않아도 해당 클래스의 필드를 테이블의 컬럼과 매핑해주지만, 추가로 변경이 필요한 옵션이 있을 때 사용
    • ex) 문자열의 기본 길이 VARCHAR(255)를 500으로 늘리거나,
      타입을 TEXT로 변경하고 싶을 때 등등

 

 

(2) 롬복 어노테이션 

  • @NoArgsConstructor
    • 기본 생성자 자동 추가 -> JPA는 기본 생성자 필수 ! 

  • @Getter

  • @Builder
    • 해당 클래스의 빌더 패턴 클래스 생성
    • 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함
    • 생성자 대신 @Builder를 사용한 이유
      : 생성자는 new Posts(a, b, c)에서 b, c, a 등으로 위치를 변경해도 문제를 찾을 수 X
      -> 빌더어느 필드에 어떤 값을 채워야 할지 명확하게 인지 O 

 

 

🚨 Entity 클래스에서는 절대 @Setter 메소드를 만들지 않음 !!!

 

* setter 사용시 

  • 그냥 아무때나 set으로 값 변경  -> 언제 어디서 왜 setting 하는지 명확하게 알 수 X 
public class Order {
	public void setStatus(boolean status) {
    	this.status = status; 
    }
}

public void 주문서비스의_취소이벤트() {
	order.setStatus(false);
}

 

 

* 올바른 사용

  • 값 변경의 목적과 의도가 명확함
  • 생성자 / 빌더 사용해서 값 채움  
public class Order {
	public void cancelOrder() {
    	this.status = false; 
    }
}

public void 주문서비스의_취소이벤트() {
	order.cancelOrder();
}

 

 

 

3. 리포지토리 - DB 접근 

 

package com.example.studyspringbootwebservice.domain.posts;

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

public interface PostsRepository extends JpaRepository<Posts, Long> {
}
  • 인터페이스 !! 
  • @Repository 필요 X
  • 💡 Entity 클래스기본 Entity Repository는 함께 위치해야 함
    • 도메인 별로 프로젝트를 분리해야 할 때, Entity와 Repository가 함께 움직여야 하므로 도메인 패키지에서 함께 관리

 

 

4. 테스트 

package com.example.studyspringbootwebservice.domain.posts;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.List;

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

@ExtendWith(SpringExtension.class)
@SpringBootTest
public class PostsRepositoryTest {

    @Autowired
    PostsRepository postsRepository;

    @AfterEach
    public void cleanup() {
        postsRepository.deleteAll();
    }

    @Test
    public void 게시글저장_불러오기() {
        //given
        String title = "테스트 게시글";
        String content = "테스트 본문";

        postsRepository.save(Posts.builder()
                .title(title)
                .content(content)
                .author("wnnns4@naver.com")
                .build());

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

        //then
        Posts posts = postsList.get(0);
        assertThat(posts.getTitle()).isEqualTo(title);
        assertThat(posts.getContent()).isEqualTo(content);
    }
}
  • @ExtendWith(SpringExtension.class)
    • @Autowired, @MockBean에 해당하는 것들만 application context를 로딩

  • @SpringBootTest
    • H2 데이터베이스 자동으로 실행

  • @AfterEach
    • @After (Junit4) -> @AfterEach (Junit5)
    • 단위 테스트가 끝날때마다 수행

 

* 쿼리 로그 확인 application.properties

spring.jpa.show-sql=true

 

 

 

5. 등록/수정/조회 API 만들기 

 

 

💡 Spring 웹 계층

- Web Layer
컨트롤러(@Controller)와 JSP 등의 뷰 템플릿 영역
필터(@Filter), 인터셉터, 컨트롤러 어드바이스(@Controller Advice) 등 외부 요청과 응답에 대한 전반적인 영역

- Service Layer 
@Service에 사용되는 서비스 영역 
일반적으로 Controller와 Dao의 중간 영역에서 사용
@Transactional이 사용되어야 하는 영역

- Repository Layer
Database와 같이 데이터 저장소에 접근하는 영역
Dao 영역

- Dto
계층 간에 데이터 교환을 위한 객체들의 영역

- Domain
도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화시킨 것
@Entity가 사용된 영역
VO처럼 값 객체들도 해당


* Domain : 비즈니스 처리 담당 !  
* Service : 트랜잭션, 도메인 간 순서 보장 ! (비즈니스 처리 X)

@Transactional 
public Order cancelOrder(int orderId) {

    // 1) DB에서 주문정보, 결제정보, 배송정보 조회
    Orders order = ordersRepository.findById(orderId);
    Billing billing = billingRepository.findByOrderId(orderId);
    Delivery delivery = deliveryRepository.findByOrderId(orderId);
    
    // 2) 배송 취소를 해야하는지 확인
    // 3) if (배송중) -> 배송취소로 변경
    delivery.cancel();
    
    // 4) 각 테이블에 취소상태 update
    order.cancle();
    billing.cancle(); 
    
    return order;
    
}​

 

 

💡 스프링에서 Bean 주입받는 방식 

1. @Autowired (권장 X)
2. setter
3. 생성자 (권장) : @ReuiredArgsConstructor + private final ~~ 
    -> 생성자를 직접 쓰지 않고 롬복 사용 : 의존성 관계가 변경될 때마다 생성자 코드를 수정하지 않아도 됨

 

 

1) 등록 

 

(1) 컨트롤러

package com.example.studyspringbootwebservice.web;

import com.example.studyspringbootwebservice.service.posts.PostsService;
import com.example.studyspringbootwebservice.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);
    }
}
  • @RequiredArgsConstructor + final
    • PostsService를 의존성 주입받음

  • @RestController
    • 리턴값에 자동으로 @ResponseBody가 붙음
    • http 응답 body에 자바 객체가 매핑되어 전달
    • @Controller + @ResponseBody 
    • -> REST API 적용 (@RestController + @PostMapping (HTTP 메소드) + @RequestBody, @ResponseBody (JSON 데이터 처리 -> Long 타입의 ID를 반환) ) 

  • @RequestBody 
    • 일반적인 GET, POST 말고, xml, json 기반의 데이터를 요청 body에 담아서 서버로 보냄
    • http 요청의 body 전부를 PostsSaveRequestDto 자바객체로 변환해서 매핑된 메소드 파라미터로 전달, 객체에 저장

  • @ResponseBody
    • 자바 객체json 형태로 반환하여 http 응답 body에 담아 클라이언트로 전송
    • 자바 객체만 리턴하도록 짜면 됨
    • @RestController를 사용하면 생략

 

 

(2) 서비스 

package com.example.studyspringbootwebservice.service.posts;

import com.example.studyspringbootwebservice.domain.posts.PostsRepository;
import com.example.studyspringbootwebservice.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();
    }
}

 

 

(3) Dto 

package com.example.studyspringbootwebservice.web.dto;

import com.example.studyspringbootwebservice.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();
    }

}

 

 

(4) 테스트 

package com.example.studyspringbootwebservice.web;

import com.example.studyspringbootwebservice.domain.posts.Posts;
import com.example.studyspringbootwebservice.domain.posts.PostsRepository;
import com.example.studyspringbootwebservice.web.dto.PostsSaveRequestDto;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.List;

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

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class PostsApiControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @AfterEach
    public void tearDown() {
        postsRepository.deleteAll();
    }

    @Test
    public void Posts_등록() {
        //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
        // post 요청 - postForEntity(url, post 요청 바디로 보낼 객체, 서버 응답을 변환할 타입)
        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);
    }

}
  • @SpringBootTest 
    • @WebMvcTest  -> JPA 기능 작동 X
    • JPA 기능까지 한번에 테스트할 때는 @SpringBootTest, TestRestTemplate 사용

  • @LocalServerPort
    • 스프링 부트 테스트에서 사용
    • 테스트 중인 웹 서버의 포트 번호 주입받을 때 사용 (임의의 포트 번호로 웹 서버 테스트 실행)

  • TestRestTemplate 
    • 스프링 프레임워크의 테스트 환경에서 사용하는 HTTP 클라이언트 라이브러리
    • 실제 서버를 실행하지 않고, 컨트롤러 엔드포인트 호출, 응답 

 

랜덤포트 실행, insert 쿼리 실행

 

 

 

2) 수정

 

(1) 컨트롤러 

@RequiredArgsConstructor
@RestController
public class PostsApiController {

    private final PostsService postsService;

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

 

 

(2) 서비스 

@RequiredArgsConstructor
@Service
public class PostsService {

    private final PostsRepository postsRepository;

    // 수정
    @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;
    }
  • postsRepository의 save(), findById() 와 달리 update쿼리를 날리는 부분이 없음 !! -> JPA의 영속성 컨텍스트 때문
    • 영속성 컨텍스트 : 엔티티를 영구 저장하는 환경
    • 트랜잭션이 적용되고 있는 상태에서 엔티티 값 변경 -> 트랜잭션 커밋/롤백 시점에 자동으로 DB에 변경내용 반영
    • -> 엔티티 객체의 값만 변경하면 -> 별도로 update 쿼리 날릴 필요 X (더티 체킹)

 

 

* Posts 객체에 update() 메소드 추가 - 엔티티 객체의 값 변경

@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;
    }

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

 

 

(3) 수정 Dto 

package com.example.studyspringbootwebservice.web.dto;

import com.example.studyspringbootwebservice.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;
    }

}

 

 

(4) 테스트

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class PostsApiControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @AfterEach
    public void tearDown() {
        postsRepository.deleteAll();
    }

    @Test
    public void Posts_수정() {
        //given
        // Posts 생성해서 DB에 저장
        Posts savedPosts = postsRepository.save(Posts.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());

        Long updateId = savedPosts.getId();
        // 변경할 내용
        String expectedTitle = "title2";
        String expectedContent = "content2";

        // 변경할 내용을 담은 PostsUpdateRequestDto 생성
        PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder()
                .title(expectedTitle)
                .content(expectedContent)
                .build();

        // 테스트할 API의 엔드포인트
        String url = "http://localhost:" + port + "/api/v1/posts/" + updateId;

        // http 요청을 위한 httpEntity 객체 생성 (http 요청 바디에 requestDto 담아서 보냄)
        HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);

        //when
        // exchange - put보다 유연함 (HttpEntity 포함)
        ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, 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(expectedTitle);
        assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
    }

}
  • HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto)
    • HttpEntity : HTTP 요청이나 응답 메시지를 나타내는 클래스
    • HttpEntity 객체를 생성하는데, PostsUpdateRequestDto 타입의 requestDto를 HTTP 요청의 바디에 담아서 보냄
    • 요청의 헤더와 바디를 커스터마이징할 수 있음
    • 바디에 다양한 데이터 타입을 담을 수 있음 (JSON, XML, 문자열 등)

  • TestRestTemplate.exchange(url, HttpMethod.PUT, requestEntity, Long.class)
    • put() 도 있지만, HttpEntity를 포함하는 등 더 유연하게 설정하기 위해 exchange() 사용
    • HttpMethod.PUT : HTTP 메소드를 명시적으로 지정 
    • requestEntity : HttpEntity 객체를 사용해서 요청의 헤더와 바디를 설정
    • Long.class : 응답 데이터 타입 지정

 

 

 

 

3) 조회 

 

(1) 컨트롤러 

@RequiredArgsConstructor
@RestController
public class PostsApiController {

    private final PostsService postsService;

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

** 응답객체 PostsResponseDto 반환 !! 

 

 

(2) 서비스 

@RequiredArgsConstructor
@Service
public class PostsService {

    private final PostsRepository postsRepository;

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

        return new PostsResponseDto(entity);
    }
}
  • DB에서 id에 해당하는 Posts 객체를 찾아서, PostsResponseDto에 그 값을 넣어서 반환

 

 

(3) 응답객체 Dto 

package com.example.studyspringbootwebservice.web.dto;

import com.example.studyspringbootwebservice.domain.posts.Posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
public class PostsResponseDto {

    private Long id;
    private String title;
    private String content;
    private String author;

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

}

 

 

(4) 테스트

 

h2 데이터베이스와 톰캣 사용해서 실행

 

① application.properties 

# h2 console 활성화
spring.h2.console.enabled=true
# 원격접속 허용
spring.h2.console.settings.web-allow-others=true
# h2 console 경로
spring.h2.console.path=/h2-console
# h2 url 경로
spring.datasource.url=jdbc:h2:mem:testdb
# class 명칭
spring.datasource.driverClassName=org.h2.Driver

 

 

② localhost:8080/h2-console 접속 

JDBC URL: jdbc:h2:mem:testdb

 

③ 쿼리 실행

insert into posts(author, content, title) values('author', 'content', 'title')

 

④ api 요청

 

 

==> 객체지향적인 코드 !!!

 

 

 

 

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

 

엔티티들이 공통적으로 꼭 가져야 하는 필드 -> 생성시간, 수정시간

(공통 -> 중복이라는 뜻 ! ) 

 

원래는 이런식으로 각 객체마다 다 일일이 생성/수정 시간 세팅해줬음

posts.setCreatedDate(new LocalDate());
posts.setUpdatedDate(new LocalDate());

 

-> JPA Auditing 사용 

 

💡 LocalDate / LocalDateTime 

- Java8 부터 등장 
- 원래 쓰던 Date, Calendar 의 문제점 : 불변객체가 아님 + Calendar는 월이 안맞음 (10월을 '9'로 표시)

 

 

1) BaseTimeEntity 추상클래스 

package com.example.studyspringbootwebservice.domain;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter
@MappedSuperclass                               // JPA Entity 클래스들이 상속할 경우 이 필드들도 테이블 컬럼으로 인식하도록 함
@EntityListeners(AuditingEntityListener.class)  // BaseTimeEntity 클래스에 Auditing 기능 포함
public abstract class BaseTimeEntity {

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

    @LastModifiedDate                           // 조회한 Entity의 값을 변경할 때 시간 자동 저장
    private LocalDateTime modifiedDate;
}
  • 모든 엔티티의 상위 클래스가 되어 엔티티들의 createdDate, modifiedDate를 자동으로 관리 !

 

 

2) Posts 객체 - BaseTimeEntity 상속받도록 변경 

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

 

 

3) Application에서 JPA Auditing 기능 활성화 

@EnableJpaAuditing      // JPA Auditing 활성화
@SpringBootApplication
public class Application { ... }

 

 

4) 테스트 

@ExtendWith(SpringExtension.class)
@SpringBootTest
public class PostsRepositoryTest {

    @Autowired
    PostsRepository postsRepository;

    @AfterEach
    public void cleanup() {
        postsRepository.deleteAll();
    }

    @Test
    public void BaseTimeEntity_등록() {
        //given
        // 시간 설정
        // LocalDateTime now = LocalDateTime.of(2023, 8, 11, 0, 0, 0);
        LocalDateTime now = LocalDateTime.now();
        // Posts 객체 생성, 저장
        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("### createdDate=" + posts.getCreatedDate() + ", modifiedDate=" + posts.getModifiedDate());

        assertThat(posts.getCreatedDate()).isAfter(now);
        assertThat(posts.getModifiedDate()).isAfter(now);
    }
}
  • LocalDateTime.now() 로 하면 현재 날짜와 시간 설정됨

 

 

 

=> BaseTimeEntity 상속 + JPA Auditing 활성화 -> 등록/수정 시간 자동화 !!