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까지 전파시킬 것인가에 대해서도 사전에 정의하는 것이 좋습니다.
- 일반적인 Web MVC구조에 따라서 Service 등록/수정/삭제/조회 하는 REST API를 만든다고 가정합니다.
<내용>
가장 단순한 건당 조회를 살펴봅니다.
Controller 클래스 입니다.
@RestController
@RequestMapping(path="/api")
public class ServiceController {
@RequestMapping(value="/services/{serviceId}", method= RequestMethod.GET)
public @ResponseBody
ServiceDto findService (@AuthenticationPrincipal LoginUserDetails loginUserDetails,
@PathVariable("serviceId") int serviceId) throws ServiceNotFoundException {
// @ResponseBody means the returned String is the response, not a view name
// @RequestParam means it is a parameter from the GET or POST request
ServiceDto serviceDto = getServiceByServiceId(serviceId, loginUserDetails);
return serviceDto;
}
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;
}
- LoginUserDetails 의 경우 로그인한 사용자의 정보를 저장합니다. - serviceId를 이용하여 데이터를 조회하고 결과값을 간단하게 검증하는 로직입니다.
- 결과값이 다음 중 하나와 같으면 현재 Service가 존재하지 않는 것으로 판단합니다.
1. 객체가 null 인 경우
2. ID필드의 값이 없는 경우
3. 값이 존재하나 로그인한 사용자의 정보에 해당하지 않는(본인의 서비스가 아닌 경우) 경우
이와 같이 Service 를 조회하고 값을 검증하는 로직은 조회 이외에 등록/수정에서도 필요하기 때문에 별도 메소드를 작성하여 재사용하였습니다.
그리고 현재 ServiceId와 로그인 사용자의 ServiceId를 비교하는 로직의 경우 Controller에서 구현하는 것보다는 정보은닉화와 설계원칙에 적합해 보여서 LoginUserDetails 내부에 구현하였습니다.
이와 같이 구현하면 현재 사용자의 serviceId등은 외부로 노출시키지 않아도 되며 (getter를 작성하지 않아도 됩니다.)
값의 검증이 필요한 모듈은 LoginUserDetails 객체에 요청하기만 하면 됩니다.
Controller layer의 경우 web과 바로 연결되어 있는 부분들을 담당하기 때문에 login관련정보나 인자값을 주로 처리하며 비지니스 로직은 Service Layer에 존재하게 됩니다.
다음으로 Service Layer를 살펴보겠습니다.
@Service
public class ServiceService {
@Autowired
private ServiceRepository serviceRepository;
@Autowired
private ModelMapper modelMapper;
public ServiceDto findServiceById(int id){
ServiceEntity serviceEntity = serviceRepository.findById(id);
return modelMapper.map(serviceEntity, ServiceDto.class);
}
지금은 아무 로직이 없기 때문에 단순히 Repository 로부터 값을 조회하여 객체의 값을 매핑만 합니다.
현재 프로젝트에서는 Spring JPA를 사용하여 시스템을 구축중인데, Entity클래스의 변경은 되도록 줄이는 것이 좋습니다. 만약 Entity 클래스 하나의 유형으로 화면 - 서비스 - 데이터를 모두 처리하게 될 경우 객체지향에서 말하는 대표적인 anti pattern이 될 수 있기 때문에 별도 DTO 클래스를 사용합니다.
var Registry = require('azure-iothub').Registry;
const chalk = require('chalk');
var connectionString = process.argv[2];
var fwVersion = '2.8.5';
var fwPackageURI = 'https://secureuri/package.bin';
var fwPackageCheckValue = '123456abcde';
var sampleConfigId = 'firmware285';
2. Update할 Configuration 을 정의한다.
// <configuration>
var firmwareConfig = {
id: sampleConfigId,
content: {
deviceContent: {
'properties.desired.firmware': {
fwVersion: fwVersion,
fwPackageURI: fwPackageURI,
fwPackageCheckValue: fwPackageCheckValue
}
}
},
// Maximum of 5 metrics per configuration
metrics: {
queries: {
current: 'SELECT deviceId FROM devices WHERE configurations.[[firmware285]].status=\'Applied\' AND properties.reported.firmware.fwUpdateStatus=\'current\'',
applying: 'SELECT deviceId FROM devices WHERE configurations.[[firmware285]].status=\'Applied\' AND ( properties.reported.firmware.fwUpdateStatus=\'downloading\' OR properties.reported.firmware.fwUpdateStatus=\'verifying\' OR properties.reported.firmware.fwUpdateStatus=\'applying\')',
rebooting: 'SELECT deviceId FROM devices WHERE configurations.[[firmware285]].status=\'Applied\' AND properties.reported.firmware.fwUpdateStatus=\'rebooting\'',
error: 'SELECT deviceId FROM devices WHERE configurations.[[firmware285]].status=\'Applied\' AND properties.reported.firmware.fwUpdateStatus=\'error\'',
rolledback: 'SELECT deviceId FROM devices WHERE configurations.[[firmware285]].status=\'Applied\' AND properties.reported.firmware.fwUpdateStatus=\'rolledback\''
}
},
// Specify the devices the firmware update applies to
targetCondition: 'tags.devicetype = \'chiller\'',
priority: 20
};
// </configuration>
deviceContent에는 Device가 참조할 값들이 들어가고, metrics에는 작업 수행 중 Device 가 기록하는 값(DeviceTwin)을 조회하여 모니터링 할 수 있는 쿼리가 들어간다.
3. RegistryManager를 통해서 해당 Configuration 정보를 등록한다.
해당 값을 String으로만 처리하는 경우에는 유효하지 않는 코드값이 들어오거나, 오타가 발생하는등의 오류를 잡아내기가 힘들며 또한 코드값이 변경/추가되었을때 유지보수가 용이하지 않기 때문에 되도록 enum type사용을 권장한다.
또한, enum type을 사용하더라도 코드값에 따른 로직분기의 경우 if - else if문의 반복을 통해서 수행하는 경우가 많은데 추후 요구사항의 변경이 발생하였을 때 코드의 가독성을 떨어뜨리고, 버그를 만드는 원인이 된다. 이부분 역시 enum type을 활용하여 소스를 깔끔하게 유지할 필요가 있다.
Firmware Update의 모니터링쿼리를 살펴보면 각 Status에 따라 쿼리의 조건문이 다르고, 인자의 수에 따라서도 미묘하게 다르게 생성되어야 하지만 공통으로 공유하는 조건도 있다.
예를 들어서 CURRENT인 경우 상태가 'current'인 device에 대해서 조회가 이루어져야 하며, APPLYING의 경우는 'downloading', 'verifying', 'applying' 의 3가지 경우에 대해서 조회가 이루져야 한다. 그러나 조회조건의 configurationId는 동일하다.
해당 로직을 외부에서 각각 구현한다면 중복로직이 존재하게 되고 추후 변경사항이 발생할 경우 영향을 받는 범위도 넓다. 이는 OCP원칙에 위배되기 때문에 확장에는 열려있고 변경에는 닫히도록 작성할 필요가 있다.
public enum FirmwareUpdate{
CURRENT(Arrays.asList(FirmwareStatus.CURRENT)),
REBOOTING(Arrays.asList(FirmwareStatus.REBOOTING)),
ERROR(Arrays.asList(FirmwareStatus.ERROR)),
ROLLEDBACK(Arrays.asList(FirmwareStatus.ROLLEDBACK)),
APPLYING(Arrays.asList(FirmwareStatus.APPLYING, FirmwareStatus.DOWNLOADING, FirmwareStatus.VERIFYING));
List<FirmwareStatus> statusList;
FirmwareUpdate(List<FirmwareStatus> statusList){
this.statusList=statusList;
}
String query(String configurationId){
String temp = "SELECT deviceId FROM devices WHERE configurations.[[" + configurationId + "]].status=\'Applied\' AND ";
if (statusList.size() == 0) {
throw new ArrayIndexOutOfBoundsException("the size of status list should be positive number");
} else if (statusList.size() == 1) {
temp += "properties.reported.firmware.fwUpdateStatus=\'" + statusList.get(0).label + "\'";
} else {
temp += "(";
int count=0;
for (FirmwareStatus status : statusList) {
temp += "properties.reported.firmware.fwUpdateStatus=\'" + status.label + "\'";
count++;
if(count < statusList.size()){
temp+=" OR ";
}
}
temp += ")";
}
return temp;
}
}
위와 같이 각 Status에 따라서 동작하는 고유의 로직은 enum형태로 가지고 있도록 하며, 외부에는 query 메소드만 노출하도록 한다.
만약 Status 에 따라서 더욱 구체적인 구현이 필요하다면 query 메소드를 abstract로 정의하고 type별로 개별구현하는 것도 가능하다.
- Azure IoT Hub를 사용하면 Device와 Server를 간편하게 연결하여 D2C (Device to Cloud), C2D (Cloud to Device) 메시지를 쉽게 전달할 수 있습니다.
- 디바이스의 메시지는 일반적으로 json string으로 구성되어 다양한 형태의 레이아웃을 특별한 처리없이 송수신할 수 있습니다.
- 이러한 유형의 데이터를 Telemetry 라고 합니다.
<내용>
- 디바이스와 서버가 주고 받는 데이터는 Telemetry외에도 제어정보가 있습니다.
- Azure IoT Hub 에서는 Device 제어를 위해서 크게 두 가지 방법을 제공하고 있습니다.
1. Device Twin
SKT ThingPlug나 AWS IoT 의 경우 Shadow 라고 부릅니다. Azure IoT Hub의 경우 Device Twin(장치쌍)이라고 부릅니다.
공유변수의 형태로 이해하면 됩니다. 디바이스와 서버가 Read/Write할 수 있는 영역이기 때문에 디바이스가 꺼져있거나 서버와 연결되어 있지 않는 상황에서도 처리가 가능합니다. (비동기)
디바이스 On/Off, Firmware Update, 데이터 주기 조정등 다양한 목적으로 사용가능합니다.
A. Tags
- 일반적으로 Server Side에서 세팅하는 값으로 Device를 구분하기 위한 값으로 많이 사용됩니다. (e.g. 모델코드, 위치정보)
- 나중에 나오는 Firmware Update시나리오에서도 Tags값을 target Condition 으로 활용합니다.
B. Properties
- Desired : 서버에서 Write하고 디바이스에서 Read 하는 용도로 사용됩니다. 주로 서버에서 의도하는 바를 디바이스에 전달하기 위한 목적으로 사용됩니다.
- Reported : 디바이스에서 Write하고 서버에서 Read 하는 용도로 사용됩니다. 디바이스가 Desired값을 읽고 그에 따른 행위를 마친뒤에 서버에 Notification을 보내는 용도로 사용합니다.
2. Method Invoke
일반적 RPC 사용과 동일합니다. 요청/응답 구조로 즉각적인 확인이 가능하며 연결이 되어 있지 않은 경우 실패합니다.
따라서 timeout 도 존재하며 exception 도 발생할 수 있습니다.
<정리>
- 결국 과거에 Remote 와 통신하기 위해서 사용했던 방식과 크게 다르지 않습니다. SDK만 별도로 존재하며 실제 내부를 살펴보면 지원하는 프로토콜에 약간의 차이만 있을 뿐 원격호출 방식은 거의 동일합니다.
- 각 방식을 비교한 자료는 다음과 같습니다.
Direct methodsTwin's desired propertiesCloud-to-device messages
Scenario
Commands that require immediate confirmation, such as turning on a fan.
Long-running commands intended to put the device into a certain desired state.For example, set the telemetry send interval to 30 minutes.
One-way notifications to the device app.
Data flow
Two-way.The device app can respond to the method right away.The solution back end receives the outcome contextually to the request.
One-way.The device app receives a notification with the property change.
One-way.The device app receives the message
Durability
Disconnected devices are not contacted.The solution back end is notified that the device is not connected.
Property values are preserved in the device twin.Device will read it at next reconnection.Property values are retrievable with theIoT Hub query language.
Messages can be retained by IoT Hub for up to 48 hours.
Targets
Single device usingdeviceId, or multiple devices usingjobs.
Single device usingdeviceId, or multiple devices usingjobs.