start.spring.io 에서 초기 세팅 후 다운로드
버전 선택 시, SNAPSHOT 나 RC1 등 뒤에 괄호로 붙어있는 버전들은 안정화된 버전이 아니므로, 붙어있는 내용이 없는 버전 중 가장 최신의 버전으로 선택하는 것을 권장함
Packaging에 나와있는 Jar과 War 중, 스프링부트에는 Tomcat이 내장되어있으므로 Jar를 선택하는 것을 권장함
Dependencies에서는 의존성을 설정할 수 있다. 즉, 프로젝트에서 사용할 라이브러리나 프레임워크를 설정하게 된다.
이 때, 라이브러리는 다른 사람이 미리 만들어 둔 기능을 가져다 사용하는 용도로 쓰이고, 프레임워크는 미리 만들어진 구조에 나의 코드를 끼워넣는 방식으로 사용된다.
예를 들어, 케이크를 만든다고 할 때, 빵부터 반죽해서 만들지 않고 빵집에서 잘 만들어둔 빵을 사와서 데코부터 시작한다고 할 때 빵은 라이브러리라고 할 수 있다.
그리고 케이크를 내가 주도해서 케이크 만들기 원데이 클래스에 가서 강사의 설명에 따라 만든다면, 원데이 클래스가 프레임워크라고 할 수 있다.
src > main > java > hello.core > CoreApplication 파일 열고 실행


@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);
}
}
GET /portion?color=red&count=2 를 spring.com:3000 으로 보내기GET (전달), POST (받기), PUT (수정), DELETE (삭제)
https://spring.com:3000/portion?color=red&count=2
| HTTP Method | HTTP path | query | API return 값 |
|---|---|---|---|
GET | /add | int num1, int num2 | num1 + num2 |
@RestController 어노테이션 붙여서 해당 클래스를 Controller (API의 진입점) 로 등록하기@RestController는 @Controller와 @ResponseBody를 합친 역할을 함. 즉, 기존에 사용되던 @Controller는 HTML 페이지를 반환하는 것이 기본값이므로 JSON, 문자열 등 데이터를 반환하기 위해서는 @ResponseBody를 추가로 붙여줘야 함. 이 때, @RestController 를 사용하면 추가 설정 없이 JSON, 문자열, 객체 데이터 등 메서드의 반환값을 그대로 응답 Body에 넣어서 반환할 수 있음.
즉, 프론트엔드와 협업하여 API를 작성하는 경우에는 HTML 화면(view)을 반환할 필요가 없으므로 @Controller 대신 @RestController 를 주로 사용한다고 함.
| 어노테이션 | 역할 | 반환 값 |
|---|---|---|
@Controller | HTML 페이지 반환 | View(템플릿 파일) |
@ResponseBody | 데이터를 응답 본문에 직접 넣음 | JSON, 문자열 등 |
@RestController | @Controller + @ResponseBody | JSON, 문자열 등 |
@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;
}
메소드의 파라미터에 @RequestParam 붙이기
GET /add?num1=10&num2=20 와 같이 쿼리 파라미터 형태로 데이터를 받아왔을 때 같은 이름을 가진 쿼리의 값을 바로 메소드의 argument로 대입하여 사용하기 위함
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;
}
}
Command + N
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;
}
}
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();
}
}

key와 value 형태로 이루어져있음. JSON의 값은 다양하게 설정이 가능하며, 심지어는 또 다른 JSON을 할당할 수도 있음. {
"key": value,
}
GET으로 설정하는 것이 더 적절하지만 공부하는 단계이므로 POST로 설정함| HTTP Method | HTTP path | HTTP body | API return 값 |
|---|---|---|---|
POST | /multiply | { "num1": 3, "num2" : 5 } | num1 * num2 |
API 메소드 생성
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;
}
}

GET API와 POST API 생성에 대해 공부한 후 다른 API 예제로 연습해보기

// 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);
}


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;
}


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(...)) 두 가지 중 하나가 있어야 제대로 객체를 만들어낼 수 있음.
- 사용자
- 도서관의 사용자를 등록할 수 있다 (이름 필수, 나이 선택)
- 도서관의 사용자 목록을 조회할 수 있다
- 도서관의 사용자 목록을 업데이트 할 수 있다 (수정)
- 도서관의 사용자를 삭제할 수 있다
- 책
- 도서관에 책을 등록 및 삭제할 수 있다
- 사용자는 책을 빌릴 수 있다 (단, 다른 사람이 이미 빌린 책은 다시 빌릴 수 없다)
- 사용자는 책을 반납할 수 있다
http://localhost:8080/v1/index.html 위치에 생성해둠
| HTTP Method | HTTP path | HTTP body |
|---|---|---|
POST | /user | { "name": String (null), "age": Integer } |
int 와 Integer 의 차이
int 는 null 값을 표현할 수 없고,Integer는 표현이 가능함 (api 작성 시 필수 값과 선택 값을 표현하기 위해 null 값이 필요함!)
여기서 처음으로 저장 기능을 구현하게 됨. 이 때, User 라는 객체를 생성해서 사용자를 저장함. (아직 데이터베이스를 연동하지 않았으므로 이렇게 처리함. 한번 서버를 켠 동안에만 데이터가 유효할 것.)

400 오류 발생 이유 및 해결: UserCreateRequest DTO 클래스의 기본 생성자가 누락되어 역직렬화 실패 문제로 인해 발생했고, public UserCreateRequest() {}를 추가해서 해결함
| HTTP Method | HTTP path | return |
|---|---|---|
GET | /user | { "id": Long, "name": String (not null), "age": Integer } |
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;
}
}