[스프링부트] 3.2. 등록/수정/조회 API 만들기

Bummy·2022년 8월 16일
0

springboot

목록 보기
5/15
post-thumbnail

지난 시간 JPA까지는 이해가 잘 되었지만 이번 시간에 했던 등록/조회/수정 API 만들기 부분이 엄청 어렵게 느껴졌다. 만들 것도 많고 이해가 가지 않는 것도 많아서 해당 부분은 여러번 공부해보려고 한다. 다시 한번 공부하며 기록한다.

API를 만들기 위해서는 3개의 클래스가 필요한데

  • Dto : Request 데이터를 받음
  • Controller : API의 요청을 받음
  • Service : 트랜젝션, 도메인 기능 간의 순서를 보장함

등록, 수정, 삭제 기능을 만들기 위해 각 파일에 코드를 작성해준다.


PostsApiController

@RequiredArgsConstructor
@RestController
public class PostsApiController {
    private final PostsService postsSerivce;

    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto)
    {
        return  postsSerivce.save(requestDto);
    }
 }

-> 스프링에서 Bean을 주입받는 방식이 다양하게 있는데

  • @Autowired
  • setter
  • 생성자

이 중 가장 권장하는 방식이 생성자로 주입받는 방식이다.
그렇다면 생성자를 하나씩 다 만들어줘야하는데
이 것을 롬복의 @RequiredArgsConstructor가 해결해준다.

  • @RequiredArgsConstructor : 롬복의 어노테이션으로 final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성해준다.

PostsService

@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

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

다음은 Controller와 Service에서 사용할 Dto 클래스를 만들어준다.

PostsSaveRequestDto

@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를 분리해서 사용하기


PostsApiControllerTest 만들어서 테스트 진행하기

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

-> HelloController를 테스트 했을 때와 달리 @WebMvcTest를 사용하지 않았는데 @WebMvcTest의 경우 JPA는 작동하지 않기에 이번에는 @SpringBootTest와 TestRestTemplate를 사용한다.


테스트를 진행하면 이렇게 잘 나오는 것을 확인할 수 있다.


수정/조회 기능 만들기

PostsApiController

 @PutMapping("/api/v1/posts/{id}")
    public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto){
        return postsSerivce.update(id, requestDto);
    }
    
 @GetMapping("/api/v1/posts/{id}")
    public PostsReponseDto findById(@PathVariable Long id){
        return postsSerivce.findById(id);
    }

PostsResponseDto

@Getter
public class PostsReponseDto {

    private Long id;
    private String title;
    private String content;
    private String author;

    public PostsReponseDto(Posts entity){
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.content = entity.getContent();
        this.author = entity.getAuthor();
    }
}

-> PostsResponseDto는 Entity의 필드 중 일부만 사용하므로 생성자로 Entity를 받아 필드에 값을 넣는다.


PostsUpdateRequestDto

@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

    public void update(String title, String content){
        this.title = title;
        this.content = content;
    }
}

PostsService

 @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 PostsReponseDto findById(Long id){
    
        Posts entity = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 사용자가 없습니다. id=" + id));
        return  new PostsReponseDto(entity);
    }
}

정상적으로 Update 쿼리를 수정하는지 테스트를 진행한다.

PostsApiControllerTest

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

이 부분에서 많은 시행착오를 겪었다.

코드를 수정하기 전에 테스트를 진행해보니

Error while extracting response for type [class java.lang.Long] and content type [application/json;charset=UTF-8]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize instance of `java.lang.Long` out of START_OBJECT token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.Long` out of START_OBJECT token
 at [Source: (PushbackInputStream); line: 1, column: 1]
org.springframework.web.client.RestClientException: Error while extracting response for type [class java.lang.Long] and content type [application/json;charset=UTF-8]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize instance of `java.lang.Long` out of START_OBJECT token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.Long` out of START_OBJECT token
 at [Source: (PushbackInputStream); line: 1, column: 1]

이러한 에러가 나왔다.

해당 오류를 해결하기 위해 책의 저자분의 깃허브도 들어가보고 같은 책으로 공부를 하는 여러 사람들의 블로그를 찾아보았지만 5판 인쇄 전에 오타가 있어서 나타나는 오류였고 나랑은 상관 없는 말 뿐이었다.
그나마 조금 비슷하신 분이 있어서 확인해보니 함수쪽에 오타가 있어서 해당 오류가 나왔고 지금은 해결을 했다는 댓글을 확인했다.
1시간정도 코드를 자세히 보니 테스트를 위한 url을 선언하는 과정에서
오타가 있었던 것을 확인할 수 있었다.


-> v1으로 선언을 해야하는데 vi로 선언되어 있어서 나타나는 에러였다.


-> 해당 코드를 수정해보니 정상적으로 테스트 되는 것을 확인할 수 있었다.

0개의 댓글