메랜샵 - 마스터리북 시뮬레이터 개발기

Murhyun2·6일 전
3

메랜샵

목록 보기
6/7
post-thumbnail

메이플스토리 메랜샵(아이템 거래 서비스)을 운영하면서, 기존의 단순한 자리거래 기능을 넘어서는 새로운 가치를 제공하고 싶었습니다. 메이플랜드 유저들의 고민 중 하나가 바로 값비싼 마스터리북이었고, 이를 시뮬레이션할 수 있는 기능이 있다면 재미있을 것이라 생각했습니다.

메이플랜드 마스터리북 시뮬레이터를 제공하는 타 사이트는 존재하지 않았기에 개발을 시작하였고, 이를 구현하려면 모든 마스터리북의 정확한 정보(이미지, 한국어 이름, 마스터리 타입)가 필요했습니다.

데이터 수집의 첫 번째 과제: 한글명 확보

API 발견과 초기 기대

메이플스토리 관련 데이터를 찾던 중 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 vs KMS의 차이점

구분GMSKMS
메이플랜드 호환성✅ 호환❌ 버전 차이 존재
아이템명 언어🇺🇸 영문🇰🇷 한국어
데이터 완전성⚠️ 일부 필드 제한✅ 풍부한 데이터

예를 들어, 같은 마스터리북을 조회했을 때:

  • GMS: "Bow Expert 30"
  • KMS4: "활 마스터리북 30"

메랜샵은 한국 서비스이므로 당연히 한글명이 필요했지만, 메이플랜드와 호환되는 GMS 버전에서는 영문명만 제공되는 상황이었습니다.

전략적 접근: 두 API 모두 활용하기

이 문제를 해결하기 위해 다음과 같은 하이브리드 접근법을 계획했습니다:

  1. GMS 62버전에서 메이플랜드 호환 아이템 목록 확인
  2. KMS 284버전에서 동일한 아이템 ID로 한국어 이름 추출
  3. 두 데이터를 조합하여 완전한 마스터리북 정보 구성

하지만 이 계획이 성공하려면 핵심적인 가정이 성립해야 했습니다:

"기본 아이템들의 ID는 버전이 달라도 동일할 것이다"

데이터 매핑 전략과 검증 과정

가설 검증: 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 범위를 찾기 위해 다음과 같은 방법을 사용했습니다:

  1. 2290000부터 순차적으로 증가하며 API 호출
  2. 404 응답이 나오는 지점까지 확인
  3. 응답이 있는 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

여러 대조 작업 끝에 2290000부터 2290096까지가 메이플랜드 마스터리북의 완전한 범위임을 확인했습니다. 총 97개의 마스터리북이 존재했고, 모든 ID에서 GMS와 KMS 모두 정상 응답을 받을 수 있었습니다.

커뮤니티의 도움: 메랜 개발자 오픈채팅 활용

데이터 매핑 과정에서 확신이 서지 않는 부분들이 있어 maplestory.io 개발자 오픈채팅방에 참여했습니다.

핵심 질문들

채팅방에서 다음과 같은 질문들을 했습니다:

  • "GMS와 KMS의 아이템 ID 체계가 동일한가요?"
  • "버전이 다름에도 불구하고 기본 아이템들의 ID는 유지되나요?"
  • "마스터리북 범위를 정확히 알 수 있는 방법이 있나요?"

얻은 조언

개발자와 다른 사용자들로부터 다음과 같은 유용한 정보를 얻었습니다:

"기본 아이템들(무기, 방어구, 마스터리북 등)의 ID는 버전 간 호환성을 위해 대부분 동일하게 유지됩니다. 다만 신규 아이템이나 리뉴얼된 아이템은 다를 수 있어요."

이 조언으로 해당 접근법이 올바른 방향임을 확신할 수 있었습니다.

검증 완료: 매핑 전략의 성공

최종적으로 다음 사실들을 확인했습니다:

  • ID 완전 동일: 모든 마스터리북에서 GMS/KMS ID 일치
  • 데이터 완전성: 97개 마스터리북 모두 양쪽 API에서 접근 가능
  • 정보 보완성: GMS(영문명) + KMS(한글명) = 완전한 데이터

이제 실제 데이터 수집을 위한 기술적 구현 단계로 넘어갈 준비가 되었습니다.

기술적 선택과 구현

아키텍처 설계: 클라우드 기반 데이터 파이프라인

데이터 매핑 전략이 확정된 후, 실제 수집과 저장을 위한 시스템 아키텍처를 설계해야 했습니다.

전체 데이터 플로우

maplestory.io API → AWS Lambda → MySQL → 메랜샵 백엔드 API

이 구조를 선택한 이유는 다음과 같습니다:

  • Lambda: 서버리스로 관리 부담 최소화, 필요시에만 실행
  • MySQL: 메랜샵 기존 인프라와의 일관성
  • 백엔드 API: 캐싱된 데이터로 빠른 조회 성능

기술 스택 선택: 왜 Python인가?

백엔드 개발자로서 평소에는 Java를 주로 사용하지만, Lambda에서는 Python을 선택했습니다.

Java vs Python 비교

고려사항JavaPython
Lambda 콜드 스타트~2-3초~500ms
패키지 크기큼 (JAR 파일)작음 (가벼운 라이브러리)
HTTP 라이브러리OkHttp, Apache HCrequests (간단)
DB 연결JDBC (무겁다)pymysql (경량)
개발 속도상대적으로 느림빠른 프로토타이핑

Python 선택의 결정적 이유

# Python - 간결한 API 호출
response = requests.get(f"https://maplestory.io/api/kms/284/item/{item_id}")
data = response.json()

# Java에서는 더 많은 보일러플레이트 코드 필요

Lambda 환경에서는 경량성과 빠른 실행이 중요했고, Python의 간결함이 이 요구사항에 완벽히 맞았습니다.

핵심 로직 구현

1. 이벤트 기반 ID 범위 처리

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))

2. 데이터 추출

핵심인 데이터 추출 로직입니다:

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

3. 마스터리 타입 자동 분류

마스터리북 이름에서 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'  # 기본값

배포와 실행 전략

1. 환경 변수 관리

민감한 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)}"})
    }

2. 데이터 무결성 보장

배치 처리 중 일부 아이템이 실패해도 이미 처리된 데이터는 유지되어야 했습니다.

트랜잭션과 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']
    ))
  • 새로운 데이터는 INSERT
  • 기존 데이터는 UPDATE
  • 중복 처리 시에도 에러 없이 안전하게 처리

3. 로깅과 모니터링 체계

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}개")

4. 향후 확장성 고려

정기적 실행을 위한 CloudWatch Events 연동도 염두에 두고 설계했습니다:

{
  "use_default": true,
  "description": "Daily mastery book data refresh"
}

5. 클라우드의 장점 활용

Lambda 선택으로 얻은 이점들:

  • 어디서나 실행 가능: 로컬 환경에 의존하지 않음
  • 안정적인 네트워크: AWS 인프라의 안정성 활용
  • 스케일링: 필요시 동시 실행 가능
  • 비용 효율: 실행시에만 과금

최종 성과

✅ 완전한 마스터리북 데이터베이스 구축: 97개 전체 수집 성공
✅ 한국어 이름 확보: 모든 아이템의 정확한 한국어 명칭 획득
✅ 이미지 URL 연동: 각 마스터리북의 아이콘 이미지 경로 확보
✅ 자동 분류 시스템: 마스터리 타입(20/30) 자동 판별 로직 완성

배운 점들

1. API 조합 활용의 가능성

"하나의 API로 해결되지 않는다고 포기하지 말자"

GMS와 KMS API를 조합하여 각각의 장점을 활용할 수 있었습니다. 이는 외부 API 활용 시 창의적 접근의 중요성을 보여주었습니다.

2. 커뮤니티의 실질적 가치

개발자 오픈채팅방에서 받은 조언이 프로젝트의 방향성을 확정하는 데 결정적이었습니다.
혼자 고민하는 시간보다 커뮤니티에 질문하는 것이 훨씬 효율적임을 깨달았습니다.

3. 클라우드 환경의 실용적 장점

AWS Lambda 활용으로 얻은 이점들:

  • 인프라 관리 부담 제거: 서버 관리, 스케일링 등 신경 쓸 필요 없음
  • 비용 효율성: 실행할 때만 과금되어 월 1달러 미만으로 운영
  • 안정성: AWS 인프라의 높은 가용성 활용
  • 확장성: 향후 정기 실행이나 다른 데이터 수집에도 활용 가능

이번 경험을 통해 외부 API 활용의 창의적 접근이 얼마나 중요한지 깨달았습니다.
하나의 API가 모든 요구사항을 충족하지 못한다고 해서 포기하지 말고, 여러 API를 조합하거나 다른 관점에서 접근해보면 해결책을 찾을 수 있을 것 입니다.😁

profile
왜?를 생각하며 개발하기

2개의 댓글

comment-user-thumbnail
6일 전

야한 글이군요 ㅇㅅㅇ...

1개의 답글