CodeGym /행동 /JAVA 25 SELF /XML 직렬화 설정: 커스텀 어댑터

XML 직렬화 설정: 커스텀 어댑터

JAVA 25 SELF
레벨 47, 레슨 4
사용 가능

1. 어댑터 사용 (@XmlJavaTypeAdapter)

JAXB는 자동변속기에 비유할 수 있습니다. 표준적인 것만 다룰 때는 완벽하게 동작하지만, 조금이라도 특이한 것을 만나면 수동 개입이 필요합니다. 예를 들어 클래스에 LocalDate나 BigDecimal 타입의 필드를 추가했다고 합시다. JAXB는 이를 XML로 변환하고 다시 되돌리는 방법을 모릅니다. 또는 날짜가 2024-06-01T00:00:00 같은 긴 문자열이 아니라, 01.06.2024처럼 보이길 원할 수도 있죠. 어떤 객체는 요소(element)보다 속성(attribute)으로 저장하는 편이 더 논리적일 수도 있고, 중첩 객체가 있는 컬렉션은 특별한 표현이 필요할 수도 있습니다.

이런 상황은 어댑터로 해결할 수 있습니다. 어댑터를 통해 JAXB에 복잡한 필드를 어떻게 직렬화/역직렬화할지, 어떤 형식으로 출력할지, 심지어 불필요한 데이터를 건너뛸지까지 지시할 수 있습니다. 즉, 자동이 처리하지 못하는 부분에서 직접 “기어를 조작”해 유연성을 얻는 셈입니다.

어댑터란?

어댑터는 JAXB에 “이 타입을 만나면 이렇게 직렬화하고, 이렇게 역직렬화해라”라고 알려주는 특수 클래스입니다. Java에서 어댑터는 추상 클래스 javax.xml.bind.annotation.adapters.XmlAdapter<ValueType, BoundType>를 구현하며, 여기서:

  • ValueType — XML에서 데이터가 표현될 형태(보통 String, 때로는 Integer, Long 또는 다른 객체).
  • BoundType — Java 클래스의 실제 타입(예: LocalDate).

예시: LocalDate 필드 직렬화

import java.time.LocalDate;
import javax.xml.bind.annotation.*;

@XmlRootElement
public class Person {
    private String name;
    private LocalDate birthDate; // 여기서 문제가 발생합니다!

    public Person() {} // JAXB는 public 무매개변수 생성자를 요구합니다

    public Person(String name, LocalDate birthDate) {
        this.name = name;
        this.birthDate = birthDate;
    }

    @XmlElement
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    @XmlElement
    public LocalDate getBirthDate() { return birthDate; }
    public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; }
}

이런 객체를 직렬화하려 하면 JAXB가 예외를 던집니다:

javax.xml.bind.JAXBException: class java.time.LocalDate nor any of its super class is known to this context.

1단계: 어댑터 생성

import javax.xml.bind.annotation.adapters.XmlAdapter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

// LocalDate <-> String 변환을 위한 어댑터
public class LocalDateAdapter extends XmlAdapter<String, LocalDate> {
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy");

    @Override
    public LocalDate unmarshal(String v) throws Exception {
        return (v == null || v.isEmpty()) ? null : LocalDate.parse(v, FORMATTER);
    }

    @Override
    public String marshal(LocalDate v) throws Exception {
        return (v == null) ? null : v.format(FORMATTER);
    }
}
  • marshal — Java 객체(LocalDate)를 XML용 문자열로 변환합니다.
  • unmarshal — XML의 문자열을 다시 Java 객체로 변환합니다.

2단계: 필드나 게터에 어노테이션 추가

@XmlJavaTypeAdapter(LocalDateAdapter.class)
public LocalDate getBirthDate() { return birthDate; }

또는 필드 자체에 어노테이션을 붙일 수 있습니다:

@XmlJavaTypeAdapter(LocalDateAdapter.class)
private LocalDate birthDate;

3단계: 결과 확인

이제 직렬화 결과는 다음과 같습니다:

<Person>
    <name>Ivan</name>
    <birthDate>01.06.2024</birthDate>
</Person>

역으로 XML을 읽을 때, 문자열 "01.06.2024"는 LocalDate 객체로 변환됩니다.

2. 어댑터를 필드, 게터 또는 전체 클래스에 적용

어댑터는 여러 방식으로 적용할 수 있습니다.

개별 필드나 게터에 적용: 가장 흔한 방식입니다.

@XmlJavaTypeAdapter(LocalDateAdapter.class)
private LocalDate birthDate;

전체 클래스에 적용: 특정 타입을 항상 어댑터로 직렬화하도록 하려면 클래스 자체에 표시할 수 있습니다:

@XmlJavaTypeAdapter(LocalDateAdapter.class)
public class LocalDate { ... }

보통 표준 클래스가 아닌, 자신이 만든 클래스에 대해 이렇게 합니다(표준 LocalDate 클래스는 수정할 수 없습니다).

컬렉션에 적용: 예를 들어 List<LocalDate>를 날짜 목록을 문자열 목록으로 바꾸는 어댑터로 직렬화할 수 있습니다.

3. 요소와 속성 이름 커스터마이징

가끔 XML 구조 요구사항이 엄격할 수 있습니다. 예를 들어 필드 이름이 <birthDate>가 아니라 <birth_date>여야 한다거나, 생년월일을 요소가 아닌 속성으로 두어야 할 수도 있습니다.

요소 이름 변경

@XmlElement(name = "birth_date")
public LocalDate getBirthDate() { return birthDate; }

XML에서는 다음과 같습니다:

<birth_date>01.06.2024</birth_date>

속성으로 직렬화

@XmlAttribute(name = "birth_date")
public LocalDate getBirthDate() { return birthDate; }

XML:

<Person birth_date="01.06.2024">
    <name>Ivan</name>
</Person>

어댑터와 조합

@XmlAttribute(name = "birth_date")
@XmlJavaTypeAdapter(LocalDateAdapter.class)
public LocalDate getBirthDate() { return birthDate; }

4. 실전 케이스

필드 제외 (@XmlTransient)

특정 필드를 XML에 포함하지 않아야 할 때가 있습니다(예: 내부 ID, 비밀번호, 임시 데이터).

@XmlTransient
private String internalCode;

이 필드는 직렬화와 역직렬화에서 모두 무시됩니다.

숫자 포맷팅

금액 필드가 있다고 해봅시다:

private BigDecimal balance;

JAXB는 BigDecimal을 원하는 형식(예: 소수점 이하 두 자리, 콤마 구분자 등)으로 바로 직렬화하지 못합니다. 어댑터를 작성합니다:

import javax.xml.bind.annotation.adapters.XmlAdapter;
import java.math.BigDecimal;

public class BigDecimalAdapter extends XmlAdapter<String, BigDecimal> {
    @Override
    public BigDecimal unmarshal(String v) throws Exception {
        return (v == null || v.isEmpty()) ? null : new BigDecimal(v.replace(",", "."));
    }

    @Override
    public String marshal(BigDecimal v) throws Exception {
        return (v == null) ? null : String.format("%.2f", v);
    }
}

사용 방법:

@XmlJavaTypeAdapter(BigDecimalAdapter.class)
private BigDecimal balance;

중첩 구조

중첩 객체가 있는 경우, 예를 들어:

public class Address {
    private String city;
    private String street;
    // ...
}

JAXB는 기본적으로 중첩 객체를 요소로 직렬화합니다. 그러나 예를 들어 city는 속성으로, street은 요소로 표현하고 싶다면 어노테이션을 사용합니다:

public class Address {
    @XmlAttribute
    private String city;
    @XmlElement
    private String street;
}

5. 예제: 어댑터를 사용한 전체 직렬화 설정

예제를 확장해봅시다. 이제 Person 클래스에 생년월일과 잔액이 있습니다.

import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.math.BigDecimal;
import java.time.LocalDate;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Person {
    @XmlElement
    private String name;

    @XmlAttribute(name = "birth_date")
    @XmlJavaTypeAdapter(LocalDateAdapter.class)
    private LocalDate birthDate;

    @XmlElement
    @XmlJavaTypeAdapter(BigDecimalAdapter.class)
    private BigDecimal balance;

    @XmlTransient
    private String password;

    public Person() {}

    public Person(String name, LocalDate birthDate, BigDecimal balance, String password) {
        this.name = name;
        this.birthDate = birthDate;
        this.balance = balance;
        this.password = password;
    }

    // getter 및 setter...
}

얻을 수 있는 결과:

  • 이름은 <name> 요소로 직렬화됩니다.
  • 생년월일은 <Person birth_date="01.06.2024">처럼 속성으로 직렬화됩니다.
  • 잔액은 <balance>1234.56</balance> 요소로 직렬화됩니다.
  • 비밀번호는 XML에 전혀 포함되지 않습니다.

XML 파일:

<Person birth_date="01.06.2024">
    <name>Ivan</name>
    <balance>1234.56</balance>
</Person>

6. 컬렉션과 중첩 객체 처리

JAXB는 적절히 어노테이션이 달린 컬렉션을 처리할 수 있습니다. 예를 들어 사람에게 주소 목록이 있다면:

@XmlElementWrapper(name = "addresses")
@XmlElement(name = "address")
private List<Address> addresses;

XML에서는 다음과 같습니다:

<addresses>
    <address city="Berlin">
        <street>Aleksandrplats, 1</street>
    </address>
    <address city="Limassol">
        <street>Aneksartisias, 10</street>
    </address>
</addresses>

컬렉션의 타입이 비표준인 경우(예: List<LocalDate>) 컬렉션의 요소에 어댑터를 적용할 수 있습니다:

@XmlElementWrapper(name = "dates")
@XmlElement(name = "date")
@XmlJavaTypeAdapter(LocalDateAdapter.class)
private List<LocalDate> importantDates;

7. 예제: 어댑터를 사용한 직렬화와 역직렬화

직렬화

Person person = new Person(
    "Ivan",
    LocalDate.of(1990, 6, 1),
    new BigDecimal("1234.56"),
    "secretPassword"
);

JAXBContext context = JAXBContext.newInstance(Person.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);

marshaller.marshal(person, System.out); // XML을 콘솔에 출력합니다

역직렬화

String xml = """
    <Person birth_date="01.06.1990">
        <name>Ivan</name>
        <balance>1234.56</balance>
    </Person>
    """;

JAXBContext context = JAXBContext.newInstance(Person.class);
Unmarshaller unmarshaller = context.createUnmarshaller();

Person person = (Person) unmarshaller.unmarshal(new StringReader(xml));
System.out.println(person.getName() + " " + person.getBirthDate() + " " + person.getBalance());

8. 직렬화와 어댑터 설정 시 흔한 오류

오류 1: public 무매개변수 생성자가 없음. 이 생성자가 없으면 JAXB는 역직렬화 시 객체를 만들 수 없어 예외를 던집니다.

오류 2: 어댑터를 잘못 적용함. @XmlJavaTypeAdapter를 잘못된 필드에 두거나 게터/세터를 빠뜨리면 JAXB는 해당 타입을 직렬화하는 방법을 알 수 없습니다.

오류 3: 역직렬화 시 형식 불일치. XML의 날짜 형식이 어댑터가 지원하는 형식과 다르면(예: "01.06.2024" 대신 "2024-06-01") unmarshal 메서드가 예외를 던집니다.

오류 4: 어댑터 없이 JAXB가 지원하지 않는 타입을 직렬화하려는 시도. 대표적인 예 — LocalDate, BigDecimal, Map, 사용자 정의 복합 타입 등.

오류 5: 어노테이션 없이 중첩 컬렉션을 처리하려는 경우. @XmlElementWrapper가 없으면 컬렉션이 기대와 다르게 직렬화되거나 JAXB가 XML을 제대로 읽지 못할 수 있습니다.

오류 6: 어댑터를 컬렉션 자체에 적용하는 실수. 리스트의 각 요소를 어댑터로 직렬화하려면 컬렉션이 아니라 요소에 어노테이션을 붙여야 합니다(예: 요소 필드 위의 @XmlJavaTypeAdapter 또는 위 예시처럼 리스트 필드에 요소 타입을 지정해 적용).

1
과제
JAVA 25 SELF, 레벨 47, 레슨 4
잠금
이벤트 날짜를 속성으로 기록하기 📅
이벤트 날짜를 속성으로 기록하기 📅
1
과제
JAVA 25 SELF, 레벨 47, 레슨 4
잠금
캘린더에서 공휴일 목록 관리 🎉
캘린더에서 공휴일 목록 관리 🎉
1
설문조사/퀴즈
XML 직렬화, 레벨 47, 레슨 4
사용 불가능
XML 직렬화
XML 직렬화
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION