본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.
공부 시작 시각 인증
수강 인증 사진
멱등성, 멱등키
- 멱등성: 첫 수행을 한 뒤, 여러 번 적용해도 결과가 달라지지 않는 작업 또는 기능의 속성
멱등성이 뭔가요?
생소한 표현이지만 알고 보면 쉬워요. 컴퓨터 과학에서 멱등하다는 것은 첫 번째 수행을 한 뒤 여러 차례 적용해도 결과를 변경시키지 않는 작업 또는 기능의 속성을 뜻해요. 즉, 멱등한 작업의
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);
}
}