https://newbiecs.tistory.com/440
ONVIF로 IP 카메라 설정 제어
IP 카메라를 코드로 제어하려면 ONVIF라는 표준 프로토콜을 이해해야 합니다. 이 글에서는 ONVIF의 기본 개념부터 Python으로 카메라의 인코더 설정을 조회하고 변경하는 방법까지 단계적으로 설명
newbiecs.tistory.com
이전 게시글에서 간단히 XML과 SOAP에 대해서 간단히 설명했다.
이 게시글에서는 XML과 SOAP 그리고 REST API와는 어떤 차이점이 있는지 살펴보자

1. XML과 SOAP
1.1 XML이란?
XML(eXtensible Markup Language)은 데이터를 태그로 감싸서 표현하는 마크업 언어입니다.
HTML과 생김새는 비슷하지만, HTML이 "어떻게 보여줄지"를 정의한다면 XML은 "데이터 구조"를 표현합니다.
<tt:Resolution>
<tt:Width>3840</tt:Width>
<tt:Height>2160</tt:Height>
</tt:Resolution>
핵심 개념
- 태그(Tag): <tt:Width> 여는 태그, </tt:Width> 닫는 태그 사이에 값이 들어갑니다.
- 트리 구조(Tree): 태그는 중첩이 가능하며, 이 계층 구조를 트리라고 합니다.
- 속성(Attribute): token="VideoEncoderToken-02-0" 처럼 태그 안에 부가 정보를 넣는 것입니다.
XML 트리 구조 시각화
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"> # xmlns: SOAP 규칙(namespace)를 따른다
<s:Header>
<wsse:Security>
<wsse:UsernameToken>
<wsse:Username>admin</wsse:Username>
<wsse:Password>p@ssw0rd</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</s:Header>
<s:Body>
<trt:GetVideoEncoderConfigurations/>
</s:Body>
</s:Envelope>
---
Envelope
├── Header
│ └── Security
│ └── UsernameToken
│ ├── Username → "admin"
│ └── Password → "admin"
└── Body
└── GetVideoEncoderConfigurations
Python에서 ElementTree로 파싱하면 이 트리를 그대로 탐색
import xml.etree.ElementTree as ET
root = ET.fromstring(xml_string) # 루트 노드(Envelope) 반환
# 트리 탐색: .// 는 하위 모든 노드에서 찾기
username = root.find('.//wsse:Username', NS)
print(username.text) # admin
# 속성(attribute) 읽기
config = root.find('.//trt:Configuration', NS)
print(config.get('token')) # "VideoEncoderToken-02-0"
1.2 SOAP이란?
SOAP(Simple Object Access Protocol)는 카메라 기능 호출 요청을 XML 형태로 만들어 HTTP로 전달하는 방식
1.2.1 SOAP 메시지의 전체 구조: Envelope / Header / Body / Fault
SOAP 메시지는 크게 4가지 요소로 구성됩니다.
1. Header (선택사항)
- 인증 정보나 부가 메타데이터를 담습니다.
- 카메라에 따라 생략 가능하지만, WS-Security 인증을 요구하는 카메라라면 반드시 포함
<s:Header>
<wsse:Security xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext">
<wsse:UsernameToken>
<wsse:Username>admin</wsse:Username>
<wsse:Password Type="PasswordDigest">hashed_pw==</wsse:Password>
<wsse:Nonce>base64_nonce==</wsse:Nonce>
<wsu:Created>2026-05-20T00:00:00Z</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</s:Header>
PasswordDigest란 비밀번호를 평문으로 보내는 것이 아니라 SHA-1(Nonce + Created + Password)를 Base64로 인코딩해서 전송합니다. 매 요청마다 Nonce(일회성 난수)가 바뀌므로 리플레이 공격을 방어합니다.
2. Body (필수)
- 실제 요청 함수(Get, Set) 또는 응답 데이터가 들어갑니다.
<s:Body>
<trt:GetVideoEncoderConfiguration xmlns:trt="http://www.onvif.org/ver10/media/wsdl"> # xmlns를 Envelope에 선언했다면 없어도 됨
<trt:ConfigurationToken>VideoEncoderToken-02-0</trt:ConfigurationToken>
</trt:GetVideoEncoderConfiguration>
<trt:SetVideoEncoderConfiguration xmlns:trt="http://www.onvif.org/ver10/media/wsdl"> # xmlns를 Envelope에 선언했다면 없어도 됨
<trt:Configuration token="VideoEncoderToken-02-0">
<tt:Name xmlns:tt="http://www.onvif.org/ver10/schema">
H264Config
</tt:Name>
<tt:UseCount xmlns:tt="http://www.onvif.org/ver10/schema">
1
</tt:UseCount>
<tt:Encoding xmlns:tt="http://www.onvif.org/ver10/schema">
H264
</tt:Encoding>
<tt:Resolution xmlns:tt="http://www.onvif.org/ver10/schema">
<tt:Width>1920</tt:Width>
<tt:Height>1080</tt:Height>
</tt:Resolution>
<tt:RateControl xmlns:tt="http://www.onvif.org/ver10/schema">
<tt:FrameRateLimit>30</tt:FrameRateLimit>
<tt:BitrateLimit>4096</tt:BitrateLimit>
</tt:RateControl>
</trt:Configuration>
<trt:ForcePersistence>true</trt:ForcePersistence>
</trt:SetVideoEncoderConfiguration>
</s:Body>
3. Fault (오류 응답)
- 요청이 실패했을 때 Body 안에 Fault 요소가 들어옵니다.
- HTTP의 4xx/5xx 상태 코드와 비슷한 역할입니다.
- 중요한 점은 SOAP Fault가 와도 HTTP 상태 코드는 보통 200 OK 입니다. Body를 파싱해야 오류를 알 수 있습니다.
<s:Body>
<s:Fault>
<s:Code>
<s:Value>s:Sender</s:Value> # 잘못된 요청 (클라이언트 잘못)
<s:Subcode>
<s:Value>ter:InvalidArgVal</s:Value> # ONVIF 세부 오류 코드
</s:Subcode>
</s:Code>
<s:Reason>
<s:Text>The requested configuration token does not exist.</s:Text>
</s:Reason>
</s:Fault>
</s:Body>
| Fault Code | 의미 |
| s:Sender | 클라이언트(요청)가 잘못됨 - 잘못된 토큰, 파라미터 오류 등 |
| s:Receiver | 서버(카메라)가 처리 못함 - 카메라 내부 오류 |
| ter:InvalidArgVal | 잘못된 인자값 |
| ter:NotAuthorized | 인증 실패 |
| ter:ActionNotSupported | 해당 카메라가 이 기능을 지원하지 않음 |
그래서 SOAP 응답을 처리할 때는 반드시 Fault 여부를 먼저 확인해야 합니다.
def parse_response(raw_xml: str):
root = ET.fromstring(raw_xml)
# Fault 먼저 확인 (HTTP 200이어도 Fault일 수 있음!)
fault = root.find('.//s:Fault', NS)
if fault is not None:
code = fault.findtext('.//s:Value', namespaces=NS)
reason = fault.findtext('.//s:Text', namespaces=NS)
raise ONVIFError(f"SOAP Fault [{code}]: {reason}")
# 정상 응답 처리
return root.find('.//trt:Configuration', NS)
1.2.2 REST API vs SOAP - 개발자 관점 비교
현대 개발에서 RST(JSON)가 지배적이지만, 산업용 장비·금융·의료 분야에서는 SOAP가 여전히 사용되고 있다.
| 항목 | REST | SOAP |
| 데이터 형식 | JSON (또는 XML) | XML 고정 |
| 전송 프로토콜 | HTTP (GET/POST/PUT/DELETE) | HTTP만 사용, 항상 POST |
| 호출 방식 | 리소스 중심 (/cameras/1/encoder) | 함수 중심 (GetVideoEncoderConfiguration) |
| 오류 처리 | HTTP 상태 코드 (4xx, 5xx) | SOAP Fault (HTTP는 항상 200) |
| 스키마 정의 | 선택 (OpenAPI) | 필수 (WSDL) |
| 인증 | JWT, OAuth2, API Key 등 다양 | WS-Security (Digest/Basic) |
| 학습 난이도 | 낮음 | 높음 |
| 주요 사용처 | 웹/모바일 API | 금융, 의료, 산업 장비 |
'Language > Python' 카테고리의 다른 글
| ONVIF로 IP 카메라 설정 제어 (0) | 2026.05.20 |
|---|---|
| [Python] Iterator, Generator 정리 (0) | 2025.12.26 |
| [FastAPI] 'Unknown Column' 에서 Alembic 사용법 (SQLAlchemy 스키마 동기화) (0) | 2025.06.15 |
| 'google-api-python-client'를 사용한 유튜브 데이터 가져오기 (3) (0) | 2023.09.04 |
| 'google-api-python-client'를 사용한 유튜브 데이터 가져오기 - pandas, argparse (2) (0) | 2023.08.29 |
댓글