3. 블로그 글 작성을 위한 API 구현하기
- 이전 시간까지 엔티티 구성이 끝났으므로, API를 하나씩 구현해보자.
- API 구현 과정 ( 요청 방향과 반대로 구현 )
① 서비스 클래스에서 메서드 구현
② 컨트롤러에서 사용할 메서드 구현
③ API 테스트
클라이언트 -- 요청 ( POST /api/articles ) --> 컨트롤러 -- save( ) --> 서비스 -- save( ) --> 리포지터리
1) 서비스 메서드 코드 작성하기
◉ 서비스 계층에 블로그에 글을 추가하는 코드 작성
- 서비스 계층에서 요청을 받을 객체인 AddArticleRequest 객체 생성
- BlogService 클래스 생성한 다음 블로그 글 추가 메서드인 save( ) 구현
◎ dto 패키지 생성 후, 컨트롤러에서 요청 본문을 받을 객체인 AddArticleRequest.java 파일 생성
< DTO와 DAO 정리 >
▪︎ DTO ( Data Transfer Object )
- 계층끼리 데이터를 교환하기 위해 사용하는 객체
- 로직을 가지지 않는 순수한 데이터 객체( Getter & Setter만 가진 클래스 )
▪︎ DAO ( Data Access Object )
- 데이터베이스와 연결되고 데이터를 조회하고 수정하는데 사용하는 객체
- 데이터베이스에 접근하기 위한 로직과 비즈니스 로직을 분리하기 위해 사용
▪︎ AddArticleRequest.java
package org.choongang.dto;
import org.choongang.domain.Article;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class AddArticleRequest {
private String title;
private String content;
// * 생성자를 사용해 객체 생성
// : Article.java의 생성자 위에 @Builder 추가 -> 생성자 호출 가능
public Article toEntity() {
return Article.builder() // 롬복이 빌더 패턴을 찾아 생성자 이용 가능
.title(title)
.content(content)
.build();
}
}
• toEntity( ) 메서드
- 빌더 패턴을 사용해 DTO를 엔티티로 만들어주는 메서드
→ 추후 블로그 글을 추가할 때 저장할 엔티티로 변환하는 용도로 사용함
- Article.java에서 생성자 위에 롬복의 애너테이션인 '@Builder' 추가
→ toEntity( ) 메서드에서 빌터 패턴 이용해 생성자 이용 가능
◎ service 패키지 생성 후, BlogService.java를 생성해 BlogService 클래스 구현
▪︎ BlogService.java
package org.choongang.service;
import java.util.List;
import org.choongang.domain.Article;
import org.choongang.dto.AddArticleRequest;
import org.choongang.repository.BlogRepository;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor // final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service // 비즈니스 로직 처리, 빈으로 등록 ( ∵ @Component가 존재 )
public class BlogService {
// * 데이터베이스와 작업 모델 ( 특히 JPA의 모델은 리포지토리 ) 멤버변수 선언
// 의존성 주입 방법
private final BlogRepository blogRepository; // 보통 클래스명의 소문자로 객체변수 선언
// * 블로그 글 추가 메서드 -> 리포지토리에 의뢰 : 보통 메서드 이름을 동일하게 설정함
// => jpa의 save() : sql의 insert into 명령문을 자동으로 생성
// * AddArticleRequest : 컨트롤러의 요청 본문을 받을 서비스 계층의 객체
// - toEntity() 메서드 : Article 객체의 빌더패턴을 사용 → 생성자 방식으로 새로운 글이 추가됨
public Article save(AddArticleRequest request) {
return blogRepository.save(request.toEntity());
}
❶ @RequiredArgsContructor
- 빈을 생성자로 생성하는 롬복에서 지원하는 애너테이션
- final 키워드나 @NotNull이 붙은 필드로 생성자를 만들어줌
→ 클래스에 선언된 final 변수들, 필드들을 매개변수로 하는 생성자를 자동으로 만들어줌
- 주로 DI( 의존성 주입 ) 중 Constructor Injection( 생성자 주입 )을 임의의 코드 없이 자동으로 설정
* @RequiredArgsContructor 적용 전
public class LombokTest {
private final String id;
private final String name;
@Autowired
public LombokTest( String id, String name ){
this.id = id ;
this.name = name ;
}
}
* @RequiredArgsContructor 적용 후
@RequiredArgsConstructor
public class LombokTest {
private final String id;
private final String name;
}
⇒ 새로운 필드를 추가할 때 다시 생성자를 만드는 번거로움을 없앨 수 있음
( @Autowired 사용하지 않고 의존 주입 )
❷ BlogService의 save( ) 메서드
public Article save(AddArticleRequest request) {
return blogRepository.save(request.toEntity());
}
• request.toEntity( )
- AddArticleRequest객체의 toEntity() 메서드 호출 결과
: Article 클래스의 생성자를 빌더패턴으로 이용해 Article 객체 생성
• BlogRepository.save( )
- JpaRepository에서 지원하는 저장 메서드인 save( ) 메서드로 AddArticleRequest 클래스에 저장된 값들을 article 데이터베이스에 저장함
2) 컨트롤러 메서드 코드 작성하기
◉ 이제 URL에 매핑하기 위한 컨트롤러 메서드 추가
▪︎ 컨트롤러 메서드의 URL 매핑 애너테이션 ( HTTP 메서드에 대응함 )
: @GetMapping, @PostMapping, @PutMapping, @DeleteMapping 등 사용 가능
▪︎ /api/articles에 POST 요청이 들어오는 경우
❶ @PostMapping을 이용해 요청을 매핑 : 해당 컨트롤러 찾음
❷ 컨트롤러에서 블로그 글을 생성하는 BlogService의 save( ) 메서드를 호출
❸ 그 결과, 생성된 블로그 글을 반환하는 작업을 할 addArticle( ) 메서드를 작성
- 전체적인 흐름
BlogService 객체 : 비즈니스 로직 처리 → BlogRepository 객체 : 데이터베이스 처리 후, Article 타입 객체로 반환됨 → 데이터베이스에 저장
◎ controller 패키지 생성 후, BlogApiController.java 파일 생성
▪︎ BlogApiController.java
package org.choongang.controller;
import java.util.List;
import org.choongang.domain.Article;
import org.choongang.dto.AddArticleRequest;
import org.choongang.dto.ArticleResponse;
import org.choongang.service.BlogService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor // final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
// 빈을 생성자로 생성하는 롬복에서 지원하는 애너테이션
@RestController // HTTP Response Body에 객체 데이터를 JSON 형식으로 반환하는 컨트롤러
public class BlogApiController {
// * 의존성 주입 중 생성자 주입에 해당
// → @RequiredArgsConstructor으로 인해 임의의 코드없이 가능
private final BlogService blogService;
// * HTTP 메서드가 POST일 때 전달받은 URL와 동일하면 메서드로 매핑
@PostMapping("/api/articles")
// * 요청 본문 값 매핑
// * 요청 API 처리
// - AddArticle() 메서드
// : 웹 브라우저가 제공한 양식 데이터(JSON 데이터)를 데이터베이스에 저장하는 메서드
// 양식 데이터 -> POST 방식 / 데이터 전처리 객체인 BlogService 호출
public ResponseEntity<Article> addArticle(@RequestBody AddArticleRequest request){
Article savedArticle = blogService.save(request);
// * 요청한 자원이 성공적으로 생성되었으며, 저장된 블로그 글 정보를 응답 객체에 담아 전송
// : 응답코드로 201, 즉 Created를 응답하고 테이블에 저장된 객체를 반환함
return ResponseEntity.status(HttpStatus.CREATED)
.body(savedArticle);
}
}
◉ Spring에서 컨트롤러를 지정해주기 위한 어노테이션
• 전통적인 Spring MVC 컨트롤러인 @Controller와 Restful 웹 서비스의 컨트롤러인 @RestController 2종류로 분류
• 두 어노테이션의 차이점 : HTTP Response Body가 생성되는 방식
• @Controller
- 주로 View를 반환하기 위해 사용함
- Data를 반환해야 하는 경우도 존재 ( JSON 형태로 데이터 반환 가능 )
• @RestController
- @Controller에 @ResponseBody가 추가된 것
- 주로 JSON 형태로 객체 데이터를 반환하기 위해 사용함
- 데이터를 응답으로 제공하는 REST API를 개발할 때 주로 사용하며, 객체를 ResponseEntity로 감싸서 반환함
◉ Spring Framework에서 비동기 통신, 즉 API 통신을 구현하기 위한 어노테이션
▪︎ HTTP(HyperText Transfer Protocol) 통신이란?
- HTTP 통신 방식은 기본적으로 '요청과 응답(request, response)'로 이루어져 있음
- 즉, 클라이언트가 요청(HttpRequest)을 서버에 보내면 서버는 클라이언트에게 응답(HttpResponse)하는 구조
▪︎ HttpRequest, HttpResponse 구조
- HttpRequest 구조 : start line / headers / body 세 부분으로 구성
- HttpResponse 구조 : status line / headers / body 세 부분으로 구성
- 클라이언트와 서버 간의 HTTP 통신에서 요청과 응답을 보낼 때, 필요한 데이터를 담아서 보내는 공간이 body!
요청하는 요청 본문을 requestBody, 응답하는 응답 본문을 responseBody라고 함
- body에 담기는 데이터 형식은 여러 종류가 있으나, 가장 대표적으로 사용되는 것이 JSON이며, xml 형식도 사용됨
• 요청 (클라이언트 → 서버) : @RequestBody
응답 (서버 → 클라이언트) : @ResponseBody
⇒ 이와 같은 어노테이션을 명시함으로써 MessageConverter를 통한 데이터 변환 과정을 거치게 됨
• @RequestBody
- 클라이언트에서 서버로 JSON 형식의 requestBody로 요청 데이터를 전송했을 때, Java에서는 해당 JSON 형식의 데이터를 받기 위해서 JSON 기반의 HTTP Body를 자바 객체로 반환
- 즉, HTTP를 요청할 때 응답에 해당하는 값을 @RequestBody 애너테이션이 붙은 대상 객체인 AddArticleRequest에 매핑함
• @ResponseBody
- 요청된 데이터를 처리 후, 서버에서 클라이언트로 다시 응답데이터를 보낼 때 자바 객체를 JSON 기반의 HTTP Body로 변환
◉ ResponseEntity
- Spring Framework에서 제공하는 클래스 중 HttpEntity 클래스가 존재
- HttpEntity 클래스 : HTTP 요청(Request) 또는 응답(Response)에 해당하는 HttpHeader와 HttpBody를 포함하는 클래스
→ HttpEntity 클래스를 상속받아 구현한 클래스가 RequestEntity, ResponseEntity 클래스
- RequestEntity 클래스 : 사용자의 HttpRequest에 대한 응답 데이터를 포함하는 클래스 → HttpStatus, HttpHeaders, HttpBody를 포함
◉ HTTP Status Code(HTTP 상태코드)
- 클라이언트가 보낸 HTTP 요청에 대한 서버의 응답을 코드로 표현한 것으로, 해당 코드로 요청의 성공 / 실패 / 실패요인 등을 알 수 있음
- 응답코드
응답코드 | 설명 |
200 OK | 요청이 성공적으로 수행되었음 |
201 Created | 요청이 성공적으로 수행되었고, 새로운 리소스가 생성되었음 |
400 Bad Request | 요청 값이 잘못되어 요청에 실패했음 |
403 Forbidden | 권한이 없어 요청에 실패했음 |
404 Not Found | 요청 값으로 찾은 리소스가 없어 요청에 실패했음 |
500 Internal Server Error | 서버 상에 문제가 있어 요청에 실패했음 |
▪︎ JPA의 save( ) 메서드 : 기본적으로 잘 처리되면 저장한 결과를 확인하기 위해 테이블의 저장결과를 다시 검색(Select)한 레코드를 반환함 → 그 결과를 통해 잘 저장되었음을 확인할 수 있음
▪︎ BlogService가 save( ) 메서드를 잘 처리한 후, 반환한 Article 객체를 Spring Boot가 자동으로 응답하기 위해서는 응답객체에 등록해야 함 → ResponseEntity에 현재 처리 상황을 등록하면 자동으로 다음 처리가 이루어짐
▪︎ 응답 상황은 정상적인 처리(HttpStatus.CREATED=200계열)인 경우를 의미
▪︎ HttpStatus.CREATED 상태 : 내부에서 자동전송 하므로 보낼 응답 결과(savedArticle)를 내용 속성에 등록하면 됨
3) API 실행 테스트하기
◉ 서비스 메서드, 컨트롤러 메서드를 구현 즉, API를 완성했으니 API가 잘 동작하는지 테스트해보자.
- 실제 데이터를 확인하기 위해 H2 콘솔을 활성화해야 함
→ application.yml 파일 수정
① application.yml 수정
# 서버 포트를 웹 기본포트인 80으로 설정
server:
port: 80
# 뷰 모델 템플릿 엔진을 jsp로 변경
spring:
mvc:
view:
# jsp 파일 시작 경로 지정 : /scr/main/webapp/WEB-INF/view/를 의미
prefix: /WEB-INF/view/
suffix: .jsp
# 데이터베이스 모델을 ORM 기술인 JPA 사용
jpa:
# jpa로 데이터베이스 처리하는 실제 sql 명령문을 로그로 보이도록 설정
show-sql: true
properties:
hibernate:
# JPA 기술 스펙의 실제 구현 패키지인 hibernate에서 sql 생성 시 정렬해서 보이도록 설정
format-sql: true
defer-datasource-initialization: true
# H2 활성화시키기 위해 코드 추가
datasource:
url: jdbc:h2:mem:testdb
h2:
console:
enabled: true
② 스프링 부트 서버 실행 후, 포스트맨 실행
- HTTP 메서드 : POST
- URL : http://localhost/api/articles
- Body : JSON
- 요청 창에 테스트용 데이터를 JSON 형식으로 작성 후, Send 클릭 시 응답 값 확인 가능
③ H2 데이터베이스에 잘 저장되었는지 확인
- 웹 브라우저에서 'localhost/h2-console'에 접속
단, 스프링 부트 서버는 여전히 실행 중인 상태여야 함
- H2 콘솔 로그인 화면이 출력 → 각 항목의 값을 잘 확인해 입력한 뒤 Connect를 클릭해 로그인
- 스프링 부트 서버 안에 내장되어 있는 H2 데이터베이스에 접속하고 데이터 확인 가능
④ [SQL statement:] 입력 창에 'SELECT * FROM ARTICLE'을 입력한 뒤 [Run]을 클릭하여 쿼리 실행
→ H2 데이터베이스에 저장된 데이터 확인 가능
- 또한, 왼쪽 화면에서 'ARTICLE'이라는 테이블 확인 가능
→ 애플리케이션을 실행하면 자동으로 생성한 엔티티 내용을 바탕으로 테이블이 생성되고, 테스트로 요청한 POST 요청에 의해 데이터가 실제로 저장됨을 확인할 수 있음
4) 반복 작업을 줄여 줄 테스트 코드 작성하기
◉ H2 콘솔에 접속해 쿼리 입력하여 데이터가 저장되어있는지, 그것이 실제로 들어있는지 확인하는 작업을 매번 테스트하기에 번거로움
→ 작업을 줄여줄 코드를 작성해보자
① BlogApiController 클래스 클릭 후, New - Others - test - JUnit Test Case 클릭
: src/test/java/패키지 아래에 BlogApiControllerTest.java 파일 생성
▪︎ BlogApiController.java
package org.choongang.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.List;
import org.choongang.domain.Article;
import org.choongang.dto.AddArticleRequest;
import org.choongang.repository.BlogRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; // 테스트용 MVC 환경 자동설정
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
// 스프링부트 테스트를 이용한 테스트를 할 경우, 반드시 '@SpringBootTest' 애너테이션 추가
@SpringBootTest // 테스트용 애플리케이션 컨텍스트
@AutoConfigureMockMvc // MockMVC 생성
class BlogApiControllerTest {
// 멤버변수 선언
@Autowired
protected MockMvc mockMvc;
// 객체 데이터들(DTO, VO) 사이의 데이터 매핑
@Autowired
protected ObjectMapper objectMapper;
// 가상 테스트 웹 환경 공간을 생성할 때 필요한 객체
@Autowired
private WebApplicationContext context;
// 이 이후에는 테스트에 필요한 객체들이 나옴
@Autowired
BlogRepository blogRepository;
@BeforeEach // 테스트 메서드 실행 전, 매번 실행되는 메서드로 초기화 담당
// mockMVC 객체 생성 및 초기화
public void mockMvcSetUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
// 새로 테스트 메서드 실행 전, 기존 DB 데이터 초기화
blogRepository.deleteAll();
}
}
▪︎ ObjectMapper 클래스
- 이 클래스로 만든 객체 : 직렬화 또는 역직렬화할 때 사용함
- 직렬화(Serialize) : 자바 시스템 내부에서 사용하는 자바 객체 → 외부에서 사용할 수 있도록 JSON 데이터로 변환
- 역직렬화(Deserialize) : JSON 데이터 → 자바 객체로 변환
- 직렬화 / 역직렬화를 사용하는 이유
: 복잡한 데이터 구조의 클래스의 객체라도 직렬화 기본 조건만 지키면 큰 작업 없이 바로 직렬화, 역직렬화가 가능함
( 기본 조건 : java.io.Serializable 인터페이스를 상속받은 객체여야 함 )
▪︎ 블로그 글 생성 API 테스트하는 코드 작성
: given - when - then 패턴 생성
given | 블로그 글 추가에 필요한 요청 객체를 만듦 |
when | 블로그 글 추가 API에 요청을 보냄 이 때 요청 타입은 JSON이며, given절에서 미리 만들어둔 객체를 요청 본문으로 함께 보냄 |
then | 응답 코드가 201 Created인지 확인함 Blog를 전체 조회해 크기가 1인지 확인하고, 실제로 저장된 데이터와 요청 값을 비교함 |
//// 기능 테스트 ////
@DisplayName("addArticle : 블로그 글 추가에 성공한다.")
@Test
public void addArticle() throws Exception{
// * given
// : 블로그 글 추가에 필요한 요청 객체를 만듦.
// - 웹으로 RestAPI를 테스트하는 것 → API 경로 Uri 필요
// - 아티클을 추가하므로 추가하는 내용 → 즉 title, content 변수에 테스트할 데이터 추가
final String url = "/api/articles";
final String title = "title";
final String content = "content";
// - AddArticleRequest DTO 클래스에 데이터를 추가한다.
final AddArticleRequest userRequest = new AddArticleRequest(title, content);
// - userRequest 객체를 JSON으로 직렬화
final String requestBody = objectMapper.writeValueAsString(userRequest);
// * when
// : 설정한 내용을 바탕으로 블로그 글 추가 API에 요청 전송
// - 위에서 설정한 조건값을 기초로 해서 인터넷을 통하여
// WAS 서버에 요청하는 것과 같은 가상 테스트 실행을 한다.
// 설정한 내용을 가상 테스트 컨텍스트에 요청 전송한다.
// - mockMvc : 가상 테스트 웹 컨텍스트
// - perform : 다음 요청 메서드를 수행하라
// - contentType : 전송하는 데이터 형식 지정
// - MediaType : 전송하는 데이터 형식 지정
// - MediaType.APPLICATION_JSON_VALUE : 보내는 데이터형식 JSON 객체 형식
// - content : 실제 요청 시 보낼 데이터
ResultActions result = mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(requestBody));
// * then
// : 우리가 기대하는 값과 같은지 비교
// - result : 위의 테스트 요청 실행 결과
// - andExpect : 다음의 결과를 기대한다.
// - status() : 응답 결과
// - isCreated() : 응답코드 201 JSON 등의 리소스가 만들어졌다.
result.andExpect(status().isCreated());
// 위의 실행결과가 기대값( 진짜로 DB에 잘 들어갔는지? )이 성립되는지 확인해보자
// 레포지토리의 findAll() : JPA에서 모든 레코드를 가져오는 메서드
// → SELECT * FROM Article ;
List<Article> articles = blogRepository.findAll();
// @BeforeEach에서 테스트 WAS 서버를 초기화하고
// 기존에 이미 들어있는 데이터를 모두 삭제했으므로,
// addArticle()가 잘 실행되었다면, 레코드 크기가 1일 것이다.
// article.size() : 리스트 컬렉션의 크기 구하기
assertThat(articles.size()).isEqualTo(1);
// articles.get(인덱스번호) : articles의 첫번째 레코드를 가져와라
assertThat(articles.get(0).getTitle()).isEqualTo(title);
assertThat(articles.get(0).getContent()).isEqualTo(content);
}
• MockMvc
- 실제 객체와 비슷하지만, 테스트에 필요한 기능만 가지는 가짜 객체를 만들어서 애플리케이션 서버에 배포하지 않고도 스프링 MVC 동작을 재현할 수 있는 클래스를 의미
- MockMvc의 메서드
perform( ) : 요청을 전송하는 역할 → 결과로 ResultActions 객체를 반환
ResultActions 객체의 andExpect( ) 메서드로 리턴 값을 검증하고 확인 가능
• post( ) : HTTP 메서드 결정 → 인자로 url 경로 이용
• contentType( ) : 요청을 보낼 때 JSON, XML 등 다양한 타입 중 하나를 골라 요청을 보냄
→ contentType(MediaType.APPLICATION_JSON_VALUE)
: 요청을 JSON TYPE의 데이터만 담고 있는 요청을 처리하겠다는 의미
▪︎ 테스트 실행 결과
'백엔드 > Spring Boot' 카테고리의 다른 글
6. 블로그 기획하고 API 만들기 (3) - 블로그 글 작성을 위한 API 구현❸ (1) | 2023.07.31 |
---|---|
6. 블로그 기획하고 API 만들기 (3) - 블로그 글 작성을 위한 API 구현❷ (0) | 2023.07.27 |
6. 블로그 기획하고 API 만들기 - (2) 블로그 개발을 위한 엔티티 구성하기 (0) | 2023.07.26 |
JSP에 CSS 및 Javascript 연결 (0) | 2023.07.25 |
6. 블로그 기획하고 API 만들기 (1) - API와 REST API (0) | 2023.07.24 |