[springboot-webserivce] Post 등록 API 생성 및 분석과 테스트

Updated:

[2020.12.28] 부터 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 책을 통해 스프링 공부를 시작했다.

공부한 내용을 글로 정리하려고 한다.

그래야 나중에 다시 찾아보며 공부 할 수 있을테니까 !!

📒 API 를 만들기 위해 필요한 클래스

API 를 만들기 위해선 세 가지 클래스가 필요하다.

  1. Request 데이터를 받아올 DTO
  2. API 요청을 받을 Controller
  3. 트랜잭션, 도메인 기능 간의 순서를 보장하는 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 는 아직 공부하고 있는 단계라서 더 이해를 해야 할 것 같다..

image

테스트는 통과!

📕 정리

읽고 공부 해야 할 내용

[Network] REST란? REST API란? RESTful이란?

[Spring Boot] 선언적 트랜잭션 @Transactional

🥮 포스트 등록 과정

  1. localhost:8080 으로 접속하면 IndexController 가 index.mustache 를 요청한다.
  2. 화면에 보이는 글 등록 버튼을 누르면 <a> 태그로 인해 localhost:8080/posts/save 주소로 이동한다.
  3. IndexController 가 posts-save.mustache 를 요청한다.
  4. posts-save.mustache 로 구성한 글 양식을 통해 글을 작성하고 등록 버튼을 누른다.
  5. 레이아웃 구조에 의해 footer.mustache 에 들어가있는 js 파일의 함수가 동작한다.
  6. ajax 를 통해 json 을 가져와 /api/v1/posts 주소로 넘겨준다.
  7. PostsApiController 가 받아온 json 을 DTO 객체로 변환하고 PostsService 에게 넘겨 save 메소드를 실행한다.
  8. PostsService 는 받아온 DTO 객체를 PostsRepository 에 넘겨 데이터베이스에 저장한다.



Categories:

Updated:

Leave a comment