[初心-Spring Boot] rest api 서버 만들기

0

초심-spring boot

목록 보기
16/16

1. REST API

1.1 API란?

Application Programming Interface
응용 프로그램에서 사용할 수 있도록, 운영 체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스를 뜻한다. 주로 파일 제어, 창 제어, 화상 처리, 문자 제어 등을 위한 인터페이스를 제공한다.

1.2 REST API란?

REpresentational State Transfer
어떤 자원에 대해 CRUD(Create, Read, Update, Delete) 연산을 수행하기 위해 URI(Resource)로 요청을 보내는 것으로, Get, Post 등의 방식(Method)을 사용하여 요청을 보내며, 요청을 위한 자원은 특정한 형태(Representation of Resource)으로 표현된다

1.3 REST API 구성요소

  • Resource
    서버는 Unique한 ID를 가지는 Resource를 가지고 있으며, 클라이언트는 이러한 Resource에 요청을 보낸다. 이러한 Resource는 URI에 해당한다.

  • Method
    서버에 요청을 보내기 위한 방식으로 GET, POST, PUT, PATCH, DELETE가 있다. CRUD 연산 중에서 처리를 위한 연산에 맞는 Method를 사용하여 서버에 요청을 보내야 한다.

  • Representation of Resource
    클라이언트와 서버가 데이터를 주고받는 형태로 json, xml, text, rss 등이 있다. 최근에는 Key, Value를 활용하는 json을 주로 사용한다.

1.4 REST API 특징

  • 일관성
    리소스에 대하여 통일되고 한정적으로 수행. 클라리언트가 플렛폼에 무관하고, 특정 언어나 기술에 종속받지 않는다.

  • 무상태성
    각각의 요청을 별개의 것으로 인식하고 처리, 이전 요청이 다음 요청에 연관되어서는 안된다. 그러므로 처리방식에 일관성이 있고, 서버의 부담이 적다.

  • 캐시 가능
    HTTP웹표준을 그대로 사용하기 떄문에 웹의 기존 인프라를 그대로 활용할 수 있다. 그러므로 REST API에서도캐싱 기능을 적용할 수 있다.

  • 서버 - 클라이언트 구조
    자원을 가지고 있는 쪽이 서버, 자원을 요청하는 쪽이 클라이언트에 해당한다. 서버는 API를 제공하며 클라이언트는 사용자 인증, 세션 관리 등 역활을 확실히 구분하여 서로 간의 의존성을 줄인다.

  • 자체 표현
    요청 메세지만 보고도 이를 쉽게 이해할 수 있는 자체 표현 구조로 되어있다.

  • 계층 구조
    Rest API 서버는 다중 계층으로 구성될 수 있으며, 보안, 로드밸런싱, 암호화 등을 위한 계층을 추가하여 구조를 변경할 수 있다. 또한 Proxy, Gateway 같은 네트워크 기반의 중간 매체를 사용할 수 있게 해준다.

1.5 REST API 규칙

  • URI는 명사를 사용한다.
  • 슬래시(/)로 계층 관계를 표현한다.
  • URI의 마지막에는 슬래시(/)를 붙이지 않는다.
  • URI는 소문자로만 구성한다.
  • 가독성이 떨어지는 경우 하이픈(-)을 사용한다.
예제 디렉토리

2. 예제 CORE 작성

FILE: Puppy.java

package com.rptp.rptpSpringBoot.core.puppy.domain;

import com.rptp.rptpSpringBoot.core.vet.domain.Vet;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Puppy {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long puppyId;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Integer age;

    @Column(nullable = false)
    private String breed;

    @ManyToOne
    @JoinColumn(name = "vet_id")
    private Vet vet;

    @Builder
    public Puppy(String name, Integer age, String breed) {
        this.name = name;
        this.age = age;
        this.breed = breed;
    }
}

FILE: PuppyRepository.java

package com.rptp.rptpSpringBoot.core.puppy.domain;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PuppyRepository extends JpaRepository<Puppy,Long> {
}

FILE: PuppyRequest.java

import lombok.*;

import javax.validation.constraints.NotBlank;

@Getter
@Setter
@Builder
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class PuppyRequest {
    @NotBlank
    private String name;

    @NotBlank
    private Integer age;

    @NotBlank
    private String breed;
}

FILE: PuppyResponse.java

package com.rptp.rptpSpringBoot.core.puppy.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.rptp.rptpSpringBoot.core.puppy.domain.Puppy;
import com.rptp.rptpSpringBoot.core.vet.domain.Vet;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PUBLIC)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class PuppyResponse {

    private Long puppyId;

    private String name;

    private Integer age;

    private String breed;

    private Long vetId;

    private String vetName;

    public static PuppyResponse of(Puppy puppy) {
        Vet vet = Optional.ofNullable(puppy.getVet()).orElseGet(Vet::new);

        return new PuppyResponse(
                puppy.getPuppyId(),
                puppy.getName(),
                puppy.getAge(),
                puppy.getBreed(),
                vet.getVetId(),
                vet.getName()
        );
    }

    public static List<PuppyResponse> listOf(List<Puppy> puppies) {
        return puppies.stream()
                .map(PuppyResponse::of)
                .collect(Collectors.toList());
    }
}

FILE: PuppyService.java

package com.rptp.rptpSpringBoot.core.puppy.service;

import com.rptp.rptpSpringBoot.common.exceptions.ResourceNotFoundException;
import com.rptp.rptpSpringBoot.core.puppy.domain.Puppy;
import com.rptp.rptpSpringBoot.core.puppy.domain.PuppyRepository;
import com.rptp.rptpSpringBoot.core.puppy.dto.PuppyRequest;
import com.rptp.rptpSpringBoot.core.puppy.dto.PuppyResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
public class PuppyService {
    private final PuppyRepository puppyRepository;

    @Transactional
    public Long savePuppy(PuppyRequest req) {
        return puppyRepository.save(
          Puppy.builder()
                  .name(req.getName())
                  .age(req.getAge())
                  .breed(req.getBreed())
                  .build()
        ).getPuppyId();
    }

    @Transactional(readOnly = true)
    public List<PuppyResponse> findAll() {
        return PuppyResponse.listOf(puppyRepository.findAll());
    }

    @Transactional(readOnly = true)
    public PuppyResponse findPuppy(Long puppyId) {
        return PuppyResponse.of(findById(puppyId));
    }

    private Puppy findById(Long puppyId) {
        return puppyRepository.findById(puppyId)
                .orElseThrow(() ->
                        new ResourceNotFoundException("Puppy","id",puppyId));
    }
 }

FILE: Vet.java

package com.rptp.rptpSpringBoot.core.vet.domain;

import com.rptp.rptpSpringBoot.core.puppy.domain.Puppy;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Vet {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long vetId;

    @Column(nullable = false)
    private String name;

    private String email;

    @OneToMany(mappedBy = "vet", cascade = CascadeType.MERGE)
    private List<Puppy> puppyList = new ArrayList<>();

    @Builder
    public Vet(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

FILE: VetRepository.java

package com.rptp.rptpSpringBoot.core.vet.domain;

import org.springframework.data.jpa.repository.JpaRepository;

public interface VetRepository extends JpaRepository<Vet, Long> {
}

FILE: VetRequest.java

package com.rptp.rptpSpringBoot.core.vet.dto;

import lombok.*;

import javax.validation.constraints.NotBlank;

@Getter
@Setter
@Builder
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class VetRequest {

    @NotBlank
    private String name;

    @NotBlank
    private String email;
}

FILE: VetResponse.java

package com.rptp.rptpSpringBoot.core.vet.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.rptp.rptpSpringBoot.core.puppy.dto.PuppyResponse;
import com.rptp.rptpSpringBoot.core.vet.domain.Vet;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.stream.Collectors;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PUBLIC)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class VetResponse {

    private Long vetId;

    private String name;

    private String email;

    private List<PuppyResponse> puppyResponses;

    public static VetResponse of(Vet vet){

        return new VetResponse(
                vet.getVetId(),
                vet.getName(),
                vet.getEmail(),
                PuppyResponse.listOf(vet.getPuppyList())
        );
    }

    public static List<VetResponse> listOf(List<Vet> vets) {
        return vets.stream()
                .map(VetResponse::of)
                .collect(Collectors.toList());
    }
}

FILE: VetService.java

package com.rptp.rptpSpringBoot.core.vet.service;

import com.rptp.rptpSpringBoot.common.exceptions.ResourceNotFoundException;
import com.rptp.rptpSpringBoot.core.vet.domain.Vet;
import com.rptp.rptpSpringBoot.core.vet.domain.VetRepository;
import com.rptp.rptpSpringBoot.core.vet.dto.VetRequest;
import com.rptp.rptpSpringBoot.core.vet.dto.VetResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
public class VetService {
    private final VetRepository vetRepository;

    @Transactional
    public Long saveVet(VetRequest req) {
        return vetRepository.save(
                Vet.builder()
                        .name(req.getName())
                        .email(req.getEmail())
                        .build()
        ).getVetId();
    }

    @Transactional(readOnly = true)
    public List<VetResponse> findAll() {
        return VetResponse.listOf(vetRepository.findAll());
    }

    @Transactional(readOnly = true)
    public VetResponse findVet(Long vetId) {
        return VetResponse.of(findById(vetId));
    }

    private Vet findById(Long vetId) {
        return vetRepository.findById(vetId)
                .orElseThrow(() ->
                        new ResourceNotFoundException("Vet","id",vetId));
    }
}
서버 실행 후sql 입력
INSERT INTO `board`.`vet` (`vet_id`, `email`, `name`) VALUES ('1', 'aaa@aaaa.com', '홍길동');
INSERT INTO `board`.`vet` (`vet_id`, `email`, `name`) VALUES ('2', 'bbb@bbbb.bbb', '이익준');
INSERT INTO `board`.`vet` (`vet_id`, `email`, `name`) VALUES ('3', 'ccc.dddd.com', '채송화');
INSERT INTO `board`.`vet` (`vet_id`, `email`, `name`) VALUES ('4', 'kkk.kkkk.kr', '장겨울');

INSERT INTO `board`.`puppy` (`puppy_id`, `age`, `breed`, `name`, `vet_id`) VALUES ('1', '1', '말티즈', '두부', '3');
INSERT INTO `board`.`puppy` (`puppy_id`, `age`, `breed`, `name`, `vet_id`) VALUES ('2', '2', '비숑', '구름', '2');
INSERT INTO `board`.`puppy` (`puppy_id`, `age`, `breed`, `name`, `vet_id`) VALUES ('3', '3', '푸들', '푸우', '3');
INSERT INTO `board`.`puppy` (`puppy_id`, `age`, `breed`, `name`, `vet_id`) VALUES ('4', '1', '치와와', '앵두', '1');
INSERT INTO `board`.`puppy` (`puppy_id`, `age`, `breed`, `name`) VALUES ('5', '10', '말티즈', '만두');

3. 예제 API 작성

FILE: PuppyController.java

import com.rptp.rptpSpringBoot.core.puppy.dto.PuppyResponse;
import com.rptp.rptpSpringBoot.core.puppy.service.PuppyService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api/puppy")
@RequiredArgsConstructor
public class PuppyController {

    private final PuppyService puppyService;

    @GetMapping("")
    public List<PuppyResponse> getPuppies() {
        return puppyService.findAll();
    }

    @GetMapping("/{id}")
    public PuppyResponse getPuppy(@PathVariable("id") Long id) {
        return puppyService.findPuppy(id);
    }
}

실행 결과

  • URI: localhost:8080/api/puppy
  • URI: localhost:8080/api/puppy/1
  • URI: localhost:8080/api/puppy/5

    Response에서 @JsonInclude(JsonInclude.Include.NON_NULL) Annotation을 넣어서 null값은 출력이 제외된 것을 볼 수 있다

4. ResponseEntity 이용

4-1. ResponseEntity란?

사용자의 HttpRequest에 대한 응답 데이터를 포함하는 클래스이다. 따라서 HttpStatus, HttpHeaders, HttpBody를 포함한다.

4-2. VetController 예제 적용

FILE: VetController.java

package com.rptp.rptpSpringBoot.controller.api;

import com.rptp.rptpSpringBoot.core.vet.dto.VetResponse;
import com.rptp.rptpSpringBoot.core.vet.service.VetService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api/vet")
@RequiredArgsConstructor
public class VetController {

    private final VetService vetService;

    @GetMapping("")
    public ResponseEntity<List<VetResponse>> getVets() {
        return ResponseEntity.ok(vetService.findAll());
    }

    @GetMapping("/{id}")
    public ResponseEntity<VetResponse> getVet(@PathVariable("id") Long id) {
        return ResponseEntity.ok(vetService.findVet(id));
    }
}
  • URI: localhost:8080/api/vet
  • URI: localhost:8080/api/vet/3

4-2. PuppyController 예제 적용

FILE: PuppyController.java

package com.rptp.rptpSpringBoot.controller.api;

import com.rptp.rptpSpringBoot.core.puppy.dto.PuppyResponse;
import com.rptp.rptpSpringBoot.core.puppy.service.PuppyService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api/puppy")
@RequiredArgsConstructor
public class PuppyController {

    private final PuppyService puppyService;

    @GetMapping("")
    public ResponseEntity<List<PuppyResponse>> getPuppies() {
        return ResponseEntity.ok(puppyService.findAll());
    }

    @GetMapping("/{id}")
    public ResponseEntity<PuppyResponse> getPuppy(@PathVariable("id") Long id) {
        return ResponseEntity.ok(puppyService.findPuppy(id));
    }
}

5. @Controller와 @RestController 차이

@Controller - View

전통적인 Spring MVC 컨트롤러인 @Controller은 주로 View를 반환하기 위해 사용한다.

Controller 구동 과정

  1. Client는 URI 형식으로 웹 서비스에 요청을 보낸다.
  2. Mapping되는 Handler와 그 Type을 찾는 DispatcherServlet이 요청을 인터셉트한다.
  3. Controller가 요청을 처리한 후에 응답을 DispatcherServlet으로 반환하고, DispatcherServlet은 View를 사용자에게 반환한다.

@Controller - Data

Spring MVC컨트롤러에서도 Data를 반환이 가능하다. Controller에서 데이터를 반환하기 위해 @ResponseBody를 활용해주어야 한다. 이를 통해 @Controller도 Json형태로 데이터를 반환할 수 있다.

1. Client는 URI 형식으로 웹 서비스에 요청을 보낸다.
2. Mapping되는 Handler와 그 Type을 찾는 DispatcherServlet이 요청을 인터셉트한다.
3. @ResponseBody를 사용하여 Client에게 Json 형태로 데이터를 반환한다.

@RestController

@RestController는 Spring MVC Controlle에 @ResponseBody가 추가된 것이다.RestController의 주용도는 Json 형태로 객체 데이터를 반환하는 것이다

@RestController가 Data를 반환하기 위해서는 viewResolver 대신에 HttpMessageConverter가 동작한다.
HttpMessageConverter에는 여러 Converter가 등록되어 있고, 반환해야 하는 데이터에 따라 사용되는 Converter가 달라진다.
단순 문자열인 경우에는 StringHttpMessageConverter가 사용되고, 객체인 경우에는 MappingJackson2HttpMessageConverter가 사용되며, 데이터 종류에 따라 서로 다른 MessageConverter가 작동하게 된다.
Spring은 클라이언트의 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보 둘을 조합해 적합한 HttpMessageConverter를 선택하여 이를 처리한다.

1. Client는 URI 형식으로 웹 서비스에 요청을 보낸다.
2. Mapping되는 Handler와 그 Type을 찾는 DispatcherServlet이 요청을 인터셉트한다.
3. RestController는 해당 요청을 처리하고 데이터를 반환한다.

참조

https://ko.wikipedia.org/wiki/API
https://mangkyu.tistory.com/46
https://devlog-wjdrbs96.tistory.com/182
https://mangkyu.tistory.com/49

0개의 댓글