[Spring] API 생성하기(2)

[Spring] API 생성하기(2)

이동욱 님의 스프링 부트와 AWS로 혼자 구현하는 웹 서비스책을 공부하며 정리한 내용입니다. 틀린 정보가 있을 수 있으니 주의하시고 댓글로 남겨주시길 바랍니다.

게시물 수정, 삭제 기능 만들기

PostsApiController

package com.hwanld.book.springboot.web; import com.hwanld.book.springboot.service.posts.PostsService; import com.hwanld.book.springboot.web.dto.PostsSaveRequestDto; 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); } }

PostsResponseDto

package com.hwanld.book.springboot.web.dto; import com.hwanld.book.springboot.domain.post.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(); } }

PostsResponseDto 는 Entity의 필드 중 일부만 사용하므로 생성자로 Entity를 받아서 필드에 저장합니다. 굳이 모든 필드를 가진 생성자가 필요하지 않기 때문에 Entity를 받아서 처리합니다.

PostsUpdateRequestDto

package com.hwanld.book.springboot.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; } }

Posts

package com.hwanld.book.springboot.domain.post; 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.content = content; this.author = author; } public void update (String title, String content) { this.title = title; this.content = content; } }

PostsService

package com.hwanld.book.springboot.service.posts; import com.hwanld.book.springboot.domain.post.Posts; import com.hwanld.book.springboot.domain.post.PostsRepository; import com.hwanld.book.springboot.web.dto.PostsResponseDto; import com.hwanld.book.springboot.web.dto.PostsSaveRequestDto; import com.hwanld.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); } }

update 기능을 자세히 보면 데이터베이스에 쿼리를 날리는 부분이 없습니다. JPA의 영속성 컨텍스트 때문입니다. 영속성 컨텍스트란, 엔티티를 영구 저장하는 환경입니다. JPA의 핵심 내용은 엔티티가 영속성 컨텍스트에 포함되어 있냐 아니냐로 갈립니다.

JPA의 엔티티 매니저가 활성화된 상태로(Spring Data JPA를 사용한다면 기본 옵션입니다.) 트랜잭션 안에서 데이터베이스를 데이터로 가져오면 이는 영속성 컨텍스트가 유지된 상태입니다.

이 상태에서 해당 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영합니다. 즉, Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없습니다! 이를 더티 체킹(Dirty Cheking)이라고 합니다.

이제 모든 기능들이 잘 작동하는지 점검하기 위해서 아래와 같은 테스트 코드를 추가할 것입니다.

PostsApiControllerTest

package com.hwanld.book.springboot.web; import com.hwanld.book.springboot.domain.post.Posts; import com.hwanld.book.springboot.domain.post.PostsRepository; import com.hwanld.book.springboot.web.dto.PostsSaveRequestDto; import com.hwanld.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 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 responseEntity = restTemplate.postForEntity(url, requestDto, Long.class); //then assertThat(responseEntity.getStatusCode()) .isEqualTo(HttpStatus.OK); assertThat(responseEntity.getBody()) .isGreaterThan(0L); List 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 requestEntity = new HttpEntity<>(requestDto); //when ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, Long.class); //then assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(responseEntity.getBody()).isGreaterThan(0L); List all = postsRepository.findAll(); assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle); assertThat(all.get(0).getContent()).isEqualTo(expectedContent); } }

이제 실제로 잘 작동하는지 확인하기 위해서 톰캣을 실행해서 직접 확인할 것 입니다. 웹 콘솔을 사용하기 위해서 application.properties 에 아래 옵션을 추가합니다.

spring.h2.console.enabled=true

application.properties 폴더의 위치가 test/resources/application.properties 가 아닌, main/resources/application.properties 아래에 있어야 정상적으로 작동할 수 있습니다. 꼭 확인 하시길 바랍니다!

이후 localhost:8080/h2-console 로 이동해서, jdbc 주소에 jdbc:h2:mem:testdb 라고 추가한 뒤 connect 버튼을 클릭하면 현재 프로젝트의 H2를 관리할 수 있는 관리 페이지로 넘어갑니다.

이어서 insert 쿼리를 실행하고, API로 이를 조회해 보면 조회에 관한 직접적인 테스트를 할 수 있습니다!

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

위와 같이 데이터를 등록했다면 API를 통해서 이를 조회하기 위해, http://localhost:8080/api/v1/posts/1 로 이동해서 insert 쿼리를 통해 넣은 값이 잘 나오는지 확인하면 됩니다!

{ "id": 1, "title": "title", "content": "content", "author": "author" }

위와 같이 나오면 성공입니다! 참고로, JSON 파일을 위와 같이 보기 위해선 Chrome의 확장 프로그램인 Json Viewer 를 설치하면 편하게 볼 수 있습니다!

from http://hwanld.tistory.com/8 by ccl(A) rewrite - 2021-12-30 23:27:55