Skip to content

Latest commit

 

History

History
161 lines (128 loc) · 10.3 KB

File metadata and controls

161 lines (128 loc) · 10.3 KB

03. Jaxb2Marshaller — JAXB 통합과 보안

무엇을 / 왜

org.springframework.oxm.jaxb.Jaxb2Marshaller는 이 모듈의 간판 구현이다. JAXB(Jakarta XML Binding, jakarta.xml.bind)를 Spring의 Marshaller/Unmarshaller 추상화 뒤에 붙인다. JAXB는 애노테이션 기반 데이터 바인딩의 자바 표준이므로, 실무에서 spring-oxm을 쓴다면 대개 이 클래스다.

이 클래스가 하는 일은 세 가지로 압축된다. (1) JAXBContext어떻게 만들고 캐시할지 관리하고, (2) 매 변환마다 thread-unsafe한 JAXB Marshaller/Unmarshaller새로 생성·초기화하며, (3) JAXB 자체가 입력 소스에 대해 안전하지 않으므로 XXE 방어를 직접 끼워 넣는다. 또한 이 클래스 하나가 GenericMarshaller, GenericUnmarshaller, MimeMarshaller, MimeUnmarshaller를 전부 구현하는 "풀옵션" 변환기다.

핵심 타입과 빈 생명주기

   Jaxb2Marshaller
     implements MimeMarshaller, MimeUnmarshaller,
                GenericMarshaller, GenericUnmarshaller,
                BeanClassLoaderAware, InitializingBean
     │
     │ 설정 입력 (셋 중 정확히 하나 필수)
     ├── contextPath        "com.example.foo:com.example.bar"
     ├── classesToBeBound   Class[] { Order.class, ... }
     └── packagesToScan     String[] { "com.example" }  → 스캔
                                   │
                                   ▼
                       ClassPathJaxb2TypeScanner
                       (@XmlRootElement/@XmlType/...
                        애노테이션 클래스 탐색)

Jaxb2MarshallerInitializingBean이라 빈 초기화 시 afterPropertiesSet()이 불린다.

  afterPropertiesSet()
     │
     ├─ contextPath / classesToBeBound / packagesToScan
     │    ▶ 정확히 하나만 설정됐는지 검증 (아니면 IllegalArgumentException)
     │
     ├─ lazyInit == false (기본) ?
     │    ▶ getJaxbContext()  ← JAXBContext 미리 생성
     │
     └─ schemaResources 있으면
          ▶ loadSchema(...)  ← 검증용 스키마 미리 로드

기본적으로 시작 시점에 JAXBContext를 만든다(lazyInit=false). JAXBContext 생성은 비싼 작업이므로, 애플리케이션 기동 때 한 번 치르고 이후엔 재사용하는 것이 합리적이다. lazyInit=true로 첫 사용 시점까지 미룰 수도 있다.

동작 흐름 1 — JAXBContext의 지연·캐시 생성

getJaxbContext()는 이 클래스에서 가장 중요한 메서드다. **이중 검사 락킹(double-checked locking)**으로 JAXBContext를 한 번만 만들어 캐시한다.

public JAXBContext getJaxbContext() {
    JAXBContext context = this.jaxbContext;     // volatile 필드
    if (context != null) {
        return context;                          // 빠른 경로: 이미 있음
    }
    this.jaxbContextLock.lock();                 // ReentrantLock
    try {
        context = this.jaxbContext;
        if (context == null) {                   // 락 안에서 재확인
            if (StringUtils.hasLength(this.contextPath)) {
                context = createJaxbContextFromContextPath(this.contextPath);
            }
            else if (!ObjectUtils.isEmpty(this.classesToBeBound)) {
                context = createJaxbContextFromClasses(this.classesToBeBound);
            }
            else if (!ObjectUtils.isEmpty(this.packagesToScan)) {
                context = createJaxbContextFromPackages(this.packagesToScan);
            }
            ...
            this.jaxbContext = context;
        }
        return context;
    }
    finally {
        this.jaxbContextLock.unlock();
    }
}

jaxbContext 필드는 volatile이라 한 스레드가 만든 컨텍스트를 다른 스레드가 안전하게 본다. 세 가지 생성 경로(contextPath/classesToBeBound/packagesToScan)가 있고, packagesToScan을 쓰면 ClassPathJaxb2TypeScanner가 클래스패스를 뒤져 @XmlRootElement, @XmlType, @XmlSeeAlso, @XmlEnum, @XmlRegistry 중 하나라도 달린 클래스를 모아 온다.

JAXBContext는 thread-safe해서 공유 캐시가 정당하다. 하지만 거기서 파생되는 Marshaller/Unmarshaller는 thread-safe가 아니다.

동작 흐름 2 — 매 호출마다 새 Marshaller, 그리고 MTOM

그래서 marshal은 매번 새 JAXB Marshaller를 만든다.

 marshal(graph, result, mimeContainer)
   │
   ├─ createMarshaller()                         ← 캐시된 JAXBContext 에서 생성
   │     └─ initJaxbMarshaller(m)                ← 프로퍼티/리스너/스키마/어댑터 주입
   │
   ├─ mtomEnabled && mimeContainer != null ?
   │     └─ m.setAttachmentMarshaller(           ← MTOM: 바이너리를 첨부로 분리
   │            new Jaxb2AttachmentMarshaller(mimeContainer))
   │
   ├─ StaxUtils.isStaxResult(result) ?
   │     └─ marshalStaxResult(m, graph, result)  ← StAX 는 직접 분기
   │     else
   │     └─ m.marshal(graph, result)             ← 나머지는 JAXB 에 위임
   │
   └─ catch JAXBException → convertJaxbException(ex)  ← Spring 예외로 번역

initJaxbMarshaller는 사용자가 설정한 marshallerProperties(들여쓰기, 인코딩 등), marshallerListener, validationEventHandler, adapters(XmlAdapter), schema를 갓 만든 JAXB 마샬러에 일괄 주입하는 초기화 훅이다. protected라 하위 클래스가 재정의해 커스텀 초기화를 더할 수 있다.

MTOM이 켜져 있고 MimeContainer가 주어지면 Jaxb2AttachmentMarshaller가 붙는다. 이것은 JAXB의 AttachmentMarshaller를 구현한 내부 클래스로, 바이너리 데이터를 만나면 본문에는 cid:UUID@host 형태 참조만 쓰고 실제 바이트는 mimeContainer.addAttachment(...)로 빼돌린다. 역방향의 Jaxb2AttachmentUnmarshallercid: 참조를 받아 컨테이너에서 원본 바이트를 되찾는다.

동작 흐름 3 — unmarshal과 XXE 방어

JAXB는 입력 소스를 그대로 신뢰한다. 그래서 Jaxb2Marshaller는 unmarshal 전에 processSource(source)소스를 안전한 SAXSource로 다시 포장한다.

// unmarshal (발췌)
public Object unmarshal(Source source, MimeContainer mimeContainer) {
    source = processSource(source);   // ← 핵심: XXE 방어 래핑
    Unmarshaller unmarshaller = createUnmarshaller();
    ...
}

processSource의 논리는 다음과 같다.

 processSource(source)
   │
   ├─ StAX 소스 또는 DOMSource 면 ─────▶ 그대로 반환 (별도 파서 없음)
   │
   └─ SAXSource / StreamSource 면
        │  내부 XMLReader 가 없으면 직접 생성:
        │    factory.setFeature(disallow-doctype-decl, !supportDtd)
        │    factory.setFeature(external-general-entities,  processExternalEntities)
        │    factory.setFeature(external-parameter-entities, processExternalEntities)
        │  + 외부 엔티티 off 면 NO_OP_ENTITY_RESOLVER 부착
        ▼
     new SAXSource(xmlReader, inputSource)  ← 안전하게 포장해 반환

기본값(supportDtd=false, processExternalEntities=false)에서는 DTD 선언을 금지하고 외부 엔티티 해석을 끈 XMLReader가 입력을 파싱하게 만든다. 이는 02장의 AbstractMarshaller가 하던 방어와 같은 정신이지만, JAXB는 그 기반 클래스를 상속하지 않으므로 여기서 독립적으로 다시 구현한 것이다. sourceParserFactory는 캐시되며, 보안 플래그를 바꾸면 무효화되어 다음 사용 때 재생성된다.

unmarshal 중 mappedClass가 설정돼 있으면 unmarshaller.unmarshal(source, mappedClass).getValue()부분 언마샬링을 수행한다 — 루트 요소에 @XmlRootElement가 없는 타입도 명시적 클래스 지정으로 풀 수 있다.

핵심 메서드 — supports의 두 갈래

Jaxb2Marshaller는 능력 질의를 Class용과 Type용으로 나눠 구현한다.

  • supports(Class): 기본적으로 @XmlRootElement 애노테이션이 있는지 확인(checkForXmlRootElement=true)하고, 그 클래스가 contextPath의 패키지에 속하거나 classesToBeBound에 등록돼 있는지 본다. setSupportJaxbElementClass(true)JAXBElement 자체도 지원으로 친다.
  • supports(Type): JAXBElement<X> 같은 ParameterizedType을 풀어, 타입 인자 X가 원시 래퍼/표준 클래스(String, BigDecimal, XMLGregorianCalendar 등)/등록된 클래스인지 검사한다. byte[](또는 그 제네릭 배열)도 받는다. Class만으로는 알 수 없는 제네릭 정보를 활용하는 것이 GenericMarshaller를 구현한 이유다.

checkForXmlRootElementfalse로 두면 EclipseLink MOXy처럼 외부 바인딩 파일로 매핑을 정의해 애노테이션이 없는 구현도 지원할 수 있다.

설계 포인트 / 확장점

  • 비싼 컨텍스트는 캐시, 가벼운 마샬러는 매번 새로: thread-safe한 JAXBContext는 이중 검사 락킹으로 공유하고, thread-unsafe한 Marshaller/Unmarshaller는 호출마다 생성한다. 동시성과 성능을 동시에 잡는 표준 패턴이다.
  • 보안 우선: JAXB의 무방비 입력 처리를 processSource로 감싸 XXE를 기본 차단한다. 외부 엔티티가 필요하면 명시적으로 켜야 한다.
  • 확장 훅: createMarshaller/createUnmarshaller(5.2부터 public), initJaxbMarshaller/initJaxbUnmarshaller(protected), convertJaxbException(protected)을 재정의해 동작을 손볼 수 있다.
  • 유연한 컨텍스트 정의: contextPath/명시 클래스/패키지 스캔 세 방식을 제공해, 명시성과 편의성 사이에서 선택하게 한다.

정리

Jaxb2Marshaller는 JAXB를 Spring 추상화로 끌어들이며, JAXBContext를 이중 검사 락킹으로 캐시하고 매 변환마다 새 JAXB 마샬러를 초기화해 동시성을 안전하게 다룬다. processSource로 입력을 안전한 SAXSource로 재포장해 XXE를 기본 차단하고, MTOM·부분 언마샬링·제네릭 타입 질의까지 지원하는 풀옵션 구현이다. 다음 챕터에서는 AbstractMarshaller를 상속하는 또 다른 구현 XStreamMarshaller와, 이 변환기들을 XML 설정으로 등록하는 config 패키지를 본다.