web패키지에 PostsApiController 생성
web.dto 패키지에 PostsSaveRequestDto 생성
com.jojoldu.book.springboot에 service 패키지 생성 후 PostsService 생성
PostsApiController 코드
⇒ Controller는 시스템으로 들어오는 요청과 응답을 담당한다.
package com.jojoldu.book.springboot.web;
import com.jojoldu.book.springboot.service.PostsService;
import com.jojoldu.book.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);
}
}
@RequestMapping(method=RequestMethod.POST, ...)
를 이용하거나@PostMapping
을 이용한다.@RequestBody
를 활용한다.PostsService 코드
⇒ Service의 역할은 Dao가 DB에서 받아온 데이터를 전달받아 가공하는 것이다.
package com.jojoldu.book.springboot.service;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.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();
}
}
📍 Controller와 Service가 어떻게 수행되는 것인지 정리를 좀 해보자.
1) Client 가 Request 를 보낸다.
2) Request URL에 알맞은 Controller 가 수신한다.
3) Controller 는 넘어온 요청을 처리하기 위해 Service 를 호출한다.
4) Service 는 알맞은 정보를 가공하여 Controller 에게 데이터를 넘긴다.
5) Controller 는 Service 의 결과물을 Client 에게 전달해준다.
Controller와 Service에서 @Autowired
가 없다.
빈을 주입할 때는 @Autowired
, setter
, 생성자
세 가지 방식이 있다. 이 중 제일 권장하는 방식은 생성자로 주입받는 것이다. 생성자로 Bean 객체를 받으면 @Autowired
와 동일한 효과를 보는 것이다.
그러면 생성자는 어디에 있을까?
이때 생성자는 @RequiredArgsConstructor
가 해결해준다. final인 모든 필드를 인자로 하는 생성자를 대신 생성해준다.
PostsSaveRequestDto 코드
⇒ DTO 클래스의 역할은 데이터를 받아오는 것이다.
package com.jojoldu.book.springboot.web.dto;
import com.jojoldu.book.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();
}
}
PostsSaveRequestDto
는 Posts
Entity와 거의 유사하지만 따로 생성했다. 절대로 Entity 클래스를 Request/Response 클래스로 사용해서는 안된다.
Entity 클래스는 데이터베이스와 맞닿아 있는 핵심 클래스다. Entity 클래스를 기준으로 테이블이 생성되고 스키마가 변경된다.
Entity 클래스가 변경되면 여러 클래스에 영향을 미친다. 그런데 Request와 Response용 DTO는 View를 위한 클래스라 많은 변경이 필요하다. 사소한 수정 사항 때문에 테이블과 연결된 Entity 클래스까지 고치는 것은 위험 부담이 크다.
그러므로 View Layer와 DB Layer의 역할을 철저하게 분리하는 것이 좋다. 실제로도 Controller에서 결과값을 보낼 때 여러 테이블을 조인해서 줘야할 경우가 자주 있다. 따라서 Entity 클래스와 Controller에 쓸 DTO는 반드시 분리해 사용하자.
테스트 패키지 중 web패키지에 PostsApiControllerTest 생성
PostsApiControllerTest 코드
package com.jojoldu.book.springboot.web;
import com.jojoldu.book.springboot.domain.posts.Posts;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsUpdateRequestDto;
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.HttpEntity;
import org.springframework.http.HttpMethod;
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 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);
}
@Test
public void Posts_수정된다() throws Exception {
// given
Posts savedPosts = postsRepository.save(Posts.builder()
.title("title")
.content("content")
.author("author")
.build());
Long updatedId = 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/" + updatedId;
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);
}
}
Posts 등록 API 테스트 결과