| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |
- GIT
- Lv2
- 알고리즘고득점Kit
- 백준
- Python
- 완전탐색
- 자바
- 깃허브
- 조합
- 1932
- 그래프탐색
- 프로그래머스
- 분할정복
- 프로그래멋
- 이코테
- 정렬
- 알고리즘
- 구현
- 그래프
- DP
- 15686
- 깃허브 프로필
- BFS
- Java
- 정수 삼각형
- 토마토
- 다익스트라
- dfs
- Summer/Winter Coding(~2018)
- 월간 코드 챌린지 시즌1
- Today
- Total
갱스터하우스
[쿠.파.프] 만료된 쿠폰 개선기 | EP1. 쿠폰 사용 API 개발 본문
이 글은 제가 학습하고 경험한 것을 바탕으로 작성하여,
일부 부족하거나 잘못된 점이 있을 수 있습니다.
부족한 부분이나 잘못된 내용에 대한 피드백을 주시면 감사히 배우겠습니다!
1. 쿠폰 사용 API 설계
쿠폰은 다음과 같은 과정을 거쳐 사용할 수 있다.
1. 사용자가 쿠폰을 사용하려고 할 때,
2. 해당 쿠폰을 사용할 수 있다면,
3. 사용 가능하다.
이걸 개발하는 관점으로 본다면,
1. 사용자가 어떠한 쿠폰의 사용을 요청할 때,
2. 해당 쿠폰의 유효성 검증을 성공한다면,
3. 데이터베이스에서 해당 쿠폰의 상태는 IN_USE로 바뀐다.
➡️ 위의 내용을 바탕으로 아래와 같이 API를 설계했다!
| 기능 | 쿠폰 사용 |
| HTTP Method | POST |
| API Path | /users/user/coupon |
| Request | UseCouponRequest { "userId" : long "couponId" : long } |
| Response | // 성공시 -> 반환 안함 // Error { "code": string "message": string } |
2. 쿠폰 사용 API 개발
➡️로직 구현
본 프로젝트(a.k.a Throwng)에서 구현한 것과 같이,
쿠폰을 사용하는 로직과 검증하는 로직을 분리해서 설계했다.
🤔그렇다면, 쿠폰에 대해서 어떤 유효성을 검증해야 할까?
여러 가지를 생각하다 보니 구현할 게 많아
우선, 가장 중요하다고 생각하는 세 가지만 검증하기로 했다.
1. 현재 사용하려고 하는 쿠폰이 존재하는 쿠폰인가?
2. 현재 사용하려고 하는 쿠폰을 발급받은 사용자와 이를 사용하려는 사용자가 동일한 사용자인가?
3. 현재 사용하려고 하는 쿠폰의 상태가 사용 가능한 상태인가?
두 번째의 경우, 본 프로젝트에서는 Token을 통해 userId를 획득해 검증했지만
[쿠.파.프]에서는 회원 기능을 구현하지 않았기에, 그냥 처음 요청할 때 request로 userId를 받기로 했다.
CouponService
@Service
@RequiredArgsConstructor
public class CouponService {
private final CouponRepository couponRepository;
private final CouponValidator couponValidator;
private final String INUSED_STATUS = "IN_USE";
@Transactional
public void useCoupon(final UseCouponRequest useCouponRequest) {
String status = couponRepository.findStatusById(useCouponRequest.getCouponId());
CouponValidationequest couponValidationequest =
CouponValidationequest.of(useCouponRequest,
status);
couponValidator.validateCoupon(couponValidationequest); // 쿠폰 검증
Coupon coupon =
couponRepository
.findById(couponValidationequest.getCouponId())
.orElseThrow(() -> new BadRequestException(NOT_EXISITED_COUPON_ID));
coupon.updateStatus(INUSED_STATUS);
couponRepository.save(coupon);
// try{
//
//
// }catch(Exception e){
// throw new BadRequestException(UNAVAILABLE_COUPON);
// }
}
}
- 처음에는 try-catch로 구현을 했다.
하지만 이럴 경우, validator에서 각각의 검증단계에서 던지는 예외가 catch에서 하나의 예외로 처리되었다.
발생하는 Exception 원인은 경우에 따라 다른데, 단순하게 "사용할 수 없는 쿠폰"으로 처리하는 것은 아니라고 생각해서
try-cathc 문을 사용하지 않았다.
CouponValidator
@Component
@RequiredArgsConstructor
public class CouponValidator {
private final CouponRepository couponRepository;
private final String availableStatus = "AVAILABLE";
private final String inuseStatus = "IN_USE";
private final String appliedStatus = "APPLIED";
private final String expiredStatus = "EXPIRED";
public void validateCoupon(final CouponValidationequest couponValidationequest){
// 쿠폰 존재 여부 확인
if(!isExisited(couponValidationequest.getCouponId())){
throw new BadRequestException(NOT_EXISITED_COUPON_ID);
}
// 쿠폰 발급자-쿠폰 사용자 동일인 확인
if(!isMatched(couponValidationequest.getCouponId(), couponValidationequest.getUserId())){
throw new BadRequestException(NOT_MATCHED_USER);
}
// 쿠폰 사용 가능 상태 확인
validateCouponStatus(couponValidationequest.getStatus());
}
public boolean isExisited(long couponId){
return couponRepository.existsById(couponId);
}
public boolean isMatched(long couponId, long userId){
long idBycoupon = couponRepository.findUserIdById(couponId);
if(idBycoupon != userId) return false;
else return true;
}
public void validateCouponStatus(String status){
if(status.equals(availableStatus)){
return;
}else if(status.equals(inuseStatus)){
throw new BadRequestException(IN_USE_COUPON_STATUS);
}else if(status.equals(appliedStatus)){
throw new BadRequestException(APPLIED_COUPON_STATUS);
}else if(status.equals(expiredStatus)){
throw new BadRequestException(EXPIRED_COUPON_STATUS);
}else{
throw new BadRequestException(INVALID_COUPON_STATUS);
}
}
}
- 본 프로젝트에서도 validateCouponStatus()를 구현할 때 고민했던 것 중 하나는,
바로 status를 어떻게 구현할 것인가였다.
enum으로 할까? 아니면 그냥 하드코딩? 근데 string으로 바로 쓰는 건 아닌 것 같은데?
그렇다고 db에 값을 저장해 놓으면 매번 조회하는 것도 일인데..!
고민 끝에 내가 택한 방법은 enum이었다.
왜냐하면 본 프로젝트는 실제로 서비스를 운영할 예정이었기 때문에, 유지보수성이 중요했다
하.지.만?
이번 쿠.파.프에서는 빠른 구현이 포인트여서 하드코딩을 했다-.
3. Postman을 이용한 API 테스트
개발한 API를 Postman을 이용해 테스트했다.
➡️ 테스트를 하며 체크해야 할 내용은 다음과 같다.
1. 각각의 실패 테스트케이스에 대한 예외처리가 잘 발생하는가?
2. 유효성 검증을 통과한 쿠폰 사용 시, db에 잘 반영이 되는가?

위의 이미지는 테스트 전 쿠폰 테이블의 상태이다.
status가 "AVAILABLE"인 쿠폰만 사용이 가능하다
그럼 각 테스트케이스에 대해 테스트해 보자!
1-1. [예외처리] 현재 사용하려고 하는 쿠폰이 존재하지 않는 쿠폰인 경우

1-2. [예외처리] 현재 사용하려고 하는 쿠폰을 발급받은 유저와 이를 사용하려고 하는 유저가 다른 경우

2. [DB 반영] 쿠폰 사용 성공


이때 당시에는 IN_USED이지만, 이후에는 IN_USE로 수정했다
1-3. [예외처리] 사용하려고 하는 쿠폰의 사용 불가능한 상태일 경우

테스트를 완료하며 쿠폰 사용 API 개발을 끝냈다.
다음 단계에서는 Redis를 이용해 쿠폰 사용 시, 현재 사용 중인 쿠폰을 Redis에 저장하고
TTL이 만료된 쿠폰을 Redis Key Notification을 이용해서 처리를 해보겠다!
'Project' 카테고리의 다른 글
| [쿠.파.프] 만료된 쿠폰 개선기 | EP3. Scheduler를 이용해 유효기간이 만료된 쿠폰 상태 업데이트 하기 (1) | 2025.03.08 |
|---|---|
| [쿠.파.프] 만료된 쿠폰 개선기 | EP2. Redis Key Notifications을 통해 TTL이 만료된 쿠폰 상태 업데이트하기 (0) | 2025.03.08 |
| [쿠.파.프] 만료된 쿠폰 개선기 | EP. 0 설계 (0) | 2025.02.24 |