-
Flutter + Spring Boot 가족, 모임 서비스 구현하기 2 (Feat. Api 설계)백엔드 2023. 9. 25. 23:33728x90반응형SMALL
가족, 모임 구현 서비스에 앞서 여러가지 공부를 진행했고 오늘부로 첫 번째 api 설계에 따른 구현을 마무리했습니다.
중간중간 DB 스키마도 변경되고 api 설계도 변경되면서 시간이 생각한 것보다 훨씬 많이 걸리게 되었습니다...🥲
그래도 첫 번째 구현을 마무리해서 그동안 있었던 부분들을 정리해보려고 합니다.
1. test 환경 구성
현재 서버 DB를 MySQL로 구성하고 있는데, test에 활용할 DB를 h2로 구성하고 싶었습니다.
그래서 각각 독립된 환경을 구성하고 설정에 따른 실행에 따라 각기 다른 DB 환경을 구성할 수 있도록 해주었습니다.
구체적인 방법은 다음 포스팅에서 확인하실 수 있습니다.
https://beomseok37.tistory.com/167
저와 같은 경우 application.yml 파일의 이름 구분을 통해서 각기 다른 환경에 대한 setting을 해줬습니다.
이 후 test 관련 설정을 가지고 있는 H2Test 클래스를 선언한 뒤, test 클래스가 H2Test를 상속 받아 사용할 수 있도록 하여 test 환경일 경우에는 h2 DB를 활용할 수 있도록 구성했습니다.
2. api test 방법
모든 api 통신에 대한 테스트 코드를 작성해주었습니다.
통신에 대한 테스트를 위해서 MockMvc를 활용했습니다. MockMvc는 실제 서버에 배포하지 않아도 Spring MVC환경을 만들어 요청과 응답을 제공해주는 클래스입니다. 컨트롤러에 대한 테스트를 하고 싶을 경우 실제 서버에 배포하지 않아도 시뮬레이션 할 수 있도록 해줍니다.
실제 사용한 틀은 다음과 같습니다.
MockMvc 주입
@AutoConfigureMockMvc class GroupApiControllerTest extends H2Test { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; ...
요청, 응답에 대한 테스트를 위해 MockMvc를 주입받습니다.
json 형태의 serialization과 deserialization을 위해 ObjectMapper를 주입받습니다.
GET METHOD
@Test void getMealInfoInGroup() throws Exception { MvcResult result = mockMvc.perform( MockMvcRequestBuilders.get(".../{PATH_VARIABLE}/..."),PATH_VARIABLE) ).andExpect( MockMvcResultMatchers.status().isOk() ).andDo( MockMvcResultHandlers.print() ).andReturn(); <RESPONSE> response = objectMapper.readValue(result.getResponse().getContentAsString(),<RESPONSE>.class); assertEquals(<EXPECT>,response); }
mockMvc.perform
- 요청을 실행할 수 있도록 해줍니다.
MockMvcRequestBuilders
- 요청에 대한 uri 생성, body 주입 등을 할 수 있습니다.
andExpect()
- 예측되는 결과들을 확인해볼 수 있습니다.
- 여러번 붙여서 실행 가능합니다.
- 위의 예시에서는 응답에 대한 http status code가 200인지 확인하고 있습니다.
andDo()
- 요청, 응답에 대한 값들에 어떠한 행동을 할 수 있습니다.
- 위의 예시에서는 요청, 응답에 대한 값들을 출력해주도록 했습니다.
andReturn()
- 실행된 요청에 대한 결과를 반환하도록 하여 직접적으로 응답에 접근할 수 있도록 해줍니다.
ObjectMapper
- 응답값의 String을 object로 역직렬화했습니다.
assertEquals
- 응답값을 예상값과 비교하여 의도한대로 동작했는지 확인합니다.
POST/PUT METHOD
@Test void createGroup() throws Exception { ... String json = objectMapper.writeValueAsString(<REQUEST_BODY>); mockMvc.perform( MockMvcRequestBuilders.post(URL) .contentType(MediaType.APPLICATION_JSON) .content(json) ).andExpect( MockMvcResultMatchers.status().isCreated() ).andReturn(); assertEquals(<EXPECT>,<ACTUAL>); }
ObjectMapper
- RequestBody object를 String으로 직렬화합니다.
MockMvcRequestBuilders
- contentType()을 통해 body의 type을 지정
- content()를 통해 직렬화한 body값을 넣어줍니다.
POST, PUT METHOD도 마찮가지로 andExpect()를 통해 응답값을 확인할 수 있습니다. 저와 같은 경우 DB값을 조회해서 확인하는 것으로 대체했습니다.
3. 테스트 초기값 설정
테스트를 위해 초기 데이터들을 설정해주고 이를 통해 테스트를 진행할 수 있도록 해주고 싶었습니다.
초반에는 각 테스트마다 똑같은 데이터를 계속해서 주기도 했으나 이럴 경우 전체 테스트를 한 번에 실행할 경우 데이터가 겹치게 되어 오류가 발생하는 경우가 많았습니다. 그래서 각 Test 클래스의 실행 전에 초기 데이터값을 설정하고 모두 실행된 후 데이터를 삭제하도록 했습니다.
@BeforeAll public static void initData(@Autowired DataSet dataSet){ dataSet.insert(); } @AfterAll public static void deleteData(@Autowired DataSet dataSet){ dataSet.delete(); }
@BeforeAll과 @AfterAll을 통해 각 테스트 클래스가 실행하기 전, 모두 실행된 후에 실행할 메서드를 지정할 수 있습니다.
각 메서드는 static으로 선언되기 때문에 빈을 사용하기 위해 매개 변수로 사용할 객체를 선언해주어야 합니다.
4. Url 설계
전반적으로 널리 알려진 url 설계 규칙을 통해 api를 만들 수 있도록 했습니다.
아래 블로그 글을 주로 참고했습니다.
5. RequestBody 속성이 한 개일 경우
요청에 대한 body값을 가져오기 위해 계속해서 dto를 만들어 사용하게 되었는데, request body에 한 개의 속성만 존재할 경우 이를 dto로 만들어야되는지 의문이었습니다. 그래서 관련 조사를 진행해보니 직렬화, 역직렬화와 연관되어 있다는 것을 알게 되었습니다.
컨트롤러 메서드 상에서 @RequestBody 어노테이션은 요청에 담겨 있는 본문인 json이나 plain test, xml 등을 다룰 수 있게 해줍니다. 본문에 담겨있는 json을 dto 클래스로 역직렬화시켜줌으로써 json 데이터를 객체로써 사용할 수 있게 되는 것입니다.
내부적으로 여러 종류의 HttpMessageConverter를 통해 요청이나 응답에 대한 변환이 동작한다고 합니다.
@RequestBody 어노테이션이 붙어있는 객체의 경우 HttpMessageConverter 사용
String 처리는 StringHttpMessageConverter 사용
application/json 타입의 경우 MappingJackson2HttpMessageConverter 사용
위와 같이 요청의 body에 담긴 내용을 Converter를 통해 역직렬화해서 객체로 사용한다는 것을 알았습니다. 그렇다는 것은 json을 역직렬화를 통해 객체로 만들 수 있는 타입이라면 dto를 따로 정의하지 않고도 사용할 수 있다는 것입니다.
그래서 저와 같은 경우 json과 비슷한 형태인 Map<String, String> 타입으로 body를 받고 이를 사용하도록 했습니다.
@PostMapping(<URL>) public ResponseEntity method( @PathVariable String pathvariable1, @PathVariable String pathvariable2, @RequestBody Map<String,String> bodyMap){ String value = bodyMap.get("key"); ...
5. Entity Serializable?
여러 자료들을 찾아보면서 implements Serializable을 한 Entity 객체를 살펴볼 수 있었습니다.
JPA 표준 스펙은 Entity에 Serializable를 구현하도록 되어 있다고 합니다. Serializable 구현을 통해 JPA 구현체에 따라 엔티티를 분산 환경에서 사용할 수 있거나, 직열화해서 다른 곳에 전송할 수 있는 가능성을 열어주는 것입니다.
하지만, 실무 환경에서 사용하지 않는 경우가 많다는 2019년도 글을 보았습니다.
해당 글에 Serializable을 구현해야하는 Entity는 무엇인지도 나와 있으니 한 번 읽어보면 좋을 것 같습니다.
6. native query & dto mapping
쿼리메서드 작성 시 native query를 많이 활용했습니다. 그러다 보니 반환받을 객체를 정의하는 것이 애매모호한 경우가 생겼습니다. 이럴 경우 dto interface를 정의하고 활용하는 방법이 있다는 것을 알았어서 이를 활용했습니다.
dto interface 정의
public interface Dto { String getName(); String getUuid(); }
반환 받을 값에 대한 getter만 정의해줍니다.
반환값 정의
@Query(value = "select name, uuid from ...",nativeQuery = true) List<Dto> findAllByXxx();
위와 같이 native query를 작성했을 경우 findAllByXxx 메서드를 실행하면 Dto 인터페이스를 통해 각 속성 값에 접근해서 사용할 수 있습니다.
7. sub query vs join query
쿼리 작성 시 여러 번의 쿼리로 나누어서 할 것인지 아니면 join 쿼리를 통해 한 번에 값을 조회할 것인지 고민을 했었습니다.
서브 쿼리가 필요한 시점은 조회한 데이터 중에서 필터링하거나 외부로 값을 반환하는데 유용한 반면,
조인 쿼리는 연관된 컬럼을 기반으로 여러 테이블을 합쳐서 조회할 때 유용합니다.
그래서 저와 같은 경우 다음과 같이 쿼리문을 정의해서 사용했습니다.
서브 쿼리
- join 문이 2 번 이상으로 굉장히 복잡해질 경우
- 중간에 조회한 데이터에서 필터링한 데이터를 활용할 경우
- 추가적으로 활용할 값을 조회하기 위해
조인 쿼리
- join문이 복잡하지 않은 경우
- 각각 다른 테이블의 컬럼을 조건으로 하는 데이터를 반환할 경우
8. 정리
api 설계와 test 과정에서 최대한 많은 것들을 배우고 정리하기 위해 노력했습니다. 아직은 기초 수준의 지식을 습득했다 생각하고, 자료를 정리하면서 알게된 세부적인 내용 또한 익힐 수 있도록 노력해야겠습니다.🧐
이전 글 보러 가기
Flutter + Spring Boot 가족, 모임 서비스 구현하기 2 (Feat. 카카오 로그인)
반응형LIST'백엔드' 카테고리의 다른 글
Spring-Boot에 Spring-Data-JPA 적용하기 (Feat. Querydsl) (0) 2024.02.23 Gradle이란? (0) 2023.09.21 Spring swagger 적용 방법 (springdoc-openapi) (0) 2023.09.18 Spring Boot 독립된 test DB 구성 (MySQL, H2) (0) 2023.09.15 Rest Template으로 Server(Client) to Server 통신하기 (0) 2023.09.15