앞서 엔티티와 레포지토리 그리고 dto를 작성해봤습니다.
이제 실제 사용자 요청에 따른 응답을 정의하기 위한 엔드포인트를 작성해보겠습니다.
그러기 위해 컨트롤러를 먼저 작성해보겠습니다.
createAccount() 메서드를 작성하여 계정 생성을 처리합니다. 이 메서드는 CustomerDto 객체를 요청 본문으로 받아 이를 처리합니다. POST 요청을 사용하며, 생성된 계정에 대해 201 상태 코드를 반환합니다.
먼저 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 클래스는 따로 분리하여 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";
}
비즈니스 로직을 처리할 서비스 레이어를 AccountsService 인터페이스와 AccountServiceImpl 구현 클래스로 분리하여 작성합니다. 서비스 클래스는 @Service 어노테이션을 통해 스프링 부트에서 관리되는 빈으로 등록되며, 자동으로 AccountRepository와 CustomerRepository를 주입받습니다.
public interface AccountService {
void createAccount(CustomerDto customerDto);
}
실제 구현 클래스인 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;
}
}
현재 로직에선 고객의 핸드폰 번호가 고유한 필드입니다. 그래서 고객 중복 방지를 위해 핸드폰 번호를 사용해봅니다.
이를 위해 별도의 예외 클래스를 만들어봅니다.
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 예외에 대해 전역으로 처리하는 코드입니다. 예외가 발생한 곳에서 따로 예외 처리 코드를 추가하지 않더라도 위의 코드가 예외를 처리해줄 수 있습니다.
추후에 더 다양한 예외에 대해서 처리하는 코드를 작성해보겠습니다.
DTO와 엔티티 간 데이터를 변환하기 위해 Mapper 패키지를 생성하고, AccountsMapper와 CustomerMapper 클래스를 작성합니다. 이 클래스들은 DTO를 엔티티로, 엔티티를 DTO로 변환하는 메서드를 포함합니다.
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;
}
}
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;
}
}