📌 기본 정보
- 주차/주제: 3주차 - REST API 설계와 구현
📚 학습 내용 요약
- API, REST API, RESTful API의 개념 정리하기API는 응용 프로그램(애플리케이션)에서 운영 체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스를 뜻합니다.✅ 기본 개념:
- 애플리케이션이 다른 애플리케이션에 요청을 보내고, 응답을 받을 수 있도록 정해진 규칙(메서드, 데이터 형식 등)을 제공합니다.
- 예를 들어, 카카오톡 앱이 구글 지도 API를 사용해서 지도를 표시하는 것처럼, 다양한 프로그램들이 서로 서로의 기능을 호출하고 결과를 받습니다.✅ REST API(Representational State Transfer) 정의:웹상에서 사용되는 여러 리소스를 HTTP URI로 표현하고, 해당 리소스에 대한 행위를 HTTP Method로 정의하는 방식을 말합니다.
- ✅ 기본 개념:
- REST API는 웹에서 데이터를 주고받기 위해 사용하는 API

- HTTP 프로토콜을 사용하여 데이터를 주고받는 방식을 정의합니다.
- REST API는 URL을 통해 리소스를 지정하고, HTTP 메서드(GET, POST, PUT, DELETE 등)를 통해 리소스에 대한 동작을 수행합니다.
- 리소스는 데이터나 객체일 수 있고, 이를 HTTP 메서드로 조회, 생성, 수정, 삭제합니다.
- 구성요소
- 자원(Resource) : HTTP URI
- 자원에 대한 행위(Verb) : HTTP Method
- 자원에 대한 행위의 내용 (Representations) : HTTP Message Pay Load, 데이터 형식RESTful API는 REST 아키텍처 스타일을 잘 따르는 APIAPI를 RESTful 하게 만들어서 API의 목적이 무엇인지 명확하게 하기 위해 RESTful 함을 지향 합니다.
- ✅ 기본 개념:
- 즉, REST API를 설계할 때 좋은 방식으로 설계된 API를 의미
- ✅ RESTful API 정의:
- RESTful API는 REST의 원칙과 규칙을 충실히 따르는 API입니다.
- /(슬래시)는 계층 관계를 나타낼때 사용합니다.
- URI 마지막 문자에 /(슬래시)를 사용하지 않습니다.
- URI에 _(underscore)는 사용하지 않도록 합니다. 또한 영어 대문자보다는 소문자를 씁니다.
- URI에 명사를 사용합니다.
- URI에 파일의 확장자(예를들어 .json , .JPGE)를 포함 시키지 않습니다.
- 각 리소스는 URL로 고유하게 식별되며, HTTP 메서드(GET, POST, PUT, DELETE 등)를 통해 리소스를 조작합니다.
- 상태를 유지하지 않으며, 각 요청은 독립적으로 처리됩니다. 즉, 서버는 각 요청에 대해 필요한 정보만을 처리하고 이전 요청을 기억하지 않습니다.
- 주로 파일 제어, 창 제어, 화상 처리, 문자 제어 등을 위한 인터페이스를 제공합니다
- ✅ API(Application Programming Interface) 정의:

✅ RESTful API 특징:
- 자원(Resource): 모든 데이터는 자원(리소스)으로 취급됩니다.
- 예: 사용자(users), 게시글(posts), 제품(products) - HTTP 메서드: 각 자원에 대해 GET, POST, PUT, DELETE 메서드를 사용해 읽기, 생성, 수정, 삭제 작업을 수행합니다.

- 무상태성(Stateless): 각 요청은 독립적이며, 서버는 이전 요청을 기억하지 않습니다.
- 사용자가 로그인하지 않은 상태에서 GET /users 요청을 보내면, 서버는 사용자 인증 정보를 기억하지 않음. 클라이언트는 매번 인증 정보를 요청에 담아 보내야 합니다.
- 유연한 응답 형식: JSON이나 XML 형식으로 데이터를 응답받을 수 있습니다.
- Uniform Interface (인터페이스 일관성): RESTful API는 일관된 인터페이스를 유지하며, URL 구조나 메서드 사용을 통해 명확한 규칙따름.
- 리소스 이름은 복수형
- Layered System (계층화)
- HTTP 메서드(GET, POST, PUT, DELETE)의 용도와 차이점 정리하기HTTPRequestMethod✅ 용도:POST 메서드는 새로운 리소스(데이터)를 서버에 생성할 때 사용됩니다. 즉, 서버에서 새로운 데이터를 추가하거나, 새로운 객체를 생성할 때 쓰는 메서드입니다.✅ 주요 특징:
- 리소스 생성: 클라이언트가 서버에 요청을 보내고, 서버는 새로운 리소스를 생성하여 저장합니다.
- 유일한 ID: 서버에서 새로운 리소스를 생성할 때, 해당 리소스에 고유 ID를 부여하는 경우가 많습니다.
- 본문(body): POST 요청은 데이터를 요청 본문(body)에 담아 보냅니다. 이 데이터는 주로 JSON 형식입니다.
- 멱등성 없음: 같은 요청을 여러 번 보내면 매번 다른 결과를 초래할 수 있습니다. (즉, 중복 생성이 될 수 있음)
- ✅ 장점:
- 서버에 데이터를 추가하는 데 적합합니다.
- 데이터를 요청 본문에 담아 보낼 수 있어 복잡한 데이터도 함께 전송 가능합니다.
- 요청이 서버 상태를 변경하기 때문에, 응답으로 생성된 리소스를 반환하는 것이 일반적입니다.
- ● POST : 생성, 서버에 자원을 등록

- 새 유저 추가: /users에 POST 요청을 보내면 새로운 유저가 생성됩니다.
@PostMapping
public String createUser(@RequestBody User user) {
service.createUser(user);
return "사용자가 저장되었습니다.";
}
POST /users
body : {date : "example"}
Content-Type : "application/json"
● GET: 읽기, 서버 자원을 얻음
✅ 용도:
GET 메서드는 주로 데이터를 조회하는 데 사용됩니다.
즉, 서버에서 데이터를 읽기만 하고, 수정하거나 삭제하지는 않습니다.
✅ 주요 특징:
- 데이터 조회: 클라이언트가 서버에 요청을 보내고, 서버는 데이터를 응답으로 보내줍니다.
- 멱등성(Idempotency): 같은 요청을 여러 번 보내도 결과가 변하지 않아요. 예를 들어, /coffees를 여러 번 요청해도, 항상 같은 커피 목록이 응답으로 돌아옵니다.
- URL 경로: 조회할 데이터를 경로로 구분해 보냅니다. 예를 들어:
- /coffees → 모든 커피 목록 조회
- /coffees/{id} → 특정 ID의 커피 조회
✅ 장점:
- 단순하고 직관적: 데이터를 요청하는 방식이 매우 직관적이라서 이해하기 쉽습니다.
- 캐싱 가능: GET 요청은 서버나 브라우저에서 캐싱될 수 있어, 동일한 요청을 더 빠르게 처리할 수 있습니다.


- 모든 유저 조회: /users/에 GET 요청을 보내면 유저 목록을 받을 수 있습니다.
- 특정 유저 조회: /users/{id}에 GET 요청을 보내면, 그 특정 유저 정보를 받을 수 있습니다.
@GetMapping("/")
public List<User> getAllUsers() {
return service.getAllUsers(); // 모든 사용자 목록 반환
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return service.getUserById(id);
}
**● PUT: 업데이트, 서버 자원을 새로운 자원으로 치환**
✅ 용도:
PUT 메서드는 기존 리소스를 수정하거나, 없는 리소스는 새로 생성할 때 사용됩니다.
즉, 서버에 있는 기존 데이터를 업데이트하거나, 존재하지 않으면 새 데이터를 추가할 수 있습니다.
✅ 주요 특징:
- 기존 리소스 수정: 클라이언트가 요청한 URI에 해당하는 기존 데이터를 수정합니다.
- 리소스 생성 가능: 만약 해당 리소스가 존재하지 않으면 새로운 리소스를 생성할 수 있습니다.
- 멱등성(Idempotency): 같은 PUT 요청을 여러 번 보내도 결과가 변하지 않음. 즉, 한 번 수정된 데이터는 같은 PUT 요청을 여러 번 보내도 결과가 동일합니다.
- 본문(body): PUT 요청은 데이터를 요청 본문(body)에 담아 보내며, 주로 JSON 형식으로 데이터를 보냅니다
✅ 장점:
- 리소스 수정에 적합합니다.
- 불필요한 중복 요청을 피할 수 있어, 같은 요청을 여러 번 보내도 데이터는 동일하게 유지됩니다.
- 데이터 업데이트와 생성을 하나의 메서드로 처리할 수 있어 간편합니다.


- 유저 정보 수정: /users/{id}에 PUT 요청을 보내면 해당 유저 정보를 수정합니다.
- 새로운 유저 생성: /users/{id}에 PUT 요청을 보내면, 해당 ID 유저가 없으면 새로 생성합니다.
@PutMapping("/{id}")
public String updateUser(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
service.updateUser(user);
return "사용자 전체 수정 완료!";
}
PUT /user/1
body : {date : "update example"}
Content-Type : "application/json"
● PATCH: 업데이트, 서버 자원의 일부만 수정
✅ 용도:
PATCH 메서드는 리소스의 일부만 수정할 때 사용됩니다.
즉, 기존 리소스를 완전히 교체하지 않고 일부 속성만 업데이트할 때 사용해요.
✅ 주요 특징:
- 부분 수정: 리소스의 전체가 아닌 일부만 수정합니다. 예를 들어, 커피의 이름만 바꾸고 가격은 그대로 두는 식입니다.
- 멱등성 없음: PATCH 요청은 멱등성을 보장하지 않아요. 즉, 같은 PATCH 요청을 여러 번 보내면 결과가 달라질 수 있습니다. (수정된 값이 여러 번 적용될 수 있음)
- 본문(body): PATCH 요청은 수정할 데이터만 본문에 담아 보냅니다. 주로 JSON 형식으로 보내며, 수정할 부분만 포함시킵니다.
- 적용 범위: PUT은 전체 리소스를 대체하지만, PATCH는 부분적인 수정만 진행해요.
✅ 장점:
- 부분 수정을 통해 불필요한 데이터 변경을 피할 수 있습니다.
- 데이터의 일부만 변경하고 나머지는 그대로 두고 싶을 때 적합합니다.

- 사용자의 정보 일부 수정: /users/{id}에 PATCH 요청을 보내면 특정 정보만 업데이트됩니다.
@PatchMapping("/{id}")
public String partialUpdateUser(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
service.partialUpdateUser(id, updates);
return "사용자 일부 수정 완료!";
}
PUT과 PATCH의 차이점과 멱등성
✅ PUT의 멱등성
PUT은 리소스 전체를 대체하는 방식
첫 번째 요청 이후에 같은 데이터를 다시 보내면 결과가 변하지 않음
✅ PATCH의 멱등성
PATCH는 부분 수정을 하는 메서드
PATCH 요청이 들어올 때마다 부분적인 값이 계속 수정될 수 있음
PUT은 리소스 전체를 대체하고, 결과가 항상 같게 유지되므로 멱등성이 보장됨.
PATCH는 부분만 수정하기 때문에, 같은 요청을 여러 번 보낼 경우 결과가 달라질 수 있어 멱등성이 보장되지 않음.
● DELETE: 삭제, 서버 자원을 삭제
✅ 용도:
DELETE 메서드는 리소스를 삭제할 때 사용됩니다.
즉, 서버에서 특정 데이터를 삭제하는 요청을 처리합니다.
✅ 주요 특징:
- 리소스 삭제: 클라이언트가 서버에 요청을 보내면, 서버는 특정 리소스를 삭제합니다.
- 멱등성(Idempotency): DELETE는 멱등성을 보장합니다. 즉, 같은 DELETE 요청을 여러 번 보내도, 결과가 같고 삭제된 리소스는 다시 복구되지 않습니다.
- URI 경로: DELETE 요청은 보통 삭제할 리소스의 URI를 지정해 보내며, 리소스를 찾아서 삭제합니다.
- 응답 상태 코드: 삭제가 성공하면 204 No Content 응답을 보낼 수 있고, 리소스가 없으면 404 Not Found를 보낼 수 있습니다.
✅ 장점:
- 리소스 삭제를 간단하고 명확하게 처리할 수 있습니다.
- 멱등성이 보장되므로 여러 번 삭제 요청을 보내도 결과가 동일합니다.

- 사용자 삭제: /users/{id}에 DELETE 요청을 보내면, 해당 사용자를 삭제합니다.
@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Long id) {
service.deleteUser(id);
return "사용자 삭제 완료!";
}
● OPTIONS: 통신옵션 조회 (리소스가 지원하고 있는 메소드의 취득)
● HEAD: 응답헤더 조회(메타 데이터의 취득)

- 간단한 API 기능 2개 구상해오기 (ex: 사용자 조회, 게시글 작성 등)
1. 도서 추가 (POST /books)
POST /books
{
"title": "자바의 정석",
"author": "남궁성",
"price": 25000,
"category": "Programming"
}
2. 도서 목록 조회 (GET /books)
GET /books?category=Programming&price_max=30000
[
{
"id": 1,
"title": "자바의 정석",
"author": "남궁성",
"price": 25000,
"category": "Programming"
},
{
"id": 2,
"title": "파이썬 완벽 가이드",
"author": "홍길동",
"price": 20000,
"category": "Programming"
}
]
💡 새롭게 알게 된 점 & 어려웠던 부분
- 새롭게 알게된 점: API 개념을 다시 정확하게 알게 되었고, HTTP 메서드와 API 기능에 대해 더 깊게 탐구하고 싶어졌습니다.
- 어려웠던 부분: HTTP 메서드와 API 기능을 더 깊게 이해하는 부분이 어려웠습니다.
- 해결 방법: HTTP 메서드와 API 기능에 대해 더 많은 예제와 실습을 통해 이해를 돕고, 구체적인 동작 방식을 연구해볼 계획입니다.
🚀 과제/실습 결과
- Spring Boot
- Gradle
- Jsp templeate
- H2 데이터베이스
- JDK 17
### 🛠️설정
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.4'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test {
useJUnitPlatform()
}
# H2 메모리 데이터베이스 설정
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# H2 콘솔 사용 설정
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# 초기 SQL 실행
spring.sql.init.mode=always
- schema 설정
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
CREATE TABLE posts (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200),
content TEXT,
user_id BIGINT,
FOREIGN KEY (user_id) REFERENCES users(id)
);
사용자조회 기능

1. Model
package com.example.userapi.model;
public class User {
private Long id;
private String name;
private String email;
public User() {} // Spring이 객체를 만들 때 기본 생성자를 사용
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// Getter / Setter
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
- id
- name
2. Controller
package com.example.userapi.controller;
import com.example.userapi.model.User;
import com.example.userapi.service.UserService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService service;
public UserController(UserService service) {
this.service = service;
}
@GetMapping("/")
public List<User> getAllUsers() {
return service.getAllUsers(); // 모든 사용자 목록 반환
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return service.getUserById(id);
}
@PostMapping
public String createUser(@RequestBody User user) {
service.createUser(user);
return "사용자가 저장되었습니다.";
}
@PutMapping("/{id}")
public String updateUser(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
service.updateUser(user);
return "사용자 전체 수정 완료!";
}
@PatchMapping("/{id}")
public String partialUpdateUser(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
service.partialUpdateUser(id, updates);
return "사용자 일부 수정 완료!";
}
@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Long id) {
service.deleteUser(id);
return "사용자 삭제 완료!";
}
}
3. Service
package com.example.userapi.service;
import com.example.userapi.model.User;
import com.example.userapi.repository.UserRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
public class UserService {
private final UserRepository repo;
public UserService(UserRepository repo) {
this.repo = repo;
}
public void createUser(User user) {
repo.save(user);
}
public List<User> getAllUsers() {
return repo.findAll(); // 모든 사용자 목록 반환
}
public User getUserById(Long id) {
return repo.findById(id)
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
}
public void updateUser(User user) {
repo.update(user);
}
public void partialUpdateUser(Long id, Map<String, Object> updates) {
User user = getUserById(id);
if (updates.containsKey("name")) {
user.setName((String) updates.get("name"));
}
if (updates.containsKey("email")) {
user.setEmail((String) updates.get("email"));
}
repo.update(user);
}
public void deleteUser(Long id) {
repo.deleteById(id);
}
}
4. Repository
package com.example.userapi.repository;
import com.example.userapi.model.User;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public class UserRepository {
private final JdbcTemplate jdbc;
public UserRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
public void save(User user) {
jdbc.update("INSERT INTO users (name, email) VALUES (?, ?)",
user.getName(), user.getEmail());
}
public List<User> findAll() {
return jdbc.query("SELECT * FROM users", userRowMapper());
}
public Optional<User> findById(Long id) {
// jdbc.query()를 통해 결과를 리스트로 받고, 그 리스트에서 첫 번째 값을 Optional로 반환
List<User> result = jdbc.query("SELECT * FROM users WHERE id = ?",
new Object[]{id},
userRowMapper());
return result.stream().findAny(); // 결과가 있으면 첫 번째 항목을 Optional로 반환
}
public void update(User user) {
jdbc.update("UPDATE users SET name = ?, email = ? WHERE id = ?",
user.getName(), user.getEmail(), user.getId());
}
public void deleteById(Long id) {
jdbc.update("DELETE FROM users WHERE id = ?", id);
}
private RowMapper<User> userRowMapper() {
return (rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email")
);
}
}
게시글 게시 기능

1. Model
package com.example.userapi.model;
public class Post {
private Long id;
private String title;
private String content;
private Long userId; // 작성자 ID
public Post() {}
public Post(Long id, String title, String content, Long userId) {
this.id = id;
this.title = title;
this.content = content;
this.userId = userId;
}
// Getter/Setter
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
}
- id
- title
- content
- userId
2. Controller
package com.example.userapi.controller;
import com.example.userapi.model.Post;
import com.example.userapi.service.PostService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/posts")
public class PostController {
private final PostService service;
public PostController(PostService service) {
this.service = service;
}
@PostMapping
public String createPost(@RequestBody Post post) {
service.createPost(post);
return "게시글이 저장되었습니다.";
}
@GetMapping("/{id}")
public Post getPost(@PathVariable Long id) {
return service.getPost(id);
}
}
3. Service
package com.example.userapi.service;
import com.example.userapi.model.Post;
import com.example.userapi.repository.PostRepository;
import com.example.userapi.repository.UserRepository;
import org.springframework.stereotype.Service;
@Service
public class PostService {
private final PostRepository postRepo;
private final UserRepository userRepo;
public PostService(PostRepository postRepo, UserRepository userRepo) {
this.postRepo = postRepo;
this.userRepo = userRepo;
}
public void createPost(Post post) {
// 작성자 존재 여부 확인 (옵션)
userRepo.findById(post.getUserId())
.orElseThrow(() -> new RuntimeException("작성자(User)를 찾을 수 없습니다."));
postRepo.save(post);
}
public Post getPost(Long id) {
return postRepo.findById(id)
.orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없습니다."));
}
}
4. Repository
package com.example.userapi.repository;
import com.example.userapi.model.Post;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public class PostRepository {
private final JdbcTemplate jdbc;
public PostRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
public void save(Post post) {
jdbc.update("INSERT INTO posts (title, content, user_id) VALUES (?, ?, ?)",
post.getTitle(), post.getContent(), post.getUserId());
}
public Optional<Post> findById(Long id) {
return jdbc.query("SELECT * FROM posts WHERE id = ?",
new Object[]{id},
rs -> {
if (rs.next()) {
return Optional.of(new Post(
rs.getLong("id"),
rs.getString("title"),
rs.getString("content"),
rs.getLong("user_id")
));
} else {
return Optional.empty();
}
});
}
}
<사용자 조회, 게시글 플로우차트>

👥 스터디 피드백
- 받은 피드백:
- REST API 설계에서 가장 중요한 원칙은 무엇인가요?
일관성이 가장 중요하며, URL, 메서드, 응답 형식 등을 일관되게 설계하면 개발자들이 API를 쉽게 사용할 수 있습니다. - HTTP 상태 코드의 의미와 중요성
상태 코드는 요청 결과를 나타내며, 404 Not Found, 500 Internal Server Error 등의 코드가 오류를 파악하고 디버깅에 중요합니다. 각 코드의 숫자는 상태를 쉽게 파악할 수 있기 때문입니다. - API 성능 최적화 방법
캐싱과 상태 코드 출력을 통해 응답 속도를 높이고, 쿼리 최적화를 통해 성능을 개선할 수 있습니다.