- 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 만들기
- Controller와 Service에서 사용할 Dto 클래스 생성
- Controller -> Service 순으로 작성
- 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
- @RequiredArgsConstructor
- final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성
- 생성자로 Bean을 주입받아 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속해서 수정하는 번거로움을 해결하기 위함
- 해당 컨트롤러에 새로운 서비스를 추가하거나, 기존 컴포넌트를 제거하는 등의 상황이 발생해도 생성자 코드를 수정하지 않아도 된다.
- @RequiredArgsConstructor
@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);
}
}
H2 데이터베이스 웹 콘솔에서 확인하기
-
H2 데이터베이스 접근
-
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 |