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

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

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

앞선 글에서는 API 계층별 구현에서는 새로운 할 일을 저장하고 저장된 할 일의 전체 리스트와 특정 할 일을 조회하는 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 구현

할 일을 삭제하기 위해서는 클라이언트로부터 id 값을 넘겨받습니다. 이는 @PathVariable을 이용해서 id를 받고 이 id를 이용해서 DB에 저장된 데이터를 삭제할 수 있습니다.


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 메서드는 지양하고 있습니다.


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을 삭제하는 비즈니스 로직을 구현합니다. 이때, JpaRepository의 인터페이스에는 삭제하는 메서드 2가지가 존재합니다. 

  • delete(Entity)
  • deleteById(Long)

엔티티를 parameter로 입력받아서 삭제처리하는 메서드와 Long 값의 id를 paramter로 받아서 삭제처리하는 메서드입니다. 그래서 이 둘은 어떻게 다른지 확인해 볼게요.

 

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

    /**
     * 할 일 삭제 (entity 이용)
     */
    @Transactional
    public void delete(Long id) {
        ToDoItem toDoItem = findById(id);
        toDoItemRepository.delete(toDoItem);
    }
    
    /**
     * 할 일 삭제 (id 이용)
     */
    @Transactional
    public void deleteById(Long id) {
        toDoItemRepository.deleteById(id);
    }
}

 

먼저 id를 입력받아서 엔티티를 조회하고 조회된 엔티티를 이용해서 삭제처리하는 방법에 대해서 알아볼게요. id를 입력받아 먼저 엔티티를 조회합니다. 기존 조회 메서드를 사용해서 조회 후 Repository의 delete() 메서드를 이용해서 삭제할 수 있습니다.

 

이 경우 에러처리를 어떻게 할지 고민해봐야 합니다. 우리가 만든 findById() 메서드는 입력받은 id 값을 이용해서 조회한 엔티티가 없다면 Exception을 발생시킵니다. 그렇다면 삭제처리시 Exception 처리를 해야 할지, 혹은 없다 하더라도 성공을 return 할지 고민이 필요합니다. 만약 에러처리가 필요 없다면 아래와 같이 변경할 수 있습니다.

/**
 * 할 일 삭제 (entity 이용)
 */
@Transactional
public void delete(Long id) {
    toDoItemRepository.findById(id)
//    		.ifPresent(toDoItem -> toDoItemRepository.delete(toDoItem))
            .ifPresent(toDoItemRepository::delete);
}

 

Repository의 findById는 Optional 타입으로 return을 줍니다. 이때, 내부 메서드중 ifPresent는 이름 그대로 만약 존재하면 Optional을 제거한 엔티티 타입을 이용할 수 있으며 위에서는 사용한 람다식을 메서드 레퍼런스로 변경했습니다.

 

💡 레서드 레퍼런스와 람다 표현식
메서드 레퍼런스와 람다 표현식은 Java 8에서 도입된 기능입니다.

람다 표현식은 익명 함수를 간단하게 표현하는 방법으로, 함수형 프로그래밍을 지원하고, 코드의 간결성과 가독성을 높여줍니다.
메서드 레퍼런스는 메서드의 이름을 이용해서 해당 메서드를 호출하는 람다 표현식을 간결하게 나타낼 수 있습니다.

 

 

다음으로 id를 입력받아서 엔티티를 조회하지 않고 바로 삭제하는 메서드를 보겠습니다.

/**
 * 할 일 삭제 (id 이용)
 */
@Transactional
public void deleteById(Long id) {
    toDoItemRepository.deleteById(id);
}

 

Repository에는 deleteById() 메서드가 존재합니다. 이 메서드만 호출하면 엔티티를 호출하지 않고 바로 엔티티 삭제가 가능합니다. 그렇다면 내부 로직은 어떻게 만들어졌는지 확인해 보겠습니다.

JpaRepository의 delete(), deleteById() 구현체

 

deleteById() 메서드를 보면 findById(id).ifPresendt(this::delete) 를 사용하고 있습니다. 앞선 우리의 로직과 동일해 보이지 않나요? 내부적으로도 엔티티를 조회하고 삭제한다는 사실을 알 수 있습니다.


ToDoItemController 클래스

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

🚀 DELETE /api/todos/{id}

 

생긴 모양은 @PatchMapping 또는 @PutMapping 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.DeleteMapping;
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();
    }

    /**
     * 할 일 삭제
     */
    @DeleteMapping("/{id}")
    public void deleteToDoItem(@PathVariable Long id) {
        toDoItemService.delete(id);
    }
}

 

할 일 삭제 API는 @PathVariable을 이용해서 Long 타입의 id를 입력받습니다. 그리고 이렇게 입력받은 parameter는 Service 레이어에서 만든 delete() 메서드를 호출해서 넘겨주도록 합니다.


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를 만들어 줍니다.

 

메서드는 DELETE이며 주소는 localhost:8080/api/todos/1로 입력합니다.

💡 id가 1이 아닐 수도 있습니다. 이는 테스트를 진행해보면서 원하는 id 값을 입력합니다.!

 

확인해보면 정상적으로 삭제된 모습을 확인할 수 있습니다.


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

 

이 글을 통해서 기존 할 일 삭제 API를 구현하는 방법을 명확하게 이해할 수 있길 바랍니다. 다음 글에서는 여기까지 구현한 부분 중 처리를 제대로 하지 않은 Exception을 Custom 하여 추가하는 방법을 알아보겠습니다. 추가적으로 필요하거나 궁금한 사항이 있다면 댓글로 남겨주세요. 도움이 되시길 바랍니다. 감사합니다. 🙇🏻

 

이전 글

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

 

[Spring Boot] To-Do List 애플리케이션: Chap1. 프로젝트 소개

이번 시리즈에서는 Spring Boot 3 버전을 사용해서 간단하게 To-Do List 애플리케이션을 만드는 프로젝트를 진행하려 합니다. 그래서 이 포스팅에서는 애플리케이션을 만드는 방법을 단계별로 소개할

koonsland.tistory.com

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

 

[Spring Boot] To-Do List 애플리케이션: Chap2. 요구사항 정리

지난 Chap1에서는 To-Do List 애플리케이션에 대해서 어떤 것을 만들고 어떤 과정으로 프로젝트를 진행하겠다는 계획에 대해서 정리했습니다. 또한 To-Do List 애플리케이션과 같은 기능을 가진 앱들

koonsland.tistory.com

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) 할 일 생성

 

댓글