코딩관계론

미션별로 수신 대상자가 달라지는 SMS 기능 개발 본문

개발/SPOT

미션별로 수신 대상자가 달라지는 SMS 기능 개발

개발자_티모 2023. 4. 6. 03:05
반응형

서론

로봇이 미션을 수행하고 그 결과를 종합하여 SMS로 사용자들에게 전달해야 하는 업무가 있었습니다. 단순한 전달이 아닌 각 공장 관리자들의 성격과 특성이 다르기 때문에, 해당 공장 담당자의 미션 결과를 다른 공장 담당자가 보는 것에 대한 거부감이 있었고, 이런 문제들 때문에 로봇이 수행한 미션별로 수신자가 달라져야 했습니다.

 

이 글은 이러한 문제를 해결하기 위한 개발 과정을 자세히 다를 예정이며,

이를 위해 요구 사항 분석, 아키텍처 설계, DB 모델링, 알고리즘 구현 등의 작업을 수행한 결과를 공유할 것입니다.

요구사항 분석

SMS 예약 미션 알고리즘 개발을 위해 먼저 요구사항을 분석하였습니다. 기본적으로 로봇이 수행한 미션 결과들을 종합한 SMS를 사용자가 원하는 시간에 받아보고 싶다는 요구사항이 있었습니다. 이를 위해 사용자가 SMS로 수신 받고 싶어하는 미션을 선택하도록 하여, 선택한 미션의 결과를 정해진 시간에 SMS로 발송하는 기능이 필요했습니다.

 

또한, 로봇이 수행한 미션 별로 수신 대상자가 달라져야 하는 요구사항도 있었습니다. 이를 해결하기 위해 수행한 미션 이름을 통해서, 어떤 공장에서 수행했는지 알아야 했고, 해당 공장의 수신 대상자를 알 수 있었어야 했기 때문에 데이터베이스 해당 정보들을 저장할 수 있어야 했습니다.

 

SMS 발송 시 코드는 동작하지만, 인터넷 연결 불안정으로 발송 실패 또는 SMS 부족으로 발신하지 못하는 문제가 발생하는 경우가 있었습니다. 이러한 문제에 대한 추적 및 대응 기능이 필요하다는 요구사항이 있었고, 이를 반영하는 기능을 구현하기로 결정하였습니다.

아키텍쳐 설계

SMS 서비스는 MSA 형식으로 구성되어, 각각의 서비스들이 독립적으로 운영됩니다. SMS 서비스는 외부로부터 요청받은 데이터를 처리하고, 해당 데이터를 기반으로 문자를 발송합니다. 비즈니스 로직 처리를 위해 SMS 서비스는 데이터베이스에 저장된 정보를 API를 통해 요청하며, 이를 바탕으로 문자 발송을 수행합니다.

 

장고 서버는 SMS 서비스에서 사용하는 데이터베이스에 대한 API 게이트웨이 역할을 수행하며, 외부에서 접근 가능한 통합된 API를 제공합니다. SMS 서비스는 이 API를 통해 데이터베이스에 접근하여 비즈니스 로직을 처리합니다. 이후 SMS 서비스는 가비아를 사용하여 문자 발송을 요청하고, 가비아 API를 통해 문자를 발송합니다.

 

이러한 아키텍처는 SMS 서비스가 어떤 대행사를 사용하는지에 대한 제약을 없애주며, 각 서비스들이 독립적으로 운영됨으로써 무중단 서비스를 구현할 수 있습니다. 또한, 이러한 아키텍처는 서비스 간의 결합도를 줄일 수 있습니다. 각 서비스는 자신의 역할에만 집중하며, 다른 서비스와의 상호작용은 API를 통해 이루어지기 때문에, 시스템 전체의 유지보수 및 업그레이드가 용이해집니다.

 

최종적으로 구성된 아키텍쳐는 아래 그림과 같습니다.

시스템 요약도

DB 모델링

DB ERD

Factory 테이블은 공장 정보를 저장합니다.
이 테이블은 name이라는 필드를 포함하며, 이 필드는 공장의 이름을 나타냅니다. 유일성을 보장하기 위해 name을 유니크 키 값으로 지정했습니다.name을 기본 키로 사용하지 않은 이유는, 기본 키 값이 변경되는 것이 아니라 name 값이 변경될 경우에 일관성을 유지하기 위함입니다.

 

FactoryReceiver 테이블은 각각의 공장의 수신자 정보를 저장합니다.
이 테이블은 address와 factory라  ForeignKey 필드와 is_checked라는 Boolean 필드를 가집니다. 해당 필드는 공장의 미션 수신 여부를 나타냅니다.address 필드는 해당 수신자의 주소를 나타내고, factory 필드는 해당 수신자가 연결된 공장을 나타냅니다. 또한 이 테이블은 하나의 공장에 대해 여러 수신자가 존재할 수 있으므로 address와 factory 두 필드를 함께 사용하여 유일성을 보장합니다.

 

FactoryMap 테이블은 각각의 공장에 대한 맵 정보를 저장합니다.
이 테이블은 name이라는 필드와 factory라는 ForeignKey 필드를 가집니다. factory 필드는 해당 맵이 속한 공장을 참조합니다.

 

Address 테이블은 다른 테이블에서 사용할 수 있는 주소 정보를 포함합니다.
이 테이블은 각각의 주소에 대한 정보를 저장합니다. 이 정보는 rank, name, phone, email, in_charge, alarm_level과 같은 필드로 구성됩니다.

 

따라서 위의 데이터베이스 모델은 중복 데이터를 최소화하고, 각각의 테이블이 하나의 목적을 가지도록 설계되어 있어 데이터의 일관성과 무결성을 보장할 수 있습니다.

 

해당 데이터베이스는 3차 정규화를 만족합니다. 그 이유는

모든 속성은 원자적(Atomic)이며, 더 이상 분해할 수 없습니다. 예를 들어, Address 모델의 rank 속성은 이름과 별개로 하나의 직급 정보만을 담고 있으며, in_charge 속성은 하나의 담당 업무 정보만을 담고 있습니다. 이렇게 각 속성이 별개의 의미를 가지고, 더 이상 분해할 수 없는 원자적인 형태로 구성되어 있으므로 제1정규화를 만족합니다.

 

모든 비-키 속성이 기본 키 전체에 의존하지 않고 유일하게 식별되어야 합니다. 예를 들어, FactoryReceiver 모델은 Address와 Factory 모델에 각각 외래 키로 연결되어 있습니다. 하지만 is_checked 속성은 이 두 모델의 어느 한쪽만을 참조하여 값을 갖습니다. 즉, is_checked 속성은 FactoryReceiver 모델의 기본 키에만 종속되며, 나머지 속성들과는 관계가 없습니다. 따라서 제2정규화를 만족합니다

 

각 모델은 기본 키가 아닌 나머지 속성들이 해당 모델의 기본 키에만 의존하고 있습니다. 예를 들어, FactoryMap 모델에서는 name 속성이 FactoryMap 모델의 기본 키인 id에 의존하고 있습니다. 따라서 이 모델은 제3정규화를 만족합니다.

class Address(models.Model):
    rank = models.CharField(max_length=50, verbose_name='직급')
    name = models.CharField(max_length=30)
    phone = models.CharField(max_length=50, blank=True)
    email = models.EmailField(max_length=100, unique=True)
    in_charge = models.CharField(max_length=50, verbose_name='담당업무')
    alarm_level = models.ManyToManyField("AlarmLevel", blank=True)

    def __str__(self):
        return f'{self.name} | {self.rank}'


class Factory(models.Model):
    name = models.CharField(max_length=100, unique=True)
    
    def __str__(self):
        return self.name

class FactoryMap(models.Model):
    name = models.CharField(max_length=100, unique=True)
    factory = models.ForeignKey(Factory, on_delete=models.CASCADE)

    def __str__(self) -> str:
        return self.factory.name + " " + self.name
        
class FactoryReceiver(models.Model):
    address = models.ForeignKey(Address, on_delete=models.CASCADE)
    factory = models.ForeignKey(Factory, on_delete=models.CASCADE)
    is_checked = models.BooleanField(default=False)

    class Meta:

        constraints = [
            models.UniqueConstraint(fields=['address', 'factory'], name='공장 수신자 중복됨')
        ]

    def __str__(self):
        return self.address.name + " " + self.factory.name

 

 

예약 미션 알고리즘 구현

핵심아이디어

핵심 아이디어는 로봇이 수행한 미션 정보를 가져와 적절한 사용자에게 문자 메시지를 발신할 수 있으며, 이때 한 로봇이 다른 공장에서 미션을 수행하더라도 해당 공장에 등록된 수신자들에게만 메시지가 발송되도록 보장합니다. 또한 코드 실행 전 서버를 업데이트함으로써 코드의 동작에 문제가 있을 경우 어디에서 문제가 발생했는지 쉽게 판별할 수 있도록 합니다.

코드 설명

이 코드는 RobotSmsHelper 클래스 내에 send_sms 메소드를 정의하고 있습니다. 이 메소드는 전송될 문자를 생성하고, 그 문자를 공장에 맞게 발송하는 일련의 과정을 수행합니다.

 

send_sms 메소드는 먼저 dock_sms 객체를 생성하고, 이 객체의 check_is_dock 메소드를 호출하여 로봇이 도크에 위치해 있는지 여부를 확인합니다. 그리고 mission_sms 객체를 생성하고, util 모듈의 get_mission_name_by_booking_mission_time 함수를 사용하여 오늘 실행할 미션 정보를 가져옵니다.

 

mission_sms 객체는 가져온 미션 정보를 바탕으로 문자 메시지를 작성하고, 해당 메시지를 공장에 맞게 전송합니다. 전송된 메시지는 util 모듈의 update_booking_info 함수를 사용하여 데이터베이스에 저장됩니다.

 

마지막으로 send_sms 메소드는 모든 미션이 처리된 후, dock_sms 객체를 사용하여 도크에 위치한 로봇에 대한 정보를 문자로 발송합니다. util 모듈의 get_fatories_by_robot 함수를 사용하여 공장 이름을 가져오고, get_phone_numberes_of_fatories 함수를 사용하여 해당 공장의 전화번호를 가져옵니다. 이후 dock_sms 객체의 send_message 메소드를 사용하여 공장에 맞는 문자를 발송합니다.

class RobotSmsHelper():
    def __init__(self, robot_ip="192.168.80.3", robot_name="beta47") -> None:
        import os
        
        # 환경 변수로부터 SMS 전송에 필요한 정보를 가져옴
        # 기본적으로는 백엔드 서버의 주소를 사용하지만, 백엔드 서버 주소가 환경 변수에 정의되어 있을 경우 해당 주소를 사용함
        self.sms_url = os.environ.get('BACKEND_SERVER_ADDRESS', "http://***.**.**.***/back") + "/core/sms"
        
        # 스카우트 장비의 IP 주소와 로봇 이름을 설정
        self.hostname =  os.environ.get('SCOUT_IP')
        self.robot_name = robot_name
        self.robot_ip = robot_ip
        
    def send_sms(self):
        """오늘 수행할 미션 정보를 바탕으로 사용자들에게 SMS를 보냄

        Args:
            today_mission_infos (_type_): mission info class로 구성된 list
            spot_stay (_type_): bool
            gabia_sms (_type_): gabia_sms class

        Returns:
            _type_: _description_
        """
        import threading

        # 로봇이 도크에 위치해 있는지 여부를 확인하는 객체 생성
        dock_sms = SpotStaySmsHelper(self.robot_ip, self.robot_name)
        is_spot_on_dock = dock_sms.check_is_dock()
        
        # 미션 정보를 처리하는 객체 생성
        mission_sms = MissionSmsHelper(self.robot_name, self.robot_ip)
        
        # util 모듈의 get_mission_name_by_booking_mission_time 함수를 사용하여 오늘 수행할 미션 정보를 가져옴
        missions_infoes = util.get_mission_name_by_booking_mission_time(self.robot_name) #mission_info return
        
        # 가져온 미션 정보를 바탕으로 문자 메시지를 생성하고, 데이터베이스를 업데이트함
        mission_sms.change_today_mission_results_depend_on_request(missions_infoes, is_spot_on_dock)
        util.update_booking_info(missions)
        
        # 만약 미션 정보가 없고 로봇이 도크에 위치해 있다면 메시지를 보내지 않음
        if not missions_infoes and is_spot_on_dock:
            print("no mission no message")
            return
        
        # 공장별 미션을 처리함
        # util 모듈의 get_fatories_by_robot 함수를 사용하여 공장 이름을 가져오고, get_phone_numberes_of_fatories 함수를 사용하여 해당 공장의 전화번호를 가져옴
        mission_names = util.get_fatories_by_robot(robot_name=self.robot_name, matching_mission_infoes=missions_infoes)
        address = util.get_phone_numberes_of_fatories(mission_names)]
        
        # 로봇이 도크에 위치해 있는지 여부를 고려하여 메시지를 보냄
        dock_sms.send_message(address=address, msg=dock_sms.create_message(is_spot_on_dock), url=self.sms_url)

 

결론

수신자 대상자가 달라지는 SMS 기능을 제공합니다. 이를 위해 요구 사항 분석, 시스템 아키텍처 설계, DB 모델링, SMS 발송 API 연동, 예약 미션 알고리즘 구현 등의 작업을 수행했습니다. 이를 통해 정확한 정보 전달을 할 수 있도록 하였으며, 성능 향상 및 확장성 고려를 통해 더욱 발전 가능한 시스템을 만들었습니다.

반응형