be_better

[Springboot] API를 작성하는 다양한 방법 본문

Springboot/이론

[Springboot] API를 작성하는 다양한 방법

NiceTry 2023. 10. 29. 17:10

목차

    GET API 만들기

    GET API는 웹 애플리케이션 서버에서 값을 가져올 때 사용하는 API이다.

     

    controller 패키지 안에 GetController 생성후 코드 작성

    @RequestMapping으로 구현하기

    GetController.java
    package com.example.hello.controller;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/api/v1/get-api")
    public class GetController {
    
        // http://localhost:9090/api/v1/get-api/hello
        @RequestMapping(value = "/hello", method = RequestMethod.GET)
        public String getHello() {
            return "Hello World";
        }
    }

     

    @RequestMapping을 설정하면 내부에 선언한 메서드의 URL 리소스 앞에 @RequestMapping의 값이 공통 값으로 추가된다.  어노테이션을 별다른 설정 없이 선언하면 HTTP의 모든 요청을 받는다. 그러나 GET 형식의 요청만 받기 위해서는 어노테이션에 별도 설정이 필요하다.

     

    @RequestMapping 어노테이션의 method 요소의 값을 RequestMethod.GET으로 설정하면 요청 형식을 GET으로만 설정할 수 있다.

     

    postman으로 확인한 결과

    @RequestMapping 어노테이션의 method 요소를 합친 어노테이션 4개 @GetMapping, @PostMapping, @PutMapping, @DeleteMapping 를 사용하게 된다.

     

    매개변수가 없는 GET 메서드 구현

    별도의 매개변수 없이 GET API를 구현하는 경우 다음과 같이 메서드를 작성할 수 있다.

        // http://localhost:9090/api/v1/get-api/name
        @GetMapping(value = "/name")
        public String getName() {
            return "Flature";
        }

     

    @PathVariable을 활용한 GET 메서드 구현

    웹 통신의 기본 목적은 데이터를 주고받는 것이기 때문에 대부분 매개변수를 받는 메서드를 작성하게 된다. 매개변수를 받을 때 자주 쓰이는 방법 중 하나는 URL 자체에 값을 담아 요청하는 것이다.

        // http://localhost:9090/api/v1/get-api/variable1/{String 값}
        @GetMapping(value = "variable1/{variable}")
        public String getVariable1(@PathVariable String variable) {
            return variable;
        }

    value값의 {}로 표시된 위치의 값을 받아 요청하는 방법으로 값을 간단히 전달할 때 주로 사용하는 방법이며, GET 요청에서 많이 사용된다.

    메서드의 매개변수와 그 값을 연결하기 위해 @PathVariable 어노테이션을 명시하며, @GetMapping 어노테이션과 @PathVariable에 지정된 변수의 이름을 동일하게 맞춰야 한다.

    만약 @GetMapping 어노테이션에 지정한 변수의 이름과 메서드 매개변수의 이름을 동일하게 맞추기 어렵다면 @PathVariable 뒤에 괄호를 열어 @GetMapping 어노테이션의 변수명을 지정한다.

        // http://localhost:9090/api/v1/get-api/variable2/{String 값}
        @GetMapping(value = "variable1/{variable}")
        public String getVariable2(@PathVariable("variable") String var) {
            return var;
        }

     

    @RequestParam을 활용한 GET 메서드 구현

    URL에 '?'을 기준으로 우측에 '{키}={값}' 형태로 구성된 요청을 전송하는 방법이다. @RequestParam을 명시해 쿼리 값과 매핑하면 된다.

        // http://localhost:9090/api/v1/get-api/request1?name=value1&email=value2&organization=value3
        @GetMapping(value = "request1")
        public String getRequestParam1(
                @RequestParam String name,
                @RequestParam String email,
                @RequestParam String organization) {
    
            return name + " " + email + " " + organization;
        }

     

    만약 쿼리 스트링에 어떤 값이 들어올지 모른다면 Map 객체를 활용할 수도 있다.

        // http://localhost:9090/api/v1/get-api/request2?key1=value1&key2=value2
        @GetMapping(value = "request2")
        public String getRequestParam2(
                @RequestParam Map<String, String> param) {
    
            StringBuilder sb = StringBuilder();
            
            param.entrySet().forEach(
                    map -> sb.append(map.getKey() + " : " + map.getValue() + "\n"));
            return sb.toString()
        }

    이와 같은 방법은 회원가입을 할 때 꼭 필요한 정보가 아닌 '취미' 같은 항목을 처리할 때 사용한다.

     

    DTO 객체를 활용한 GET 메서드 구현

    DTO란 Data Transfer Object의 약자로, 다른 레이어 간의 데이터 교환에 활용된다. 간단히 말하면 각 클래스 및 인터페이스를 호출하면서 전달하는 매개변수로 사용되는 객체이다.

     

    DTO는 데이터를 교환하는 용도로만 사용하는 객체이기 때문에 DTO에는 별도의 로직이 포함되지 않는다.

     

    먼저 dto 패키지를 생성하고 그 안에 MemberDto를 생성한다.

    MemberDto.java
    package com.example.hello.dto;
    
    public class MemberDto {
        
        private String name;
        private String email;
        private String organization;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getEmail() {
            return email;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    
        public String getOrganization() {
            return organization;
        }
    
        public void setOrganization(String organization) {
            this.organization = organization;
        }
    
        @Override
        public String toString() {
            return "MemberDto{" +
                    "name='" + name + '\'' +
                    ", email='" + email + '\'' +
                    ", organization='" + organization + '\'' +
                    '}';
        }
    }

    feild 객체를 선언하고 Getter/Setter/toString() 메서드를 구현한다.

    마우스 오른쪽버튼을 클릭 -> Generate -> Getter/Setter/toString 을 선택 후 OK

     

    DTO 클래스에 선언된 필드는 컨트롤러의 메서드에서 쿼리 파라미터의 키와 매핑된다. 받아야 할 파라미터가 많을 경우에는 DTO 객체를 활용해 코드의 가독성을 높일 수 있다.

        // http://localhost:9090/api/v1/get-api/request3/name=value1&email=value2&organization=value3
        @GetMapping(value = "request3")
        public String getRequestParam3(MemberDto memberDto) {
            return memberDto.toString();
        }

     

    POST API 만들기

    POST API는 웹 애플리케이션을 통해 데이터베이스 등의 저장소에 리소스를 저장할 때 사용되는 API이다. URL의 경로나 파라미터에 변수를 넣어 요청했던 GET API와 달리 POST API에서는 저장하고자 하는 리소스나 값을 HTTP바디에 담아 서버에 전달하기 때문에 URI가 더 간단하다.

     

    controller패키지 안에 PostController 컨트롤러 클래스를 생성하고 진행한다.

     

    @RequestBody를 활용한 POST 메서드 구현

    일반적으로 POST 형식의 요청은 클라이언트가 서버에 리소스를 저장하는데 사용한다. 그로므로 클라이언트의 요청 트래픽에 값이 포함되어 있다. 즉, POST 요청에서는 리소스를 담기 위해 HTTP Body에 값을 넣어 전송한다.

     

    Body 영역에 작성되는 값을 일반적으로 JSON(JavaScript Object Notation) 형식으로 전송된다.

     

    @RequestBody와 Map을 활용한 메서드 구현
        // http://localhost:9090/api/v1/post-api/member
        @PostMapping(value = "/member")
        public String postMember(@RequestBody Map<String, Object> postData) {
            StringBuilder sb = new StringBuilder();
            
            postData.entrySet().forEach(map -> {
                sb.append(map.getKey() + " : " + map.getValue() + "\n");
            });
            
            return sb.toString();
        }

    @RequestBody라는 어노테이션은 HTTP의 Body 내용을 해당 어노테이션이 지정된 객체에 매핑하는 역할을 한다.

     

    Map 객체는 요청을 통해 어떤 값이 들어오게 될지 특정하기 어려울 때 주로 사용하지만, 요청 메시지에 들어갈 값이 정해져 있다면 DTO 객체를 매개변수로 삼아 작성할 수 있다.

    // http://localhost:9090/api/v1/post-api/member2
    @PostMapping(value = "/member2")
    public String postMemberDto(@RequestBody MemberDto memberDto) {
        return memberDto.toString();
    }

    이렇게 작성하면 MemberDto의 멤버 변수를 요청 메시지의 키와 매핑해 값을 가져온다.

     

    PUT API 만들기

    PUT API는 웹 애플리케이션 서버를 통해 데이터베이스 같은 저장소에 존재하는 리소스 값을 업데이트 하는 데 사용한다. 리소스를 서버에 전달하기 위해 HTTP Body를 활용해야 하기 때문에 컨트롤러 클래스를 구현하는 방법은 POST API와 거의 동일하다.

     

    @RequestBody를 활용한 PUT 메서드 구현

    // http://localhost:9090/api/v1/put-api/member
    @PutMapping(value = "/member")
    public String putMember(@RequestBody Map<String, Object> putData) {
        StringBuilder sb = new StringBuilder();
    
        putData.entrySet().forEach(map -> {
            sb.append(map.getKey() + " : " + map.getValue() + "\n");
        });
    
        return sb.toString();
    }

    POST API와 마찬가지로 서버에 어떤 값이 들어올지 모르는 경우에는 Map 객체를 활용해 값을 받을 수 있다.

     

    서버에 들어오는 요청에 담겨 있는 값이 정해져 있는 경우네는 DTO 객체를 활용해 구현한다.

    // http://localhost:9090/api/v1/put-api/member1
    @PutMapping(value = "/member1")
    public String putMemberDto1(@RequestBody MemberDto memberDto) {
        return memberDto.toString();
    }
    
    // http://localhost:9090/api/v1/put-api/member2
    @PutMapping(value = "/member2")
    public MemberDto putMemberDto2(@RequestBody MemberDto memberDto) {
        return memberDto;
    }

    return 타입을 MemberDto로 했을 경우에는 Header의 content-type이 application/json으로 설정되어있는 것을 확인할 수 있고 결과값이 json 형태로 되어있는 것도 확인할 수 있다.

     

    @ResponseEntity를 활용한 PUT 메서드 구현

    스프링 프레임워크에는 HttpEntity라는 클래스가 있는데 헤더(Header)와 Body로 구성된 HTTP 요청과 응답을 구성하는 역할을 수행한다. RequestEntity와 ResponseEntity는 HttpEntity를 상속받아 구현한 클래스인데 ReponseEntity는 서버에 들어온 요청에 대해 응답 데이터를 구성해서 전달할 수 있게 한다.

     

    ReponseEntity 클래스
    public class ResponseEntity<T> extends HttpEntity<T> {
        private final Object status;
        
        .. 생략 ..
    }

    이 클래스를 활용하면 응답 코드 변경은 물론 Header와 Body를 더욱 쉽게 구성할 수 있다.

     

    리턴 타입에 ResponseEntity를 적용한 Put 메서드 예시
    // http://localhost:9090/api/v1/put-api/member3
    @PutMapping(value = "/member3")
    public ResponseEntity<MemberDto> putMemberDto3(@RequestBody MemberDto memberDto) {
        return ResponseEntity
                .status(HttpStatus.ACCEPTED)
                .body(memberDto);
    }

    이와같이 status를 설정할 수 있는데 HttpStatus.ACCEPTED는 202의 응답 코드를 가지고 있고 수행 결과는 다음과 같다.

     

    DELETE API 만들기

    DELETE API는 웹 애플리케이션 서버를 거쳐 데이터베이스 등의 저장소에 있는 리소스를 삭제할 때 사용한다. 컨트롤러를 통해 값을 받는 단계에서는 간단한 값을 받기 때문에 GET 메서드와 같이 URI에 값을 넣어 요청을 받는 형식으로 구현된다.

     

    @PathVariable을 활용한 구현
    // http://localhost:9090/api/v1/delete-api/{String 값}
    @DeleteMapping(value = "/{variable}")
    public String DeleteVariable(@PathVariable String variable) {
        return variable;
    }

     

    @RequestParam을 활용한 구현
    // http://localhost:9090/api/v1/delete-api/request1?email=value
    @DeleteMapping(value = "/request1}")
    public String getRequestParam1(@PathVariable String email) {
        return "email : " + email;
    }

     

    [ 한걸음 더 ] REST API 명세를 문서화하는 방법 - Swagger

    API를 개발하면 명세를 관리해야 한다. 명세란 해당 API가 어떤 로직을 수행하는지 설명하고 이 로직을 수행하기 위해 어떤 값을 요청하며, 이에 따른 응답값으로는 무엇을 받을 수 있는지를 정리한 자료이다.

     

    API는 개발 과정에서 계속 변경되므로 작성한 명세 문서도 주기적인 업데이트가 필요한데 이 작업은 번거롭고 시간 또한 오래걸린다. 이 문제를 해결하기 위해 등장한것이 바로 Swagger라는 오픈소스 프로젝트이다.

     

    Swagger를 사용하기 위해서는 의존성부터 추가해야 한다.

     

    build.gradle에 의존성 추가
    dependeicies {
    	implementation 'io.springfox:springfox-swagger2:2.9.2'
    	implementation 'io.springfox:springfox-swagger-ui:2.9.2'
    }

     

    의존성을 적용하고 config라는 패키지를 만들고 그 안에 Swagger 클래스를 생성한다.

    @Configuration
    @EnableSwagger2
    public class SwaggerConfiguration {
    
        @Bean
        public Docket api() {
            return new Docket(DocumentationType.SWAGGER_2)
                    .apiInfo(apiInfo())
                    .select()
                    .apis(RequestHandlerSelectors.basePackage("com.example.hello"))
                    .paths(PathSelectors.any())
                    .build();
        }
    
        private ApiInfo apiInfo() {
            return new ApiInfoBuilder()
                    .title("Spring Boot Open API Test with Swagger")
                    .description("설명 부분")
                    .version("1.0.0")
                    .build();
        }
    }

    springboot 3.0.0버전 이상이 아닌 버전에서만 사용가능하다.

     

    앱을 실행한 후 웹 브라우저를 통해 http://localhost:9090/swagger-ui.html 로 접속하면 Swagger 페이지가 출력된다.

     

    Swagger를 통해 각 API들의 상세한 정보를 확인하기 위해 코드에 몇가지 설정을 추가할 수 있다.

     

    기존에 작성한 getRequestParam1 메서드에 설정 추가
    // http://localhost:9090/api/v1/get-api/request1?name=value1&email=value2&organization=value3
    @ApiOperation(value = "GET 메서드 예제", notes = "RequestParam을 활용한 GET Method")
    @GetMapping(value = "request1")
    public String getRequestParam1(
            @ApiParam(value = "이름", required = true) @RequestParam String name,
            @ApiParam(value = "이메일", required = true) @RequestParam String email,
            @ApiParam(value = "회사", required = true) @RequestParam String organization) {
    
        return name + " " + email + " " + organization;
    }
    • @ApiOperation: 대상 API의 설명을 작성하기 위한 어노테이션
    • @ApiParam: 매개변수에 대한 설명 및 설정을 위한 어노테이션. 메서드의 매개변수뿐 아니라 DTO 객체를 매개변수로 사용할 경우 DTO 클래스 내의 매개변수에도 정의할 수 있다.

    Swagger 페이지를 확인해보면

     

    [Try it out] 버튼을 통해 직접 서버와 통신하고 결과를 받아볼 수도 있다.

     

    [ 한걸음 더 ] 로깅 라이브러리 - Logback

    로깅(logging)이란 애플리케이션이 동작하는 동안 시스템의 상태나 동작 정보를 시간순으로 기록하는것을 의미한다.

    로깅은 개발 영역 중 '비기능 요구사항'에 속하나. 사용자나 고객에게 필요한 기능은 아니지만 로깅은 문제를 해결할 때 원인을 분석하는 데 꼭 필요한 요소이다.

     

    자바 진영에서 가장 많이 사용되는 로깅 프레임워크는 Logback이다. slf4j를 기반으로 구현되었으며, 과거에 사용되던 log4j에 비해 월등한 성능을 자랑한다.

     

    Logback 특징

    • 크게 5개의 로그 레벨(TRACE, DEBUG, INFO, WARN, ERROR)을 설정할 수 있다.
      • ERROR: 로직 수행 중에 시스템에 심각한 문제가 발생해서 애플리케이션의 작동이 불가능한 경우
      • WARN: 시스템 에러의 원인이 될 수 있는 경고 레벨
      • INFO: 애플리케이션의 상태 변경과 같은 정보 전달을 위해 사용
      • DEBUG: 애플리케이션의 디버깅을 위한 메시지를 표시하는 레벨
      • TRACE: DEBUG 레벨보다 더 상세한 메시지를 표현하기 위한 레벨
    • 실제 운영 환경과 개발 환경에서 각각 다른 출력 레벨을 설정해서 로그를 확인할 수 있다.
    • Logback의 설정 파일을 일정 시간마다 스캔해서 애플리케이션을 재가동하지 않아도 설정을 변경할 수 있다.
    • 별도의 프로그램 지원 없이도 자체적으로 로그 파일을 압축할 수 있다.
    • 저장된 로그 파일에 대한 보관 기간 등을 설정해서 관리할 수 있다.

     

    Logback 설정

    Logback을 사용하기 위한 설정 파일은 클래스패스(classpath)에 있는 설정 파일을 자동으로 참조하므로 리소스 폴더 안에 생성한다. 파일명의 경우 스프링 부트에서는 logback-spring.xml 로 지정한다.

     

    Logback 설정 파일 예시
    <?xml version="1.0" encoding="UTF-8" ?>
    <configuration>
        <property name="LOG_PATH" value="./logs"/>
    
        <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
            <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                <level>INFO</level>
            </filter>
            <encoder>
                <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>
            </encoder>
        </appender>
    
        <appender name="INFO_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                <level>INFO</level>
            </filter>
            <file>${LOG_PATH}/info.log</file>
            <append>true</append>
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>${LOG_PATH}/info_${type}.%d{yyyy-MM-dd}.gz</fileNamePattern>
                <maxHistory>30</maxHistory>
            </rollingPolicy>
            <encoder>
                <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>
            </encoder>
        </appender>
    
        <root level="INFO">
            <appender-ref ref="console"/>
            <appender-ref ref="INFO_LOG"/>
        </root>
    </configuration>

     

    Appender 영역

    Appender 영역은 로그의 형태를 설정하고 어떤 방법으로 출력할지를 설정하는 곳이다.

     

    Logback의 설정 파일을 이용하면 각 구현체를 등록해서 로그를 원하는 형식으로 출력할 수 있다. Appender의 대표적인 구현체는 다음과 같다.

    • ConsoleAppender: 콘솔에 로그를 출력
    • FileAppender: 파일에 로그를 저장
    • RollingFileAppender: 여러 개의 파일을 순회하면서 로그를 저장
    • SMTPAppender: 메일로 로그를 전송
    • DBAppender: 데이터베이스에 로그를 저장

    로그를 어떤 방식으로 저장할지 지정하는 방법은 appender 요소의 class 속성에 각 구현체를 정의하는 방법이 있다. 그리고 하단에 filter요소로 각 Appender가 어떤 레벨로 로그를 기록하는지 지정한다.

     

    다음으로 encoder 요소를 통해 로그의 표현 형식을 패턴(pattern)으로 정의한다.

     

    대표적인 패턴
    • %Logger{length}: 로거의 이름
    • %-5level: 로그 레벨. -5는 출력 고정폭의 값
    • %msg(%message): 로그 메시지
    • %d: 로그 기록 시간
    • %p: 로깅 레벨
    • %F: 로깅이 발생한 애플리케이션 파일명
    • %M: 로깅이 발생한 메서드 이름
    • %I: 로깅이 발생한 호출지의 정보
    • %thread: 현재 스레드명
    • %t: 로깅이 발생한 스레드명
    • %c: 로깅이 발생한 카테고리
    • %C: 로깅이 발생한 클래스명
    • %m: 로그 메시지
    • %n: 줄바꿈
    • %r: 애플리케이션 실행 후 로깅이 발생한 시점까지의 시간
    • %L: 로깅이 발생한 호출 지점의 라인 수

     

    로그 형식 예
    <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>

     

    Root 영역

    설정 파일에 정의된 Appender를 활용하려면 Root 영역에서 Appender를 참조해서 로깅 레벨을 설정한다. 특정 패키지에 대해 다른 로깅 레벨을 설정하고 싶다면 root 대신 logger를 사용할 수 있다.

        <root level="INFO">
            <appender-ref ref="console"/>
            <appender-ref ref="INFO_LOG"/>
        </root>
        
    또는
        
        <logger name="com.example.hello.controller" lever="DEBUG" additivity="false">
            <appender-ref ref="console"/>
            <appender-ref ref="INFO_LOG"/>
        </logger>

    logger 요소의 name 속성에는 패키지 단위로 로깅이 적용될 범위를 지정하고 level 속성으로 로그 레벨을 지정한다. additivity 속성은 앞에서 지정한 패키지 범위에 하위 패키지를 포함할지 여부를 결정한다. 기본값은 true이며, 이 경우 하위 패키지를 모두 포함한다.

     

    Logback 적용하기

    Logback은 출력할 메시지를 Appender에게 전달할 Logger 객체를 각 클래스에 정의해서 사용한다.

     

    GetController의 LOGGER 전역 변수로 Logger 객체를 정의하고 Logger는 LoggerFactory를 통해 객체를 생성한다. 이때 클래스의 이름을 함께 지정해서 클래스의 정보를 Logger에서 가져가게 한다.

    @RestController
    @RequestMapping("/api/v1/get-api")
    public class GetController {
        
        private final Logger LOGGER = LoggerFactory.getLogger(GetController.class);
    
        // http://localhost:9090/api/v1/get-api/hello
        @RequestMapping(value = "/hello", method = RequestMethod.GET)
        public String getHello() {
            LOGGER.info("getHello 메서드가 호출되었습니다.");
            return "Hello World";
        }
    
        // http://localhost:9090/api/v1/get-api/name
        @GetMapping(value = "/name")
        public String getName() {
            LOGGER.info("getName 메서드가 호출되었습니다.");
            return "Flature";
        }
    }

    이 때 org.slf4j 패키지의 Logger, LoggerFactory를 import 해야한다.

     

    postman으로 테스트를 해보면 console에 정상적으로 log가 찍히는 것을 볼 수 있다.

     

    로그를 통해 컨트롤러에 들어오는 값을 확인하고 싶다면 변수의 값이 들어갈 부분을 중괄호({})로 지정하면 포매팅을 통해 고르 메시지가 구성된다.

    // http://localhost:9090/api/v1/get-api/variable1/{String 값}
    @GetMapping(value = "variable1/{variable}")
    public String getVariable1(@PathVariable String variable) {
        LOGGER.info("@PathVariable을 통해 들어온 값 : {}", variable);
        return variable;
    }