[76일차]SoloProject - 1

유태형·2022년 8월 16일
0

코드스테이츠

목록 보기
75/77

오늘의 목표

  1. API 문서 설명
  2. 소스 코드
  3. 작성한 API 문서



내용

API 문서 작성

API 문서의 역할

애플리케이션이나 프로그램, 시스템을 개발할 때 한가지 분야의 소수 개발자만 참여한다면 그때 그때 직접적으로 논의하고 수정해 나갈 수 있지만, 개발의 범위가 커지고 인원이 많아진다면 필요할 때 마다 논의하는 것이 힘들어 질 것이고, 미리 정해놓은 약속이 필요할 것입니다. 그 약속을 정의해 놓은 문서가 API 문서입니다.

보통 프론트엔드 - 백엔드 간의 협업 시 주고 받을 데이터나 메서드를 개발 전 사전에 정의하여 혼선을 생기는 상황을 방지하는데에도 사용됩니다.



API 문서의 종류

API문서 작성은 여러방법으로 작성할 수도 있습니다.gitbook, postman처럼 개발자가 타자를 직접 치는 방법도 있고, 손으로 직접 그리면서도 할 수 있고, Rest API Docs와 같이 테스트 통과시에만 자동화로 API문서를 작성할 수도 있습니다.

그 중에서도 저는 Rest API Docs를 활용하여 API문서를 작성하였습니다.



Rest API Docs 설명

Rest API Docs에 대한 자세한 설명 : https://velog.io/@ds02168/55일차Swagger-Spring-Rest-Docs

아래의 소스코드도 위의 내용을 토대로 작성하였습니다.




소스 코드

build.gradle application.yml등의 설정은 위의 설명과 동일하게 수행하였습니다.

Service, Repository, Mapper는 실제 서비스 구현이 목적이 아니라 Mocking객체로 대체하여 테스트 하므로 로직 구현은 필요하지 않습니다.(테스트에 필요한 메서드 선언만)



컨트롤러

@Slf4j
@RestController
@Validated
@RequestMapping("v1/members")
public class MemberController {
    private final MemberService memberService;
    private final MemberMapper mapper;

    public MemberController(MemberService memberService, MemberMapper mapper){
        this.memberService = memberService;
        this.mapper = mapper;
    }

    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberDto.Post requestBody){
        Member member = mapper.memberPostToMember(requestBody);
        Member createdMember = memberService.createMember(member);
        MemberDto.response response = mapper.memberToMemberResponse(createdMember);

        return new ResponseEntity<>(
                new SingleResponseDto(response),
                HttpStatus.CREATED);
    }

    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember(@PathVariable("member-id") @Positive long memberId,
                                      @Valid @RequestBody MemberDto.Patch requestBody){
        requestBody.setMemberId(memberId);
        Member member = memberService.updateMember(mapper.memberPatchToMember(requestBody));
        MemberDto.response response = mapper.memberToMemberResponse(member);

        return new ResponseEntity<>(
                new SingleResponseDto<>(response),HttpStatus.OK
        );
    }

    @GetMapping("/{member-id}")
    public ResponseEntity getMember(@PathVariable("member-id") @Positive long memberId){
        Member member = memberService.findMember(memberId);
        MemberDto.response response = mapper.memberToMemberResponse(member);
        return new ResponseEntity<>(
                new SingleResponseDto<>(response),
                HttpStatus.OK
        );
    }

    @GetMapping
    public ResponseEntity getMembers(@Positive @RequestParam int page,
                                     @Positive @RequestParam int size){
        Page<Member> pageMembers = memberService.findMembers(page -1,size);
        List<Member> members = pageMembers.getContent();
        List<MemberDto.response> responses = mapper.membersToMemberResponses(members);
        return new ResponseEntity<>(
                new MultiResponseDto<>(responses,pageMembers),
                        HttpStatus.OK
        );
    }

    @DeleteMapping("/{member-id}")
    public ResponseEntity deleteMember(@PathVariable("member-id") @Positive long memberId){
        memberService.deleteMember(memberId);

        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}

기존의 컨틀롤러 역할과 동일하게 작성하였습니다. 실질적인 서비스 로직 처리는 수행하지 않고 클라이언트와 비즈니스 로직을 이어주는 역할을 수행합니다.

  1. 엔드포인트로 매개변수를 통해 입력받음
  2. Service, Mapper의 메서드 호출
  3. ResponseEntity 객체로 감싸서 반환

핸들러 메서드마다 차이가 존재하므로 약간씩 다를 수 있으나 크게 위의 3단계를 충족 하도록 작성하였습니다.



테스트

@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
public class MemberControllerRestDocsTest {
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private Gson gson;

    @MockBean
    private MemberService memberService;
    @MockBean
    private MemberMapper mapper;

    @Test
    public void postMemberTest() throws Exception {
        //given
        MemberDto.Post post = new MemberDto.Post("김코딩","s4goodbye!","m",
                "코드스테이츠",5,1);
        String content = gson.toJson(post);
        MemberDto.response responseDto =
                new MemberDto.response(1L,"김코딩","s4goodbye!","m",
                        "코드스테이츠",5,1);
        given(mapper.memberPostToMember(Mockito.any(MemberDto.Post.class))).willReturn(new Member());
        given(memberService.createMember(Mockito.any(Member.class))).willReturn(new Member());
        given(mapper.memberToMemberResponse(Mockito.any(Member.class))).willReturn(responseDto);

        //when
        ResultActions actions =
                mockMvc.perform(
                        post("/v1/members")
                                .accept(MediaType.APPLICATION_JSON)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(content)
                );

        //then
        actions
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.data.name").value(post.getName()))
                .andExpect(jsonPath("$.data.sex").value(post.getSex()))
                .andExpect(jsonPath("$.data.company_name").value(post.getCompany_name()))
                .andDo(document(
                        "post-member",
                        preprocessRequest(prettyPrint()),
                        preprocessResponse(prettyPrint()),
                        requestFields(
                                List.of(
                                        fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),
                                        fieldWithPath("password").type(JsonFieldType.STRING).description("비밀번호"),
                                        fieldWithPath("sex").type(JsonFieldType.STRING).description("성별"),
                                        fieldWithPath("company_name").type(JsonFieldType.STRING).description("회사명"),
                                        fieldWithPath("company_type").type(JsonFieldType.NUMBER).description("업종"),
                                        fieldWithPath("company_location").type(JsonFieldType.NUMBER).description("지역")
                                )
                        ),
                        responseFields(
                                List.of(
                                        fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터"),
                                        fieldWithPath("data.memberId").type(JsonFieldType.NUMBER).description("회원 식별자"),
                                        fieldWithPath("data.name").type(JsonFieldType.STRING).description("이름"),
                                        fieldWithPath("data.password").type(JsonFieldType.STRING).description("비밀번호"),
                                        fieldWithPath("data.sex").type(JsonFieldType.STRING).description("성별"),
                                        fieldWithPath("data.company_name").type(JsonFieldType.STRING).description("회사명"),
                                        fieldWithPath("data.company_type").type(JsonFieldType.NUMBER).description("업종"),
                                        fieldWithPath("data.company_location").type(JsonFieldType.NUMBER).description("지역")
                                )
                        )
                ));
    }

    @Test
    public void patchMemberTest() throws Exception{
        //given
        long memberId = 1L;
        MemberDto.Patch patch = new MemberDto.Patch(memberId,"김코딩","m",
                "코드스테이츠",5,1);
        String content = gson.toJson(patch);

        MemberDto.response responseDto =
                new MemberDto.response(1L,"김코딩","s4goodbye!","m",
                        "코드스테이츠",5,1);
        given(mapper.memberPatchToMember(Mockito.any(MemberDto.Patch.class))).willReturn(new Member());
        given(memberService.updateMember(Mockito.any(Member.class))).willReturn(new Member());
        given(mapper.memberToMemberResponse(Mockito.any(Member.class))).willReturn(responseDto);

        //when
        ResultActions actions =
                mockMvc.perform(
                        patch("/v1/members/{member-id}",memberId)
                                .accept(MediaType.APPLICATION_JSON)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(content)
                );

        //then
        actions
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.memberId").value(patch.getMemberId()))
                .andExpect(jsonPath("$.data.name").value(patch.getName()))
                .andExpect(jsonPath("$.data.company_name").value(patch.getCompany_name()))
                .andDo(document(
                        "patch-member",
                        preprocessRequest(prettyPrint()),
                        preprocessResponse(prettyPrint()),
                        pathParameters(
                            parameterWithName("member-id").description("회원 식별자")
                        ),
                        requestFields(
                                List.of(
                                        fieldWithPath("memberId").type(JsonFieldType.NUMBER).description("회원 식별자").ignored(),
                                        fieldWithPath("name").type(JsonFieldType.STRING).description("이름").optional(),
                                        fieldWithPath("sex").type(JsonFieldType.STRING).description("성별").optional(),
                                        fieldWithPath("company_name").type(JsonFieldType.STRING).description("회사명").optional(),
                                        fieldWithPath("company_type").type(JsonFieldType.NUMBER).description("업종").optional(),
                                        fieldWithPath("company_location").type(JsonFieldType.NUMBER).description("지역").optional()
                                )
                        ),
                        responseFields(
                                List.of(
                                        fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터"),
                                        fieldWithPath("data.memberId").type(JsonFieldType.NUMBER).description("회원 식별자"),
                                        fieldWithPath("data.name").type(JsonFieldType.STRING).description("이름"),
                                        fieldWithPath("data.password").type(JsonFieldType.STRING).description("비밀번호"),
                                        fieldWithPath("data.sex").type(JsonFieldType.STRING).description("성별"),
                                        fieldWithPath("data.company_name").type(JsonFieldType.STRING).description("회사명"),
                                        fieldWithPath("data.company_type").type(JsonFieldType.NUMBER).description("업종"),
                                        fieldWithPath("data.company_location").type(JsonFieldType.NUMBER).description("지역")
                                )
                        )
                ));
    }
    @Test
    public void getMemberTest() throws Exception{
        //given
        long memberId = 1L;
        MemberDto.response responseDto =
                new MemberDto.response(1L,"김코딩","s4goodbye!","m",
                        "코드스테이츠",5,1);
        given(memberService.findMember(Mockito.anyLong())).willReturn(new Member());
        given(mapper.memberToMemberResponse(Mockito.any(Member.class))).willReturn(responseDto);

        //when
        ResultActions actions =
                mockMvc.perform(
                        get("/v1/members/{member-id}",memberId)
                                .accept(MediaType.APPLICATION_JSON)
                );

        //then
        actions
                .andExpect(status().isOk())
                .andDo(document(
                        "get-member",
                        preprocessRequest(prettyPrint()),
                        preprocessResponse(prettyPrint()),
                        pathParameters(
                                parameterWithName("member-id").description("회원 식별자")
                        ),
                        responseFields(
                                List.of(
                                        fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터"),
                                        fieldWithPath("data.memberId").type(JsonFieldType.NUMBER).description("회원 식별자"),
                                        fieldWithPath("data.name").type(JsonFieldType.STRING).description("이름"),
                                        fieldWithPath("data.password").type(JsonFieldType.STRING).description("비밀번호"),
                                        fieldWithPath("data.sex").type(JsonFieldType.STRING).description("성별"),
                                        fieldWithPath("data.company_name").type(JsonFieldType.STRING).description("회사명"),
                                        fieldWithPath("data.company_type").type(JsonFieldType.NUMBER).description("업종"),
                                        fieldWithPath("data.company_location").type(JsonFieldType.NUMBER).description("지역")
                                )
                        )
                ));
    }


    @Test
    public void getMembersTest() throws Exception{
        //given
        Member member1 = new Member(1L,"김코딩","s4goodbye!","m",
                "코드스테이츠",5,1);
        Member member2 = new Member(2L,"박해커","1q2w3e4r!","m",
                "유어클래스",4,2);
        Member member3 = new Member(3L,"최개발","spring123@","w",
                "스프링",3,6);

        Page<Member> pageMembers = new PageImpl<>(List.of(member1,member2,member3), PageRequest.of(1,10, Sort.by("memberId").descending()),3);
        List<MemberDto.response> responses = List.of(
                new MemberDto.response(1L,"김코딩","s4goodbye!","m",
                        "코드스테이츠",5,1),
                new MemberDto.response(2L,"박해커","1q2w3e4r!","m",
                        "유어클래스",4,2),
                new MemberDto.response(3L,"최개발","spring123@","w",
                        "스프링",3,6)
        );

        given(memberService.findMembers(Mockito.anyInt(),Mockito.anyInt())).willReturn(new PageImpl<>(List.of()));
        given(mapper.membersToMemberResponses(Mockito.anyList())).willReturn(responses);

        String page = "1";
        String size = "10";
        MultiValueMap<String,String> queryParams = new LinkedMultiValueMap<>();
        queryParams.add("page",page);
        queryParams.add("size",size);


        //when
        ResultActions actions =
                mockMvc.perform(
                        get("/v1/members")
                                .accept(MediaType.APPLICATION_JSON)
                                .params(queryParams)
                );

        //then
        actions
                .andExpect(status().isOk())
                .andDo(document(
                        "get-members",
                        preprocessRequest(prettyPrint()),
                        preprocessResponse(prettyPrint()),
                        requestParameters(
                                parameterWithName("page").description("페이지"),
                                parameterWithName("size").description("페이지당 회원 수")
                        ),
                        responseFields(
                                List.of(
                                        fieldWithPath("data").type(JsonFieldType.ARRAY).description("결과 데이터"),
                                        fieldWithPath("data[].memberId").type(JsonFieldType.NUMBER).description("회원 식별자"),
                                        fieldWithPath("data[].name").type(JsonFieldType.STRING).description("이름"),
                                        fieldWithPath("data[].password").type(JsonFieldType.STRING).description("비밀번호"),
                                        fieldWithPath("data[].sex").type(JsonFieldType.STRING).description("성별"),
                                        fieldWithPath("data[].company_name").type(JsonFieldType.STRING).description("회사명"),
                                        fieldWithPath("data[].company_type").type(JsonFieldType.NUMBER).description("업종"),
                                        fieldWithPath("data[].company_location").type(JsonFieldType.NUMBER).description("지역"),

                                        fieldWithPath("pageInfo.page").type(JsonFieldType.NUMBER).description("현재 페이지"),
                                        fieldWithPath("pageInfo.size").type(JsonFieldType.NUMBER).description("페이지 크기"),
                                        fieldWithPath("pageInfo.totalElements").type(JsonFieldType.NUMBER).description("전체 회원 수"),
                                        fieldWithPath("pageInfo.totalPages").type(JsonFieldType.NUMBER).description("전체 페이지 수")
                                )
                        )
                ));
    }

    @Test
    public void deleteMemberTest() throws Exception {
        //given
        long memberId = 1L;
        doNothing().when(memberService).deleteMember(Mockito.anyLong());

        //when
        ResultActions actions =
                mockMvc.perform(
                        delete("/v1/members/{member-id}",memberId)
                                .accept(MediaType.APPLICATION_JSON)
                );

        //then
        actions
                .andExpect(status().isNoContent())
                .andDo(document(
                        "delete-member",
                        preprocessRequest(prettyPrint()),
                        preprocessResponse(prettyPrint()),
                        pathParameters(
                                parameterWithName("member-id").description("회원 식별자")
                        )
                ));
    }
}

Rest API Docs는 큰 특징이 2가지 존재합니다.

스프링 컨테이너의 빈을 사용하지 않고 Mock객체를 만들어 Mock컨테이너에서 가짜 빈 객체를 주입받는 테스트 케이스를 그대로 활용할 수 있어, 실제 구현 전 설계 단계에서 작성 할 수 있습니다.

또, 기존의 테스트케이스 작성과 동일하고 마지막에 andDo()메서드를 추가함으로 써 andExpect()메서드 등의 테스트를 위한 메서드가 모두 통과하면 API 문서를 작성합니다.

에너테이션메서드링크의 게시물에 설명이 적혀 있습니다.

given - when - then 3단계를 나누어 수행 하라는 말씀이 머릿속에 생생하게 남아 이번에도 나누어서 진행하였습니다.

여러 목록을 한번에 지정한 갯수만큼 가져오는 page는 기억이 가물 가물 했지만 이번에 다시 작성해 봄으로써 중요한걸 잊을 뻔 했다 싶었습니다.



adoc

= 전국 사업자 연합 애플리케이션
:sectnums:
:toc: left
:tocleves: 4
:toc-title: Table of Contents
:source-highlighter: prettify

Yoo Tae Hyong <1995musso@gmail.com>

v1.0.0, 2022,08.16

***
== MemberController
=== 회원 등록
.curl-request
include::{snippets}/post-member/curl-request.adoc[]

.http-request
include::{snippets}/post-member/http-request.adoc[]

.request-fields
include::{snippets}/post-member/request-fields.adoc[]

.http-response
include::{snippets}/post-member/http-response.adoc[]

.response-fields
include::{snippets}/post-member/response-fields.adoc[]

=== 회원 정보 수정
.curl-request
include::{snippets}/patch-member/curl-request.adoc[]

.http-request
include::{snippets}/patch-member/http-request.adoc[]

.request-fields
include::{snippets}/patch-member/request-fields.adoc[]

.path-parameters
include::{snippets}/patch-member/path-parameters.adoc[]

.http-response
include::{snippets}/patch-member/http-response.adoc[]

.response-fields
include::{snippets}/patch-member/response-fields.adoc[]

=== 회원 정보 검색
.curl-request
include::{snippets}/get-member/curl-request.adoc[]

.http-request
include::{snippets}/get-member/http-request.adoc[]

.path-parameters
include::{snippets}/get-member/path-parameters.adoc[]

.http-response
include::{snippets}/get-member/http-response.adoc[]

.response-fields
include::{snippets}/get-member/response-fields.adoc[]

=== 회원 정보 목록
.curl-request
include::{snippets}/get-members/curl-request.adoc[]

.http-request
include::{snippets}/get-members/http-request.adoc[]

.request-parameters
include::{snippets}/get-members/request-parameters.adoc[]

.http-response
include::{snippets}/get-members/http-response.adoc[]

.response-fields
include::{snippets}/get-members/response-fields.adoc[]

=== 회원 정보 삭제
.curl-request
include::{snippets}/delete-member/curl-request.adoc[]

.http-request
include::{snippets}/delete-member/http-request.adoc[]

.path-parameters
include::{snippets}/delete-member/path-parameters.adoc[]

.http-response
include::{snippets}/delete-member/http-response.adoc[]

한번에 정리가 가능하도록 index.adoc에서 다른 .adoc자료들을 불러들이도록 하였습니다. 핸들러 메서드 별로 나뉘어 어떤 데이터들이 주고 받는지 JSONTable로 확인 할 수 있습니다.




작성한 API 문서




후기

이전에 학습해 두었 던 Rest API DOCS가 유용하게 사용되어 어렵지 않게 문서를 작성할 수 있었습니다. 다만 page같은 클래스는 잊어버릴 뻔 하여 가끔씩이라도 다시 보는게 중요하다 싶었습니다.




GitHub

https://github.com/ds02168/solo-project-Entrepreneur

profile
오늘도 내일도 화이팅!

0개의 댓글