springboot

[SpringBoot] Spring Boot로 REST API 만들기

inhooo00 2024. 9. 28. 01:29

REST API 란?

  • REST : Representational State Transfer 의 약자로 '네트워크에서 통신을 구성할 때 이런 구조로 설계하라는 지침' 정도로 볼 수 있다.

  • API : Application Programming Interface 의 약자로 응용 프로그램 프로그래밍 인터페이스를 뜻한다. 프로그램을 작성하기 위한 일련의 부프로그램, 프로토콜 등을 정의하여 상호 작용을 하기위한 인터페이스 사양을 말한다.
  • REST API : API는 주로 프로그램 내부 단에서 이루어진다. 하지만 보다 다양한 분야에 쓰일수 있도록 '네트워크'와 '웹'에 맞춰진 API 통신 아키텍쳐가 등장했한것이 REST API이다. 주로 웹 API 쪽에서 사용된다. 웹 API = REST API 라고 봐도 볼 수있다.
    현실적으로 네트워크의 99.99%는 인터넷이라부르는 HTTP 기반 네트워크 이므로 REST API라고 하면 HTTP에 쓰이는걸 의미하는 경우가 많다. 그냥 API 라고 말하면 REST API를 의미하는 경우도 많아졌다.



REST 구성

  • 자원(Resource) : HTTP URI (URI : Uniform Resource Identifier : 자원을 식별하기 위한 문자열의 구성) ( ex : https://example.com/user/ABC ' / ' 는 계층관계를 의미)
  • 행위(Verb) : HTTP Method (GET, POST, PUT, DELETE 등)
    • GET : 자원 조회
    • POST : 요청 데이터 처리, 주로 등록에 사용
    • PUT : 자원 전체 변경
    • PATCH : 자원 부분 변경
    • DELETE : 자원 삭제
  • 표현(Representations) : HTTP Message, JSON, XML, RSS 등



REST의 특징

  • Client-Server(클라이언트-서버 구조) : 클라이언트와 서버로 분리되어야하며 서로 의존성이 없어야 한다.
    = REST Server 는 API를 제공하고 비지니스 로직 처리 및 저장을 책임짐. Client는 사용자 인증이나 context(세션, 로그인 정보)등을 관리하고 책임짐
  • Stateless(무상태성) : 상태 정보를 따로 저장하지 않으며, 이용자가 누구인지 혹은 어디서 접근하는지와 관계 없이 결과가 무조건 동일해야한다. 따라서 REST API는 필연적으로 오픈될 수 밖에 없다.
    = Client의 context를 Server에 저장하지 않음, Server는 모든 요청을 완전히 별개의 요청으로 처리함
  • Cache(캐시 처리 기능) : HTTP를 비롯한 네트워크 프로토콜에서 제공하는 캐싱 기능을 적용할 수 있어야 한다.
    = 대량의 요청을 효율적으로 처리 가능
  • Uniform Interface(인터페이스 일관성) : 데이터가 표준 형식으로 전송될 수 있도록 구성 요소 간 통합 인터페이스를 사용한다. REST API 태반이 HTTP를 사용하기 때문에 HTTP 표준인 URL과 응답코드, Request-Response Method 등을 사용한다.
    = 특정 언어나 기술에 종속되지 않음
  • Layered System(계층 구조) : API는 REST 조건을 만족하면 필연적으로 오픈될 수 밖에 없기 때문에, 요청된 정보를 검색하는데 있어 계층 구조로 분리되어야 한다.
  • Self-descriptiveness(자체 표현) : API를 통해 전송되는 내용은 별도 문서 없이 쉽게 이해할 수 있도록 자체 표현 구조를 지녀야 한다. 마찬가지로 웹 표준인 JSON과 XML이 주로 사용된다.



RESTful 이란?

  • REST API를 제공하는 웹 서비스
    • (Web Service : 네트워크 상에서 서로 다른 종류의 컴퓨터들 간에 상호작용하기 위한 소프트웨어 시스템 )
  • 즉, REST 원리를 따르는 시스템은 RESTful이란 용어로 지칭



RESTful은 왜 쓰는 걸까?

  • 이해하기 쉽고 사용하기 쉬운 REST API를 만드는 것!
  • RESTful한 API를 구현하는 근본적인 목적은 일관적인 컨벤션을 통한 API의 이해도 및 호환성을 높이는 것이다.



그러면 어떤 것이 RESTful하지 못할까?

  • CRUD 기능을 모두 POST로만 처리하는 API
    • CRUD는 대부분의 컴퓨터 소프트웨어가 가지는 기본적인 데이터 처리 기능인 Create(생성), Read(읽기), Update(갱신), Delete(삭제)를 묶어서 일컫는 말이다.
      • C : Create, 생성
      • R : Read, 읽기
      • U : Update, 수정 또는 갱신
      • D : Delete, 삭제
  • route에 resource, id 외의 정보가 들어가는 경우(/students/updateName)



Spring boot란?

지금까지 REST API가 무엇인지 알아보았다. 이번에는 REST API를 구현하기 위해서 사용할 Spring Boot에 대해서 알아보자.

Spring Boots는 실행만 하면 Spring 기반의 상용화가 가능한 애플리케이션을 쉽게 만들기 위해 단독 실행을 가능하게 해주는 Spring 프로젝트이다.
"관습 위의 설정"이라는 철학에 따라 복잡한 설정 없이도 애플리케이션을 빠르게 실행할 수 있게 설정되었다.
[Spring Boot의 차별화된 장점들]
1. 내장 서버 : Tomcat, Jetty, Undertow와 같은 웹 서버를 내장하고 있어 별도의 웹 서버 설치 및 설정 없이 애플리케이션을 실행할 수 있다.
2. 자동 구성(Auto Configuration) : Spring Boot는 프로젝트의 의존성을 기반으로 자동으로 구성을 해준다. 예를 들어, spring-boot-starter-web 의존성이 포함되면 웹 애플리케이션으로 구성해준다.
3. 스타터 의존성(Starter Dependencies) : 기능별로 묶여있는 의존성 패키지로 개발자는 필요한 기능의 스타터만 추가하면 관련된 모든 의존성이 자동으로 포함된다.
4. 액추에이터(Actuator): 애플리케이션의 운영 정보를 제공하는 모듈로, 메트릭, 상태 체크, 환경 정보 등 다양한 엔드포인트를 제공한다.



Spring boot 패키지 구조

Controller

  • Client와 Service의 중간자 역할!
  • Client에서 보낸 요청 URL에 따라 적절한 응답을 한다.
  • 이 때, Client(View)에서 Request Body에 담긴 데이터(DTO)를 Service에 넘겨주고, Service에서 처리하고 Response Body에 담겨 반환된 데이터(DTO)를 돌려주는 역할을 한다.

Service

  • Business Logic을 통한 데이터 가공자 역할!
  • Client의 요청(request)에 대해 어떤 처리를 할지 결정하는 부분이다.
    • 한마디로 요청들어온 부분을 개발자가 어떻게 변환하여(가공하여) 다시 사용자에게 전달할지 결정하는 부분이다.

DAO(Repository)

  • 실제 DB에 접근하기 위한 객체!
  • 실제로 DB에 접근하여 데이터를 CRUD 하는 객체이다.
  • Service와 DB를 연결해주는 역할을 하고 있으며, 인터페이스와 이에 대한 구현체를 만들어서 구현체에 CRUD관련 기능을 구현하고, 이를 DI(Dependency Injection) 해주는 방식으로 사용된다.
    • DI/IOC는 Spring Boot의 정체성이므로 반드시 따로 공부 필수.

Entity

  • DB 테이블에 대응하는 하나의 클래스!
  • 실제 DB의 테이블과 매칭될 핵심 클래스이다.
  • 최대한 외부에서 Entity 클래스의 Setter method를 사용하지 않도록 해당 클래스 안에서 필요한 logic을 구현해야한다. 이 때, Constructor(생성자) 또는 Builder를 사용한다.

DTO

  • 데이터 교환을 도와주는 우편물 상자 역할!
  • DTO는 계층간 데이터 교환을 도와주는 객체이다.
  • 세부적인 로직을 갖고 있지 않은 순수한 데이터 객체라고 생각하면 된다. 오직 Getter/Setter method만 가지고 있으며, 계층간 데이터 교환시 사용한다.

Spring Boot 생성

왼쪽에 있는 Spring Initializr을 선택하고 설정을 아래와 맞춰준다.

  • Name : 프로젝트 이름
  • Location : 저장 위치
  • Group : 프로젝트를 만드는 그룹의 이름, 대부분 기업의 도메인 명을 역순으로 작성
  • Artifact : 빌드 결과물의 이름
  • Package name : 프로젝트에 생성할 패키지 설정
  • JDK : 설치한 JDK 버전 17을 사용
  • Java : 버전 17을 사용
  • Packaging : 배포를 위해 프로젝트를 압축하는 방법을 선택, 이번엔 Jar 를 선택

Spring Boot DevTools, Spring Web, Lombok 의존성을 추가한다.

Create 버튼 클릭~

그렇게 생성된 프로젝트의 main() method를 실행시키면 정상적으로 Strated 되는 것을 볼 수 있다!

이 위치에 Package 하나를 생성합니다. 이름은 controller라고 한다.

위에서 Spring Boot 패키지 구조를 참고!

이 패키지에 HelloController 클래스를 만들어 준다.

클래스를 이렇게 작성해 주고 실행.

그 후 http://localhost:8080/hello URL로 들어간다.

이렇게 잘 뜨면 성공!

여기서 의문점이 들면 위에서 설명한 것을 잘 이해하고 있는 것이다.

아까 응답은 JSON 또는 XML과 같은 데이터 형식으로 반환한다고 했는데 왜 문자열 그대로 출력될까?

문자열이 바로 나오는 이유는 Spring Framework의 @RestController 애노테이션이 기본적으로 문자열을 HTTP 응답으로 반환할 때 JSON 변환이 필요하지 않다고 인식하기 때문에 "Hello" 문자열은 그대로 HTTP 응답으로 전송되고, 브라우저에서는 단순한 텍스트로 표시한다.

 

Spring Boot로 REST API 만들기

음악을 등록하고 플레이하는 노래방 서비스를 구현해보자.


패키지 구조

  • controller : 컨트롤러 클래스를 모아두는 패키지.
  • service : 서비스 클래스를 모아두는 패키지.
  • repository : 레포지토리 클래스를 모아두는 패키지.
  • domain : DB 테이블 컬럼과 동일한 필드를 가진 클래스(DB 처리용 클래스)를 모아두는 패키지.
  • dto : Data Transfer Object, dto 클래스를 모아두는 패키지.

Domain

package com.example.restapistudy.domain;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Music {
    private Long id; // 조회할 music 번호
    private String name; // 노래 이름
    private String singer; // 가수
    private Long count; // 노래를 실행한 횟수

    @Builder
    public Music(Long id, String name,String singer, Long count){
        this.id = id;
        this.name = name;
        this.singer = singer;
        this.count = count;
    }

    public void initId(Long id) { this.id = id;} // 노래 번호 초기화 메서드
}
  • Music이라는 domain을 만들었다.
  • 안에 컬럼으로 id, name, singer, count가 있다.
  • @Getter, @Setter을 사용해서 메서드를 자동으로 생성해준다.
    • @Setter은 실제 서비스에서는 지양한다. (자바의 객체지향 핵심 원칙 중 하나인 캡슐화 때문)

Dto

package com.example.restapistudy.dto;

import com.example.restapistudy.domain.Music;
import lombok.Builder;
import lombok.Data;

@Data
public class MusicDto {
    private Long id;
    private String name;
    private String singer;
    private Long count;

    @Builder
    public MusicDto(Long id, String name, String singer, Long count) {
        this.id = id;
        this.name = name;
        this.singer = singer;
        this.count = count;
    }

    public Music toEntity() { // Dio 객체를 Music으로 변환하는 메서드
        return Music.builder()
                .id(id)
                .name(name)
                .singer(singer)
                .count(count)
                .build();
    }
}
  • dto 클래스는 데이터를 주고 받는 클래스이다.
  • 따라서 내가 주고 싶은 데이터나 받고 싶은 데이터를 관리하는 클래스라고 생각하면 된다.
  • 지금은 dto가 하나만 있어도 충분하지만, 실제 프로젝트에서는 굉장히 많은 dto를 사용하게 된다.

Repository

package com.example.restapistudy.repository;

import com.example.restapistudy.domain.Music;

import java.util.List;

public interface MusicRepository {
    void save(Music music);

    Music findById(Long id);

    List<Music> findAll();

    void updateById(Long id, Music music);

    void deleteById(Long id);
}
  • repository는 데이터베이스와 가장 가까이 있는 계층이기 때문에 변경이 자주 일어난다.
  • 따라서 인터페이스를 통해서 구현할 목록을 쉽게 정의할 수 있고 새로운 구현체가 인터페이스를 준수하기만 하면, 내부 코드를 수정할 필요가 없다.
package com.example.restapistudy.repository;

import com.example.restapistudy.domain.Music;
import org.springframework.stereotype.Repository;

import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Repository
public class MemoryMusicRepository implements MusicRepository {
    private static Map<Long, Music> store = new HashMap<>(); //DB 흉내

    @Override // 입력 받은 id로 노래 저장.
    public void save(Music music) {
        store.put(music.getId(), music);
    }

    @Override // id(key)로 노래 찾기
    public Music findById(Long id) {
        return store.get(id);
    }

    @Override // 전체 리스트 찾기 (count 내림차순)
    public List<Music> findAll() {
        return store.values().stream().sorted(Comparator.comparing(Music::getCount).reversed()).toList();
    }

    @Override // 새로운 내용으로 초기화
    public void updateById(Long id, Music music) {
        store.put(id, music);
    }

    @Override // id(key)로 노래 삭제
    public void deleteById(Long id) {
        store.remove(id);
    }
}
  • 우리는 데이터베이스를 아직 사용하고 있지 않기에 메모리에만 저장하는 방법으로 구현하겠다.
  • 자바의 인터페이스를 알면 위의 로직을 쉽게 이해할 수 있다. (인터페이스 미준수, 즉 오버라이딩하지 않으면 오류)

Service

package com.example.restapistudy.service;

import com.example.restapistudy.domain.Music;
import com.example.restapistudy.dto.MusicDto;
import com.example.restapistudy.repository.MusicRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MusicService {
    private final MusicRepository musicRepository;

    public MusicService(MusicRepository musicRepository) { // 생성자
        this.musicRepository = musicRepository;
    }

    public void saveMusic(MusicDto musicDto) { // 노래 정보 저장 메서드
        Music music = musicDto.toEntity();

        musicRepository.save(music);
    }

    public MusicDto findMusicById(Long id) { // 노래 찾기 메서드
        Music music = musicRepository.findById(id);

        return MusicDto.builder()
                .id(music.getId())
                .name(music.getName())
                .singer(music.getSinger())
                .count(music.getCount())
                .build();
    }

    public List<MusicDto> findAllMusic() { // 노래 리스트 찾기 메서드
        return musicRepository.findAll()
                .stream()
                .map(music -> {
                    return MusicDto.builder()
                            .id(music.getId())
                            .name(music.getName())
                            .singer(music.getSinger())
                            .count(music.getCount())
                            .build();
                })
                .toList();
    }

    public void updateMusicById(Long id, MusicDto musicDto) { // 노래 업데이트 메서드
        Music music = musicDto.toEntity();
        music.initId(id);

        musicRepository.updateById(id, music);
    }

    public void deleteMusicById(Long id) { // 노래 삭제 메서드
        musicRepository.deleteById(id);
    }
}
  • service의 역할이 무엇인지 한번 생각해보자.
  • 클라이언트가 어떻게 데이터를 처리할지 결정하는 로직이다.
  • repository에서 저장하는 메서드를 만들었으면, 이제 service에서는 domain의 값을 받아와서 repository의 메서드를 이용해서 저장한다.
  • 위 말이 반드시 이해가 되어야한다. 곱씹고 넘어가자.

Controller

package com.example.restapistudy.controller;

import com.example.restapistudy.dto.MusicDto;
import com.example.restapistudy.service.MusicService;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
public class MusicController {
    private final MusicService musicService;

    //생성자 주입
    public MusicController(MusicService musicService) {
        this.musicService = musicService;
    }

    @PostMapping("music") //노래 저장
    public void save(@RequestBody MusicDto musicDto) {
        musicService.saveMusic(musicDto);
    }

    @GetMapping("music/{id}") // 노래 찾기
    public MusicDto findMusicById(@PathVariable Long id) {
        return musicService.findMusicById(id);
    }

    @GetMapping("music/list") // 노래 리스트 찾기
    public List<MusicDto> findAllMusic() {
        return musicService.findAllMusic();
    }

    @PatchMapping("music/{id}") // 노래 수정
    public void updateMusicById(@PathVariable Long id, @RequestBody MusicDto musicDto) {
        musicService.updateMusicById(id, musicDto);
    }

    @DeleteMapping("music/{id}") // 노래 삭제
    public void deleteMusicById(@PathVariable Long id) {
        musicService.deleteMusicById(id);
    }
}
  • @RequestBody : HTTP 요청의 분문(json)을 자바 객체로 변환해주는 어노테이션이다.
  • @PathVariable : URI에 변수를 넣을 수 있게 해준다.
  • 보통 GET 메서드에서는 @PathVariable을 쓰고 POST 메서드에서는 @RequestBody를 쓴다.
  • Controller는 우리가 구현한 메서드들을 HTTP 통신을 통해 사용할 수 있도록 API를 제공하는 역할을 한다.


포스트맨 사용하기

  • API를 쉽게 테스트 할 수 있는 도구인 Postman을 사용해보자.
  • GET 메서드는 local 환경에서 URL만으로 테스트가 가능하지만, 다른 메서드들은 테스트할 수 없다.
  • 따라서 우리는 POSTMAN을 사용해 API가 정상적으로 작동하는지 테스트할 수 있다.

포스트맨 회원가입 및 사용 방법

  • 실습으로 진행하자.


참고자료

 

[GDSC] Spring Boot로 REST API 만들어보기

스프링과 스프링부트(Spring Boot)ㅣ정의, 특징, 사용 이유, 생성 방법

Spring Boot에서 REST API 설계 및 구현

RESTful한 API가 무슨 뜻일까?