본문 바로가기
쿤즈 프로젝트/To-do List Application

[Spring Boot] To-Do List 애플리케이션: Chap8. API 계층별 구현 (4) 할 일 수정

by Koonz:) 2024. 8. 5.
728x90

앞선 글에서는 API 계층별 구현에서는 새로운 할 일을 저장하고 저장된 할 일의 전체 리스트와 특정 할 일을 조회하는 API를 구현해 보았습니다.

 

이번 글에서는 To-Do List 애플리케이션의 기존 할 일을 수정하는 API를 구현하는 방법에 대해 설명합니다. 이 API는 클라이언트가 기존의 할 일을 조회하고 조회된 할 일을 수정할 수 있도록 합니다.


프로젝트 구조 설정

프로적테의 기본 구조는 다음과 같습니다. 현재까지 구성되어 있는 레이어에 기능들을 추가해 보도록 할게요.

src/main/java
└── com.koonsland.todo
    ├── controller
    │   └── ToDoItemController.java
    ├── model
    │   └── ToDoItem.java
    ├── repository
    │   └── ToDoItemRepository.java
    ├── service
    │   └── ToDoItemService.java
    └── TodoApplication.java

 

LAYERED
LAYERED
Controller
Controller
Service
Service
Model
Model
Database
Database
Repository
Repository
Text is not SVG - cannot display

 

Controller: 클라이언트의 요청을 처리하고 응답을 반환하는 컨트롤러 클래스가 위치합니다. 실제 요청을 받을 수 있는 API 클래스들을 만들 수 있습니다.

Service: 비즈니스 로직을 구현하는 서비스 클래스가 위치합니다. 비즈니스 로직은 데이터를 저장, 조회, 수정, 삭제 기능을 호출하는 로직들을 담는 클래스들입니다.

Repository: 데이터베스와의 상호작용을 처리하는 Repository 인터페이스가 위치합니다.

Model: 데이터베이스 테이블과 매핑되는 엔티티 클래스가 위치합니다.


할 일 수정 API 구현

할 일 수정 API는 수정하고자 하는 할 일에 대한 정보를 클라이언트로부터 받습니다. 받은 데이터에 대해 유효성 체크를 진행하고 이상이 있다면 오류를, 이상이 없다면 비즈니스 로직을 거쳐 데이터베이스에 저장하도록 합니다.


ToDoItem 엔티티 클래스

TodoItem 엔티티 클래스는 수정 메서드를 추가하였습니다.

package com.koonsland.todo.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;

import java.time.LocalDateTime;

@Entity
@Table(name = "todo_item")
public class ToDoItem {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "title", nullable = false)
    private String title;

    @Column(name= "description", length = 500)
    private String description;

    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    protected ToDoItem() {}

    private ToDoItem(String title, String description) {
        this.title = title;
        this.description = description;
    }

    // 생성 메서드
    public static ToDoItem createToDoItem(String title, String description) {
        return new ToDoItem(title, description);
    }

    @PrePersist
    protected void onCreate() {
        LocalDateTime now = LocalDateTime.now();
        this.createdAt = now;
        this.updatedAt = now;
    }

    @PreUpdate
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    // Getters
    // Setters 생성 안함 (필요한 비즈니스 메서드만 생성 예정)
    public Long getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public String getDescription() {
        return description;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public LocalDateTime getUpdatedAt() {
        return updatedAt;
    }

    // 수정 메서드
    public void updateTitleAndDescription(String title, String description) {
        this.title = title;
        this.description = description;
    }
}

 

🧑🏻‍💻 Lombok 롬복
롬복을 사용하면 Getter, Setter를 사용하지 않고 어노테이션만으로 줄일 수 있습니다. 다만 이 프로젝트는 가장 기본적인 내용으로 진행 중이며 롬복을 알기 전에 어떤 메서드들로 구성되고 수정해야 하는지 알기 위해 사용하지 않고 진행합니다.

 

수정 메서드는 자신이 가지고 있는 필드의 데이터를 바로 수정합니다. 여기에서 Setter를 사용하지 않은 이유가 나타납니다. 수정하고자 하는 기능의 메서드는 반드시 그 기능만을 수행해야 합니다. 하지만 단순히 setXXX() 메서드는 그 값이 왜 수정되어야 하는지에 대한 이유가 전혀 드러나지 않기 때문에 Setter 메서드는 지양하고 있습니다.

 

여기서 수정메서드는 제목과 설명 모두를 수정하는 메서드입니다. 이렇게 비즈니스 메서드를 만들어서 사용하는 것이 명확하고 좋습니다. 그리고 이렇게 수정하는 것만으로도 데이터베이스에 쿼리가 전달됩니다. 이를 변경감지(Dirty Checking)라고 합니다.

🔥Dirty Checking
Dirty Checking은 JPA 엔티티가 변경되었을 때, 데이터베이스에 그 변경 사항을 자동으로 반영해주는 기능을 말합니다. 이를 통해 개발자는 객체의 상태 변화를 추적하고, 필요한 경우 데이터베이스에 업데이트를 자동으로 수행할 수 있습니다.

 

Dirty Checking은 어떤식으로 이루어지는지 예제를 완성하고 확인해 볼게요.


ToDoItemRepository 인터페이스

ToDoItemRepository 인터페이스를 그대로 사용합니다. 추가적으로 해야 할 작업은 없습니다.

package com.koonsland.todo.repository;

import com.koonsland.todo.model.ToDoItem;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ToDoItemRepository extends JpaRepository<ToDoItem, Long> {
}

ToDoItemService 클래스

ToDoItem을 조회하고 업데이트하는 비즈니스 로직을 ToDoItemService 클래스에 구현해 보도록 하겠습니다.

package com.koonsland.todo.service;

import com.koonsland.todo.dto.ToDoItemDto;
import com.koonsland.todo.model.ToDoItem;
import com.koonsland.todo.repository.ToDoItemRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.NoSuchElementException;

@Service
public class ToDoItemService {

    private final ToDoItemRepository toDoItemRepository;

    public ToDoItemService(ToDoItemRepository toDoItemRepository) {
        this.toDoItemRepository = toDoItemRepository;
    }

    /**
     * 모든 할 일 조회
     */
    public List<ToDoItem> findAll() {
        return toDoItemRepository.findAll();
    }

    /**
     * 특정 할 일 조회
     */
    public ToDoItem findById(Long id) {
        return toDoItemRepository.findById(id)
                .orElseThrow(() -> new NoSuchElementException("할 일을 찾을 수 없어요. id: [" + id +"]"));
    }

    /**
     * 할 일 저장
     */
    @Transactional
    public ToDoItem save(ToDoItemDto toDoItemDto) {
        // 엔티티 생성
        ToDoItem toDoItem = ToDoItem.createToDoItem(toDoItemDto.title(), toDoItemDto.description());

        // 저장
        toDoItemRepository.save(toDoItem);

        return toDoItem;
    }

    /**
     * 할 일 수정
     */
    @Transactional
    public ToDoItem update(Long id, ToDoItemDto toDoItemDto) {
        // 엔티티 조회
        ToDoItem toDoItem = findById(id);

        // 엔티티 수정
        toDoItem.updateTitleAndDescription(toDoItemDto.title(), toDoItemDto.description());

        return toDoItem;
    }
}

 

우선은 ToDoItemDto 클래스를 통해서 값을 넘겨받습니다. 이 값은 Controller에서 받을 값을 담은 클래스이며 내부, 외부 데이터 전달용으로 사용되는 클래스입니다. 클래스의 구성은 다음과 같습니다.

package com.koonsland.todo.dto;

import org.springframework.lang.NonNull;

public record ToDoItemDto(@NonNull String title, @NonNull String description) {
}

 

record 클래스 (Java 14 이상)로 만들어졌으며 제목과 설명 필드만 가지고 있는 클래스입니다. 이 record 클래스는 Controller에서 다시 한번 사용하게 될 예정입니다.

 

update 메서드는 2가지 parameter를 받습니다. update의 기본은 어떠한 데이터를 수정하는지 정해져 있고 그 데이터의 값을 받아서 수정하는 것입니다. 그래서 첫 번째 인자는 id를 입력받습니다. 이 id는 데이터베이스에 저장할 때 자동으로 생성된 id 값을 의미합니다. 그리고 이 id값과 수정할 ToDoItemDto 값을 전달해 줍니다.

 

먼저 전달받은 id 값의 엔티티가 실제로 존재하는지 확인하기 위해서 조회 메서드를 이용해서 조회합니다. 이미 위해서 findById()라는 상세 조회 메서드를 만들었고 이렇게 조회한 ToDoItem 값의 메서드를 이용해서 title, description 값을 수정하도록 합니다.

 

이렇게 수정된 ToDoItem을 반환합니다. 그럼 자동으로 데이터베이스에 update 문법이 전달됩니다.

Hibernate: 
    select
        tdi1_0.id,
        tdi1_0.created_at,
        tdi1_0.description,
        tdi1_0.title,
        tdi1_0.updated_at 
    from
        todo_item tdi1_0 
    where
        tdi1_0.id=?
Hibernate: 
    update
        todo_item 
    set
        description=?,
        title=?,
        updated_at=? 
    where
        id=?

 

로그를 확인해 보면 조회인 select 문법과 수정인 update 문법이 사용된 것을 볼 수 있습니다.


ToDoItemController 클래스

ToDoItemController 클래스에서는 새로운 API를 하나 만들어야 합니다. 이번에는 수정하는 API이므로 @PatchMapping 어노테이션 또는 @PutMapping 어노테이션을 사용합니다. Url 패턴은 다음과 같습니다.

🚀 PATCH /api/todos/{id} or PUT /api/todos/{id}
둘 중 어느 것을 사용해도 무방합니다. 다만 클라이언트에서 호출할 때, method에 put 또는 patch를 맞춰서 API 호출하면 됩니다.

 

ToDoItem 할 일을 새롭게 생성하는 Rest API를 만들어 보도록 하겠습니다.

package com.koonsland.todo.controller;

import com.koonsland.todo.dto.ToDoItemDto;
import com.koonsland.todo.model.ToDoItem;
import com.koonsland.todo.service.ToDoItemService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api/todos")
public class ToDoItemController {

    private final ToDoItemService toDoItemService;

    public ToDoItemController(ToDoItemService toDoItemService) {
        this.toDoItemService = toDoItemService;
    }

    /**
     * 모든 ToDoItem 조회
     */
    @GetMapping
    public List<ToDoItem> getAllToDoItems() {
        return toDoItemService.findAll();
    }

    /**
     * 특정 ID를 이용한 ToDoItem 조회
     */
    @GetMapping("/{id}")
    public ToDoItem getToDoItemById(@PathVariable Long id) {
        return toDoItemService.findById(id);
    }

    /**
     * 할 일 등록
     */
    @PostMapping
    public Long createToDoItem(@RequestBody @Valid ToDoItemDto toDoItemDto) {
        ToDoItem toDoItem = toDoItemService.save(toDoItemDto);
        return toDoItem.getId();
    }

    /**
     * 할 일 수정
     */
    @PatchMapping("/{id}")
    public Long updateToDoItem(@PathVariable Long id, @RequestBody @Valid ToDoItemDto toDoItemDto) {
        ToDoItem toDoItem = toDoItemService.update(id, toDoItemDto);
        return toDoItem.getId();
    }
}

 

할 일 수정 API는 위에서 만든 ToDoItemDto 클래스를 이용해서 데이터를 받습니다. 이 프로젝트에서는 @PatchMapping 어노테이션을 사용했으며 @PathVariable로 id 값을 입력받습니다. PATCH로 들어온 데이터는 Body에 데이터를 넣어서 전달해야 합니다. 이렇게 전달된 값을 자동으로 ToDoItemDto에 매핑하기 위해서 @RequestBody 어노테이션을 붙여줍니다. 그리고 이렇게 받은 Dto는 Service 레이어로 데이터를 넘겨줍니다.


API 테스트

모든 할 일 조회 API를 테스트하기 위해 Postman 앱을 사용할 수 있습니다.

 

웹사이트: https://www.postman.com

 

Postman API Platform | Sign Up for Free

Postman is an API platform for building and using APIs. Postman simplifies each step of the API lifecycle and streamlines collaboration so you can create better APIs—faster.

www.postman.com

 

앱을 설치하고 테스트를 진행해 볼게요.

Postman 앱을 실행하여 API 만들기를 선택하여 할 일을 생성하는 API를 만들어 줍니다.

 

메서드는 PATCH이며 주소는 localhost:8080/api/todos/1로 입력하고 Body 값을 추가로 입력해 줍니다. Body는 raw형태의 JSON으로 설정하고 아래와 같이 입력해 줍니다.

💡 id가 1이 아닐 수도 있습니다. 테스트하면서 이 값은 저장할 때 자동으로 정해지는 값이기 때문에 반드시 특정 할 일 조회 API를 호출해서 특정 id를 알고 있다는 전제조건이 필요합니다!
{
    "title": "최초 할 일(수정)",
    "description": "최초 할 일 등록(수정)"
}

 

이렇게 입력하고 Send 버튼을 누르면 Controller에서 Reponse로 전달해 준 id 값이 나타납니다.

 

H2 데이터베이스이기 때문에 SQL 쿼리는 조금 다를 수 있지만 그래도 쿼리를 직접 생성하지 않고 Hibernate가 만들어서 DB에 쿼리를 전달해서 저장하도록 했습니다. H2 콘솔에 접속하여 저장된 데이터도 확인해 보겠습니다.

H2 콘솔 할 일 수정

 

데이터가 정상적으로 수정된 모습도 확인할 수 있습니다.


이번 글에서는 To-Do List 애플리케이션의 기존 할 일을 수정하는 API를 구현하는 방법을 설명했습니다. Controller, Service, Repository 계층 구현 및 엔티티 정의를 통해 API를 완성했습니다.

 

이 글을 통해서 기존 할 일 수정 API를 구현하는 방법을 명확하게 이해할 수 있길 바랍니다. 다음 글에서는 할 일 삭제 API를 구현해 보도록 할게요. 추가적으로 필요하거나 궁금한 사항이 있다면 댓글로 남겨주세요. 도움이 되시길 바랍니다. 감사합니다. 🙇🏻

 

이전 글

2024.06.16 - [쿤즈 프로젝트/To-do List Application] - [Spring Boot] To-Do List 애플리케이션: Chap1. 프로젝트 소개

2024.06.20 - [쿤즈 프로젝트/To-do List Application] - [Spring Boot] To-Do List 애플리케이션: Chap2. 요구사항 정리

2024.06.26 - [쿤즈 프로젝트/To-do List Application] - [Spring Boot] To-Do List 애플리케이션: Chap3. 기본 프로젝트 설정

2024.06.27 - [쿤즈 프로젝트/To-do List Application] - [Spring Boot] To-Do List 애플리케이션: Chap4. 데이터 모델링

2024.06.26 - [쿤즈 프로젝트/To-do List Application] - [Spring Boot] To-Do List 애플리케이션: Chap5. API 계층별 구현 (1) 모든 할 일 조회

2024.06.27 - [쿤즈 프로젝트/To-do List Application] - [Spring Boot] To-Do List 애플리케이션: Chap6. API 계층별 구현 (2) 특정 할 일 조회(상세조회)

2024.06.27 - [쿤즈 프로젝트/To-do List Application] - [Spring Boot] To-Do List 애플리케이션: Chap7. API 계층별 구현 (3) 할 일 생성

 

댓글