티스토리 뷰
등록/수정/조회 API를 만들자
본격저으로 등록, 수정, 삭제 기능을 만들어보자
등록 기능을 만들어보자
먼저 클래스들을 만들자
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을 사용하면 된다.
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);
}
}
예전 MyBatis를 쓰던 것과 달리 JPA를 씀으로 좀 더 객체지향적으로 코딩할 수 있음을 느꼈을 것이다.
조회는 톰캣을 실행하여 확인하자
로컬 환경에서는 데이터베이스로 H2를 사용하므로 메모리에서 실행하기 때문에 직접 접근하려면 웹 콘솔을 사용해야하만 한다.
application.properties에세 웹 콘솔 옵션을 활성화 하자
spring.h2.console.enabled=true
- 톰캣을 실행하자
- localhost:8082/h2-console로 접속(port를 8082로 설정했다.)
- 콘솔 화면에서 로 변경 후 Connect를 클릭하여 H2를 관리할 수 있는 관리페이지로 이동한다.
- 왼쪽 상단에
POSTS
테이블이 정상적으로 노출되어 있는 것을 확인할 수 있고, 간단한SELECT * FROM POSTS;
쿼리를 실행하여 결과를 확인할 수 있다.
이제 관리페이지에서 데이터를 등록해보자
INSERT INTO POSTS (AUTHOR, CONTENT, TITLE) VALUES ('author', 'content', 'title');
올바르게 데이터가 insert 되었다.
이제 우리가 코딩한 url로 API를 조회해보자
localhost:8082/api/v1/posts/1 입력
정상적으로 나타나는 것을 확인할 수 있다.
참고
http://www.yes24.com/Product/Goods/83849117?Acode=101
'Spring > SpringBoot 실습' 카테고리의 다른 글
[SpringBoot 실습] 서버 템플릿 엔진과 머스테치 소개 (0) | 2021.01.03 |
---|---|
[SpringBoot 실습] JPA Auditing으로 생성시간/수정시간 자동화하기 (3) | 2020.11.20 |
[SpringBoot 실습] JPA로 데이터베이스를 다뤄보자 (2) | 2020.01.25 |
[SpringBoot 실습] 실습 요구사항 (0) | 2020.01.25 |
[SpringBoot 실습] 스프링 부트와 테스트 코드 (0) | 2020.01.20 |
- Total
- Today
- Yesterday
- 이펙티브자바
- 자바8
- springboot
- 이펙티브 자바
- ifPresent
- 김영한
- junit
- 빈 순환 참조
- java8
- Spring
- jdk버전
- 정적팩터리메서드
- 팩토리 메소드 패턴
- 스프링부트
- package-private
- effectivejava
- try catch finally
- 연관관계
- 인프런
- flatMap
- JPA
- @Lazy
- Effective Java
- try with resources
- java
- 복사 팩토리
- 빌더 패턴
- mustache
- 생성자
- 점층적 생성 패턴
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |