Day4
๋ฑ๋ก/์์ /์กฐํ API ๋ง๋ค๊ธฐ
๐API๋ฅผ ๋ง๋ค๊ธฐ ์ํด ์ด 3๊ฐ์ ํด๋์ค๊ฐ ํ์ํฉ๋๋ค
Request ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ DTO
API ์์ฒญ์ ๋ฐ์ Controller
ํธ๋์ญ์
, ๋๋ฉ์ธ ๊ธฐ๋ฅ ๊ฐ์ ์์๋ฅผ ๋ณด์ฅํ๋ Service
โ
service๋ ํธ๋์ญ์
, ๋๋ฉ์ธ ๊ฐ์ ์์๋ฅผ ๋ณด์ฅ
Web Layer, Service Layer, Repository Layer, Dtos, Domain Model
Web Layer
ํํ ์ฌ์ฉํ๋ ์ปจํธ๋กค๋ฌ์ JSP/Freemarker ๋ฑ์ ๋ทฐ ํ
ํ๋ฆฟ ์์ญ
์ด์ธ์๋ @Filter, ์ธํฐ์
ํฐ, ์ปจํธ๋กค๋ฌ ์ด๋๋ฐ์ด์ค(@ControllerAdvice)๋ฑ ์ธ๋ถ ์์ฒญ๊ณผ ์๋ต์ ๋ํ ์ ๋ฐ์ ์ธ ์์ญ์ ์ด์ผ๊ธฐํฉ๋๋ค
Service Layer
@Service์ ์ฌ์ฉ๋๋ ์๋น์ค ์์ญ
์ผ๋ฐ์ ์ผ๋ก Controller์ Dao์ ์ค๊ฐ ์์ญ์์ ์ฌ์ฉ
@Transactional์ด ์ฌ์ฉ๋์ด์ผ ํ๋ ์์ญ
Repository Layer
db์ ๊ฐ์ด ๋ฐ์ดํฐ ์ ์ฅ์์ ์ ๊ทผํ๋ ์์ญ
๊ธฐ์กด์ DAO๋ผ๊ณ ์๊ฐํ๋ฉด ๋จ
Dtos
Dto(Data Transfer Object)๋ ๊ณ์ธต ๊ฐ์ ๋ฐ์ดํฐ ๊ตํ์ ์ํ ๊ฐ์ฒด๋ฅผ ์ด์ผ๊ธฐํ๋ฉฐ
Domain Model
๋๋ฉ์ธ์ด๋ผ๊ณ ๋ถ๋ฆฌ๋ ๊ฐ๋ฐ ๋์์ ๋ชจ๋ ์ฌ๋์ด ๋์ผํ ๊ด์ ์์ ์ดํดํ ์ ์๊ณ ๊ณต์ ํ ์ ์๋๋ก ๋จ์ํ ์ํจ ๊ฒ
@Entity๊ฐ ์ฌ์ฉ๋ ์์ญ์ ๋๋ฉ์ธ ๋ชจ๋ธ
๋ฌด์กฐ๊ฑด db์ ํ
์ด๋ธ๊ณผ ๊ด๊ณ๊ฐ ์์ด์ผ๋ง ํ๋ ๊ฒ์ ์๋
โ
web, service, repository, dto, domain์ด 5๊ฐ์ง์ ๋ ์ด์ด์์ ๋น์ง๋์ค๋ก์ง์ ์ฒ๋ฆฌํ๋ ๊ณณ์ Domain
โ
์๋น์ค๋ ํธ๋์ญ์
๊ณผ ๋๋ฉ์ธ ๊ฐ์ ์์๋ง ๋ณด์ฅ
๐์ฝ๋ ์์ฑ
/src/main/java/com/kyu/book/springboot/service/posts/PostsService.class
package com.kyu.book.springboot.service.posts;
import com.kyu.book.springboot.domain.posts.PostsRepository;
import com.kyu.book.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional
public Long save(PostsSaveRequestDto requestDto){
return postsRepository.save(requestDto.toEntity()).getId();
}
}
์คํ๋ง์์ Bean์ ์ฃผ์
๋ฐ๋ ๋ฐฉ์
โ
๊ฐ์ฅ ๊ถ์ฅํ๋ ๋ฐฉ์ : ์์ฑ์ โ @RequiredArgsConstructor๋ก ํด๊ฒฐ
@RequiredArgsConstructor๋ final์ด ์ ์ธ๋ ๋ชจ๋ ํ๋๋ฅผ ์ธ์๊ฐ์ผ๋ก ์์ฑํ๋ ์์ฑ์
Controller์ Service์์ ์ฌ์ฉํ Dtoํด๋์ค
src/main/java/com/kyu/book/springboot/web/dto/PostsSaveRequestDto.java
package com.kyu.book.springboot.web.dto;
import com.kyu.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ํด๋์ค๋ db์ ๋ง๋ฟ์ ํต์ฌ ํด๋์ค, Entityํด๋์ค๋ฅผ ๊ธฐ์ค์ผ๋ก ํ
์ด๋ธ์ด ์์ฑ๋๊ณ , ์คํค๋ง๊ฐ ๋ณ๊ฒฝ๋จ
โ
View Layer์ DB Layer์ ์ญํ ๋ถ๋ฆฌ๋ฅผ ์ฒ ์ ํ๊ฒ ํ๋ ๊ฒ์ด ์ข์ : Controller์์ ๊ฒฐ๊ณผ๊ฐ์ผ๋ก ์ฌ๋ฌ ํ
์ด๋ธ์ ์กฐ์ธํด์ ์ค์ผํ ๊ฒฝ์ฐ๊ฐ ๋น๋ฒํ๊ธฐ ๋๋ฌธ์
โEntity ํด๋์ค์ Controller์์ ์ธ Dto๋ ๋ถ๋ฆฌํด์ ์ฌ์ฉํ๋ผ
Entityํด๋์ค๋ฅผ ํ
์คํธํ๋ ํ
์คํธ ์ฝ๋
src/test/java/com/kyu/book/springboot/web/PostsApiControllerTest.java
package com.kyu.book.springboot.web;
import com.kyu.book.springboot.domain.posts.Posts;
import com.kyu.book.springboot.domain.posts.PostsRepository;
import com.kyu.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.AssertionsForClassTypes.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 postEnroll() 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์ ์ฌ์ฉํ๋ผ
์์ / ์กฐํ๊ธฐ๋ฅ
src/main/java/com/kyu/book/springboot/web/PostsApiController.java
package com.kyu.book.springboot.web;
import com.kyu.book.springboot.service.posts.PostsService;
import com.kyu.book.springboot.web.dto.PostsResponseDto;
import com.kyu.book.springboot.web.dto.PostsSaveRequestDto;
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/main/java/com/kyu/book/springboot/web/dto/PostsResponseDto.class
package com.kyu.book.springboot.web.dto;
import com.kyu.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();
}
}
src/main/java/com/kyu/book/springboot/web/PostsUpdateRequestDto.class
package com.kyu.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;
}
}
src/main/java/com/kyu/book/springboot/domain/posts/Posts ์ ์ด๊ฑฐ ์ถ๊ฐ
public void update(String title, String content){
this.title = title;
this.content = content;
}
src/main/java/com/kyu/book/springboot/service/posts/PostsService ์ ์ด๊ฑฐ ์ถ๊ฐ
@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);
}
update ๊ธฐ๋ฅ์์ db์ ์ฟผ๋ฆฌ๋ฌธ์ ์๋ ๋ฆผ โ JPA์ ์์์ฑ ์ปจํ
์คํธ ๋๋ฌธ์ ํ์ ์์
์์์ฑ ์ปจํ
์คํธ : ์ํฐํฐ๋ฅผ ์๊ตฌ ์ ์ฅํ๋ ํ๊ฒฝ
ใดJPA์ ํต์ฌ ๋ด์ฉ์ ์ํฐํฐ๊ฐ ์์์ฑ ์ปจํ
์คํธ์ ํฌํจ๋์ด ์๋ ์๋๋๋ก ๊ฐ๋ฆผ
JPA์ ์ํฐํฐ ๋งค๋์ ๊ฐ ํ์ฑํ๋ ์ํ(JPA์ฌ์ฉ์ ๊ธฐ๋ณธ ์ต์
) ์, ํธ๋์ญ์
์์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ฉด ์ด ๋ฐ์ดํฐ๋ ์์์ฑ ์ปจํ
์คํธ๊ฐ ์ ์ง๋ ์ํ์ด๋ค
์ด ์ํ์์ ํด๋น ๋ฐ์ดํฐ์ ๊ฐ์ ๋ณ๊ฒฝํ๋ฉด ํธ๋์ญ์
์ด ๋๋ ๋ ํ
์ด๋ธ์ ๋ณ๊ฒฝ๋ฌธ์ ๋ฐ์
์ฆ, Entity ๊ฐ์ฒด์ ๊ฐ๋ง ๋ณ๊ฒฝํ๋ฉด ๋ณ๋๋ก Update์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆด ํ์๊ฐ ์๋ค = ๋ํฐ ์ฒดํน
update๋ฅผ ์ถ๊ฐํ์ผ๋ ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑ
src/test/java/com/kyu/book/springboot/web/PostsApiControllerTest ์ ์ถ๊ฐ
@Test
public void postUpdates() 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);
}
local์์๋ db๋ฅผ h2๋ฅผ ์ฌ์ฉํ๊ณ ๋ฉ๋ชจ๋ฆฌ์์ ์คํํ๊ธฐ ๋๋ฌธ์ ์ง์ ์ ๊ทผํ๋ ค๋ฉด ์น ์ฝ์์ ์ฌ์ฉํด์ผํจ
application-properties์ ๋ค์์ ์ถ๊ฐ
spring.h2.console.enabled=true
JDBC URL ๋ถ๋ถ์ jdbc:h2:mem:testdb ๋ก ๋ณ๊ฒฝํ, connect๋ฒํผ ํด๋ฆญ
insert into posts(author, content, title) values ('author', 'content', 'title'); ์ ์ง์ด ๋ฃ๊ณ run