스프링부트와 AWS로 혼자 구현하는 웹서비스 따라하기

저번 시간에 JpaRepository 를 이용해 CRUD 메서드를 사용해보고, 그 메서드가 어떤 쿼리를 보내주는 지 한 번 확인해봤습니다.

이제 본격적으로 API를 만들어보지요.

시작👊

API 를 만들기 위해서는 총 3개의 클래스가 필요합니다.

  • Request 데이터를 받을 DTO
  • API 요청을 받을 Controller
  • 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service

domain 에 대한 설명이 있는데 잘 이해를 못하겠다 ㅠ

일단

CRUD 기능을 만들어보겠습니다.

  1. 일단 클래스를 만들어준다.
    1-1. web 패키지에 PostsApIController
    1-2. web / dto 패키지에 PostsSaveRequestDto
    1-3. service / posts 패키지를 만들고 PostsService 를 만들어준다.

  2. 코드를 채워넣는다.

// PostsApiController.java

package com.prac.webservice.springboot.web;

import com.prac.webservice.springboot.service.posts.PostsService;
import com.prac.webservice.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);
    }
}
// PostsService.java

package com.prac.webservice.springboot.service.posts;

import com.prac.webservice.springboot.domain.posts.PostsRepository;
import com.prac.webservice.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;

@RequiredArgsConstructor
@Service
public class PostsService {

    private final PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestDto requestDto) {
        return postsRepository.save(requestDto.toEntity()).getId();

    }
}
// PostsSaveRequestDto.java

package com.prac.webservice.springboot.web.dto;

import com.prac.webservice.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();
    }
}

Entity 와 Controller Dto

스프링을 어느 정도 써보셨던 분들이라면 Controller 와 Service 에서 @Autowired 가 없는 것이 어색하게 느껴질겁니다.

죄송합니다, 저자님. 전혀 느끼지 못했습니다😔

@Autowired 보다 생성자로 주입 받는 것을 더욱 권장하는데, 위의 코드에서는 생성자가 어디에 있을 까요?
바로 @RequiredArgsConstructor 에서 해결해줍니다. final 선언된 모든 필드를 인자값으로 하는 생성자를 롬복의 @RequiredArgsConstructor 가 대신 생성해준 것입니다.

Dto 클래스를 보면 Entity 클래스와 거의 유사한 형태임에도 따로 클래스를 만들었습니다. 하지만, 유사하다고 해서 절대로 Entity 클래스를 Request/Response 클래스로 사용해서는 안됩니다.
Entity 클래스는 데이터베이스와 맞닿은 핵심 클래스 입니다. Entity클래스를 기준으로 테이블이 생성되고 스키마가 변경됩니다. 그에 반해 화면 변경은 사소한 기능 변경인데, 이를 위해 테이블과 연결된 Entity 클래스를 변경하는 것은 너무 큰 변경입니다. 수많은 서비스 클래스나 비즈니스 로직들이 Entity 클래스를 기준으로 동작해, Entity 클래스가 변경되면 여러 클래스에 영향을 끼치지만, Request, Response 용 Dto 는 view 를 위한 클래스기 때문에 정말 자주 변경이 필요합니다.
꼭 Entity 클래스와 Dto 는 분리해서 사용합시다.

호... 대단해 정말. 너무 재밌다. 너무 흥미롭다.
Entity 클래스!

CRUD Test 를 위한 코드를 짜봅시다

  1. test web 에 PostsApiControllerTest.java 를 만들어줍니다.
  2. 코드를 넣습니다.
// PostsApiControllerTest.java

package com.prac.webservice.springboot.web;

import com.prac.webservice.springboot.domain.posts.Posts;
import com.prac.webservice.springboot.domain.posts.PostsRepository;
import com.prac.webservice.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 PostApiControllerTest {

    @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);
    }

}

지금 Api Controller Test 에서는 HelloController 와는 달리 @WebMvcTest 를 사용하지 않았는데, @WebMvcTest 는 JPA 기능이 작동하지 않기 때문입니다. 지금 같이 JPA 기능 까지 한번에 테스트할 대에는 @SpringBootTest 와 TestRestTemplate 를 사용하면 됩니다!

WebEnvironment.RANDOM_PORT 로 랜덤 포트 실행과 insert 쿼리가 실행 되어있는 걸 확인할 수 있다.

내일은 수정/조회 기능 만들기를 따라해보자!

profile
BEAT A SHOTGUN

0개의 댓글