-
[Spring Boot][토이 프로젝트] 1. KakaoMap API 백엔드처리 및 테스트 코드 작성Spring/Study 2023. 6. 29. 18:21
토이 프로젝트 설명
목적
본 프로젝트의 목적은
첫 번째로 프론트의 카카오 맵 API 활용을 연습하려는 목적 백엔드의 지도 API 활용 방법 공부이다.
두 번째로 Controller의 테스트 코드를 작성하기위해 WebMvcTest 활용 방법을 연습했고
세 번째로 기상청 공공 API를 활용한 외부 데이터 수집 방법에 대한 학습을 목적으로 WebClient를 활용했다.이번 글은 첫 번째와 두 번째의 내용을 정리했고, 추가적인 글을 통해 세 번째의 내용을 정리할 예정이다.
사용한 기술 및 스택
언어 : JAVA
프레임워크 : Spring Boot, JPA, WebFlux
데이터베이스 : H2, MariaDB
DevOps : AWS, Docker
라이브러리 : Lombok, H2, MariaDB JDBC, QueryDSL, Netty, JSON-Simple
프로젝트 기간
23.06.18 ~ 23.06.24
이후 테스트 코드 연습 및 코드 리팩토링 지속중...
KakaoMap API 학습 기록
KakaoMap API 활용
백엔드 관점에서의 지도 API
백엔드 관점에서의 지도 API 사용 방법은 간단했었다. 특정 건물의 위도, 경도를 저장하고 그 위치의 건물 이름 및 건물에 대한 상세 정보를 저장하면 끝이였다.
간단하게 Entity 클래스를 작성해봤다.
package com.list.kakaoMap.entity; import jakarta.persistence.*; import lombok.*; @Entity @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class Store { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "store_id") private Long id; @Column private String name; @Column private String detail; @Column private double posX; @Column private double posY; }
Store 라는 엔티티에 이름, 상세정보, X, Y 값을 매핑 시켜뒀다.
간단한 이름을 통한 조회
간단하게 CRUD 구현만 진행하였으나, 건물 검색기능 정도는 구현해서 사용해보는 방법이 좋을 것 같아서
QueryDsl을 사용하여 입력받은 이름이 포함된 건물 조회 기능을 구현했다.
public List<Store> findStore(String name) { QStore qStore = new QStore("store"); return jpaQueryFactory .selectFrom(qStore) .where(qStore.name.contains(name)) .fetch(); }
파라미터로 name을 받으면 name 값을 지닌 Q타입 Store를 조회하도록 작성하였다.
WebMvcTest
Controller를 테스트 하기 위해 WebMvcTest를 사용해봤다.
다음은 StoreController 전체 코드 내용이다.
StoreController 전체 코드
package com.list.kakaoMap.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.list.kakaoMap.entity.Store; import com.list.kakaoMap.service.StoreService; import jakarta.transaction.Transactional; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.context.junit.jupiter.SpringExtension; 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.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import java.util.ArrayList; import java.util.List; import static com.list.kakaoMap.dto.StoreDto.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.when; import static org.springframework.http.RequestEntity.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(StoreController.class) class StoreControllerTest { @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; @MockBean StoreService storeService; @Test @DisplayName("가게 등록 테스트") public void addTest() throws Exception { StoreRequest storeRequest = new StoreRequest("testId", "testDetail", 11.01, 27.09); when(storeService.addStore(any())).thenReturn(new Store(1L, "testId", "testDetail", 11.01, 27.09)); ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.post("/store") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(storeRequest))); resultActions .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.success").value(true)) .andDo(print()); } @Test @DisplayName("가게 조회 테스트") public void findTest() throws Exception { //given Store storeResponse1 = new Store(1L, "test_7e6b067e0783", "test_c5d923cc5984", 97.45, 72.58); Store storeResponse2 = new Store(2L, "test_1123klnsas", "test_29sne215231", 23.56, 28.65); Store storeResponse3 = new Store(3L, "test_24901hsksa", "test_32kjba9e1", 55.27, 81.21); List<Store> storeResponseList = new ArrayList<>(); storeResponseList.add(storeResponse1); storeResponseList.add(storeResponse2); storeResponseList.add(storeResponse3); when(storeService.findStore(any())).thenReturn(storeResponseList); MultiValueMap<String, String> param = new LinkedMultiValueMap<>(); param.add("name", "test"); //when ResultActions perform = mockMvc.perform(MockMvcRequestBuilders.get("/store") .param("name","test")); //then perform.andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.result").exists()) .andDo(print()); } @Test @DisplayName("가게 정보 수정 테스트") public void editTest() throws Exception { //given StoreRequest storeRequest = new StoreRequest("test_12523as", "test_29sne215231", 23.56, 28.65); Store store = Store.builder() .id(2L) .name("test_12523as") .detail("test_29sne215231") .posX(23.56) .posY(28.65) .build(); when(storeService.editStore(any(),any())).thenReturn(store); //when ResultActions perform = mockMvc.perform(MockMvcRequestBuilders.put("/store/" + 2) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(storeRequest))); //then perform.andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.name").value("test_12523as")) .andDo(print()); } @Test @DisplayName("가게 삭제 테스트") public void deleteTest() throws Exception { //given given(storeService.deleteStore(1L)) .willReturn(true); //when ResultActions perform = mockMvc.perform(MockMvcRequestBuilders.delete("/store/" + 1)); //then perform.andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(jsonPath("$.success").value(true)); } }
애노테이션
@WebMvcTest(StoreController.class) // StoreController 를 테스트 하기 위한 Test @MockBean // 가짜 빈을 등록하여 실제 로직처럼 행동하는 빈을 등록
when .thenReturn
가짜 빈을 등록했으므로 실제 서비스 계층에서 특정 메소드를 실행하면 리턴값을 직접 지정해주는 방법이다.
when(storeService.addStore(any())).thenReturn(new Store(1L, "testId", "testDetail", 11.01, 27.09));
가게 등록 테스트를 예시로 들면, "addStore()라는 메소드에 any() 어떤 파라미터값을 넣어도 new Store(...) 라는 객체가 반환 될 것이다." 라는 의미이다.
ObjectMapper
ObjectMapper는 Jackson 라이브러리의 클래스로 "JAVA의 객체 <--> JSON 값" 으로 서로 변경할 때 사용한다.
StoreRequest storeRequest = new StoreRequest("testId", "testDetail", 11.01, 27.09); ... ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.post("/store") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(storeRequest)));
mockMvc로 POST로 바디 값을 전송해야 할 때, JSON 형식의 값을 전송하므로, 보내야할 storeRequest 값을 JSON 으로 변환 하는데 사용했다.
objectMapper.writeValueAsString(storeRequest); // storeRequest 라는 JAVA 객체르 JSON 형태의 문자열로 변환
물론 본 테스트 코드에서는 작성하지 않았지만
objectMapper.readValue(Json문자열, 변환할 DTO) // DTO에 맞춰서 JSON 문자열을 JAVA 객체로 변환
과 같이 사용도 가능했다.