[LG CNS AM Inspire CAMP 1기] Rest API (1)

니니지·2025년 2월 3일

LG CNS AM Inspire Camp 1기

목록 보기
31/47

INTRO

안녕하세요, 오늘은 프로젝트에서 Rest API 방식을 설정하였으며, 본 포스팅에서는 API 테스트 및 문서화하는 방법을 기록했습니다.

1. API Test

https://www.postman.com/
https://chromewebstore.google.com/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm?pli=1
https://insomnia.rest/download

- 목록 조회

- 등록


- 상세 조회

- 수정

- 삭제

2. API 문서화

- Swagger

https://swagger.io/

- SpringDoc

스프링 부트 프로젝트를 사용해 API 문서 생성을 자동화

- 의존성 추가 ⇒ build.gradle

// https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui
implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.8.4'

- 문서 확인

http://localhost:8080/swagger-ui/index.html ⇐ Swagger UI
http://localhost:8080/v3/api-docs ⇐ OpenAPI JSON Format
http://localhost:8080/v3/api-docs.yaml ⇐ YAML Format 다운로드

- 문서 경로 변경 ⇒ application.properties 파일에 설정 정보를 추가

springdoc.swagger-ui.path=/swagger-ui.html
springdoc.api-docs.path=/api-docs

- 문서에 포함할 RestController 지정

springdoc.packages-to-scan=board.controller, board.test
springdoc.paths-to-match=/v1, /api/**


- 설정 클래스를 생성

https://springdoc.org/#properties 를 참고하여 작성

package board.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;

@Configuration
public class SpringDocConfiguration {
    @Bean
    public OpenAPI openAPI() {
        return new OpenAPI().info(
            new Info()
                .title("SpringBoot Board REST API")
                .description("스프링 부트 기반의 게시판 REST API")
                .version("v1.0")
                .license(new License().name("Apache 2.0").url("http://myapiserver.com"))
            );
    }
}

- API 명세에 내용을 추가

package board.controller;

import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import com.fasterxml.jackson.databind.ObjectMapper;

import board.dto.BoardDto;
import board.dto.BoardFileDto;
import board.service.BoardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.servlet.http.HttpServletResponse;

@RestController
@RequestMapping("/api")
public class RestApiBoardController {
    
    @Autowired
    private BoardService boardService;
    
    // 목록 조회
    @Operation(summary = "게시판 목록 조회", description = "등록된 게시물 목록을 조회해서 반환합니다.")
    @GetMapping("/board")
    public List<BoardDto> openBoardList() throws Exception {
        return boardService.selectBoardList();
    }
    
    // 저장 처리
    @PostMapping(value = "/board", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public void insertBoard(@RequestParam("board") String boardData, MultipartHttpServletRequest request) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();
        BoardDto boardDto = objectMapper.readValue(boardData, BoardDto.class);
        boardService.insertBoard(boardDto, request);
    }
    
    // 상세 조회
    @Operation(summary = "게시판 상세 조회", description = "게시물 아이디와 일치하는 게시물의 상세 정보를 조회해서 반환합니다.")
    @Parameter(name = "boardIdx", description = "게시물 아이디", required = true)
    @GetMapping("/board/{boardIdx}")
    public BoardDto openBoardDetail(@PathVariable("boardIdx") int boardIdx) throws Exception {
        return boardService.selectBoardDetail(boardIdx);        
    }
    
    // 수정 처리
    @PutMapping("/board/{boardIdx}")
    public void updateBoard(@PathVariable("boardIdx") int boardIdx, @RequestBody BoardDto boardDto) throws Exception {
        boardDto.setBoardIdx(boardIdx);
        boardService.updateBoard(boardDto);
    }    
    
    // 삭제 처리
    @DeleteMapping("/board/{boardIdx}")
    public void deleteBoard(@PathVariable("boardIdx") int boardIdx) throws Exception {
        boardService.deleteBoard(boardIdx);
    }
    
    // 첨부파일 다운로드 
    @GetMapping("/board/file")
    public void downloadBoardFile(@RequestParam("idx") int idx, @RequestParam("boardIdx") int boardIdx, HttpServletResponse response) throws Exception {
        BoardFileDto boardFileDto = boardService.selectBoardFileInfo(idx, boardIdx);
        if (ObjectUtils.isEmpty(boardFileDto)) {
            return;
        }
        
        Path path = Paths.get(boardFileDto.getStoredFilePath());
        byte[] file = Files.readAllBytes(path);
        
        response.setContentType("application/octet-stream");
        response.setContentLength(file.length);
        response.setHeader("Content-Disposition", "attachment; fileName=\"" + URLEncoder.encode(boardFileDto.getOriginalFileName(), "UTF-8") + "\";");
        response.setHeader("Content-Transfer-Encoding", "binary");
        response.getOutputStream().write(file);
        response.getOutputStream().flush();
        response.getOutputStream().close();
    }
}

- BoardApplication.java 파일에 HiddenHttpMethodFilter를 빈으로 추가

package board;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.filter.HiddenHttpMethodFilter;

@SpringBootApplication
public class BoardApplication {
    
	public static void main(String[] args) {
	    SpringApplication.run(BoardApplication.class, args);
	}

	@Bean
	public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
	    return new HiddenHttpMethodFilter();
	}
}

3. 상세 조회 기능에 조회 결과 여부에 따라 상태 코드를 반환하도록 수정

- HTTP 상태 코드

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status

- RestApiBoardController 수정 ⇒ ResponseEntity를 사용하도록 변경

 // 상세 조회
    @Operation(summary = "게시판 상세 조회", description = "게시물 아이디와 일치하는 게시물의 상세 정보를 조회해서 반환합니다.")
    @Parameter(name = "boardIdx", description = "게시물 아이디", required = true)
    @GetMapping("/board/{boardIdx}")
    public ResponseEntity<Object> openBoardDetail(@PathVariable("boardIdx") int boardIdx) throws Exception {
        BoardDto boardDto = boardService.selectBoardDetail(boardIdx);
        if (boardDto == null) {
            Map<String, Object> result = new HashMap<>();
            result.put("code", HttpStatus.NOT_FOUND.value());
            result.put("name", HttpStatus.NOT_FOUND.name());
            result.put("message", "게시판 번호 " + boardIdx + "와 일치하는 게시물이 존재하지 않습니다.");
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(result);
        } else {
            return ResponseEntity.status(HttpStatus.OK).body(boardDto);
        }
    }

- BoardServiceImpl 수정 ⇒ 게시판 정보가 존재하는 경우에만 첨부 파일을 조회하도록 수정

@Override
    public BoardDto selectBoardDetail(int boardIdx) {
        boardMapper.updateHitCnt(boardIdx);

        // int i = 10 / 0;
        
        BoardDto boardDto = boardMapper.selectBoardDetail(boardIdx);
        if (boardDto != null) {
            List<BoardFileDto> boardFileInfoList = boardMapper.selectBoardFileList(boardIdx);
            boardDto.setFileInfoList(boardFileInfoList);
        }
        return boardDto;
    }

- 확인

존재하는 게시물을 상세 조회

C:\Users\myanj> curl -X GET http://localhost:8080/api/board/8 -v

존재하지 않는 게시물 조회

C:\Users\myanj> curl -X GET http://localhost:8080/api/board/88 -v

3. 용도에 맞게 DTO 세분화

- 글쓰기 화면에서 입력을 제한할 수 있도록 입력값을 저장할 DTO를 추가 ⇒ BoardInsertRequest

profile
지니니

0개의 댓글