목차
05. API를 작성하는 다양한 방법
ㄴ5.1 프로젝트 설정
ㄴ5.2 GET API 만들기
ㄴ5.2.1 @RequestMapping으로 구현하기
ㄴ5.2.2 매개변수가 없는 GET 메서드 구현
ㄴ5.2.3 @PathVariable을 활용한 GET 메서드 구현
ㄴ5.2.4 @RequestParam을 활용한 GET 메서드 구현
ㄴ5.2.5 DTO 객체를 활용한 GET 메서드 구현
ㄴ5.3 POST API 만들기
ㄴ5.3.1 @RequestMapping으로 구현하기
ㄴ5.3.2 @RequestBody를 활용한 POST 메서드 구현
ㄴ5.4 PUT API 만들기
ㄴ5.4.1 @RequestBody를 활용한 PUT메서드 구현
ㄴ5.4.2 ResponseEntity를 활용한 PUT메서드 구현
ㄴ5.5 DELETE API 만들기
ㄴ5.5.1 @PathVariable과 @RequestParam을 활용한 DELETE 메서드 구현
ㄴ5.6 [한걸음 더] REST API 명세를 문서화 하는 방법 - Swagger
ㄴ5.7 [한걸음 더] 로깅 라이브러리 - Logback
ㄴ5.7.1 Logback 설정
ㄴ5.7.2 Logback 적용하기
ㄴ5.8 정리
5장의 목적 > 각 HTTP 메서드(GET, POST, PUT, DELETE)에 해당하는 API를 개발해보고 그 과정에서 필요한 내용 학습
데이터 베이스 설치하지 않아 정확한 기능 구현은 할 수 없지만 외부의 요청을 받아 응답하는 기능을 구현해서 컨트롤러가 어떻게 구성되는지 알아보자.
5.1 프로젝트 설정
5장에서 실습할 프로젝트를 생성.
나는 4장에서 hello로 실습한 것 그대로 가져갈 것이다.
5.2 GET API 만들기
GET API란? 웹 애플리케이션 서버에서 값을 가져올 때 사용하는 API
GET API를 작성하는 방법은 다양하다.
실무에서는 HTTP메서드에 따라 컨트롤러 클래스를 구분하지 않으나, 해당 실습에서는 controller 패키지 안에 GetController 클래스를 생성함. -> 나는 한 클래스 안에 HTTP 메서드 API를 같이 넣을 예정
- 지금은 안쓰는 @RequestMapping method=RequestMethod.GET으로 GET API 구현
@RestController
@RequestMapping("/api/v1/get-api")
public class HttpMethodController {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String getHello() {
return "Hello, World";
}
}
- 매개변수가 없는 GET API 구현
@RestController
@RequestMapping("/api/v1/get-api")
public class HttpMethodController {
@GetMapping("/name")
public String getName() {
return "Flature";
}
}
- @PathVariable을 활용한 GET API 구현
@RestController
@RequestMapping("/api/v1/get-api")
public class HttpMethodController {
@GetMapping("/variable1/{variable}")
public String getVariable1(@PathVariable String variable){
return variable;
}
}
{} 중괄호는 실제 url에서는 안씀. 중괄호를 쓰는 이유는 해당 위치에서 값을 받겠다는 의미이다. 그리고 중괄호 안에 변수 이름과 값을 줄 변수의 이름이 같아야 한다.
값을 다르게 할 경우는 다음과 같다.
@RestController
@RequestMapping("/api/v1/get-api")
public class HttpMethodController {
@GetMapping("/variable2/{variable}")
public String getVariable2(@PathVariable(value = "variable") String var){
return var;
}
// @PathVariable("variable")로 value 생략 가능
}
- @RequestParam을 활용한 GET API 구현
@RestController
@RequestMapping("/api/v1/get-api")
public class HttpMethodController {
@GetMapping("/request")
public String getRequestParam1(
@RequestParam String name,
@RequestParam String email,
@RequestParam String organization) {
return name + " " + email + " " + organization;
}
}
만일 쿼리 스트링에 어떤 값이 들어올지 모른다면 아래와 같은 방법인 Map과 같은 객체를 활용할 수도 있음.
@RestController
@RequestMapping("/api/v1/get-api")
public class HttpMethodController {
@GetMapping("/request2")
public String getRequestParam2(
@RequestParam Map<String, String> param) {
StringBuilder sb = new StringBuilder();
param.entrySet().forEach(map -> {
sb.append(map.getKey() +" : " + map.getValue() + "\n");
});
return sb.toString();
}
}
** Tip URI와 URL의 차이
URL : 우리가 흔히 말하는 웹 주소를 의미. 리소스가 어디에 있는지 알려주기 위한 경로를 의미함.
URI : 특정 리소스를 식별할 수 있는 식별자
- DTO 객체를 활용한 활용한 GET API 구현
DTO란? Data Transfer Object의 약자로, 다른 레이어 간의 데이터 교환에 활용됨.(Entity가 아닌 교환 목적으로 쓰임)
각 클래스 및 인터페이스를 호출하면서 전달하는 매개변수로 사용되는 데이터 객체이다.
DTO는 설명한 것 처럼 데이터를 교환하는 용도로만 사용하는 객체이기 때문에 DTO에는 별도의 로직이 포함되지 않음.(6장 데이터베이스에서 좀 더 자세하게 다룰 예정)
** Tip DTO와 VO
DTO = Data Transfer Object
VO = Value Object
두 개의 역할을 엄밀하게 구분하지 않고 사용할 때가 많지만 정확하게는 역할과 사용법에서 차이가 있다.
VO는 데이터 그 자체로 의미가 있는 객체이며 읽기전용(Read-Only)으로 설계하는 것이 가장 특징적이다.
즉, VO는 값을 변경할 수 없게 만들어 데이터 신뢰성을 유지해야함
DTO는 데이터 전송을 위해 사용되는 데이터 컨테이너이며, 같은 애플리케이션 내부에서 사용되는 것이 아니라 다른 서버(시스템)으로 전달하는 경우에 사용된다.
앞서 말한 다른 레이어간 데이터 교환에 DTO를 활용한다는 것은 애플리케이션 내부에 정의된 레이어 일 수도 있고, 인프라 관점에서의 서버 아키텍처 상의 레이어일 수 도 있다.
팀 내부적으로 용어나 개념의 역할 범위를 설정하고 합의해서 사용하는 것이 업무 효율성을 높일 것이다.
package dto를 만들고 그안에 MemberDto 클래스를 생성한 뒤 스프링의 어노테이션을 활용해서 다음과 같이 getter, setter, toString() 오버라이드 구현을 할 수 있다.
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class MemberDto {
private String name;
private String email;
private String organization;
}
@RestController
@RequestMapping("/api/v1/get-api")
public class HttpMethodController {
@GetMapping("/request3")
public String getRequestParam1(MemberDto memberDto) {
//return memberDto.getName() + " " + memberDto.getEmail() + " " + memberDto.getOrganization();
return memberDto.toString();
}
}
5.3 POST API 만들기
POST API는 웹 애플리케이션을 통해 데이터베이스 등의 저장소에 리소스를 저장할 때 사용되는 API이다.
앞에서 살펴본 GET API에서는 URL의 경로나 파라미터에 변수를 넣어 요청을 보냈지만 POST API에서는 저장하고자 하는 리소스나 값을 HTTP 바디(body)에 담아 서버에 전달한다.. 그래서 URI가 GET API에 간단하다.
GET API 이후로는 @RequestMapping을 사용하지 않고 @PostMapping, @PutMapping @DeleteMapping등만 사용 할 것이다.
- @RequestBody을 활용한 POST API 구현
Body 영역에 작성되는 값은 일반적으로 JSON(JavaScript Objec Notation) 형식으로 전송된다.
@RestController
@RequestMapping("/api/v1")
public class HttpMethodController {
@PostMapping("/post-api/member")
public String postMember(
@RequestBody Map<String, String> param) {
StringBuilder sb = new StringBuilder();
param.entrySet().forEach(map -> {
sb.append(map.getKey() +" : " + map.getValue() + "\n");
});
return sb.toString();
}
@PostMapping("/post-api/member2")
public String postMemberDto(
@RequestBody Map<String, String> param) {
StringBuilder sb = new StringBuilder();
param.entrySet().forEach(map -> {
sb.append(map.getKey() +" : " + map.getValue() + "\n");
});
return sb.toString();
}
}
5.4 PUT API 만들기
PUT API는 웹 애플리케이션 서버를 통해 데이터베이스 같은 저장소에 존재하는 리소스 값을 업데이트 하는데 사용한다.
POST API와 비교하면 요청을 받아 실제 데이터 베이스에 반영하는 과정(서비스 로직)에서 차이가 있지만 컨트롤러 클래스를 구현하는 방법은 거의 동일하다. 리소스를 서버에 전달할 때 HTTP Body를 활용하기 때문.
- @RequestBody을 활용한 PUT API 구현
Body 영역에 작성되는 값은 일반적으로 JSON(JavaScript Objec Notation) 형식으로 전송된다.
@RestController
@RequestMapping("/api/v1")
public class HttpMethodController {
@PutMapping("/put-api/member")
public String putMember(
@RequestBody Map<String, String> putData) {
StringBuilder sb = new StringBuilder();
putData.entrySet().forEach(map -> {
sb.append(map.getKey() +" : " + map.getValue() + "\n");
});
return sb.toString();
}
@PutMapping("/put-api/member1")
public String putMemberDto1(
@RequestBody MemberDto memberDto) {
return memberDto.toString();
}
@PutMapping("/put-api/member2")
public MemberDto putMemberDto2(
@RequestBody MemberDto memberDto) {
return memberDto;
}
}
memberDto.toString()을 했던 것만 이렇게 MemberDto()형식으로 나오고 나머지는 Json 형식으로 name, emai, organization이 잘 나온다.
- ResponseEntity를 활용한 PUT API 구현
스프링 프레임워크에는 HttpEntity라는 클래스가 있으며 HttpEntity는 헤더(Header)와 바디(Body)로 구성된 HTTP 요청과 응답을 구성하는 역할을 수행한다. 이것을 상속받아 구현한 것이 RequestEntity, ResponseEntity이며 자체적으로 Json 형식에 맞게 데이터를 전송할 수 있다.
POST API 뿐만 아니라 다른 API에서도 사용이 가능하다.
이것 말고도 커스텀한 Request, Response 를 사용하면 더 자유롭게 데이터를 주고 받을 수 있다.
@RestController
@RequestMapping("/api/v1")
public class HttpMethodController {
@PutMapping("/put-api/member3")
public ResponseEntity<MemberDto> putMemberDto3(
@RequestBody MemberDto memberDto) {
return ResponseEntity
.status(HttpStatus.ACCEPTED)
.body(memberDto);
}
}
5.5 DELETE API 만들기
DELETE API는 웹 애플리케이션 서버를 거쳐 데이터 베이스 등의 저장소에 있는 리소스를 삭제할 때 사용한다. 서버에서는 클라이언트로부터 리소스를 식별할 수 있는 값을 받아 데이터베이스나 캐시에 있는 리소스를 조회하고 삭제하는 역할을 수행한다. 이때 컨트롤러를 통해 값을 받기 때문에 GET 메서드와 같이 URI에 값을 넣어 요청을 받는 형식으로 구현된다.
- @PathVariable과 @RequestParam을 활용한 DELETE 메서드 구현
@RestController
@RequestMapping("/api/v1")
public class HttpMethodController {
@DeleteMapping("/delete-api/{variable}")
public String DeleteVariable(
@PathVariable String variable) {
return variable;
}
@DeleteMapping("/delete-api/request1")
public String getRequestParam1(
@RequestParam String email) {
return "email : " + email;
}
}
5.6 [한걸음 더] REST API 명세를 문서화 하는 방법 - Swagger
API를 개발하면 명세를 관리해야한다. 명세란 해당 API가 어떤 로직을 수행하는지 설명하고 이 로직을 수행하기 위해 어떤 값을 요청하고, 응답을 어떤 값으로 받는 지를 정리한 자료이다.
API는 개발 과정에서 계속 변경되므로 작성한 명세 문서도 주기적으로 업데이트를 해야함. 이러한 것을 관리하고 해결하기 위한 것이 Swagger라는 오픈 소스 프로젝트이다.
Maven에서는 pom.xml에 의존성을 추가하고, Gradle에서는 build.gradle에 아래의 의존성을 주입한다.
implementation 'io.springfox:springfox-boot-starter:3.0.0'
implementation 'io.springfox:springfox-swagger-ui:3.0.0
이 때 주의해야할 점은 swagger 3버전은 spring boot 2.7이하 버전들로만 호환이 되서 사용이 가능하다.
필자도 3.x로 스프링 부트를 구현하다가 에러가 나서 다운그레이드를 해줬다.
동작 확인도 Swagger 페이지를 통해 테스트가 가능하다.
config 패키지 생성 후 SwaggerConfig 클래스 생성한 뒤 다음과 같은 코드를 넣는다.
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.springboot.hello(패키지)"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("프로젝트 이름")
.description("프로젝트 설명")
.version("2.0")
.build();
}
}
웹 애플리케이션을 실행한 뒤에 'http://localhost:8080/swagger-ui/index.html' 을 웹에서 띄우면 다음과 같이 swagger를 확인할 수 있다.
필자는 실행시켰을 때
org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointer
이런 오류로 실행이 안되서
@Configuration
@EnableWebMvc
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.springboot.hello"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("프로젝트 이름")
.description("프로젝트 설명")
.version("2.0")
.build();
}
}
어노테이션을 @EnableWebMvc로 바꿔서 진행했다.
API를 눌러보면 Parameters, Responses로 어떻게 요청을하고 응답을 받는지 확인할 수 있고 왼쪽 하얀색 버튼인 Try it out 버튼을 눌러서 실제로 요청을 수정해서 test할 수 있다.
부가적으로 설명을 추가하는 어노테이션은
@ApiOperation
@ApiResponse
으로 조작해서 커스텀화 할 수 있다.
5.7 [한걸음 더] 로깅 라이브러리 - Logback
로깅(logging)이란 애플리케이션이 동작하는 동안 시스템의 상태나 동작 정보를 시간순으로 기록하는 것을 의미한다.
로깅은 개발 영역 중 '비기능 요구사항'에 속한다. 즉, 사용자나 고객에게 필요한 기능은 아니지만 디버깅하거나 개발 이후 발생한 문제를 해결할 때 원인을 분석하는 데 꼭 필요한 요소이다.
자바에서 많이 사용되는 로깅 프레임워크는 Logback이다. Logback이란 log4j 이후에 출시된 로깅 프레임 워크로서 slf4j를 기반으로 구현됐고 성능이 월등하다. 그리고 스프링 부트의 spring-boot-starter-web 라이브러리 내부에 내장돼 있어 별도의 의존성을 추가하지 않아도 사용 가능하다.
Loback의 특징
- 크게 5개의 로그 레벨(TRACE, DEBUG, INFO, WARN, ERROR)을 설정할 수 있다.
- ERROR : 로직 수행 중에 시스템에 심각한 문제가 발생해서 애플리케이션의 작동이 불가능한 경우를 의미
- WARN : 시스템 에러의 원인이 될 수 있는 경고 레벨을 의미
- INFO : 애플리케이션의 상태 변경과 같은 정보 전달을 위해 사용
- DEBUG : 애플리케이션의 디버깅을 위한 메시지를 표시하는 레벨을 의미
- TRACE : DEBUG 레벨보다 더 상세한 메시지를 표현하기 위한 레벨을 의미
- 실제 운영 환경과 개발 환경에서 각각 다른 출력 레벨을 설정해서 로그를 확인할 수있다.
- Logback의 설정 파일을 일정 시간마다 스캔해서 애플리케이션을 재가동하지 않아도 설정ㅇ르 변경할 수 있습니다.
- 별도의 프로그램 지원 없이도 자체적으로 로그 파일을 압축할 수 있다.
- 저장된 로그 파일에 대한 보관 기간 등을 설정해서 관리할 수 있다.
Tip 각 로그 레벨에 대해서는 아파치 Log4j의 레벨에 관련된 내용을 보면 도움이 된다.
https://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/Level.html
Level (Apache Log4j 1.2.17 API)
org.apache.log4j Class Level java.lang.Object org.apache.log4j.Priority org.apache.log4j.Level All Implemented Interfaces: Serializable Direct Known Subclasses: UtilLoggingLevel public class Levelextends Priorityimplements Serializable Defines the minimum
logging.apache.org
1. LogBack 설정
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<property name="LOGS_PATH" value="./logs"/>
<property name="LOGS_LEVEL" value="INFO"/>
<!-- Appenders -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>[%d{yyyy-MM-dd HH:mm:ss}:%-3relative][%thread] %-5level %logger{35} - %msg%n</pattern>
<charset>UTF-8</charset>
</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>${LOGS_PATH}/info_${type}.%d{yyyy-MM-dd}.gz</fileNamePattern>
<maxHistory>30</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%-5level] [%thread] %logger - %msg%n</pattern>
</encoder>
</appender>
<!-- TRACE > DEBUG > INFO > WARN > ERROR > OFF -->
<!-- Root Logger -->
<root level="${LOGS_LEVEL}">
<appender-ref ref="console"/>
<appender-ref ref="INFO_LOG"/>
</root>
</configuration>
- Property 영역
- Appender 영역
- Encoder 영역
- Pattern 영역
- Root 영역
많은 영역이 존재하지만 Logback 설정에서 가장 중요한 영역은 Appender과 Root영역이다.
Appender 영역
Appender 영역은 로그의 형태를 설정하고 어떤 방법으로 출력할지를 설정하는 곳이다.
Appender 자체는 하나의 인터페이스를 의미하고, 하위에 여러 구현체가 존재한다.
그림 5.28에 등장하는 각 구현체들을 등록해서 원하는 로그 형식을 출력 가능하다.
- ConsoleAppender : 콘솔에 로그를 출력
- FileAppender : 파일에 로그를 저장
- RollingfileAppender : 여러 개의 파일을 순회하면서 로그를 저장
- SMTPAppender : 메일로 로그를 전송
- DBAppender : 데이터베이스에 로그를 저장
위의 xml파일을 살펴보면, 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 | 로깅이 발생한 호출 지점의 라인 수 |
위와 같은 패턴을 활용해 다음과 같은 패턴을 만들 수 있다.
ex >
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>
Root 영역
설정 파일에 정의된 Appender를 활용하려면 Root 영역에서 Appender를 참조해서 로깅 레벨을 설정한다.
만약 특정 패키지에 대해 다른 로깅 레벨을 설정하고 싶다면 root 대신 logger를 사용해 예제 5.25와 지정한다.
logger 요소의 name 속성에는 패키지 단위로 로깅이 적용될 범위를 지정하고 level 속성으로 로그 레벨을 지정한다.
additivity 속성은 지정한 패키지 범위에 하위 패키지를 포함할지 여부를 결정한다. 기본 값은 true이며, 이 경우 하위 패키지를 모두 포함한다.
2. Logback 적용하기
Logback은 출력할 메시지를 Appender에 전달할 Logger 객체를 각 클래스에 정의해서 사용한다.
** Logbackk 부분은 프로젝트 할때 딱 한번만 해봐서 아직 잘 모르겠다. 추후 이해되거나 알게되는게 있으면 추가할 예정 **
5.8 정리
컨트롤러를 작성해서 외부의 인터페이스를 노출하는 방법, Swagger, Logback을 알아봤다.
현재 이 책 장에서는 아직 다른 계층은 공부하지 않았기 때문에 단순한 API를 작성하는 예제만 다뤘지만 컨트롤러를 통해 값을 받는 방법은 이번 장에서 거의 대부분의 케이스를 다뤘기 때문에 컨트롤러를 작성하는데 무리가 없을 것이다.
Swagger가 제공하는 다양한 어노테이션을 통해 Swagger 페이지가 명세로써의 역할을 잘 수행하도록 다듬는 연습이 중요할 것이다.
Logback으로 어떤 정보를 로그로 기록하는 것이 향후 애플리케이션의 유지 보수에서 효과적일지 고민하는 것도 중요하다.
'BookStudy > 스프링 부트 핵심 가이드' 카테고리의 다른 글
[스프링 부트 핵심 가이드] 08. Spring Data JPA 활용 (0) | 2023.09.17 |
---|---|
[스프링 부트 핵심가이드] 06. 데이터베이스 연동 (0) | 2023.09.08 |
[스프링 부트 핵심 가이드] 04. 스프링 부트 애플리케이션 개발하기 (0) | 2023.08.30 |
[스프링 부트 핵심 가이드] 03. 개발 환경 구성 (0) | 2023.08.24 |
[스프링 부트 핵심 가이드] 02. 개발에 앞서 알면 좋은 기초 지식 (0) | 2023.08.24 |