Spring Boot 프로젝트를 생성합니다.
Spring Initializr 사용 (https://start.spring.io)
Generate 버튼을 눌러 프로젝트를 다운로드하고, IntelliJ IDEA에서 열어줍니다.
src/main/java/projectname/app
├── app.java // 메인 클래스
└── controller // API 요청을 처리할 컨트롤러
└── service // 비즈니스 로직을 처리할 서비스
└── repository // 데이터베이스와 연동
└── domain // 데이터 구조를 정의할 모델

먼저 model 클래스를 생성합니다. (저는 할 일을 구현하기위해 Task 객체를 생성했습니다.)
package com.example.techeer_partners_api_session.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Task {
@Id // 데이터베이스 테이블의 기본키임을 나타냄.
@GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 생성 ID
private Long id;
private String title; // 할 일 내용
private Boolean isDone = false; // 기본값 : false
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 void setIsDone(Boolean isDone) {
this.isDone = isDone;
}
public Boolean getIsDone() {
return isDone;
}
}
데이터베이스와 통신하기 위해 TaskRepository 클래스를 생성합니다.
package com.example.techeer_partners_api_session.TaskRepository;
import com.example.techeer_partners_api_session.domain.Task;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface TaskRepository extends JpaRepository<Task, Long> {
// 기본적인 CRUD 메서드는 JpaRepository에서 제공
// 추가적인 쿼리 메서드는 필요에 따라 선언할 수 있음
// 완료된 할 일만 조회
List<Task> findByIsDoneTrue(); // isDone == true인 할 일 목록만 조회하는 메서드입니다.
// 미완료된 할 일만 조회
List<Task> findByIsDoneFalse(); // isDone == false인 할 일 목록만 조회하는 메서드입니다.
}
참고 : @Repository 어노테이션을 TaskRepository에 명시하지 않은 이유는 JpaRepository가 이미 @Repository를 포함하고 있기 때문입니다.
주의 : JpaRepository로 쉽게 코드를 짤 수 있지만, JpaRepository의 기능에 대해 자세히 알고 사용하는 것이 좋다.
다음 단계로 넘어가기 전에, JpaRepository를 짧게 설명하고 넘어가겠습니다.
JpaRepository는 CrudRepository와 PagingAndSortingRepository를 상속받아, 다음과 같은 주요 기능을 제공합니다:
기본적인 CRUD (Create, Read, Update, Delete) 기능:
페이징과 정렬:
Spring Data JPA의 상속 구조
JpaRepository는 CrudRepository와 PagingAndSortingRepository를 상속받고, CrudRepository는 다시 Repository 인터페이스를 상속합니다.
Repository (기본 인터페이스)
↓
CrudRepository (CRUD 메서드 제공)
↓
PagingAndSortingRepository (페이징/정렬 메서드 제공)
↓
JpaRepository (JPA 관련 메서드 제공)
비즈니스 로직(예: 데이터 저장)을 처리할 서비스 클래스입니다.
package com.example.techeer_partners_api_session.TaskService;
import com.example.techeer_partners_api_session.TaskRepository.TaskRepository;
import com.example.techeer_partners_api_session.domain.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class TaskService {
private final TaskRepository taskRepository;
@Autowired
//생성자 주입
public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository; // 초기화
}
// Task 생성
public Task createTask(Task task) { // 단일 객체로 반환
return taskRepository.save(task);
}
// 모든 Task 조회
public List<Task> getAllTasks() { // 리스트로 반환
return taskRepository.findAll();
}
// 완료된 할 일 조회
public List<Task> getCompletedTasks() {
return taskRepository.findByIsDoneTrue();
}
// 미완료된 할 일 조회
public List<Task> getIncompleteTasks() {
return taskRepository.findByIsDoneFalse();
}
// ID로 특정 Task 일부 수정
public Task partialUpdateTask(Long id, Map<String, Object> updates) {
Task existingTask = taskRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Task not found"));
if (updates.containsKey("title")) { // 요청에서 title 필드가 포함되어 있는지 확인.
existingTask.setTitle((String) updates.get("title"));
}
if (updates.containsKey("isDone")) { // 요청에 isDone 필드가 포함되어 있는지 확인.
existingTask.setIsDone((Boolean) updates.get("isDone"));
}
return taskRepository.save(existingTask); // 업데이트된 Task 저장
}
// ID로 특정 Task 삭제
public void deleteTask(Long id) { // 반환 값 필요 없음.
taskRepository.deleteById(id);
}
}
- final란? :
final 키워드가 붙은 필드는 초기화 이후 값이 변경되지 않음을 보장합니다.
public Task partialUpdateTask(Long id, Map<String, Object> updates) {
Task existingTask = taskRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Task not found"));
- Map<String, Object>란?:
데이터를 키-값 쌍으로 저장하는 자료구조이며 동적으로 업데이트할 필드를 처리하는 데 유용합니다.
String (키의 타입): 맵의 키는 항상 String 타입입니다. 이는 updates 맵에 저장된 각 항목이 어떤 필드를 업데이트하려는지를 나타냅니다.
Object (값의 타입): 맵의 값은 Object 타입입니다. 이는 해당 필드를 어떤 값으로 수정할 것인지를 나타내며, 모든 객체를 저장할 수 있다는 의미입니다. 예를 들어, String, Integer, Boolean, Date 등 다양한 타입의 값을 저장할 수 있습니다
updates는 그냥 Map의 이름(변경 가능)
- taskRepository.findById(id).orElseThrow()란?
Optional을 사용하여 값이 없을 경우 예외를 발생시키는 방법으로, 예외 처리 흐름을 명확히 할 수 있는 유용한 메서드입니다.
findById에서 데이터베이스에 값이 없다면 Optional.empty()를 반환하는데 이걸 받으면 .orElseThrow()가 예외를 발생시켜 RuntimeException을 던짐.
-> new RuntimeException("Task not found") : 메시지는 "Task not found". 출력
if (updates.containsKey("title")) { // 요청에서 title 필드가 포함되어 있는지 확인.
existingTask.setTitle((String) updates.get("title"));
}
if (updates.containsKey("isDone")) { // 요청에 isDone 필드가 포함되어 있는지 확인.
existingTask.setIsDone((Boolean) updates.get("isDone"));
}
나머지 코드 설명은 코드에 주석으로 처리하였습니다.
HTTP 요청을 받아 서비스 계층을 호출하여 처리 결과를 클라이언트에 반환하는 클래스입니다.
package com.example.techeer_partners_api_session.Controller;
import com.example.techeer_partners_api_session.TaskService.TaskService;
import com.example.techeer_partners_api_session.domain.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/tasks")
public class TaskController {
private final TaskService taskService;
@Autowired
// 생성자 주입
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
// Task 생성
@PostMapping // POST 요청이 오면 해당 메서드 실행
public ResponseEntity<Map<String, Object>> createTask(@RequestBody Task task) {
Task createdTask = taskService.createTask(task);
// 응답 메시지 구성
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", "success");
response.put("message", "할 일이 생성되었습니다.");
response.put("data", null);
// 201 Created 상태 코드와 함께 응답 반환
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
// 모든 Task 조회
@GetMapping // GET 요청이 오면 해당 메서드 실행
public ResponseEntity<Map<String, Object>> getAllTasks() {
List<Task> tasks = taskService.getAllTasks();
// 응답 데이터 구성
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", "success");
response.put("message", "모든 일이 조회되었습니다.");
response.put("data", tasks);
// 200 OK 상태 코드와 함께 반환
return ResponseEntity.ok(response);
}
// 완료된 Task 조회
@GetMapping("/completed") // /tasks/completed로 GET 요청이 들어오면 해당 메서드 호출
public ResponseEntity<Map<String, Object>> getCompletedTasks() {
List<Task> completedTasks = taskService.getCompletedTasks();
// 응답 데이터 구성
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", "success");
response.put("message", "완료 된 일이 조회되었습니다.");
response.put("data", completedTasks);
// 200 OK 상태 코드와 함께 반환
return ResponseEntity.ok(response);
}
// 미완료된 Task 조회
@GetMapping("/incomplete") // /tasks/incomplete로 GET 요청이 들어오면 해당 메서드 호출
public ResponseEntity<Map<String, Object>> getIncompleteTasks() {
List<Task> incompleteTasks = taskService.getIncompleteTasks();
// 응답 데이터 구성
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", "success");
response.put("message", "미완료 된 일이 조회되었습니다.");
response.put("data", incompleteTasks);
// 200 OK 상태 코드와 함께 반환
return ResponseEntity.ok(response);
}
// Task 수정
@PatchMapping("/{id}") // /tasks/{id}로 PATCH 요청이 들어오면 해당 메서드 호출
public ResponseEntity<Map<String, Object>> partialUpdateTask(
@PathVariable Long id,
@RequestBody Map<String, Object> updates) {
taskService.partialUpdateTask(id, updates);
// 응답 메시지 구성
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", "success");
response.put("message", "할 일이 수정되었습니다.");
response.put("data", null);
// 200 OK 상태 코드와 함께 응답 메시지 반환
return ResponseEntity.ok(response);
}
// Task 삭제
@DeleteMapping("/{id}") // /tasks/{id}로 DELETE 요청이 들어오면 해당 메서드 호출
public ResponseEntity<Map<String, Object>> deleteTask(@PathVariable Long id) {
taskService.deleteTask(id);
// 응답 메시지 구성
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", "success");
response.put("message", "할 일이 삭제되었습니다.");
response.put("data", null);
// 200 OK 상태 코드와 함께 응답 메시지 반환
return ResponseEntity.ok(response);
}
}
- @RestController란?
@Controller와 @ResponseBody를 결합한 형태로, 메서드의 반환 값을 JSON 또는 XML 형태로 반환합니다.
REST API의 엔드포인트를 구현하는 클래스에 사용합니다.
- @RequestMapping()란?
클래스 또는 메서드에 매핑되는 URL 경로를 정의합니다.
이 경우, /tasks로 시작하는 모든 요청은 이 컨트롤러에서 처리합니다.
- @RequsetBody란?
{
"title": "Learn Spring Boot",
"description": "Study the basics of Spring Boot"
}
Task task = new Task("Learn Spring Boot", "Study the basics of Spring Boot");
- @PathVariable란?
URL 경로에서 값을 추출하는 데 사용됩니다.
이 경우, /tasks/{id}로 들어오는 요청에서 {id} 값을 메서드 파라미터로 매핑합니다.
(예: /tasks/5 → id = 5)
나머지 Annotation은 위 코드 내에 주석처리 하였습니다.
- ResponseEntity란?
return ResponseEntity.ok(createdTask); // HTTP 200 + 데이터
return ResponseEntity.noContent().build(); // HTTP 204 (No Content)
return ResponseEntity.notFound().build(); // HTTP 404 (Not Found)
@GetMapping // GET 요청이 오면 해당 메서드 실행
public ResponseEntity<Map<String, Object>> getAllTasks() {
List<Task> tasks = taskService.getAllTasks();
// 응답 데이터 구성
Map<String, Object> response = new HashMap<>();
response.put("status", "success");
response.put("message", "모든 일이 조회되었습니다.");
response.put("data", tasks);
// 200 OK 상태 코드와 함께 반환
return ResponseEntity.ok(response);
}
<Map<String, Object>> 타입을 맞춰줍니다.
LinkedHashMap<>을 사용하여 응답 메시지의 데이터를 저장할 공간을 생성하면서 입력된 순서대로 키를 유지합니다.
response.put(key, value)로 저장
ResponseEntity.ok(response)로 200 ok과 함께 response를 반환
- 수정 할 때 PUT vs PATCH
특징 : PUT ㅣ PATCH
사용 목적 : 전체 리소스를 교체 ㅣ 리소스의 일부를 수정
전송 데이터 : 리소스 전체 데이터 필요 ㅣ 수정하려는 데이터만 필요
누락된 필드 : 기본값(null)로 초기화 ㅣ 기존 값 유지
멱등성 보장 : 보장 ㅣ 구현에 따라 다름
HTTP 요청 크기 : 크기가 클 수 있음 ㅣ 크기가 상대적으로 작음
나머지 코드 해석은 위 코드 내에 주석처리 하였습니다.






아직 많이 부족 부분도 많지만 최대한 공부하면서 실습한 내용을 정리해 보았습니다. 감사합니다.