AI 기반 QA 혁신: Python AI 에이전트, LLM, RPA 통합 자율 복구 시스템 구축 실전 가이드
테스트 케이스 자동 생성부터 결함 예측 및 자율 복구까지, 최적의 QA 파이프라인 구축 전략
이지웍스랩 AI리서치 · 2026-09-08 · B2B 업무 자동화 · 읽는 데 12분
빠르게 변화하는 비즈니스 환경에서 소프트웨어 품질 보증(QA)은 그 어느 때보다 중요해졌습니다. 그러나 여전히 많은 기업이 반복적이고 수동적인 QA 프로세스로 인해 시간과 비용을 낭비하고, 치명적인 결함을 놓치는 악순환에 시달리고 있습니다. 이제 Python AI 에이전트, LLM(거대 언어 모델), RPA(로봇 프로세스 자동화)의 통합을 통해 이러한 한계를 극복하고, 테스트 케이스 자동 생성부터 실행, 결함 예측, 심지어 자율 복구까지 가능한 혁신적인 AI 기반 QA 시스템을 구축할 때입니다. 본 아티클은 실제 현장에서 바로 적용할 수 있는 구체적인 가이드와 코드를 제공하여, 여러분의 QA 프로세스를 한 차원 높은 수준으로 끌어올릴 것입니다.
1. 서론: AI 기반 QA 혁신이 필요한 이유
전통적인 QA 방식은 개발 속도를 따라가지 못하고 있습니다. 애자일(Agile) 및 데브옵스(DevOps) 환경에서는 짧은 주기로 수많은 배포가 이루어지며, 이는 테스트해야 할 범위와 복잡성을 기하급수적으로 증가시킵니다. 수동 테스트는 비효율적일 뿐만 아니라 인적 오류의 가능성을 내포하며, 자동화된 테스트조차 테스트 케이스 작성 및 유지보수에 막대한 리소스가 소요됩니다.
이러한 문제의식 속에서, AI와 자동화 기술의 융합은 QA 분야에 새로운 지평을 열고 있습니다. 특히, Python 기반의 AI 에이전트가 LLM의 강력한 언어 이해 및 생성 능력과 RPA의 실제 시스템 조작 능력을 결합한다면, QA 프로세스 전반을 지능적으로 자동화하고 최적화할 수 있습니다. 이는 단순히 테스트를 자동화하는 것을 넘어, 결함을 예측하고 스스로 복구를 제안하며, 궁극적으로는 사람의 개입 없이도 시스템의 품질을 상시 보증하는 자율적인 QA 시스템 구축을 가능하게 합니다.
2. 실전 아키텍처 및 구현 코드: AI QA 에이전트 구축
AI 기반 QA 시스템의 핵심은 Python으로 구현된 AI 에이전트입니다. 이 에이전트는 Orchestrator 역할을 수행하며, LLM API 호출을 통해 테스트 케이스를 생성하고, RPA 봇에게 실행 지시를 내리며, 실행 결과를 분석하고, 머신러닝 모델을 활용하여 결함을 예측하며, 필요한 경우 자율 복구 프로세스를 트리거합니다.
아래 코드는 이러한 AI 에이전트의 핵심 로직을 보여줍니다. `AIAgent` 클래스는 LLM을 이용한 테스트 케이스 생성, RPA 시스템 연동, 그리고 결과 분석 및 결함 예측 모듈을 통합합니다. 특히, `generate_test_cases` 메서드는 LLM에게 특정 기능 명세와 과거 결함 데이터를 기반으로 효과적인 테스트 케이스를 생성하도록 프롬프트를 구성합니다. `execute_tests_with_rpa` 메서드는 생성된 테스트 케이스를 RPA 봇이 이해할 수 있는 형태로 변환하여 실행을 지시합니다.
import os
import json
import requests
from typing import List, Dict, Any
# 가상의 LLM API 및 RPA API URL
LLM_API_URL = os.getenv('LLM_API_URL', 'http://localhost:8000/llm')
RPA_API_URL = os.getenv('RPA_API_URL', 'http://localhost:8001/rpa')
class AIAgent:
def __init__(self, model_name: str = "gpt-4o-mini"):
self.model_name = model_name
print(f"[AIAgent] 초기화: LLM 모델 '{self.model_name}' 사용")
def _call_llm(self, prompt: str, temperature: float = 0.7) -> str:
"""LLM API를 호출하여 응답을 받습니다."""
headers = {'Content-Type': 'application/json'}
payload = {
'model': self.model_name,
'prompt': prompt,
'temperature': temperature
}
try:
response = requests.post(LLM_API_URL, headers=headers, json=payload, timeout=60)
response.raise_for_status() # HTTP 오류 발생 시 예외 발생
return response.json().get('response', '')
except requests.exceptions.RequestException as e:
print(f"[ERROR] LLM API 호출 실패: {e}")
return ""
def _call_rpa(self, task_payload: Dict[str, Any]) -> Dict[str, Any]:
"""RPA API를 호출하여 테스트 실행을 지시합니다."""
headers = {'Content-Type': 'application/json'}
try:
response = requests.post(RPA_API_URL, headers=headers, json=task_payload, timeout=120)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"[ERROR] RPA API 호출 실패: {e}")
return {'status': 'failed', 'error': str(e)}
def generate_test_cases(self, feature_spec: str, past_defects: List[Dict[str, str]]) -> List[Dict[str, str]]:
"""LLM을 사용하여 테스트 케이스를 생성합니다."""
past_defects_str = "\n".join([f"- {d['description']} (Severity: {d['severity']})" for d in past_defects])
prompt = f"""
당신은 숙련된 QA 엔지니어입니다. 다음 기능 명세와 과거 결함 데이터를 기반으로, JSON 형식의 테스트 케이스 목록을 생성해주세요.
각 테스트 케이스는 'id', 'description', 'steps'(단계별 설명 리스트), 'expected_result' 필드를 포함해야 합니다.
<기능 명세>
{feature_spec}
<과거 결함 데이터 (참고용)>
{past_defects_str}
생성된 테스트 케이스 목록은 JSON 배열 형식으로만 응답해주세요.
예시: [
{{"id": "TC001", "description": "로그인 성공 테스트", "steps": ["ID 입력", "PW 입력", "로그인 버튼 클릭"], "expected_result": "메인 페이지 진입"}}
]
"""
print("[AIAgent] LLM에 테스트 케이스 생성 요청...")
llm_response = self._call_llm(prompt, temperature=0.5)
try:
test_cases = json.loads(llm_response)
if isinstance(test_cases, list) and all(isinstance(tc, dict) for tc in test_cases):
print(f"[AIAgent] {len(test_cases)}개의 테스트 케이스 생성 완료.")
return test_cases
else:
raise ValueError("LLM 응답이 올바른 JSON 배열 형식이 아닙니다.")
except json.JSONDecodeError as e:
print(f"[ERROR] LLM 응답 JSON 파싱 실패: {e}\n응답 내용: {llm_response}")
return []
except ValueError as e:
print(f"[ERROR] {e}")
return []
def execute_tests_with_rpa(self, test_cases: List[Dict[str, str]]) -> List[Dict[str, Any]]:
"""RPA를 사용하여 테스트 케이스를 실행합니다."""
results = []
print(f"[AIAgent] RPA를 통해 {len(test_cases)}개의 테스트 실행 요청...")
for tc in test_cases:
rpa_payload = {
'test_id': tc['id'],
'test_description': tc['description'],
'test_steps': tc['steps']
}
rpa_result = self._call_rpa(rpa_payload)
results.append({
'test_id': tc['id'],
'description': tc['description'],
'status': rpa_result.get('status', 'unknown'),
'actual_result': rpa_result.get('actual_result', ''),
'error_message': rpa_result.get('error', '')
})
print(f" - TC {tc['id']}: {rpa_result.get('status', 'unknown')}")
print("[AIAgent] RPA 테스트 실행 완료.")
return results
def predict_and_analyze_defects(self, test_results: List[Dict[str, Any]], historical_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""테스트 결과를 분석하고 LLM 및 ML 모델을 활용하여 결함을 예측/분석합니다."""
# 실제 ML 모델을 연동하는 부분은 복잡하므로, LLM을 활용한 분석으로 대체
# 실제 구현에서는 LightGBM, RandomForest 등 ML 모델을 사용하여 결함 패턴 학습 및 예측
failed_tests = [res for res in test_results if res['status'] == 'failed']
if not failed_tests:
print("[AIAgent] 실패한 테스트가 없어 결함 분석을 건너뜁니다.")
return []
prompt = f"""
다음은 최근 실행된 테스트 결과 중 실패한 항목들입니다. 이를 기반으로 잠재적인 결함의 원인을 분석하고, 가능한 해결책을 JSON 형식으로 제안해주세요.
각 결함 분석은 'test_id', 'problem_summary', 'potential_cause', 'suggested_solution' 필드를 포함해야 합니다.
<실패한 테스트 결과>
{json.dumps(failed_tests, indent=2)}
<과거 결함 및 해결 이력 (참고용)>
{json.dumps(historical_data, indent=2)}
생성된 결함 분석 목록은 JSON 배열 형식으로만 응답해주세요.
"""
print("[AIAgent] LLM에 결함 분석 및 해결책 제안 요청...")
llm_response = self._call_llm(prompt, temperature=0.3)
try:
defect_analysis = json.loads(llm_response)
if isinstance(defect_analysis, list):
print(f"[AIAgent] {len(defect_analysis)}개의 결함 분석 및 해결책 제안 완료.")
return defect_analysis
else:
raise ValueError("LLM 응답이 올바른 JSON 배열 형식이 아닙니다.")
except json.JSONDecodeError as e:
print(f"[ERROR] 결함 분석 LLM 응답 JSON 파싱 실패: {e}\n응답 내용: {llm_response}")
return []
except ValueError as e:
print(f"[ERROR] {e}")
return []
def self_heal(self, defect_analysis: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""분석된 결함에 대해 자율 복구를 시도하거나 복구 방안을 제안합니다."""
if not defect_analysis:
print("[AIAgent] 자율 복구할 결함이 없습니다.")
return []
healing_reports = []
print("[AIAgent] 자율 복구 프로세스 시작...")
for defect in defect_analysis:
# 실제 자율 복구 로직 (예: 코드 패치, 설정 변경, 롤백 등) 구현
# 여기서는 LLM을 통해 복구 스크립트를 생성하는 예시
prompt = f"""
다음 결함 분석을 바탕으로, 해당 문제를 해결하기 위한 Python 또는 Bash 스크립트(예: 설정 파일 수정, 캐시 삭제, 서비스 재시작 등)를 생성해주세요.
단, 스크립트만 생성하고, 위험한 작업은 반드시 주석 처리하거나 사용자 승인을 요구하는 로직을 포함해주세요.
<결함 분석>
{json.dumps(defect, indent=2)}
생성된 스크립트와 함께 복구 시도 결과를 JSON 형식으로 응답해주세요.
예시: {{"test_id": "TC001", "status": "proposed", "script": "# 복구 스크립트 내용\nprint('Service restart command')", "message": "복구 스크립트 제안 완료"}}
"""
llm_response = self._call_llm(prompt, temperature=0.3)
try:
healing_report = json.loads(llm_response)
if isinstance(healing_report, dict):
healing_reports.append(healing_report)
print(f" - TC {defect['test_id']}: 자율 복구 {healing_report.get('status', 'unknown')}")
else:
raise ValueError("LLM 응답이 올바른 JSON 형식이 아닙니다.")
except json.JSONDecodeError as e:
print(f"[ERROR] 자율 복구 LLM 응답 JSON 파싱 실패: {e}\n응답 내용: {llm_response}")
healing_reports.append({'test_id': defect['test_id'], 'status': 'failed', 'message': f'LLM 응답 파싱 오류: {e}'})
except ValueError as e:
print(f"[ERROR] {e}")
healing_reports.append({'test_id': defect['test_id'], 'status': 'failed', 'message': str(e)})
print("[AIAgent] 자율 복구 프로세스 완료.")
return healing_reports
# --- 시스템 실행 예시 --- #
if __name__ == "__main__":
# 환경 변수 설정 (실제 환경에서는 .env 파일 등 사용)
# os.environ['LLM_API_URL'] = 'https://api.openai.com/v1/chat/completions' # 실제 LLM API URL
# os.environ['RPA_API_URL'] = 'http://your-rpa-system/api/v1/tasks'
agent = AIAgent(model_name="gpt-4o-mini") # 또는 "claude-3-opus-20240229", "gemini-1.5-pro-latest"
# 1. 기능 명세 및 과거 결함 데이터 준비
feature_spec_example = "사용자는 웹사이트에서 상품을 검색하고 장바구니에 추가한 후 주문할 수 있어야 합니다. 결제는 신용카드와 간편결제를 지원합니다."
past_defects_example = [
{"description": "장바구니에 상품 추가 시 간헐적으로 수량 오류 발생", "severity": "High"},
{"description": "모바일 환경에서 결제 페이지 레이아웃 깨짐", "severity": "Medium"}
]
# 2. 테스트 케이스 자동 생성
generated_tcs = agent.generate_test_cases(feature_spec_example, past_defects_example)
if not generated_tcs:
print("테스트 케이스 생성에 실패했습니다. 종료합니다.")
sys.exit(1)
# 3. RPA를 통한 테스트 실행 (가상 시뮬레이션)
print("\n--- RPA 테스트 실행 시뮬레이션 ---")
mock_rpa_results = []
for i, tc in enumerate(generated_tcs):
status = 'passed' if i % 3 != 0 else 'failed' # 3개 중 1개 실패 가정
actual_result = "정상 동작" if status == 'passed' else "예상과 다른 동작 발생"
error_message = "" if status == 'passed' else "오류 코드 500: 데이터베이스 연결 실패 (가상)"
mock_rpa_results.append({
'test_id': tc['id'],
'description': tc['description'],
'status': status,
'actual_result': actual_result,
'error_message': error_message
})
# 실제 환경에서는 agent.execute_tests_with_rpa(generated_tcs) 호출
test_execution_results = mock_rpa_results # 가상 결과 사용
print("--- RPA 테스트 실행 시뮬레이션 완료 ---\n")
# 4. 결함 예측 및 분석
historical_analysis_data = [
{"test_id": "TC005", "problem_summary": "로그인 기능 간헐적 실패", "potential_cause": "세션 관리 오류", "suggested_solution": "세션 저장 방식 변경"}
]
defect_analysis_reports = agent.predict_and_analyze_defects(test_execution_results, historical_analysis_data)
# 5. 자율 복구 시도
healing_results = agent.self_heal(defect_analysis_reports)
print("\n--- 최종 요약 ---")
print(f"생성된 테스트 케이스: {len(generated_tcs)}개")
print(f"실행된 테스트 결과: {len(test_execution_results)}개")
print(f"분석된 결함: {len(defect_analysis_reports)}개")
print(f"자율 복구 시도: {len(healing_results)}개")
for res in healing_results:
print(f" - [복구 보고] TC {res['test_id']}: 상태={res['status']}, 메시지={res['message']}")
if 'script' in res:
print(f" [제안 스크립트]:\n{res['script']}")
3. 실무 적용 효과 및 성능 벤치마크
AI 기반 QA 시스템은 단순히 업무 부담을 줄이는 것을 넘어, QA 프로세스의 전반적인 품질과 효율성을 혁신적으로 개선합니다. 특히 반복적인 회귀 테스트, 다양한 시나리오 기반의 탐색적 테스트, 그리고 복잡한 시스템 통합 테스트에서 그 효과가 두드러집니다. 아래 표는 AI 기반 QA 시스템 도입 전후의 주요 성과 지표 변화를 보여줍니다.
| 구분 | 기존 수작업/부분 자동화 | AI 기반 QA 시스템 |
|---|---|---|
| 테스트 케이스 생성 시간 | 수일 ~ 수주 | 수분 ~ 수시간 (LLM 기반) |
| 테스트 실행 시간 | 수시간 ~ 수일 | 수분 ~ 수시간 (RPA 병렬 처리) |
| 결함 발견율 | 평균 70% | 90% 이상 (AI 예측/분석) |
| 수동 개입률 (테스트/분석) | 80% 이상 | 10% 미만 (예외 처리 및 승인 제외) |
| QA 리소스 절감 | 불가 | 30% 이상 (장기적 관점) |
| 신규 기능 배포 속도 | 지연 발생 가능 | 최대 50% 가속화 |
4. 결함 예측 및 자율 복구 메커니즘
AI 기반 QA 시스템의 진정한 가치는 단순히 결함을 찾아내는 것을 넘어, 결함의 발생 가능성을 예측하고, 스스로 복구 방안을 제안하며, 심지어 직접 복구까지 시도하는 데 있습니다. 이를 위해 머신러닝 모델은 과거 결함 데이터, 시스템 로그, 코드 변경 이력, 테스트 실행 결과 등을 학습하여 특정 패턴에서 결함이 발생할 확률을 예측합니다.
예측된 결함이나 실제 발생한 결함에 대해서는 LLM이 다시 한번 핵심적인 역할을 수행합니다. LLM은 결함 메시지, 스택 트레이스, 시스템 로그 등을 분석하여 문제의 근본 원인을 파악하고, 개발자 문서, 코드 저장소, 과거 해결 사례 등을 참고하여 가장 효과적인 복구 방안을 제시합니다. 이 복구 방안은 단순한 설명이 아니라, 실제 시스템에 적용 가능한 코드 패치, 설정 파일 변경 스크립트, 혹은 서비스 재시작 명령 형태로 제안될 수 있습니다. AI 에이전트는 이러한 제안을 검토하고, 사전에 정의된 안전 프로토콜에 따라 자동 적용하거나, 개발자의 최종 승인을 거쳐 적용합니다.
5. 안정적인 운영을 위한 가드레일 및 모니터링
AI 기반 자율 시스템은 강력한 만큼, 오작동 시 치명적인 영향을 줄 수 있습니다. 따라서 안정적인 운영을 위한 견고한 가드레일(Guardrail) 구축이 필수적입니다. 첫째, 모든 자동화된 변경사항은 반드시 버전 관리 시스템에 기록되고, 롤백이 가능한 구조여야 합니다. 둘째, '휴먼-인-더-루프(Human-in-the-Loop, HITL)' 원칙을 적용하여, 중요하거나 위험도가 높은 자율 복구 제안은 반드시 개발자 또는 QA 엔지니어의 최종 승인을 거치도록 설계해야 합니다. 셋째, 상세한 감사 로깅 및 실시간 모니터링 시스템을 구축하여 AI 에이전트의 모든 활동(테스트 케이스 생성, 실행, 복구 시도 등)을 추적하고, 이상 징후 발생 시 즉시 알림을 받을 수 있도록 해야 합니다.
또한, LLM의 응답 품질은 프롬프트 엔지니어링에 크게 좌우됩니다. 지속적인 프롬프트 최적화와 함께, LLM이 생성한 테스트 케이스나 복구 스크립트의 유효성을 검증하는 별도의 자동화된 검증 단계를 포함하는 것이 중요합니다. 예를 들어, 생성된 테스트 스크립트가 문법적으로 올바른지, 실행 가능한지를 사전에 확인하는 린팅(Linting) 및 샌드박스 실행 환경을 도입할 수 있습니다.
자주 막히는 지점
LLM이 생성한 테스트 케이스가 실제 기능 명세와 동떨어지거나 비효율적입니다.
원인: LLM 프롬프트가 충분히 구체적이지 않거나, LLM이 최신 도메인 지식이나 시스템 컨텍스트를 충분히 학습하지 못했을 수 있습니다. 또한, 과거 결함 데이터가 부족하거나 제대로 활용되지 않았을 가능성도 있습니다.
해결: 프롬프트 엔지니어링을 고도화하세요. 기능 명세를 더욱 상세히 제공하고, 시스템 아키텍처 다이어그램, API 명세서, UI/UX 가이드라인 등 풍부한 컨텍스트를 RAG(Retrieval Augmented Generation) 방식으로 LLM에 전달합니다. 또한, 과거 발생했던 실제 결함 데이터를 잘 정제하여 LLM 학습 또는 프롬프트에 포함시켜, LLM이 실제 문제를 유발했던 시나리오를 고려하도록 유도합니다. 필요한 경우, 특정 도메인에 특화된 경량 LLM을 파인튜닝하는 것을 고려할 수 있습니다.
RPA 봇을 통한 테스트 실행이 간헐적으로 실패하거나 불안정합니다.
원인: RPA 봇은 UI 기반의 자동화이므로, 시스템 환경 변화(화면 해상도, 요소 ID 변경, 네트워크 지연 등)에 민감합니다. 비동기적인 웹 페이지 로딩, 예상치 못한 팝업, 시스템 자원 부족 등이 원인일 수 있습니다.
해결: RPA 스크립트를 더욱 견고하게 만드세요. 요소 식별자를 CSS Selector, XPath 등 여러 방식으로 정의하여 유연성을 확보하고, 명시적인 대기 시간(Explicit Wait)을 사용하여 페이지 로딩을 기다리도록 합니다. 실패 시 재시도 로직을 구현하고, 스크린샷 캡처 및 상세 로그를 남겨 디버깅을 용이하게 합니다. 또한, RPA 봇이 실행되는 환경을 표준화하고 전용 리소스를 할당하여 외부 요인에 의한 영향을 최소화합니다. AI 에이전트가 RPA 실행 전 시스템 상태를 점검하는 'Pre-flight check' 로직을 추가하는 것도 도움이 됩니다.
핵심 요약
- AI 에이전트, LLM, RPA의 통합은 QA 프로세스 전반을 지능적으로 자동화하여 효율성과 품질을 동시에 극대화할 수 있습니다.
- 테스트 케이스 자동 생성, 실행, 결함 예측 및 자율 복구 시스템 구축은 개발 사이클을 가속화하고 QA 비용을 절감하는 핵심 전략입니다.
- 안정적인 AI 기반 QA 시스템 운영을 위해 프롬프트 엔지니어링 고도화, 견고한 RPA 스크립트 작성, 그리고 휴먼-인-더-루프(HITL) 원칙을 포함한 강력한 가드레일 구축이 필수적입니다.
자주 묻는 질문
LLM 사용 시 발생하는 비용은 어떻게 관리해야 하나요?
LLM 사용 비용은 주로 토큰 사용량에 비례합니다. 비용 효율적인 운영을 위해, 첫째, 프롬프트의 길이를 최적화하고 불필요한 정보 전달을 최소화합니다. 둘째, 복잡도가 낮은 작업에는 gpt-4o-mini와 같은 저비용 모델을 사용하고, 고도의 추론이 필요한 경우에만 고성능 모델을 활용하는 전략을 수립합니다. 셋째, RAG(Retrieval Augmented Generation)를 통해 LLM이 모든 정보를 생성하는 대신, 내부 지식 기반에서 필요한 정보를 검색하도록 하여 토큰 사용량을 줄일 수 있습니다. 마지막으로, LLM API 사용량을 실시간으로 모니터링하고 예산 알림을 설정하는 것이 중요합니다.
기존의 QA 시스템 (Jenkins, Jira 등)과 어떻게 통합할 수 있나요?
AI 기반 QA 에이전트는 기존 시스템과의 연동을 통해 시너지를 극대화할 수 있습니다. 예를 들어, Jenkins와 같은 CI/CD 파이프라인에 AI 에이전트의 테스트 케이스 생성 및 실행 단계를 통합하여 코드 커밋 시 자동으로 QA가 시작되도록 할 수 있습니다. Jira와 같은 이슈 트래킹 시스템과는 API 연동을 통해 AI 에이전트가 발견한 결함을 자동으로 이슈로 등록하고, 자율 복구 시도 결과를 코멘트로 업데이트하도록 설정할 수 있습니다. 이 과정에서 웹훅(Webhook)이나 REST API를 활용하여 각 시스템 간의 데이터 흐름을 자동화합니다.
AI 에이전트의 자율 복구 시스템은 얼마나 안전하게 동작하나요?
자율 복구 시스템의 안전성은 설계와 구현 방식에 따라 달라집니다. 본 가이드에서 강조한 '가드레일' 원칙을 철저히 지키는 것이 중요합니다. 즉, 모든 자율 복구 시도는 반드시 롤백 가능한 형태로 구현하고, 중요한 시스템 변경은 '휴먼-인-더-루프(HITL)' 원칙에 따라 개발자나 QA 엔지니어의 최종 승인을 거치도록 합니다. 또한, 복구 스크립트 실행 전 샌드박스 환경에서 사전 검증을 수행하고, 실행 후 시스템의 건전성(Health Check)을 즉시 확인하는 단계를 포함해야 합니다. 지속적인 모니터링과 로깅을 통해 비정상적인 동작을 신속하게 감지하고 대응할 수 있는 체계를 갖추는 것이 핵심입니다.