티스토리 뷰

반응형

등록/수정/조회 API를 만들자

본격저으로 등록, 수정, 삭제 기능을 만들어보자

 

등록 기능을 만들어보자

먼저 클래스들을 만들자

image

Controller : PostApiController

DTO : PostsSaveRequestDto

Service : PostsService

 

 

//Controller
package com.mesung.book.springboot.web;

import com.mesung.book.springboot.service.posts.PostsService;
import com.mesung.book.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PutMapping;
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);
    }

}


//Service
package com.mesung.book.springboot.service.posts;

import com.mesung.book.springboot.domain.posts.Posts;
import com.mesung.book.springboot.domain.posts.PostsRepository;
import com.mesung.book.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class PostsService {

    //Autowired가 없네? 이유는 생성자로 의존성 주입을 받기 때문인다(@RequiredArgsContructor)
    //롬복을 사용하게 되면 해당 클래스의 의존성 관계가 변경되어도 코드 변경이 필요없기 때문이다.
    private final PostsRepository postsRepository;

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

}


//DTO
package com.mesung.book.springboot.web.dto;

import com.mesung.book.springboot.domain.posts.Posts;
import javafx.geometry.Pos;
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 클래스와 거의 유사한 DTO를 만들었는데, 절대로 Entity 클래스로 Request/Response 클래스로 사용해서는 안된다.

그 이유는, Entity 클래스는 데이터베이스와 맞닿은 핵심 클래스이다. 사소한 UI 변경에 테이블과 연결되어 있는 Entity 클래스를 변경하는 것은 너무 큰 변경이다.

Entity 클래스가 변경되면 여러 클래스에 영향을 끼치지만 Request 및 Response용 DTO는 View를 위한 클래스라 자주 변경이 된다.

즉, View Layer와 DB Layer는 철저하게 역할을 분리해놔야 한다.(Entity 클래스와 Controller에서 쓸 DTO는 분리해라)

 

 

자 이제 Test코드를 작성해보자

import com.mesung.book.springboot.domain.posts.Posts;
import com.mesung.book.springboot.domain.posts.PostsRepository;
import com.mesung.book.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 postsInsert() throws Exception {
        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를 테스트하는 HelloController와 달리 @WebMvcTest를 사용하지 않는다.

 

@WebMvcTest의 경우 JPA 기능이 작동하지 않고 , Controller와 ControllerAdvice 등 외부 연동과 관련되 부분만 활성화되니 지금처럼 JPA 기능까지 한번에 테스트할 경우 @SpringBootTest와 TestRestTemplate을 사용하면 된다.

image

WebEnvironment.RANDOM_PORT로 인한 랜덤 포트 실행과 insert 쿼리가 실행된 것을 모두 확인할 수 있다.

 

 

TestRestTemplate

@SpringBootTest와 TestRestTemplate을 사용하면 편리하게 웹 통합 테스트를 할 수 있다.

@SpringBootTest에서 Web Environment 설정을 하였다면 TestRestTemplate는 그에 맞춰서 자동을 설정되어 빈이 생성되는 것이다.

 

기존 컨트롤러 테스트에서 많이 사용 하던 MockMvc와 차이가 존재한다.

MockMvc는 Servlet Container를 생성하지 않는 반면, @SpringBootTest와 TestRestTemplate는 Servlet Container를 사용한다. 그래서 마치 실제 서버가 동작하는 것처럼 테스트를 할 수 있는 것이다.

//TestRestTemplate 사용
ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);

assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isGreaterThan(0L);

 

수정 기능을 만들어보자

Controller : PostApiController

DTO : PostResponseDto, PostUpdateRequestDto

Service : PostsService

 

 

Controller

//PostApiController
@RequiredArgsConstructor
@RestController
public class PostsApiController {

    ...

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

}

PostResponseDto

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

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

굳이 모든 필드를 가진 생성자가 필요하진 않으므로 Dto는 Entity를 받아 처리한다.

 

PostUpdateRequestDto

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

PostsService

import com.mesung.book.springboot.domain.posts.Posts;
import com.mesung.book.springboot.domain.posts.PostsRepository;
import com.mesung.book.springboot.web.dto.PostsResponseDto;
import com.mesung.book.springboot.web.dto.PostsSaveRequestDto;
import com.mesung.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) {
        return null;
    }
}

소스를 자세히 보면 @Autowired가 없는 것을 확인할 수 있다. 이유는, 생성자로 의존성 주입을 받기 때문인다(@RequiredArgsContructor)

롬복(@RequiredArgsContructor)을 사용하게 되면 해당 클래스의 의존성 관계가 변경되어도 코드 변경이 필요없기 때문이다.

 

또한 update 기능에서 신기한 것은 데이터베이스에 쿼리를 날리는 부분이 없다. 이것이 가능한 이유는 JPA의 영속성 컨텍스트 때문이다. 영속성 컨텍스는 엔티티를 영구 저장하는 환경이다.

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

 

 

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 postsUpdate() throws Exception {
        Posts savePosts = postsRepository.save(Posts.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());

        Long updateId = savePosts.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);

        ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, Long.class);

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

image

예전 MyBatis를 쓰던 것과 달리 JPA를 씀으로 좀 더 객체지향적으로 코딩할 수 있음을 느꼈을 것이다.

 

조회는 톰캣을 실행하여 확인하자

로컬 환경에서는 데이터베이스로 H2를 사용하므로 메모리에서 실행하기 때문에 직접 접근하려면 웹 콘솔을 사용해야하만 한다.

 

application.properties에세 웹 콘솔 옵션을 활성화 하자

spring.h2.console.enabled=true

 

  1. 톰캣을 실행하자
  2. localhost:8082/h2-console로 접속(port를 8082로 설정했다.)
  3. image
  4. 콘솔 화면에서 image 로 변경 후 Connect를 클릭하여 H2를 관리할 수 있는 관리페이지로 이동한다.
  5. image
  6. 왼쪽 상단에 POSTS 테이블이 정상적으로 노출되어 있는 것을 확인할 수 있고, 간단한 SELECT * FROM POSTS; 쿼리를 실행하여 결과를 확인할 수 있다.

이제 관리페이지에서 데이터를 등록해보자

INSERT INTO POSTS (AUTHOR, CONTENT, TITLE) VALUES ('author', 'content', 'title');

 

올바르게 데이터가 insert 되었다.

 

이제 우리가 코딩한 url로 API를 조회해보자

 

localhost:8082/api/v1/posts/1 입력

image

정상적으로 나타나는 것을 확인할 수 있다.

 

참고

http://www.yes24.com/Product/Goods/83849117?Acode=101

 

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

가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현한다. JPA와 JUnit 테스트, 그레이들, 머스테치, 스프링 시큐리티를 활용한 소셜 로그인 등으로 애플리케이션을 개발하고, 뒤이어 AWS 인프라의 기본 사용법과 AWS EC2와 R...

www.yes24.com

 

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함