공통적인 로직을 Service Layer 에 두면 다른 REST API 나 Controller 활용할 때에도 별도로 검증로직을 추가하지 않아도 됩니다.
또한 값의 의미를 검증하는 부분은 비지니스와 연관성이 있다고 판단하였습니다.
먼저 서비스명과 서비스코드의 문자패턴을 정규표현식으로 나타냅니다.
문자패턴을 정규표현식으로 나타내면서 (시작문자,종료문자,트림,길이 등) 에 대한 처리를 합니다.
public class ValidationPattern {
public final static Pattern serviceNamePattern = Pattern.compile("(^[a-z][a-z0-9]{1,19}$)");
public final static Pattern serviceCodePattern = Pattern.compile("(^[a-z][a-z0-9-]{1,19}$)");
}
이후 해당 패턴을 이용하여 검증하는 로직을 구현합니다.
@Service
public class ServiceService {
@Autowired
private ServiceRepository serviceRepository;
@Autowired
private ModelMapper modelMapper;
public ServiceDto createService(ServiceDto serviceDto) throws DataFormatViolationException {
String serviceCode = serviceDto.getServiceCode();
checkServiceCode(serviceCode);
ServiceEntity serviceEntity =modelMapper.map(serviceDto, ServiceEntity.class);
serviceRepository.save(serviceEntity);
return modelMapper.map(serviceEntity, ServiceDto.class);
}
private void checkServiceCode(String serviceCode) throws DataFormatViolationException {
if(serviceCode == null){
throw new DataFormatViolationException("Code value should be not null");
}else{
Pattern codePattern = ValidationPattern.serviceCodePattern;
Matcher matcher = codePattern.matcher(serviceCode);
if(!matcher.matches()){
throw new DataFormatViolationException("Code value should be consist of alphabet lowercase, number and '-', (length is from 2 to 20)");
}
}
}
7. 결론
두서없이 쓰다보니 글의 요점이 모호해진 것 같습니다.
정리해보면...
- 어떠한 문제를 해결하기 위해서 바로 코딩으로 뛰어들어서는 안된다.
- 문제를 재해석하여 나만의 방식으로 표현하고 시간복잡도 / 공간복잡도 를 정리한다.
- 로직은 최대한 간결하게! 대/중/소, 중복없이, 누락없이!
- MVC 의 경우 각 Layer의 하는 일들이 나누어져 있다.
- 만약 시간복잡도가 높은 연산이 있다면 이러한 연산은 최소한으로 해주는 것이 좋고, 이러한 필터링을 각 Layer별로 해주면 효과적이다.
@Test
public void updateDeviceTelemetryInterval() throws Exception {
int deviceId = 1;
String serviceName = "AAAService";
//given service info
ServiceDto serviceDto = new ServiceDto();
serviceDto.setServiceId(1);
AzureIotHubEntity azureIotHubEntity = new AzureIotHubEntity();
azureIotHubEntity.setAzureIotHubConnectionString("");
serviceDto.setAzureIotHub(azureIotHubEntity);
//given device info
DeviceDto deviceDto = new DeviceDto();
deviceDto.setDeviceId(1);
//given interval info
Gson gson = new Gson();
DeviceDto device = new DeviceDto();
device.setTelemetryInterval(1);
String str = gson.toJson(device);
given(serviceService.findServiceByName(serviceName)).willReturn(serviceDto);
given(deviceService.findDeviceById(deviceId)).willReturn(deviceDto);
//when then
MockHttpServletRequestBuilder createUserRequest = post("/ServiceName/setTelemetryInterval/1")
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
.content(str);
this.mvc.perform(createUserRequest)
.andExpect(status().is2xxSuccessful());
}
이와 같은 테스트코드는 정상적 상황을 테스트하는 상황입니다.
만약 해당하는 서비스가 없는 상황에서는 다음과 같은 Exception이 발생하도록 내부적으로 정의했습니다.
@ResponseStatus(value= HttpStatus.NOT_FOUND, reason="No such Service")
public class ServiceNotFoundException extends Exception {
public ServiceNotFoundException(String message){
super(message);
}
public ServiceNotFoundException(String message, Throwable cause) {
super(message,cause);
}
}
이와 같은 Exception이 의도한 것처럼 발생하는지 우리는 일반적으로 TestCode에서 아래와 같은 annotation을 통해서 처리합니다.
@Test(expected = ServiceNotFoundException.class)
그런데 여기서 주의해야 할점이 있습니다!!!
우리가 일반적으로 Spring mvc를 사용할때 위와 같은 형태로 Exception을 정의할 경우, 내부적으로 Exception을 catch하여 밖으로 나가지 않도록 하고 HTTP STATUS코드를 변경하게 됩니다.
따라서 위와 같이 @Test(expected) 를 추가하더라도 Exception은 잡히지 않습니다.
어떻게 하면 내가 의도한 Exception을 잡아낼 수 있을지 Spring 내부소스를 살펴보았습니다.
final class TestDispatcherServlet extends DispatcherServlet {
@Override
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
ModelAndView mav = super.processHandlerException(request, response, handler, ex);
// We got this far, exception was processed..
DefaultMvcResult mvcResult = getMvcResult(request);
mvcResult.setResolvedException(ex);
mvcResult.setModelAndView(mav);
return mav;
}
위와 같이 result에 resolvedException에 발생한 Exception정보를 저장하고 있습니다.
그렇다면 아래와 같이 테스트 코드를 작성하면 될것이라고 추측됩니다.
@Test
public void updateDeviceTelemetryIntervalNonExistService() throws Exception {
//given
ServiceDto serviceDto = new ServiceDto();
Gson gson = new Gson();
DeviceDto device = new DeviceDto();
device.setTelemetryInterval(1);
String str = gson.toJson(device);
given(serviceService.findServiceByName("TestService")).willReturn(serviceDto);
//when then
MockHttpServletRequestBuilder createUserRequest = post("/AAAService/setTelemetryInterval/1")
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
.content(str);
this.mvc.perform(createUserRequest)
.andExpect((result)->assertTrue(result.getResolvedException().getClass().isAssignableFrom(ServiceNotFoundException.class)))
.andExpect(status().is4xxClientError());
}
테스트 결과 404 NotFound 와 exception의 유형을 모두 확인할 수 있습니다.
public interface DeviceTelemetryRepository extends DocumentDbRepository<DeviceTelemetry, String>{
List<DeviceTelemetry> findAll();
@Query(value="SELECT * FROM DeviceTelemetry where deviceId= ?1 and date >= ?2 and date <= ?3", nativeQuery = true)
List<DeviceTelemetry> findDeviceTelemetryByDeviceIdAndDateGreaterThanAndDateLessThan(@Param("deviceId") int deviceId,
@Param("from") long from,
@Param("to") long to );
}
3. Entity클래스의 경우 테이블에 따라서 잦은 변경이 일어나기 때문에 Controller, Service Layer에서는 DTO클래스를 별도 로 사용하는 것을 권장한다. (디자인 패턴 참고)
@Service public class DeviceTelemetryService { @Autowired private DeviceTelemetryRepository deviceTelemetryRepository;
@Autowired private ModelMapper modelMapper;
public List<DeviceTelemetryDto> getDeviceTelemetry(int deviceId, long from, long to){ return deviceTelemetryRepository.findDeviceTelemetryByDeviceIdAndDateBetween(deviceId, from, to).stream() .map(deviceTelemetry -> modelMapper.map(deviceTelemetry, DeviceTelemetryDto.class)) .collect(Collectors.toList()); }
}
4. Controller 까지 완성하여. 테스트하였는데 아래와 같은 오류가 발생한다.
@RequestMapping(path="/api/") public class DeviceTelemetryController {
@RequestMapping(value="/{serviceId}/{deviceId}/telemetry", method= RequestMethod.GET) public @ResponseBody List<DeviceTelemetryDto> getAllDeviceTelemetry(@PathVariable("deviceId") int deviceId, @RequestParam("from") long from, @RequestParam("to") long to) {
java.lang.IllegalArgumentException: unsupported keyword: GREATER_THAN (1): [IsGreaterThan, GreaterThan]
at com.microsoft.azure.spring.data.documentdb.repository.query.DocumentDbQueryCreator.from(DocumentDbQueryCreator.java:82) ~[spring-data-cosmosdb-2.0.3.jar:na]
at com.microsoft.azure.spring.data.documentdb.repository.query.DocumentDbQueryCreator.and(DocumentDbQueryCreator.java:56) ~[spring-data-cosmosdb-2.0.3.jar:na]
at com.microsoft.azure.spring.data.documentdb.repository.query.DocumentDbQueryCreator.and(DocumentDbQueryCreator.java:25) ~[spring-data-cosmosdb-2.0.3.jar:na]
at org.springframework.data.repository.query.parser.AbstractQueryCreator.createCriteria(AbstractQueryCreator.java:122) ~[spring-data-commons-2.0.10.RELEASE.jar:2.0.10.RELEASE]
at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:95) ~[spring-data-commons-2.0.10.RELEASE.jar:2.0.10.RELEASE]
at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:81) ~[spring-data-commons-2.0.10.RELEASE.jar:2.0.10.RELEASE]
at com.microsoft.azure.spring.data.documentdb.repository.query.PartTreeDocumentDbQuery.createQuery(PartTreeDocumentDbQuery.java:38) ~[spring-data-cosmosdb-2.0.3.jar:na]
at com.microsoft.azure.spring.data.documentdb.repository.query.AbstractDocumentDbQuery.execute(AbstractDocumentDbQuery.java:25) ~[spring-data-cosmosdb-2.0.3.jar:na]
5. Azure Cosmos DB에 문의하니 다음과 같은 답변을 받았다.
spring-data-cosmosdb 2.0.6을 사용해야 하는데 아무리 찾아도 azure-documentdb-spring-boot-starter에 포함된 최신버전은 2.0.4가 최신버전이다!
다시 문의를 했다. 수동으로 업데이트해야하는가?
결론은 "azure-documentdb는 구 네이빙 버전입니다. 신버전을 사용하세요."
Thanks for filing issue, according to your stack trace, seems you are using version 2.0.3, if you are using Spring Boot 2.0.x, try upgrading to 2.0.6.
You can use the azure-cosmosdb-spring-boot-starter(not azure-documentdb-spring-boot-starter) version 2.0.13, which uses spring-data-cosmosdb 2.0.6. Between should have been supported in 2.0.6, check this issue. Also you can reference the integration tests in this repo for the usage.
6. pom.xml에서 azure-cosmosdb-spring-boot-starter 최신버전으로 변경하였다.
7. 하지만 Spring JPA의 naming rule에 같은컬럼을 중복해서 사용하면 에러가 발생한다.
BETWEEN 쿼리로 변경!
(GREATER_THAN, BETWEEN등은 위에서 언급한 2.0.6 버전에서 정상지원한다.)
8. Repository 소스 변경(@Query는 없어도 쿼리를 자동으로 생성하여 동작한다)
public interface DeviceTelemetryRepository extends DocumentDbRepository<DeviceTelemetry, String> {
List<DeviceTelemetry> findAll();
// @Query(value="SELECT * FROM DeviceTelemetry where deviceId= ?1 and date >= ?2 and date <= ?3", nativeQuery = true) List<DeviceTelemetry> findDeviceTelemetryByDeviceIdAndDateBetween(@Param("deviceId") int deviceId, @Param("from") long from, @Param("to") long to ); }