http://localhost:8080/api/v1/posts/1으로 접속해 API 조회 기능 테스트
JPA Auditing으로 생성시간/수정시간 자동화
보통 엔티티에는 데이터의 생성시간과 수정시간을 포함함
이 두 정보는 차후 유지보수에서 중요한 정보이기 때문
그렇다 보니 매번 DB에 삽입/갱신하기 전에 날짜 데이터를 등록/수정하는 코드가 사용됨
위의 과정을 JPA Auditing으로 자동화 가능
Java8부터 등장한 LocalDate와 LocalDateTime 사용
LocalDate 사용
BaseTimeEntity.java
Posts.java
Application.java
JPA Auditing 테스트 코드 작성하기
PostsRepositoryTest.java
User user = findUser();Group group = user.getGroup();
User user = userDao.findUser();Group group = groupDao.findGroup(user.getGroupId());
implementation('org.springframework.boot:spring-boot-starter-data-jpa') // Spring-boot용 Spring Data JPA 추상화 라이브러리
// 인메모리 관계형 DB로 별도의 설치 없이 프로젝트 의존성만으로 관리 가능
// 메모리에서 실행돼 앱을 재시작할 때마다 초기화되고, 이를 이용해 테스트 용도로 많이 사용
implementation('com.h2database:h2')
package com.jojoldu.book.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;// 주요 어노테이션을 클래스에 가깝게// Kotlin 등 새 언어 전환 시 불필요한 어노테이션(e.g. lombok의 어노테이션) 삭제에 용이@Getter // 클래스내 모든 필드에 Getter 자동 생성@NoArgsConstructor // 기본 생성자 추가@Entity // 테이블과 링크될 클래스임을 나타냄public class Posts{ @Id // 해당 테이블의 PK 필드 @GeneratedValue(strategy = GenerationType.IDENTITY) // PK의 생성 규칙 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 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();}
// 아래의 경우 new Example(b, a)처럼 a와 b의 위치를 변경해도 코드를 실행하기 전에는 문제를 찾을 수 없음public Example(String a, String b){ this.a = a; this.b = b;}
Example.builder() .a(a) .b(b) .build();
// Entity 클래스와 기본 Entity Repository는 항상 함께 위치package com.jojoldu.book.springboot.domain.posts;import org.springframework.data.jpa.repository.JpaRepository;public interface PostsRepository extends JpaRepository<Posts, Long>{}
package com.jojoldu.book.springboot.domain.posts;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.assertThat;@ExtendWith(SpringExtension.class)@SpringBootTestpublic class PostsRepositoryTest{ @Autowired PostsRepository postsRepository; // 단위 테스트가 끝날 때마다 수행되는 메소드를 지정 // 배포 전 전체 테스트를 수행할 때 테스트간 데이터 침범을 막기 위해 사용 @AfterEach public void cleanup() { postsRepository.deleteAll(); } @Test public void savePost() { // given String title = "테스트 게시글"; String content = "테스트 본문"; postsRepository.save(Posts.builder() // 테이블 posts에 insert/update 쿼리 실행(id 값이 있으면 update, 없으면 insert) .title(title) .content(content) .author("jojoldu@gmail.com") .build()); // when List<Posts> postsList = postsRepository.findAll(); // 테이블 posts에 있는 모든 데이터 조회 // then Posts posts = postsList.get(0); assertThat(posts.getTitle()).isEqualTo(title); assertThat(posts.getContent()).isEqualTo(content); }}
// 기존 importimport com.jojoldu.book.springboot.web.dto.PostsResponseDto;@RequiredArgsConstructor@RestControllerpublic class PostsApiController{ // 기존 코드 @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 PostsReponseDto findById(@PathVariable Long id) { return postsService.findById(id); }}
public class Posts{ //기존 코드 public void update(String title, String content) { this.title = title; this.content = content; }}
// 기존 importimport com.jojoldu.book.springboot.web.dto.PostsResponseDto;import com.jojoldu.book.springboot.web.dto.PostsUpdateRequestDto;import com.jojoldu.book.springboot.domain.posts.Posts;@RequiredArgsConstructor@Servicepublic class PostsService{ // 기존 코드 @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; } @Transactional(readOnly = true) public PostsResponseDto findById(Long id) { Posts entity = postsRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("해당 사용자가 없습니다. id=" + id)); return new PostsResponseDto(entity); }}
@ExtendWith(SpringExtension.class)@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)public class PostsApiControllerTest{ // 기존 코드 @Test public void PostsUpdate() throws Exception { //given Posts savedPosts = postsRepository.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> requestEntity = new HttpEntity<>(requestDto); // when 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); }}
// 다음과 같이 수정spring.jpa.show-sql=truespring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialectspring.jpa.properties.hibernate.dialect.storage_engine=innodbspring.datasource.hikari.jdbc-url=jdbc:h2:mem://localhost/~/testdb;MODE=MYSQLspring.h2.console.enabled=true
// 생성일 추가 코드 예시public void savePosts(){ // ... posts.setCreateDate(new LocalDate()); postsRepository.save(posts);}
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.EntityListeners;import javax.persistence.MappedSuperclass;import java.time.LocalDateTime;@Getter@MappedSuperclass // JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우, 필드들도 칼럼으로 인식되게 함@EntityListeners(AuditingEntityListener.class) // BaseTimeEntity 클래스에 Auditing 기능 포함public class BaseTimeEntity{ @CreatedDate // Entity가 생성되어 저장될 때 시간이 자동으로 저장 private LocalDateTime createdDate; @LastModifiedDate // 조회한 Entity의 값을 변경할 때 시간이 자동 저장 private LocalDateTime modifiedDate;}
public class Posts extends BaseTimeEntity {}
@EnableJpaAuditing // 어노테이션 추가하여 JPA Auditing 어노테이션들을 모두 활성화@SpringBootApplicationpublic class Application{ public static void main(String[] args) { SpringApplication.run(Application.class, args); }}
public class PostsRepositoryTest{ // 기존 코드 @Test public void registerBaseTimeEntity() { // given LocalDateTime now = LocalDateTime.of(2021, 11, 16, 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(">>>>>>>>> create Date="+posts.getCreatedDate()+", modifiedDate="+posts.getModifiedDate()); assertThat(posts.getCreatedDate()).isAfter(now); assertThat(posts.getModifiedDate()).isAfter(now); }}