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/...
애노테이션 클래스 탐색)
Jaxb2Marshaller는 InitializingBean이라 빈 초기화 시 afterPropertiesSet()이 불린다.
afterPropertiesSet()
│
├─ contextPath / classesToBeBound / packagesToScan
│ ▶ 정확히 하나만 설정됐는지 검증 (아니면 IllegalArgumentException)
│
├─ lazyInit == false (기본) ?
│ ▶ getJaxbContext() ← JAXBContext 미리 생성
│
└─ schemaResources 있으면
▶ loadSchema(...) ← 검증용 스키마 미리 로드
기본적으로 시작 시점에 JAXBContext를 만든다(lazyInit=false). JAXBContext 생성은 비싼 작업이므로, 애플리케이션 기동 때 한 번 치르고 이후엔 재사용하는 것이 합리적이다. lazyInit=true로 첫 사용 시점까지 미룰 수도 있다.
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가 아니다.
그래서 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(...)로 빼돌린다. 역방향의 Jaxb2AttachmentUnmarshaller는 cid: 참조를 받아 컨테이너에서 원본 바이트를 되찾는다.
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가 없는 타입도 명시적 클래스 지정으로 풀 수 있다.
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를 구현한 이유다.
checkForXmlRootElement를 false로 두면 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 패키지를 본다.