계좌 생성 기능 구현

날아올라돼지야·2024년 8월 19일
0

앞서 엔티티와 레포지토리 그리고 dto를 작성해봤습니다.
이제 실제 사용자 요청에 따른 응답을 정의하기 위한 엔드포인트를 작성해보겠습니다.
그러기 위해 컨트롤러를 먼저 작성해보겠습니다.

AccountController에서 REST API 생성

createAccount() 메서드를 작성하여 계정 생성을 처리합니다. 이 메서드는 CustomerDto 객체를 요청 본문으로 받아 이를 처리합니다. POST 요청을 사용하며, 생성된 계정에 대해 201 상태 코드를 반환합니다.

AccountController 생성

먼저 AccountController를 생성하고 /api 라는 접두사로 시작하는 모든 요청을 받기 위해 클래스 레벨에서 /api path를 정의합니다. 그리고 모든 응답을 JSON 형식으로 반환하도록 설정합니다.

그 다음 createAccount() 메서드를 작성하여 계정 생성을 처리합니다. 이 메서드는 CustomerDto 객체를 요청 본문으로 받아 이를 처리합니다. POST 요청을 사용하며, 생성된 계정에 대해 201 상태 코드를 반환합니다.

작성한 코드는 아래와 같습니다.

@RestController
@RequestMapping(path="/api", produces = {MediaType.APPLICATION_JSON_VALUE})
@AllArgsConstructor
@Validated
public class AccountController {

    private AccountService accountService;

    @PostMapping("/create")
    public ResponseEntity<ResponseDto> createAccount(@Valid @RequestBody CustomerDto customerDto) {
        accountService.createAccount(customerDto);
        return ResponseEntity
                .status(HttpStatus.CREATED)
                .body(new ResponseDto(AccountConstant.STATUS_201, AccountConstant.MESSAGE_201));
    }
	
}

AccountConstant 생성

AccountConstant 클래스는 따로 분리하여 API 응답 시 사용할 상태코드와 메시지를 담는 역할을 합니다. 이 클래스는 상수만을 포함하도록 설계되었으며, 생성자를 private으로 설정하여 인스턴스화되지 않도록 합니다.

public class AccountConstant {
    private AccountConstant() {
        // restrict instantiation
    }

    public static final String  SAVINGS = "Savings";
    public static final String  ADDRESS = "123 Main Street, New York";
    public static final String  STATUS_201 = "201";
    public static final String  MESSAGE_201 = "Account created successfully";
    public static final String  STATUS_200 = "200";
    public static final String  MESSAGE_200 = "Request processed successfully";
    public static final String  STATUS_417 = "417";
    public static final String  MESSAGE_417_UPDATE= "Update operation failed. Please try again or contact Dev team";
    public static final String  MESSAGE_417_DELETE= "Delete operation failed. Please try again or contact Dev team";
    // public static final String  STATUS_500 = "500";
    // public static final String  MESSAGE_500 = "An error occurred. Please try again or contact Dev team";

}

AccountService 인터페이스 생성

비즈니스 로직을 처리할 서비스 레이어를 AccountsService 인터페이스와 AccountServiceImpl 구현 클래스로 분리하여 작성합니다. 서비스 클래스는 @Service 어노테이션을 통해 스프링 부트에서 관리되는 빈으로 등록되며, 자동으로 AccountRepository와 CustomerRepository를 주입받습니다.

public interface AccountService {
    void createAccount(CustomerDto customerDto);
}

AccountServiceImpl 생성

실제 구현 클래스인 Impl 클래스를 작성합니다.

public class AccountServiceImpl implements AccountService {

    private AccountRepository accountRepository;
    private CustomerRepository customerRepository;
    /**
     * @param customerDto - CustomerDto Object
     */
    @Override
    public void createAccount(CustomerDto customerDto) {
        Customer customer = CustomerMapper.mapToCustomer(customerDto, new Customer());
        Optional<Customer> optionalCustomer = customerRepository.findByMobileNumber(customerDto.getMobileNumber());

        Customer savedCustomer = customerRepository.save(customer);
        accountRepository.save(createNewAccount(savedCustomer));
    }

    private Account createNewAccount(Customer customer) {
        Account newAccount = new Account();
        newAccount.setCustomerId(customer.getCustomerId());
        long randomAccNumber = 1000000000L + new Random().nextInt(900000000);

        newAccount.setAccountNumber(randomAccNumber);
        newAccount.setAccountType(AccountConstant.SAVINGS);
        newAccount.setBranchAddress(AccountConstant.ADDRESS);
        return newAccount;
    }
}
  • CustomerRepository로부터 고객 정보를 저장하고 AccountRepository에 해당 고객 정보를 포함하여 새로운 계좌 정보를 저장합니다.

Exception 처리

현재 로직에선 고객의 핸드폰 번호가 고유한 필드입니다. 그래서 고객 중복 방지를 위해 핸드폰 번호를 사용해봅니다.

이를 위해 별도의 예외 클래스를 만들어봅니다.
CustomerAlreadyExistsException 이름의 클래스와 그 안에 코드를 아래와 같이 작성합니다.

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class CustomerAlreadyExistsException extends RuntimeException {
    public CustomerAlreadyExistsException(String message) {
        super(message);
    }
}

예외를 발생시키는 코드는 createNewAccount 메서드 내에 있습니다.

위에서 생성한 createNewAccount 코드를 아래와 같이 수정합니다.

public void createAccount(CustomerDto customerDto) {
    Customer customer = CustomerMapper.mapToCustomer(customerDto, new Customer());
    Optional<Customer> optionalCustomer = customerRepository.findByMobileNumber(customerDto.getMobileNumber());

    if (optionalCustomer.isPresent()) {
        throw new CustomerAlreadyExistsException("Customer already registered with given mobile phone number : " + customerDto.getMobileNumber());
    }
    
    Customer savedCustomer = customerRepository.save(customer);
    accountRepository.save(createNewAccount(savedCustomer));
}

Optional 객체의 isPresent 메서드를 활용해 이미 DB에 고객 정보가 있다면 예외를 발생시키는 코드를 추가합니다.

예외처리는 어디에서?

예외가 발생하면 현재 코드를 호출하는 컨트롤러에서 예외처리를 할 수 있겠습니다. 하지만 스프링은 전역적으로 예외를 처리할 수 있도록 편의성을 제공합니다. 전역 예외 처리를 활용해 매번 try ~ catch를 쓰지 않고 예외를 처리해 보도록 합니다.

exception/GlobalExceptionHandler.java

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(CustomerAlreadyExistsException.class)
    public ResponseEntity<ErrorResponseDto> handleCustomerAlreadyExistsException(CustomerAlreadyExistsException exception,
                                                                                 WebRequest webRequest){
        ErrorResponseDto errorResponseDTO = new ErrorResponseDto(
                webRequest.getDescription(false),
                HttpStatus.BAD_REQUEST,
                exception.getMessage(),
                LocalDateTime.now()
        );
        return new ResponseEntity<>(errorResponseDTO, HttpStatus.BAD_REQUEST);
    }
}

CustomerAlreadyExistsException 예외에 대해 전역으로 처리하는 코드입니다. 예외가 발생한 곳에서 따로 예외 처리 코드를 추가하지 않더라도 위의 코드가 예외를 처리해줄 수 있습니다.

추후에 더 다양한 예외에 대해서 처리하는 코드를 작성해보겠습니다.

Mapper 생성

DTO와 엔티티 간 데이터를 변환하기 위해 Mapper 패키지를 생성하고, AccountsMapper와 CustomerMapper 클래스를 작성합니다. 이 클래스들은 DTO를 엔티티로, 엔티티를 DTO로 변환하는 메서드를 포함합니다.

CustomerMapper

public class CustomerMapper {
    public static CustomerDto mapToCustomerDto(Customer customer, CustomerDto customerDto) {
        customerDto.setName(customer.getName());
        customerDto.setEmail(customer.getEmail());
        customerDto.setMobileNumber(customer.getMobileNumber());
        return customerDto;
    }

    public static Customer mapToCustomer(CustomerDto customerDto, Customer customer) {
        customer.setName(customerDto.getName());
        customer.setEmail(customerDto.getEmail());
        customer.setMobileNumber(customerDto.getMobileNumber());
        return customer;
    }
}

AccountMapper

public class AccountMapper {

    public static AccountDto mapToAccountsDto(Account accounts, AccountDto accountsDto) {
        accountsDto.setAccountNumber(accounts.getAccountNumber());
        accountsDto.setAccountType(accounts.getAccountType());
        accountsDto.setBranchAddress(accounts.getBranchAddress());
        return accountsDto;
    }

    public static Account mapToAccounts(AccountDto accountsDto, Account accounts) {
        accounts.setAccountNumber(accountsDto.getAccountNumber());
        accounts.setAccountType(accountsDto.getAccountType());
        accounts.setBranchAddress(accountsDto.getBranchAddress());
        return accounts;
    }
}
profile
무슨 생각하며 사니

0개의 댓글