토스페이먼츠 시리즈 (10) _ 지급대행 API 서브몰 - 등록 & 토스 측 오류 수정

2022. 5. 6. 13:00·프로젝트/토스페이먼츠 PG 연동 시리즈

지급대행

앞에서 우리 서비스에 등록된 판매자들이 정산받기 위해 등록하는 계좌에 대한 검증을 금융결제원의 계좌실명조회 API를 통해 하려고 했었다. 하지만 이는 일반적인 가맹점에서는 불가능하고 토스페이먼츠에서 제공하는 API를 통해서 서브몰들을 등록/조회/정산 등을 할 수 있다. 따라서 기존 금융결제원 방식의 계좌번호 실명조회 기능을 버리고 토스페이먼츠에서 제공하는 지급대행 API를 통해서 서브몰을 등록하고 조회 후 정산 관련 업무를 위한 기능까지 만들어보겠다.


서브몰 구조 & 등록 구현

우리 서비스를 이용하는 판매자는 우선적으로 자신의 은행, 계좌번호 등을 등록하여 서브몰을 등록해줘야 한다. 이때 서브몰의 고유한 아이디, 상호명, 대표명, 사업자 등록번호, 개인/법인 등이 필요하고 정산을 받기 위한 은행 - 계좌번호가 필요하다.

 

서브몰 등록을 위해 받는 DTO 클래스

@Data
public class SubmallReqDto {
	@ApiModelProperty(value = "코디네이터 이메일", required = true)
	private String crdiEmail;
	@ApiModelProperty(value = "상호 명", required = true)
	private String companyName;
	@ApiModelProperty(value = "대표명", required = true)
	private String representativeName;
	@ApiModelProperty(value = "사업자 유형(개인/법인)", required = true)
	private String type;
	@ApiModelProperty(value = "사업자 등록 번호 (법인의 경우 입력)", example = "0000000000")
	private String businessNumber;
	private SubmallAccountDto account;
    
	public Submall toEntity(String subMallId) {
		...
	}

	public TosspaymentSubmallReq toTossReq() {
		...
	}
}
@Data
public class SubmallAccountDto {
	@ApiModelProperty(value = "은행명", required = true)
	private BANK_CODE bank;
	@ApiModelProperty(value = "계좌번호", required = true)
	private String accountNumber;
	@ApiModelProperty(value = "예금주", required = true)
	private String holderName;

	public TosspaymentSubmallAccountDto toTossAccount() {
		...
	}
}

해당 DTO들을 통해서 사용자들의 요청 정보를 받게 된다.

Entity 서브몰 객체

public class Submall {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "seq", nullable = false)
	private Long seq;

	@Column(nullable = false)
	private String crdiEmail;

	@Column(nullable = false)
	private String submallId;

	@Column(nullable = false)
	private String companyName;

	@Column(nullable = false)
	private String representativeName;

	@Column
	private String businessNumber;

	@Column
	private String type;

	@Column(nullable = false)
	private String accountNumber;

	@Column(nullable = false)
	private String bank;

	@Setter
	@Column(nullable = false)
	private boolean activate;

	@Setter
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "CRDI_SEQ")
	private Member crdi;

	public SubmallResDto toDto() {
		return SubmallResDto.builder()
				.crdiEmail(crdiEmail)
				.submallId(submallId)
				.companyName(companyName)
				.representativeName(representativeName)
				.businessNumber(businessNumber)
				.type(type)
				.accountNumber(accountNumber)
				.bank(bank)
				.build();
	}
}
  • 실제 DB에 테이블로서 존재하게 될 서브몰 객체이다.
  • 고객 당 활성화된 서브몰을 하나씩만 갖게 하도록 하기 위해 activate를 사용하게 했다.

Controller 서브몰 등록 API

public class SubmallController {

	private final SubmallService submallService;
	private final ResponseService responseService;

	@PostMapping
	@ApiOperation(value = "서브몰 등록", notes = "판매자가 은행, 계좌번호 등을 이용해 자신의 서브몰을 등록합니다.")
	public SingleResult<SubmallResDto> registSubmall(
			@ApiParam(value = "요청 객체") @ModelAttribute SubmallReqDto submallReqDto
	) {
		try {
			return responseService.getSingleResult(
					submallService.registSubmall(submallReqDto)
			);
		} catch (Exception e) {
			e.printStackTrace();
			throw new BussinessException(e.getMessage());
		}
	}
}
  • 먼저 프론트측에서 서브몰을 등록 할 수 있도록 API를 열어주고 Service 쪽에서 토스페이먼츠의 API를 호출하여 서브몰을 생성하게 된다.
  • 사용자가 서브몰을 생성할 때 전달하는 은행, 계좌번호, 예금주 등의 정보를 통해 토스페이먼츠 측에서 제대로 된 정보인지 검증 후 알려주게 된다.

잘못된 서브몰 생성 요청 시 응답 전문

Service 서브몰 등록 구현

@Transactional
public SubmallResDto registSubmall(SubmallReqDto submallReqDto) {

    Optional<Submall> submallOptional = submallRepository
            .findByCrdiEmailAndActivateTrue(submallReqDto.getCrdiEmail());
    if (submallOptional.isPresent()) {
        throw new BussinessException(ExMessage.SUBMALL_ERROR_ALREADY_REGIST);
    }

    if (!(submallReqDto.getType().equals("법인") || submallReqDto.getType().equals("개인"))) {
        throw new BussinessException(ExMessage.SUBMALL_ERROR_WRONG_BUSINESS_NUMBER);
    }

    if (submallReqDto.getType().equals("법인") && submallReqDto.getBusinessNumber().length() != 10) {
        throw new BussinessException(ExMessage.SUBMALL_ERROR_WRONG_BUSINESS_NUMBER);
    }

    RestTemplate rest = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    String encodedAuth = new String(Base64.getEncoder().encode((testSecretApiKey + ":").getBytes(StandardCharsets.UTF_8)));
    headers.setBasicAuth(encodedAuth);
    headers.setContentType(MediaType.APPLICATION_JSON);
    headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

    TosspaymentSubmallReq submallReq = submallReqDto.toTossReq();
    TosspaymentSubmallRes subMallRes;
    try {
        subMallRes = rest.exchange(
                tossOriginUrl + "/payouts/sub-malls",
                HttpMethod.POST,
                new HttpEntity<>(new Gson().toJson(submallReq), headers),
                TosspaymentSubmallRes.class
        ).getBody();
        if (subMallRes == null) throw new BussinessException("NULL");
    } catch (Exception e) {
        e.printStackTrace();
        String errorResponse = e.getMessage().split(": ")[1];
        String errorMessage = new Gson()
                .fromJson(
                        errorResponse.substring(1, errorResponse.length() - 1),
                        TossErrorDto.class
                ).getMessage();
        throw new BussinessException(errorMessage);
    }

    try {
        Submall submall = submallReqDto.toEntity(submallReq.getSubMallId());
        memberRepository.findByEmailFJ(submallReqDto.getCrdiEmail())
                .ifPresentOrElse(
                        C -> C.addSubmalls(submall)
                        , () -> {
                            throw new BussinessException(ExMessage.MEMBER_ERROR_NOT_FOUND);
                        });
        return submall.toDto();
    } catch (Exception e) {
        throw new BussinessException(ExMessage.DB_ERROR_SAVE);
    }
}
  • 먼저 해당 회원 앞으로 활성화된 서브몰이 존재하는지 확인한다.
  • 서브몰을 등록하기 위해 필요한 정보를 담은 객체를 토스페이먼츠 측에 요청을 위한 형태로 변경 후 보낸다. 정상적으로 이뤄지면 해당 회원에 서브몰 연관관계를 맺어줌으로서 레포지토리에 저장한다.
  • 서브몰 등록을 요청할 때 RequestBody에 JSON 형식으로 요청 데이터를 담아서 보내야 한다. 따라서 HttpEntity의 body부분에 Gson을 이용해서 JSON문자열을 담아줬다.
  • 저장 후 응답객체로 변환하여 반환해준다. 만약 토스페이먼츠 측에서 에러를 감지하여 전달하면 해당 내용을 반환하게 된다.

서브몰 등록 테스트

이제 서브몰 등록 API를 완성했으니 테스트를 진행해 보겠다. 필요로 하는 값들을 넣고 요청을 보낸다.

테스트 실패 정리

사실 위에처럼 하면 잘 요청이 가고 등록도 된다. 하지만 나는 두가지 이유로 실패했는데 해당 부분을 정리하고자 한다.

  1. 위에서는 요청 시 type 매개변수를 통해 (개인/법인)을 넣어주고 있지만 처음에는 넣지 않았었다.
  2. 서브몰을 구분하기 위한  고유한 키로서 필요한 subMallId에 UUID를 자동생성 후 넣어줬었다.

1. TYPE

참고로 이 당시에는 토스페이먼츠의 서브몰 등록 API 설명 문서에 TYPE 관련 설명이 나와있지 않았습니다.

 

먼저 TYPE 관련 문제를 살펴보자. 필자는 공식 문서에 나와있는대로 필수값들을 다 넣고 요청을 진행했었다. 그런데 요청을 하면 필수 파라미터라고 나와있는 부분을 다 입력했음에도 불구하고 필수 파라미터를 누락했다는 에러메시지가 반환되어 다시 토스페이먼츠 공식 문서를 찾아보았다.

TYPE 관련 내용은 없다.

공식문서에는 위와 같이 서브몰 아이디, 계좌정보 등이 필수라고 되어있는 것을 볼 수 있다. 현재 나는 서브몰 ID를 UUID로 만들어주고 있으므로 입력할 필요는 없었고 그 외 정보는 모두 입력해줬는데 도데체 어디가 누락되었다는 것인지 알 수가 없었다. 헤더에도 Basic 인가코드를 제대로 넣어주고 있었으므로 인가코드 문제 또한 아니었다.

 

그런데 토스페이먼츠의 API 테스트를 확인해보니 정작 테스트에서는 앞에 나와있던 서브몰 등록 API와 다른 파라미터를 추가적으로 받고있었고 필수 여부도 달랐다.

 

처음보는 account.holderName, type 파라미터가 존재하는 것을 볼 수 있다. holderName이야 필수값이 아니니 논외하더라도, API 문서에서는 못보던 필수 파라미터 TYPE이 추가된 것이 문제였다.

여기서 테스트를 해봐도 필수 파라미터가 누락되었다고 나오는 것을 확인할 수 있었다. TYPE에 필요한 값이 뭔질 모르니 당연히 누락일 수 밖에 없긴하다.

TYPE에 어떠한 값이 들어가야되는지 전혀 알 수가 없었으므로 결국 토스페이먼츠 측에 문의를 넣어보았다.

이렇게 문의 후 기다려보았고

연락이 왔다. 따로 공유는 받지 못했지만 공식문서를 확인해보니 변경된 것을 볼 수 있었다.

이렇게 type에 대한 정의가 추가되었고, type이 개인/법인 일 때에 맞춰서 어떤 값들이 필수인지 추가 되었다. 결국 공식문서에 정확한 내용이 나와있지 않아서 문의를 넣었었고, 그 결과 API에 맞게 정확하게 문서가 수정된 것을 확인할 수 있었다.

2. subMallId

서브몰을 구분하기 위한 고유한 값으로 필수 값이다. 필자는 해당 값을 따로 사용자에게 받지 않고 서브몰을 신청하면 UUID를 통해 중복이없는 값을 자동 생성 후 넣어주도록 했다. 하지만 지속적으로 등록에 실패하였다.

 

알고보니 subMallId로 가질 수 있는 최대 길이가 20byte 였고, uuid는 24byte이므로 실패하게 된 것이였다. 이에 날짜 & 시간을 이용한 방식으로 서브몰id를 생성 후 넣어주도록 변경하였다. 문서에 subMallId가 최대로 가질 수 있는 길이를 알려줬으면 더 좋았을거 같다.

 

어쨋든 위와같은 방법으로 두가지 오류를 해결할 수 있었다.


테스트

요청 후 정상적으로 등록 후 반환된 것을 볼 수 있다.


Reference

https://docs.tosspayments.com/guides/apis/payout-submall#%EC%84%9C%EB%B8%8C%EB%AA%B0-%EC%A0%95%EB%B3%B4-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0.

반응형
저작자표시 비영리 변경금지 (새창열림)

'프로젝트 > 토스페이먼츠 PG 연동 시리즈' 카테고리의 다른 글

토스페이먼츠 시리즈 (마지막) _ 마무리  (0) 2022.05.10
토스페이먼츠 시리즈 (11) _ 지급대행 API 서브몰 - 조회, 수정, 삭제  (1) 2022.05.10
토스페이먼츠 시리즈 (9) _ 가상계좌 결제취소  (1) 2022.05.02
토스페이먼츠 시리즈 (8) _ 가상계좌 결제 2  (0) 2022.04.28
토스페이먼츠 시리즈 (7) _ 가상계좌 결제 1  (0) 2022.04.24
'프로젝트/토스페이먼츠 PG 연동 시리즈' 카테고리의 다른 글
  • 토스페이먼츠 시리즈 (마지막) _ 마무리
  • 토스페이먼츠 시리즈 (11) _ 지급대행 API 서브몰 - 조회, 수정, 삭제
  • 토스페이먼츠 시리즈 (9) _ 가상계좌 결제취소
  • 토스페이먼츠 시리즈 (8) _ 가상계좌 결제 2
구름뭉치
구름뭉치
구름의 개발일기장
    반응형
  • 구름뭉치
    구름 개발일기장
    구름뭉치
  • 전체
    오늘
    어제
    • ALL (283)
      • 프로젝트 (23)
        • 토스페이먼츠 PG 연동 시리즈 (12)
        • JWT 방식 인증&인가 시리즈 (6)
        • 스우미 웹 애플리케이션 프로젝트 (1)
        • 스프링부트 기본 보일러 플레이트 구축 시리즈 (2)
        • 마이크로서비스 아키텍쳐 시리즈 (1)
      • 스프링 (43)
        • 스프링부트 API 설계 정리 (8)
        • 스프링부트 RestAPI 프로젝트 (18)
        • 스프링부트 WebSocket 적용기 (3)
        • 스프링 JPA 정리 시리즈 (5)
        • 스프링 MVC (5)
        • 스프링 배치 (2)
        • 토비의 스프링 정리 (2)
      • 기술 학습 (28)
        • 아파치 카프카 (9)
        • 클린 코드 (4)
        • 디자인 패턴의 아름다움 (2)
        • 모던 자바 인 액션 (7)
        • JVM 스레드 딥다이브 (6)
      • Web (25)
        • 정리글 (20)
        • GraphQL 정리글 (2)
        • Jenkins 정리글 (3)
      • 취업 (6)
      • CS (77)
        • 네트워크 전공 수업 정리 (11)
        • OSI 7계층 정리 (12)
        • 운영체제 정리 (19)
        • 데이터베이스 정리 (5)
        • MySql 정리 (17)
        • GoF의 Design Pattern 정리 (12)
      • 알고리즘 (70)
        • 백준 (56)
        • 프로그래머스 (12)
        • 알고리즘 정리본 (1)
      • 기초 지식 정리 (2)
      • 일상 (8)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    마우스 패드
    mx master s3 for mac
    마우스
    동유럽
    크로아티아
    류블라냐
    키보드 손목 받침대
    레이저
    부다페스트
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
구름뭉치
토스페이먼츠 시리즈 (10) _ 지급대행 API 서브몰 - 등록 & 토스 측 오류 수정
상단으로

티스토리툴바