메이플스토리 메랜샵(아이템 거래 서비스)을 운영하면서, 기존의 단순한 자리거래 기능을 넘어서는 새로운 가치를 제공하고 싶었습니다. 메이플랜드 유저들의 고민 중 하나가 바로 값비싼 마스터리북이었고, 이를 시뮬레이션할 수 있는 기능이 있다면 재미있을 것이라 생각했습니다.
메이플랜드 마스터리북 시뮬레이터를 제공하는 타 사이트는 존재하지 않았기에 개발을 시작하였고, 이를 구현하려면 모든 마스터리북의 정확한 정보(이미지, 한국어 이름, 마스터리 타입)가 필요했습니다.
메이플스토리 관련 데이터를 찾던 중 maplestory.io API를 발견했습니다. 이 API는 메이플스토리의 아이템, 스킬, 맵 등 다양한 게임 데이터를 JSON 형태로 제공하는 비공식 API였습니다.
https://maplestory.io/api/kms/284/item/{item_id}
https://maplestory.io/api/gms/62/item/{item_id}
Swagger 문서도 잘 정리되어 있고, 아이콘 이미지까지 제공하는 것을 보고 "이거면 마스터리북 데이터를 쉽게 수집할 수 있겠다"고 생각했습니다.
하지만 실제로 API를 테스트해보면서 중요한 문제를 발견했습니다:
구분 | GMS | KMS |
---|---|---|
메이플랜드 호환성 | ✅ 호환 | ❌ 버전 차이 존재 |
아이템명 언어 | 🇺🇸 영문 | 🇰🇷 한국어 |
데이터 완전성 | ⚠️ 일부 필드 제한 | ✅ 풍부한 데이터 |
예를 들어, 같은 마스터리북을 조회했을 때:
"Bow Expert 30"
"활 마스터리북 30"
메랜샵은 한국 서비스이므로 당연히 한글명이 필요했지만, 메이플랜드와 호환되는 GMS 버전에서는 영문명만 제공되는 상황이었습니다.
이 문제를 해결하기 위해 다음과 같은 하이브리드 접근법을 계획했습니다:
하지만 이 계획이 성공하려면 핵심적인 가정이 성립해야 했습니다:
"기본 아이템들의 ID는 버전이 달라도 동일할 것이다"
하이브리드 접근법의 성공 여부는 "GMS와 KMS에서 동일한 아이템 ID를 사용하는가?"에 달려 있었습니다. 이를 검증하기 위해 몇 가지 마스터리북 ID로 테스트해보았습니다.
마스터리북은 일반적으로 2290000
대역의 ID를 사용한다는 정보를 바탕으로, 임의의 ID들로 테스트를 시작했습니다:
# GMS 62 테스트
GET https://maplestory.io/api/gms/62/item/2290000
→ "Bow Expert 20"
# KMS 284 테스트
GET https://maplestory.io/api/kms/284/item/2290000
→ "활 마스터리북 20"
결과는 놀라웠습니다. 동일한 ID로 두 API 모두 정상적으로 응답했고, 아이템의 기본 정보(타입, 아이콘 등)도 일치했습니다.
ID 동일성이 확인되자, 이제 전체 마스터리북의 정확한 범위를 파악해야 했습니다.
마스터리북 ID 범위를 찾기 위해 다음과 같은 방법을 사용했습니다:
# 대략적인 검증 로직
for item_id in range(2290000, 2291000):
gms_response = requests.get(f"https://maplestory.io/api/gms/62/item/{item_id}")
kms_response = requests.get(f"https://maplestory.io/api/kms/284/item/{item_id}")
if gms_response.status_code == 200 and kms_response.status_code == 200:
# 마스터리북 여부 확인 로직
if is_mastery_book(gms_response.json()):
valid_ids.append(item_id)
여러 대조 작업 끝에 2290000부터 2290096까지가 메이플랜드 마스터리북의 완전한 범위임을 확인했습니다. 총 97개의 마스터리북이 존재했고, 모든 ID에서 GMS와 KMS 모두 정상 응답을 받을 수 있었습니다.
데이터 매핑 과정에서 확신이 서지 않는 부분들이 있어 maplestory.io 개발자 오픈채팅방에 참여했습니다.
채팅방에서 다음과 같은 질문들을 했습니다:
개발자와 다른 사용자들로부터 다음과 같은 유용한 정보를 얻었습니다:
"기본 아이템들(무기, 방어구, 마스터리북 등)의 ID는 버전 간 호환성을 위해 대부분 동일하게 유지됩니다. 다만 신규 아이템이나 리뉴얼된 아이템은 다를 수 있어요."
이 조언으로 해당 접근법이 올바른 방향임을 확신할 수 있었습니다.
최종적으로 다음 사실들을 확인했습니다:
이제 실제 데이터 수집을 위한 기술적 구현 단계로 넘어갈 준비가 되었습니다.
데이터 매핑 전략이 확정된 후, 실제 수집과 저장을 위한 시스템 아키텍처를 설계해야 했습니다.
maplestory.io API → AWS Lambda → MySQL → 메랜샵 백엔드 API
이 구조를 선택한 이유는 다음과 같습니다:
백엔드 개발자로서 평소에는 Java를 주로 사용하지만, Lambda에서는 Python을 선택했습니다.
고려사항 | Java | Python |
---|---|---|
Lambda 콜드 스타트 | ~2-3초 | ~500ms |
패키지 크기 | 큼 (JAR 파일) | 작음 (가벼운 라이브러리) |
HTTP 라이브러리 | OkHttp, Apache HC | requests (간단) |
DB 연결 | JDBC (무겁다) | pymysql (경량) |
개발 속도 | 상대적으로 느림 | 빠른 프로토타이핑 |
# Python - 간결한 API 호출
response = requests.get(f"https://maplestory.io/api/kms/284/item/{item_id}")
data = response.json()
# Java에서는 더 많은 보일러플레이트 코드 필요
Lambda 환경에서는 경량성과 빠른 실행이 중요했고, Python의 간결함이 이 요구사항에 완벽히 맞았습니다.
Lambda 함수는 다양한 방식으로 호출할 수 있도록 설계했습니다:
def get_item_ids_from_event(event: Dict[Any, Any]) -> list:
# 1. 범위 지정: {"start_id": 2290000, "end_id": 2290096}
if 'start_id' in event and 'end_id' in event:
return list(range(event['start_id'], event['end_id'] + 1))
# 2. 특정 ID 리스트: {"item_ids": [2290001, 2290005]}
if 'item_ids' in event:
return list(set(int(item_id) for item_id in event['item_ids']))
# 3. 단일 ID: {"single_id": 2290096}
if 'single_id' in event:
return [int(event['single_id'])]
# 4. 기본값 (테스트용)
return list(range(2290000, 2290006))
핵심인 데이터 추출 로직입니다:
def extract_mastery_data(data: Dict[Any, Any], item_id: int) -> Optional[Dict[str, Any]]:
try:
# KMS에서 한국어 이름과 기본 정보 추출
item_info = {
'id': item_id,
'name': data['description']['name'], # 한국어 이름
'description': data['description']['description'],
'icon_raw_url': f"https://maplestory.io/api/kms/284/item/{item_id}/iconraw"
}
# 마스터리 타입 추출 (이름에서 20 또는 30 찾기)
mastery_type = extract_mastery_type(item_info['name'])
item_info['mastery_type'] = mastery_type or '20' # 기본값
return item_info
except KeyError as e:
print(f"❌ ID {item_id}: 필수 키 누락 - {str(e)}")
return None
마스터리북 이름에서 20/30을 자동으로 추출하는 로직:
def extract_mastery_type(name: str) -> Optional[str]:
# 정규식으로 이름 끝의 20 또는 30 찾기
match = re.search(r'\b(20|30)\s*$', name)
if match:
return match.group(1)
# 추가 패턴 확인
if name.endswith('20'):
return '20'
elif name.endswith('30'):
return '30'
return '20' # 기본값
민감한 DB 정보는 Lambda 환경 변수로 관리:
# 환경 변수 확인
required_env_vars = ['DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME']
missing_vars = [var for var in required_env_vars if not os.environ.get(var)]
if missing_vars:
return {
'statusCode': 400,
'body': json.dumps({'error': f"누락된 환경 변수: {', '.join(missing_vars)}"})
}
배치 처리 중 일부 아이템이 실패해도 이미 처리된 데이터는 유지되어야 했습니다.
트랜잭션과 UPSERT를 활용했습니다:
pythondef save_to_mysql(cursor, data: Dict[str, Any]):
sql = """
INSERT INTO mastery_books (id, name, description, icon_raw_url, mastery_type)
VALUES (%s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
description = VALUES(description),
icon_raw_url = VALUES(icon_raw_url),
mastery_type = VALUES(mastery_type),
updated_time = CURRENT_TIMESTAMP
"""
cursor.execute(sql, (
data['id'], data['name'], data['description'],
data['icon_raw_url'], data['mastery_type']
))
Lambda 실행 중 어떤 단계에서 문제가 발생했는지 추적이 어려운 문제가 있어서 구조화된 로깅 시스템을 구축했습니다:
pythondef lambda_handler(event, context):
print(f"받은 이벤트: {json.dumps(event, ensure_ascii=False)}")
print(f"DB 연결 정보: {DB_HOST}, 사용자: {DB_USER}")
print(f"처리할 아이템 ID: {len(item_ids)}개")
for i, item_id in enumerate(item_ids, 1):
print(f"진행률: {i}/{len(item_ids)} - ID {item_id} 처리 중...")
# ... 처리 로직
print(f"ID {item_id}: {extracted_data['name']} 저장 완료")
print(f"최종 결과: 성공 {success_count}개, 실패 {error_count}개")
정기적 실행을 위한 CloudWatch Events 연동도 염두에 두고 설계했습니다:
{
"use_default": true,
"description": "Daily mastery book data refresh"
}
Lambda 선택으로 얻은 이점들:
✅ 완전한 마스터리북 데이터베이스 구축: 97개 전체 수집 성공
✅ 한국어 이름 확보: 모든 아이템의 정확한 한국어 명칭 획득
✅ 이미지 URL 연동: 각 마스터리북의 아이콘 이미지 경로 확보
✅ 자동 분류 시스템: 마스터리 타입(20/30) 자동 판별 로직 완성
"하나의 API로 해결되지 않는다고 포기하지 말자"
GMS와 KMS API를 조합하여 각각의 장점을 활용할 수 있었습니다. 이는 외부 API 활용 시 창의적 접근의 중요성을 보여주었습니다.
개발자 오픈채팅방에서 받은 조언이 프로젝트의 방향성을 확정하는 데 결정적이었습니다.
혼자 고민하는 시간보다 커뮤니티에 질문하는 것이 훨씬 효율적임을 깨달았습니다.
AWS Lambda 활용으로 얻은 이점들:
이번 경험을 통해 외부 API 활용의 창의적 접근이 얼마나 중요한지 깨달았습니다.
하나의 API가 모든 요구사항을 충족하지 못한다고 해서 포기하지 말고, 여러 API를 조합하거나 다른 관점에서 접근해보면 해결책을 찾을 수 있을 것 입니다.😁
야한 글이군요 ㅇㅅㅇ...