목표
SpringBoot와 AWS IoT를 연동하여, SpringBoot에서 REST API 요청을 통해 AWS IoT 사물 생성 및 Device Shadow 업데이트 구현
구현할 아키텍처 구조










arn:partition:iot:region:AWS-account-ID:Resource-type/Resource-name






아래 링크에서 적당한 버전을 가져왔다.
https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-iot
// AWS IoT Core
implementation 'com.amazonaws:aws-java-sdk-iot:1.12.700'
해당 사용자를 사용해 Spring Boot에서 AWS IoT 클라이언트를 생성할 것이다.
필요 권한:AdministratorAccess,AWSIoTDataAccess,AWSIoTFullAccess


aws:
accessKeyId: {IAM 액세스키}
secretKey: {IAM 시크릿 키}
certificateArn: {AWS IoT 인증서 ARN}
@Configuration
public class AwsConfig {
@Value("${aws.accessKeyId}")
private String accessKeyId;
@Value("${aws.secretKey}")
private String secretKey;
/**
* AWS IoT 클라이언트를 빈으로 등록
*/
@Bean
public AWSIot getIotClient(){
// AWS IoT 클라이언트 생성
return AWSIotClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(
// IAM 액세스 키 아이디와 시크릿키를 사용해 인증 정보 제공
new BasicAWSCredentials(accessKeyId, secretKey)))
// AWS 리전을 서울로 설정
.withRegion(Regions.AP_NORTHEAST_2)
.build();
}
/**
* AWS IoT Data 클라이언트를 빈으로 등록
*/
@Bean
public AWSIotDataClient getIotDataClient(){
// AWS IoT Data 클라이언트 생성
return (AWSIotDataClient) AWSIotDataClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(new AWSCredentials() {
// IAM 액세스 키 아이디와 시크릿키를 사용해 인증 정보 설정
@Override
public String getAWSAccessKeyId() {
return accessKeyId;
}
@Override
public String getAWSSecretKey() {
return secretKey;
}
}))
// AWS 리전을 서울로 설정
.withRegion(Regions.AP_NORTHEAST_2)
.build();
}
}
@Configuration
@RequiredArgsConstructor
public class MQTTConfig {
private final AwsConfig awsConfig;
/**
* AWS IoT Device Shadow에 메세지 게시
* : 패널의 고유 번호를 이름으로하는 사물의 shadow에 어떤 비상 작동 버튼이 눌렸는지 넣어 메세지 게시
*
* AWS IoT Device Shadow: 디바이스의 현재 상태를 저장하여 디바이스가 오프라인 상태일 때도 정보를 클라우드에 보존하여
* 언제든지 상태 정보에 접근할 수 있도록 한다.
*/
public void publishToShadow(String panelNumber, EmergencyStatus emergencyStatus) throws IOException{
// 게시할 주제 설정 (패널 고유 번호를 이름으로 하는 사물의 shadow에 메세지를 게시하기 위해)
String topic = "$aws/things/" + panelNumber + "/shadow/update";
// EmergencyStatus 객체를 JSON 문자열로 변환 (payload는 JSON 형태여야 하기때문)
ObjectMapper objectMapper = new ObjectMapper();
String emergencyStatusJson = objectMapper.writeValueAsString(emergencyStatus);
// Device Shadow 내용을 업데이트하는 payload 설정 (비상 작동 모드를 status에 넣어 보냄)
String payload = "{\"state\":{\"reported\":{\"status\":" + emergencyStatusJson +"}}}";
ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(payload);
// PublishRequest 객체 생성
PublishRequest publishRequest = new PublishRequest();
publishRequest.withPayload(byteBuffer);
publishRequest.withTopic(topic);
publishRequest.setQos(0);
// AWS IoT Data Client를 통해 메세지 게시
awsConfig.getIotDataClient().publish(publishRequest);
System.out.println("성공적으로 메세지를 게시했습니다.");
}
}
포스트맨을 통해 등록을 요청하면, AWS IoT에 해당 이름의 사물을 생성한다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/solar-panels")
public class SolarPanelController {
private final SolarPanelService solarPanelService;
// 생략
// 태양광 패널 등록 <- 수정 필요 dto로 이름 받아오기
@PostMapping("/register/{thingName}")
public ResponseEntity<String> createThing(@PathVariable String thingName){
return ResponseEntity.status(HttpStatus.OK).body(solarPanelService.createThingAutomatically(thingName));
}
}
@Service
@RequiredArgsConstructor
public class SolarPanelService {
private final SolarPanelRepository solarPanelRepository;
private final ElectronicService electronicService;
private final BillService billService;
private final UserService userService;
private final AwsConfig awsConfig;
// 생략
/**
* 자동으로 AWS IoT에 사물을 등록하는 메서드
*/
public String createThingAutomatically(String thingName) {
// 해당 이름의 사물이 이미 존재하는지 확인
if(!describeThing(thingName)){
// 사물이 존재하지 않는 경우, 사물 생성
CreateThingResult response = awsConfig.getIotClient()
.createThing(new CreateThingRequest().withThingName(thingName));
// 사물에 인증서 연결
AttachThingPrincipalRequest attachThingPrincipalRequest = new AttachThingPrincipalRequest()
.withPrincipal(certificateArn)
.withThingName(thingName);
awsConfig.getIotClient().attachThingPrincipal(attachThingPrincipalRequest);
System.out.print("사물이 성공적으로 생성되었습니다.");
return "사물이 성공적으로 생성되었습니다.";
}
// 사물이 이미 존재하는 경우, 아래 메세지 반환
return "해당 이름의 사물이 이미 존재합니다.";
}
/**
* 해당 이름의 사물이 존재하는지 확인하는 메서드
*/
private boolean describeThing(String thingName) {
// 사물 이름이 null이라면 사물이 없다는 의미이므로 false 반환
if(thingName == null){
return false;
}
try {
// 사물이 AWS IoT에 존재하는지 확인 -> 예외가 발생하지 않는다면 사물이 존재함을 의미하므로 true 반환
DescribeThingResponse(thingName);
return true;
} catch (ResourceNotFoundException e){
// 사물이 존재하지 않는경우, 예외가 발생하으로 false 반환
return false;
}
}
/**
* AWS IoT에 해당 이름으로 이미 등록된 사물이 있는지 확인하는 메서드
*/
private DescribeThingResult DescribeThingResponse(String thingName) {
// 해당 사물의 이름으로 DescribeThingRequest 객체를 생성
DescribeThingRequest describeThingRequest = new DescribeThingRequest();
describeThingRequest.setThingName(thingName);
// AWS IoT 클라이언트를 사용하여 describeThing 요청을 보내고, 결과 반환
return awsConfig.getIotClient().describeThing(describeThingRequest);
}
// 생략
}
포스트맨을 통해 비상버튼 기능을 요청하면, 해당 태양광패널의 고유번호를 가진 사물의 Device Shadow를 업데이트 한다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/solar-panels")
public class EmergencyController {
private final EmergencyService emergencyService;
// 강풍 비상 버튼
@PostMapping("{panelId}/strong-wind")
public ResponseEntity<String> publishStrongWindMessage(@PathVariable Long panelId) throws IOException {
EmergencyStatus emergencyStatus = EmergencyStatus.STRONG_WIND;
return ResponseEntity.status(HttpStatus.OK).body(emergencyService.publishToShadow(panelId, emergencyStatus));
}
// 폭설 비상 버튼
@PostMapping("{panelId}/heavy-snow")
public ResponseEntity<String> publishHeavySnowMessage(@PathVariable Long panelId) throws IOException {
EmergencyStatus emergencyStatus = EmergencyStatus.HEAVY_SNOW;
return ResponseEntity.status(HttpStatus.OK).body(emergencyService.publishToShadow(panelId, emergencyStatus));
}
// 비상 작동 해제 버튼
@PostMapping("{panelId}/normal")
public ResponseEntity<String> publishNormalMessage(@PathVariable Long panelId) throws IOException {
EmergencyStatus emergencyStatus = EmergencyStatus.NORMAL;
return ResponseEntity.status(HttpStatus.OK).body(emergencyService.publishToShadow(panelId, emergencyStatus));
}
}
@Service
@RequiredArgsConstructor
public class EmergencyService {
private final MQTTConfig mqttConfig;
private final SolarPanelService solarPanelService;
/**
* 비상 버튼을 눌렀을 때, 해당 패널의 Device Shadow로 메세지를 게시하는 메서드
*/
public String publishToShadow(Long panelId, EmergencyStatus emergencyStatus) throws IOException {
// 패널 Id를 이용해 해당 패널의 정보 조회 -> 패널의 고유 번호 가져오기
SolarPanel panel = solarPanelService.findById(panelId);
String panelNumber = panel.getPanelNumber();
// MQTTConfig를 이용해 패널 고유 번호와 비상 상태를 Device Shadow에 게시
mqttConfig.publishToShadow(panelNumber, emergencyStatus);
// 패널의 현재 모양을 알려주는 imageNumber를 비상모드 모양에 따라 변경
if(emergencyStatus == EmergencyStatus.STRONG_WIND){
panel.setImageNumber(3);
} else if (emergencyStatus == EmergencyStatus.HEAVY_SNOW) {
panel.setImageNumber(1);
} else {
// 비상작동 해제 버튼을 눌렀을 경우: 현재 태양 위치를 바라보도록 수정해야함
panel.setImageNumber(2);
}
return emergencyStatus.getTitle() + "버튼이 눌렸습니다.";
}
}





