본문 바로가기
BackEnd/Project

[Loan] Ch06. 대출 상환 등록 기능 구현

by 개발 Blog 2024. 9. 14.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

이번 장에서는 대출 상환 등록 기능을 구현하며, 기존의 RepaymentService와 BalanceService에 새로운 메서드를 추가해 상환 처리 및 잔액 업데이트 로직을 완성한다. 상환 등록 기능은 상환 금액을 기록하고, 그에 따른 잔액 변동을 처리하는 중요한 기능이다. 이를 통해 사용자는 실시간으로 상환 내역과 남은 대출 잔액을 확인할 수 있다.

RepaymentDTO 클래스

RepaymentDTO 클래스에는 상환 요청 및 응답을 처리하기 위한 Request와 Response 객체가 추가되었다.

package com.example.loan.dto;

import lombok.*;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

public class RepaymentDTO implements Serializable {

    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    @Getter
    @Setter
    public static class Request {

        private BigDecimal repaymentAmount;
    }

    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    @Getter
    @Setter
    public static class Response {

        private Long applicationId;

        private BigDecimal repaymentAmount;

        private BigDecimal balance;

        private LocalDateTime createdAt;

        private LocalDateTime updatedAt;
    }
}
  • Request: 사용자가 상환하려는 금액을 받기 위한 필드가 포함되었다.
  • Response: 상환 후 반환할 응답 데이터로, 상환 금액, 잔액, 그리고 생성 및 수정 시간을 포함하여 상환 처리 결과를 클라이언트에게 전달한다.

RepaymentRepository 인터페이스

이 클래스에서는 데이터베이스에 상환 내역을 저장하고 관리하기 위해 JPA 기반의 레포지토리를 제공한다. 상환 내역을 CRUD 방식으로 다루기 위한 메서드들이 기본 제공된다.

package com.example.loan.repository;

import com.example.loan.domain.Repayment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface RepaymentRepository extends JpaRepository<Repayment, Long> {

}

RepaymentService 및 RepaymentServiceImpl 클래스

이 두 클래스는 새롭게 추가된 클래스이며, 대출 상환 요청을 처리하는 핵심 비즈니스 로직을 포함하고 있다.

 

RepaymentService 인터페이스

상환 관련 서비스 메서드의 구조를 정의한다.

package com.example.loan.service;

import com.example.loan.dto.RepaymentDTO;

import static com.example.loan.dto.RepaymentDTO.*;

public interface RepaymentService {
    Response create(Long applicationId, Request request);
}
  • 이 메서드는 상환 요청을 받아 처리한 후 상환 결과를 반환하는 메서드이다. 상환 가능 여부를 확인하고, 상환 내역을 데이터베이스에 저장한 후 잔액을 업데이트한다. 이를 통해 전체적인 상환 처리 흐름을 관리한다.

RepaymentServiceImpl 클래스

RepaymentService 인터페이스를 구현하여 실제 상환 로직을 처리한다.

package com.example.loan.service;

import com.example.loan.domain.Application;
import com.example.loan.domain.Entry;
import com.example.loan.domain.Repayment;
import com.example.loan.dto.BalanceDTO;
import com.example.loan.dto.RepaymentDTO;
import com.example.loan.exception.BaseException;
import com.example.loan.exception.ResultType;
import com.example.loan.repository.ApplicationRepository;
import com.example.loan.repository.EntryRepository;
import com.example.loan.repository.RepaymentRepository;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.stereotype.Service;

import java.util.Optional;

import static com.example.loan.dto.BalanceDTO.RepaymentRequest.*;
import static com.example.loan.dto.RepaymentDTO.*;

@Service
@RequiredArgsConstructor
public class RepaymentServiceImpl implements RepaymentService {

    private final RepaymentRepository repaymentRepository;

    private final ApplicationRepository applicationRepository;

    private final EntryRepository entryRepository;

    private final BalanceService balanceService;

    private final ModelMapper modelMapper;

    @Override
    public Response create(Long applicationId, Request request) {
        if (!isRepayableApplication(applicationId)) {
            throw new BaseException(ResultType.SYSTEM_ERROR);
        }

        Repayment repayment = modelMapper.map(request, Repayment.class);
        repayment.setApplicationId(applicationId);

        repaymentRepository.save(repayment);

        BalanceDTO.Response updatedBalance = balanceService.repaymentUpdate(applicationId,
                BalanceDTO.RepaymentRequest.builder()
                        .repaymentAmount(request.getRepaymentAmount())
                        .type(RepaymentType.REMOVE)
                        .build());

        Response response = modelMapper.map(repayment, Response.class);
        response.setBalance(updatedBalance.getBalance());

        return response;
    }

    private boolean isRepayableApplication(Long applicationId) {
        Optional<Application> existedApplication = applicationRepository.findById(applicationId);
        if (existedApplication.isEmpty()) {
            return false;
        }

        if (existedApplication.get().getContractedAt() == null) {
            return false;
        }

        Optional<Entry> existedEntry = entryRepository.findByApplicationId(applicationId);
        return existedEntry.isPresent();
    }
}

BalanceService 및 BalanceServiceImpl 클래스

이 두 클래스는 기존 클래스에 메서드가 추가된 케이스이다. 잔액을 관리하는 기능을 제공하며, 이번 업데이트에서는 상환에 따른 잔액 업데이트 로직이 추가되었다.

 

BalanceService 인터페이스

잔액 업데이트 관련 서비스 메서드를 정의한다.

Response repaymentUpdate(Long applicationId, RepaymentRequest request);
  • 상환 요청에 따라 잔액을 업데이트하는 기능을 제공한다. 상환 금액을 잔액에서 차감하거나, 롤백 시 다시 잔액에 더하는 처리를 한다.

BalanceServiceImpl 클래스

BalanceService 인터페이스를 구현하여 상환 후 잔액을 처리하는 로직을 포함한다.

@Override
public Response repaymentUpdate(Long applicationId, RepaymentRequest request) {
    Balance balance = balanceRepository.findByApplicationId(applicationId).orElseThrow(() -> {
        throw new BaseException(ResultType.SYSTEM_ERROR);
    });

    BigDecimal updatedBalance = balance.getBalance();
    BigDecimal repaymentAmount = request.getRepaymentAmount();

    // 상환 정상: balance - repaymentAmount
    // 상환금 롤백 : balance + repaymentAmount
    if (request.getType().equals(RepaymentType.ADD)) {
        updatedBalance = updatedBalance.add(repaymentAmount);
    } else {
        updatedBalance = updatedBalance.subtract(repaymentAmount);
    }

    balance.setBalance(updatedBalance);

    Balance updated = balanceRepository.save(balance);

    return modelMapper.map(updated, Response.class);
}
  • 잔액 조회: 먼저 해당 대출 신청의 잔액을 조회한 후, 요청 타입에 따라 잔액에서 상환 금액을 차감하거나(REMOVE), 롤백 시 잔액에 다시 더한다(ADD).
  • 잔액 저장 및 반환: 계산된 잔액을 데이터베이스에 저장하고, 그 결과를 응답 객체로 반환하여 상환 후 잔액 변동 내역을 클라이언트에게 전달한다.

BalanceDTO 클래스

RepaymentRequest객체가 추가되었다.

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Setter
public static class RepaymentRequest {

    public enum RepaymentType {
        ADD,
        REMOVE
    }

    private RepaymentType type;

    private BigDecimal repaymentAmount;
}
  • 상환 요청 처리: 이 메서드는 상환 요청을 처리하는 비즈니스 로직을 구현한다. 상환할 대출 신청이 유효한지 검증한 후, 상환 내역을 저장하고 잔액을 업데이트한다.
  • 검증 로직: 상환 가능한 대출 신청인지 확인하는 isRepayableApplication 메서드를 통해, 대출 신청 상태 및 관련된 엔트리(Entry)를 검사하여 상환이 가능한지 여부를 판단한다.
  • 잔액 업데이트: 상환이 완료되면 BalanceService를 호출해 잔액을 업데이트하고, 그 결과를 응답으로 반환한다.

InternalController 클래스

@PostMapping("{applicationId}/repayments")
public ResponseDTO<RepaymentDTO.Response> create(@PathVariable Long applicationId, @RequestBody RepaymentDTO.Request request) {
    return ok(repaymentService.create(applicationId, request));
}
  • 클라이언트로부터 상환 요청을 받아, 해당 요청을 RepaymentService로 전달하는 역할을 한다. 이 메서드는 상환 요청이 성공적으로 처리되면 상환 내역과 잔액 정보를 응답으로 반환한다.

실행 테스트 

대출 상환 등록 기능 구현을 완료한 후, 실제로 기능이 정상 동작하는지 확인하기 위해 다음과 같은 실행 테스트를 진행한다.

  • 신청 등록 (500만 원)
    • 대출 신청을 500만 원으로 등록한다. 이 단계에서는 대출 신청이 정상적으로 시스템에 기록된다.
  • 심사 등록 (500만 원)
    • 신청된 금액에 대해 심사 과정을 등록한다. 심사 결과는 신청한 500만 원에 대해 진행되며, 정상적으로 심사 결과가 기록된다.
  • 한도 부여 (500만 원)
    • 대출 가능 한도를 500만 원으로 설정한다. 한도가 정상적으로 부여된 후 대출 계약과 집행이 가능해진다.
  • 대출 계약 (500만 원)
    • 한도 부여 후, 500만 원에 대해 대출 계약을 체결한다. 계약이 체결된 후, 대출 집행이 가능해진다.
  • 대출 집행 (500만 원)
    • 계약 체결 후 500만 원이 집행된다. 집행 완료 후 사용자는 대출금이 지급되었으며, 잔액은 500만 원으로 기록된다.
  • 대출 상환 (100만 원)
    • 500만 원 중 100만 원을 상환하는 요청을 보낸다. 상환 요청이 정상적으로 처리되면 잔액이 400만원으로 갱신된다.

위 흐름에 따라 100만원을 상환한 후 서버 응답을 확인해 보면, 상환 처리 결과가 정상적으로 반환되었음을 확인할 수 있다.

상환 기능이 정상적으로 동작하며, 상환 요청에 따른 잔액 변동이 정확하게 처리됨을 확인할 수 있었다. 500만 원에서 100만 원 상환 후 남은 잔액이 400만 원으로 정확하게 계산되었으며, 상환 내역과 함께 응답이 반환되었다. 이를 통해 대출 상환 기능의 안정성을 검증할 수 있었다.