이번 장은 각 HTTP 메서드에 해당하는 API개발을 하고 외부의 요청을 받아 응답하는 기능 구현을 통해 컨트롤러가 어떻게 구성되는지 알아보겠습니다.
5장에서 실습할 프로젝트를 생성합니다. groupId는 'com.springboot'로 설정하고 name&artifactId는 'api'로 설정합니다.
GET API는 웹 애플리케이션 서버에서 값을 가져올 때 사용하는 API입니다. 실무에서는 HTTP 메서드에 따라 컨트롤러 클래스를 구분하지 않지만, 실습에서는 메서드별로 클래스를 생성합니다.
클래스 수준에서 @RequestMapping을 설정하면 내부에 선언한 메소드의 URL 리소스 앞에 @RequestMapping의 값이 공통 값으로 추가됩니다.
- controller의 method들과 요청을 매핑할 때 사용한다.
→요청 URL을 어떤 메소드가 처리할지 여부를 결정하는 것입니다.
- URL, HTTP method, request parameters, headers 와 media types과 같은 다양한 속성을 가집니다.
- 공유 매핑으로 클래스 수준에서 사용하거나 엔드 포인트 매핑을 위해 메서드 수준에서 사용할 수 있습니다.
@RequestMapping 어노테이션은 더이상 사용되지 않습니다. 특별히 @RequestMapping을 활용해야 하는 내용이 아니라면 각 HTTP 메서드에 맞는 어노테이션을 사용합니다.
-@GETMapping
-@PostMapping
-@PutMapping
-@DeleteMapping
별도의 매개변수 없이 GET 메서드 구현을 위해 아래와 같이 코드를 작성할 수 있습니다.
@RestController
@RequestMapping("/api/v1/get-api")
public class GetController {
// @RequestMapping(value="/hello", method= RequestMethod.GET)
// public String getHello() {
// return "Hello World";
// }
@GetMapping(value="/name")
public String getName() {
return "Falture";
}
@PathVariable을 활용한 GET 메서드 구현을 해보겠습니다
매개변수를 받을 때 자주 쓰이는 방법 중 하나는 URL 자체에 값을 담아 요청하는 것으로 아래 코드로 작성됩니다.
@GetMapping(value="/variable1/{variable}")
public String getVariable1(@PathVariable String variable){
return variable;
}
중괄호로 표시된 위치의 값을 받아 요청하는 것을 볼 수 있습니다. GET요청에서 많이 사용되며 값을 간단히 전달할 때 주로 사용됩니다.
이러한 방법을 사용할 때 지켜야 할 규칙은 다음과 같습니다.
1. @GetMapping 어노테이션의 값으로 URL을 입력할 때 중괄호를 사용해 어느 위치에서 값을 받을지 지정해야 합니다.
2. 메서드와 매개변수와 그 값을 연결하기 위해 @PathVariable을 명시합니다.
3. @GetMapping 어노테이션과 @PathVariable에 지정된 변수의 이름을 동일하게 맞춰야 합니다.
4. 만약 3번이 어렵다면 @PathVariable뒤에 괄호를 열어 @GetMapping 어노테이션의 변수명을 지정합니다.
@RequestParam을 활용한 GET메서드 구현을 위해 아래와 같이 코드를 작성할 수 있습니다.
위 방법처럼 URL경로에 값을 담아 요청을 보내는 방법 외에도 쿼리 형식으로 값을 전달할 수도 있습니다. URL에서 '?'를 기준으로 우측에 '{키}={값}' 형태로 구성된 요청을 전송하는 방법입니다.
@GetMapping(value="/request1")
public String getRequestParam1(
@RequestParam String name,
@RequestParam String email,
@RequestParam String organization
) {
return name + " " + email + " " + organization;
}
만약 쿼리스트링에 어떤 값이 들어올지 모른다면 아래와 같이 Map 객체를 활용할 수도 있습니다.
@GetMapping(value="/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();
}
위와 같이 코드를 작성하면 값에 상관없이 요청을 받을 수 있습니다. 매개변수의 항목이 일정하지 않은 경우는 Map 객체로 받는 것이 효율적입니다.
DTO 객체를 활용한 GET 메서드 구현을 위해 패키지와 클래스를 만듭니다.
package com.springboot.api.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 eamil){
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+'\''+
'}';
}
}
DTO 클래스에는 전달하고자 하는 필드 객체를 선언하고 getter/setter 메서드를 구현합니다. 쿼리스트링의 키가 정해져 있지만 받아야 할 파라미터가 많을 경우에는 아래와 같이 DTO 객체를 활용해 코드의 가독성을 높입니다.
@GetMapping(value="/request3")
public String getRequestParam3(MemberDto memberDto){
return memberDto.toString();
}
POST API는 웹 애플리케이션을 통해 데이터베이스 등의 저장소에 리소스를 저장할 때 사용되는 API입니다. POSTAPI는 저장하고자 하는 리소스 값은 HTTP 바디에 담아 서버에 전달합니다.
@RestController
@RequestMapping("/api/v1/post-api")
public class PostController {
컨트롤러 클래스에서 공통 URL 설정을 해줍니다.
@RequestMapping으로 구현 방법은 method=RequestMethod.POST부분을 제외하면 GET API와 동일합니다.
@RequestMapping(value="/domain", method=RequestMethod.POST)
public String postExample(){
return "Hello Post ApI";
}
@RequestBody를 활용한 POST 메서드를 구현해보겠습니다. Body영역에 작성되는 값은 일정한 형태를 취합니다.
@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();
}
Map 객체는 위에 언급했던 것처럼 어떤 값이 들어오게 될지 특정하기 어려울 때 주로 사용합니다.
@PostMapping(value="/member2")
public String postMemberDto(@RequestBody MemberDto memebrDto){
return memebrDto.toString();
}
위와 같이 작성하면 MemberDto의 멤버 변수를 요청 메시지의 키와 매핑해 값을 가져옵니다.
PUT API를 만들기 위해 PutController 클래스를 작성해줍니다.
@RestController
@RequestMapping("/api/v1/post-api")
public class PutController {
@RequestBody를 활용한 PUT 메서드를 아래와 같이 구현합니다.
@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 객체를 활용해 값을 받아옵니다.
@PutMapping(value = "member1")
public String postMemberDto1(@RequestBody MemberDto memberDto){
return memberDto.toString();
}
@PutMapping(value="/member2")
public MemberDto postMemberDto2(@RequestBody MemberDto memberDto){
return memberDto;
}
ResponseEntitiy를 활용한 PUT 메서드 구현은 아래와 같이 구현합니다.
@PutMapping(value="/member3")
public ResponseEntity<MemberDto> postMemberDto3(@RequestBody MemberDto memberDto){
return RequestEntity
.status(HttpStatus.ACCEPTED)
.body(memberDto);
}
DELETE API는 저장소에 있는 리소스를 삭제할 때 사용합니다.
@RestController
@RequestMapping("/api/v1/delete-api")
public class DeleteController {
@PathVariable과 @RequestParam을 활용한 DELETE API 메서드를 구현하는 방법은 아래와 같습니다.
@DeleteMapping(value="/{variable}")
public String DeleteVariable(@PathVariable String variable){
return variable;
}
@DeleteMapping(value="/request1")
public String getRequestParam1(@RequestParam String email){
return "e-mail : " + email;
}
REST API 명세를 문서화 하는 방법에는 Swagger가 있습니다.
먼저 Swagger를 사용하기 위해 pom.xml 파일에 의존성을 추가해줍니다.
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swqgger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swqgger-ui</artifactId>
<version>2.9.2</version>
</dependency>
두번째로 Swagger에 관련된 설정 코드를 작성합니다. config 패키지를 생성한 후 그 안에 생성하는 것이 좋습니다.
package com.springboot.api.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
@Bean
public Docket api(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.api(RequestHandlerSelectors.basePackage("com.springboot.api"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("Spring Boot Open API Test with Swagger")
.description("설명 부분")
.version("1.0.0")
.build();
}
}
위 과정을 마치면 Swagger 페이지가 출력됩니다.
Swagger를 더 잘 활용하기 위해 @RequestParma을 활용한 GET 메서드에 대한 명세의 세부 내용을 설정해보겠습니다.
@ApiOperation(value="GET 메서드 예제", notes="@RequestParma을 활용한 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;
}
위와 같이 설정한 후 API 명세를 살펴보면 Name과 Description이 나와있는걸 확인할 수 있습니다.
로깅이란 시스템의 상태나 동작 정보를 시간순으로 기록하는 것을 의미합니다.
가장 많이 사용되는 자바 로깅 프레임워크는 Logback으로, 스프링 부트 라이브러리 내부에 내장되어 있어 사용 시 별도의 의존성 추가가 요구되지 않습니다. 로그백은 다음과 같이 다섯가지의 특징을 같습니다.
- 5가지의 로그 레벨(TRACE, DEBUG, INFO, WARN, ERROR)을 설정할 수 있음
- 실제 운영 환경과 개발 환경 각각 다른 출력 레벨 설정 가능
- Logback의 설정 파일을 일정시간마다 스캔 -> 애플리케이션 재가동하지 않아도 설정 변경 가능
- 자체적인 로그 파일 압축
- 로그 보관 기간 설정하여 관리
로그백을 사용하기 위한 설정 파일을 만들어보겠습니다.
<?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{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"/>
</root>
</configuration>
이제 Logback 설정에서 가장 중요한 어팬더 영역과 루트 영역에 대해 좀더 알아보겠습니다.
Appender 영역은 각 구현체를 등록해 로그를 원하는 형식으로 출력할 수 있습니다.
어팬더의 대표적인 구현체는 다음과 같습니다.
- ConsileAppender: 콘솔에 로그를 출력
- FileAppender: 파일에 로그를 저장
- RollingFileAppender: 여러 개의 파일을 순회하면서 로그를 저장
- SMTPAppender: 메일로 로그를 전송
- DBAppender: 데이터베이스에 로그를 저장
로그를 어떤 방식으로 저장할지 지정하는 방법은 appender 요소의 class 속성에 각 구현체를 정의하는 것입니다. 또한 하단의 filter 요소로 각 Appender가 어떤 레벨로 로그를 기록하는지 지정합니다.
encoder 요소를 통해 로그 의 표현 방식을 패턴으로 정의합니다. 다음은 대표적인 패턴입니다.
패턴 | 의미 |
%Logger{length} | 로거 이름 |
%-5level | 로그 레벨 |
%msg or %message | 로그 메세지 |
%d | 로그 기록 시간 |
%p | 로깅 레벨 |
%F | 로깅 발생 애플리케이션 파일명 |
%M | 로깅 발생 메서드 이름 |
%I | 로깅 발생 호출지 정보 |
%thread | 현재 스레드명 |
%t | 로깅 발생 스레드명 |
%c | 로깅 발생 카테고리 |
%C | 로깅 발생 클래스명 |
%m | 로그 메세지 |
%n | 줄바꿈 |
%r | 애플리케이션 실행부터 로깅 발생 시점까지 시간 |
%L | 로깅 발생 호출 지점 라인 수 |
위와 같은 패턴을 활용해 원하는 패턴을 만들 수 있습니다.
Root영역에서 어팬더를 참조해서 로깅 레벨을 설정합니다. 특정 패키지에 대해 다른 로깅 레벨을 설정하고 싶다면 root 대신 logger를 사용해 지정할 수 있습니다.
<logger name="cohttp://m.springboot.api.controller" level="DEBUG" additivity="false">
<appender-ref ref="console"/>
<appender-ref ref="INFO_LOG"/>
</logger>
logger 요소의 name 속성에는 패키지 단위로 로깅이 적용될 범위를 지정하고 level 속성으로 로그 레벨을 지정합니다.
이제 실습중인 프로젝트에 Logback 적용해 보겠습니다. Logger는 LoggerFactory를 통해 객체를 생성합니다. 이때 클래스의 이름을 함께 지정해 클래스의 정보를 Logger에서 가져가게 합니다.
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String getHello() {
// 로그 출력
LOGGER.info("getHello 메소드가 호출되었습니다.");
return "Hello World";
}
@GetMapping(value = "/name")
public String getName() {
// 로그 출력
LOGGER.info("getName 메소드가 호출되었습니다.");
return "Flature";
}
@GetMapping(value = "/variable1/{variable}")
public String getVariable1(@PathVariable String variable) {
// 컨트롤러에 들어오는 값을 포함하여 로그 출력
LOGGER.info("@PathVariable을 통해 들어온 값 : {}", variable);
return variable;
}
info 레벨에서 로그가 출력됩니다. 또한 변수를 지정해 변수로 들어오는 값을 로깅할 수도 있습니다. 변수의 값이 들어갈 부분을 중괄호로 지정하면 포매팅을 통해 로그 메시지가 구성됩니다.
정리하자면, 컨트롤러를 작성해 외부에 인터페이스를 노출하는 방법을 알아보았습니다. 또한 Swagger와 Logback도 다루어보았고 다양한 어노테이션을 통해 Swagger 페이지가 명세로써의 역할도 수행할 수 있게 잘 다듬는 연습을 했습니다. 6장의 내용을 통해 데이터베이스를 다루는 연습을 해봅시다.
6장. 데이터베이스 연동
애플리케이션은 데이터를 주고 받는 것이 주 목적입니다. 가장 널리 사용되는 마리아DB를 애플리케이션에 적용해보겠습니다.
마리아DB를 설치합니다.
https://mariadb.com/kb/en/mariadb-server-10-6-5/
계속 단계를 넘어가면 패스워드를 지정하는 페이지가 나오는데 계정의 패스워드를 지정해줍니다. 다음으로 가장 대중적으로 사용되는 문자 인코딩 방식인 UTF-8을 기본값으로 설정하기 위해 [Use UTF8 as default server's character set]에 체크합니다. 다음으로는 서버 이름과 포트 번호를 설정해야 합니다. 그 밖의 단계는 모두 다음을 눌러 기본 설정을 그대로 유지한 채로 설치를 마칩니다.
앞으로 사용할 데이터베이스의 접속 정보를 등록하기 위해 좌측 하단의 신규 버튼을 누릅니다. 세션에 나온 항목에 이름을 지정하고 우측의 입력 항목에 다음과 같이 입력하고 좌측 하단의 저장 버튼을 클릭합니다. 다음 열기 버튼을 누르면 데이터베이스에 정상적으로 접속됩니다.
ORM(Object Relational Mapping)은 객체 관계 매핑으로, 객체와 RDB의 테이블을 자동으로 매핑하는 방법입니다. 클래스는 데이터베이스의 테이블과 매핑하기 위해 만들어진 것이 아니기 때문에 어쩔 수 없는 불일치가 존재합니다. ORM은 그 불일치와 제약사항을 해결하는 역할입니다.
다음으로 ORM의 장점과 단점을 알아보겠습니다.
장점
1. 데이터베이스 쿼리를 객체지향적으로 조작할 수 있습니다.
2. 재사용 및 유지보수가 편리합니다.
3. 데이터베이스에 대한 종속성이 줄어듭니다.
단점
1. 온전한 서비스를 구현하기에는 한계가 있습니다.
2. 애플리케이션의 객체 관점과 데이터베이스의 관계 관점이 불일치가 발생합니다.
JPA(Java Persistence API)는 ORM 기술 표준으로 채택된 인터페이스 모음입니다. JPA는 더 구체화된 스펙을 포함합니다. 대표적인 JPA는 다음과 같습니다. EclipseLink, Hibernate, DataNucleus
하이버네이트는 자바의 ORM 프레임워크로 JPA가 정의하는 인터페이스를 구현하고있는 구현체 중 하나입니다. Spring Data JPA는 JPA를 편하게 사용할 수 있도록 지원하는 스프링 하위 프로젝트 중 하나입니다. CRUD 처리에 필요한 인터페이스를 제공하며 엔티티 매니저를 직접 다루지 않고 리포지토리를 정의해 사용함으로써 스프링이 적합한 쿼리를 동적으로 생성하는 방식으로 데이터베이스를 조작합니다.
영속성 컨텍스트는 애플리케이션과 데이터베이스 사이에서 엔티티와 레코드의 괴리를 해소하는 기능과 객체를 보관하는 기능을 수행합니다. 엔티티 객체가 영속성 컨텍스트에 들어오면 JPA는 엔티티 객체의 매핑 정보를 데이터베이스에 반영하는 작업을 수행합니다. 영속성 컨텍스트는 세션 단위의 생명주기를 가지는데, 세션이 생성되면 영속성 컨텍스트가 만들어지고 세션이 종료되면 영속성 컨텍스트가 사라집니다.
엔티티 매니저는 엔티티를 관리하는 객체로서, 데이터베이스에 접근하여 CRUD 작업을 수행합니다. 앞서 언급한 Spring Data JPA의 실제 내부 구현체인 SimpleJpaRepository가 리포지토리에서 엔티티 매니저를 사용합니다. 엔티티 매니저는 엔티티 매니저 팩토리가 만듭니다. 이는 데이터베이스에 대응하는 객체입니다.
엔티티 매니저 팩토리로 생성된 엔티티 매니저는 엔티티를 영속성 컨텍스트에 추가해서 영속 객체로 만드는 작업을 수행하고, 영속성 컨텍스트와 데이터베이스를 비교하면서 실제 데이터베이스를 대상으로 작업을 수행합니다.
엔티티의 생명주기는 비영속, 영속, 준영속, 삭제 총 네가지 상태로 구분됩니다.
비영속은 영속성 컨텍스트에 추가되지 않은 엔티티 객체 상태를 말합니다.
영속은 영속성 컨텍스트에 추가되어 엔티티 객체가 관리되는 상태를 말합니다.
준영속은 영속 엔티티 객체가 컨텍스트와 분리된 상태를 말합니다.
삭제는 영속성 컨텍스트에 삭제를 요청한 상태입니다.
퀴즈
1. 웹 어플리케이션 서버에서 값을 가져올 때 사용하는 API는? GET API
2. GET 요청을 구현할 때 쿼리 형식으로 값을 전달하는 형식이 있다. 이때 이 형식을 처리하는 방법은? @RequestParam
3. 어떤 값이 들어올 지 모르는 상태에서 사용할 수 있는 객체는? Map 객체
4. 다른 레이어 간의 데이터 교환에 활용되는 데이터 객체는? DTO
5. POST API와 GET API의 차이는? 전자는 바디에 담아 서버에 전달, 후자는 URL 경로나 파라미터에 변수를 넣어 요청
6. 빈칸에 들어갈 단어를 작성하시오. @RequestParam
@DeleteMapping(value="/request1")
public String getRequestParam1([ ] String email){
return "e-mail : " + email;
}
7. 빈칸에 들어갈 단어를 작성하시오. @RequestBody
@PostMapping(value="/member")
public String postMember([ ] Map<String, Object> postData){
StringBuilder sb = new StringBuilder();
postData.entrySet().forEach(map -> {
sb.append(map.getKey() + " : " + map.getValue() + "\n");
});
return sb.toString();
}
8. 엔티티의 생명주기 중 하나로, 영속성 컨텍스트에 의해 객체가 관리되는 상태는? 영속
9. Logback 설정에서 로그의 형태를 설정하고 어떤 방법으로 출력할지 설정하는 곳은? Appender 영역
10. Appender의 대표적인 구현체 중 메일로 로그를 전송하는 구현체는? SMTPAppender
[출처] 장정우, 『스프링 부트 핵심 가이드』, 위키북스(2022), p.55-103.
에디터 채드
[스프링1] 7장. 테스트 코드 작성하기 (0) | 2023.11.17 |
---|---|
[스프링1] 6. 데이터베이스 연동 (0) | 2023.11.10 |
[스프링 1팀] 1장 ~ 4장. 스프링 부트란? + 개발에 앞서 알면 좋은 기초 지식 + 개발환경 구성 + 스프링부트 애플리케이션 개발하기 (0) | 2023.10.13 |
[스프링1] 섹션6. 스프링 DB 접근 기술 JPA (0) | 2023.10.06 |
[스프링1] 섹션 4. 스프링 빈과 의존관계 (0) | 2023.09.29 |