Language/JAVA

[JAVA] XML 및 JSON 파싱 (SAX parser, DOM parser, JSON )

gangintheremark 2024. 1. 25. 13:55
728x90

XML 은 태그 등을 이용하여 문서나 데이터를 구조화하는 언어이다.

xml 태그는 자유롭게 생성하기 때문에 최초 작성자의 의도대로 작성되는지 확인할 필요가 있으며 DTD 또는 Schema를 이용해서 문서의 규칙을 작성한다. 이러한 DTD, Schema를 잘 따른 문서를 "valid 하다" 라고한다.

 

파싱이란 문서에서 필요한 정보를 얻기 위해 태그를 구별하고 내용을 추출하는 과정이다.

 

문서의 파싱에는 대표적으로 3가지 방식이 있다

 

SAX(Simple API for XML) parser

  • 문서를 읽으면서 태그의 시작, 종료 등 이벤트 기반으로 처리하는 방식
  • 빠르고 한 번에 처리하기 때문에 다양한 탐색이 어렵다

DOM(Document Object Model) parser

  • 문서를 전부 로딩한 후 문서 구조 전체를 자료구조에 저장하여 탐색하는 방식
  • 다양한 탐색이 가능하지만 느리고 무거우며 큰 문서를 처리하기 힘들다

JSON

  • 객체를 key-value 의 쌍으로 관리한다.

 

SAX parser

SAX Parser FactorySAX parser 을 생성하고 이벤트를 체크하는 메서드를 가진 DefaultHandler 를 등록한 후, 문서를 읽다가 이벤트가 발생하면 메서드를 호출하여 문서를 처리한다.

 

다음과 같은 메서드를 XML 태그에 따라 재정의하여 사용한다.qName 은 태그명이다.

  • startElement(String uri, String localName, String qName, Attributes attributes): 태그 시작
  • endElement(String uri, String localName, String qName): 태그 끝
  • characters(char[] ch, int start, int length): 문자열 시작

 

💻[실습] 일별 박스오피스 XML 파일이 있을 때, SAX parser 를 이용하여 파싱해보기

① 등수, 영화제목, 개봉일, 누적 관객 수를 저장할 BoxOffice DTO를 생성하여 데이터를 저장한다

 

② Handler 작성

SAXParser 을 구성하고 파라미터로 받은 xml 파일을 파싱한다.

public class BoxOfficeSaxParser extends DefaultHandler implements BoxOfficeParser {

    private List<BoxOffice> list = new ArrayList<>(); // 파싱된 내용을 저장할 List
    private BoxOffice current; // 현재 파싱하고 있는 대상 객체
    private String content;  // 방금 읽은 텍스트 내용

    // singleton 구조
    private static BoxOfficeSaxParser parser = new BoxOfficeSaxParser();

    public static BoxOfficeSaxParser getParser() {
        return parser;
    }

    @Override
    public List<BoxOffice> getBoxOffice(InputStream resource) {
        // SAXParser를 구성하고 boxoffice.xml을 파싱
        try {
            SAXParserFactory factory = SAXParserFactory.newInstance();
            SAXParser parser = factory.newSAXParser();
            parser.parse(resource, this);
        } catch (IOException | ParserConfigurationException | SAXException e) {
            e.printStackTrace();
        }
        return list;
    }

... 

}

DefaultHanlder 를 상속받아 메서드를 태그에 따라 재정의해준다.

    ...

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        if (qName.equals("dailyBoxOffice")) {
            current = new BoxOffice();
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if (qName.equals("rank")) {
            current.setRank(Integer.parseInt(content));
        } else if (qName.equals("movieNm")) {
            current.setMovieNm(content);
        } else if (qName.equals("openDt")) {
            current.setOpenDt(current.toDate(content));
        } else if (qName.equals("audiAcc")) {
            current.setAudiAcc(Integer.parseInt(content));
        } else if (qName.equals("dailyBoxOffice")) {
            list.add(this.current);
        }
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        this.content = String.valueOf(ch, start, length);
    }

③ XML 파싱 및 결과확인

public class BoxOfficeCLI {
    private BoxOfficeParser parser = null;
    private InputStream resource = null;

    private void readBoxOfficeList() {
        // resource와 parser를 구성해서 정보 가져오기
        this.resource = BoxOfficeCLI.class.getResourceAsStream("../res/boxoffice.xml");
        this.parser = BoxOfficeSaxParser.getParser();

        List<BoxOffice> list = parser.getBoxOffice(resource);

        for (BoxOffice boxOffice : list) {
            System.out.println(boxOffice);
        }
    }
}


DOM parser

문서를 완전히 메모리에 로딩 후 필요한 내용을 찾는다. DocumentBuilderFacotryDocumentBuilder 를 생성한 후 DOM Tree 를 구성한다.

 

DOM Tree

  • 문서를 구성하는 모든 요소를 Node(태그, 속성, 값) 으로 구성
  • 태그들은 root 노드를 시작으로 부모-자식의 관계 구성

getElementsByTagName 을 통해 최상위 element을 추출한 후 반복문을 통해 하위 element 들을 끄집어낸다.
② 끄집어낸 하위 노드들에서 반복문을 돌며 getNodeName() 을 통해 태그명을 찾고 getTextContent() 를 통해 원하는 데이터를 추출한다.

public class BoxOfficeDomParser implements BoxOfficeParser {

    private List<BoxOffice> list = new ArrayList<>();
    private static BoxOfficeDomParser parser = new BoxOfficeDomParser();

    private BoxOfficeDomParser() { }

    public static BoxOfficeDomParser getParser() {
        return parser;
    }

    @Override
    public List<BoxOffice> getBoxOffice(InputStream resource) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();

            Document doc = builder.parse(resource); // 문서 로딩
            Element root = doc.getDocumentElement(); // 최상위 element
            parse(root);
        } catch (IOException | ParserConfigurationException | SAXException e) {
            e.printStackTrace();
        }
        return list;
    }

    private void parse(Element root) {
        // root에서 dailyBoxOffice를 추출한 후 BoxOffice를 생성해 list에 저장
        NodeList boxOffices = root.getElementsByTagName("dailyBoxOffice");
        for (int i = 0; i < boxOffices.getLength(); i++) {
            Node child = boxOffices.item(i);
            list.add(getBoxOffice(child));
        }
    }

    private static BoxOffice getBoxOffice(Node node) {
        BoxOffice boxOffice = new BoxOffice();
        // node 정보를 이용해서 BoxOffice를 구성하고 반환
        NodeList subNodes = node.getChildNodes();
        for (int i = 0; i < subNodes.getLength(); i++) {
            Node sub = subNodes.item(i);
            if (sub.getNodeName().equals("rank")) {
                boxOffice.setRank(Integer.parseInt(sub.getTextContent()));
            } else if(sub.getNodeName().equals("movieNm")) {
                boxOffice.setMovieNm(sub.getTextContent());
            } else if(sub.getNodeName().equals("openDt")) {
                boxOffice.setOpenDt(boxOffice.toDate(sub.getTextContent()));
            } else if(sub.getNodeName().equals("audiAcc")) {
                boxOffice.setAudiAcc(Integer.parseInt(sub.getTextContent()));
            }
        }
        return boxOffice;
    }
}

 

③ XML 파싱 및 결과확인

 private void readBoxOfficeList() {
        this.resource = BoxOfficeCLI.class.getResourceAsStream("../res/boxoffice.json"); 
        this.parser = BoxOfficeDomParser.getParser();

        List<BoxOffice> list = parser.getBoxOffice(resource);
        System.out.println("list size: " + list.size());
        for (BoxOffice boxOffice : list) {
            System.out.println(boxOffice);
        }
    }

JSON

간결한 문법, 단순한 텍스트, 적은 용량으로 대부분의 언어 및 플랫폼에서 사용 가능하다

 

JSON 파일을 자바로 생각해보면 Map<String, Map<String, Object>> 구조로 되어있다. 내가 필요한 데이터는 dailyBoxOfficeList 로 List<Map<String,Object>> 구조이다.

 

ObjectMapper 를 이용하여 문서를 읽고 원하는 데이터를 get()을 이용하여 끄집어낸다.

② 끄집어 낸 데이터를 convertValue() 를 통해 BoxOffice 객체에 저장한다.

public class BoxOfficeJsonParser implements BoxOfficeParser {

    private List<BoxOffice> list = new ArrayList<>();
    private static BoxOfficeJsonParser parser = new BoxOfficeJsonParser();

    public static BoxOfficeJsonParser getParser() {
        return parser;
    }

    @Override
    public List<BoxOffice> getBoxOffice(InputStream resource) {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd")); // 날짜 변경

        try {
            Map<String, Map<String, Object>> result = mapper.readValue(resource, Map.class);
            List<Map<String, Object>> list = (List) result.get("boxOfficeResult").get("dailyBoxOfficeList");

            for (Map<String, Object> info : list) {
                BoxOffice office = mapper.convertValue(info, BoxOffice.class);
                this.list.add(office);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return list;
    }
}

 

③ 문서에서 BoxOffice DTO 의 멤버변수와 일치하는 데이터만을 가져오기 위해 BoxOffice 클래스에 @JsonIgnoreProperties 어노테이션을 추가한다.

@JsonIgnoreProperties(ignoreUnknown = true)
public class BoxOffice {
    private Integer rank; // 등수
    private String movieNm; // 영화제목
    private Date openDt; // 개봉일
    private Integer audiAcc;// 누적 관객 수

    ...
}

 

④ XML파싱 및 결과 실행

    private void readBoxOfficeList() {
        this.resource = BoxOfficeCLI.class.getResourceAsStream("../res/boxoffice.json"); 
        this.parser = BoxOfficeJsonParser.getParser();

        List<BoxOffice> list = parser.getBoxOffice(resource);
        System.out.println("list size: " + list.size());
        for (BoxOffice boxOffice : list) {
            System.out.println(boxOffice);
        }
    }
728x90