반응형

↓↓↓이전내용

2024.01.10 - [Web Application/Backend] - [REST API] Spring Boot로 REST API 프로젝트 (8) - JUnit TEST controller 계층 test 예제

 

[REST API] Spring Boot로 REST API 프로젝트 (8) - JUnit TEST controller 계층 test 예제

↓↓↓ 이전내용 2024.01.05 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (7) - JUnit TEST service 계층 test 예제 [REST API] Spring Boot로 REST API CRUD 간단 구현 (7) - JUnit TEST service 계층 test 예

im-gonna.tistory.com

 

지금까지 만들어온 rest api의 document를 생성해보자. 

 

💡 API Documentation

개발자나 api 사용자에게 api 내 서비스와 상호작용하는 방법을 설명하는 문서이다.
특히 협업할 때 api 문서를 참고하여 api의 동작을 이해하고, 어떤 데이터를 보내고 어떤 응답을 기대하는지 확인하여 여러 개발자가 함께 작업하는데 도움을 준다.

 

이러한 api 문서를 자동으로 생성해주는 프레임워크가 있는데, 바로 Swagger이다.

 

💡 Swagger

swagger는 rest api를 설계하고 문서화하는데 매우 효과적이고, 개발자 커뮤니티에서 많이 사용되고 있다.

따라서 swagger는 개발자가 api를 개발하고, api간의 상호작용을 정의해주는 api 문서를 자동으로 생성해주는 도구이다.

스프링 부트에서 swagger를 사용하려면 springfox 의존성을 추가해 주면 된다. (자세한건 아래에서)

 

swagger의 사용
- api design
- api development
- api testing
- api mocking
- api governance
- api monitoring
- api documentation ✔

 

 

우리는 프론트/ui 디자이너와 일 할 때 api document를 가지고 이 request가 어떻게 동작하게 되는지를 보다 쉽게 이해 시킬 수 있다.

또한 이 메서드의 파라미터는 몇개인지, url은 무엇인지.. 등등

어떻게 response를 생성하고 client에게 전달하는지 그 과정을 설명해주는것이 api 문서이다.

 

만약 document에 수정이 필요하더라도 swagger를 통하면 쉽게 수정하고 바로 반영되기 때문에, 변경된 사항을 바로 확인하고 업무를 지속할 수 있다는 강력한 장점이 있다.

 

 

이제 spring boot에서 swagger를 사용하여 api 문서를 생성해보자.

 

 

💡 swagger 사용을 위한 종속성 추가

 

1. build.gradle

implementation 'io.springfox:springfox-boot-starter:3.0.0'
implementation 'io.springfox:springfox-swagger-ui:3.0.0'

swagger를 사용하기 위해서 springfox-boot-starter 의존성을 추가해준다. 

그리고 swagger-ui도 추가해준다. swagger-ui는 api 문서를 사용자 친화적으로 보기 편리하게 만들어진 브라우저인데, API의 엔드포인트, 매개변수, 응답 등에 대한 정보를 시각적으로 볼 수 있으며, 테스트할 수도 있다.

 

위 코드를 build.gradle의 dependency 안에 추가해준다.

주의할 점은 두개의 버전이 동일해야 한다는 점!!

 

2. yml

  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher

 

이 코드는 yml파일에서 Spring MVC 프레임워크의 경로 매칭 전략을 지정하는 데 사용된다.

이 설정은 경로 매칭 전략을 ant_path_matcher로 설정한 것인데, spring의 ant 스타일의 경로 패턴 매칭 전략을 사용한다는 것을 의미한다.

이러한 mvc 경로 매칭은 URL과 컨트롤러 메소드 사이의 매핑을 결정하는 프로세스이다.

사용자가 특정 URL로 요청을 보내면, spring mvc는 해당 요청을 어떤 컨트롤러 메소드와 매핑할지를 결정하고 실행한다.

ant 스타일의 경로 패턴은 와일드카드(*)를 사용해서 유연한 경로 매칭을 가능하게 하는데, 이를 이용해서 URL을 매칭한다.

예를 들어서 /cloudvendor/*는 /cloudvendor/ 또는 /cloudvendor/c5 와같은 URL과 일치할 수 있다.

따라서 해당 URL과 일치하는 메소드를 매핑하게 된다.

 

 

위와 같이 swagger를 사용할 준비가 되었으면, spring boot를 실행해본다.

실행하면 api 문서도 자동으로 만들어진다.

 

8080 포트에서 성공적으로 연결되었다.

 

 

💡 POST MAN에서 api 문서 생성 확인

 

post man에서 실행 후 생성된 api document를 확인해보자.

 

일단 post를 통해 C5를 생성해보자. 완료

get을 통해 C5를 검색하고 읽어보자. 완료

 

새로운 get request를 만들어서 경로를 http://localhost:8080/v3/api-docs 로 지정해주면, 문서를 읽을 수 있다.

여기서 v3은 version 3라는 뜻. 우리는 3.0.0을 사용하기 때문에! (2.x이면 v2)

이 request의 이름은 Swagger document라고 지정해주자.

send를 누르니, 아래와 같은 response가 출력되었다.

 

openapi의 버전은 3.0.3

생성된 api 문서의 title은 Api Documentation이고, 그 아래와 같은 내용으로 문서가 구성되어 있다.

 

 

💡 swagger-ui

 

위와 같이 확인하니 잘 와닿지 않는다.

그런데 Swagger는 사용자에게 편리한 UI 페이지를 제공하여 문서를 확인할 수 있게한다. 

그게 바로 swagger-ui이다.

http://localhost:8080/swagger-ui/ 이 링크를 웹에 입력하여 확인해보자.

 

swagger-ui 페이지를 보니, postman에서 response에 있던 내용들로 문서가 시각적으로 제공되고있음을 확인할 수 있다.

api document url

 

이렇게 swagger가 자동 생성해준 문서 내용을 내가 커스텀해서 수정할 수도 있다.

현재는 너무 일반적인 정보뿐이고, cloud vendor api의 특성에 맞게 디테일한 것을 추가하고 수정할 수 있다.

basic-error-controller(패키지 정보)를 지우고, api의 주된 controller 내용만 보이게 하며, api 정보도 cloud vendor로 변경해보려 한다.

 

 

💡 api 문서 커스텀하기

 

spring boot application이 시작되는 지점에다가 docket bean을 생성함으로써 커스텀을 진행할 수 있다.

 

docket이란?

docket은 swagger를 사용하여 api 문서를 생성하기 위한 설정 클래스로, api 문서의 어떤 정보를 어떻게 표시할지 등을 설정해주기 위한 용도이다.

 

즉, 실행 컨트롤러 클래스 하에 아래 코드를 추가해준다.

@Bean
	public Docket swaggerConfiguration(){
		return new Docket(DocumentationType.SWAGGER_2)
				.select()
				.paths(PathSelectors.ant("/cloudvendor/*"))
				.apis(RequestHandlerSelectors.basePackage("com.mstoy.restdemo"))//패키지를 그대로
				.build()
				.apiInfo(apiCustomData()); //인자로 ApiInfo를 주어야 함, 아래에서 하나 생성해주고 넣어줄것임
	}
	private ApiInfo apiCustomData(){//여기다가 costom 파라미터들을 설정해주면 됨
		return new ApiInfo(
				"Cloud Vendor API Application",
				"Cloud Vendor Documentation",
				"1.0",
				"Cloud Vendor Service Terms",
				new Contact("Minseo Kim", "https://github.com/minseo0102",
						"sk49058275@gmail.com"),
				"Minseo 0102 License",
				"https://github.com/minseo0102",
				Collections.emptyList()
		);

	}
  • public Docket swaggerConfiguration()
    - Docket을 반환하는 swaggerConfiguration 함수를 정의한다.
    - Docket을 반환하기 때문에 Docket 객체를 동적 할당해준뒤, return한다.
    - 인자로는 문서 타입을 넣어주는데, swagger2로 한다.
    - yml 파일에서 우리는 ant 스타일의 경로 패턴을 사용하기로 했기때문에, 경로 paths는 "/cloudvendor/*"로 설정하여 cloudvendorController에 있는 모든 crud 메서드와 매핑되도록 한다.
    - apis(RequestHandlerSelectors.basePackage("com.mstoy.restdemo"))를 통해 해당 패키지에 속한 컨트롤러만 문서화 하도록 해서 basic-error-controller가 뜨지 않도록 하였다.
    - api 문서의 내용을 커스텀하기 위해서 apiInfo에 내가 설정해준 내용을 담아야 하는데, 이는 ApiInfo라는 객체에 담아서 인자로 전달할 수 있다. 따라서 아래 private으로 ApiInfo 객체를 반환하는 메서드 apiCustomData()를 만들어 주었다.
  • private ApiInfo apiCustomData()
    - ApiInfo 객체를 동적 할당하여 반환하도록 하고, 인자로 커스텀할 정보를 담아준다.
    - 어떤 인자들을 담을 수 있는지는 ApiInfo 클래스를 참고하여 확인할 수 있다.
    - title(string), description(string), version(string), termsOfServiceUrl(string), contact(Contact), license(string), licenseUrl(string), vencorExtentions(collection) 총 8개의 파라미터가 있다.
    - title : api 문서의 제목이다. 프로젝트 또는 api의 이름이 여기에 들어간다. cloudvendor api이므로 "Cloud Vendor API Application"로 설정해주었다.
    - description : api 문서에 대한 간단한 설명이다. api의 목적이나 주요 기능을 적어준다.
    - version : api의 현재 버전이다. 변경될때마다 버전을 높혀줄 수 있다. 1.0을 시작으로 한다.
    - termsOfServiceUrl : 서비스 이용 약관의 URL을 지정한다. api 사용자에게 약관을 제공하고자 할때 활용한다.
    - contact : api에 대한 연락처이다. 담당자 이름, 이메일, 웹사이트를 제공한다. 내 이름과, 이메일, 깃허브 주소를 적었다.
    - license : api의 라이센스 정보다.
    - licenseUrl : 라이센스에 대한 url이다. 라이센스에 대한 자세한 정보를 사용자에게 제공할 때 활용한다. 내 깃허브 주소를 적어주었다.
    - vendorExtensions : swagger 명세에서 제공하는 기능 외 추가적인 속성을 지정할 때 사용한다. 그냥 빈 collection으로 넣어주었다.

커스텀 할 apiInfo에 대해서는 인자로 ApiInfo를 넣어주어야 하기 때문에, private으로 ApiInfo 객체를 반환하는 메서드를 선언해서 원하는 정보로 커스텀 해주고 이 메서드를 apiInfo의 인자로 호출하여 반환되는 ApiInfo 객체를 인자로 넘겨준다.

 

 

bean 객체에서 paths의 경우

.paths(PathSelectors.ant("/cloudvendor/*"))

 

이렇게 실행했을 때, 왜 get이랑 delete만 document에 뜨는지 모르겠다.

모두 띄우기 위해서 와일드카드로 *를 하나 더 붙여주었다.

(이유는 아직 모르겠음.)

 

내가 초기에 controller 클래스에서 각 mapping의 경로를 지정할 때 "/"를 붙이는 걸 빼먹어서 포함이 안되었던 것이다.

추가해주니, 위처럼 * 하나만 붙혀도 crud 모두 document 상에서 확인되었다.

 

실행하고 http://localhost:8080/swagger-ui/ 를 다시 접속하여 커스텀 되었는지 확인해보자.

 

우리가 ApiInfo에다가 설정해준대로, api document 제목도 Cloud Vendor API Application이라고 뜨며, 이 api가 cloud vendor api임을 더욱 이해할 수 있다.

또한 아래 파란색 글씨로, 나의 이름, wedsite, email 정보들이 있으며, license 정보도 잘 적혀있다.

website, email, license의 경우 누르면 해당 페이지로 이동할 수 있다.

또한 cloud vendor api만 문서화함으로써 아래 basic-error-controller도 제외된 것을 확인할 수 있다.

 

cloud vendor controller에 대한 모든 crud 메서드가 뜨는 것을 확인할 수 있다.

 

저안에서 직접 값을 입력하고 검색하며 api의 동작을 확인하고 이해할 수 있다.

 

 

 

api 사용자가 이 swagger ui를 열어서 봤을 때 조금더 명확하게 정보를 이해하도록 할 수는 없을까??

특정 model의 어떤 속성은 무슨 속성을 말하는 걸까? 이런 디테일한 정보를 알려주기 위해서 좀 더 디테일하게 커스텀을 진행해보자. 필수는 아니지만, 이렇게 디테일하게 알려줌으로써 api 사용자에게 확실한 정보를 줄 수 있다.

 

이를 위해서 각 controller와 model의 속성 각각에 추가 정보를 담아서 document에 반영해보자.

 

 

💡 controller & model api doc detail custom

 

먼저 controller의 get 메서드이다.

@GetMapping("/{vendorId}")
@ApiOperation(value = "cloud vendor id", notes = "Provide cloud vendor details",
response = ResponseEntity.class)

 

vendorId로 cloudvendor를 검색하기 위한 (읽기 위한) 메서드이다.

이 메서드에 대한 정보를 좀 더 디테일하게 알려주기 위해서 아래에 @ApiOperation이라는 어노테이션을 추가한다.

그리고 이 메서드의 동작에 대한 내용을 인자에 적어주는 것이다.

value = 이 메서드의 입력으로 들어가는 정보 = cloud vendor의 id

notes = 이 메서드가 어떤 역할을 하는지 = 해당 cloud vendor의 정보를 제공한다.

response = 응답 형태 = ResponseEntity 형태로 반환을 한다.

 

이러한 작은 코드가 api document를 더욱 명확하게 만들어 준다.

 

 

다음으로 model인 cloudvendor를 보자.

@Entity
@Table(name = "cloud_vendor_info")
@ApiModel(description = "This table holds cloud vendor information.")

이 entity가 어떤 정보인지에 대한 설명을 추가하고자 한다.

table 아래 @ApiModel 어노테이션을 추가하여 이 entity에 대한 desciption을 추가해줄 수 있다.

 

또한 entity 내의 각 속성에 대해서도 추가 설명을 적을 수 있다.

@Id
@ApiModelProperty(notes = "This is a Cloud Vendor Id. It shall be unique.")
private String vendorId;

vendorId에 대한 추가 정보를 @ApiModelProperty 어노테이션을 추가함으로써 적을 수 있다.

 

실행 후, swagger-ui를 확인해보자.

 

변경된 내용을 보면, get은 cloud vendor id를 입력으로 받으며, 이 메서드는 cloud vendor details을 제공한다는 설명이 있는 것을 확인할 수 있다.

 

 

 

또한 model의 cloudvendor를 보면 description에 추가해준 내용이 반영되어, 이 모델은 cloud vendor의 정보를 담는 table임을 명확하게 전달하고 있다.

또한 그 중 vendorId 속성에 대해서도, cloud vendor id를 나타내며 unique한 특성을 가진다는 정보를 전달하고 있다.

 

이로써 우리가 커스텀한 대로 api document에 잘 반영되는 것을 확인할 수 있으며, api의 동작이 좀 더 이해하기 쉽고 명확해진 것을 알 수 있다.

 

이렇게 api 문서를 통해 api 사용자가 이해하기 쉽도록 정보를 전달함으로써 개발 효율을 높힐 수 있다.

반응형
반응형

↓↓↓ 이전내용

2024.01.05 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (7) - JUnit TEST service 계층 test 예제

 

[REST API] Spring Boot로 REST API CRUD 간단 구현 (7) - JUnit TEST service 계층 test 예제

↓↓↓ 이전 내용 2023.12.30 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (6) - JUnit TEST 기본 설정 및 repository 계층 test 예제 [REST API] Spring Boot로 REST API CRUD 간단 구현 (6) - JUnit TEST

im-gonna.tistory.com

 

이전 내용에 이어서 이번에는 최상단 layer인 Controller의 test를 진행해보자.

 

main에 있는 controller class 이름에 ctrl+shift+t를 눌러서 test 클래스를 자동 생성해준다.

 

💡 Controller test class

 

import 되어있는 jupiter assertion은 제거한다. (이전에 설명했지만 기본 assertion 사용!)

 

  • 클래스 어노테이션 추가

외부 data와 직접 상호작용하는 계층이기 때문에 test class에 어노테이션을 추가해준다.

 

그리고 컨트롤러 클래스를 인자로 가져온다.

 

  • mockMvc 주입

 

web layer url test이기 때문에, 모의 MVC가 필요하다.

따라서 MockMvc 객체를 하나 선언하여 사용한다.

 

  • 필요한 객체 선언

controller 레이어는 service 레이어와 통신하기 때문에 모의 service를 만들어주어야 한다.

그리고 cloudvendor 인스턴스 2개를 각각 만들어 주고, 이들을 담을 리스트로 하나 선언해준다.

 

  • setUp 메서드

 cloudVendorOne 인스턴스와 cloudVendorTwo 인스턴스를 생성해주고, list에 담아준다.

 

 

  • getCloudVendorDetails()의 test 메서드

  • controller에 정의된 getCloudVendorDetails()의 로직을 보면, cloudVendorService의 getCloudVendor()를 호출한 결과를 반환한다.
  • 따라서 cloudVendorService의 getCloudVendor()에 cloudVendorId가 1로 전달하며, 이 메서드를 호출하면 cloudVendorOne을 return하여 성공적으로 보이도록 한다.
  • 그리고 mockMvc의 perform메서드를 통해 get 동작을 수행하도록 하고 url은 controller에서 지정한대로 cloudVendorId에 따라 달라지므로, /cloudvendor/1 로 가져오도록 한다.
  • andDo(print()): 이 메서드는 테스트 결과를 출력한다. 테스트가 실행될 때 컨트롤러가 반환하는 HTTP 응답 등의 정보를 콘솔에 출력하여 디버깅이나 테스트 결과 확인을 도와준다.
  • andExpect(status().isOk()): 이 메서드는 특정한 조건을 검증하는데, 여기서는 HTTP 응답의 상태 코드가 "200 OK"인지 확인한다. 만약 상태 코드가 200이 아니면 테스트는 실패하는 것이다.
  • 여기까지하면, perform get에서 빨간 줄이 쳐질 것이다. 예외처리가 필요하다. 메서드에 throws Exception을 추가해주자.

getCloudVendorDetails test 결과

 

테스트 결과 성공이다.

getCloudVendorDetails()의 test를 위해 모의(mock) http 서블릿 요청이 제대로 구축되는 것을 확인할 수 있다.

(이는 마치 레포지토리 레이어에서 내장된 데이터베이스 h2를 사용하는 것과 유사한 상황이다)

파라미터와 헤더는 아무것도 필요로 하지 않았다.

 

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"data":{"vendorId":"1","vendorName":"Amazon","vendorAddress":"USA","vendorPhoneNumber":"xxxxx"},"httpStatus":"OK","message":"Requested Vendor Details are given here"}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

그 아래 더 내려서 response 부분을 보면, status는 200, 에러 사항 없음, body data에 controller에서 반환하는 CloudVendorOne에 대한 data가 담겨져 있는 것을 확인할 수 있다.

 

 

  • getAllCloudVendorDetails()의 test 메서드

  • 앞의 get메서드와 로직은 유사하나, service 메서드를 getAll로 수정해주고, 가져오는 단위가 list이기 때문에, 초기에 설정해주었던 cloudVendorList를 return하도록 한다.
  • 모든 cloudVendor를 가져올 때에는 기본 url을 사용하기 때문에, /cloudvendor로 mapping되도록 한다. 

getAllCloudVendorDetails test 결과

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = [{"vendorId":"1","vendorName":"Amazon","vendorAddress":"USA","vendorPhoneNumber":"xxxxx"},{"vendorId":"2","vendorName":"GCP","vendorAddress":"UK","vendorPhoneNumber":"yyyyy"}]
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

body를 보면 one, tow 모두 값을 가져오는 것을 확인할 수 있다.

 

 

  • deleteCloudVendorDetails()의 test 메서드

  • controller의 delete 메서드 로직을 보면, service의 delete메서드를 호출하고, 이 안에서는 Success 메시지를 반환하도록 되어있다.
  • 따라서 cloudvendorId가 1인 cloudvendor를 delete하도록 service 메서드가 호출되면, Success를 return하도록 하여, service의 delete 메서드 호출을 성공적으로 보이게 한다.
  • 그리고 mockMvc의 perform을 통해 delete mapping시 url은 /cloudvendor/1에 매핑되도록 한다.

deleteCloudVendorDetails test 결과

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"33"]
     Content type = text/plain;charset=UTF-8
             Body = Cloud Vendor Deleted Successfully
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

response의 body를 보면 controller의 해당 메서드에서 반환하는 값인 메시지를 반환하는 것을 확인할 수 있다.

 

 

  • createCloudVendorDetails()의 test 메서드

 

  • controller에서 create메서드 로직을 살펴보면, cloudVendorService의 create메서드를 호출한다. 그 메서드 안에서는 success를 return한다.
  • 따라서 create메서드가 호출되면 Sucess를 return하도록 한다.
  • perform의 post mapping 시 /cloudvendor url과 mapping되도록 한다.
  • 그런데, 내용으로 전달되는 data를 json형식으로 전달하여야 하기 때문에, contentType을 json으로 지정해준다.
  • 그런데, cloudvendor는 entity이기 때문에, json형식으로 바꾸어주기 위해 위 코드를 작성해준다.
  • json 형식의 cloudvendor를 requestJson에 저장해주고, requestJson을 content에 넣는다.

createCloudVendorDetails test 결과

 

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"33"]
     Content type = text/plain;charset=UTF-8
             Body = Cloud Vendor Created Successfully
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

response의 body를 보면 controller의 해당 메서드에서 반환하는 값인 메시지를 반환하는 것을 확인할 수 있다.

 

 

  • updateCloudVendorDetails()의 test 메서드

  • 앞의 create 메서드와 로직이 유사하기 때문에, service 메서드의 create를 update로 변경하고, perform메서드는 post를 put으로 변경한다.

updateCloudVendorDetails test 결과

 

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"33"]
     Content type = text/plain;charset=UTF-8
             Body = Cloud Vendor Updated Successfully
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

역시 response의 body에는 controller의 해당 메서드에서 반환하는 값인 메시지를 담고 있다.

 

모든 unit test를 마쳤으므로, 전체 test class의 test coverage를 확인해보자.

 

controller의 test coverage 결과

test coverage가 100% 인것을 확인하였다.

 

summery

 

 

↓↓↓ 다음 내용

2024.01.25 - [Web Application/Backend] - [REST API] Spring Boot로 REST API 프로젝트 (9) - Swagger로 api document 생성하기

 

[REST API] Spring Boot로 REST API 프로젝트 (9) - Swagger로 api document 생성하기

↓↓↓이전내용 2024.01.10 - [Web Application/Backend] - [REST API] Spring Boot로 REST API 프로젝트 (8) - JUnit TEST controller 계층 test 예제 [REST API] Spring Boot로 REST API 프로젝트 (8) - JUnit TEST controller 계층 test 예제 ↓

im-gonna.tistory.com

반응형
반응형

↓↓↓ 이전 내용

2023.12.30 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (6) - JUnit TEST 기본 설정 및 repository 계층 test 예제

 

[REST API] Spring Boot로 REST API CRUD 간단 구현 (6) - JUnit TEST 기본 설정 및 repository 계층 test 예제

↓↓↓ 이전 내용 2023.12.15 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (5)-사용자 정의 ResponseEntity [REST API] Spring Boot로 REST API CRUD 간단 구현 (5)-사용자 정의 ResponseEntity 이전 내

im-gonna.tistory.com

 

지난 시간 repository 계층의 test를 수행했다.

이번에는 service 계층의 test를 수행하겠다.

 

💡 Service layer test 클래스 자동 생성하기

 

직접 디렉토리를 생성하고 test 클래스를 작성해도 되지만, service 클래스에서 "create test" 버튼 하나로 test 클래스를 바로 생성할 수 있다.

 

create test를 누르면 다음과 같은 창이 뜬다.

 

JUnit5는 기본으로 설정되어 있으며, test class 이름도 알아서 지정해 주는데, 적절해 보이므로 그대로 사용할 것이다.

마찬가지로 설정/해제 메서드를 추가해준다. setUp과 tearDown에 해당한다.

그리고 class의 모든 메서드에 대한 단위 test를 위해 모두 ✔ 하여 추가해준다.

 

 

OK버튼을 누르면, 위와같이 test 디렉토리 하에 repository와 병렬하게 service 단이 생성되었고, 그 안에 test 클래스도 잘 생성되었다.

 

💡 CloudVendorService test 코드 작성하기

 

클래스를 들여다 보면, jUnit Jupiter assertion이 import되어 있는데, 우리는 저번 시간과 동일하게 기본 assertion을 사용할 것이기 때문에 이 부분은(이미 주석처리 되어있지만) 지워준다.

 

우리는 arg.com에서 제공하는 assertion 라이브러리를 사용할 것이다.

 

 

  • Mock 객체 생성하기

 

서비스 계층이 데이터베이스와 직접적으로 통신해서는 안되기 때문에, 데이터베이스 응답을 얻으려면 repository layer를 moking해야 한다. 이 말은 repository 클래스의 객체를 mock object (모의 객체)로서 생성하여 사용함으로써 데이터를 사용한다는 것이다.

 

가상 객체라고 생각하면 된다.

 

따라서 CloudVendorRepository의 경우 mock 객체로서 사용한다.

 

autoCloseable 객체를 사용하는 이유는 리소스를 자동 해제하는 기능으로서 자원을 안전하게 사용하기 위함이다.

 

 

  • 설정 메서드 setUp()

 

클래스 실행 시 전체 클래스에 대한 mock이 자동으로 열리도록 한다. 이는 Mockito의 openMocks 메서드를 사용한다.

그리고 repository test에서와 동일하게, service 객체와 cloudVendor 객체를 생성해준다.

 

  • 해제 메서드

해제 시에는 autoCloseable을 close해주면 된다.

 

  • create 메서드에 대한 test

  • cloudvendor와 repository에 대해서 mock 객체를 선언해준다.
  • when 메서드를 사용해서 cloudvendorrepository의 save메서드를 호출하였을 때 cloudvendor를 return하도록 하여, 저장이 성공한 것처럼 보이도록 설정한다.
  • asserthat 메서드를 통해 실제 service 클래스의 create 메서드를 호출한 결과가 Success와 같은지 확인한다.
  • 결과가 true이면 test가 통과된다. 

createCloudVendor()의 unit test 결과

 

 

  • update 메서드에 대한 test

    • update 메서드는 앞선 create와 유사하므로 실제 service 클래스에서 호출하는 메서드를 update로만 바꾸어 주면 된다.
    • 이 역시 호출결과가 Success를 반환하면 test 통과이다.

updateCloudVendor()의 unit test 결과

 

  • get 메서드에 대한 test

  • mock 객체를 생성한다.
  • when 메서드를 사용해서 cloudvendorrepository의 findById메서드를 호출하였을 때, cloudvendor를 return하도록 하는데, 이때 find결과가 있을수도 없을 수도 있으므로 optional로 nullable을 설정해준다.
  • asserthat 메서드를 통해 실제 service 클래스의 get 메서드를 호출하여 그 cloudvendor의 name과 클래스에서 선언한 cloudvendor의 name이 동일한지 확인한다.
  • 결과가 true이면 테스트는 통과이다.

getCloudVendor()의 unit test 결과

 

  • get all 메서드에 대한 test

  • 앞선 get메서드와 로직은 비슷하나, all 이므로 list단위로 반환한다는 차이가 있다.
  • 따라서 리스트 중 첫번째 cloudVendor의 phoneNumber를 비교하도록 한다.
  • 결과로 true를 반환하면 test는 통과이다.

getAllCloudVendor()의 unit test 결과

 

  • delete 메서드에 대한 test

  • 이 메서드의 test에서는 mock을 생성할 때, 실제로 repository 클래스의 메서드를 사용할 수 있도록 설정해준다.
  • 이를 위해서 Mokito의 CALLS_REAL_METHODS를 인자로 함께 넣어준다.
  • 또한 doAnswer를 사용해, cloudVendorRepository가 호출되었을 때 실제로 Method를 사용하도록 하고 deleteById 메서드를 호출하도록 한다.
  • asserThat으로 service의 delete메서드를 수행하고 나서 Success를 반환해야 한다.
  • 반환한다면 test는 통과한다.

deleteCloudVendor()의 unit test 결과

 

service test 클래스의 test 실행 결과 모두 통과하는 것을 확인하였다.

service 단의 모든 unit test가 성공적

 

 

test시에는 내 코드가 실제로 구현한 모든 코드를 test할 수 있는지 확인할 필요가 있다.

보통 90%이상이면 옳다고 판단한다.

IntelliJ에서는 아래와 같이 test coverage를 확인할 수 있도록 기능을 제공하고 있다. 

test coverage 확인 결과 91%

 

service class의 test coverage 확인 결과 91%가 cover되는 것을 확인하였다.

그럼 어느 부분을 cover하지 못했을까?

실제 service class의 코드를 살펴보자.

 

각 메서드마다 test가 cover된 부분은 왼쪽에 초록색 바로 표시가 된다. 

내리다 보니, getCloudVendor 메서드 쪽에서 중간에 빨간색 바를 확인하였다.

 

왼쪽을 보면 throw~ 예외처리 부분에 대한 test의 범위는 빨간색으로 표시되면서 포함되지 않은 것을 확인할 수 있다.

 

이렇게 test covergae를 통해서 내가 짠 test 코드가 실제로 구현의 어느 부분까지 cover할 수 있는지 확인 가능하다.

 

 

오늘은 service layer의 test code를 구현해보았다.

 

다음 시간에는 Controller의 test를 구현해보자.

 

↓↓↓ 다음 내용

2024.01.10 - [Web Application/Backend] - [REST API] Spring Boot로 REST API 프로젝트 (8) - JUnit TEST controller 계층 test 예제

 

[REST API] Spring Boot로 REST API 프로젝트 (8) - JUnit TEST controller 계층 test 예제

↓↓↓ 이전내용 2024.01.05 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (7) - JUnit TEST service 계층 test 예제 [REST API] Spring Boot로 REST API CRUD 간단 구현 (7) - JUnit TEST service 계층 test 예

im-gonna.tistory.com

 

반응형
반응형

 

↓↓↓ 이전 내용

2023.12.15 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (5)-사용자 정의 ResponseEntity

 

[REST API] Spring Boot로 REST API CRUD 간단 구현 (5)-사용자 정의 ResponseEntity

이전 내용↓↓↓ 2023.11.22 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (4)-예외처리/handle Exception [REST API] Spring Boot로 REST API CRUD 간단 구현 (4)-예외처리/handle Exception 이전 내용↓

im-gonna.tistory.com

💡 JUnit test 기본 설정

 

작성한 코드가 백에서 잘 동작하는 지를 확인하기 위해 TEST하는 기능이 있다.

스프링 부트에서는 JUnit을 활용하여 테스트 한다.

 

JUnit을 이용한 테스트 시 다음 네가지 종속성을 확인해야 한다.

JUNIT5, AssertJ, Mockito, H2Database

 

기본적으로 spring initializer로 프로젝트 생성 시 앞에 3개는 자동으로 종속되어 있다.

build.gradle의 dependency를 보면 확인이 가능하다.

 

따라서 h2 database만 따로 종속성을 추가해준다.

test시에 사용할 것이므로 다음 코드를 추가한다.

testImplementation 'com.h2database:h2:1.4.200'

 

test 사용 시 필요한 기능을 갖춘 종속성은 모두 추가해주었다.

 

그렇다면 test는 어디서 진행되는 것일까?

 

 

디렉토리를 보면 main과 test가 나란히 있는 것을 알 수 있다.

우리는 지금까지 main 이하에 디렉토리와 클래스를 생성해가며 실행했었지만, 나란히 존재하는 test 디렉토리에서는 unit test를 진행할 수 있다. 여기서 test를 실행한다.

 

실제 프로젝트가 돌아가는 동작은 main에서 일어난다. main에도 java와 resources 하위 디렉토리가 존재한다.

따라서 test를 위한 test 디렉토리 하에도 java와 병렬하게 resources 디렉토리를 생성해주자.

resources 디렉토리를 생성하였으면 그 안에 application.yml 파일을 추가해준다.

 

테스트 시 사용하는 데이터베이스는 h2이기 때문에, 스프링부트에 내장된 h2 데이터베이스를 연동하기 위해서 아래 코드를 yml파일에 추가해준다.

spring:
  datasource:
    url: jdbc:h2://mem:db;DB_CLOSE_DELAY=-1
    username: sa
    password: sa
    driver-class-name: org.h2.Driver

  # The SQL dialect makes Hibernate generate better SQL for the chosen database
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.H2Dialect
    show-sql: true

  #JPA settings
  jpa.hibernate.ddl-auto: create-drop

 

create-drop으로 하는 이유는 스프링부트를 종료했을 때 모두 drop하고 새로운 애플리케이션이 돌아갔을 때 새로 받기 위함이다.

 

💡 Repository test

 

이제 테스트를 할 기초 준비를 마쳤으니, repository의 동작부터 test해보자.

 

main의 형태와 동일하게 test에서도 repository 디렉토리를 만들어주고, 그 아래 CloudVendorRepositoryTest 클래스를 추가하여 CloudVendorRepository를 test한다.

 

 

이제 클래스를 test클래스로 정의해주자.

 

repository test 클래스를 test하기 위해서 @DataJpaTest 어노테이션을 추가해 식별해준다.

이때, 테스트 작업을 할 때, 모든 초기화 작업을 수행하는 것이 모범 사례이다.

무슨 의미이냐 하면, 테스트를 할 때도 비교할 만한 대상이 있어야 하기 때문에, 테스트 용 데이터를 넣어준다는 것이다.

 

main에서 만들어준 CloudVendorRepository가 정상 동작하는 지 확인해야 하기 때문에, repository를 하나와, 데이터를 저장해주기 위해 CloudVendor 객체를 하나 정의해 준다.

 

  • 설정 메서드

 

설정 메서드를 통해 각 test케이스가 실행되기 전에, 미리 데이터베이스에 테스트 용 객체를 생성해준다.

위와 같은 값을 가지는 CloudVendor 객체를 생성해주고, 데이터 베이스에 저장해준다.

 

  • 해제 메서드

 

해제 메서드를 통해 각 test케이스가 실행되고 난 후에는, 다시 객체를 반환하여 메모리를 반환해준다.

 

 

  • 성공 테스트

먼저, 성공적으로 조회가 된 경우에 대한 테스트를 정의해보자.

  • 테스트 케이스를 사용할 때마다 @Test 어노테이션을 사용하여, 아래 메서드가 일반 메서드가 아니라, 테스트 케이스임을 식별할 수 있다. 따라서 이 표시가 있는 메서드 옆에는 개별적으로 실행할 수 있도록 재생 표시가 있는 것을 확인할 수 있다.
  • 이 test의 메서드 이름은 testFindByVendorName_Found이다.
  • cloudVendorRepository의 findByVendorName을 통해 "Amazon"을 이름으로 가진 cloudVendor를 찾도록 하여 조회 결과를 list에 저장한다.
  • assertThat을 통해 실제로 예상되는 값을 반환하는지 확인할 수 있다.
  • asserThat을 통해 list에 저장된 cloudVendor의 id가, 우리가 설정해준 Amazon의 id와 동일한지 확인한다.
  • asserThat을 통해 list에 저장된 cloudVendor의 Address가, 우리가 설정해준 Amazon의 Address와 동일한지 확인한다.
  • 모두 동일하다면 이 테스트는 성공적인 결과를 반환할 것이다.

 

첫번째 테스트에 대한 실행 결과 로그를 살펴보자.

 

select와 insert 쿼리가 실행되며 데이터베이스에 입력하고, 주어진 입력에 대해 조회한 것을 확인할 수 있다.

 

또한 EmbeddedDatabaseFactory 사용을 시작하는 것과 함께, 스프링부트에 내장된 jdbc h2 데이터베이스를 사용한다는 것을 확인할 수 있다.

즉, 테스트 시에는 내 pc에 있는 mysql db가 아닌, 내장된 h2 데이터베이스를 사용하여 테스트 한다.

 

  • 실패 테스트

다음으로, 조회에 실패한 경우에 대한 테스트를 정의해보자.

 

 

  • 데이터베이스에 존재하지 않는 cloudvendor 이름을 찾으려고 하는 실패 테스트 케이스에 대한 테스트이다.
  • 현재는 내장 db에 Amazon 객체만 들어가 있는 상태임을 명심하자.
  • 이 메서드는 testFindByVendorName_NotFound이다.
  • 마찬가지로, repository 조회를 GCP로 조회하고 그 결과를 list에 저장하도록 한다.
  • 당연히 내장 db에는 Amazon에 대한 데이터밖에 없기 때문에 GCP로 조회한 결과는 비어있어야 한다.
  • 이를 확인하기 위해 asserThat 메서드를 통해서 lsit가 비어 있음이 true임을 확인하도록 한다.
  • 이 assertion이 통과하면 리스트는 비어있다는 것이고, 이 테스트는 성공적으로 통과하게 된다.

 

💡 respository test 클래스에 대한 실행 결과

 레포지토리 테스트 클래스를 실행한 결과, 내부에 있는 unit 테스트 두개(성공사례, 실패사례) 모두 성공적으로 잘 테스트 되었음을 ✔표시를 통해 확인할 수 있다.

 

만약 test case가 잘못 적용된다면 어떻게 될까? 다음과 같이 실패 케이스에서 isTrue 부분을 isFalse로 바꾸어 보자.

 

 

테스트 결과 NotFound 테스트에서는 실패 표시가 나타나고, 왜 실패했는지 옆에 로그를 통해 설명해주고 있다.

테스트 로직 내에서는 FALSE를 기대했으나, 실제 값은 TRUE를 반환하므로 테스트 결과가 실패로 뜨고 있다.

따라서 이런 로그를 통해 테스트 내에서 어떤 값을 예상하고 출력하는지 확인함으로써 수정할 수 있다.

 

이렇게 Repository test를 수행할 수 있다!

 

 

↓↓↓ 다음 내용

2024.01.05 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (7) - JUnit TEST service 계층 test 예제

 

[REST API] Spring Boot로 REST API CRUD 간단 구현 (7) - JUnit TEST service 계층 test 예제

↓↓↓ 이전 내용 2023.12.30 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (6) - JUnit TEST 기본 설정 및 repository 계층 test 예제 [REST API] Spring Boot로 REST API CRUD 간단 구현 (6) - JUnit TEST

im-gonna.tistory.com

 

반응형
반응형
REST : 웹(HTTP)의 장점을 활용한 아키텍쳐

 

1. REST (REpresentational State Transfer) 기본

  • REST의 요소
  • method
method 의미 idemptent
POST Create No
GET Select Yes
PUT Update Yes
DELETE Delete Yes

 

** idempotent : 한 번 수행하냐, 여러 번 수행했을 때 결과가 같나?

 

  • Resource
    ✔ http://myweb/users와 같은 URI
    ✔ 모든 것을 Resource (명사)로 표현하고, 세부 Resource에는 id를 붙임

  • Message
    ✔ 메시지 포맷이 존재
    : JSON, XML 과 같은 형태가 있음 (최근에는 JSON 을 씀)
HTTP POST, http://myweb/users/
{
	"users" : {
		"name" : "terry"
	}
}

 

  • REST 특징
  • Uniform Interface (일관된 인터페이스)
    • HTTP 표준을 기반으로 하며, 특정 언어나 기술에 종속되지 않음.
    • 예를 들어, REST API가 HTTP와 JSON을 사용하여 정의되었다면, 어떤 플랫폼이든 해당 API에 접근 가능.
  • Self-Descriptive Messages (자기 서술적 메시지)
    • API 메시지 자체만으로도 그 의미를 이해할 수 있도록 설계해야 함.
    • 메시지를 보고도 어떤 리소스에 어떤 동작을 수행하는지 직관적으로 이해할 수 있어야 함.
  • HATEOAS (Hypermedia As The Engine Of Application State)
    • 응답에는 현재의 상태를 나타내는 하이퍼링크가 포함되어야 함.
    • 클라이언트는 이 링크를 통해 다음에 수행 가능한 작업을 이해하고 진행할 수 있음.
  • Statelessness (무상태성)
    • 각각의 요청은 독립적이며, 서버는 클라이언트의 상태를 저장하지 않음.
    • 이로써 서버는 간단해지고, 확장성이 높아짐.
  • Resource 지향 아키텍처 (ROA)
    • 자원(리소스)을 중심으로 하는 아키텍처
    • 각 리소스는 고유한 URI(Uniform Resource Identifier)를 가지며, 명사 형태로 정의됨.
  • Client-Server Architecture (클라이언트-서버 아키텍처)
    • 시스템을 클라이언트와 서버로 분리함으로써 각각의 역할을 명확하게.
  • Cache Ability (캐시 사용 가능)
    • 응답은 캐싱될 수 있어서, 동일한 요청에 대한 반복적인 처리를 최소화
  • Layered System (계층화 구조)
    • 시스템은 계층화될 수 있어서, 각 계층은 독립적으로 구현

  • Code On Demand (선택적 코드 전송)
    • 서버로부터 클라이언트가 실행 가능한 코드를 전송할 수 있습니다. 이는 선택 사항이며, 일반적으로는 사용되지 않음

=> REST는 자원을 중심으로 하며, 간단하고 일관된 인터페이스를 통해 클라이언트와 서버 간의 통신을 단순화하고 효율적으로 만들기 위한 웹 아키텍처

 

 

***참고
https://gyoogle.dev/blog/web-knowledge/REST%20API.html

반응형

'Web Application' 카테고리의 다른 글

HTTP status code (HTTP 상태 코드)  (0) 2023.12.28
HTTP Request Methods  (0) 2023.12.23
쿠키(cookie)와 세션(session)의 차이  (1) 2023.12.22
브라우저 동작 방법  (0) 2023.12.20
[Node.js] Node.js와 Javascript의 개념  (0) 2023.09.13
반응형

클라우드 환경에서 HTTP API를 통해 통신하는 것이 대부분이다.

이때, 응답 상태 코드를 통해 성공/실패 여부를 확인할 수 있으므로 API 문서를 작성할 때 꼭 알아야 할 것이 HTTP status code이다.

 

  • 10x : 정보 확인
  • 20x : 통신 성공
  • 30x : 리다이렉트
  • 40x : 클라이언트 오류
  • 50x : 서버 오류

 

200번대 : 통신 성공

상태 코드 이름 의미
200 OK 요청 성공(GET)
201 Create 생성 성공(POST)
202 Accepted 요청 접수O,
리소스 처리 X
204 No Contents 요청 성공 O,
내용 없음

 

 

300번대 : 리다이렉트

상태코드 이름 의미
300 Multiple Choice 요청 URI에 여러 리소스가 존재
301 Move Permanently 요청 URI가 새 위치로 옮겨감
304 Not Modified 요청 URI의 내용이 변경 X

 

 

400번대 : 클라이언트 오류

상태코드 이름 의미
400 Bad Request API에서 정의되지 않은 요청 들어옴
401 Unauthorized 인증 오류
403 Forbidden 권한 밖의 접근 시도
404 Not Found 요청 URI에 대한 리소스 존재 X
405 Method Not Allowed API에서 정의되지 않은 메소드 호출
406 Not Accepted 처리 불가
408 Request Timeout 요청 대기 시간 초과
409 Conflict 모순
429 Too Many Request 요청 횟수 상한 초과

 

 

500번대 : 서버 오류

상태코드 이름 의미
500 Internal Server Error 서버 내부 오류
502 Bad Gateway 게이트웨이 오류
503 Service Unavailable 서비스 이용 불가
504 Gateway Timeout 게이트웨이 시간 초과

 

 

***참고

https://gyoogle.dev/blog/web-knowledge/HTTP%20status%20code.html

반응형

'Web Application' 카테고리의 다른 글

REST API  (0) 2023.12.29
HTTP Request Methods  (0) 2023.12.23
쿠키(cookie)와 세션(session)의 차이  (1) 2023.12.22
브라우저 동작 방법  (0) 2023.12.20
[Node.js] Node.js와 Javascript의 개념  (0) 2023.09.13

+ Recent posts