[springboot-webserivce] Post 등록 API 생성 및 분석과 테스트
Updated:
[2020.12.28] 부터 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 책을 통해 스프링 공부를 시작했다.
공부한 내용을 글로 정리하려고 한다.
그래야 나중에 다시 찾아보며 공부 할 수 있을테니까 !!
📒 API 를 만들기 위해 필요한 클래스
API 를 만들기 위해선 세 가지 클래스가 필요하다.
- Request 데이터를 받아올 DTO
- API 요청을 받을 Controller
- 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service
DTO 는 Data Transfer Object 의 줄임말로 데이터 객체를 의미한다.
데이터를 담아 전송하는 역할을 하기 때문에 setter 는 존재하지 않는다.
기존에는 Service 에서 DAO 를 통해 데이터베이스에 접근 후 모든 로직을 처리했다고 한다.
하지만 이것을 도메인 모델로 바꾸게 된다면 Service 클래스는 트랜잭션과 도메인 간의 순서만 보장해 주게 된다.
트랜잭션은 데이터베이스의 상태를 변경하는 작업의 단위를 의미한다.
PostsSaveRequestDto, PostsService, PostsApiController 세 가지 클래스를 통해 Post 등록 API 를 만들어 보려고 한다.
📌 PostApiController
[Spring] Spring Data JPA 적용과 테스트 을 통해 JPA 까지 적용을 하고 본격적인 Posts API 를 만들기로 했다.
/hello URL 로 GET 요청을 보내면 hello 를 반환해주는 API 와 비슷한 구조였는데 모르는 부분이 많아 정리하려고 한다.
src/main/java/com/boks/springboot/web/PostsApicontroller 클래스를 만들자.
package com.boks.springboot.web;
import com.boks.springboot.service.posts.PostsService;
import com.boks.springboot.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
스프링에서 Bean 을 주입받는 방법으로는 세 가지가 있다.
@Autowired, setter, 생성자가 있는데 가장 추천하는 방법은 생성자를 통해 주입하는 방법이라고 한다.
때문에 롬복 어노테이션을 사용해 final 로 선언된 필드를 가지고 생성자를 만든다.
이렇게 만드는 이유는 나중에 필드 값을 바꿀 일이 있어도 일일이 바꿔 줄 필요가 없기 때문이다.
@RestController
Spring MVC Controller 에서 JSON 형태로 객체 데이터를 반환하는 경우 사용한다.
더 이해를 하려면 RESTful API 를 이해하고 있어야 하는데 다음에 바짝 공부하고 정리해야겠다.
@PostMapping
스프링 4.3 버전 이전에는 @RequestMapping(value = “/”, method = “…”) 의 어노테이션을 사용했다고 한다.
하지만 4.3 버전 이후에는 더 확실하게 알아 볼 수 있도록
@PostMapping @GetMapping @DeleteMapping @PutMapping 으로 바뀌었다고 한다.
@RequestBody
HTTP 요청의 body 내용을 자바 객체로 바꾸어 준다.
이 코드에서는 글을 작성하고 등록을 누르면 생성되는 body 내용을 PostsSaveRequestDto 객체로 바꾸어 준다.
✏️ PostsService
src/main/java/com/boks/springboot/service/posts/PostsService 클래스를 만들자.
package com.boks.springboot.service.posts;
import com.boks.springboot.domain.posts.PostsRepository;
import com.boks.springboot.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();
}
}
@Transactional
트랜잭션은 데이터베이스의 상태를 바꾸는 작업의 단위이다.
네 가지의 성질을 가지는데 원자성, 일관성, 격리성, 지속성이다.
원자성은 이 트랜잭션 안의 명령은 전부 실패하거나 성공해야한다.
일부 작업만 성공하고 일부 작업만 실패하는 경우는 없어야한다.
이 어노테이션을 넣어주면 작업 안에서 에러가 발생하는 경우 자동으로 롤백을 해준다고 한다.
🗂 PostsSaveRequestDto
java/com/boks/springboot/web/dto/PostsSaveRequestDto 클래스를 생성한다.
이 클래스를 Request, Response 객체로 사용 할 것이다.
[Spring] Spring Data JPA 적용과 테스트 여기서 만든 Entity 클래스와 유사하다.
하지만 절대로 Request, Response 클래스를 Entity 클래스로 사용하면 안된다.
Request, Response 클래스는 View 를 위한 클래스라서 자주 변경이 되기 때문이다.
또 View 레이어와 DB 레이어의 역할은 철저히 분리해 주는 것이 좋다.
package com.boks.springboot.web.dto;
import com.boks.springboot.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();
}
}
@Getter, @NoArgsConstructor
롬복의 어노테이션으로 getter 와 기본 생성자를 만들어준다.
@Builder
기존 생성자를 만드는 것과 똑같지만 toEntity 메소드 에서와 같이 필드 값을 명확히 보여줘 실수를 막을 수 있다.
만약 기본 생성자로 사용을 한다면 객체 생성 과정에서 title 과 content 의 순서가 바뀌어도 에러가 발생하지 않기 때문이다.
⚙️ API 테스트
세 가지 클래스를 통해 Post API 를 만들었다.
API 를 만들었으면 꼭 해야 할 테스트가 남아있다.
java/com/boks/springboot/web/PostsApiControllerTest 클래스를 만들어주자.
package com.boks.springboot.web;
import com.boks.springboot.domain.posts.Posts;
import com.boks.springboot.domain.posts.PostsRepository;
import com.boks.springboot.web.dto.PostsSaveRequestDto;
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.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 uploadPost() 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);
}
}
단위 테스트를 하기 위해선 @MockMvcTest 를 사용하면 되지만
컨트롤러에서 서비스까지 넘어가는 테스트를 해야하기 때문에
전체적인 흐름을 테스트 할 수 있는 @SpringBootTest 를 사용한다.
TestRestTemplate 는 아직 공부하고 있는 단계라서 더 이해를 해야 할 것 같다..
테스트는 통과!
📕 정리
읽고 공부 해야 할 내용
[Network] REST란? REST API란? RESTful이란?
[Spring Boot] 선언적 트랜잭션 @Transactional
🥮 포스트 등록 과정
- localhost:8080 으로 접속하면 IndexController 가 index.mustache 를 요청한다.
- 화면에 보이는 글 등록 버튼을 누르면 <a> 태그로 인해 localhost:8080/posts/save 주소로 이동한다.
- IndexController 가 posts-save.mustache 를 요청한다.
- posts-save.mustache 로 구성한 글 양식을 통해 글을 작성하고 등록 버튼을 누른다.
- 레이아웃 구조에 의해 footer.mustache 에 들어가있는 js 파일의 함수가 동작한다.
- ajax 를 통해 json 을 가져와 /api/v1/posts 주소로 넘겨준다.
- PostsApiController 가 받아온 json 을 DTO 객체로 변환하고 PostsService 에게 넘겨 save 메소드를 실행한다.
- PostsService 는 받아온 DTO 객체를 PostsRepository 에 넘겨 데이터베이스에 저장한다.
Leave a comment