토이프로젝트/선착순 이벤트 쿠폰 시스템

Mysql 기반 선착순 쿠폰 발급 기능 개발 (3)

feel2 2024. 4. 18. 00:18
반응형

 

이제부터 만든 서비스를 호출하기 위해 presentation Layer 설계를 해보자.

 

그전까지는 다른곳에서 가져다 쓸 mycoupon-core 에서 작업하였고, 지금은 mycore-api에서 작업을 진행하겠다.

 

  • CouponIssueController
// mycouponapi/controller/CouponIssueController.java

package com.example.mycouponapi.controller;

import com.example.mycouponapi.dto.CouponIssueRequestDto;
import com.example.mycouponapi.dto.CouponIssueResponseDto;
import com.example.mycouponapi.service.CouponIssueRequestService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class CouponIssueController {

    private final CouponIssueRequestService couponIssueRequestService;

    @PostMapping("/v1/issue")
    public CouponIssueResponseDto issueV1(@RequestBody CouponIssueRequestDto body) {
        couponIssueRequestService.issueRequestV1(body);

        return new CouponIssueResponseDto(true, null);
    }
}

 

조금씩 변화하는 걸 보여주기 위해 제일 초기 버전인 v1 으로 api를 작성하였다.

 

편한 Dto 사용을 위해 record 타입을 사용하였다. record는 간단하게 말하자면 불변객체를 쉽게 만들 수 있는 타입이며, 불변을 보장하는 것외에도 getter, ToString, hashCode 등 기본으로 제공하는 기능들이 많으니 궁금하면 더 알아보자.

 

  • CouponIssueRequestDto
// mycouponapi/dto/CouponIssueRequestDto

package com.example.mycouponapi.dto;

public record CouponIssueRequestDto(long userId,long couponId) {}

 

  • CouponIssueResponseDto
// mycouponapi/dto/CouponIssueResponseDto

package com.example.mycouponapi.dto;

public record CouponIssueResponseDto(boolean isSuccess, String comment) {}

 

이 얼마나 간단한가!! 만약 record를 쓰지 않고 dto를 작성해야 한다면 다음과 같이 작성이 될 것이다.

 

  • CouponIssueRequestDto_1

// mycouponapi/dto/CouponIssueRequestDto_1

package com.example.mycouponapi.dto;

import lombok.*;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class CouponIssueRequestDto_1 {
    private long userId;
    private long couponId;

}

 

보통 객체 생성을 위해 빌더패턴을 자주 이용하기 때문에 @Builder 도 붙여주었다.

Builder패턴을 사용하지 않으려면 정적 팩토리 메서드 패턴을 써서 각 객체 생성을 메서드로 정의해 주면 된다.

 

위와 비교해보면 정말 간단해지지 않았는가? 하지만 이런 record 타입도 단점이 존재하니 유의해서 쓰자.

 

  • CouponIssueRequestService

// mycouponapi/dto/CouponIssueRequestService

package com.example.mycouponapi.service;

import com.example.mycouponapi.dto.CouponIssueRequestDto;
import com.example.mycouponcore.service.CouponIssueService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class CouponIssueRequestService {
    private final CouponIssueService couponIssueService;

    public void issueRequestV1(CouponIssueRequestDto requestDto) {

        couponIssueService.issue(requestDto.couponId(), requestDto.userId());
        log.info("쿠폰 발급 완료. couponId: %s, userId: %s"
                .formatted(requestDto.couponId(), requestDto.userId()));

    }
}

 

 

위와 같이 mycoupon-core 에 있는 빈을 가져다 쓸 수 있는건 메인 함수에서 mycoupon-core 에 있는 빈을 import해주었기 때문이다.

 

  • MycouponApiApplication
// mycouponapi/MycouponApiApplication.java

package com.example.mycouponapi;

import com.example.mycouponapi.dto.CouponIssueRequestDto_1;
import com.example.mycouponcore.MyCouponCoreConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

@Import(MyCouponCoreConfiguration.class) // 여기서 core쪽 bean을 사용하기 위해 import해줌
@SpringBootApplication
public class MycouponApiApplication {

    public static void main(String[] args) {
        System.setProperty("spring.config.name", "application-core, application-api");
        SpringApplication.run(MycouponApiApplication.class, args);
    }

}

 

 

마지막으로 예외가 발생했을 때 exception을 잡아주기 위해 excetionHandler 를 만들어 주자.

 

  • CouponControllerAdvice
// mycouponapi/exception/handler/CouponControllerAdvice.java

package com.example.mycouponapi.exception.handler;

import com.example.mycouponapi.dto.CouponIssueResponseDto;
import com.example.mycouponcore.exception.CouponIssueException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class CouponControllerAdvice {

    @ExceptionHandler(CouponIssueException.class)
    public CouponIssueResponseDto couponIssueExceptionHandler(CouponIssueException exception) {
        return new CouponIssueResponseDto(false, exception.getErrorCode().message);
    }
}

 

 

예외가 터지면 점점 위로 올라오기 때문에 잡아서 따로 처리를 해줘야 내가 원하는 핸들링을 해줄 수 있다.

이제 한번 테스트를 진행해보자.

  • 정상 응답 테스트

 

  • 중복 예외 발생 테스트(같은 요청 한번 더 보내면)

 

나머지 db 값을 바꿔가며 다른 예외가 발생하는 것을 확인해 보는 것이 좋다.

반응형