๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Back-end ๋ฐ๋ธŒ์ฝ”์Šค/week 08 TIL (Jpa)

[TIL] 221209 - JPA : REST-API, API ๋ฌธ์„œํ™”

by young-ji 2022. 12. 20.

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)
    1. ๋ฆฌ์ŠคํŠธ(PAGING)
    2. ๋‹จ๊ฑด (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์˜ ๋„์›€ ์—†์ด ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด๋ณด์ž.

  1. ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€
<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 ๊ฐ•ํ™๊ตฌ๋‹˜

 

๋Œ“๊ธ€