API, REST API
API란?
- 네트워크에서 API는 프로그램 간에 상호작용을 위한 매개체를 말한다.
- 식당을 비유로 들어보자.
- 우리는 식당에 가면 주방으로 바로 가는 것이 아닌, 점원에게 주문을 요청하고, 점원은 주방에 가서 요리를 만들어달라고 요청한다. 그리고 요리가 완료되면 우리에게 음식이 서빙된다.
- 손님(주문자, 주문 요청) -> 점원 (주방에 요리 요청) -> 주방(요리를 만들어서 다시 점원에게 전달) -> 점원(손님에게 요청에 맞는 요리를 서빙)
- 여기서 손님은 클라이언트, 주방을 서버라고 생각하고, 점원을 API라고 생각하면 된다.
- 웹 사이트에서도 마찬가지이다.
- 사용자는 웹 사이트의 주소를 입력해서 '네이버 웹페이지 보여줘'라고 요청을 API에게 보낸다.
- API는 사용자의 요청을 받아서 서버로 들고 간다.
- 서버는 API에게 전달받은 요청을 처리해 결과(데이터)를 만들고 다시 API로 전달한다.
- API는 서버로부터 전달받은 데이터를 브라우저(사용자 측)에 전달한다.
- 그러면 사용자는 요청한 네이버 웹페이지를 볼 수 있게 된다.
REST API (Representational State Transfer)
- Rest API는 웹의 장점을 최대한 활용하는 API이다.
- 자원을 이름으로 구분하여 자원의 상태를 주고받는 API 방식이다. = 명확하고 이해하기 쉬운 API
- 어려울 것 없다. 개발자들이 따르는 URL 설계 방식을 말하는 것이다. (스프링 부트, 리액트 등의 기술이라고 착각 X)
- REST하게 디자인한 API를 RESTful API라고 부르기도 한다.
특징
- Rest API는 서버/클라이언트 구조, 무상태, 캐시 처리, 계층화, 인터페이스 일관성 같은 특징을 가지고 잇다.
- 장점
- URL만 보고도 무슨 처리를 하는 API인지 명확하게 알 수 있다.
- 상태가 없다는 특징으로 인해 클라이언트와 서버의 역할이 명확하게 분리된다.
- HTTP 표준을 사용하는 모든 플랫폼에서 사용할 수 있다.
- 단점
- HTTP 메서드(GET, POST..)와 같은 방식의 개수에 제한이 있다.
- 설계를 하기 위해 공식적으로 제공되는 표준 규약이 없다.
사용 방법
1. URL에는 동사를 쓰지 말고, 자원을 표시해야 한다.
- 여기서 자원은 가져오는 데이터를 말한다.
- ex
- id = 1인 학생의 정보를 가져오는 URL = "/student/1", "/articles/1"
- "/get-student?student_id=1", "/articles/show/1 이렇게 작성하는 건 자원이 아닌 다른 표현을 섞고, 동사를 사용했으므로 추후 개발 시 혼란을 줄 수 있기 때문에 적합한 RESTful API 방식이 아니다.
- 서버에 데이터를 요청하는 URL을 설계할 때, 누구는 get을 누구는 show를 쓴다면 엉망이 될 것이기 때문.
- 따라서 동사는 쓰지 않는다.
- id = 1인 학생의 정보를 가져오는 URL = "/student/1", "/articles/1"
2. 동사는 HTTP 메서드로 해결한다.
- HTTP 메서드는 서버에 요청하는 방법을 나눈 것이다.
- 주로 사용하는 HTTP 메서드는 GET(읽고), POST(만들고), PUT(업데이트하고), DELETE(삭제)이다. = CRUD
- URL에 입력하는 값이 아니라, 내부에서 처리하는 방식을 미리 정하는 것이다.
프로젝트 준비하기
프로젝트 준비
- 이전에 테스트를 위해 작성한 코드를 삭제한다. src/main/resources 폴더에 있는 application.yml, SpringBootDeveloperApplication.java 파일을 제외한 나머지 파일을 삭제 해야 하는데.. 학습했던 파일을 다 지울 순 없으니 깃포크를 사용하여 github에 올려보도록 하자.
깃포크를 사용하여 깃허브 연결
1. 먼저 나의 깃허브에 새로운 repository를 생성해주었다. 이름은 "springboot-developer-2rd"로 생성했다.
2. 레포지터리 링크를 복사한다.
3. 깃포크로 이동한다. File -> create new local repository를 클릭한다.
4. 여태까지 작업했던 프로젝트 파일을 찾아서 폴더를 열어준다.
5. 그러면 프로젝트이름과 같은 이름을 가진 깃포크 레포지터리가 생성된다.
6. 이 레포지터리를 깃허브와 연결해야 하므로, Remotes 우클릭 -> add new remotes 를 클릭해서 -> 생성해둔 깃허브 레포지터리 URL을 불여넣는다. -> test connection을 진행하여 정상적으로 잘 되는지 테스트 하고, add new Remote를 눌러서 생성해준다.
7. 그러면 이제 깃허브에 생성한 레포지터리와 깃포크가 연동된다! 로컬에서 작업해준 코드들을 깃허브에 올리기 위해 push를 해주면, 잘 업로드 된 것을 확인할 수 있다.
8. 이후 branch를 하나 더 생성했고, push 해주었다. 이제 여기서 블로그 프로젝트를 시작해보도록 하겠다.
9. 이제 src/main/resources 폴더에 있는 application.yml, SpringBootDeveloperApplication.java 파일을 제외한 나머지 파일을 삭제해보자.
블로그 개발을 위한 엔티티 구성하기
- 엔티티를 구성하고, 구성한 엔티티를 사용하기 위한 repository를 추가해보자.
Entitiy 구성하기
- 엔티티와 매핑되는 테이블 구조
컬럼명 | 자료형 | null 허용여부 | 키 | 설명 |
id | BIGINT | N | 기본키(PK) | 일련번호, 기본키 |
title | VARCHAR(255) | N | 게시물의 제목 | |
content | VARCHAR(255) | N | 내용= |
- springbootDeveloper 패키지 하위에 domain이라는 이름의 패키지 생성, Article.java 클래스 생성 후 코드 작성
package me.jinsoyeong.springbootdeveloper.domain;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity // 엔티티로 지정
@Getter
// 원래 자바 컴파일러가 기본 생성자를 자동으로 생성해주지만,
// 클래스에 다른 생성자가 있을 경우 기본 생성자를 생성하지 않기 때문에 오류 발생, 따라서 애너테이션이 필요함
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Article {
@Id //id 필드를 기본키로 지정
@GeneratedValue(strategy = GenerationType.IDENTITY) // 기본키 자동으로 1 증가
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "title", nullable = false)
private String title;
@Column(name = "content", nullable = false)
private String content;
/*
* @Builder 태너테이션
* - 롬복에서 지원하는 애너테이션
* - 생성자 위에 사용하면, 빌더 패턴 방식으로 객체를 생성할 수 있음
* - 빌터 패턴: 객체를 유연하고, 직관적으로 생성할 수 있기 때문에 개발자들이 애용하는 디자인 패턴이다.
* - 빌터패턴을 사용하면 어느 필드에 어떤 값이 들어가는지 명시적으로 파악 가능
* @Builder를 클래스 위에 붙이면 모든 필드 포함 (보통 JPA에서는 지양)
* @Builder를 생성자 위에 붙이면 해당 생성자의 매개변수만 빌더에 포함됨
→ JPA 엔티티에서는 보통 생성자 위에 붙이는 것이 더 안전한 선택
**/
@Builder
public Article(String title, String content){
this.title = title;
this.content = content;
}
}
** 빌더 패턴
// 빌더 패턴을 사용하지 않았을 때
new Article("abc", "def");
// 빌터 패턴을 사용했을 때
Article.builder()
.title("abc")
.content("def")
.build();
-> Article 객체를 생성 시, title = "abc", content = "def"값으로 초기화한다고 가정할 때,
builder 패턴을 사용하지 않으면 "abc, "def"는 어느 필드에 들어가는 값인지 파악하기 어렵다.
따라서 어느 필드에 어느 값이 매칭되는지 바로 확인할 수 있도록 빌더패턴을 사용한다. 객체 생성 코드의 가독성이 높기 때문
@builder 애너테이션은 빌더 패턴을 사용하기 위한 코드를 자동으로 생성하므로 간편하게 빌더 패턴을 사용해 객체를 만들수 있는 것이다.
레포지터리 생성
- springbootdeveloper 패키지에 repository 하위 패키지를 만들고, BlogRepository.java 파일을 생성 후, BlogRepository 인터페이스 생성
/*
* JpaRepository<T, ID>는 Spring Data JPA에서 제공하는 인터페이스
* T → 관리할 엔티티 타입 (예: Article)
ID → 엔티티의 기본 키(Primary Key) 타입 (예: Long)
* Article 엔티티의 기본키가 Long 타입임을 의미하는 것임
* */
public interface BlogRepository extends JpaRepository<Article, Long> {
}
- 엔티티 Article과 엔티티의 PK타입 Long을 인수로 넣는다.
- 이제 이 레포지터리를 이용할 때, JpaRepository에서 제공하는 여러 메서드를 사용할 수 있게 됨
블로그 글 작성을 위한 API 구현
- 서비스 클래스에서 메서드를 구현
- 컨트롤러에서 사용할 메서드를 구현
- 이후 API를 실제로 테스트
서비스 메서드 코드 작성
- 블로그에 글을 추가하는 코드를 서비스 계층에 작성해보자.
- 서비스 계층에서 요청을 받을 객체인 AddArticleRequest 객체를 생성하고, BlogService 클래스를 생성한 다음, 글 추가 메서드 save()를 구현
1. dto
- springbootdeveloper 패키지에 dto 패키지를 생성한 다음, dto 패키지에 컨트롤러에서 요청한 본문을 받을 객체인 AddArticleRequest.java 파일을 생성한다.
- DTO는 계층끼리 데이터를 교환하기 위해 사용하는 객체이다. 단순히 데이터를 옮기기 위해 사용하는 전달자 역할을 하는 객체이기 때문에 별도의 비즈니스 로직을 포함하지 않는다.
package me.jinsoyeong.springbootdeveloper.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import me.jinsoyeong.springbootdeveloper.domain.Article;
@NoArgsConstructor // 기본 생성자 추가
@AllArgsConstructor // 모든 필드 값을 파라미터로 받는 생성자 추가
@Getter
public class AddArticleRequest {
private String title;
private String content;
// Article 클래스에서 생성한 빌터 패턴 생성자를 사용하여 객체 생성
// DTO를 엔티티로 만들어주는 메서드
// 추후 블로그 글을 추가할 때 저장할 엔티티로 변환하는 용도로 사용됨
public Article toEntity(){
return Article.builder()
.title(title)
.content(content)
.build();
}
}
Q. 이전에는 @NoArgsConstructor(access = AccessLevel.PROTECTED)을 사용했는데,
지금 코드에서는 @NoArgsConstructor만 사용 ** Why?
A. 이 차이는 해당 클래스가 JPA 엔티티인지, DTO(Data Transfer Object)인지에 따라 달라진다.
1. 클래스 유형이 JPA 엔티티인 경우 @NoArgsConstructor(access = AccessLevel.PROTECTED) 설정
- JPA는 기본 생성자를 사용해 객체를 생성하는데, 이때 public 생성자로 열려 있으면 실수로 직접 호출해서 객체를 생성할 가능성이 있음.
- 그래서 PROTECTED로 설정해서 JPA는 사용할 수 있지만, 외부에서는 직접 호출하지 못하도록 제한하는 게 좋음.
- 특히, @Builder 패턴을 사용할 때 생성자를 PROTECTED로 두는 것이 권장됨.
2. 클래스 유형이 DTO인 경우 @NoArgsConstructor 사용
- DTO는 외부에서 데이터를 주고받는 역할만 하므로, 제한할 필요가 없음
- 보통 JSON 데이터를 매핑하거나 클라이언트에서 값을 받을 때 기본 생성자가 필요함
- 따라서 기본 생성자를 PUBLIC으로 열어둬도 문제 없음
Q. 여기서는 클래스 내에서 다른 생성자를 사용하지 않는데 왜 NoArgsContructor 애너테이션을 사용하고 있을까?
A. @AllArgsConstructor가 모든 필드를 매개변수로 받는 생성자를 자동으로 생성하기 때문에, 자바의 기본 생성자가 자동으로 생성되지 않음!!
그래서 @NoArgsConstructor를 명시적으로 추가해야 기본 생성자가 생성됨.
Q. Article 클래스에서 생성자에만 @Builder를 사용해도 Article.builder()로 빌드 메서드를 사용할 수 있는걸까?
A. @Builder를 생성자에 적용하면 Lombok이 자동으로 내부 static 클래스를 생성해 주기 때문에 가능하다. 즉, 클래스 위에 @Builder를 붙인 것과 동일한 방식으로 Article.builder()가 생성됨.
단, @Builder를 생성자에만 적용하면, id는 빌더에 포함되지 않음!
만약 클래스 전체에 @Builder를 적용했다면?
➡️ id까지 포함된 Article.builder().id(1L).title("제목").content("내용").build(); 같은 코드도 가능함. 하지만 JPA에서는 id를 자동 생성하는 경우가 많으므로, 일반적으로 id를 빌더에서 제외하는 게 더 좋음. 그래서 JPA 엔티티에서는 생성자 위에만 @Builder를 붙이는 게 일반적이다.
2. service
- springbootdeveloper 패키지에 service 하위 패키지 생성 -> BlogService.java 생성
package me.jinsoyeong.springbootdeveloper.service;
import lombok.RequiredArgsConstructor;
import me.jinsoyeong.springbootdeveloper.domain.Article;
import me.jinsoyeong.springbootdeveloper.dto.AddArticleRequest;
import me.jinsoyeong.springbootdeveloper.repository.BlogRepository;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor // final이 붙거나 @NonNull이 붙은 필드로 생성자를 추가함
@Service // 해닽 클래스를 빈으로 서블릿 컨테이너에 등록
public class BlogService {
private final BlogRepository blogRepository;
/*
* 메서드: JpaRepository에서 지원하는 저장 메서드 save()
* 역할: AddArticleRequest 클래스에 저장된 값들을 article 데이터베이스에 저장
* */
public Article save(AddArticleRequest request){
return blogRepository.save(request.toEntity());
}
}
** 참고
- BlogRepository 인터페이스는 JpaRepository를 상속받고 있음.
- JpaRepository의 부모 클래스인 CrudRepositry에 save()메서드가 선언되어 있음.
- 이 메서드를 사용하면 데이터베이스에 Article 엔티티를 저장할 수 있는 것
컨트롤러 메서드 작성
- URL에 매핑하기 위한 컨트롤러 메서드를 추가
- URL 매핑 어노테이션(@GetMapping, PostMapping, PutMapping, Deletemapping 등)을 사용 가능
- 각 메서드는 HTTP 메서드에 대응해야함
- /api/articles에 POST 요청이 오면 @PostMapping을 이용해 요청을 매핑한 뒤 -> 블로그 글을 생성하는 BlogService의 save() 메서드를 호출하고 -> 생성된 블로그 글을 반환하는 작업을 할 AddArticle()메서드를 작성
1. springbootdeveloper 패키지에 controller 패키지 생성 -> BlogApiController.java 파일 생성
@RequiredArgsConstructor // 클래스의 모든 final 필드와 @NonNull로 표시된 필드를 매개변수로 갖는 생성자를 자동으로 생성
@RestController // HTTP Response Body에 객체 데이터를 JSON 형식으로 반환하는 컨트롤러
public class BlogApiController {
private final BlogService blogService;
// HTTP 메서드가 POST일 때 전달받은 URL과 동일하면 메서드로 매핑
@PostMapping("/api/articles")
// @RequestBody로 요청 본문 값 매핑, 응답에 해당하는 값을 대상 객체인 AddArticleRequest에 매핑
public ResponseEntity<Article> addArticle(@RequestBody AddArticleRequest request){
Article saveArticle = blogService.save(request);
// 요청한 자원이 성공적으로 생성되면 저장된 블로그 글 정보를 응답 객체에 담아 전송
// 응답코드로 201 CREATED: 오청이 성공적으로 수행되었고, 새로운 리소스가 생성되었음
return ResponseEntity.status(HttpStatus.CREATED)
.body(saveArticle);
}
}
Q. 왜 final 또는 @NonNull 필드만 @RequiredArgsConstructor에 포함되는 걸까?
A.
-> 자바에서는 개발자가 생성자를 하나도 작성하지 않으면 기본 생성자(매개변수가 없는 생성자)를 자동으로 추가해 줌.
여기서, 기본 생성자는 필드를 초기화하지 않음.
하지만, final 필드는 반드시 한 번 초기화되어야 함. 따라서 기본 생성자는 이를 해주지 않기 때문에 final 필드는 생성자가 필요함.
final 키워드는 한 번만 값을 할당할 수 있는 필드를 의미하기 때문에 생성자에서 반드시 값을 할당해야 함.
때문에 @RequiredArgsConstructor 가 필요함
-> 그럼 NonNull은?
@NonNull을 붙이면, 해당 필드는 null이 되면 안 된다는 의미.
Lombok은 @NonNull이 붙은 필드에 대해 널 체크하는 생성자를 만들어 줌. 즉, 널체크 로직이 추가되기 때문에 생성자가 필요함
Q. 그냥 @Controller만 사용하면 되지 왜 RestController를 사용할까?
A. @Controller를 사용하면, 보통 HTML 파일(JSP, Thymeleaf 등)을 응답으로 반환할 때 사용함.
- @Controller는 반환값이 View 이름으로 해석됨
- 즉, return "hello"; 하면 hello.html 파일이 렌더링됨
반면, @RestController는 기본적으로 HTTP 응답의 Body에 객체 데이터를 JSON 형식으로 반환할 때 사용.
즉, API 서버를 만들 때 주로 사용함.
- @RestController는 반환값이 View가 아니라, 바로 HTTP Response Body에 들어감
- 즉, return "Hello, Spring!"이라는 텍스트 또는 JSON을 응답으로 보냄.
@Controller + @ResponseBody = @RestController라고 보면 됨.
따라서 @RestController는
- API 서버를 만들 때 (JSON 데이터 반환)
- 클라이언트(React, Vue, 모바일 앱 등)에서 데이터만 요청할 때
- HTML 화면이 필요 없는 RESTful API를 만들 때
Q. 그럼 여기서 또 궁금한 점! Restful API는 JSP를 사용하지 않는 건가?
A. RESTful API는 JSP 같은 서버 사이드 템플릿을 사용하지 않는 구조. 대신
JSON 데이터를 반환하고, 클라이언트(React, Vue, 모바일 앱 등)에서 데이터를 받아서 화면을 구성하는 방식
✅ RESTful API는 JSP를 사용하지 않는 이유
1. JSON 데이터를 반환하는 것이 목적
RESTful API는 데이터를 주고받는 역할만 담당하기 때문에, JSP 같은 HTML 렌더링이 필요 없음. API 서버는 JSON 데이터를 클라이언트(웹, 앱 등)에게 보내고 끝!
2. 프론트엔드와 백엔드가 분리된 구조
JSP는 서버에서 HTML을 만들어서 반환하는 방식이지만, RESTful API는 데이터를 반환하고, 화면은 프론트엔드(React, Vue 등)에서 담당. 따라서 요즘은 프론트엔드-백엔드 분리 아키텍처가 대세!
3. JSP는 서버에서 HTML을 렌더링하므로 무겁다
REST API는 클라이언트-서버 간의 데이터를 주고받는 데 최적화되어 있음. JSP는 서버에서 HTML을 만들고 전달하는 방식이라 서버 부하가 증가할 수 있음.
✅ 그럼 JSP는 언제 사용할까?
전통적인 웹 애플리케이션 (백엔드에서 화면을 직접 렌더링) 예: Spring MVC + JSP, Spring Boot + Thymeleaf 백엔드에서 JSP로 HTML을 만들어서 브라우저에 전달하는 방식 관리 페이지(Admin Page) 내부 직원용 페이지라서 SPA(React, Vue)까지는 필요 없을 때 JSP 사용 가능하다고 생각하면 됨.
📌 RESTful API 구조
[프론트엔드] (React) ←→ [백엔드 REST API] (Spring Boot) ←→ [데이터베이스] (MySQL)
1️⃣ 사용자가 웹사이트에서 버튼 클릭
2️⃣ React/Vue가 REST API 요청 (fetch 또는 axios 사용)
3️⃣ 백엔드(Spring Boot)가 JSON 데이터 응답
4️⃣ 프론트엔드에서 데이터를 가공해서 화면에 표시
API 실행 테스트
실데 데이터를 확인하기 위해 H2 콘솔을 활성화 해야 함. 현재 H2 콘솔은 비활성화되어있기 때문에 활성화를 위해 속성 파일을 수정해줘야 함.
Q. H2 콘솔을 언제 사용할까?
A.
- Spring Boot 개발 중, 데이터베이스를 간편하게 확인할 때
- SQL 테스트 및 테이블 구조를 확인할 때
- 임시 데이터 저장소로 활용할 때 (H2는 기본적으로 인메모리 DB라서 앱이 종료되면 데이터가 사라짐!)
resources/application.yml 파일에 코드 추가
# h2의 콘솔을 활성화해줌
h2:
console:
enabled: true
포스트맨 실행해서 실제 값이 스프링 부트 서버에 저장되는 과정 보기
- HTTP 메서드를 POST로 하고, 서버에 요청을 보내 값을 저장하는 과정이다.
JSON으로 보낸 데이터가 H2 데이터베이스에 잘 저장됐는지 확인해보기
http://localhost:8080/h2-console 에 접속하여 콘솔 로그인 화면에 사진처럼 입력
접속하면, H2 데이터베이스에 접속하고 데이터를 확인할 수 있게 된다. SQL statement 입력창에 SELECT 질의문을 입력한 뒤, Run을 눌러서 쿼리를 실행한다.
앞서 post 요청에 의해 정상적으로 데이터가 입력된 것을 볼 수 있다.
반복 작업을 줄여줄 테스트 코드 작성
매번 이렇게 H2 데이터베이스를 확인하는 방법은 비효율적이다. 그래서 이 작업을 줄여줄 테스트 코드를 작성해보자.
저번에도 말했듯이, 테스트 코드 파일의 단축키는 클래스 파일에 커서를 놓고 ctrl + enter를 누르면 generate 창이 뜬다. 여기서 생성해주면 된다.
@SpringBootTest // 태스트용 어플리케이션 컨텍스트
@AutoConfigureMockMvc // MockMVC 생성 및 자동 구성
class BlogApiControllerTest {
@Autowired
protected MockMvc mockMvc;
protected ObjectMapper objectMapper; // 직렬화, 역직렬화를 위한 클래스
private WebApplicationContext context;
BlogRepository blogRepository;
@BeforeEach
public void mockMvcSetUp(){
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.build();
blogRepository.deleteAll();
}
}
** 코드 뜯어보기
1️⃣ @SpringBootTest
- Spring Boot 애플리케이션 컨텍스트를 로드해서 통합 테스트를 실행하는 어노테이션
- 실제 애플리케이션 환경과 유사한 상태에서 테스트 가능
2️⃣ @AutoConfigureMockMvc
- MockMvc(Spring MVC 테스트용 가짜 HTTP 요청 객체)를 자동으로 구성
- 이를 통해 컨트롤러를 직접 호출하지 않고도 MockMvc를 이용한 테스트 수행 가능
- @AutoConfigureMockMvc는 Spring Boot가 MockMvc를 자동으로 생성하고 설정하는 어노테이션
- @Autowired MockMvc mockMvc;로 바로 사용할 수 있도록 지원
- 원래는 Spring 컨테이너에 등록된 Bean이면 @Autowired를 붙여서 자동으로 주입할 수 있음. 근데 MockMvc는 기본적으로 Spring이 자동으로 빈으로 등록하지 않음! 그래서 @AutoConfigureMockMvc가 필요함.
- @Service, @Repository, @Component 는 Spring이 자동으로 Bean으로 등록하지만, MockMvc의 경우 그게 아님
3️⃣ protected ObjectMapper objectMapper
- Jackson의 ObjectMapper
- JSON <-> Java 객체 변환(직렬화/역직렬화) 수행
- HTTP 요청/응답에서 JSON 데이터를 다룰 때 필요
4️⃣ private WebApplicationContext context;
BlogRepository blogRepository;
- WebApplicationContext → Spring의 웹 애플리케이션 컨텍스트
- Spring 컨텍스트를 활용해서 실제 애플리케이션처럼 컨트롤러를 동작하도록 MockMvc를 설정하기 위해 필요함
- 이 설정을 하면 Spring의 모든 @Controller, @Service, @Repository 빈을 포함한 상태로 테스트 가능해짐
만약 이걸 설정하지 않으면 컨트롤러 테스트 시 제대로 동작하지 않을 수도 있음
** 왜 MockMvc만으로는 안되나?
-> MockMvc는 아래와 같은 역할을 함
✔ HTTP 요청을 컨트롤러에 전달
✔ 컨트롤러의 동작을 테스트
✔ 응답 코드, JSON 데이터, 헤더 등을 검증 가능
하지만, MockMvc 자체만으로는 컨트롤러가 정상적으로 실행되지 않을 수도 있음!
MockMvc만으로 실행되지 않는 이유
- 컨트롤러는 서비스 & 리포지토리 등 여러 빈(@Service, @Repository)에 의존하기 때문!
그래서 Spring 컨텍스트를 로드해서(= @SpringBootTest) 실행할 수 있도록 설정하는 것임
Spring은 아래와 같은 흐름으로 동작함
- 요청이 들어오면 DispatcherServlet이 요청을 받아서 HandlerMapping을 통해 해당 요청을 처리할 컨트롤러를 찾고 컨트롤러가 실행됨
➡ MockMvc만으로는 DispatcherServlet을 포함한 전체 Spring MVC 동작을 재현하기 어렵기 때문에, Spring 컨텍스트(WebApplicationContext)를 이용해서 테스트 환경을 설정하는 것!
- BlogRepository → 블로그 데이터를 다루는 JPA 레포지토리
5️⃣ @BeforeEach
- 각 테스트 실행 전 실행되는 메서드
- @BeforeEach가 붙은 메서드는 각각의 테스트가 실행될 때마다 먼저 실행됨
6️⃣ this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
- MockMvc 수동 설정
- MockMvcBuilders.webAppContextSetup(context)를 통해 WebApplicationContext를 기반으로 MockMvc 설정
- 이를 통해 실제 서블릿 컨텍스트와 유사한 환경에서 컨트롤러 테스트 가능
- 사실 이건 @AutoConfigureMockMvc 어노테이션 덕분에 수동 설정을 안해도 되지만, 추가적인 설정(예: DB 초기화, 커스텀 필터 적용 등)이 필요하면 직접 MockMvc 객체를 생성하는 것임.
7️⃣ blogRepository.deleteAll();
- 테스트 전 데이터 초기화
- 테스트 전에 저장된 블로그 데이터를 모두 삭제하여 테스트 환경을 초기화
- 이렇게 해야 테스트 간 데이터가 꼬이는 것을 방지 할 수 있음
Q. MockMvc?
🚀 일반적인 웹 요청 처리 흐름
- 클라이언트가 URL 요청 (예: POST /api/articles)
- 스프링이 해당 URL을 처리할 컨트롤러를 찾음
- 컨트롤러의 메서드를 실행하고 응답 반환
- HTML 또는 JSON 등의 응답이 클라이언트에게 전달됨
🧪 MockMvc를 활용한 테스트 흐름
- MockMvc가 가짜 HTTP 요청을 만듦 (mockMvc.perform())
- Spring MVC 내부에서 컨트롤러를 찾아 해당 요청을 처리 (실제 웹 서버를 띄우지 않음)
- 컨트롤러가 실행되고 응답을 반환
- 응답 데이터를 검증할 수 있음
-> 즉, MockMvc는 실제 브라우저 없이도 컨트롤러의 동작을 테스트할 수 있도록 해주는 도구이다.
Q. 그러면 왜 MockMvc를 사용할까?
- 실제 서버를 띄울 필요 없이 컨트롤러 테스트 가능 (빠름)
- Spring의 URL 매핑, 요청 처리 흐름을 그대로 테스트 가능
- HTTP 요청과 응답을 검증할 수 있음 (예: 응답 상태 코드, 반환된 JSON 값 확인 등)
즉, MockMvc
자체만으로는 컨트롤러가 사용하는 서비스(@Service), 리포지토리(@Repository) 빈을 로드하지 않음
컨트롤러가 정상적으로 동작하려면 Spring 컨텍스트를 함께 로드해야 함 그래서 @SpringBootTest와 @AutoConfigureMockMvc를 사용해서 Spring 환경에서 테스트를 실행하는 것!
Q. DB 초기화와 관련된 커스터마이징을 위해 MockMvc를 수동으로 생성한 경우에도 @AutoConfigureMockMvc 어노테이션을 붙여야 할까?
A. 수동으로 MockMvc 객체를 생성할 때도 @AutoConfigureMockMvc를 사용하는 것이 좋다. 왜냐하면@AutoConfigureMockMvc는 테스트 환경에 필요한 기본 설정을 자동으로 해주기 때문에, 이를 통해 테스트용 MockMvc 설정을 보장할 수 있기 때문이다.
@AutoConfigureMockMvc는 기본적으로 웹 애플리케이션 컨텍스트(WebApplicationContext)를 설정하고, 그 안에서 MockMvc 객체를 생성한다. 그 후 기본적인 MockMvc 설정(예: 컨트롤러 테스트 등)이 자동으로 적용된다. 만약 추가적인 설정이 필요해서 MockMvc를 수동으로 생성하고자 한다면, MockMvcBuilders.webAppContextSetup(context).build();와 같이 직접 설정을 하게 되는데, 이 경우에도 @AutoConfigureMockMvc가 있어야 스프링이 MockMvc와 관련된 설정을 자동으로 처리해준다.
자바 직렬화, 역직렬화
- HTTP에서는 JSON을, 자바에서는 객체를 사용한다. 하지만 서로 형식이 다르기 때문에 형변환 작업이 필요하다. 이 과정을 직렬화, 역직렬화 라고 한다.
- 직렬화: java 시스템 내부에서 사용되는 객체를 외부에서 사용하도록 데이터를 변환하는 작업을 이야기 한다. (Java 객체 -> JSON)
- 예를 들어 title은 '제목' content는 '내용'이라는 값이 들어 있는 객체가 있다고 가정을 하면,
- 이때 이 객체를 JSON 형식으로 직렬화 할 수 있다.
- 역직렬화: 직렬화의 반대이다. 외부에서 사용하는 데이터를 자바의 객체 형태로 변환하는 작업을 이야기 한다. (JSON -> Java 객체로 변환)
블로그 글 생성 API를 테스트하는 코드 작성
- given: 블로그 글 추가에 필요한 요청 객체를 만듬
- when: 블로그 글 추가 API에 요청을 보냄. 이때, 요청 타입은 JSON이고, given절에서 미리 만들어둔 객체를 요청 본문으로 함께 보낸다.
- then: 응답코드가 201 created인지 확인한다. Blog를 전체 조회해 크기가 1인지 확인하고, 실제로 저장된 데이터와 요청값을 비교한다.
@DisplayName("addArticle: 블로그 글 추가에 성공한다.")
@Test
public void addArticle() throws Exception {
// given
final String url = "/api/articles";
final String title = "title";
final String content = "content";
final AddArticleRequest userRequest = new AddArticleRequest(title, content);
// 객체 -> JSON으로 직렬화
final String requestBody = objectMapper.writeValueAsString(userRequest);
// when
// 설정한 내용을 바탕으로 요청 전송을 테스트 하는 코드
/*
mockMvc.perform(): MockMvc 객체를 사용하여 HTTP 요청을 실제처럼 수행합니다. 여기서는 post(url)을 사용하여 POST 요청을 보냅니다.
.contentType(MediaType.APPLICATION_JSON_VALUE): 요청의 컨텐츠 타입을 설정합니다.
여기서는 JSON 데이터를 보낼 것이므로 application/json으로 설정합니다.
.content(requestBody): 요청 본문에 앞에서 직렬화한 requestBody를 담습니다.
결과: 이 코드가 실행되면 /api/articles에 POST 요청을 보내고, 응답을 **result**에 저장합니다.*/
ResultActions result = mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(requestBody));
// then 요청 결과를 확인
/*
* blogRepository.findAll(): BlogRepository를 사용하여 데이터베이스에서 모든 블로그 글을 조회합니다.
assertThat(articles.size()).isEqualTo(1): 블로그 글이 1개 저장되었는지 확인합니다.
* POST 요청으로 글을 추가했으므로, 한 개의 글이 저장되어야 합니다.
assertThat(articles.get(0).getTitle()).isEqualTo(title):
* 저장된 블로그 글의 제목이 테스트 데이터로 사용한 title과 일치하는지 확인합니다.
assertThat(articles.get(0).getContent()).isEqualTo(content): 저장된 블로그 글의 내용
* */
result.andExpect(status().isCreated());
List<Article> articles = blogRepository.findAll();
assertThat(articles.size()).isEqualTo(1);
assertThat(articles.get(0).getTitle()).isEqualTo(title);
assertThat(articles.get(0).getContent()).isEqualTo(content);
}
}
Q. 왜 엔티티가 있음에도 불구하고 DTO가 필요한 걸까?
A. DTO를 사용하는 이유는 주로 클라이언트와의 데이터 교환 시 불필요한 데이터 노출을 방지하고, 전송할 데이터만 효율적으로 전달 하려는 목적이다. DTO를 사용하지 않으면 데이터베이스에서 모든 정보를 가져오거나, 엔티티에서 원하는 데이터만 사용하더라도 데이터의 효율성이나 보안 측면에서 불편할 수 있다.
'FrameWork > Spring Boot' 카테고리의 다른 글
블로그 기획하고 API 만들기 - 블로그 글 조회(개별 글), 삭제, 수정 (0) | 2025.03.28 |
---|---|
블로그 기획하고 API 만들기 - 블로그 글 목록 조회 (0) | 2025.03.28 |
DBMS/ ORM/ 스프링 데이터 JPA 조회, 삭제, 추가 메서드, 쿼리 메서드 사용 (0) | 2025.03.06 |
MockMvc를 사용하여 컨트롤러를 테스트하는 코드 작성해보기 (0) | 2025.03.04 |
스프링부트 3 테스트 코드 알아보기 (feat. JUnit, AssertJ) (0) | 2025.02.27 |