Python 3.x 프로젝트에서 YunTongXun(容联云通讯) 플랫폼을 활용해 SMS 인증 코드를 발송하는 방법을 살펴본다. 해당 플랫폼은 가입 후 실명 인증 없이도 테스트용 문자 발송 기능을 제공하며, 다만 무료 테스트 계정은 미리 등록한 3개의 휴대폰 번호로만 발송 가능하고 템플릿은 1개만 사용할 수 있다.
SDK 구성 및 프로젝트 설정
공식 문서에서 Python SDK를 내려받으면 CCPRestSDK.py와 xmltojson.py 두 개의 핵심 파일이 포함되어 있다. 프로젝트 내 yuntongxun 폴더를 생성하고 이 파일들을 배치한 후, 기존 SendTemplateSMS.py 대신 싱글톤 패턴으로 재구성한 모듈을 사용한다.
메시지 발송 클래스 구현
아래 sms_dispatcher.py는 공식 예제를 재구성한 싱글톤 패턴 기반의 발송 클래스다. Python 3 문법에 맞게 수정했으며, 계정 정보는 실제 콘솔 값으로 대체해야 한다.
# coding=utf-8
from .CCPRestSDK import REST
# 콘솔에서 확인한 계정 정보
ACCOUNT_SID = '실제_ACCOUNT_SID'
AUTH_TOKEN = '실제_AUTH_TOKEN'
APP_ID = '실제_AppID'
# 서버 접속 정보
HOST = 'app.cloopen.com'
PORT = '8883'
API_VERSION = '2013-12-26'
class MessageSender(object):
_singleton = None
def __new__(cls):
if cls._singleton is None:
instance = super(MessageSender, cls).__new__(cls)
instance.client = REST(HOST, PORT, API_VERSION)
instance.client.setAccount(ACCOUNT_SID, AUTH_TOKEN)
instance.client.setAppId(APP_ID)
cls._singleton = instance
return cls._singleton
def deliver_verification_code(self, phone_number, content_values, template_id):
"""
인증 문자 발송 메서드
Args:
phone_number: 수신자 번호
content_values: 템플릿 변수 값 목록 (예: ['1234', '5'])
template_id: 템플릿 식별자
"""
response = self.client.sendTemplateSMS(
phone_number,
content_values,
template_id
)
print('response:', response)
status = response.get("statusCode")
return 0 if status == "000000" else -1
if __name__ == '__main__':
sender = MessageSender()
outcome = sender.deliver_verification_code(
"176xxxxxxxx",
["5678", "3"],
1
)
print(outcome)
Python 3 마이그레이션 핵심 수정 사항
원본 SDK가 Python 2 기반으로 작성되어 있어 Python 3 환경에서 여러 문법 오류가 발생한다. 핵심 수정 내역은 다음과 같다.
모듈 임포트 변경
# Python 2
# import md5
# import urllib2
# Python 3
from hashlib import md5
import urllib.request as urllib2
해시 및 인코딩 처리
# 문자열을 바이트로 변환 후 해싱
signature = self.AccountSid + self.AccountToken + self.Batch
signature = signature.encode('utf-8')
sig = md5(signature).hexdigest().upper()
# Base64 인코딩 시 바이트 변환 필수
src = self.AccountSid + ":" + self.Batch
auth = base64.encodestring(src.encode()).strip()
요청 본문 전송 방식
# Python 2 방식 (미지원)
# req.add_data(body)
# Python 3 방식 - 바이트 타입으로 변환
req.data = body.encode()
네트워크 오류 {'172001':'网络错误'} 해결
위 문법 수정 후에도 네트워크 오류가 지속될 수 있다. 세 가지 추가 조치가 필요하다.
1. 상대 경로 임포트 적용
가상 환경의 xmltojson.py 대신 프로젝트 내 SDK 파일을 사용하도록 수정한다.
# 잘못된 방식
# from xmltojson import xmltojson
# 올바른 방식
from .xmltojson import xmltojson
2. SSL 인증서 검증 비활성화
Python 2.7.9 이후부터 HTTPS 연결 시 SSL 인증서 검증이 기본 적용된다. YunTongXun 서버가 자체 서명 인증서를 사용하므로 검증을 우회해야 한다.
import ssl
# 전역적으로 SSL 인증서 검증 비활성화
ssl._create_default_https_context = ssl._create_unverified_context
3. 요청 데이터 타입 최종 확인
최종적으로 req.data에 할당하는 값이 bytes 타입인지 다시 한번 확인한다. 문자열 그대로 전달하면 서버에서 빈 본문으로 해석해 오류가 발생한다.
완성된 SDK 핵심 모듈 (CCPRestSDK.py)
아래는 Python 3 호환성을 완전히 적용한 핵심 SDK 코드의 발송 메서드 부분이다.
import datetime
import json
import base64
from hashlib import md5
import urllib.request as urllib2
from .xmltojson import xmltojson
class REST:
def __init__(self, server_ip, server_port, soft_version):
self.ServerIP = server_ip
self.ServerPort = server_port
self.SoftVersion = soft_version
self.AccountSid = ''
self.AccountToken = ''
self.AppId = ''
self.Batch = ''
self.BodyType = 'xml'
self.Iflog = True
def setAccount(self, account_sid, account_token):
self.AccountSid = account_sid
self.AccountToken = account_token
def setAppId(self, app_id):
self.AppId = app_id
def sendTemplateSMS(self, to, datas, temp_id):
now = datetime.datetime.now()
self.Batch = now.strftime("%Y%m%d%H%M%S")
# 서명 생성
sign_content = (self.AccountSid + self.AccountToken + self.Batch).encode('utf-8')
signature = md5(sign_content).hexdigest().upper()
# 요청 URL 구성
endpoint = (
f"https://{self.ServerIP}:{self.ServerPort}/"
f"{self.SoftVersion}/Accounts/{self.AccountSid}/"
f"SMS/TemplateSMS?sig={signature}"
)
# 인증 헤더 생성
auth_raw = f"{self.AccountSid}:{self.Batch}"
auth_header = base64.encodestring(auth_raw.encode()).strip()
# HTTP 요청 준비
request = urllib2.Request(endpoint)
self._apply_headers(request)
request.add_header("Authorization", auth_header)
# 요청 본문 구성
if self.BodyType == 'json':
data_str = '","'.join(datas)
payload = (
f'{{"to": "{to}", "datas": ["{data_str}"], '
f'"templateId": "{temp_id}", "appId": "{self.AppId}"}}'
)
request.add_header("Accept", "application/json")
request.add_header("Content-Type", "application/json;charset=utf-8")
else:
items = ''.join(f'<data>{item}</data>' for item in datas)
payload = (
f'<?xml version="1.0" encoding="utf-8"?>'
f'<SubAccount><datas>{items}</datas>'
f'<to>{to}</to><templateId>{temp_id}</templateId>'
f'<appId>{self.AppId}</appId></SubAccount>'
)
request.add_header("Accept", "application/xml")
request.add_header("Content-Type", "application/xml;charset=utf-8")
# 중요: bytes 타입으로 변환하여 전송
request.data = payload.encode('utf-8')
# 요청 실행
try:
response = urllib2.urlopen(request)
raw_data = response.read()
response.close()
if self.BodyType == 'json':
result = json.loads(raw_data)
else:
converter = xmltojson()
result = converter.main(raw_data)
return result
except Exception as exc:
print(f"Request failed: {exc}")
return {'172001': '网络错误'}
def _apply_headers(self, req):
content_type = "application/json" if self.BodyType == 'json' else "application/xml"
req.add_header("Accept", content_type)
req.add_header("Content-Type", f"{content_type};charset=utf-8")
위 코드는 Python 3.6 이상 환경에서 정상 동작하며, SSL 인증서 문제와 바이트/문자열 변환 이슈를 모두 해결한 상태다. 실제 운영 환경에서는 SSL 검증 우회 대신 공식 인증서를 적용하는 것이 보안상 권장된다.