Spring Boot ์ผ์ ๊ด๋ฆฌ API ๊ณผ์ ์ ๋ฆฌ (Step0 ~ Step4)
์ด๋ฒ ๊ณผ์ ๋ Spring Boot + JPA + MySQL๋ก ์ผ์ (Schedule) CRUD API๋ฅผ ๋ง๋๋ ๊ฒ์ด ๋ชฉํ์๋ค.
์ฒ์์๋ ํ๋ก์ ํธ ๊ตฌ์กฐ๋ถํฐ ํท๊ฐ๋ ธ๊ณ , ์์กด์ฑ/DB ์ฐ๊ฒฐ/DTO/์์ธ ์ฒ๋ฆฌ๊น์ง ํ ๋ฒ์ ํ๋ ค๋๊น ๊ณ์ ๋งํ๋ค.
๊ทธ๋์ Step์ ๋๋ ์ “ํ๋์ฉ ๊ตฌํ -> Postman์ผ๋ก ํ์ธ -> ๋ค์ ๋จ๊ณ” ๋ฐฉ์์ผ๋ก ์งํํ๋ค.
๊ฐ๋ฐ ํ๊ฒฝ
- Java 17
- Spring Boot
- Spring Data JPA
- MySQL
- Gradle
- Postman
Step0. API ๋ช ์ธ ๋ฐ ERD ์์ฑ
๊ณผ์ ์์ ์ ์ ๋จผ์ ๋ฌด์จ API๋ฅผ ๋ง๋ค ๊ฑด์ง๋ถํฐ ์ ๋ฆฌํ๋ค.
์ฒ์์ ๊ตฌํ๋ถํฐ ๋ค์ด๊ฐ๊ณ ์ถ์๋๋ฐ, API์ DB ๊ตฌ์กฐ๋ฅผ ๋จผ์ ์ก์๋๋๊น ์ค๊ฐ์ ๋ ํ๋ค๋ ธ๋ค.
ERD
Schedule ๋จ์ผ ์ํฐํฐ๋ก ์งํํ๋ค.
์ปฌ๋ผ๋ช ํ์ ์ค๋ช
| id | BIGINT | PK, ์๋ ์ฆ๊ฐ |
| title | VARCHAR | ์ผ์ ์ ๋ชฉ |
| content | VARCHAR | ์ผ์ ๋ด์ฉ |
| author | VARCHAR | ์์ฑ์ |
| password | VARCHAR | ์์ /์ญ์ ๊ฒ์ฆ์ฉ ๋น๋ฐ๋ฒํธ |
API ๋ช ์ธ
์์ฒญ/์๋ต์ JSON ํํ๋ก ํต์ผํ๊ณ , ์๋ต์๋ ๋น๋ฐ๋ฒํธ๋ฅผ ํฌํจํ์ง ์๋๋ก DTO๋ก ๋ถ๋ฆฌํ๋ค.
1) ์ผ์ ์์ฑ
- Method: POST
- URL: /api/schedules
Request
{
"title": "์ฒซ ์ผ์ ",
"content": "๊ณต๋ถํ๊ธฐ",
"author": "์ง๋ฏผ",
"password": "1234"
}
Response
{
"id": 1,
"title": "์ฒซ ์ผ์ ",
"content": "๊ณต๋ถํ๊ธฐ",
"author": "์ง๋ฏผ"
}
2) ์ ์ฒด ์ผ์ ์กฐํ
- Method: GET
- URL: /api/schedules
Response
[
{
"id": 1,
"title": "์ฒซ ์ผ์ ",
"content": "๊ณต๋ถํ๊ธฐ",
"author": "์ง๋ฏผ"
}
]
3) ๋จ๊ฑด ์ผ์ ์กฐํ
- Method: GET
- URL: /api/schedules/{id}
์ฑ๊ณต Response (200)
{
"id": 1,
"title": "์ฒซ ์ผ์ ",
"content": "๊ณต๋ถํ๊ธฐ",
"author": "์ง๋ฏผ"
}
์คํจ Response (404)
ํด๋น ์ผ์ ์ด ์์ต๋๋ค.
4) ์ผ์ ์์
- Method: PUT
- URL: /api/schedules/{id}
Request
{
"title": "์์ ํ ์ ๋ชฉ",
"content": "์์ ํ ๋ด์ฉ",
"password": "1234"
}
Response
{
"id": 1,
"title": "์์ ํ ์ ๋ชฉ",
"content": "์์ ํ ๋ด์ฉ",
"author": "์ง๋ฏผ"
}
5) ์ผ์ ์ญ์
- Method: DELETE
- URL: /api/schedules/{id}
Request
{
"password": "1234"
}
Response
์ญ์ ์๋ฃ
Step1. ์ผ์ ์์ฑ (Create)
Step1 ๋ชฉํ๋ DB์ ์ ์ฅ๋๊ณ , Postman์์ ์๋ต์ด ์ค๋ ๊ฒ์ด์๋ค.
์ฌ๊ธฐ์ ๊ฐ์ฅ ๋จผ์ ๋งํ๋ ๊ฑด ์์กด์ฑ ์ค์ ๊ณผ MySQL ์ฐ๊ฒฐ์ด์๋ค.
1) ์์กด์ฑ ์ค์ (Gradle)
์ฒ์์๋ spring-boot-starter๋ง ๋ค์ด์์ด์ Web/JPA/MySQL์ด ์ ๋ถ ์ ๋๋ค.
๊ทธ๋์ ์๋์ฒ๋ผ web + jpa + mysql connector + lombok์ ์ถ๊ฐํ๋ค.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
์ด ๋จ๊ณ์์ ์ค์ํ ํฌ์ธํธ๋ Gradle Reload์๋ค.
build.gradle์ ์์ ํด๋ ๋ฆฌ๋ก๋๋ฅผ ์ ํ๋ฉด ๊ณ์ ๋นจ๊ฐ ์ค์ด ๋จ์ ์์๋ค.
2) MySQL ์ค์ (application.yml)
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/scheduleapp?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: root
password: ๋น๋ฐ๋ฒํธ
jpa:
hibernate:
ddl-auto: update
show-sql: true
MySQL์์ DB๊ฐ ์์ผ๋ฉด ์ฐ๊ฒฐ์ด ์ ๋๊ธฐ ๋๋ฌธ์ ๋จผ์ ์์ฑํ๋ค.
CREATE DATABASE scheduleapp;
์๋ฒ ์คํ ์ ์ฝ์์ create table schedules ...๊ฐ ์ฐํ๋ฉด ์ฑ๊ณต์ด๋ค.
3) Entity ์์ฑ (Schedule)
Step1์์๋ createdAt/updatedAt์ ์์ง ๋ถ์ด์ง ์๊ณ ํ๋๋ง ๋จผ์ ๋ง๋ค์๋ค.
@Entity
@Table(name = "schedules")
@Getter
@NoArgsConstructor
public class Schedule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String content;
@Column(nullable = false)
private String author;
@Column(nullable = false)
private String password;
public Schedule(String title, String content, String author, String password) {
this.title = title;
this.content = content;
this.author = author;
this.password = password;
}
public void update(String title, String content) {
this.title = title;
this.content = content;
}
}
์ฌ๊ธฐ์ ์ค์ํ๋ ๋ถ๋ถ:
- ํด๋์ค๋ช /ํ์ผ๋ช ์ ์๋ฌธ์๋ก ๋ง๋ค์ด์ ์ธ์์ด ์ด์ํด์ก๋ ์ ์ด ์์๋ค.
- lombok์ด ์ค์น๋์ด ์๋๋ฐ๋ ๋นจ๊ฐ ์ค์ด ์ฌ๋ผ์ง์ง ์์ annotation processing์ ์ผ์ผ ํ๋ค.
4) Repository ์์ฑ
public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
}
5) ์ผ์ ์์ฑ API (POST)
์ด๊ธฐ์๋ ์ํฐํฐ๋ฅผ ๊ทธ๋๋ก ์๋ต์ผ๋ก ๋ด๋ณด๋ด์ password๊ฐ ๋
ธ์ถ๋๋ค.
๊ณผ์ ์กฐ๊ฑด์ด ์๋ต์์ ๋น๋ฐ๋ฒํธ ์ ์ธ๋ผ์ Response DTO๋ฅผ ๋ง๋ค์๋ค.
Response DTO
@Getter
public class ScheduleResponseDto {
private Long id;
private String title;
private String content;
private String author;
public ScheduleResponseDto(Schedule schedule) {
this.id = schedule.getId();
this.title = schedule.getTitle();
this.content = schedule.getContent();
this.author = schedule.getAuthor();
}
}
Controller (POST)
@PostMapping
public ResponseEntity<ScheduleResponseDto> create(@RequestBody Schedule request) {
Schedule saved = scheduleRepository.save(
new Schedule(
request.getTitle(),
request.getContent(),
request.getAuthor(),
request.getPassword()
)
);
return ResponseEntity.ok(new ScheduleResponseDto(saved));
}

Step2. ์ผ์ ์กฐํ (Read)
Step2๋ ์ ์ฒด ์กฐํ์ ๋จ๊ฑด ์กฐํ๋ฅผ ๊ตฌํํ๋ค.
1) ์ ์ฒด ์ผ์ ์กฐํ (GET /api/schedules)
@GetMapping
public ResponseEntity<List<ScheduleResponseDto>> findAll() {
List<Schedule> schedules = scheduleRepository.findAll();
List<ScheduleResponseDto> response = schedules.stream()
.map(ScheduleResponseDto::new)
.toList();
return ResponseEntity.ok(response);
}
์ฌ๊ธฐ์ stream์ ์ด ์ด์ :
- entity ๋ฆฌ์คํธ๋ฅผ DTO ๋ฆฌ์คํธ๋ก ๋ณํํ๋ ๋ก์ง์ด ๊น๋ํด์ง๋ค.
- ๋ฐ๋ณต๋ฌธ์ผ๋ก ํด๋ ๋์ง๋ง, ๋ณํ ๋ชฉ์ ์ด ๋ช ํํ๊ฒ ๋ณด์๋ค.

2) ๋จ๊ฑด ์ผ์ ์กฐํ (GET /api/schedules/{id})
์ฒ์์๋ orElseThrow()๋ก ์์ธ๋ฅผ ๋์ก๋๋ฐ, ์ญ์ ๋ id ์กฐํ ์ 500์ด ๋ ์ ๋ณ๋ก์๋ค.
์๋ ์์์ ํด๋ผ์ด์ธํธ ์์ฒญ์ด ํ๋ฆฐ ๊ฒ์ด๋ฏ๋ก 404๊ฐ ์์ฐ์ค๋ฝ๋ค๊ณ ํ๋จํด์ ์๋ต ์ฝ๋๋ฅผ ์ง์ ๋ถ๊ธฐํ๋ค.
@GetMapping("/{id}")
public ResponseEntity<?> findOne(@PathVariable Long id) {
return scheduleRepository.findById(id)
.<ResponseEntity<?>>map(schedule ->
ResponseEntity.ok(new ScheduleResponseDto(schedule))
)
.orElseGet(() ->
ResponseEntity.status(404).body("ํด๋น ์ผ์ ์ด ์์ต๋๋ค.")
);
}
์ด๋ ์๋ฐ ์ ๋ค๋ฆญ ํ์
์ถ๋ก ๋๋ฌธ์ ์ปดํ์ผ ์๋ฌ๊ฐ ๋ฌ๋๋ฐ,
.<ResponseEntity<?>>map(...) ํํ๋ก ๋ฐํ ํ์
์ ๊ณ ์ ํด์ ํด๊ฒฐํ๋ค.

Step3. ์ผ์ ์์ (Update)
์์ ์ ๋น๋ฐ๋ฒํธ๊ฐ ๋ง์ ๋๋ง ์์ ์ด ํต์ฌ์ด์๋ค.
์์ ์์ฒญ DTO
@Getter
@NoArgsConstructor
public class ScheduleUpdateRequestDto {
private String title;
private String content;
private String password;
}
์์ API (PUT /api/schedules/{id})
@PutMapping("/{id}")
public ResponseEntity<ScheduleResponseDto> update(
@PathVariable Long id,
@RequestBody ScheduleUpdateRequestDto request
) {
Schedule schedule = scheduleRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("ํด๋น ์ผ์ ์ด ์์ต๋๋ค."));
if (!schedule.getPassword().equals(request.getPassword())) {
throw new IllegalArgumentException("๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.");
}
schedule.update(request.getTitle(), request.getContent());
Schedule saved = scheduleRepository.save(schedule);
return ResponseEntity.ok(new ScheduleResponseDto(saved));
}
- ์์ ์ ์ ๋ฐ๋์ ๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ค.
- ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ์ ํต๊ณผํด์ผ update ๋ฉ์๋๊ฐ ์คํ๋๋ค.
- ์๋ต์ DTO๋ก ๋ฐํํด์ password ๋ ธ์ถ์ ๋ง๋๋ค.

Step4. ์ผ์ ์ญ์ (Delete)
์ญ์ ๋ ์์ ๊ณผ ๋๊ฐ์ด ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ์ด ํต์ฌ์ด์๋ค.
์ญ์ ์์ฒญ DTO
@Getter
@NoArgsConstructor
public class ScheduleDeleteRequestDto {
private String password;
}
์ญ์ API (DELETE /api/schedules/{id})
@DeleteMapping("/{id}")
public ResponseEntity<String> delete(
@PathVariable Long id,
@RequestBody ScheduleDeleteRequestDto request
) {
Schedule schedule = scheduleRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("ํด๋น ์ผ์ ์ด ์์ต๋๋ค."));
if (!schedule.getPassword().equals(request.getPassword())) {
throw new IllegalArgumentException("๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.");
}
scheduleRepository.delete(schedule);
return ResponseEntity.ok("์ญ์ ์๋ฃ");
}
์ญ์ ํ ๋จ๊ฑด ์กฐํ ํ์ธ
์ญ์ ํ์ ๊ฐ์ id๋ฅผ ์กฐํํ์ ๋ 404๊ฐ ๋ด๋ ค์ค๋์ง ํ์ธํ๋ค.


ํธ๋ฌ๋ธ์ํ
1) Lombok ๋นจ๊ฐ ์ค์ด ์ ์์ด์ง
- build.gradle์ Lombok ์์กด์ฑ์ ์ถ๊ฐํด๋ ๋นจ๊ฐ ์ค์ด ๊ณ์ ๋จ์๋ค.
- IntelliJ์์ annotation processing์ ์ผ๊ณ , Lombok ํ๋ฌ๊ทธ์ธ์ ํ์ฑํํ๋ ํด๊ฒฐ๋๋ค.
2) ์์กด์ฑ์ด ๋ถ์กฑํด์ JPA/Web์ด ๋์ํ์ง ์์
- ์ฒ์์ spring-boot-starter๋ง ์์ด์ @Entity, JpaRepository, Controller๊ฐ ์ ๋ถ ์ ๋๋ก ๋์ํ์ง ์์๋ค.
- starter-web, data-jpa, mysql-connector ์ถ๊ฐ ํ ํด๊ฒฐ.
3) ์ญ์ ๋ id ์กฐํ ์ 500 ์๋ต
- orElseThrow() ์์ธ๊ฐ ๊ทธ๋๋ก ์ฌ๋ผ๊ฐ๋ฉด์ 500์ด ๋ด๋ค.
- ์๋ ์์์ 404๊ฐ ๋ง๊ธฐ ๋๋ฌธ์, ResponseEntity๋ก 404๋ฅผ ์ง์ ๋ด๋ ค์ฃผ๋๋ก ์์ ํ๋ค.
ํ๊ณ
์ฒ์์ CRUD๋๊น ๊ธ๋ฐฉ ๋๋๊ฒ ์ง๋ผ๊ณ ์๊ฐํ๋๋ฐ, ์ค์ ๋ก ํด๋ณด๋ ๊ตฌํ ์์ฒด๋ณด๋ค ํ๊ฒฝ ์ค์ ๊ณผ ํ๋ฆ ์ดํด์์ ์๊ฐ์ด ๋ง์ด ๊ฑธ๋ ธ๋ค.
ํนํ DTO๋ฅผ ์ ์จ์ผ ํ๋์ง, ์์ธ๋ฅผ ๋์ง๋ฉด ์ 500์ด ๋๋์ง ๊ฐ์ ๋ถ๋ถ์ ์ง์ ๋ถ๋ชํ๋ด์ผ ๊ฐ์ด ์๋ค.
๋, Postman์ผ๋ก ๋งค ๋จ๊ณ๋ง๋ค ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ ๊ฒ์ด ๋์์ด ๋ง์ด ๋๋ค.
์ฝ๋๋ง ๋ณด๊ณ ๋ ๊ฒ ๊ฐ์๋ฐ?๊ฐ ์๋๋ผ ์ค์ ์์ฒญ/์๋ต์ ๋ณด๋ฉด์ ์ด๋๊ฐ ์๋ชป๋๋์ง ๋ฐ๋ก ํ๋จํ ์ ์์๋ค.
์ด๋ฒ ๊ณผ์ ๋ฅผ ์งํํ๋ฉด์ ๋
๊ฐ ์ด์๋ก ๋ชธ ์ํ๊ฐ ์ข์ง ์์ ๋์ ๊ณผ์ ๊น์ง ์์ฑํ ํ ์ ์ถํ์ง ๋ชปํด ์์ฌ์์ด ๋ง์ด ๋จ๋๋ค.
์๊ฐ ๋ด์ ๋์ ๊ณผ์ ๊น์ง ์ ์ถ์ ๋ชปํ์์ง๋ง ๋ฐ๋ก ๊ฐ์ธ ํ์ต์๊ฐ์ ๊ณ์ ํด๋ณผ ์๊ฐ์ด๋ค.