OpenRTB 경매에서 bid를 수신한 SSP는 그 bid가 Banner인지, Video인지, Native인지 알아야 한다. 잘못된 타입으로 판단하면 광고가 깨지거나 트래킹이 누락된다. 이 글은 실무에서 자주 맞닥뜨리는 mtype 판별 문제와 그 대응 전략을 정리한다.


1. 왜 미디어 타입 판별이 어려운가

하나의 Impression에 DSP가 Banner와 Video를 동시에 응찰할 수 있는 멀티포맷 Impression이 일반적으로 사용된다.

{
  "imp": [{
    "id": "1",
    "banner": {"w": 300, "h": 250},
    "video": {"mimes": ["video/mp4"], "w": 300, "h": 250}
  }]
}

DSP는 이 중 하나를 선택해 응답한다. SSP 입장에서는 받은 bid가 Banner 응답인지 Video 응답인지 명시적으로 알아야 이후 처리(adm 파싱, 트래킹, 캐싱)를 올바르게 할 수 있다.

문제는 bid.mtype 필드가 권장(Recommended) 필드라는 점이다. 없어도 스펙 위반이 아니다. 레거시 DSP나 OpenRTB 2.4 기반 비더는 mtype을 아예 보내지 않는 경우가 많다.


2. mtype 필드

bid.mtype은 OpenRTB 2.6에서 추가된 필드로, bid의 마크업 타입을 명시한다.

OpenRTB 버전mtype 상태
2.5 이하없음
2.6추가됨 (권장, Recommended)

mtype 값 정의:

mtype 값미디어 타입adm 포맷예시
1BannerHTML/JS<div><img src="..."/></div>
2VideoVAST XML<VAST version="3.0">...</VAST>
3AudioDAAST XML<DAAST version="1.0">...</DAAST>
4NativeNative JSON{"assets":[...],"link":{...}}

mtype이 있으면 가장 신뢰도 높은 타입 정보다. 무조건 우선 사용한다.


3. mtype이 없을 때 Fallback 전략 체인

mtype이 없는 bid를 받았다고 해서 바로 에러 처리하면 안 된다. 순차적으로 시도할 수 있는 fallback이 있다.

우선순위 요약

우선순위방법신뢰도설명
1bid.mtype높음OpenRTB 표준 필드
2bid.ext.prebid.type높음Prebid 확장
3bid.ext.mediaType중간일부 비더 확장
4Imp 단일 타입 추론중간멀티포맷이 아닌 경우만
5AdM 콘텐츠 파싱낮음최후의 수단

전략 1: Extension 필드

Prebid Server는 bid.ext.prebid.type을 fallback으로 사용한다. 일부 비더는 mtype 대신 이 필드를 채워 보낸다.

{
  "seatbid": [{
    "bid": [{
      "id": "bid-1",
      "impid": "imp-1",
      "price": 2.50,
      "adm": "...",
      "ext": {
        "prebid": {
          "type": "banner"
        }
      }
    }]
  }]
}

전략 2: Impression 기반 추론

요청한 Impression에 단일 미디어 타입만 있으면 그것을 사용한다. 멀티포맷이면 추론할 수 없다.

func inferMediaType(bid Bid, imp Impression) MediaType {
    types := []MediaType{}

    if imp.Banner != nil { types = append(types, Banner) }
    if imp.Video != nil  { types = append(types, Video) }
    if imp.Audio != nil  { types = append(types, Audio) }
    if imp.Native != nil { types = append(types, Native) }

    if len(types) == 1 {
        return types[0]  // 단일 타입이면 확정
    }

    return Unknown  // 복수 타입이면 추론 불가
}

전략 3: AdM 콘텐츠 파싱

adm 문자열을 직접 분석해 타입을 추론한다. 가장 신뢰도가 낮으므로 최후의 수단이다.

패턴추론 타입
<VAST 또는 <?xml...VASTVideo
<DAASTAudio
JSON + "assets" 배열Native
HTML/JavaScriptBanner
func inferFromAdM(adm string) MediaType {
    trimmed := strings.TrimSpace(adm)

    if strings.HasPrefix(trimmed, "<?xml") || strings.HasPrefix(trimmed, "<VAST") {
        return Video
    }

    if strings.HasPrefix(trimmed, "{") {
        var native map[string]interface{}
        if json.Unmarshal([]byte(adm), &native) == nil {
            if _, ok := native["assets"]; ok {
                return Native
            }
        }
    }

    return Banner
}

4. Response 처리 플로우차트

flowchart TB
    BID["bid 수신"] --> CHECK_MTYPE{"bid.mtype > 0?"}

    CHECK_MTYPE -->|Yes| USE_MTYPE["mtype 사용"]
    CHECK_MTYPE -->|No| EXT{"bid.ext에 타입 정보 있음?"}

    EXT -->|Yes| USE_EXT["ext에서 타입 추출"]
    EXT -->|No| INFER{"Impression 단일 타입?"}

    INFER -->|Yes| USE_INFER["Imp 미디어 타입 사용"]
    INFER -->|No| ADM{"AdM 파싱으로 추론?"}

    ADM -->|성공| USE_ADM["AdM 분석 결과 사용"]
    ADM -->|실패| ERROR["에러 처리"]

    USE_MTYPE --> PROCESS["타입별 adm 처리"]
    USE_EXT --> PROCESS
    USE_INFER --> PROCESS
    USE_ADM --> PROCESS

    PROCESS --> BANNER{"Banner?"}
    PROCESS --> VIDEO{"Video?"}
    PROCESS --> NATIVE{"Native?"}
    PROCESS --> AUDIO{"Audio?"}

    BANNER --> HTML["HTML 렌더링"]
    VIDEO --> VAST["VAST 파싱"]
    NATIVE --> NJSON["Native JSON 파싱"]
    AUDIO --> DAAST["DAAST 파싱"]

5. 멀티포맷 Impression 처리

멀티포맷 Impression에서 mtype이 없으면 Imp 기반 추론이 불가능하다. 이 경우 반드시 mtype이 있어야 타입을 확정할 수 있다.

flowchart LR
    IMP["Imp: banner + video"] --> BID["Bid 수신"]
    BID --> MTYPE{"mtype?"}
    MTYPE -->|있음| OK["타입 결정"]
    MTYPE -->|없음| FAIL["추론 불가 → 에러"]

비더(adapter) 개발 시 응답에 mtype을 반드시 설정해야 하는 이유가 여기 있다.

func getBidType(bid openrtb2.Bid) openrtb_ext.BidType {
    switch bid.MType {
    case openrtb2.MarkupBanner:
        return openrtb_ext.BidTypeBanner
    case openrtb2.MarkupVideo:
        return openrtb_ext.BidTypeVideo
    case openrtb2.MarkupAudio:
        return openrtb_ext.BidTypeAudio
    case openrtb2.MarkupNative:
        return openrtb_ext.BidTypeNative
    default:
        return inferBidType(bid)  // fallback
    }
}

mtype을 결정할 수 없는 경우의 권장 처리:

  • 해당 bid만 건너뛰기 (다른 유효한 bid는 처리)
  • 에러 로그 기록
  • 메트릭 수집 (타입 파싱 실패 비율 모니터링)

상황별 처리 요약

상황권장 처리
mtype > 0그대로 사용
mtype = 0, ext 있음ext에서 타입 추출
단일 미디어 타입 ImpImp 타입 사용
멀티포맷 + mtype 없음에러 처리
AdM 분석 필요최후의 수단으로만

6. Native VideoAsset vs Video Ad

실무에서 가장 많이 혼동하는 케이스다. 둘 다 “비디오"를 포함하지만 완전히 다른 개념이다.

핵심 차이: 비디오가 광고 전체인가, 일부인가

flowchart TB
    subgraph VideoAd["Video Ad (mtype=2)"]
        VA["비디오 = 광고 전체"]
        VA --> VA1["비디오 플레이어에서 재생"]
        VA --> VA2["VAST XML로 모든 것 정의"]
        VA --> VA3["예: YouTube 프리롤"]
    end

    subgraph NativeVideoAsset["Native VideoAsset (mtype=4 내부)"]
        NVA["비디오 = 광고의 구성요소 중 하나"]
        NVA --> NVA1["피드 내 인라인 재생"]
        NVA --> NVA2["Native JSON의 asset 배열 안"]
        NVA --> NVA3["예: 인스타그램 피드 광고"]
    end
관점Video AdNative VideoAsset
비디오의 역할광고 전체광고의 일부 (asset 중 하나)
다른 요소없음Title, Image, CTA 등과 함께
adm 포맷VAST XMLNative JSON (내부에 vasttag 포함 가능)
mtype 값24
강제성시청 강제 (스킵 가능)자연스러운 노출
소리기본 ON보통 음소거
트래킹25/50/75/100% 쿼타일Native 레벨 impression

adm 포맷 비교

Video Ad (VAST XML):

<?xml version="1.0"?>
<VAST version="3.0">
  <Ad id="12345">
    <InLine>
      <Creatives>
        <Creative>
          <Linear>
            <Duration>00:00:30</Duration>
            <TrackingEvents>
              <Tracking event="start">...</Tracking>
              <Tracking event="complete">...</Tracking>
            </TrackingEvents>
            <MediaFiles>
              <MediaFile type="video/mp4">
                https://cdn.example.com/video.mp4
              </MediaFile>
            </MediaFiles>
          </Linear>
        </Creative>
      </Creatives>
    </InLine>
  </Ad>
</VAST>

Native Ad with VideoAsset (Native JSON):

{
  "ver": "1.2",
  "assets": [
    {"id": 1, "title": {"text": "새로운 게임 출시!"}},
    {"id": 2, "img": {"url": "https://cdn.example.com/icon.png", "w": 80, "h": 80}},
    {"id": 3, "video": {"vasttag": "<VAST>...</VAST>"}},
    {"id": 4, "data": {"type": 2, "value": "지금 다운로드하세요"}},
    {"id": 5, "data": {"type": 12, "value": "설치하기"}}
  ],
  "link": {"url": "https://click.example.com"},
  "imptrackers": ["https://track/imp"]
}

Native VideoAsset 안에도 vasttag로 VAST를 포함할 수 있지만, 이는 Native 광고의 한 구성요소로 재생된다. Video Ad가 아니다.

코드로 보는 차이

func renderAd(bid Bid) {
    switch bid.MType {
    case 2: // Video Ad
        vast := parseVAST(bid.AdM)
        videoPlayer.load(vast.MediaFiles[0].URL)
        videoPlayer.play()
        // 플레이어가 모든 것 담당

    case 4: // Native Ad
        native := parseNativeJSON(bid.AdM)

        renderTitle(native.Assets.Title)
        renderIcon(native.Assets.Icon)
        renderMainImage(native.Assets.MainImage)
        renderDescription(native.Assets.Description)
        renderCTA(native.Assets.CTA)

        // VideoAsset이 있으면 인라인으로 렌더링
        if native.Assets.Video != nil {
            inlineVideo := createInlineVideo(native.Assets.Video)
            inlineVideo.autoplay = true
            inlineVideo.muted = true
            layout.addView(inlineVideo)
        }
    }
}

한 문장 요약: Video Ad는 “비디오 광고"이고, Native VideoAsset은 “비디오가 포함된 Native 광고"이다.


7. Prebid Server 참고 구현

Prebid Server의 exchange/utils.go는 이 전략을 그대로 구현하고 있다.

func getMediaTypeForBid(bid openrtb2.Bid) (BidType, error) {
    mType := bid.MType

    // 1. mtype이 있으면 우선 사용
    if mType > 0 {
        switch mType {
        case openrtb2.MarkupBanner:
            return BidTypeBanner, nil
        case openrtb2.MarkupVideo:
            return BidTypeVideo, nil
        case openrtb2.MarkupAudio:
            return BidTypeAudio, nil
        case openrtb2.MarkupNative:
            return BidTypeNative, nil
        default:
            return "", fmt.Errorf("invalid mType: %d", mType)
        }
    }

    // 2. Fallback: bid.ext.prebid.type 확인
    return getPrebidMediaTypeForBid(bid)
}

func getPrebidMediaTypeForBid(bid openrtb2.Bid) (BidType, error) {
    if bid.Ext != nil {
        var bidExt ExtBid
        if json.Unmarshal(bid.Ext, &bidExt) == nil {
            if bidExt.Prebid != nil {
                return ParseBidType(bidExt.Prebid.Type)
            }
        }
    }

    return "", errors.New("failed to parse bid mediatype")
}

Prebid Server는 Imp 기반 추론과 AdM 파싱을 adapter 레벨로 위임한다. 각 비더 adapter의 MakeBids() 함수에서 타입을 TypedBid.BidType으로 명시해 반환하도록 설계되어 있다.


핵심 원칙

  1. mtype이 있으면 무조건 신뢰
  2. Fallback은 순차적으로 시도
  3. 확실하지 않으면 에러 처리 (잘못된 타입보다 에러가 낫다)
  4. 비더 adapter 개발 시 mtype 필수 반환
  5. Native 안의 VideoAsset은 Video Ad가 아니다