일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 카렌
- 마인크래프트뮤지컬
- 언리얼
- JWT
- 데이터베이스
- Enhanced Input System
- Express
- 파이썬서버
- 미니프로젝트
- 언리얼뮤지컬
- EnhancedInput
- 언리얼프로그래머
- node
- 스터디
- R
- 프린세스메이커
- VUE
- 스마일게이트
- flask
- 프메
- Unseen
- 정글사관학교
- Ajax
- 디자드
- 레베카
- Bootstrap4
- 게임개발
- 알고풀자
- Jinja2
- 으
- Today
- Total
Showing
[스프링부트] Spring Data JPA 적용(4) 게시글 수정/조회 기능 API 완성 및 JPA Auditing으로 생성 및 수정 시간 자동화 본문
[스프링부트] Spring Data JPA 적용(4) 게시글 수정/조회 기능 API 완성 및 JPA Auditing으로 생성 및 수정 시간 자동화
RabbitCode 2023. 6. 3. 06:271. 수정/조회 기능 제작
(1) PostsApiController
package com.jojo.book.springbootwebservice.web;
import com.jojo.book.springbootwebservice.service.posts.PostsService;
import com.jojo.book.springbootwebservice.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PathVariable;
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);
}
@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 findId(@PathVariable Long id){
return postsService.findById(id);
}
}
(2) PostsResponseDto
package com.jojo.book.springbootwebservice.web.dto;
import com.jojo.book.springbootwebservice.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();
}
}
(3) PostsUpdateRequestDto
package com.jojo.book.springbootwebservice.web.dto;
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) Posts
public void update(String title, String content) { //추가
this.title = title;
this.content = content;
}
(5) PostsService
package com.jojo.book.springbootwebservice.service.posts;
import com.jojo.book.springbootwebservice.domain.posts.Posts;
import com.jojo.book.springbootwebservice.domain.posts.PostsRepository;
import com.jojo.book.springbootwebservice.web.dto.PostsResponseDto;
import com.jojo.book.springbootwebservice.web.dto.PostsSaveRequestDto;
import com.jojo.book.springbootwebservice.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;
}
@Transactional
public PostsResponseDto findById (Long id){
Posts entity = postsRepository.findById(id)
.orElseThrow(()->new IllegalArgumentException("해당 게시글이 없습니다. id="+id));
return new PostsResponseDto(entity);
}
}
신기한 것은 update 기능에서 데이터베이스에 쿼리를 날리는 부분이 없다는 것이다. 이게 가능한 이유는 JPA의 영속성 컨텍스트 때문이다. 영속성 컨텍스트란, 엔티티를 영구저장하는 환경이다. 일종의 논리적 개념이며, JPA의 핵심 내용은 엔티티가 영속성 컨텍스트에 포함되어 있냐 아니냐로 갈린다.
(6) 테스트 코드로 검증 PostApiControllerTest
package com.jojo.book.springbootwebservice.web;
import com.jojo.book.springbootwebservice.domain.posts.Posts;
import com.jojo.book.springbootwebservice.domain.posts.PostsRepository;
import com.jojo.book.springbootwebservice.web.dto.PostsSaveRequestDto;
import com.jojo.book.springbootwebservice.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 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);
}
@Test
public void Posts_수정된다() 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);
}
}
2. 조회 기능 확인(실제 톰캣 실행)
로컬 환경에선 데이터베이스로 H2를 사용한다. 메모리에서 실행하기 때문에 직접 접근하려면 웹 콘솔을 사용해야만 한다. 먼저 웹콘솔을 활성화한다.
application.properties
spring.h2.console.enabled=true
위의 코드를 추가한 뒤 Application 클래스의 main 메소드를 실행한다. 정상 실행됐다면 톰캣이 8080 포트로 실행되었을 것이다. 여기서 웹 브라우저에 http://localhost:8080/h2-console 로 접속하면 웹 콘솔화면이 등장한다.
jdbc url를 밑 화면과 같이 jdbc:h2:mem:testdb로 되어 있지 않다면 똑같이 작성해야 한다.
connect를 누르면 아래와 같은 화면으로 접속하게 된다
현재 등록된 데이터가 없으므로, 간단하게 insert 쿼리를 실행해보고 이를 API로 조회해보도록 한다.
insert into posts (author, content, title) values ('author', 'content', 'title')
등록된 데이터를 확인한 후 API를 요청해보도록 한다. 브라우저에 http://localhost:8080/api/v1/posts/1 을 입력해 API 조회 기능을 테스트 해본다
JSON Viewer이라는 확장 프로그램을 크롬에 설치하면 아래와 같이 받아볼 수 있다.
지금까지 기본적인 등록/수정/조회 기능을 모두 만들고 테스트 해보았다. 특히 등록/ 수정은 테스트 코드로 보호해주므로 이후 변경 사항이 있어도 안전하게 변경할 수 있다!
3. JPA Auditing으로 생성 및 수정 시간 자동화
보통 엔티티에는 해당 테이터의 생성시간과 수정시간을 포함한다. 언제 만들어졌는지, 언제 수정되었는지 등은 차후 유지보수에 있어 굉장히 중요한 정보이기 때문이다. 매번 DB에 삽입하기 전, 갱신하기 전에 날짜 데이터를 등록/수정하는 코드가 여기저기 반복해서 쓰여진다면 프로제그가 몹시 지저분해질 것이다. 대안으로 JPA Auditing을 사용할 수 있다.
(1) LocalData 사용
날짜타입을 사용하도록 한다. Java8부터 LocalData와 LocalDataTime이 등장했다. 그간 Java의 기본 날짜 타입인 Date의 문제점을 제대로 고친 타입이다.
domain 패키지에 BaseTimeEntity 클래스를 생성한다.
package com.jojo.book.springbootwebservice.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
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
BaseTimeEntity 클래스는 모든 Entity의 상위 클래스가 되어 Entity들의 createdDate, modifiedDate를 자동으로 관리하는 역할이다.
코드설명
import javax.persistence.MappedSuperclass; // JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우 필드들(createdDate, modifiedDate)도 칼럼으로 인식하도록 한다.
@EntityListeners(AuditingEntityListener.class) // BaseTimeEntity 클래스에 Auditing 기능을 포함시킨다.
@CreatedDate // Entity가 생성되어 저장될 때 시간이 자동 저장된다.
@LastModifiedDate // 조회한 Entity의 값을 변경할 때 시간이 자동 저장된다.
그리고 Posts 클래스가 BaseTimeEntity를 상속받도록 변경한다.
package com.jojo.book.springbootwebservice.domain.posts;
import com.jojo.book.springbootwebservice.domain.BaseTimeEntity;
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 extends BaseTimeEntity {
마지막으로 JPA Auditting 어노테이션들을 모두 활성화할 수 있도록 Application 클래스에 활성화 어노테이션을 하나 추가한다.
package com.jojo.book.springbootwebservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
코드는 완성되었다.
(2) JPA Auditing 테스트 코드 작성
PostsRepositoryTest 클래스에 테스트 메소드를 하나 더 추가한다.
package com.jojo.book.springbootwebservice.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("jojo@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(2023,6,3,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(">>>>>>> created="+ posts.getCreatedDate()+", modifiedDate="+posts.getModifiedDate());
assertThat(posts.getCreatedDate()).isAfter(now);
assertThat(posts.getModifiedDate()).isAfter(now);
}
}
앞으로 추가될 엔티티들은 더이상 등록일/수정일로 고민할 필요가 없다. BaseTimeEntity만 상속받으면 자동으로 해결되기 때문이다.
'JAVA, SPRING > 스프링 부트와 AWS로 혼자 구현하는 웹서비스' 카테고리의 다른 글
[스프링부트] 스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (1) | 2023.06.04 |
---|---|
[스프링부트] 머스테치 화면 구성, 게시글 등록과 목록 확인, 수정, 삭제 화면 만들기 (0) | 2023.06.03 |
[스프링부트] Spring Data JPA 적용(3) Spring 웹 계층에 대한 이해와 게시글 등록 기능 (0) | 2023.06.03 |
[스프링부트] Spring Data JPA 적용(2) JpaRepository 생성 (0) | 2023.06.02 |
[스프링부트] Spring Data JPA 적용(1) Entity 클래스 작성, 빌더패턴 vs 생성자 패턴 (0) | 2023.06.01 |