2020. 1. 31. 00:51ㆍProject E/Project E 파트2
API를 만들려면 총 3개의 클래스가 필요
1) Request 데이터를 받을 Dto
2) API 요청을 받을 Controller
3) 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service
[*] 서비스단에서 비지니스 로직을 처리하면 안된다.
서비스단에서는 트랜잭션, 도메인 간 순서 보장 역할만 담당
웹 레이어 : 외부 요청과 응답에 대한 영역
뷰 템플릿 영역
필터
인터셉터
컨트롤러 어드바이스
서비스 레이어
@Service 에 사용되는 서비스 영역
일반적으로 컨트롤러와 Dao의 중간 영역
@Transactional 이 사용되어야 하는 영역
리포지토리 영역 (Dao 영역)
데이터베이스와 같이 데이터 저장소에 접근하는 영역
Dtos (Dto 단)
계층 간에 데이터 교환을 위한 객체 (Dto)
뷰 템플릿 엔진에서 사용될 객체나 리포지토리 레이어에서 결과로 넘겨준 객체
도메인 모델
모든사람이 동일한 관점에서 이해할 수 있고 공유할 수 있게 단순화시킨 것으로 속성(명사)이라고 생각하면 된다.
@Entity 영역
단, 무조건 데이터베이스의 테이블과 관계가 있어야만 하는건 아니다.
VO처럼 값 객체들도 이 영역에 해당
src/main/java
com.minokuma.book.springboot
web.dto 패키지
PostsSaveRequestDto.java
package com.minokuma.book.springboot.web.dto;
import com.minokuma.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();
}
}
Entity 클래스는 데이터베이스와 맞닿은 핵심 클래스로 테이블이 생성되고, 스키마가 변경되므로
Request/Response 클래스로 사용해서는 절대 안된다.
Request/Response 용인 Dto는 뷰를 위한 클래스라 자주 변경이 필요하기때문이다.
따라서 뷰 레이어와 DB 레이어의 역할 분리를 철저하게 해야 한다.
즉, 컨트롤러에서 쓸 Dto와 DB 레이어에서 쓸 Entity는 분리되어야한다.
src/main/java
com.minokuma.book.springboot
service.posts 패키지
PostsService.java
package com.minokuma.book.springboot.service.posts;
import com.minokuma.book.springboot.domain.posts.PostsRepository;
import com.minokuma.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();
}
}
Bean 주입 방법
@Autowired : 비권장
@setter
@생성자 : 권장
(*) final로 선언된 필드를 롬복의 @RequiredArgsContructor가 대신 생성
src/main/java
com.minokuma.book.springboot
web 패키지
PostsApiController.java
package com.minokuma.book.springboot.web;
import com.minokuma.book.springboot.service.posts.PostsService;
import com.minokuma.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);
}
}
테스트
src/test/java
com.minokuma.book.springboot
web 패키지
PostsApiControllerTest.java
package com.minokuma.book.springboot.web;
import com.minokuma.book.springboot.domain.posts.Posts;
import com.minokuma.book.springboot.domain.posts.PostsRepository;
import com.minokuma.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 registPosts() 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";
ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);
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);
}
}
@WebMvcTest 의 경우 JPA 기능이 작동하지 않기때문에, JPA 기능까지 한번에 테스트할 때는
@SpringBootTest와 TestRestTemplate를 사용하면 된다.
Posts 등록 API 테스트 결과
WebEnvironment.RANDOM_PORT 로 인한 랜덤 포트 실행과 insert 쿼리가 실행된 것을 확인가능하다.
이로서 등록 기능 완료
domain.posts
Posts.java
package com.minokuma.book.springboot.domain.posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@NoArgsConstructor
@Entity
public class Posts {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 500, nullable = false)
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
private String author;
@Builder
public Posts(String title, String content, String author){
this.title = title;
this.content = content;
this.author = author;
}
public void update(String title, String content){
this.title = title;
this.content = content;
}
}
web.dto
PostsUpdateRequestDto.java
package com.minokuma.book.springboot.web.dto;
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;
}
}
web.dto
PostsResponseDto.java
package com.minokuma.book.springboot.web.dto;
import com.minokuma.book.springboot.domain.posts.Posts;
import lombok.Getter;
@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();
}
}
service.posts
PostsService.java
package com.minokuma.book.springboot.service.posts;
import com.minokuma.book.springboot.domain.posts.Posts;
import com.minokuma.book.springboot.domain.posts.PostsRepository;
import com.minokuma.book.springboot.web.dto.PostsResponseDto;
import com.minokuma.book.springboot.web.dto.PostsSaveRequestDto;
import com.minokuma.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){
Posts entity = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
return new PostsResponseDto(entity);
}
}
web
PostsApiController
package com.minokuma.book.springboot.web;
import com.minokuma.book.springboot.service.posts.PostsService;
import com.minokuma.book.springboot.web.dto.PostsResponseDto;
import com.minokuma.book.springboot.web.dto.PostsSaveRequestDto;
import com.minokuma.book.springboot.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto){
return postsService.save(requestDto);
}
@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);
}
}
테스트 코드
src/test/java
com.minokuma.book.springboot
web.dto
PostsApiControllerTest.java
package com.minokuma.book.springboot.web;
import com.minokuma.book.springboot.domain.posts.Posts;
import com.minokuma.book.springboot.domain.posts.PostsRepository;
import com.minokuma.book.springboot.web.dto.PostsSaveRequestDto;
import com.minokuma.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 registPosts() 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";
ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);
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 modifyPosts() throws Exception {
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);
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);
}
}
테스트 실행 결과
웹 콘솔 활성화
src/main/resources
application.properties
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
spring.h2.console.enabled=true
저장 후 Application 클래스의 main 메소드 실행해서
http://localhost:8080/h2-console/
H2 콘솔 접속 화면
JDBC URL
jdbc:h2:mem:testdb
테스트 쿼리 실행
테스트 데이터 삽입
insert into posts (
author, content, title
) values (
'author', 'content', 'title'
);
등록한 테스트 데이터 API 요청
http://localhost:8080/api/v1/posts/1
API 조회를 할 수 있는데, 정렬된 JSON을 보려면, 크롬 웹스토어에서 JSON 뷰어를 설치한다.
JSON 뷰어 설치
설치 후 API 요청 결과
'Project E > Project E 파트2' 카테고리의 다른 글
[E-2-5] JPA Auditing 으로 생성시간 및 수정시간 자동화하기 (0) | 2020.01.31 |
---|---|
[E-2-3] 스프링 부트에서 JPA로 데이터베이스 조작 (0) | 2020.01.30 |