SpringBoot Part4 (5)
ํ์ต๋ชฉํ
- ์ํฐํฐ (์ฃผ๋ฌธ๊ด๋ฆฌ vs ๊ฒ์ํ)
- API ๊ฐ๋ฐ (JUnit, Lombok, OSIV)
- API ๋ฌธ์ํ (๋ ์คํธ ๋ฅ์ค, ์ค์จ๊ฑฐ)
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
<!-- ๋ ์คํธ ๋ฅ์ค -->
</dependency>
์ฃผ๋ฌธ๊ด๋ฆฌ API ๊ฐ๋ฐ
- ์ฃผ๋ฌธ์์ฑ(POST)
- ์ฃผ๋ฌธ์กฐํ(GET)
- ๋ฆฌ์คํธ(PAGING)
- ๋จ๊ฑด (ONE)
Service layer
converter
entity๋ฅผ Transaction ๋ฌถ์ ๋ฐ์ ๊น์ง ๋๊ณ ๋๊ฐ๋ ๊ฒ์ ์ข์ง ์๋ค. entity๋ RDB์ ์ด๋ ์ ๋ ํต์ ์ ํ๊ณ ์๋ ๊ฐ์ฒด์ด๊ธฐ ๋๋ฌธ์ entity๊ฐ ๋ฌถ์ ๋ฐ์ผ๋ก ๋น ์ ธ๋๊ฐ๋ฉด ์์์น ๋ชปํ ๊ณณ์์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค. (OSIV)
→ dto ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ ํต์ ํ๋ค.
Orderservice.class
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private OrderConverter orderConverter;
@Transactional
public String save(OrderDto orderDto) {
// 1. dto -> entity ๋ณํ (์ค์์)
Order order = orderConverter.convertOrder(orderDto);
// 2. orderRepository.save(entity) -> ์์ํ
Order entity = orderRepository.save(order);
// 3. ๊ฒฐ๊ณผ ๋ฐํ
return entity.getUuid(); // entity ๊ฐ์ฒด๊ฐ ์๋ id๋ง ๋ฐ
}
@Transactional
public OrderDto findOne(String uuid){
// 1. ์กฐํ๋ฅผ ์ํ ํค๊ฐ ์ธ์๋ก ๋ฐ๊ธฐ
// 2. orderRepository.findByIdd(uuid) -> ์กฐํ(์์ํ๋ ์ํฐํฐ)
OrderDto orderDto = orderRepository.findById(uuid)
.map(orderConverter::convertOrderDto) // 3. entity -> dto
.orElseThrow(() -> new IllegalArgumentException("์ฃผ๋ฌธ์ ์ฐพ์ ์ ์์ต๋๋ค."));
return orderDto;
}
@Transactional
public Page<OrderDto> findAll(Pageable pageable){
return orderRepository.findAll(pageable) //pageable ๊ฐ์ฒด๋ฅผ ์ด์ฉํ๋ฉด ์์ฝ page ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋ผ ์ ์๋ค.
.map(orderConverter::convertOrderDto);
}
}
page ๊ฐ์ฒด
Controller layer
๊ณตํต ์๋ต ํผ
@Getter
@Setter
@NoArgsConstructor
public class ApiResponse<T> {
private int statusCode;
private T data;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime serverDataTime;
public ApiResponse(int statusCode, T data) {
this.statusCode = statusCode;
this.data = data;
this.serverDataTime = LocalDateTime.now();
}
public static <T> ApiResponse<T> ok(T data){
return new ApiResponse<>(200,data);
}
public static <T> ApiResponse<T> fail(int statusCode,T data){
return new ApiResponse<>(statusCode,data);
}
}
OrderController.class
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@ExceptionHandler(NotFoundException.class) // ๊ณตํต ์์ธ์ฒ๋ฆฌ
public ApiResponse<String> notFoundHandler(NotFoundException e) {
return ApiResponse.fail(404, e.getMessage());
}
@ExceptionHandler(Exception.class)
public ApiResponse<String> internalServerErrorHandler(NotFoundException e) {
return ApiResponse.fail(500, e.getMessage());
}
@PostMapping("/orders")
public ApiResponse<String> save(@RequestBody OrderDto orderDto) {
String uuid = orderService.save(orderDto);
return ApiResponse.ok(uuid);
}
@GetMapping("/orders/{uuid}")
public ApiResponse<OrderDto> getOne(@PathVariable String uuid) throws NotFoundException {
OrderDto orderDto = orderService.findOne(uuid);
return ApiResponse.ok(orderDto);
}
@GetMapping("/orders")
public ApiResponse<Page<OrderDto>> getAll(Pageable pageable){
Page<OrderDto> all = orderService.findAll(pageable);
return ApiResponse.ok(all);
}
}
controller test
์ดํ๋ฆฌ์ผ์ด์ ์ ๋์ฐ์ง์๊ณ ํ ์คํธ ์ฝ๋๋ก ํ์ธ
package com.example.kdtspringjpa.order.controller;
...
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@AutoConfigureMockMvc
@SpringBootTest
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private OrderService orderService;
@Autowired
private ObjectMapper objectMapper;
@Test
void saveCallTest() throws Exception {
// When
OrderDto orderDto = OrderDto.builder()
.uuid(UUID.randomUUID().toString())
.memo("๋ฌธ์ ๋ณด๊ด ํด์ฃผ์ธ์.")
.orderDatetime(LocalDateTime.now())
.orderStatus(OrderStatus.OPENED)
.memberDto(
MemberDto.builder()
.name("์ฅ์์ง")
.nickName("young")
.address("์์ง๋ค ์ง")
.age(28)
.description("---")
.build()
)
.orderItemDtos(List.of(
OrderItemDto.builder()
.price(1000)
.quantity(100)
.itemDtos(List.of(
ItemDto.builder()
.type(ItemType.FOOD)
.chef("๋ฐฑ์ข
์")
.price(1000)
.build()
))
.build()
))
.build();
// When // Then
mockMvc.perform(post("/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(orderDto)))
.andExpect(status().isOk())
.andDo(print());
}
@Test
void getAll() throws Exception {
mockMvc.perform(get("/orders")
.param("page", String.valueOf(0))
.param("size",String.valueOf(10))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(print());
}
}
- ์๋ต ํ๊ธ์ด ๊นจ์ง ๊ฒฝ์ฐ : yaml์ server encoding ์ถ๊ฐ
server:
servlet:
encoding:
charset: UTF-8
enabled: true
force: true
API ๋ฌธ์ํ - RestDocs
mockmvc๋ฅผ ์ด์ฉํด์ test code ๊ธฐ๋ฐ์ผ๋ก API ๋ฌธ์ ๋ง๋ค๊ธฐ
@Test
void saveCallTest() throws Exception {
// When
OrderDto orderDto = OrderDto.builder()
.uuid(UUID.randomUUID().toString())
.memo("๋ฌธ์ ๋ณด๊ด ํด์ฃผ์ธ์.")
.orderDatetime(LocalDateTime.now())
.orderStatus(OrderStatus.OPENED)
.memberDto(
MemberDto.builder()
.name("์ฅ์์ง")
.nickName("young")
.address("์์ง๋ค ์ง")
.age(28)
.description("---")
.build()
)
.orderItemDtos(List.of(
OrderItemDto.builder()
.price(1000)
.quantity(100)
.itemDtos(List.of(
ItemDto.builder()
.type(ItemType.FOOD)
.chef("๋ฐฑ์ข
์")
.price(1000)
.build()
))
.build()
))
.build();
// When // Then
mockMvc.perform(post("/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(orderDto)))
.andExpect(status().isOk())
.andDo(print())
.andDo(document("order-save", // ํ์ผ๋ช
requestFields(
fieldWithPath("uuid").type(JsonFieldType.STRING).description("UUID"),
fieldWithPath("orderDatetime").type(JsonFieldType.STRING).description("orderDatetime"),
fieldWithPath("orderStatus").type(JsonFieldType.STRING).description("orderStatus"),
fieldWithPath("memo").type(JsonFieldType.STRING).description("memo"),
fieldWithPath("memberDto").type(JsonFieldType.OBJECT).description("memberDto"),
fieldWithPath("memberDto.id").type(JsonFieldType.NULL).description("memberDto.id"),
fieldWithPath("memberDto.name").type(JsonFieldType.STRING).description("memberDto.name"),
fieldWithPath("memberDto.nickName").type(JsonFieldType.STRING).description("memberDto.nickName"),
fieldWithPath("memberDto.age").type(JsonFieldType.NUMBER).description("memberDto.age"),
fieldWithPath("memberDto.address").type(JsonFieldType.STRING).description("memberDto.address"),
fieldWithPath("memberDto.description").type(JsonFieldType.STRING).description("memberDto.desc"),
fieldWithPath("orderItemDtos[]").type(JsonFieldType.ARRAY).description("memberDto.desc"),
fieldWithPath("orderItemDtos[].id").type(JsonFieldType.NULL).description("orderItemDtos.id"),
fieldWithPath("orderItemDtos[].price").type(JsonFieldType.NUMBER).description("orderItemDtos.price"),
fieldWithPath("orderItemDtos[].quantity").type(JsonFieldType.NUMBER).description("orderItemDtos.quantity"),
fieldWithPath("orderItemDtos[].itemDtos[]").type(JsonFieldType.ARRAY).description("orderItemDtos.itemDtos"),
fieldWithPath("orderItemDtos[].itemDtos[].price").type(JsonFieldType.NUMBER).description("orderItemDtos.itemDtos"),
fieldWithPath("orderItemDtos[].itemDtos[].stockQuantity").type(JsonFieldType.NUMBER).description("orderItemDtos.itemDtos"),
fieldWithPath("orderItemDtos[].itemDtos[].type").type(JsonFieldType.STRING).description("orderItemDtos.itemDtos"),
fieldWithPath("orderItemDtos[].itemDtos[].chef").type(JsonFieldType.STRING).description("orderItemDtos.itemDtos")
),
responseFields(
fieldWithPath("statusCode").type(JsonFieldType.NUMBER).description("์ํ์ฝ๋"),
fieldWithPath("data").type(JsonFieldType.STRING).description("๋ฐ์ดํฐ"),
fieldWithPath("serverDataTime").type(JsonFieldType.STRING).description("์๋ต์๊ฐ")
)
));
}
- ๋ฌธ์์ API๊ฐ ์ผ์นํ์ง์์๋ ์ค๋ฅ๋ฅผ ๋์์ค ๋ฌธ์์ ์ต์ ํ๋ฅผ ์ ์งํ ์ ์๋ค๋ ์ฅ์ ์ด ์๋ค. (์คํ๊ณผ ์ผ์นํ๋์ง)
target > generated-snippets ํด๋์ ๋ฌธ์๊ฐ ์์ฑ๋ ๊ฒ์ ํ์ธ ๊ฐ๋ฅ.
๊ฐ๋ ์ฑ์ด ๋จ์ด์ง๋ ๋ฌธ์๋ฅผ ํ์ธํ ์์๋ค.
โ
์๋จ ๊ฐ์ด๋์ ๋ฐ๋ผ intelliJ ํ๋ฌ๊ทธ์ธ์ ์ค์นํ๋ฉด ํจ์ฌ ๋ณด๊ธฐ ์ข์์ง๋ค!
IntelliJ์ ๋์ ์์ด ํ๋ฌ๊ทธ์ธ์ ์ถ๊ฐํ์ฌ ํ์ผ์ ๋ง๋ค์ด๋ณด์.
- ํ๋ฌ๊ทธ์ธ ์ถ๊ฐ
<plugin>
<!-- for Spring REST Docs -->
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>2.0.0</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>${spring-restdocs.version}</version>
</dependency>
</dependencies>
</plugin>
2. index.adoc ํ์ผ์ ํตํด ์ํ๋ ํผ์ ๋ง๋ค ์ ์๋ค.
:hardbreaks:
ifndef::snippets[]
:snippets: ../../../target/generated-snippets
endif::[]
3. ํ๋ฌ๊ทธ์ธ ์คํ
target > generated-docs ํด๋์ html ๋ฌธ์๊ฐ ์์ฑ๋ ๊ฒ์ ํ์ธ ํ ์์๋ค.
API ๋ฌธ์ํ - Swagger
๋จ์ : ์ด์ ์ฝ๋์์ docment ํ๋ฅผ ํ๊ธฐ์ํ ๋ฌธ์ ์ด๋ ธํ ก์ด์ ์ด ์ถ๊ฐ๋๋ค.
→ ์ ์ง๋ณด์๊ฐ ํ๋ค๋ค. ํ ์คํธ ์ฝ๋๋ฅผ ์งํํ์ง ์๊ณ ๋ฌธ์ํ๋ฅผ ํ๋ค๋ณด๋ ๊ฒ์ฆ์ด ์ฌ๋๋ก ๋์ง์์ ๋ฌธ์๊ฐ ์์ฑ๋ ์ ์๋ค.
์ถ์ฒ - backend dev course ๊ฐํ๊ตฌ๋
'Back-end ๋ฐ๋ธ์ฝ์ค > week 08 TIL (Jpa)' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[TIL] 221208 - JPA : SpringDataJPA (0) | 2022.12.14 |
---|---|
[TIL] 221207 - JPA : ์ฐ๊ด๊ด๊ณ ๋งคํ, ๊ณ ๊ธ ๋งคํ, ํ๋ก์ (0) | 2022.12.14 |
[TIL] 221206 - JPA : ์์์ฑ ์ปจํ ์คํธ (0) | 2022.12.07 |
[TIL] 221205 - JPA : JPA ์๊ฐ (0) | 2022.12.07 |
๋๊ธ