사례로 배워보는 디자인패턴 #2 - 비지니스 로직을 담자
<개요>
- 일전에는 간단히 MVC Layer로 조회 API를 만들어 봤습니다.
- 오늘은 비지니스 로직 구현 및 Null 처리에 대해서 정리해보겠습니다.
<내용>
이번에는 신규 등록하는 API를 작업해보도록 하겠습니다.
@RequestMapping(value="/services", method= RequestMethod.POST)
public @ResponseBody
ServiceDto createService (@AuthenticationPrincipal LoginUserDetails loginUserDetails,
@RequestBody ServiceDto service, HttpServletResponse response) throws DataFormatViolationException, ServiceAlreadyExistsException {
// serviceCode 중복 체크 수행
ServiceDto serviceDto = getServiceByServiceId(serviceId, loginUserDetails);
UserEntity userEntity = new UserEntity();
userEntity.setUserId(loginUserDetails.getUserId());
service.setUser(userEntity);
ServiceDto ret = serviceService.createService(service);
response.setStatus(HttpServletResponse.SC_CREATED);
return ret;
}
- 등록은 일반적으로 POST 방식을 사용하며 멱등성을 보장해야 합니다.
- 등록하기 전에 기존에 같은 id가 존재하는지 중복체크 수행을 합니다.
신규등록을 하기 위해서 이전에 만들었던 로직을 재사용할 수 있습니다. (getServiceByServiceId)
그런데 이전에 만들었던 부분에서 약간 보완해야 할 부분이 있습니다.
private ServiceDto getServiceByServiceId(int serviceId, LoginUserDetails loginUserDetails) throws ServiceNotFoundException {
ServiceDto serviceDto = serviceService.findServiceById(serviceId);
if( (serviceDto == null) ||
loginUserDetails.checkNotAvailableService( serviceDto.getServiceId()) ||
StringUtils.isEmpty(serviceDto.getServiceId())){
throw new ServiceNotFoundException(String.valueOf(serviceId));
}
return serviceDto;
}
이와 같이 Service에서 조회된 값이 Null인지를 반드시 체크해줘야 합니다.
serviceDto가 null 인경우 getServiceId()를 수행할때 NullPointerException이 발생하기 때문입니다.
NullPointerException의 경우 다른 Layer로 전파되지 않도록 하는 것이 로직을 간결하게 만드는데 도움이 됩니다.
또한 Controller Layer의 경우 앞에서 말씀드린 것처럼 Web과 연결되는 부분에 대해서만 담당하도록 하는 것이 좋습니다.
그럼 어떻게 하는게 좋을까요?
시스템의 특성상 다양한 처리방법이 존재하지만 이번 예제에서는 간단히 Service Layer에서 Optional을 이용해서 구현했습니다.
@Service
public class ServiceService {
@Autowired
private ServiceRepository serviceRepository;
public ServiceDto findServiceById(int id){
ServiceEntity serviceEntity = serviceRepository.findById(id).orElse(new ServiceEntity());
return modelMapper.map(serviceEntity, ServiceDto.class);
}
Null인 경우 빈 객체를 하나 생성하여 Return 하도록 하였습니다.
이렇게 하면 더이상 Controller Layer에서는 Null에 대해서 신경쓰지 않아도 되기 때문에 조금 더 간결해집니다.
private ServiceDto getServiceByServiceId(int serviceId, LoginUserDetails loginUserDetails) throws ServiceNotFoundException {
ServiceDto serviceDto = serviceService.findServiceById(serviceId);
if( loginUserDetails.checkNotAvailableService( serviceDto.getServiceId()) ||
StringUtils.isEmpty(serviceDto.getServiceId())){
throw new ServiceNotFoundException(String.valueOf(serviceId));
}
return serviceDto;
}
이제 Service Layer에 생성로직을 구현합니다.
신규 생성시 간단한 비지니스 규칙을 하나 추가해 보겠습니다.
비지니스 규칙 : serviceCode는 영문소문자와 숫자, '-' 로만 구성되어야 하며 2 ~ 20글자까지 허용한다.
public ServiceDto createService(ServiceDto serviceDto) throws DataFormatViolationException {
Pattern codePattern = ValidationPattern.serviceCodePattern;
Matcher matcher = codePattern.matcher(serviceDto.getServiceCode());
if(!matcher.matches()){
throw new DataFormatViolationException("Code value should be consist of alphabet lowercase, number and '-', (length is from 2 to 20)");
}
ServiceEntity serviceEntity =modelMapper.map(serviceDto, ServiceEntity.class);
serviceRepository.save(serviceEntity);
return modelMapper.map(serviceEntity, ServiceDto.class);
}
}
이와 같이 로직을 구성하고 값을 저장합니다. Repository Layer에서는 serviceCode 컬럼 값에 대해서 신경쓰지 않고 데이터 저장에만 집중할 수 있습니다.
UI에서도 이와 같은 validation을 동일하게 구현할 수 있지만 보다 시스템을 튼튼하게 만들기 위해서는 Service Layer에서 반드시 체크해야 합니다. 나중에 다른 비지니스 프로세스를 개발할 때 createService를 재사용할 수도 있기 때문입니다.
ServiceCode를 검증하는 중 발생하는 오류에 대해서는 별도 Exception으로 처리하였습니다.
되도록이면 Raw Exception을 사용하는 것은 지양합니다. 그리고 어떤 Exception을 어느 Layer까지 전파시킬 것인가에 대해서도 사전에 정의하는 것이 좋습니다.
아키텍처 레벨의 디자인패턴에 대해서는 나중에 추가로 정리하겠습니다.
<정리>
- 1장과 동일한 포인트입니다. 각 Layer는 역할에 맞는 기능이 구현되어야 합니다.
- 로직은 Controller Layer에 담지 않습니다.
- Null 처리는 표준화 합니다.
- Exception 은 상세하게 사용합니다.