섹션 2. 생애 최초 API 만들기

김민지·2025년 3월 18일

서버 기초 강의

목록 보기
2/12

스프링 프로젝트를 시작하는 방법

  1. start.spring.io 에서 초기 세팅 후 다운로드
  • 버전 선택 시, SNAPSHOTRC1 등 뒤에 괄호로 붙어있는 버전들은 안정화된 버전이 아니므로, 붙어있는 내용이 없는 버전 중 가장 최신의 버전으로 선택하는 것을 권장함

  • Packaging에 나와있는 JarWar 중, 스프링부트에는 Tomcat이 내장되어있으므로 Jar를 선택하는 것을 권장함

  • Dependencies에서는 의존성을 설정할 수 있다. 즉, 프로젝트에서 사용할 라이브러리나 프레임워크를 설정하게 된다.

    • 이 때, 라이브러리는 다른 사람이 미리 만들어 둔 기능을 가져다 사용하는 용도로 쓰이고, 프레임워크는 미리 만들어진 구조에 나의 코드를 끼워넣는 방식으로 사용된다.

    • 예를 들어, 케이크를 만든다고 할 때, 빵부터 반죽해서 만들지 않고 빵집에서 잘 만들어둔 빵을 사와서 데코부터 시작한다고 할 때 빵은 라이브러리라고 할 수 있다.
      그리고 케이크를 내가 주도해서 케이크 만들기 원데이 클래스에 가서 강사의 설명에 따라 만든다면, 원데이 클래스가 프레임워크라고 할 수 있다.

  1. src > main > java > hello.core > CoreApplication 파일 열고 실행

  • ✨ 한글 깨짐 오류 해결: 파일 > 설정 > gradle 검색 > 인텔리제이로 변경

@SpringBootApplication과 서버

  • @SpringBootApplication : 스프링을 실행시킬 때 필요한 다양한 설정들을 자동으로 해주는 어노테이션
package com.group.libraryapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication // 어노테이션
public class LibraryAppApplication {

  public static void main(String[] args) {
  	// 서버 시작 코드
    SpringApplication.run(LibraryAppApplication.class, args);
  }

}

네트워크란 무엇인가?!

  • 서로 다른 컴퓨터 간에 인터넷을 사용해서 요청을 보내기 위해서는 어떤 컴퓨터에게 보낼 것인지와 해당 컴퓨터 안의 어떤 프로그램에게 보낼 것인지를 정의해야한다.
  • 요청을 받을 컴퓨터를 식별하기 위해 컴퓨터 별 고유 주소(IP)를 사용하고, 요청을 받을 프로그램을 식별하기 위해 포트 번호를 사용한다.
    하나의 포트는 하나의 프로그램에만 사용될 수 있으므로 식별이 가능하다.
  • 이 때, IP주소는 대부분 숫자로만 구성되어있는데 이는 사람이 사용하기 어려워서 등장한 것이 도메인 이름이다. (Domain Name System)

HTTP와 API란 무엇인가?!

  • HTTP(Hyper Text Transfer Protocol) : 웹을 통한 컴퓨터 간의 통신에 대한 표준화된 방식으로, HTTP method와 path가 핵심이다.
    예: GET /portion?color=red&count=2spring.com:3000 으로 보내기
  • HTTP method의 종류: GET (전달), POST (받기), PUT (수정), DELETE (삭제)
  • API (Application Programming Interface) : 정해진 약속을 통해 특정 기능을 수행하는 것
  • URL ( Uniform Resource Locator) :
    예: https://spring.com:3000/portion?color=red&count=2
  • Client Server 구조

GET API 개발하고 테스트하기

  1. API 명세 작성하기: 간단한 덧셈 API를 설계하는 것에 앞서서 API 명세를 먼저 작성해보자.
HTTP MethodHTTP pathqueryAPI return 값
GET/add int num1, int num2num1 + num2
  1. API 요청을 처리하고자 하는 클래스 위에 @RestController 어노테이션 붙여서 해당 클래스를 Controller (API의 진입점) 로 등록하기
  • @RestController@Controller@ResponseBody를 합친 역할을 함. 즉, 기존에 사용되던 @Controller는 HTML 페이지를 반환하는 것이 기본값이므로 JSON, 문자열 등 데이터를 반환하기 위해서는 @ResponseBody를 추가로 붙여줘야 함. 이 때, @RestController 를 사용하면 추가 설정 없이 JSON, 문자열, 객체 데이터 등 메서드의 반환값을 그대로 응답 Body에 넣어서 반환할 수 있음.

  • 즉, 프론트엔드와 협업하여 API를 작성하는 경우에는 HTML 화면(view)을 반환할 필요가 없으므로 @Controller 대신 @RestController 를 주로 사용한다고 함.

어노테이션역할반환 값
@ControllerHTML 페이지 반환View(템플릿 파일)
@ResponseBody데이터를 응답 본문에 직접 넣음JSON, 문자열 등
@RestController@Controller + @ResponseBodyJSON, 문자열 등
  1. @GetMapping 어노테이션 붙이기
  • @GetMapping 으로 URL에서 값(PathVariable) 받기
    GET /users/10"사용자 ID: 10" 응답
    @GetMapping("/users/{id}")
    public String getUserById(@PathVariable Long id) {
        return "사용자 ID: " + id;
    }
  • @GetMapping 으로 쿼리 파라미터(RequestParam) 받기
    GET /api/search?keyword=Spring"검색어: Spring" 응답
	@GetMapping("/search")
    public String search(@RequestParam(defaultValue = "검색어") String keyword) {
        return "검색어: " + keyword;
    }
  1. 메소드의 파라미터에 @RequestParam 붙이기
    GET /add?num1=10&num2=20 와 같이 쿼리 파라미터 형태로 데이터를 받아왔을 때 같은 이름을 가진 쿼리의 값을 바로 메소드의 argument로 대입하여 사용하기 위함

  2. main 클래스를 실행시켜서 서버를 시작시킨 후 포스트맨에서 API 요청 보내며 테스트하기

package com.group.libraryapp.controller.calculator;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CalculatorController {
    @GetMapping("/add") // GET /add
    public int addTwoNumbers(@RequestParam int num1, @RequestParam int num2) {
        return num1 + num2;
    }
}
  1. 여기서, 지금은 간단한 테스트 API이므로 쿼리 파라미터의 개수가 많지 않지만 추후에 여러개의 값을 다루게 될 경우 argument로 받아오는 방식은 불편할 수 있으므로 객체를 도입해보기
  • 맥북에서는 생성자 추가 단축키가 Command + N
  • 아래와 같이 객체 생성
    ⭐️ 이 때, 아래의 객체와 같이 데이터를 전달해주는 객체를 Data Transfer Object (DTO)라고 부른다.
package com.group.libraryapp.dto.calculator.request;

public class CalculatorAddRequest {
    private int num1;
    private int num2;

    public CalculatorAddRequest(int num1, int num2) {
        this.num1 = num1;
        this.num2 = num2;
    }

    public int getNum1() {
        return num1;
    }

    public int getNum2() {
        return num2;
    }
}
  • 생성한 객체를 앞서 작성한 API에서 사용하도록 설정하기
package com.group.libraryapp.controller.calculator;

import com.group.libraryapp.dto.calculator.request.CalculatorAddRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CalculatorController {
    @GetMapping("/add") // GET /add
    // request 객체로 받아오도록 새로 설정!
    public int addTwoNumbers(CalculatorAddRequest request) {
        return request.getNum1() + request.getNum2();
    }
}
  • 다시 동일하게 동작하는지 확인하기 위한 포스트맨 테스트

복습 질문

  1. 어노테이션을 사용하는 이유 (효과) 는 무엇일까?
  2. 나만의 어노테이션은 어떻게 만들 수 있을까?

POST API 개발하고 테스트하기

  • JSON은 아래와 같이 keyvalue 형태로 이루어져있음. JSON의 값은 다양하게 설정이 가능하며, 심지어는 또 다른 JSON을 할당할 수도 있음.
{
	"key": value,
}
  1. API 명세 작성하기: 간단한 곱셈 API를 설계하는 것에 앞서서 API 명세를 먼저 작성해보자.
  • 실제로는 곱셈 API도 데이터를 단순히 받는 것이므로 GET으로 설정하는 것이 더 적절하지만 공부하는 단계이므로 POST로 설정함
HTTP MethodHTTP pathHTTP bodyAPI return 값
POST/multiply { "num1": 3, "num2" : 5 }num1 * num2
  1. API 메소드 생성

  2. GET과 달리 요청의 body에서 데이터를 가져와야 하므로 @RequestBody 를 argument로 받은 객체의 앞에 붙여줘야 함

  • @RequestBody 는 HTTP body로 들어오는 JSON을 객체로 바꿔줌
package com.group.libraryapp.controller.calculator;

import com.group.libraryapp.dto.calculator.request.CalculatorAddRequest;
import com.group.libraryapp.dto.calculator.request.CalculatorMultiplyRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CalculatorController {
    @GetMapping("/add") // GET /add
    public int addTwoNumbers(CalculatorAddRequest request) {
        return request.getNum1() + request.getNum2();
    }
    @PostMapping("/multiply") // POST /multiply
    public int multiplyTwoNumbers(@RequestBody CalculatorMultiplyRequest request) {
        return request.getNum1() * request.getNum2();
    }
}
package com.group.libraryapp.dto.calculator.request;

public class CalculatorMultiplyRequest {
    private int num1;
    private int num2;

    public CalculatorMultiplyRequest(int num1, int num2) {
        this.num1 = num1;
        this.num2 = num2;
    }

    public int getNum1() {
        return num1;
    }

    public int getNum2() {
        return num2;
    }
}
  1. 테스트
  • 한 Controller 클래스 안에 여러개의 API를 추가할 수 있음을 알 수 있음!

추가 예제들

  • GET API와 POST API 생성에 대해 공부한 후 다른 API 예제로 연습해보기
  1. 입력된 두 수의 덧셈, 뺄셈, 곱셈 결과값을 JSON 형태로 반환하기

  • 아래와 같이 Response를 위한 DTO 객체를 생성한 후, 이를 반환하여 JSON 형태 반환을 설정함.
// Response를 위한 DTO 객체
package com.group.libraryapp.dto.calculator.request;

public class CalculatorCalcResponse {
    private int add;
    private int minus;
    private int multiply;

    public CalculatorCalcResponse(int add, int minus, int multiply) {
        this.minus = minus;
        this.add = add;
        this.multiply = multiply;
    }

    public int getAdd() {
        return add;
    }

    public int getMinus() {
        return minus;
    }

    public int getMultiply() {
        return multiply;
    }
}
    // 추가 예제 -> 문제 1
    @GetMapping("/api/v1/calc")
    public CalculatorCalcResponse CalculateTwoNumbers(CalculatorAddRequest request) {
        int sum = request.getNum1() + request.getNum2();
        int minus = request.getNum1() - request.getNum2();
        int multiply = request.getNum1() * request.getNum2();

        return new CalculatorCalcResponse(sum, minus, multiply);
    }
  1. 날짜를 입력하면 요일을 JSON 형태로 반환하기

  • LocalDate 는 시간 정보 없이 "년-월-일" 형태의 날짜만 다뤄야 하는 경우에 유용하게 사용됨

    • 오늘 날짜 가져오기
      LocalDate today = LocalDate.now();

    • 특정 날짜 생성
      LocalDate date = LocalDate.of(2025, 3, 21);
      → 직접 연, 월, 일 입력

    • 문자열 → 날짜
      LocalDate date = LocalDate.parse("2025-03-21");
      "yyyy-MM-dd" 형식만 지원

    • 날짜 → 문자열: String str = date.toString();
      "yyyy-MM-dd" 형태로 변환됨

    • 날짜 연산
      date.minusWeeks(2);, date.minusYears(1);, date.plusDays(5);, date.plusMonths(1);

    • 요일 확인
      DayOfWeek dow = date.getDayOfWeek();
      → 이 때, 결과값이 MONDAY ~ SUNDAY 등의 형태로 나오기는 하지만 주의해야 할 점은 이들이 String 타입이 아닌 enum 타입이라는 것이다. 따라서, String parsing 등 문자열 관련 연산을 하기 위해서는 toString 을 해줘야 한다.

    • 월 / 일 / 연도 개별 값 추출하기
      date.getYear(), date.getMonthValue(), date.getDayOfMonth()

// 추가 예제 -> 문제 2: 날짜를 입력하면 요일을 JSON 형태로 반환하기
    @GetMapping("/getdate")
    public Map<String, String> getDate(@RequestParam String date) {
        Map<String, String> response = new HashMap<>();
        try {
            // 날짜 문자열을 LocalDate로 변환
            LocalDate dateObject = LocalDate.parse(date);

            // 요일 구하기 (첫 3글자만 반환)
            String dayOfTheWeek = dateObject.getDayOfWeek().toString().substring(0, 3);

            // JSON 응답 반환
            response.put("dayOfTheWeek", dayOfTheWeek);

        } catch (DateTimeParseException e) {
            response.put("error", "잘못된 날짜 형식입니다. (예: 2023-01-01)");
        }
        return response;
    }
  1. 여러개의 숫자를 배열 형태로 입력받아 총합을 반환 (JSON 형태가 아니어야 함)

package com.group.libraryapp.dto.calculator.request;

public class CalculatorSumResponse {
    private int[] numbers;

    public CalculatorSumResponse() {} // 역직렬화를 해결하기 위해 추가한 기본 생성자

    public CalculatorSumResponse(int[] numbers) {
        this.numbers = numbers;
    }

    public int[] getNumbers() {
        return numbers;
    }
}
// 추가 예제 -> 문제 3: 여러개의 숫자를 배열 형태로 입력받아 총합을 반환
    @PostMapping("/sum")
    public int sumArrayofNumbers(@RequestBody CalculatorSumResponse response) {
        int sum = 0;
        for (int num : response.getNumbers()) {
            sum += num;
        }
        return sum;
    }
  • 이 때, @RequestBody객체로 변환할 때, 기본 생성자 (파라미터 없는 생성자) or setter 메서드 둘 중 적어도 하나가 필요함. 생성자만 작성해서 역직렬화에 실패하며 500 오류가 발생하는 문제를 겪음.

  • 역직렬화 (Deserialization)란?
    클라이언트가 JSON을 보냄 → 컨트롤러에서 @RequestBody로 받음 → Java 객체로 변환의 과정
    (반대로 직렬화는 Java 객체 → JSON, XML...으로 변환하는 과정을 말함)
    Jackson이라는 라이브러리가 내부적으로 처리하는데,
    이 때 Jackson은 기본 생성자나 (new CalculatorSumResponse())
    setter (setNumbers(...)) 두 가지 중 하나가 있어야 제대로 객체를 만들어낼 수 있음.

예제 ) 도서 관리 애플리케이션a 요구사항

  • 사용자
    • 도서관의 사용자를 등록할 수 있다 (이름 필수, 나이 선택)
    • 도서관의 사용자 목록을 조회할 수 있다
    • 도서관의 사용자 목록을 업데이트 할 수 있다 (수정)
    • 도서관의 사용자를 삭제할 수 있다
    • 도서관에 책을 등록 및 삭제할 수 있다
    • 사용자는 책을 빌릴 수 있다 (단, 다른 사람이 이미 빌린 책은 다시 빌릴 수 없다)
    • 사용자는 책을 반납할 수 있다
  • 해당 강의에서는 서버 단에서의 API 생성에 대해 다루므로 프론트단을 생략하고, 미리 웹 화면을 구현하기 위한 html 파일을 http://localhost:8080/v1/index.html 위치에 생성해둠

유저 생성 API 개발

  • 도서관의 사용자를 등록할 수 있다 (이름 필수, 나이 선택)

API 명세서

HTTP MethodHTTP pathHTTP body
POST/user { "name": String (null), "age": Integer }
  • intInteger 의 차이
    intnull 값을 표현할 수 없고,Integer는 표현이 가능함 (api 작성 시 필수 값과 선택 값을 표현하기 위해 null 값이 필요함!)

  • 여기서 처음으로 저장 기능을 구현하게 됨. 이 때, User 라는 객체를 생성해서 사용자를 저장함. (아직 데이터베이스를 연동하지 않았으므로 이렇게 처리함. 한번 서버를 켠 동안에만 데이터가 유효할 것.)

  • 400 오류 발생 이유 및 해결: UserCreateRequest DTO 클래스의 기본 생성자가 누락되어 역직렬화 실패 문제로 인해 발생했고, public UserCreateRequest() {}를 추가해서 해결함

유저 조회 API 개발과 테스트

HTTP MethodHTTP pathreturn
GET/user { "id": Long, "name": String (not null), "age": Integer }
  • controller에서 (getter가 있는) 객체를 반환하면 곧바로 JSON 형태로 객체의 모든 데이터를 반환할 수 있음!
  • List<User> 에 담겨있는 User의 순서를 id값으로 설정하기

package com.group.libraryapp.controller.user;

import com.group.libraryapp.domain.user.User;
import com.group.libraryapp.dto.user.request.UserCreateRequest;
import com.group.libraryapp.dto.user.response.UserResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
public class UserController {
    private final List<User> users = new ArrayList<>();

    @PostMapping("/user") // POST /user
    public void createUser(@RequestBody UserCreateRequest request) {
        users.add(new User(request.getName(), request.getAge()));
    }

    @GetMapping("/user") // GET /user
    public List<UserResponse> getUsers() {
        List<UserResponse> userResponses = new ArrayList<>();
        for (int i = 0; i < users.size(); i++) {
            userResponses.add(new UserResponse(i + 1, users.get(i)));
        }
        return userResponses;
    }
}
package com.group.libraryapp.dto.user.response;

import com.group.libraryapp.domain.user.User;

public class UserResponse {
    private long id;
    private String name;
    private Integer age;

    public UserResponse(long id, User user) {
        this.id = id;
        this.name = user.getName();
        this.age = user.getAge();
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }
}
package com.group.libraryapp.domain.user;

public class User {
    private String name;
    private Integer age;

    public User(String name, Integer age) {
        // name은 NOT NULL 이므로 조건을 걸어야 함
        if (name == null || name.trim().isEmpty()) { // 이렇게 예외를 던져두면, name이 비어있는 경우 User 객체 자체가 생성이 안됨
            throw new IllegalArgumentException("name은 비워둘 수 없습니다.");
        }
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }
}
package com.group.libraryapp.dto.user.request;

public class UserCreateRequest {
    private String name;
    private Integer age = null;

    public UserCreateRequest() {}
    public UserCreateRequest(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }
}

0개의 댓글