[SpringBoot(2)] JSON형태로 전송, WebMVCTest를 통한 Controller test

배지원·2022년 11월 16일
0

실습

목록 보기
16/24

기존에는 Controller에서 데이터를 View로만 보냈었다면 이번에는 View가 아닌 JSON형태로 데이터를 보내기 위해 RestController를 사용할 것이다.

또한 WebMVCTest를 통해 Controller만의 Test를 만들어 검사를 해볼 것이다.

1. RestController와 Controller의 차이점

  • ControllerModel 을 통해 데이터를 사용자가 볼 수 있는 화면인 View로 전송하여 가공된 데이터를 볼 수 있다.

    @Controller
    @RequestMapping("/hospitals")
  • 백엔드 작업자 혼자서 프로젝트를 만들때는 ControllerMustache 를 통해 데이터를 주고받을 수 있지만, 프론트 엔드 개발자와 현업해서 프로젝트를 진행할 경우 주로 ReactSpringboot간 데이터 통신을 통해 주고 받는다. 그럼 이때 데이터를 어떤 형태로 주고 받을까??

    바로 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 형식으로 서울에 대한 데이터를 전송해줘야 한다.

RestController 실습

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());
    }
}
  • JSON 형식으로 데이터를 보내기 위해서 DTO를 따로 분리하여 데이터를 전송한다. 이때 DTO를 따로 분리하는 이유는 만약 프론트(React)와 백엔드(SpringBoot)에서 통신을 한다고 가정해보자 그럼 JSON형식으로 데이터를 주고 받을텐데 만약 프론트에서 설정한 변수명과 백엔드에서 설정한 변수명이 다르다면 데이터를 주고 받을 수 있을까?? JSON에 작성된 데이터 타입, 변수명 형식 그대로 프론트에서도 같아야지 데이터를 주고 받을 수 있다. 그런데 만약 중간에 프론트에서 데이터 타입이나 변수명을 수정한다고 할때 DTO를 분리하지 않고 Entity를 통해 그대로 보낸다면 데이터가 일치하지 않아 오류가 발생하고 Entity를 수정하기에는 시간이 오래 걸린다. 따라서 이를 위해 JSON형태로 만들기 위한 틀인 DTO를 따로 분리하여 프론트에서 값을 수정해도 DTO에서만 바로 간단하게 수정해서 데이터를 주고 받을 수 있도록 한다.

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로
    }
}
  • JSON 형태로 보내기 위해서는 ResponseEntity를 사용하여 보낼 수 있다. 이때 위에서 말한 것 처럼 hospitalResponse DTO를 분리하여 DTO 형식으로 JSON 데이터를 보낸다.


2. 레이어드 아키텍처란?

  • 코드 아키텍처를 구성할 때는 확장성, 재사용성, 유지 보수 가능성, 가독성과 같은 요소들을 엄두해야한다.

    따라서, 이를 위해 코드의 구조를 어떻게 구성하고 관리해야 하는지에 대한 많은 패턴들이 나와있는데 레이어드 아키텍처는 그 중 백엔드 API 코드에서 가장 널리 사용되는 패턴 중 하나이다.

  • 레이어드 아키텍처의 핵심 요소는 단방향 의존성으로 각각의 레이어는 자기 보다 하위의 있는 레이어에만 의존을 한다. 즉, Presentation -> Business -> Persistence 구조로 이루어져 있다.

  • 기본적으로 Presentation 계층, Business 계층, 데이터 접근 계층으로 3단계로 나누어져 있다.

(1) Presentation layer

  • 해당 시스템을 사용하는 사용자 혹은 클라이언트 시스템과 연결되는 부분으로 웹사이트에서는 UI 부분에 해당하고 백엔드 API에서는 HTTP 요청을 읽어 들이는 로직을 구현한다.
  • 즉, 시스템을 구현하기 보다는 URL을 맵핑해주고 View로 데이터를 넘겨주는 역할을 한다.
  • Controller가 여기에 해당한다.

(2) Business Layer

  • 실제 시스템이 구현해야하는 로직을 현재 레이어에서 동작된다.
  • Controller에서 코드가 중복되는 부분을 처리하기 위해 별도의 객체로 분리를 하는데 그게 Business Layer 이다.
  • Service 가 여기에 해당한다.

(3) Persistence Layer

  • 데이터베이스와 관련된 로직을 구현하는 부분이다.
  • 데이터를 생성, 수정, 읽기 등을 처리하여 실제로 데이터베이스에 데이터를 저장, 수정, 호출하는 역할을 한다.
  • Repository가 여기에 해당한다.

레이어드 아키텍처 실습

  • 기존 코드에서는 Controller에서 바로 Repository를 의존하게끔 만들었었다. 하지만 그렇게 의존하게 되면 Controller에서 중복코드가 발생시 코드 가독성이 떨어지고 효율성이 낮아진다. 따라서 중간에 Service를 생성하여 이를 방지하고 효율성을 높일 수 있다.

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;
   }
}
  • Service 에서는 하위 계층인 Repository를 의존하도록 한다.
  • Service안에는 실제 시스템이 어떤 식으로 동작을 해야하는지 기능을 넣는 곳이라 보면 된다.
  • 위의 코드에서는 Repository를 통해 데이터를 받아와 13일때 영업중, 3일때 폐업으로 출력할 수 있게끔 시스템의 동작을 설계하고 있다.

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로
   }
}
  • Controller에서는 하층 계층인 Service를 의존한다.
  • Service를 통해 객체를 호출하여 만들어진 기능과 데이터만을 View로 보내주기만 한다.

3. WebMVCTest

  • 웹에서 사용되는 요청과 응답에 대한 테스트를 수행할 수 있다.
  • 컨트롤러의 의존성인 Service와 Repository가 필요할 경우 @MockBean으로 모의 의존관계를 생성하여 주입받아 테스트를 진행한다.
  • 테스트의 구조는 간단하게 아래와 같다
    Given: 테스트 수행 전, 필요한 환경 설정. (ex. 변수, Mock 객체 정의)
    When : 실제 테스트 코드와 결과값을 받는다.
    Then : 기대값과 테스트 결과값이 일치하는지 검증한다. (ex. assert.Equals(expected, result) )

❓ Controller를 테스트 하는 이유는?

기존에는 API 통신이 잘 되는지 확인하기 위해서는
(SpringBoot를 실행 → 웹브라우저 or Api 호출 App → /api/v1/hospitals/111 → 눈으로 확인)
과정을 통해 진행을 했으나 WebMVCTest를 하게 되면 이 단계를 모두 생략할 수 있어 편리하다.

실습

(1) @WebMvcTest(테스트 대상 클래스 이름.class)

@WebMvcTest(HospitalRestController.class)
  • 해당 클래스명을 지정을 안해주면 모든 Controller 관련 빈 객체가 모두 로드가 된다. @SpringBootTest보다 가볍게 테스트하기 위해 사용한다.

(2) @MockBean

@Autowired
MockMvc mockMvc;	// MockMvc를 의존함
    
@MockBean   // @Autowired아님 --> HospitalService는 테스트를 위해 가짜 객체를 쓰겠다는 뜻
HospitalService hospitalService; // 가짜 객체를 쓰면 좋은점 DB와 상관없이 테스트 가능
  • MockMvc의 코드는 모두 합쳐져 있어 구분하기는 애매하지만 전체적인 'When-Then'의 구조를 갖추고 있다.

  • MockBean은 실제 빈 개체가 아닌 Mock(가짜) 객체를 생성해서 주입하는 역할을 수행한다.

  • @MockBean이 선언된 객체는 실제 객체가 아니기 때문에 실제 행위를 수행하지 않는다. 그렇기에 해당 객체는 개발자가 Mockito의 given( )메서드를 통해 동작을 정의해야 한다.

(3) @Test

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요청을 보내는 것처럼 통신테스트 코드를 작성해서 컨트롤러를 테스트 할 수 있다.

    • andExpect( ) : 예상되는 결과값을 넣어 실제 결과값과 비교할 수 있음
    • andDo( ) : 요청과 응답의 전체 내용을 커맨드 상으로 출력시켜줌
  • verify( ) : 지정된 메서드가 실행됐는지 검증하는 역할로 given( )에 저의된 동작과 대응하다.

profile
Web Developer

0개의 댓글