본문 바로가기
web/springboot

[springboot] REST API 만들기 (REST의 개념/ controller unit test / @AutoConfigureMybatis )

by fien 2020. 11. 20.

 

REST Representational State Transfer

  • REST란 웹과 같은 분산 하이퍼미디어 시스템을 위한 소프트웨어 아키텍처의 한 형식이다.
  • 자원을 나타내는 이름으로 구분하고 해당 자원의 상태(Representation of Resource)를 주고 받는 것을 의미한다.
  • HTTP URI + HTTP Method

    HTTP URI를 통해 제어할 자원을 명시하고 HTTP Method(GET, POST, PUT, DELETE ~CRUD Operation)를 통해 해당 자원을 제어하는 명령을 내리는 방식의 아키텍처

  • 자원의 상태(실제 데이터)는 request body에 json이나 xml 형식으로 전달하는 것이 일반적이다.

 

특징

  • 서버와 클라이언트의 역할을 명확하게 분리한다

    Server - 자원을 가진 쪽, Client - 자원을 요청하는 쪽

  • 무상태 Stateless

    서버측에서 클라이언트의 context를 유지할 필요 없다. 각각의 요청은 완전히 별개의 것으로 인식.

  • 인터페이스의 일관성 Uniform Interface

    HTTP 표준 프로토콜을 따르는 모든 플랫폼에서 사용 가능

  • 캐시 가능 Cacheable
  • 계층형 구조 Layered System

    다중 계층으로 구성될 수 있다.

 

장점

  • 웹과 http 프로토콜을 사용하므로 범용적이고 별도의 인프라 구축이 필요없다.
  • 요청이 의도하는 바를 명확하게 나타낸다. Self-Descriptive

단점

  • REST는 설계 가이드일 뿐 별도의 표준이 존재하지 않는다.
  • HTTP Method가 제한적이다

 

Rest API

REST 기반으로 서비스 API를 구현 한 것

**API Application Programming Interface : 데이터와 기능의 집합을 제공하여 프로그램들이 서로 상호작용하는 것을 도와주는 매개체. 타인이 만든 프로그램의 동작이나 내부를 몰라도 활용할 수 있게 해주는 중간 매개체

Rest API 설계

  • URI는 자원(resource)을 표현해야 한다.
    • 동사 대신 명사, 대문자 대신 소문자 사용
    • 복수명사 사용
  • 자원에 대한 행위는 HTTP Method(GET, POST, PUT, DELETE 등)로 표현
    • URI에 행위에 대한 동사 표현이 들어가면 안된다.
  • 슬래시(/)는 계층 관계를 나타내는데 사용한다.
  • URI 끝에 슬래시(/)를 포함하지 않는다.
  • 밑줄(_)은 사용하지 않는다.
  • 하이픈(-)은 가독성을 높이는데 사용한다.
  • 파일 확장자는URI에 포함하지 않는다. 대신 Accept header로 표시

RestController

스프링에서 REST API를 만들기 위해 사용하는 컨트롤러 어노테이션.

기존 Controller는 주로 ModelAndView를 통해 view를 응답한다.

반면, RestController는 데이터를 응답한다.

@RestController

= @Controller + @ResponseBody

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@RestController
@RequiredArgsConstructor
@RequestMapping("/member")
public class MemberController {
    //private final MemberDao memberDao;
    private final MemberService memberService;
    private final MemberMapper memberMapper;
 
    @GetMapping()
    public List<Member> getMemberList(){
        return memberMapper.findAll();
    }
 
    @GetMapping("/{memberId}")
    public Member getMember(@PathVariable Long memberId){
        return memberMapper.findOneById(memberId);
    }
 
    @PostMapping()
    public Long insertMember(@RequestBody Member member){
        memberMapper.save(member);
        return member.getMemberId();
    }
 
    @PutMapping("/{memberId}")
    public void updateMember(@PathVariable Long memberId, @RequestBody Member member){
        member.setMemberId(memberId);
        memberMapper.update(member);
    }
 
    @DeleteMapping("/{memberId}")
    public void deleteMember(@PathVariable Long memberId){
        memberService.delete(memberId);
    }
 
}
 
cs

 

기존 URI 와 RESTful URI 비교

기능 method restful URL 기존 URI
회원 등록 POST /member /member/insertMember
회원 목록 GET /member /member/getMemberList
회원 상세 GET /member/{memberId} /member/getMemberDetail
회원 수정 PUT /member/{memberId} /member/updateMember
회원 삭제 DELETE /member/{memberId} /member/deleteMember

기존 URI는 규격이 명확하지 않아서 표현이 다양하기 때문에 사용하거나 협업하기에 불편하다

 


Springboot - Controller Unit test

@SpringBootTest

슬라이싱하지 않고 전체 application context를 구성하기 때문에 전체 통합 테스트를 하기에 적합하다.

 

@WebMvcTest

  • Controller(Web) layer를 테스트 할 때 사용-Sliced test
  • @Controller, @ControllerAdvice, @JsonComponent와 Filter, WebMvcConfiguer, HandlerMethodArgumentResolver만 로드

    이 외에 필요한 Bean을 직접 로드해줘야 한다. → @Import(*Service.class)

  • @MockBean

    MockBean 어노테이션을 붙이면 가짜 객체를 만들어서 주입해준다. 해당 객체의 내부의 동작들은 실행되지 않는다.

  • Mybatis를 사용한다면 @MybatisTest 대신 @AutoConfigureMybatis를 선언해야한다.

    이렇게하면 mapper가 빈으로 등록되고 알아서 주입된다

 

음 근데 이정도로 설정 할거면 그냥 springboottest+autoConfigureMockMvc 로 돌리는게 나을것 같다는 생각이든다. controller 에 서비스 처리가 정상적으로 이뤄져있는지 확인하니까 단위테스트가 아닌 느낌.. 그럼 컨트롤러 단위 테스트는 무슨 용도로 사용하는지 좀 더 찾아봐야 할듯. 우선은 @WebMvcTest로 테스트 코드를 작성했다.

 

 

MockMvc

스프링 mvc를 테스트 하기 위해 사용하는 것으로 ajax나 client(browser)의 요청을 컨트롤러가 받아 처리하는 것과 같은 테스트를 진행할 수 있다. 실제 객체와 비슷하지만 테스트에 필요한 기능만 가지는 가짜 객체를 만들어서 애플리케이션 서버에 배포하지 않고도 스프링 MVC 동작을 재현할 수 있다. spring-boot-test에 기본 포함

  • perform() : 요청을 전송. 결과를 검증할 수 있는 ResultActions 객체를 리턴 받는다
  • get("URI") : HTTP 메소드 결정, 파라미터는 URI
  • params() : MultiValueMap<String,String>으로 파라미터 값을 전달한다 ~ @RequestParam
  • content() : body안에 데이터를 담아 전달한다.
    • objectMapper를 통해 객체를 바이트로 변환해서 전달한다.

      이때 writeValueAsBytes 메서드로는 값이 정상적으로 전달 되는데

      writeValueAsString 메서드로 하면 제대로 전달되지 않는다. 왜인지는.. 알게되면 알려주세요

  • contentType() : header의 content-type 설정
  • andDo(print()) : 요청과 응답의 전체 메세지를 확인할 수 있다.
  • andExpect() : 응답을 검증하는 역할
    • status() 상태코드 검증
      • isOk() : 200
      • isNotFound() : 404
      • isMethodNotAllowed() : 405
    • content() 응답 검증
      • string() 응답 메세지 비교
    • jsonPath() json 객체의 값을 비교할 수 있습니다

아래 코드를 보시고 눈치껏 쓰거나 필요한 것은 그때그때 검색해서 추가하면 될 듯 합니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@RunWith(SpringRunner.class)
@WebMvcTest({MemberController.class})
@AutoConfigureMybatis
@Import(MemberService.class)
public class MemberControllerTest {
    @Autowired
    MockMvc mockMvc;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private MemberMapper memberMapper;
        
    //이렇게 선언하면 controller 내부의 services는 모키토로 만들어진 빈을 주입받음 
    //@MockBean
    //private MemberService memberService;
 
    @Test
    public void joinMember() throws Exception {
        Member member = new Member("fine","ty","1111");
        //when
        ResultActions actions =
                         mockMvc.perform(post("/member")
                                .contentType("application/json")
                                .content(objectMapper.writeValueAsBytes(member)))
                        .andDo(print());
        Member m = memberMapper.findOneByUserId("fine");
        //then
        actions.andExpect(status().isOk())
                .andExpect(content().string(m.getMemberId().toString()));
    }
 
    @Test
    public void getMemberList_test() throws Exception {
        //when
        ResultActions actions = mockMvc.perform(get("/member"))
                .andDo(print());
        //then
        actions.andExpect(status().isOk())
                .andExpect(jsonPath("$[*]",hasSize(2)))
                .andExpect(jsonPath("$[0].userId",is("zero")))
                .andExpect(jsonPath("$[0].name",is("taeyeon")))
                .andExpect(jsonPath("$[1].userId",is("spark")));
    }
}
cs



다음 포스팅은 spring에서 http 통신으로 외부의 api를 호출하는 내용으로 돌아오겠습니다.

댓글