기존에는 Controller에서 데이터를 View로만 보냈었다면 이번에는 View가 아닌 JSON형태로 데이터를 보내기 위해 RestController를 사용할 것이다.
또한 WebMVCTest를 통해 Controller만의 Test를 만들어 검사를 해볼 것이다.
Controller는 Model
을 통해 데이터를 사용자가 볼 수 있는 화면인 View로 전송하여 가공된 데이터를 볼 수 있다.
@Controller
@RequestMapping("/hospitals")
백엔드 작업자 혼자서 프로젝트를 만들때는 Controller
와Mustache
를 통해 데이터를 주고받을 수 있지만, 프론트 엔드 개발자와 현업해서 프로젝트를 진행할 경우 주로 React와 Springboot간 데이터 통신을 통해 주고 받는다. 그럼 이때 데이터를 어떤 형태로 주고 받을까??
바로 JSON 형식으로 데이터를 주고 받는다. 그래서 JSON 형식으로 보내기 위해서는 RestController를 통해 만들수 있다.
@RestController
@RequestMapping("/api/v1/hospitals")
이처럼 RestController를 통해 사용할 때는 /api/v1/을 붙여줌으로써 api기능(Json 형식으로 데이터를 리턴해준다)을 한다는 암시를 준다.
❓ JSON이란?
Json은 파싱 또는 직렬화 없이도 JavaScrpit 프로그램에서 사용할 수 있다.
JavaScript 객체 리터럴, 배열, 스칼라 데이터를 표현하는 텍스트 기반의 방식이다.
{id:"",roadNameAddress:"",hospitalName:"",patientRoomCount:"",totalNumberOfBeds:"",businessTypeName:"",totalAreaSize:""}
위의 그림처럼 프론트에서 서울이라는 데이터를 출력하기 위해서는 JSON 형식으로 서울에 대한 데이터를 전송해줘야 한다.
Hospital
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Table(name = "nation_wide_hospitals")
public class Hospital {
@Id
private Integer id;
@Column(name = "hospital_name") // DB 변수와 entity 변수명이 다를경우 Column을 통해 직접 연결할 수 있다.
private String hospitalname;
@Column(name = "full_address")
private String fulladdress; // cammel 형식으로 작성시 자동으로 DB에 _ 형식으로 들어감 fullAddress => full_address
@Column(name = "business_type_name")
private String businesstypename;
@Column(name = "road_name_address")
private String roadNameAddress; // DB에서 road_name_address를 의미
@Column(name = "patient_room_count")
private Integer patientroomcount; // patient_room_count
@Column(name = "total_number_of_beds")
private Integer totalnumberofbeds;
@Column(name = "total_area_size")
private Float totalareasize;
// HospitalEntity를 HospitalResponse DTO로 만들어주는 부분
public static HospitalResponse of(Hospital hospital) {
return new HospitalResponse(hospital.getId(),
hospital.getRoadNameAddress(),hospital.getHospitalname(),
hospital.getPatientroomcount(),hospital.getTotalnumberofbeds(),
hospital.getBusinesstypename(),hospital.getTotalareasize());
}
}
HospitalResponse
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class HospitalResponse {
private Integer id;
private String roadnameaddress;
private String hospitalname;
private Integer patientroomcount;
private Integer totalnumberofbeds;
private String businesstypename;
private Float totalareasize;
}
HospitalRestController
@RestController
@RequestMapping("/api/v1/hospitals")
public class HospitalRestController {
private final HospitalRepository hospitalRepository;
public HospitalRestController(HospitalRepository hospitalRepository) {
this.hospitalRepository = hospitalRepository;
}
@GetMapping("/{id}")
public ResponseEntity<HospitalResponse> get(@PathVariable Integer id){
Optional<Hospital> hospital = hospitalRepository.findById(id); // Entity
HospitalResponse hospitalResponse = Hospital.of(hospital.get()); // DTO
return ResponseEntity.ok().body(hospitalResponse); // Return은 DTO로
}
}
코드 아키텍처를 구성할 때는 확장성, 재사용성, 유지 보수 가능성, 가독성과 같은 요소들을 엄두해야한다.
따라서, 이를 위해 코드의 구조를 어떻게 구성하고 관리해야 하는지에 대한 많은 패턴들이 나와있는데 레이어드 아키텍처는 그 중 백엔드 API 코드에서 가장 널리 사용되는 패턴 중 하나이다.
레이어드 아키텍처의 핵심 요소는 단방향 의존성으로 각각의 레이어는 자기 보다 하위의 있는 레이어에만 의존을 한다. 즉, Presentation -> Business -> Persistence
구조로 이루어져 있다.
기본적으로 Presentation 계층, Business 계층, 데이터 접근 계층으로 3단계로 나누어져 있다.
HospitalService
@Service
public class HospitalService
private final HospitalRepository hospitalRepository;
public HospitalService(HospitalRepository hospitalRepository) {
this.hospitalRepository = hospitalRepository;
}
public HospitalResponse getHospital(Integer id) {
Optional<Hospital> optHospital = hospitalRepository.findById(id); // Entity
Hospital hospital = optHospital.get();
HospitalResponse hospitalResponse = Hospital.of(hospital); // DTO
if (hospital.getBusinessStatusCode() == 13) {
hospitalResponse.setBusinessStatusName("영업중");
} else if (hospital.getBusinessStatusCode() == 3) {
hospitalResponse.setBusinessStatusName("폐업");
} else {
hospitalResponse.setBusinessStatusName(String.valueOf(hospital.getBusinessStatusCode()));
}
return hospitalResponse;
}
}
HospitalRestController
@RestController
@RequestMapping("/api/v1/hospitals")
public class HospitalRestController {
private final HospitalService hospitalService;
public HospitalRestController(HospitalService hospitalService) {
this.hospitalService = hospitalService;
}
@GetMapping("/{id}")
public ResponseEntity<HospitalResponse> get(@PathVariable Integer id) { // ResponseEntity도 DTO타입
HospitalResponse hospitalResponse = hospitalService.getHospital(id); // DTO
return ResponseEntity.ok().body(hospitalResponse); // Return은 DTO로
}
}
Given
: 테스트 수행 전, 필요한 환경 설정. (ex. 변수, Mock 객체 정의)When
: 실제 테스트 코드와 결과값을 받는다.Then
: 기대값과 테스트 결과값이 일치하는지 검증한다. (ex. assert.Equals(expected, result) )❓ Controller를 테스트 하는 이유는?
기존에는 API 통신이 잘 되는지 확인하기 위해서는
(SpringBoot를 실행 → 웹브라우저 or Api 호출 App → /api/v1/hospitals/111 → 눈으로 확인)
과정을 통해 진행을 했으나 WebMVCTest를 하게 되면 이 단계를 모두 생략할 수 있어 편리하다.
@WebMvcTest(HospitalRestController.class)
@Autowired
MockMvc mockMvc; // MockMvc를 의존함
@MockBean // @Autowired아님 --> HospitalService는 테스트를 위해 가짜 객체를 쓰겠다는 뜻
HospitalService hospitalService; // 가짜 객체를 쓰면 좋은점 DB와 상관없이 테스트 가능
MockMvc의 코드는 모두 합쳐져 있어 구분하기는 애매하지만 전체적인 'When-Then'의 구조를 갖추고 있다.
MockBean은 실제 빈 개체가 아닌 Mock(가짜) 객체를 생성해서 주입하는 역할을 수행한다.
@MockBean이 선언된 객체는 실제 객체가 아니기 때문에 실제 행위를 수행하지 않는다. 그렇기에 해당 객체는 개발자가 Mockito의 given( )메서드를 통해 동작을 정의해야 한다.
void jsonResponse() throws Exception {
// HospitalResponse에 저장되는 값을 미리 설정함
// Repository 부분(데이터 미리 설정)
HospitalResponse hospitalResponse = HospitalResponse.builder()
.id(2321)
.roadnameaddress("서울특별시 서초구 서초중앙로 230, 202호 (반포동, 동화반포프라자빌딩)")
.hospitalname("노소아청소년과의원")
.patientroomcount(0)
.totalnumberofbeds(0)
.businesstypename("의원")
.totalareasize(0.0f)
.businessstatuscode("영업중")
.build();
// Service 부분
given(hospitalRestService.getHospital(2321))
.willReturn(hospitalResponse);
int hospitalId = 2321;
// 앞에서 Autowired한 mockMvc
// Controller 부분(json 형식으로 반환)
String url = String.format("/api/v1/hospitals/%d", hospitalId);
mockMvc.perform(get(url))
.andExpect(status().isOk()) // 정상동작
.andExpect(jsonPath("$.hospitalname").exists()) // $는 루트 $아래에 hospitalname이 있어야 함
// 반환되는 json 데이터의 hospitalname의 값이 "노소아청소년과의원"으로 반환됨
.andExpect(jsonPath("$.hospitalname").value("노소아청소년과의원"))
.andDo(print());
verify(hospitalRestService).getHospital(hospitalId);
}
Test를 하는 부분이다
builder를 통해 생성자의 순서에 상관없이 데이터 대입이 가능하고, 파라미터 이름과 값을 같이 넣어줌으로서 가독성을 높힐 수 있다. builder 정리
given( ) : 이 객체가 어떤 메서드가 호출되고 어떤 파라미터를 주입받는지 가정한 후 willReturn( ) 메서드를 통해 어떤 결과를 리턴할 것인지 정의하는 구조이다.
perform( ) : HTTP 메서드로 URL을 정의해서 사용한다.
서버로 URL요청을 보내는 것처럼 통신테스트 코드를 작성해서 컨트롤러를 테스트 할 수 있다.
verify( ) : 지정된 메서드가 실행됐는지 검증하는 역할로 given( )에 저의된 동작과 대응하다.