Spring Boot 통합테스트 코드, REST API문서 작성 준비하기

REST API, 통합테스트 코드
홍윤's avatar
Oct 18, 2024
Spring Boot 통합테스트 코드, REST API문서 작성 준비하기
 
💡
통합 테스트는 MockMvcSpringBootTest를 사용하고, API 문서는 Spring REST Docs를 활용하기

1. 통합 테스트 코드 작성 (JUnit + MockMvc)

통합 테스트에서는 실제 애플리케이션의 여러 구성 요소들이 올바르게 협력하여 동작하는지 검증합니다. 아래 예시는 Spring Boot 애플리케이션에서 Controller와 Service, Repository가 함께 작동하는 통합 테스트를 보여줍니다.
notion image

Gradle 설정 (build.gradle)

dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' testImplementation 'com.fasterxml.jackson.core:jackson-databind' }

예제 코드: BoardControllerTest.java

@AutoConfigureMockMvc @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class BoardControllerTest { @Autowired private MockMvc mockMvc; @Test public void testCreateBoard() throws Exception { String requestBody = "{\"title\": \"테스트 게시글\", \"content\": \"내용입니다.\"}"; mockMvc.perform(post("/api/board") .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.title").value("테스트 게시글")) .andDo(print()); } @Test public void testGetBoard() throws Exception { int boardId = 1; mockMvc.perform(get("/api/board/" + boardId)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(boardId)) .andDo(print()); } }

설명:

  • @AutoConfigureMockMvc: MockMvc를 자동 설정하여 의존성 주입할 수 있게 합니다.
  • @SpringBootTest: 통합 테스트를 위해 Spring Boot 애플리케이션의 모든 컨텍스트를 로드합니다.
  • mockMvc.perform(...): HTTP 요청을 테스트합니다.
  • andExpect(...): 응답의 상태 코드나 JSON 필드 값을 검증합니다.
  • andDo(print()): 테스트 결과를 콘솔에 출력하여 디버깅에 도움이 됩니다.

2. REST API 문서 작성 (Spring REST Docs)

REST API 문서를 작성할 때, Spring REST Docs는 테스트 코드와 통합하여 자동으로 API 문서를 생성해줍니다.

Gradle 설정 (build.gradle)

plugins { id 'org.asciidoctor.convert' version '1.5.8' } dependencies { testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' } test { outputs.dir snippetsDir } asciidoctor { inputs.dir snippetsDir dependsOn test }

REST Docs 통합 예제

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; @AutoConfigureMockMvc @SpringBootTest @AutoConfigureRestDocs(outputDir = "build/generated-snippets") public class BoardControllerTest { @Autowired private MockMvc mockMvc; @Test public void testCreateBoard() throws Exception { String requestBody = "{\"title\": \"테스트 게시글\", \"content\": \"내용입니다.\"}"; mockMvc.perform(post("/api/board") .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) .andExpect(status().isCreated()) .andDo(document("create-board", preprocessResponse(prettyPrint()))); } }

문서 생성하기

  1. 테스트 실행:
    1. ./gradlew test
      테스트가 통과되면 build/generated-snippets 디렉토리에 문서 스니펫이 생성됩니다.
  1. Asciidoctor를 사용해 HTML 문서 생성
    1. ./gradlew asciidoctor
      build/docs/asciidoc 디렉토리에 생성된 HTML 파일을 열어보세요.

3. 문서 자동 생성의 장점

  • 항상 최신 문서 유지: 테스트와 함께 실행되므로, 코드가 변경되면 문서도 자동으로 업데이트됩니다.
  • 정확한 문서: 테스트 결과를 기반으로 문서를 생성하기 때문에 실제 동작과 동일한 문서를 얻을 수 있습니다.

결론

통합 테스트는 애플리케이션의 기능을 전체적으로 검증할 수 있는 중요한 과정입니다. 이를 통해 코드의 정확성을 높일 수 있으며, REST API 문서 작성도 테스트와 결합하여 자동으로 생성하면 관리가 쉬워집니다.
 

1. BoardControllerTest

package org.example.springv3.controller; import com.fasterxml.jackson.databind.ObjectMapper; import org.example.springv3.board.BoardRequest; import org.example.springv3.core.util.JwtUtil; import org.example.springv3.user.User; import org.example.springv3.user.UserRequest; import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 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.request.MockMvcRequestBuilders; import org.springframework.transaction.annotation.Transactional; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; //@IgnoreForbiddenApisErrors() 이걸 만들어서 TEST코드에서 토큰을 만들어서 테스트한다. @Transactional//롤백이 된다. //아래 2 어노테이션은 항상 고정이다. //Mock을 빈 컨테이너에 띄우는 것. @AutoConfigureMockMvc //MOCK 환경은 가짜 8080포트를 띄우는 것이다. @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) public class BoardControllerTest { @Autowired private MockMvc mockMvc; private ObjectMapper om = new ObjectMapper(); private String accessToken; @BeforeEach // 메소드 실행되기 전에 실행되는게 BeforeEach,리턴을 받지 못한다. 또 다른 하나는 BeforeALl 단 한번만 실행된다! public void setUp() { System.out.println("나 실행되고 있니?"); User sessionUser = User.builder().id(1).username("ssar").build(); accessToken = JwtUtil.create(sessionUser); // 클래스 필드 accessToken에 할당 System.out.println(accessToken); } @Test public void delete_test() throws Exception { // given int id = 2; // when ResultActions actions = mockMvc.perform( delete("/api/board/" + id) .header("Authorization", "Bearer " + accessToken) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(jsonPath("$.status").value(200)); actions.andExpect(jsonPath("$.msg").value("성공")); actions.andExpect(jsonPath("$.body").isEmpty()); } @Test public void save_test() throws Exception { //given //토큰 만들기 BoardRequest.SaveDTO saveDTO = new BoardRequest.SaveDTO(); saveDTO.setTitle("title 11"); saveDTO.setContent("content 11"); //JoinDTO를 json으로 바꾸는 것. 즉 asString으로 읽는 것 String requestBody = om.writeValueAsString(saveDTO); //System.out.println(requestBody); //mockMvc.perform을 해서 특정 주소에 통신을 수행해보는 것이다. //주소는 get, post 등을 정하고 post라면 .content가 필요 (우리가 만들어둔 json을 넣어줌) //.contentType으로 타입을 정해서 보내준다. //그 결과를 actions로 받는 것이다. //이때 join 엔드포인트로 POST 요청을 하는 것인데 이는 MockMvcRequestBuilders.post() 메서드를 사용하여 구현하므로 //MockMvcRequestBuilders를 import 해주면 그냥 mockMvc((post()) 이런식으로 사용할 수 있다. get, post, put 등 사용가능 //when ResultActions actions = mockMvc.perform(MockMvcRequestBuilders.post("/api/board") .header("Authorization", "Bearer " + accessToken) //Body가 필요없으면 content를 사용 할 이유가 없다 기본중에 기본! .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); //eyes String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); //then //모든 JSON 타입의 데이터를 받기 위해서 모든 걸 다 테스트에 적어야한다! 안 적으면 RETS API 문서를 뽑지 못한다. Ex) id,username,email등등 //받는 모든 데이터를 다 적어서 확인해야지 REST API 문서에서 다 뽑아서 만들어준다. // actions.andExpect(jsonPath("$.status").value(200)); // actions.andExpect(jsonPath("$.msg").value("성공")); // actions.andExpect(jsonPath("$.body.id").value(4)); } @Test public void login_test() throws Exception { //given UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO(); loginDTO.setUsername("ssar"); loginDTO.setPassword("1234"); //JoinDTO를 json으로 바꾸는 것. 즉 asString으로 읽는 것 String requestBody = om.writeValueAsString(loginDTO); System.out.println(requestBody); //mockMvc.perform을 해서 특정 주소에 통신을 수행해보는 것이다. //주소는 get, post 등을 정하고 post라면 .content가 필요 (우리가 만들어둔 json을 넣어줌) //.contentType으로 타입을 정해서 보내준다. //그 결과를 actions로 받는 것이다. //이때 join 엔드포인트로 POST 요청을 하는 것인데 이는 MockMvcRequestBuilders.post() 메서드를 사용하여 구현하므로 //MockMvcRequestBuilders를 import 해주면 그냥 mockMvc((post()) 이런식으로 사용할 수 있다. get, post, put 등 사용가능 //when ResultActions actions = mockMvc.perform(MockMvcRequestBuilders.post("/login") .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); //eyes String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); String responseJWT = actions.andReturn().getResponse().getHeader("Authorization"); System.out.println(responseJWT); //then //모든 JSON 타입의 데이터를 받기 위해서 모든 걸 다 테스트에 적어야한다! 안 적으면 RETS API 문서를 뽑지 못한다. Ex) id,username,email등등 //받는 모든 데이터를 다 적어서 확인해야지 REST API 문서에서 다 뽑아서 만들어준다. //notnull를 넣은 이유는? actions.andExpect(header().string("Authorization", Matchers.notNullValue())); actions.andExpect(jsonPath("$.status").value(200)); actions.andExpect(jsonPath("$.msg").value("성공")); actions.andExpect(jsonPath("$.body").isEmpty()); } @Test public void save400_test() throws Exception { //given //토큰 만들기 BoardRequest.SaveDTO saveDTO = new BoardRequest.SaveDTO(); saveDTO.setTitle(""); saveDTO.setContent("content 11"); //JoinDTO를 json으로 바꾸는 것. 즉 asString으로 읽는 것 String requestBody = om.writeValueAsString(saveDTO); //System.out.println(requestBody); //mockMvc.perform을 해서 특정 주소에 통신을 수행해보는 것이다. //주소는 get, post 등을 정하고 post라면 .content가 필요 (우리가 만들어둔 json을 넣어줌) //.contentType으로 타입을 정해서 보내준다. //그 결과를 actions로 받는 것이다. //이때 join 엔드포인트로 POST 요청을 하는 것인데 이는 MockMvcRequestBuilders.post() 메서드를 사용하여 구현하므로 //MockMvcRequestBuilders를 import 해주면 그냥 mockMvc((post()) 이런식으로 사용할 수 있다. get, post, put 등 사용가능 //when ResultActions actions = mockMvc.perform(MockMvcRequestBuilders.post("/api/board") .header("Authorization", "Bearer " + accessToken) //Body가 필요없으면 content를 사용 할 이유가 없다 기본중에 기본! .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); //eyes String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); //then //모든 JSON 타입의 데이터를 받기 위해서 모든 걸 다 테스트에 적어야한다! 안 적으면 RETS API 문서를 뽑지 못한다. Ex) id,username,email등등 //받는 모든 데이터를 다 적어서 확인해야지 REST API 문서에서 다 뽑아서 만들어준다. // actions.andExpect(jsonPath("$.status").value(200)); // actions.andExpect(jsonPath("$.msg").value("성공")); // actions.andExpect(jsonPath("$.body.id").value(4)); } }
💡

코드설명

클래스 및 어노테이션 설정

@Transactional @AutoConfigureMockMvc @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) public class BoardControllerTest { ... }
  • @Transactional: 각 테스트 메서드가 실행될 때마다 데이터베이스 트랜잭션을 시작하고, 테스트가 끝나면 자동으로 롤백됩니다. 이를 통해 테스트 환경이 항상 동일한 상태를 유지할 수 있습니다.
  • @AutoConfigureMockMvc: MockMvc를 자동으로 설정하여 주입합니다. 이를 통해 실제 서버를 띄우지 않고도 HTTP 요청을 테스트할 수 있습니다.
  • @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK): Spring 애플리케이션 컨텍스트를 로드하여 테스트 환경을 설정하며, 실제 서버를 띄우지 않고 Mock 환경에서 테스트를 진행합니다.

클래스 필드와 setUp 메서드

@Autowired private MockMvc mockMvc; private ObjectMapper om = new ObjectMapper(); private String accessToken; @BeforeEach public void setUp() { System.out.println("나 실행되고 있니?"); User sessionUser = User.builder().id(1).username("ssar").build(); accessToken = JwtUtil.create(sessionUser); // 클래스 필드 accessToken에 할당 System.out.println(accessToken); }
  • mockMvc: HTTP 요청을 모의로 테스트할 수 있는 객체입니다.
  • ObjectMapper om: Java 객체를 JSON으로 변환하거나 그 반대로 변환할 때 사용합니다.
  • accessToken: 인증 헤더에 사용할 JWT 토큰을 저장합니다.
  • @BeforeEach: 각 테스트가 실행되기 전에 호출되며, User 객체를 기반으로 JWT 토큰을 생성합니다.

delete_test 메서드

@Test public void delete_test() throws Exception { // given int id = 2; // when ResultActions actions = mockMvc.perform( delete("/api/board/" + id) .header("Authorization", "Bearer " + accessToken) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(jsonPath("$.status").value(200)); actions.andExpect(jsonPath("$.msg").value("성공")); actions.andExpect(jsonPath("$.body").isEmpty()); }
  • 설명: 이 테스트는 특정 게시글을 삭제하는 요청을 테스트합니다.
  • Given: id가 2인 게시글을 삭제할 예정입니다.
  • When: /api/board/2로 DELETE 요청을 보내고, Authorization 헤더에 JWT 토큰을 추가합니다.
  • Then: 응답에서 status가 200, msg가 "성공", body가 비어 있는지 확인합니다.

save_test 메서드

@Test public void save_test() throws Exception { //given BoardRequest.SaveDTO saveDTO = new BoardRequest.SaveDTO(); saveDTO.setTitle("title 11"); saveDTO.setContent("content 11"); String requestBody = om.writeValueAsString(saveDTO); // when ResultActions actions = mockMvc.perform(MockMvcRequestBuilders.post("/api/board") .header("Authorization", "Bearer " + accessToken) .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); // eyes String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); }
  • 설명: 이 테스트는 새로운 게시글을 생성하는 요청을 테스트합니다.
  • Given: BoardRequest.SaveDTO 객체를 생성하고, 제목과 내용을 설정합니다.
  • When: /api/board로 POST 요청을 보내고, JSON 본문에 위에서 만든 데이터를 포함합니다.
  • Then: (주석 처리된 부분) 응답에서 status가 200, msg가 "성공", body가 예상한 값과 일치하는지 검증할 수 있습니다.

login_test 메서드

@Test public void login_test() throws Exception { //given UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO(); loginDTO.setUsername("ssar"); loginDTO.setPassword("1234"); String requestBody = om.writeValueAsString(loginDTO); System.out.println(requestBody); //when ResultActions actions = mockMvc.perform(MockMvcRequestBuilders.post("/login") .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); //eyes String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); String responseJWT = actions.andReturn().getResponse().getHeader("Authorization"); System.out.println(responseJWT); //then actions.andExpect(header().string("Authorization", Matchers.notNullValue())); actions.andExpect(jsonPath("$.status").value(200)); actions.andExpect(jsonPath("$.msg").value("성공")); actions.andExpect(jsonPath("$.body").isEmpty()); }
  • 설명: 이 테스트는 로그인 기능을 테스트합니다.
  • Given: usernamepassword를 설정한 LoginDTO를 생성합니다.
  • When: /login으로 POST 요청을 보냅니다.
  • Then: 응답에서 JWT 토큰이 있는지 확인하고, status가 200이며 msg가 "성공"인지 검증합니다.

save400_test 메서드

@Test public void save400_test() throws Exception { //given BoardRequest.SaveDTO saveDTO = new BoardRequest.SaveDTO(); saveDTO.setTitle(""); // 비어있는 제목 saveDTO.setContent("content 11"); String requestBody = om.writeValueAsString(saveDTO); //when ResultActions actions = mockMvc.perform(MockMvcRequestBuilders.post("/api/board") .header("Authorization", "Bearer " + accessToken) .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); //eyes String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); }
  • 설명: 이 테스트는 잘못된 데이터를 사용해 게시글 생성 시 에러를 발생시키는지 확인합니다.
  • Given: 제목이 비어있는 SaveDTO를 설정합니다.
  • When: /api/board로 POST 요청을 보냅니다.
  • Then: 응답에서 적절한 에러가 반환되는지 검증할 수 있습니다.

전체 요약

  • 테스트의 기본 구조:
    • Given: 테스트 준비 단계로, 필요한 데이터와 환경 설정을 준비합니다.
    • When: 실제 요청을 보내고 결과를 받아옵니다.
    • Then: 받은 응답이 예상한 대로인지 검증합니다.
  • MockMvc를 통해 애플리케이션의 다양한 엔드포인트를 쉽게 테스트할 수 있습니다.
  • 각 메서드는 특정 기능을 검증하며, 다양한 케이스 (성공, 실패)들을 커버할 수 있도록 설계됩니다.
 

2. UserControllerTest

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @Transactional//롤백이 된다. //아래 2 어노테이션은 항상 고정이다. //Mock을 빈 컨테이너에 띄우는 것. @AutoConfigureMockMvc //MOCK 환경은 가짜 8080포트를 띄우는 것이다. @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) public class UserControllerTest { @Autowired private MockMvc mockMvc; private ObjectMapper om = new ObjectMapper(); @Test public void join_test() throws Exception { //given UserRequest.JoinDTO joinDTO = new UserRequest.JoinDTO(); joinDTO.setUsername("haha"); joinDTO.setPassword("1234"); joinDTO.setEmail("haha@nate.com"); //JoinDTO를 json으로 바꾸는 것. 즉 asString으로 읽는 것 String requestBody = om.writeValueAsString(joinDTO); //System.out.println(requestBody); //mockMvc.perform을 해서 특정 주소에 통신을 수행해보는 것이다. //주소는 get, post 등을 정하고 post라면 .content가 필요 (우리가 만들어둔 json을 넣어줌) //.contentType으로 타입을 정해서 보내준다. //그 결과를 actions로 받는 것이다. //이때 join 엔드포인트로 POST 요청을 하는 것인데 이는 MockMvcRequestBuilders.post() 메서드를 사용하여 구현하므로 //MockMvcRequestBuilders를 import 해주면 그냥 mockMvc((post()) 이런식으로 사용할 수 있다. get, post, put 등 사용가능 //when ResultActions actions = mockMvc.perform(MockMvcRequestBuilders.post("/join") .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); //eyes String responseBody = actions.andReturn().getResponse().getContentAsString(); //System.out.println(responseBody); //then //모든 JSON 타입의 데이터를 받기 위해서 모든 걸 다 테스트에 적어야한다! 안 적으면 RETS API 문서를 뽑지 못한다. Ex) id,username,email등등 //받는 모든 데이터를 다 적어서 확인해야지 REST API 문서에서 다 뽑아서 만들어준다. actions.andExpect(jsonPath("$.status").value(200)); actions.andExpect(jsonPath("$.msg").value("성공")); actions.andExpect(jsonPath("$.body.id").value(4)); actions.andExpect(jsonPath("$.body.username").value("haha")); actions.andExpect(jsonPath("$.body.email").value("haha@nate.com")); actions.andExpect(jsonPath("$.body.profile").isEmpty()); } @Test public void login_test() throws Exception { //given UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO(); loginDTO.setUsername("ssar"); loginDTO.setPassword("1234"); //JoinDTO를 json으로 바꾸는 것. 즉 asString으로 읽는 것 String requestBody = om.writeValueAsString(loginDTO); System.out.println(requestBody); //mockMvc.perform을 해서 특정 주소에 통신을 수행해보는 것이다. //주소는 get, post 등을 정하고 post라면 .content가 필요 (우리가 만들어둔 json을 넣어줌) //.contentType으로 타입을 정해서 보내준다. //그 결과를 actions로 받는 것이다. //이때 join 엔드포인트로 POST 요청을 하는 것인데 이는 MockMvcRequestBuilders.post() 메서드를 사용하여 구현하므로 //MockMvcRequestBuilders를 import 해주면 그냥 mockMvc((post()) 이런식으로 사용할 수 있다. get, post, put 등 사용가능 //when ResultActions actions = mockMvc.perform(MockMvcRequestBuilders.post("/login") .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); //eyes String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); String responseJWT = actions.andReturn().getResponse().getHeader("Authorization"); System.out.println(responseJWT); //then //모든 JSON 타입의 데이터를 받기 위해서 모든 걸 다 테스트에 적어야한다! 안 적으면 RETS API 문서를 뽑지 못한다. Ex) id,username,email등등 //받는 모든 데이터를 다 적어서 확인해야지 REST API 문서에서 다 뽑아서 만들어준다. //notnull를 넣은 이유는? actions.andExpect(header().string("Authorization", Matchers.notNullValue())); actions.andExpect(jsonPath("$.status").value(200)); actions.andExpect(jsonPath("$.msg").value("성공")); actions.andExpect(jsonPath("$.body").isEmpty()); } }
💡

코드설명

join_test 메서드 (회원 가입 테스트)

@Test public void join_test() throws Exception { // given UserRequest.JoinDTO joinDTO = new UserRequest.JoinDTO(); joinDTO.setUsername("haha"); joinDTO.setPassword("1234"); joinDTO.setEmail("haha@nate.com"); String requestBody = om.writeValueAsString(joinDTO); // when ResultActions actions = mockMvc.perform(MockMvcRequestBuilders.post("/join") .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); // eyes String responseBody = actions.andReturn().getResponse().getContentAsString(); // then actions.andExpect(jsonPath("$.status").value(200)); actions.andExpect(jsonPath("$.msg").value("성공")); actions.andExpect(jsonPath("$.body.id").value(4)); actions.andExpect(jsonPath("$.body.username").value("haha")); actions.andExpect(jsonPath("$.body.email").value("haha@nate.com")); actions.andExpect(jsonPath("$.body.profile").isEmpty()); }
  • 설명: 회원 가입 기능을 테스트합니다.
  • Given: JoinDTO 객체에 사용자 이름, 비밀번호, 이메일을 설정합니다.
    • om.writeValueAsString(joinDTO);: joinDTO 객체를 JSON 형식의 문자열로 변환합니다.
  • When: /join 엔드포인트로 POST 요청을 보냅니다.
    • mockMvc.perform 메서드를 사용해 JSON 본문을 포함한 POST 요청을 보내고, 응답을 **actions*에 저장합니다.
  • Then: 응답의 status, msg, body 필드를 검증합니다.
    • jsonPath("$.status").value(200): 상태 코드가 200인지 확인합니다.
    • jsonPath("$.msg").value("성공"): 메시지가 "성공"인지 확인합니다.
    • jsonPath("$.body.id").value(4): 응답의 body.id가 4인지 확인합니다.
    • jsonPath("$.body.username").value("haha"): username이 "haha"인지 확인합니다.
    • jsonPath("$.body.email").value("haha@nate.com"): email이 "haha@nate.com"인지 확인합니다.
    • jsonPath("$.body.profile").isEmpty(): profile 필드가 비어있는지 확인합니다.

4. login_test 메서드 (로그인 테스트)

@Test public void login_test() throws Exception { // given UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO(); loginDTO.setUsername("ssar"); loginDTO.setPassword("1234"); String requestBody = om.writeValueAsString(loginDTO); System.out.println(requestBody); // when ResultActions actions = mockMvc.perform(MockMvcRequestBuilders.post("/login") .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); // eyes String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); String responseJWT = actions.andReturn().getResponse().getHeader("Authorization"); System.out.println(responseJWT); // then actions.andExpect(header().string("Authorization", Matchers.notNullValue())); actions.andExpect(jsonPath("$.status").value(200)); actions.andExpect(jsonPath("$.msg").value("성공")); actions.andExpect(jsonPath("$.body").isEmpty()); }
  • 설명: 로그인 기능을 테스트합니다.
  • Given: LoginDTO 객체에 사용자 이름과 비밀번호를 설정합니다.
    • om.writeValueAsString(loginDTO);: loginDTO 객체를 JSON 형식의 문자열로 변환합니다.
  • When: /login 엔드포인트로 POST 요청을 보냅니다.
    • *mockMvc.perform*을 사용해 JSON 본문을 포함한 POST 요청을 보내고, 응답을 **actions*에 저장합니다.
  • Then: 응답의 Authorization 헤더와 status, msg 필드를 검증합니다.
    • header().string("Authorization", Matchers.notNullValue()): Authorization 헤더가 비어있지 않은지 확인합니다.
    • jsonPath("$.status").value(200): 상태 코드가 200인지 확인합니다.
    • jsonPath("$.msg").value("성공"): 메시지가 "성공"인지 확인합니다.
    • jsonPath("$.body").isEmpty(): 응답 본문이 비어있는지 확인합니다.

전체 요약

  • 테스트의 기본 구조:
    • Given: 테스트 준비 단계로, 필요한 데이터와 설정을 만듭니다.
    • When: 실제 요청을 보내고 결과를 받아옵니다.
    • Then: 결과가 예상과 일치하는지 검증합니다.
  • MockMvc를 사용하여 애플리케이션의 다양한 엔드 포인트를 쉽게 테스트할 수 있습니다. 각 테스트는 특정 기능(회원 가입, 로그인 등)을 검증합니다.
  • 주의할 점: 테스트를 작성할 때, 다양한 시나리오 (성공, 실패, 잘못된 입력 등)에 대한 케이스를 고려하는 것이 중요합니다.
 

3.FilterConfig

@Bean public FilterRegistrationBean<?> corsFilter(){ FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter()); bean.addUrlPatterns("/api/*"); bean.setOrder(0); return bean; }
💡

코드설명

이 코드의 목적은 Spring 애플리케이션에 CORS (Cross-Origin Resource Sharing) 규칙을 적용하는 필터를 설정하는 것입니다. 각 줄의 코드가 어떤 역할을 하는지 하나씩 설명드리겠습니다.

코드 분석

Bean public FilterRegistrationBean<?> corsFilter() { FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter()); bean.addUrlPatterns("/api/*"); bean.setOrder(0); return bean; }

1. @Bean

  • 설명: 이 메서드가 Spring 애플리케이션의 Bean으로 등록된다는 것을 의미합니다.
  • 의도: Spring 애플리케이션이 시작될 때 자동으로 실행되며, 이 필터를 모든 HTTP 요청에 적용할 수 있게 됩니다.

2. public FilterRegistrationBean<?> corsFilter()

  • 설명: FilterRegistrationBean을 반환하는 메서드를 정의합니다.
  • 의도: Spring 애플리케이션에서 필터를 설정하고 등록할 수 있도록 도와줍니다.

3. FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter());

  • 설명: 새로운 CorsFilter 인스턴스를 생성하고, 이를 FilterRegistrationBean으로 감싸서 등록합니다.
    • CorsFilter: 다른 도메인에서 서버로 요청을 보낼 때 CORS 규칙을 적용하여 요청을 허용하거나 차단하는 역할을 합니다.
    • FilterRegistrationBean: 필터를 등록하고 설정하는 데 사용되는 Spring 클래스입니다.
  • 의도: 애플리케이션의 특정 경로에 대해 CORS 정책을 쉽게 설정할 수 있도록 필터를 감싸서 관리합니다.

4. bean.addUrlPatterns("/api/*");

  • 설명: 이 코드 줄은 필터가 적용될 URL 경로 패턴을 정의합니다.
    • "/api/*": /api/로 시작하는 모든 경로에 대해 필터가 적용됩니다. 예를 들어 /api/products, /api/user 등의 요청에 CORS 규칙이 적용됩니다.
  • 의도: API 요청에만 CORS 필터를 적용하고, 다른 경로에는 적용하지 않도록 설정합니다.

5. bean.setOrder(0);

  • 설명: 필터의 우선순위를 설정합니다.
    • setOrder(0): 우선순위가 높다는 의미입니다. 숫자가 낮을수록 우선순위가 높으며, 이 필터가 다른 필터보다 먼저 실행됩니다.
  • 의도: CORS 필터를 다른 필터보다 우선적으로 실행시켜 요청이 올바르게 허용되거나 차단되도록 보장합니다.

6. return bean;

  • 설명: 설정된 FilterRegistrationBean을 반환하여 Spring 애플리케이션에 필터를 등록합니다.
  • 의도: Spring이 이 설정을 읽고, /api/* 경로로 오는 요청에 대해 CORS 필터를 적용하도록 만듭니다.

전체 요약

이 코드는 Spring 애플리케이션에 CORS 필터를 설정하여, /api/로 시작하는 모든 경로에서 오는 요청에 대해 CORS 규칙을 적용합니다. 기본적으로 브라우저는 다른 도메인으로의 요청을 차단하기 때문에, 이를 통해 특정 경로에 대한 요청을 허용하거나 차단할 수 있습니다. 필터는 우선순위가 높게 설정되어 다른 필터보다 먼저 적용됩니다.
예시 사용 시나리오: 예를 들어, 클라이언트 애플리케이션이 http://localhost:3000에 있고, 서버가 http://localhost:8080/api/products에서 제공하는 API에 요청을 보내려 할 때, CORS 설정이 없다면 브라우저는 요청을 차단합니다. 이 코드를 통해 /api/ 경로에 대한 요청은 허용되도록 설정할 수 있습니다.
 

4. CorsFilter

public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { System.out.println("CORS 필터 작동"); HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; response.setHeader("Access-Control-Expose-Headers", "Authorization"); //프론트쪽 포트 번호만 허용해주면 된다. "*" 를 해두는 것은 말이 안 된다. response.setHeader("Access-Control-Allow-Origin", "3000"); response.setHeader("Access-Control-Allow-Methods", "POST, PUT, PATCH, GET, DELETE, OPTIONS"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Api-Key, X-Requested-With, Content-Type, Accept, Authorization"); // 웹소켓: OPTIONS 메서드에 대한 응답 헤더 설정 // 통신하기 전에 미리 가서 한 번 확인하는 것이다. 이게 OK면 통신 가능한 것. // 소문자가 올지 대문자가 올지 몰라서 equalsIgnoreCase로 받음 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { System.out.println("여기걸림?"); response.setStatus(HttpServletResponse.SC_OK); }else { chain.doFilter(req, res); } } }
💡

코드설명

이 코드는 Spring 애플리케이션에서 CORS (Cross-Origin Resource Sharing) 문제를 해결하기 위해 작성된 CorsFilter 클래스입니다. 이 클래스는 모든 HTTP 요청에 대해 특정 CORS 규칙을 적용합니다.

코드 분석

  1. 클래스 선언 및 인터페이스 구현
    1. public class CorsFilter implements Filter {
      • CorsFilter 클래스는 Filter 인터페이스를 구현합니다.
      • Filter 인터페이스는 웹 애플리케이션에서 요청이 처리되기 전에 HTTP 요청을 가로채는 데 사용됩니다.
  1. doFilter 메서드
    1. @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
      • *doFilter*는 필터의 핵심 메서드로, 모든 요청과 응답을 처리합니다.
      • *ServletRequest*와 **ServletResponse*를 받아와서 HTTP 요청과 응답을 제어할 수 있습니다.
      • *FilterChain*을 통해 요청이 필터링된 후 다음 단계로 계속 진행할 수 있습니다.
  1. CORS 필터 로직 시작
    1. System.out.println("CORS 필터 작동"); HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res;
      • System.out.println을 통해 필터가 작동할 때마다 콘솔에 메시지를 출력합니다.
      • 요청과 응답을 HTTP 요청과 응답으로 형 변환하여, 이후의 CORS 설정에서 사용합니다.
  1. CORS 헤더 설정
    1. rsponse.setHeader("Access-Control-Expose-Headers", "Authorization"); response.setHeader("Access-Control-Allow-Origin", "3000"); response.setHeader("Access-Control-Allow-Methods", "POST, PUT, PATCH, GET, DELETE, OPTIONS"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Api-Key, X-Requested-With, Content-Type, Accept, Authorization");
      • Access-Control-Expose-Headers: 클라이언트에서 접근할 수 있도록 허용된 응답 헤더를 정의합니다. 여기서는 Authorization 헤더를 허용합니다.
      • Access-Control-Allow-Origin: CORS 요청을 허용할 도메인을 지정합니다. "3000"은 클라이언트 애플리케이션이 작동하는 포트를 의미합니다. 특정 도메인만 허용할 수 있으며, "*"을 사용하면 모든 도메인을 허용합니다.
      • Access-Control-Allow-Methods: 허용할 HTTP 메서드 목록을 지정합니다. 예를 들어, GET, POST, DELETE, OPTIONS 등의 메서드를 명시합니다.
      • Access-Control-Max-Age: 브라우저가 프리플라이트 요청(OPTIONS 요청)의 응답을 캐시할 수 있는 시간을 초 단위로 지정합니다. 3600초 동안 캐시하므로, 한 시간 내에 같은 요청이 다시 보내지면 프리플라이트 요청을 생략할 수 있습니다.
      • Access-Control-Allow-Headers: 요청 헤더 중 허용할 헤더 목록을 지정합니다. 예를 들어, Authorization 헤더가 포함된 요청만 허용하도록 할 수 있습니다.
  1. OPTIONS 메서드에 대한 처리
    1. if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { System.out.println("여기걸림?"); response.setStatus(HttpServletResponse.SC_OK); } else { chain.doFilter(req, res); }
      • 설명: CORS에서 프리플라이트 요청은 클라이언트가 서버와 통신하기 전에 미리 서버의 CORS 설정을 확인하기 위해 보내는 OPTIONS 메서드를 사용한 요청입니다.
      • if ("OPTIONS".equalsIgnoreCase(request.getMethod())): 요청이 OPTIONS 메서드인지 확인합니다.
        • OPTIONS 요청이라면, 200 OK 상태 코드를 반환하여 브라우저에게 요청이 허용됨을 알려줍니다.
        • 그렇지 않으면 **chain.doFilter(req, res);*로 요청이 다음 필터 또는 실제 컨트롤러로 전달됩니다.

요약

CorsFilter 클래스는 클라이언트에서 서버로 CORS 요청을 보낼 때 필요한 규칙을 설정하고, 해당 요청을 허용 또는 차단합니다. 특히, 특정 HTTP 메서드(예: GET, POST), 특정 헤더(예: Authorization), 특정 도메인에서 오는 요청만 허용하도록 설정할 수 있습니다.
사용 시나리오:
  • 클라이언트 애플리케이션이 다른 도메인(예: http://localhost:3000)에 있고, 서버가 http://localhost:8080에 있을 때, CORS 필터를 통해 두 도메인이 서로 통신할 수 있도록 허용할 수 있습니다.
  • OPTIONS 메서드를 통한 프리플라이트 요청에 대해서도 적절하게 응답하여, 클라이언트가 원활하게 통신할 수 있게 설정합니다.
 

5. Run 'Tests in spring-v3…

notion image
💡
  • IntelliJ IDE에서 특정 모듈의 테스트를 실행하는 장면입니다.
  • Run 'Tests in spring-v3...' 옵션을 선택하여 현재 모듈에 포함된 모든 테스트 케이스를 실행하려고 하고 있습니다. 이 작업을 통해 모듈의 기능이 의도한 대로 작동하는지 검증하는 테스트를 수행합니다.
 

6. ./gradlew clean build

notion image
💡
  • Gradle 빌드가 성공적으로 완료된 장면입니다.
  • ./gradlew clean build 명령어를 실행한 후 BUILD SUCCESSFUL 메시지가 나타나고 있습니다. 이는 프로젝트의 전체 빌드 과정이 에러 없이 완료되었음을 의미합니다. clean build 명령어는 이전 빌드 결과를 삭제하고 새로 빌드하는 작업을 수행합니다.

7. 이게 됐다면 AWS에 던질 수 있다!

notion image
💡
이제 AWS에 던질 수 있다.
Share article

Uni