UMC_3주차 과제

seonseon·2025년 9월 27일
1

연합동아리_umc

목록 보기
5/6

1) Soft Delete란?

Soft Delete란 데이터를 실제로 삭제하지 않고, 삭제 여부를 나타내는 상태값(예: deleted=true)만 업데이트하여 마치 삭제된 것처럼 취급하는 방식이다.

Spring Boot에서 Soft Delete를 구현하는 전체 흐름

  • DELETE /users/{id} 앤드포인트 요청 → 실제 삭제 X / deleted = true로 표시

  • 이후 findAll() 등에서는 deleted = false인 데이터만 조회

  • Soft Delete 시 deleted = true와 함께 deleted_at 타임스탬프 저장

  • 별도의 스케줄러(Spring Scheduler) 를 돌려서

    deleted = true && deleted_at < (현재 시간 - 30일)인 데이터만 진짜 삭제

    1. User 엔티티에 deletedAt 필드 추가
import java.time.LocalDateTime;
import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class User {

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

    private String name;

    private boolean deleted = false;

    private LocalDateTime deletedAt; // 삭제 시간 저장

    // Getter & Setter 생략
}
  1. Soft Delete 처리 시 deletedAt도 설정
public void softDeleteUser(Long userId) {
    User user = userRepository.findById(userId)
        .orElseThrow(() -> new IllegalArgumentException("User not found"));

    user.setDeleted(true);
    user.setDeletedAt(LocalDateTime.now()); // 삭제 시각 저장

    userRepository.save(user);
}
  1. 물리 삭제를 위한 Repository 메서드 추가
import java.time.LocalDateTime;

public interface UserRepository extends JpaRepository<User, Long> {

    // 삭제된 지 30일 이상 지난 사용자
    List<User> findByDeletedTrueAndDeletedAtBefore(LocalDateTime dateTime);

    void deleteAllByDeletedTrueAndDeletedAtBefore(LocalDateTime dateTime);
}

4. 스케줄러로 일정 주기로 영구 삭제 처리

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;

@Component
public class UserCleanupScheduler {

    private final UserRepository userRepository;

    public UserCleanupScheduler(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 매일 새벽 3시에 실행
    @Scheduled(cron = "0 0 3 * * *")
    public void permanentlyDeleteOldSoftDeletedUsers() {
        LocalDateTime threshold = LocalDateTime.now().minusDays(30);
        userRepository.deleteAllByDeletedTrueAndDeletedAtBefore(threshold);
        System.out.println("Soft deleted users older than 30 days have been permanently deleted.");
    }
}

Soft Delete에 적합한 HTTP Method

Soft Delete는 리소스를 실제로 제거하지 않고 상태만 변경하는 행위이므로, 리소스의 속성을 갱신하는 개념에 가깝다.

따라서 PUT 또는 PATCH 메서드를 사용하는 것이 적절하다.

  • PATCH
    PATCH /users/123
    { "deleted": true }
    
    특정 필드(deleted)만 부분적으로 수정하는 것이므로 Soft Delete에는 가장 적합하다.
  • PUT
    PUT /users/123
    { "id": 123, "name": "홍길동", "deleted": true }
    
    전체 리소스를 갱신하는 방식이므로 Soft Delete에도 사용할 수 있으나, 불필요하게 많은 데이터가 포함될 수 있다.

2) 컨트롤 URI

URI는 리소스를 식별하도록 설계해야하지만, 실무에서는 리소스에 대한 행위가 너무 다양하므로 URI에 행위가 표현될 수 있다. 이를 컨트롤 URI라 한다. 예를 들어, '주문'이라는 리소스가 있다고 했을 때, /orders로 주문 등록, 조회, 수정, 삭제 행위말고도 배달 시작이라는 행위를 해야한다고 하자. 이때는 HTTP Method만으로는 구분할 수 없으므로 /orders/{orderId}/start-delivery 와 같은 컨트롤 URI을 사용한다.

3) RESTful 웹 API 구현

  • REST(Representational State Transfer) 아키텍처 원칙을 사용하여 클라이언트와 서비스 간에 느슨하게 결합된 상태 비 상태 인터페이스를 달성하는 웹 API.

1. 플랫폼 독립성

RESTful 웹 API는 플랫폼에 독립적이어야 한다.

이는 클라이언트가 서버의 내부 구현을 몰라도 API를 호출할 수 있음을 의미한다.

플랫폼 독립성을 달성하기 위해서는 다음과 같은 조건을 충족해야 한다.

  • HTTP라는 표준 프로토콜을 사용한다.
  • 데이터 교환은 JSON이나 XML과 같은 익숙한 형식을 지원한다.
  • 명확한 API 명세서를 제공하여 누구나 쉽게 이해할 수 있게 한다.

즉, 어떤 운영체제나 언어를 사용하더라도 동일한 방식으로 호출할 수 있어야 하며, 이것이 플랫폼 독립성의 핵심이다.

2. 느슨한 결합

RESTful 웹 API는 느슨한 결합(Loose Coupling)을 가져야 한다.

이는 클라이언트와 서버가 서로 독립적으로 발전할 수 있음을 의미한다.

  • 클라이언트는 서버의 내부 구현을 알 필요가 없다.
  • 서버 역시 클라이언트의 내부 구조를 알 필요가 없다.
  • 양쪽은 단지 표준 프로토콜데이터 형식(JSON/XML)이라는 약속만 지키면 된다.

예를 들어, 서버에서 데이터베이스 구조를 변경하더라도 API 응답 형식이 동일하다면 클라이언트는 수정 없이 그대로 동작할 수 있다.

반대로 클라이언트의 UI가 바뀌더라도 서버는 별도의 수정 없이 기존 기능을 유지할 수 있다.


리소스 URI 명명 규칙 정리

1. 리소스 이름은 명사로 표현한다

리소스는 행위가 아닌 객체(명사)로 표현해야 한다.

예컨대 /create-order처럼 동사형을 URI에 넣지 않는다. HTTP 메서드가 이미 행위를 표현하므로 URI는 리소스 자체를 가리키는 명사여야 한다.

POST /create-order <- 잘못된 예시
POST /orders <- 올바른 예시

2. 컬렉션은 복수 명사로 표기한다

컬렉션을 가리키는 URI는 복수형을 사용한다.

예: /customers는 고객 컬렉션을 의미하고, /customers/5는 ID가 5인 고객 리소스를 의미한다.

이렇게 계층적으로 구성하면 직관적이며 프레임워크 라우팅과도 잘 맞는다.

GET /customers       → 고객 컬렉션 전체 조회
GET /customers/5     → 특정 고객(ID=5) 조회

3. 리소스 간 관계는 URI 계층으로 표현한다

관련 리소스는 계층적 URI로 노출할 수 있다.

예: /customers/5/orders는 고객 5의 주문 목록을 의미한다.

필요시 역방향 접근도 가능하지만, 과도하게 확장하면 유지보수가 어려워진다.

GET /customers/5/orders   → 고객 5의 주문 목록

4. 복잡한 관계는 URI로 깊게 표현하지 말고 링크 또는 응답 본문에 포함한다

/customers/1/orders/99/products처럼 여러 단계로 깊게 표현하면 유연성이 떨어지고 유지보수가 어려워진다.

관련 리소스는 응답 본문에 링크(HATEOAS 방식)로 포함해 클라이언트가 쉽게 탐색하도록 하는 것이 바람직하다.

GET /customers/1/orders       → 고객 1의 모든 주문 반환
(응답 본문 안에 각 주문의 products 링크 포함)

5. URI는 단순하게 유지한다

컬렉션/항목 수준(예: /resources / /resources/{id}) 이상으로 복잡한 패턴을 만들지 않는 것이 좋다.

단순한 URI가 확장성과 가독성에 유리하다.


6. 너무 많은 작은 리소스를 노출하지 않는다 (번잡한 API 금지)

작은 조각 리소스를 잔뜩 노출하면 클라이언트가 여러 요청을 해야 하므로 서버 부하와 네트워크 지연이 증가한다.

필요한 정보는 적절히 비정규화하거나 묶어서 한 번의 요청으로 제공할 수 있도록 설계한다.

다만 불필요한 데이터까지 과도하게 반환하는 것은 피해야 한다(대역폭/지연 트레이드오프 고려).

--- 잘못된 예시 ---
GET /orders/99/customer/name
GET /orders/99/customer/address
GET /orders/99/customer/phone

--- 좋은 예시 ---
GET /orders/99/customer

7. 데이터베이스 내부 구조를 그대로 노출하지 않는다

API는 비즈니스 엔터티와 행위를 모델링해야 하며, 데이터베이스 테이블 구조를 그대로 노출하면 안 된다.

테이블을 그대로 리소스로 노출하면 공격 표면이 넓어지고 내부 스키마 변경 시 클라이언트에 영향을 준다.

필요하면 DB와 API 사이에 매핑(어댑터) 계층을 두어 내부 변경을 클라이언트로부터 격리한다.

--- 나쁜 예시 (DB 테이블 그대로 노출 ---
GET /tbl_user
GET /tbl_order

--- 좋은 예시 (비즈니스 엔터티 기반) ---
GET /users
GET /orders

출처 : https://learn.microsoft.com/ko-kr/azure/architecture/best-practices/api-design

0개의 댓글