• Spring 웹 계층알아보기
  • 각 계층에 작성해야하는 로직 구분하기
  • API 만들기

API 만들기

  • save
  • update
  • get

Spring 웹 계층

  • Web Layer

    • Controller, JSP/Freemarker 등의 View Template영역
    • Filter(@Filter), Interceptor, Controller Advice(@ControllerAdvice) 등 외부 요청과 응답에 대한 전반적인 영역
  • Service Layer

    • @Service에 사용되는 서비스 영역
    • Controller와 Dao 중간 영역
    • @Transactional이 사용되는 영역
  • Repository Layer

    • Database와 같이 데이터 저장소에 접근하는 영역
  • Dtos

    • Dto(Data Transfer Object)는 계층 간에 데이터 교환을 위한 객체이며 Dtos는 해당 객체들의 영역을 이야기함
    • View Template Engine에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등
  • Domain Model

    • 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고, 공유할 수 있도록 단순화시킨 것
    • @Entity가 사용되는 영역
    • 데이터베이스의 테이블과 관계되는 것 뿐만아니라 VO처럼 값 객체들도 이 영역에 해당
    • 비즈니스 처리를 담당하는 영역

Save API 만들기

  1. Controller와 Service에서 사용할 Dto 클래스 생성
  2. Controller -> Service 순으로 작성
  3. save기능 테스트 코드 작성

web/dto/PostsSaveRequestDto

  • Dtos

    • Request 데이터를 받은 Dto
    • 계층 간에 데이터 교환을 위한 객체
  • PostsSaveRequestDto

    • Entity 클래스를 기준으로 테이블이 생성되고, 스키마가 변경되므로 Requst/Response 클래스로 사용해선 안된다.
    • Entity 클래스와 Controller에서 쓸 Dto 클래스는 분리되어 사용되야 한다.
@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();
    }
}

web/PostsAPIController

  • Controller Layer (web)

    • 외부 요청과 응답에 대한 전반적인 로직
  • PostsAPIController

    1. @RequiredArgsConstructor
      • final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성
      • 생성자로 Bean을 주입받아 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속해서 수정하는 번거로움을 해결하기 위함
      • 해당 컨트롤러에 새로운 서비스를 추가하거나, 기존 컴포넌트를 제거하는 등의 상황이 발생해도 생성자 코드를 수정하지 않아도 된다.
@RequiredArgsConstructor
@RestController
public class PostsAPIController {

    private final PostsService postsService;

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

service/PostsService

  • Service Layer
    • 트랜잭션
    • 도메인 간 순서 보장 (@Transactional)
@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

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

PostsControllerTest

  • Posts.save 테스트
    • @WebMvcTest 대신 @SpringBootTest와 TestRestTemplate을 사용
    • @WebMvcTest는 JPA 기능이 작동하지 않는다.
    • @SpringBootTest와 TestRestTemplat을 통해 Controller와 ControllerAdvice 등 외부 연동과 관련된 부분만 사용
@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 testSave() {
        // 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);
    }
}
  • 로그 확인
    • WebEnvironment.RANDOM_PORT를 통해 tomcat의 포트가 3421로 구동된 것을 확인
    • insert 쿼리 실행 확인

Update / findById API 만들기

PostsResponseDto

  • PostsReponseDto
    • Entity의 필드 중 일부를 사용하므로 생성자로 Entity를 받아 필드에 대입
    • 모든 필드를 가진 생성자가 필요하지 않으므로 Dto는 Entity를 받아 처리
@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();
    }
}

PostsAPIController

  • PostsAPIController
    • Posts의 id값과 update할 PostsUpdateRequestDto 값을 json 데이터 타입으로 전달하여 호출
    • 해당 Posts의 값을 조회하기 위한 findById 메서드를 정의
@RequiredArgsConstructor
@RestController
public class PostsAPIController {

    private final PostsService postsService;

    // save

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

PostsService

  • PostsService
    • update 기능에서 데이터베이스에 쿼리를 날리는 부분이 없다.
    • JPA의 영속성 컨텍스트, 엔티티를 영구 저장하는 환경
    • JPA의 엔티티 매니저가 활성화된 상태(Spring Data JPA의 기본 옵션)로 트랜잭션안에서 데이터베이스에서 데이터를 가져오는 경우 이 데이터는 영속성 컨텍스트가 유지된 상태이다.
    • 이 상태에서 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영
    • Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없다. (더티 체킹)
@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    ...

    @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) {
        Posts entity = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시물이 존재하지 않습니다. id=" + id));
        return new PostsResponseDto(entity);
    }
}

PostsAPIControllerTest

  • PostsAPIControllerTest
    • Update 기능을 테스트 하기 위해서 savePosts를 통해 Insert 쿼리 호출
    • Update할 값을 설정하여 PostsUpdateRequestDto를 통해 데이터를 만들기
    • restTemplate.exchange(url.toString(), HttpMethod.PUT, requestEntity, Long.class)로 update 실행
    • assertThat을 통해 정상 호출 확인
    • postsRepository.findAll()를 이용하여 데이터 호출
    • 입력된 값이 기대값과 같은지 확인 후 완료
@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();
    }

    // save

    @Test
    public void testUpdate() {
        // 변경하기 전 데이터 입력
        Posts savePosts = postsRepository.save(
                Posts.builder()
                        .title("title")
                        .content("content")
                        .author("author")
                        .build()
        );

        Long updateId = savePosts.getId();
        String exceptedTitle = "title2";
        String exceptedContent = "content2";
        // 데이터 변경을 위한 Dto 생성
        PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder()
                .title(exceptedTitle)
                .content(exceptedContent)
                .build();

        StringBuilder url = new StringBuilder();

        url.append("http://localhost:");
        url.append(port);
        url.append("/api/v1/posts/");
        url.append(updateId);

        HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);

        // when
        ResponseEntity<Long> responseEntity = restTemplate.exchange(url.toString(), 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(exceptedTitle);
        assertThat(all.get(0).getContent()).isEqualTo(exceptedContent);

    }
}

update Test

H2 데이터베이스 웹 콘솔에서 확인하기

insert into posts 
(author, content, title)
values
('author', 'content', 'title');
  • GET: /api/v1/posts/1 URL 호출로 findById 기능 확인

  • PUT: /api/v1/posts/1 URL 호출로 update 기능 확인
    • insomnia라는 프로그램으로 실행하여 테스트

 

 

  • 추가 PostsUpdateRequestDto

'Spring > SpringBoot' 카테고리의 다른 글

[SpringBoot] Mustache  (0) 2020.05.28
[SpringBoot] JPA Auditing  (0) 2020.05.28
[SpringBoot] 설정파일 yaml로 변경하기  (0) 2020.05.28
[SpringBoot] Spring Data JPA 설정  (0) 2020.05.28
[SpringBoot] lombok 설정 및 테스트  (0) 2020.05.28

+ Recent posts