XML은 Java가 아니다. 어떻게 파싱하지?
3줄 요약
- Java 코드에서 SQL을 찾으려면 XML 파일을 파싱해야 한다 (iBatis/MyBatis)
- XML 파싱에는 JDOM2 라이브러리를 사용했다 (DOM보다 직관적)
- 폐쇄망 환경에서는 DTD 검증을 비활성화해야 한다 (외부 네트워크 차단)
1. 들어가며: Java가 아닌 걸 어떻게 분석하지?
이전 글에서 재귀와 백트래킹으로 호출 흐름을 추적했다.
UserController.selectUser()
└── UserServiceImpl.selectUser()
└── UserDAO.selectUser()
└── ??? ← 이제 SQL을 연결해야 한다
DAO까지 추적하는 건 됐다. 이제 SQL을 연결해야 한다.
전자정부프레임워크에서 SQL은 Java 코드에 직접 쓰지 않고 XML 파일에 분리되어 있다는 건 알고 있었다.
// UserDAO.java
public UserVO selectUser(String userId) {
return (UserVO) selectOne("userDAO.selectUser", userId);
// ↑ 이 ID로 XML에서 SQL을 찾아야 함
}

문제는 XML은 Java가 아니라는 것이다.
지금까지는 JavaParser로 Java 코드를 분석했다. 하지만 XML은 Java가 아니니까 JavaParser로는 읽을 수 없다.
[지금까지]
Java 코드 → JavaParser로 분석 ✅
[이제 해야 할 것]
XML 파일 → ??? 로 분석 → 매핑 ID로 연결
XML을 파싱하는 다른 방법이 필요하다.
2. SQL이 XML에 분리된 구조 이해하기
2.1 전자정부프레임워크의 구조
전자정부프레임워크(eGovFrame)나 레거시 프로젝트에서는 SQL을 XML 파일에 분리하는 경우가 많다.
(이 글을 쓰면서 생각났는데, DAO에서 직접 쿼리를 작성하는 프로젝트도 있다. 현재 도구는 XML 분리 방식만 지원하는데, Java 코드 내 SQL 추출도 JavaParser의 StringLiteralExpr로 가능할 것 같다. 추후 살펴볼 예정이다. 여기서는 XML 분리 방식을 다룬다.)
다음과 같이 분리한다:
[Java 코드] [XML 파일]
UserDAO.java User_SQL.xml
│ │
└── selectOne("userDAO.selectUser") ──→ <select id="selectUser">
SELECT * FROM TB_USER
WHERE USER_ID = #userId#
</select>
- Java 코드: SQL의 "이름"만 호출 (
"userDAO.selectUser") - XML 파일: 실제 SQL 쿼리가 저장된 곳
2.2 왜 이렇게 분리할까?
| 이유 | 설명 |
|---|---|
| SQL 수정 편의성 | Java 코드 컴파일 없이 XML만 수정 가능 |
| 역할 분리 | Java 개발자와 DBA의 작업 영역 분리 |
| 유지보수 | SQL을 한 곳에서 관리 |
| 동적 SQL | 조건에 따라 SQL 변경 가능 (if, choose 등) |
2.3 우리가 해야 할 일
[목표]
DAO에서 호출하는 "userDAO.selectUser"라는 이름으로
XML 파일에서 실제 SQL 쿼리를 찾아 연결하기!
이걸 하려면 XML 파일을 읽고 분석(파싱)해야 한다.
3. XML이 뭔지 먼저 이해하기
3.1 XML이란?
XML = eXtensible Markup Language
데이터를 저장하고 전달하기 위한 텍스트 형식
HTML을 알고 있다면 비슷하게 생겼다고 느낄 것이다.
<!-- HTML -->
<div class="user">홍길동</div>
<!-- XML -->
<user id="1">홍길동</user>
둘 다 <태그>내용</태그> 형식이다.
3.2 XML 구조 기본
XML은 트리 구조다. (또 트리!)
<?xml version="1.0" encoding="UTF-8"?>
<sqlMap namespace="userDAO">
<select id="selectUser" resultClass="userVO">
SELECT * FROM TB_USER
WHERE USER_ID = #userId#
</select>
<insert id="insertUser">
INSERT INTO TB_USER ...
</insert>
</sqlMap>
이걸 트리로 그리면:
[sqlMap] ← 루트 요소 (namespace="userDAO")
│
├── [select] ← 자식 요소 (id="selectUser")
│ └── "SELECT * FROM TB_USER..." ← 텍스트 내용
│
└── [insert] ← 자식 요소 (id="insertUser")
└── "INSERT INTO TB_USER..." ← 텍스트 내용
3.3 XML 용어 정리
| 용어 | 설명 | 예시 |
|---|---|---|
| 요소(Element) | 태그로 감싼 것 | <select>...</select> |
| 속성(Attribute) | 태그 안의 key="value" | id="selectUser" |
| 텍스트(Text) | 태그 사이의 내용 | SELECT * FROM TB_USER |
| 루트 요소 | 최상위 요소 (하나만 존재) | <sqlMap> |
| 자식 요소 | 다른 요소 안에 있는 요소 | <select>, <insert> |
4. iBatis vs MyBatis XML 구조
4.1 두 가지 SQL 매퍼
전자정부프레임워크에서는 주로 iBatis를 사용하고, 최신 프로젝트에서는 MyBatis를 사용한다.
둘 다 "SQL을 XML에 분리해서 관리"하는 방식은 같지만, 문법이 조금 다르다.
4.2 iBatis XML 구조
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="userDAO">
<select id="selectUser" parameterClass="string" resultClass="userVO">
SELECT
USER_ID as userId,
USER_NAME as userName
FROM TB_USER
WHERE USER_ID = #userId#
</select>
</sqlMap>
iBatis 특징:
- 루트 태그:
<sqlMap> - 파라미터 표기:
#paramName#(샾 두 개로 감싸기) - 결과 타입:
resultClass - 파라미터 타입:
parameterClass
4.3 MyBatis XML 구조
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.dao.UserMapper">
<select id="selectUser" parameterType="String" resultType="UserVO">
SELECT
USER_ID as userId,
USER_NAME as userName
FROM TB_USER
WHERE USER_ID = #{userId}
</select>
</mapper>
MyBatis 특징:
- 루트 태그:
<mapper> - 파라미터 표기:
#{paramName}(샾 중괄호) - 결과 타입:
resultType - 파라미터 타입:
parameterType
4.4 비교 정리
| 항목 | iBatis | MyBatis |
|---|---|---|
| 루트 태그 | <sqlMap> |
<mapper> |
| 파라미터 | #param# |
#{param} |
| 결과 타입 속성 | resultClass |
resultType |
| 상태 | 개발 중단 | 현재 활발히 개발 중 |
우리 도구는 둘 다 지원해야 한다!
레거시 프로젝트에서는 iBatis를 쓰고, 신규 프로젝트에서는 MyBatis를 쓰기 때문이다.
5. XML 파싱 라이브러리 선택: JDOM2
5.1 XML 파싱이란?
파싱(Parsing) = 텍스트를 구조화된 데이터로 변환하는 것
XML 파일을 읽어서, Java에서 사용할 수 있는 객체로 바꾸는 것이다.
[XML 파일] [Java 객체]
<select id="selectUser"> → Element("select")
SELECT * FROM TB_USER ├── attribute: id="selectUser"
</select> └── text: "SELECT * FROM TB_USER"
5.2 Java XML 파싱 방법들
Java에서 XML을 파싱하는 방법은 여러 가지가 있다.
| 방법 | 특징 | 장점 | 단점 |
|---|---|---|---|
| DOM | 전체를 메모리에 로드 | 수정 가능, 직관적 | 메모리 많이 사용 |
| SAX | 이벤트 기반 순차 읽기 | 메모리 효율적 | 코드 복잡, 수정 불가 |
| StAX | 풀 파싱 (SAX 개선) | SAX보다 편리 | 여전히 복잡 |
| JDOM2 | DOM 기반 + Java 친화적 | 매우 간결, 직관적 | 외부 라이브러리 |
| XPath | 경로로 요소 검색 | 복잡한 검색에 유리 | 학습 필요 |
5.3 왜 JDOM2를 선택했나?
1. 코드가 직관적이다
// JDOM2: 깔끔!
Element root = document.getRootElement();
String namespace = root.getAttributeValue("namespace");
List<Element> selects = root.getChildren("select");
// 표준 DOM: 장황...
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(file);
Node root = doc.getDocumentElement();
NamedNodeMap attrs = root.getAttributes();
String namespace = attrs.getNamedItem("namespace").getNodeValue();
JDOM2가 훨씬 읽기 쉽다!
2. Java 컬렉션과 잘 어울린다
Java 컬렉션이란?
List,Set,Map같은 Java 기본 자료구조들을 말한다.
예:List<String>,ArrayList,HashMap등
// JDOM2: List<Element> 반환 → Java 컬렉션!
List<Element> children = root.getChildren("select");
for (Element child : children) {
// for-each 바로 사용 가능
}
// 표준 DOM: NodeList 반환 → Java 컬렉션이 아님
NodeList children = root.getElementsByTagName("select");
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
// 인덱스로 접근해야 하고, 캐스팅도 필요...
}
JDOM2는 결과를 List<Element>로 반환하기 때문에 for-each, Stream API 등 Java에서 익숙한 방식을 그대로 쓸 수 있다.
3. 우리 프로젝트에 적합하다
- SQL 매퍼 XML은 크기가 작다 → 메모리 문제 없음
- 읽기만 하면 됨 → SAX의 효율성 불필요
- 코드 유지보수 중요 → 직관적인 API 우선
5.4 JDOM2 설치
// build.gradle
dependencies {
implementation 'org.jdom:jdom2:2.0.6.1'
}
6. 실제 코드로 XML 파싱하기
6.1 전체 흐름
1. XML 파일 읽기
2. 루트 요소 확인 (sqlMap 또는 mapper)
3. namespace 추출
4. select, insert, update, delete 태그 순회
5. 각 태그에서 id, SQL 쿼리 추출
6. 결과 저장 (namespace.id → SQL 매핑)
6.2 기본 파싱 코드
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.input.SAXBuilder;
public class XmlParsingExample {
public static void main(String[] args) throws Exception {
// 1. SAXBuilder 생성 (XML을 읽는 도구)
SAXBuilder builder = new SAXBuilder();
// 2. XML 파일 읽기
Document document = builder.build(new File("User_SQL.xml"));
// 3. 루트 요소 가져오기
Element root = document.getRootElement();
System.out.println("루트 태그: " + root.getName());
// 출력: 루트 태그: sqlMap
// 4. 속성 가져오기
String namespace = root.getAttributeValue("namespace");
System.out.println("네임스페이스: " + namespace);
// 출력: 네임스페이스: userDAO
// 5. 자식 요소 순회
for (Element child : root.getChildren("select")) {
String id = child.getAttributeValue("id");
String sql = child.getText();
System.out.println("SQL ID: " + id);
System.out.println("쿼리: " + sql);
}
}
}
출력:
루트 태그: sqlMap
네임스페이스: userDAO
SQL ID: selectUser
쿼리: SELECT USER_ID as userId, USER_NAME as userName FROM TB_USER WHERE USER_ID = #userId#
6.3 용어 정리 (코드에서 쓰는 것들)
| JDOM2 용어 | 역할 | 비유 |
|---|---|---|
SAXBuilder |
XML 파일을 읽어서 Document로 변환 | 책을 펼치는 사람 |
Document |
XML 파일 전체를 담은 객체 | 펼쳐진 책 전체 |
Element |
XML의 태그 하나 | 책의 한 챕터 |
getChildren() |
자식 요소들을 가져오기 | 챕터의 하위 섹션들 |
getAttributeValue() |
속성 값 가져오기 | 챕터 제목 읽기 |
getText() |
태그 안의 텍스트 | 본문 내용 읽기 |
6.4 프로젝트 실제 코드: IBatisParser
실제 프로젝트에서는 좀 더 복잡하다.
public class IBatisParser {
// 처리할 SQL 태그 목록
private static final List<String> SQL_TAGS =
List.of("select", "insert", "update", "delete");
public Map<String, SqlInfo> parseFile(Path xmlFile) throws Exception {
Map<String, SqlInfo> sqlMap = new HashMap<>();
// 1. XML 파싱 준비
SAXBuilder builder = new SAXBuilder();
Document document = builder.build(xmlFile.toFile());
Element root = document.getRootElement();
// 2. 루트 요소 확인 (sqlMap 또는 mapper만 처리)
String rootName = root.getName().toLowerCase();
if (!rootName.equals("sqlmap") && !rootName.equals("mapper")) {
return sqlMap; // SQL 매퍼 XML이 아님
}
// 3. namespace 추출
String namespace = root.getAttributeValue("namespace");
String fileName = xmlFile.getFileName().toString();
// 4. SQL 태그 순회 (select, insert, update, delete)
for (String tagName : SQL_TAGS) {
List<Element> elements = root.getChildren(tagName);
for (Element element : elements) {
// 5. 각 SQL 정보 추출
String id = element.getAttributeValue("id");
String query = extractQuery(element);
// 6. SqlInfo 객체 생성
SqlInfo sqlInfo = new SqlInfo(fileName, namespace, id);
sqlInfo.setQuery(query);
// 7. 결과 저장 (namespace.id를 키로)
sqlMap.put(sqlInfo.getFullSqlId(), sqlInfo);
}
}
return sqlMap;
}
}
핵심 포인트:
- iBatis와 MyBatis 둘 다 지원
sqlmap또는mapper루트 태그 모두 처리
- 모든 SQL 타입 처리
- select, insert, update, delete 모두 파싱
- 결과를 Map으로 저장
- 키:
"userDAO.selectUser"(namespace.id) - 값: SqlInfo 객체 (쿼리, 파라미터 등)
- 키:
6.5 동적 SQL 처리 (재귀 필요!)
iBatis/MyBatis에는 동적 SQL이라는 게 있다.
<select id="selectUserList" resultClass="userVO">
SELECT * FROM TB_USER
WHERE USE_YN = 'Y'
<isNotEmpty property="userName">
AND USER_NAME LIKE '%' || #userName# || '%'
</isNotEmpty>
</select>
<isNotEmpty> 같은 중첩 태그 안에도 SQL이 있다.
단순히 getText()만 하면 중첩된 내용을 놓친다!
왜 놓칠까?
getText()는 직접 자식 텍스트만 가져온다. 자식 요소 안에 있는 텍스트는 무시한다.
<select> ← getText()가 가져오는 범위
SELECT * FROM TB_USER ← ✅ 직접 자식 텍스트 (가져옴)
WHERE USE_YN = 'Y' ← ✅ 직접 자식 텍스트 (가져옴)
<isNotEmpty> ← 자식 요소 시작
AND USER_NAME = #name# ← ❌ 손자 텍스트 (무시됨!)
</isNotEmpty> ← 자식 요소 끝
</select>
그림으로 보면:
[select] ─── getText() ──→ "SELECT * FROM ... WHERE USE_YN = 'Y'"
│
└── [isNotEmpty] ─── getText() ──→ "AND USER_NAME = #name#"
│
└── (이 텍스트는 select의 getText()에 포함 안 됨!)
getText()는 한 단계 아래만 보기 때문에, 중첩된 동적 SQL 태그 안의 내용을 놓친다.
// 틀린 방법
String sql = element.getText();
// 결과: "SELECT * FROM TB_USER WHERE USE_YN = 'Y'"
// <isNotEmpty> 안의 "AND USER_NAME = #name#"이 빠졌다!
해결: 재귀적으로 모든 내용 추출
private String extractQuery(Element element) {
StringBuilder query = new StringBuilder();
extractQueryRecursive(element, query);
return query.toString();
}
private void extractQueryRecursive(Element element, StringBuilder query) {
// 모든 "내용물(Content)"을 순회
for (Content content : element.getContent()) {
if (content instanceof Text) {
// 텍스트면 그대로 추가
query.append(((Text) content).getText());
}
else if (content instanceof Element) {
// 자식 요소면 태그 포함해서 추가
Element child = (Element) content;
// 여는 태그: <isNotEmpty property="userName">
query.append("<").append(child.getName());
for (Attribute attr : child.getAttributes()) {
query.append(" ").append(attr.getName())
.append("=\"").append(attr.getValue()).append("\"");
}
query.append(">");
// 자식 요소도 재귀 처리
extractQueryRecursive(child, query);
// 닫는 태그: </isNotEmpty>
query.append("</").append(child.getName()).append(">");
}
}
}
결과:
SELECT * FROM TB_USER
WHERE USE_YN = 'Y'
<isNotEmpty property="userName">
AND USER_NAME LIKE '%' || #userName# || '%'
</isNotEmpty>
동적 SQL 태그까지 포함된 원본 그대로를 추출할 수 있다!
7. 트러블슈팅: DTD 검증 문제
7.1 문제 상황
처음에 코드를 실행했더니 에러가 났다.
java.net.ConnectException: Connection timed out
at org.jdom2.input.SAXBuilder.build(...)
인터넷 연결이 없으니까 에러가 났다!
7.2 원인 분석
XML 파일 맨 위를 보면:
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
이게 DTD(Document Type Definition)다.
DTD란?
"이 XML은 이런 형식을 따라야 한다"를 정의한 문서
마치 "계약서 양식"처럼, XML이 올바른 형식인지 검증하는 데 사용
JDOM2가 XML을 읽을 때 DTD도 함께 로드하려고 한다.
그런데 그 DTD 파일이 인터넷 URL이다: http://www.ibatis.com/dtd/sql-map-2.dtd
→ 폐쇄망(인터넷 연결 없음)에서는 DTD를 가져올 수 없다!
7.3 해결: DTD 검증 비활성화
SAXBuilder builder = new SAXBuilder();
// DTD 검증 비활성화 (3가지 설정)
builder.setFeature(
"http://apache.org/xml/features/nonvalidating/load-external-dtd",
false
);
builder.setFeature(
"http://xml.org/sax/features/external-general-entities",
false
);
builder.setFeature(
"http://xml.org/sax/features/external-parameter-entities",
false
);
// 이제 인터넷 없이도 파싱 가능!
Document document = builder.build(xmlFile);
각 설정의 의미:
| 설정 | 의미 |
|---|---|
load-external-dtd = false |
외부 DTD 파일 로드하지 않음 |
external-general-entities = false |
외부 일반 엔티티 로드하지 않음 |
external-parameter-entities = false |
외부 파라미터 엔티티 로드하지 않음 |
왜 3가지나 설정해야 하나?
XML에는 DTD 외에도 외부 리소스를 참조하는 방법이 여러 가지다.
모든 종류의 외부 참조를 막아야 폐쇄망에서 안전하게 동작한다.
7.4 보안 관점
사실 DTD 비활성화는 보안 측면에서도 좋은 습관이다.
XXE(XML External Entity) 공격 이라는 게 있다:
- 악의적인 XML이 외부 파일을 읽어가는 공격
- 예: 서버의
/etc/passwd파일을 읽어감
DTD/외부 엔티티를 비활성화하면 이런 공격도 방지할 수 있다.
8. 전체 연결: DAO에서 SQL 찾기
8.1 지금까지의 흐름
[56번 글] 호출 흐름 추적
UserController.selectUser()
└── UserServiceImpl.selectUser()
└── UserDAO.selectUser()
└── ??? ← 여기서 멈춤
[이 글] XML 파싱으로 연결
UserDAO.selectUser()
│
├── Java 코드: selectOne("userDAO.selectUser", userId)
│ ↓
└── XML에서 찾기: sqlMap.get("userDAO.selectUser")
↓
SELECT * FROM TB_USER WHERE USER_ID = #userId#
8.2 FlowAnalyzer에서 SQL 연결
// FlowAnalyzer.java (일부)
public FlowResult analyze(Path projectPath, List<ParsedClass> parsedClasses) {
// 1. XML 파일 파싱 → SQL Map 생성
IBatisParser ibatisParser = new IBatisParser();
Map<String, SqlInfo> sqlMap = ibatisParser.parseProject(projectPath);
// 2. 호출 흐름 분석할 때 SQL 연결
for (ParsedClass clazz : parsedClasses) {
if (clazz.getClassType() == ClassType.DAO) {
// DAO 메서드 분석
for (ParsedMethod method : clazz.getMethods()) {
// DAO 메서드명으로 SQL 찾기
String sqlId = clazz.getClassName() + "." + method.getMethodName();
SqlInfo sqlInfo = IBatisParser.findBySqlId(sqlMap, sqlId);
if (sqlInfo != null) {
// SQL 정보 연결!
method.setSqlInfo(sqlInfo);
}
}
}
}
// 3. 트리 생성할 때 SQL도 함께 출력
// ...
}
8.3 최종 결과
[GET] /user/detail.do
└── [Controller] UserController.selectUser()
│
├── [Service] UserServiceImpl.selectUser()
│ └── [DAO] UserDAO.selectUser()
│ └── SQL: SELECT * FROM TB_USER WHERE USER_ID = #userId#
│ └── 테이블: TB_USER
│
└── [Service] UserServiceImpl.selectDeptName()
└── [DAO] DeptDAO.selectDept()
└── SQL: SELECT * FROM TB_DEPT WHERE DEPT_ID = #deptId#
└── 테이블: TB_DEPT
Controller부터 SQL까지 전체 흐름을 추적할 수 있게 됐다!
9. 마치며
정리
- 왜 XML 파싱이 필요한가?
- SQL이 Java 코드에 없고 XML에 있기 때문
- iBatis/MyBatis 프로젝트에서는 SQL 매퍼 XML 파싱 필수
- JDOM2를 선택한 이유
- DOM보다 코드가 직관적
- Java 컬렉션과 잘 어울림
- 우리 용도(작은 XML 읽기)에 적합
- DTD 비활성화가 필요한 이유
- 폐쇄망에서 외부 DTD 로드 실패
- 보안 측면에서도 좋은 습관
- 동적 SQL 처리
- 중첩 태그가 있으면 재귀적으로 추출
이 글을 쓰며 배운 것
기술적으로:
- XML과 HTML의 차이, XML 구조 이해
- JDOM2 vs DOM vs SAX 비교
- DTD와 XXE 보안 문제
개발 습관으로:
- 폐쇄망 환경을 미리 고려해야 함
- 외부 리소스 의존성은 문제가 될 수 있음
다음 글 예고
이제 Controller → Service → DAO → SQL 전체 흐름을 추적할 수 있다.
그런데... 정말 모든 경우에 잘 동작할까?
다음 글에서는 정적 분석의 한계를 다룬다:
- 인터페이스에 구현체가 여러 개면?
- 동적으로 결정되는 것들은?
- 이런 한계를 어떻게 사용자에게 알려줄까?
참고 자료
이 글은 Code Flow Tracer 프로젝트를 만들면서 배운 내용입니다.
'토스 러너스하이 2기 > 기술' 카테고리의 다른 글
| 분석 결과를 Excel로 정리하기 - Apache POI 활용기 (0) | 2026.01.10 |
|---|---|
| 분석 결과를 어떻게 보여줄까? - CLI 출력 구현기 (1) | 2026.01.07 |
| 정적 분석의 한계 - 해결할 수 없는 것들 (0) | 2026.01.04 |
| 호출 흐름을 어떻게 따라가나? - 재귀와 백트래킹 (0) | 2025.12.26 |
| JavaParser로 Java 코드 분석하기 - 처음 배운 사람의 정리 (0) | 2025.12.23 |