Spring

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

sangyunpark 2023. 9. 27. 22:00

GET API 만들기

@RequestMapping으로 구현하기

@RequestMapping은 별다른 설정 없이 선언하면 HTTP의 모든 요청을 받는다.

GET 형식의 요청만 받기 위해서는 어노테이션에 별도 설정이 필요하다.

@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String getHello(){
    return "Hello world";
}

method에 RequestMethod.GET을 선언해준다.

 

 

스프링 4.3 버전 이후로는 @RequestMapping 어노테이션 보다는

@GetMapping

@PostMapping

@PutMapping

@DeleteMapping

이 사용된다.

 

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

@GetMapping("/name")
private String getName(){
    return "박상윤";
}

 

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

URL 자체에 값을 담아 요청하는 것

@GetMapping(value = "/variable1/{variable}") // url에 정보 담기
private String getVariable1(@PathVariable String variable){
    return variable;
}

 

 

@PathVariable에 변수명을 매핑하는법

@GetMapping(value = "/variable2/{variable}")
    private String getVariable2(@PathVariable("variable") String var){
        return var;
    }

 

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

@GetMapping(value="/request1")
    private String getRequestParam1(
            @RequestParam String name,
            @RequestParam String email,
            @RequestParam String organization
    ){
        return name + " " + email + " " + organization;
    }

쿼리 형식으로 값 전달

URI에서 ?를 기준으로 오른쪽에 "{키} = {값}" 형태로 구성된 요청을 전송하는 방법

 

 

쿼리스트링에 어떤 값이 들어올지 모른다면..?

/*
* @RequestParam
* Map객체로 받기
* 매개변수 항목이 일정하지 않을 때는 Map 객체로 받는 것이 효율적이다.
* */
@GetMapping(value = "/request2")
private String getRequestParam2(@RequestParam Map<String,String> param){
    StringBuilder sb = new StringBuilder();

    param.forEach((key, value) -> sb.append(key).append(" : ").append(value).append("\n"));

    return sb.toString();
}

Map객체를 활용

상황 예시)
회원 가입 관련 API에서 사용자는 회원 가입을 하면서 ID 같은 필수 항목이 아닌 취미 같은 선택 항목에 대해서는 값을 기입하지 않는 경우

매개변수의 항목이 일정하지 않을 수 있으므로, Map 객체로 받는 것이 효율적이다.

 

URL vs URI
URL : 웹 주소를 의미하며, 리소스가 어디에 있는지 알려주기 위한 경로를 의미한다.
URI : 특정 리소스를 식별할 수 있는 식별자를 의미한다.

웹에서 URL을 통해 리소스가 어느 서버에 위치해 있는지 알 수 있으며, 그 서버에 접근해서 리소스에 접근하기 위해서는 대부분 URI가 필요하다.

 

URL -> 서버 접근

URI -> 서버의 리소스 접근

 

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

 

DTO란?

Data Transfer Object의 약자로, 다른 레이어 간의 데이터 교환에 활용된다.

각 클래스 및 인터페이스를 호출하면서 전달하는 매개변수로 사용되는 데이터 객체

 

데이터를 교환하는 용도로만 사용하는 객체

package com.example.springbootcore;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@AllArgsConstructor
@ToString
public class MemberDto {
    private String name;
    private String email;
    private String organization;
}

 

DTO 클래스에 전달하고자 하는 필드 객체를 선언하고 getter/setter 메서드를 구현

DTO 클래스에 선언된 필드는 컨트롤러의 메서드에서 쿼리 파라미터의 키와 매핑된다.

 

쿼리스트링의 키가 정해져 있지만 받아야 할 파라미터가 많을 경우에는 DTO 객체를 활용해 코드의 가독성을 높일 수 있다.

 

DTO를 활용한 GET 메서드

    @GetMapping(value = "/request3")
    private String getRequestParam3(MemberDto memberDto){
        return memberDto.toString();
    }

코드의 양이 확연하게 줄어들었다.

 

POST API 만들기

언제 사용?

웹 애플리케이션을 통해 데이터베이스 등의 저장소에 리소스를 저장할 때 사용되는 API

저장하고자 하는 리소스나 값을 HTTP 바디(body)에 담아 서버에 전달한다.

 

여기서 잠깐

@ResponseBody, @RequestBody 역할은?

@RequestBody
 어노테이션과 @ResponseBody 어노테이션이 각각 HTTP요청 바디를 자바객체로 변환하고 자바객체를 다시 HTTP 응답 바디로 변환해준다. 

 

컨트롤러 생성

 

@RequestMapping

@RequestMapping(value = "/domain", method = RequestMethod.POST)
    private String postExample(){
        return "Hello Post ApI";
    }

 

@PostMapping

@PostMapping(value = "/member")
    private String postMember(@RequestBody Map<String,Object> postData){
        StringBuilder sb = new StringBuilder();
        
        postData.forEach((key, value) -> sb.append(key).append(" : ").append(value).append("\n"));
        
        return sb.toString();
    }

@Request Body : HTTP의 Body 내용을 해당 어노테이션이 지정된 객체에 매핑하는 역할

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

 

DTO 객체를 활용한 POST API 구현

    @PostMapping(value = "/member2")
    private String postMember(@RequestBody MemberDto memberDto){
        return memberDto.toString();
    }

MemerDto의 멤버 변수를 요청 메시지의 키와 매핑해 값을 가져온다.

 

PUT API 만들기

웹 애플리케이션 서버를 통해 데이터베이스 같은 저장소에 존재하는 리소스 값을 업데이트 하는 데 사용한다.

리소스를 서버에 전달하기 위해 HTTP Body를 활용해야 한다.

서버에서 HTTP Body 값을 받기 위해 @RequestBody를 사용한다.

@RestController
public class PutController {
    @PutMapping(value = "/member")
    public String postMember(@RequestBody Map<String, Object> putData){
        StringBuilder sb = new StringBuilder();

        putData.entrySet().forEach(map -> {
            sb.append(map.getKey() + " : " + map.getValue() + "\n");
        });

        return sb.toString();
    }
}

서버에 어떠한 값이 들어올지 모르는 경우에는 Map 객체를 활용해 값을 받을 수 있다.

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

 

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

 @PutMapping(value = "/member1")
    public String postMemberDto1(@RequestBody MemberDto memberDto){
        return memberDto.toString();
    }

    @PutMapping(value = "/member2")
    public MemberDto postMemberDto2(@RequestBody MemberDto memberDto){
        return memberDto;
    }

postMemberDto1의 return 값 : String

postMemberDto2의 return 값 : MemberDto

 

어떤 차이가 존재할까?

String 타입으로 값을 전달받게 되며, DTO 객체의 toString 메서드 결괏값이 출력된다.

headers 항목의 content-type을 보면 text/plain으로서 결괏값으로 일반 문자열이 전달된다.

 

headers 항목의 content-type 항목이 application/json 형식으로 전달이 되었다.

 

추가 +

@RestController 어노테이션이 지정된 클래스는 @Responsebody를 생략할 수 있는데,

@ResponseBody 어노테이션은 자동으로 값을 JSON과 같은 형식으로 변환해서 전달하는 역할을 수행한다.

 

ResponseEntity를 활용한 PUT 메서드

 

스프링 프레임워크에는 HttpEntity라는 클래스가 존재한다.

HttpEntity는 헤더(Header)와 Body로 구성된 HTTP 요청과 응답을 구성하는 역할을 수행한다.

public class HttpEntity<T> {

	/**
	 * The empty {@code HttpEntity}, with no body or headers.
	 */
	public static final HttpEntity<?> EMPTY = new HttpEntity<>();


	private final HttpHeaders headers;

	@Nullable
	private final T body;
    
    ...
 }

 

RequestEntity와 ResponseEntity는 HttpEntity를 상속받아 구현한 클래스이다.

ResponseEntity는 서버에 들어온 요청에 대해 응답 데이터를 구성해 전달할 수 있게 한다.

ResponseEntity는 HttpEntity로부터 HttpHeaders와 Body를 가지고 자체적으로 HttpStatus를 구현

 

public class ResponseEntity<T> extends HttpEntity<T> {

   private final Object status;
    ..생략..
}

 

ResponseEntity 적용하기

@PutMapping(value = "/member3")
public ResponseEntity<MemberDto> postMemberDto3(@RequestBody MemberDto memberDto){
      return ResponseEntity.status(HttpStatus.ACCEPTED).body(memberDto);
}

HttpStatus.ACCEPTED는 응답 코드 202를 가지고 있다.

 

응답 요청 결과

DELETE API 만들기

웹 애플리케이션 서버를 거쳐 데이터베이스 등의 저장소에 있는 리소스를 삭제할 때 사용한다.

서버 : 클라이언트로부터 리소스를 식별할 수 있는 값을 받아 데이터베이스나 캐시에 있는 리소스를 조회하고 삭제하는 역할을 수행한다.

컨트롤러를 통해 값을 받는 단계에서는 간단한 값을 받으므로 GET 메서드와 같이 URI에 값을 넣어 요청을 받는 형식으로 구현된다.

 

@RestController
public class DeleteController {
    @DeleteMapping(value = "/{variable}")
    public String DeleteVariable(@PathVariable String variable){
        return variable;
    }
    
    @DeleteMapping(value = "/request1")
    public String getRequestParams(@RequestParam String email){
        return "email : " + email;
    }
}

 

REST API 명세를 문서화하는 방법 - Swagger

API를 개발하면 명세를 관리해야 한다.

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

 

API는 개발 과정에서 계속 변경되므로 작성한 명세 문서도 주기적인 업데이트가 필요하다.

명세작업은 시간이 너~무 오래걸린다! 어떻게 하는게 좋을까? Swagger라는 오픈 소스 프로젝트를 사용하라!

 

build.gradle에 의존성 추가

    implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '3.0.0'

 

Configuration 작성(설정)

package com.example.springbootcore.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

@Configuration
public class SwaggerConfiguration {
    @Bean
    public Docket api(){
        return new Docket(DocumentationType.OAS_30)
                .useDefaultResponseMessages(false) // 응답 코드 노출 x
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.springbootcore.controller")) // controller 위치
                .paths(PathSelectors.any()) // 특정 경로 지정
                .build()
                .apiInfo(apiInfo())
    }

    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("Spring Boot Open API Test with Swagger")
                .description("설명 파트")
                .version("1.0.0")
                .build();
    }
}

 

스웨거 접속

SpringBoot 3.x 인경우 아직 Swagger가 지원해주지 않아서

Springdoc-openapi를 적용

 

따로 configuration을 해줄 필요가 없다.

 

추가적인 어노테이션 Swagger

@ApiOperation(value = "GET 메서드 예제", notes = "@RequestParam을 활용한 GET Method")
@GetMapping(value = "/request")

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 클래스 내의 매개변수에도 정의할 수 있다.

 

로깅 라이브러리 - Logback

로깅이란?

애플리케이션이 동작하는 동안 시스템의 상태나 동작 정보를 시간순으로 기록하는 것

 

로깅은 개발 영역중에 비기능 요구사항에 속한다.(사용자, 고객에게 필요한 기능은 x)

디버깅하거나 개발 이후 발생한 문제를 해결할 때 원인을 분석하는 데 꼭 필요한 요소이다.

 

자바 진영에서 가장 많이 사용하는 로깅 프레임워크 : Logback(slf4j를 기반으로 구현)

 

Logback의 특징

(1) 크게 5개의 로그 레벨(TRACE, DEBUG, INFO, WARN, ERROR)을 설정할 수 있다.

    - ERROR : 로직 수행 중에 시스템에 심각한 문제가 발생해서 애플리케이션의 작동이 불가능한 경우

    - WARN : 시스템 에러의 원인이 될 수 있는 경고 레벨을 의미

    - INFO : 애플리케이션의 상태 변경과 같은 정보 전달을 위해 사용

    - DEBUG : 애플리케이션의 디버깅을 위한 메시지를 표시하는 레벨을 의미

    - TRACE : DEBUG 레벨보다 더 상새한 메시지를 표현하기 위한 레벨을 의미

 

(2) 실제 운영 환경과 개발 환경에서 각각 다른 출력 레벨을 설정해서 로그를 확인할 수 있다.

(3) Logback의 설정 파일을 일정 시간마다 스캔해서 애플리케이션을 재가동하지 않아도 설정을 변경할 수 있다.

(4) 별도의 프로그램 지원 없이도 자체적으로 로그 파일을 압축할 수 있다.

(5) 저장된 로그 파일에 대한 보관 기간 등을 설정해서 관리할 수 있다.

 

Logback 설정하기

일반적으로 클래스패스(classpath)에 있는 설정 파일을 자동으로 참조하므로 Logback 설정 파일은 리소스 폴더 안에 생성한다.

파일명의 경우 일반적인 자바 또는 스프링 프로젝트에서는 logback.xml이라는 이름으로 참조하지만 스프링 부트에서는 logback-spring.xml 파일을 참조한다.

 

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <property name="LOG_PATH" value="./logs/"/>
    
    <!--Appenders -->
    <appender name = "console" class = "ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <pattern>[%d{yyy-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>
    
    <!-- TRACE > DEBUG > INFO > WARN > ERROR > OFF -->  
    <!-- Root Logger -->
    <root level="INFO">
        <appender-ref ref="console"/>
        <appender-ref ref="INFO_LOG"/>
    </root>
</configuration>

Property 영역, Appender 영역, Encoder 영역, Pattern 영역, Root 영역이 존재한다.

 

Appender 영역

로그의 형태를 설정하고 어떠한 방법으로 출력할지를 설정하는 부분이다.

Appender 자체는 하나의 인터페이스를 의미하며, 하위에 여러 구현체가 존재한다.

 

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

 

- ConsoleAppender : 콘솔에 로그를 출력

- FileAppender : 파일에 로그를 저장

- RollingFileAppender : 여러 개의 파일을 순회하면서 로그를 저장

- SMTPAppender : 메일로 로그를 저장

- DBAppender : 데이터베이스에 로그를 저장

 

 

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

 

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

 

로그 형식 예시

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

 

Root 영역

설정 파일에 정의된 Appender를 활용하려면 Root 영역에서 Appender를 참조해서 로깅 레벨을 설정한다.

특정 패키지에 대해 다른 로깅 레벨을 설정하고 싶다면 root 대신 logger를 사용해도 된다.

 

ex) Root 사용

<root level = "INFO">
    <appender-ref  ref = "console"/>
    <appender-ref  ref = "INFO_LOG"/>
</root>

ex) Logger 사용

<logger name = "com.springboot.api.controller" level = "DEBUG" additivity = "false">
    <appender-ref ref = "console"/>
    <appender-ref ref = "INFO_LOG"/>
</logger>

name 속성 : 패키지 단위로 로깅이 적용될 범위를 지정하고 level 속성으로 로그 레벨을 지정한다.

additivity 속성 : 앞에서 지정한 패키지 범위에 하위 패키지를 포함할지 여부를 결정한다.

기본값은 true, 하위 패키지를 모두 포함한다.

 

 

Logback 적용하기

 

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

ex ) GetController에 Logger 적용하기

public class GetController {
    
    private final Logger LOGGER = (Logger) LoggerFactory.getLogger(GetController.class);
    
..
}

Logger는 LoggerFactory를 통해 객체를 생성한다.

이때 클래스의 이름을 함께 지정해서 클래스의 정보를 Logger에서 가져가게 한다.

 

 @GetMapping("/name")
 private String getName(){
    LOGGER.info("getName 메서드가 호출됬습니다.");
    return "박상윤";
 }

info레벨에서 로그가 출력된다.

 

ex) 변수의 값을 로그로 출력

    @GetMapping(value = "/variable1/{variable}") // url에 정보 담기
    private String getVariable1(@PathVariable String variable){
        LOGGER.info("@PathVariable을 통해 들어온 값 : {}");
        return variable;
    }

변수를 지정해 변수로 들어오는 값을 로깅할 수 있다.

변수의 값이 들어갈 부분을 중괄호({})로 지정하면 포매팅을 통해 로그 메시지가 구성된다.

 

Http 요청

 

결과

 

스터디 가이드

Java에서 문자열을 합치는 방법
1. + 연산자
2. String 클래스의 concat() 메서드 활용
3. String 클래스의 append() 메서드 활용
4. Stirng 클래스의 format() 메서드 활용