본문 바로가기

패스트캠퍼스

패스트캠퍼스 환급챌린지 56일차 : 9개 도메인 프로젝트로 끝내는 백엔드 웹 개발 (Java/Spring) 초격차 패키지 Online 강의 후기

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.

공부 시작 시각 인증

공부 시작 시각 인증

수강 인증 사진

수강 인증 사진

멱등성, 멱등키

 

멱등성이 뭔가요?

생소한 표현이지만 알고 보면 쉬워요. 컴퓨터 과학에서 멱등하다는 것은 첫 번째 수행을 한 뒤 여러 차례 적용해도 결과를 변경시키지 않는 작업 또는 기능의 속성을 뜻해요. 즉, 멱등한 작업의

www.tosspayments.com

  • 멱등성 특징
    • API 설계 및 데이터 처리에서 오류를 방지하고 안정성을 높이는 데 필수!
    • 예를 들면, HTTP 메서드 중, GET 요청을 여러 번 보내도 서버 상태나 응답 데이터가 변하지 않는다.
    • 멱등성이 왜 필요한 가? → 네트워크 오류로 인해 고객의 돈이 여러 번 차감되는 것을 방지하기 위해 → 시스템의 신뢰성을 높이고 예기치 않은 오류를 줄인다.
  • 멱등키: 멱등성을 유지하기 위해 사용되는 고유 키

멱등키 기능 시퀀스 다이어그램

Idempotency.java

package org.fastcampus.common.idempotency;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.fastcampus.common.ui.*;

@Getter
@AllArgsConstructor
public class Idempotency {
    private final String key;
    private final Response<?> response;
}

IdempotencyEntity.java

package org.fastcampus.common.idempotency.repository.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.fastcampus.common.idempotency.*;
import org.fastcampus.common.utils.*;

@Entity
@Table(name = "community_idempotency")
@NoArgsConstructor
@AllArgsConstructor
public class IdempotencyEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true, nullable = false)
    private String idempotencyKey;
    @Getter
    @Column(nullable = false)
    private String response;

    public IdempotencyEntity(Idempotency idempotency) {
        this.idempotencyKey = idempotency.getKey();
        this.response = ResponseObjectMapper.toStringResponse(idempotency.getResponse());
    }

    public Idempotency toIdempotency() {
        return new Idempotency(this.idempotencyKey, ResponseObjectMapper.toResponseObject(response));
    }
}

ResponseObjectMapper.java

package org.fastcampus.common.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.fastcampus.common.ui.*;

public class ResponseObjectMapper {
    private ResponseObjectMapper() {
        throw new IllegalStateException("Utility class");
    }

    private static final ObjectMapper objMapper = new ObjectMapper();

    public static Response toResponseObject(String response) {
        try {
            return objMapper.readValue(response, Response.class);
        } catch (Exception e) {
            return null;
        }
    }

    public static String toStringResponse(Response<?> response) {
        try {
            return objMapper.writeValueAsString(response);
        } catch (JsonProcessingException e) {
            return null;
        }
    }
}

IdempotencyRepository.java

package org.fastcampus.common.idempotency;

public interface IdempotencyRepository {
    Idempotency getByKey(String key);
    void save(Idempotency idempotency);
}

JpaIdempotencyRepository.java

package org.fastcampus.common.idempotency.repository;

import java.util.Optional;
import org.fastcampus.common.idempotency.repository.entity.*;
import org.springframework.data.jpa.repository.JpaRepository;

public interface JpaIdempotencyRepository extends JpaRepository<IdempotencyEntity, Long> {
    Optional<IdempotencyEntity> findByIdempotencyKey(String key);
}

IdempotencyRepositoryImpl.java

package org.fastcampus.common.idempotency.repository;

import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.fastcampus.common.idempotency.*;
import org.fastcampus.common.idempotency.repository.entity.*;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class IdempotencyRepositoryImpl implements IdempotencyRepository {
    private final JpaIdempotencyRepository jpaIdempotencyRepository;

    @Override
    public Idempotency getByKey(String key) {
        Optional<IdempotencyEntity> idempotencyEntity = jpaIdempotencyRepository.findByIdempotencyKey(key);
        return idempotencyEntity.map(IdempotencyEntity::toIdempotency).orElse(null);
    }

    @Override
    public void save(Idempotency idempotency) {
        jpaIdempotencyRepository.save(new IdempotencyEntity(idempotency));
    }
}

IdempotencyAspect.java

package org.fastcampus.common.idempotency;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.fastcampus.common.ui.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
@RequiredArgsConstructor
public class IdempotencyAspect {
    private final IdempotencyRepository idempotencyRepository;
    private final HttpServletRequest request;

    @Around("@annotation(Idempotent)")
    public Object checkIdempotency(ProceedingJoinPoint joinPoint) throws Throwable {
        String idempotencyKey = request.getHeader("Idempotency-Key");
        if (idempotencyKey == null) {
            return joinPoint.proceed();
        }

        Idempotency idempotency = idempotencyRepository.getByKey(idempotencyKey);
        if (idempotency != null) {
            return idempotency.getResponse();
        }

        Object result = joinPoint.proceed();
        Idempotency newIdempotency = new Idempotency(idempotencyKey, (Response<?>) result);
        idempotencyRepository.save(newIdempotency);
        return result;
    }
}

PostController.java

package org.fastcampus.post.ui;

import lombok.RequiredArgsConstructor;
import org.fastcampus.common.idempotency.*;
import org.fastcampus.common.ui.*;
import org.fastcampus.post.application.PostService;
import org.fastcampus.post.application.dto.CreatePostRequestDto;
import org.fastcampus.post.application.dto.LikeRequestDto;
import org.fastcampus.post.application.dto.UpdatePostRequestDto;
import org.fastcampus.post.domain.*;
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;

@RestController
@RequestMapping("/post")
@RequiredArgsConstructor
public class PostController {
    private final PostService postService;

    @PostMapping
    public Response<Long> createPost(@RequestBody CreatePostRequestDto dto) {
        Post post = postService.createPost(dto);
        return Response.OK(post.getId());
    }

    @PatchMapping("/{postId}")
    public Response<Long> updatePost(@PathVariable(name = "postId") Long postId, @RequestBody UpdatePostRequestDto dto) {
        Post post = postService.updatePost(postId, dto);
        return Response.OK(post.getId());
    }

    @Idempotent
    @PostMapping("/like")
    public Response<Void> likePost(@RequestBody LikeRequestDto dto) {
        postService.likePost(dto);
        return Response.OK(null);
    }

    @PostMapping("/unlike")
    public Response<Void> unlikePost(@RequestBody LikeRequestDto dto) {
        postService.unlikePost(dto);
        return Response.OK(null);
    }
}

학습 인증샷

학습 인증샷

공부 종료 시각 인증

공부 종료 시각 인증

https://bit.ly/4hTSJNB