본문 바로가기
BackEnd/Project

[Loan] Ch03. 대출 신청 이용 약관 등록 기능 구현

by 개발 Blog 2024. 9. 12.

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

 

이번 장에서는 대출 신청 페이지에서 신청자가 이용 약관에 동의한 정보를 기록하는 기능을 구현한다. 앞서 등록된 약관 정보를 신청자에게 제공하고, 신청자가 해당 약관에 동의한 내용을 저장하여 추후 확인할 수 있도록 처리한다.

1. ApplicationController에 메서드 추가

약관에 동의한 정보를 저장하는 기능을 처리하기 위해 POST 메서드를 추가한다. 신청자가 동의한 약관 정보를 받아서 해당 신청 ID와 연관된 약관 동의 정보를 기록한다.

@PostMapping("/{applicationId}/terms")
public ResponseDTO<Boolean> acceptTerms(@PathVariable Long applicationId, @RequestBody AcceptTerms request){
    return ok(applicationService.acceptTerms(applicationId, request));
}
  • acceptTerms 메서드:
    클라이언트에서 대출 신청 ID와 함께 신청자가 동의한 약관 목록을 받아 처리한다. 약관 동의 정보를 저장하고, 성공 여부를 반환한다.

2. AcceptTerms 엔티티 추가

신청자가 동의한 약관 정보를 저장하기 위한 AcceptTerms 엔티티를 추가한다. 이 엔티티는 대출 신청 ID와 약관 ID를 저장하며, 신청자와 약관의 매핑 정보를 기록한다.

package com.example.loan.domain;

import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.Where;

import javax.persistence.*;

@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
@Where(clause = "is_deleted=false")
public class AcceptTerms extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false, updatable = false)
    @EqualsAndHashCode.Include
    private Long acceptTermsId;

    @Column(columnDefinition = "bigint NOT NULL COMMENT '신청 ID'")
    private Long applicationId;

    @Column(columnDefinition = "bigint NOT NULL COMMENT '약관 ID'")
    private Long termsId;
}
  • 필드:
    • applicationId: 대출 신청 ID.
    • termsId: 신청자가 동의한 약관 ID.

3. ApplicationDTO 수정

신청자가 동의한 약관 목록을 전달받기 위해 AcceptTerms DTO를 추가한다. 동의한 약관들의 ID 리스트를 포함하며, 이를 통해 여러 약관에 동의한 정보를 한 번에 저장할 수 있다.

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public static class AcceptTerms {
    List<Long> acceptTermsIds;
}

4. AcceptTermsRepository 추가

AcceptTermsRepository를 통해 약관 동의 정보를 데이터베이스에 저장하고 관리할 수 있도록 JPA Repository를 추가한다.

package com.example.loan.repository;

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

@Repository
public interface AcceptTermsRepository extends JpaRepository<AcceptTerms, Long> {

}

5. ApplicationService 및 ApplicationServiceImpl 수정

약관 동의 기능을 구현하기 위해 ApplicationService에 acceptTerms 메서드를 추가하고, 이를 ApplicationServiceImpl에서 구현한다.

Boolean acceptTerms(Long applicationId, AcceptTerms request);
public class ApplicationServiceImpl implements ApplicationService{

    private final TermsRepository termsRepository;
    private final AcceptTermsRepository acceptTermsRepository;
    
    ...
    
    @Override
    public Boolean acceptTerms(Long applicationId, ApplicationDTO.AcceptTerms request) {
        applicationRepository.findById(applicationId).orElseThrow(() -> {
            throw new BaseException(ResultType.SYSTEM_ERROR);
        });

        List<Terms> termsList = termsRepository.findAll(Sort.by(Direction.ASC, "termsId"));
        if (termsList.isEmpty()) {
            throw new BaseException(ResultType.SYSTEM_ERROR);
        }

        List<Long> acceptTermsIds = request.getAcceptTermsIds();
        if (termsList.size() != acceptTermsIds.size()) {
            throw new BaseException(ResultType.SYSTEM_ERROR);
        }

        List<Long> termsIds = termsList.stream().map(Terms::getTermsId).collect(Collectors.toList());
        Collections.sort(acceptTermsIds);

        if (!termsIds.containsAll(acceptTermsIds)) {
            throw new BaseException(ResultType.SYSTEM_ERROR);
        }

        for (Long termsId : acceptTermsIds) {
            AcceptTerms accepted = AcceptTerms.builder()
                    .termsId(termsId)
                    .applicationId(applicationId)
                    .build();
            acceptTermsRepository.save(accepted);
        }

        return true;
    }
  • 대출 신청 조회:
    먼저 applicationRepository를 통해 전달받은 applicationId로 해당 대출 신청 정보를 조회한다. 만약 존재하지 않으면 BaseException을 발생시킨다.
  • 약관 리스트 조회:
    termsRepository를 사용해 등록된 약관 리스트를 조회하고, 이를 신청자가 동의한 약관 목록과 비교한다. 약관이 등록되지 않았거나 신청자가 동의한 약관의 개수가 일치하지 않으면 예외를 발생시킨다.
  • 약관 ID 검증:
    신청자가 동의한 약관 리스트가 등록된 약관 리스트와 정확히 일치하는지 확인한다. 이때 신청자가 동의한 약관 ID를 정렬하여 등록된 약관 ID와 비교한다. 약관이 일치하지 않으면 예외가 발생한다.
  • 동의 정보 저장:
    검증이 완료되면 신청자가 동의한 약관 ID를 기반으로 AcceptTerms 엔티티를 생성하고, acceptTermsRepository를 통해 데이터베이스에 저장한다. 각 약관에 대한 동의 정보가 개별적으로 저장된다.
  • 결과 반환:
    모든 동의 정보가 정상적으로 저장되면 true를 반환하여 동의가 완료되었음을 나타낸다.

6. 테스트 코드 추가

약관 동의 기능이 정상적으로 동작하는지 검증하기 위해 테스트 코드를 작성한다.

@Mock
private TermsRepository termsRepository;

@Mock
private AcceptTermsRepository acceptTermsRepository;

...

@Test
void Should_AddAcceptTerms_When_RequestAcceptTermsOfApplication() {
    Terms entityA = Terms.builder()
            .termsId(1L)
            .name("대출 이용 약관 1")
            .termsDetailUrl("https://abc-storage.acc/dslfjdlsfjlsdddads")
            .build();

    Terms entityB = Terms.builder()
            .termsId(2L)
            .name("대출 이용 약관 2")
            .termsDetailUrl("https://abc-storage.acc/dslfjdlsfjlsdweqwq")
            .build();

    List<Long> acceptTerms = Arrays.asList(1L, 2L);

    ApplicationDTO.AcceptTerms request = ApplicationDTO.AcceptTerms.builder()
            .acceptTermsIds(acceptTerms)
            .build();

    Long findId = 1L;

    when(applicationRepository.findById(findId)).thenReturn(
            Optional.ofNullable(Application.builder().build()));
    when(termsRepository.findAll(Sort.by(Sort.Direction.ASC, "termsId"))).thenReturn(Arrays.asList(entityA, entityB));
    when(acceptTermsRepository.save(ArgumentMatchers.any(AcceptTerms.class))).thenReturn(AcceptTerms.builder().build());


    Boolean actual = applicationService.acceptTerms(findId, request);
    assertThat(actual).isTrue();
}

@Test
void Should_ThrowException_When_RequestNotAllAcceptTermsOfApplication() {
    Terms entityA = Terms.builder()
            .termsId(1L)
            .name("대출 이용 약관 1")
            .termsDetailUrl("https://abc-storage.acc/dslfjdlsfjlsdddads")
            .build();

    Terms entityB = Terms.builder()
            .termsId(2L)
            .name("대출 이용 약관 2")
            .termsDetailUrl("https://abc-storage.acc/dslfjdlsfjlsdweqwq")
            .build();

    List<Long> acceptTerms = Arrays.asList(1L);

    ApplicationDTO.AcceptTerms request = ApplicationDTO.AcceptTerms.builder()
            .acceptTermsIds(acceptTerms)
            .build();

    Long findId = 1L;

    when(applicationRepository.findById(findId)).thenReturn(
            Optional.ofNullable(Application.builder().build()));
    when(termsRepository.findAll(Sort.by(Sort.Direction.ASC, "termsId"))).thenReturn(Arrays.asList(entityA, entityB));

    assertThrows(BaseException.class, () -> applicationService.acceptTerms(findId, request));

}

@Test
void Should_ThrowException_When_RequestNotExistAcceptTermsOfApplication() {
    Terms entityA = Terms.builder()
            .termsId(1L)
            .name("대출 이용 약관 1")
            .termsDetailUrl("https://abc-storage.acc/dslfjdlsfjlsdddads")
            .build();

    Terms entityB = Terms.builder()
            .termsId(2L)
            .name("대출 이용 약관 2")
            .termsDetailUrl("https://abc-storage.acc/dslfjdlsfjlsdweqwq")
            .build();

    List<Long> acceptTerms = Arrays.asList(1L, 3L);

    ApplicationDTO.AcceptTerms request = ApplicationDTO.AcceptTerms.builder()
            .acceptTermsIds(acceptTerms)
            .build();

    Long findId = 1L;

    when(applicationRepository.findById(findId)).thenReturn(
            Optional.ofNullable(Application.builder().build()));
    when(termsRepository.findAll(Sort.by(Sort.Direction.ASC, "termsId"))).thenReturn(Arrays.asList(entityA, entityB));

    assertThrows(BaseException.class, () -> applicationService.acceptTerms(findId, request));

}

1) 정상적인 약관 동의 테스트 

이 테스트는 신청자가 모든 등록된 약관에 정상적으로 동의했을 때, 동의 정보가 데이터베이스에 올바르게 저장되는지 확인한다.

  1. 먼저 TermsRepository에서 약관 2개(entityA, entityB)가 반환되도록 설정한다.
  2. 신청자가 2개의 약관(acceptTerms)에 동의한 요청을 생성하고, 해당 요청을 테스트한다.
  3. applicationRepository에서 대출 신청 정보를 조회하고, 동의한 약관 리스트가 등록된 약관 리스트와 일치하는지 검증한다.
  4. AcceptTermsRepository를 통해 동의한 정보가 정상적으로 저장되었는지 확인한다.
  5. 최종적으로 acceptTerms 메서드의 반환 값이 true임을 검증하여 동의 과정이 정상적으로 완료되었음을 확인한다.

2) 일부 약관 미동의 테스트 

이 테스트는 신청자가 일부 약관에만 동의한 경우, 예외가 발생하는지 확인한다. 약관 전체에 동의하지 않은 경우에는 예외가 발생해야 한다.

  1. TermsRepository에서 약관 2개가 반환되도록 설정하지만, 신청자는 1개의 약관에만 동의한다(acceptTerms).
  2. 약관 전체에 동의하지 않았으므로, acceptTerms 메서드 실행 시 BaseException이 발생해야 한다.
  3. 테스트는 이 예외가 발생했는지 검증한다.

3) 존재하지 않는 약관에 대한 동의 테스트 

이 테스트는 신청자가 존재하지 않는 약관에 동의하려고 할 때 발생하는 예외를 검증한다. 등록되지 않은 약관에 대해 동의 요청을 보낼 경우, 예외가 발생해야 한다.

  1. TermsRepository에서 두 개의 약관이 반환되지만, 신청자는 존재하지 않는 약관 ID(1L, 3L)에 동의한다.
  2. 등록되지 않은 약관(3L)에 대해 동의하려고 시도하므로 BaseException이 발생해야 한다.
  3. 테스트는 이 예외가 발생했는지 검증한다.

7. 실행 테스트

이제 구현한 기능을 실제로 테스트해 본다. 테스트는 약관 등록, 대출 신청, 그리고 약관 동의 등록을 차례대로 진행하면서 발생할 수 있는 예외 상황도 함께 검증한다.

 

1) 약관 두 개 등록

먼저 POST /terms API를 사용해 약관을 두 개 등록한다. 각 약관은 고유한 ID와 약관 상세 URL을 가진다.

 

2) 대출 신청 등록

다음으로 POST /applications API를 사용하여 대출 신청을 진행한다. 신청자는 기본적인 개인정보와 함께 대출 상담에 대한 메모, 주소 등을 입력하여 신청을 완료한다.

 

3) 신청자가 약관에 동의 등록

이제 POST /applications/{applicationId}/terms API를 통해 신청자가 등록한 약관에 동의할 수 있다. 예를 들어, 약관 1번과 2번에 대해 동의한 경우, 서버는 성공 응답을 반환한다.

4) 예외 상황 테스트

4-1 없는 약관에 대한 동의 시 에러 발생

존재하지 않는 약관 ID(예: 3번)에 대해 동의하려고 시도하면, 서버는 시스템 에러를 반환한다.

 

4.2. 일부 약관만 동의 시 에러 발생

등록된 약관이 두 개인데, 그 중 하나만 동의하려고 할 때도 시스템 에러가 발생한다.

 

이로써 대출 신청 시 약관 동의 기능 구현이 완료되었다. 신청자가 등록된 약관에 동의한 정보를 기록함으로써 서비스 이용 약관에 대한 명확한 관리를 할 수 있다. 다음 장에서는 대출 신청과 관련된 추가적인 기능을 구현할 예정이다.