✅ OXM(Object-XML Mapping)
JAXB가 JavaSE와 JavaEE 표준에 포함되어 있지만 JAXB 외에도 아래와 같은 XML과 오브젝트 매핑 기술이 있습니다.
- Castor XML : 설정파일이 필요 없는 인트로스펙션 모드를 지원하기도 하는 매우 간결하고 가벼운 바인딩 프레임워크
- JiBX : 뛰어난 퍼포먼스를 자랑하는 XML 바인딩 기술
- XmlBeans : 아파치 XML 프로젝트의 하나, XML의 정보셋을 효과적으로 제공
- Xstream : 관례를 이용해서 설정이 없는 바인딩을 지원하는 XML 바인딩 기술의 하나
이렇게 XML과 자바 오브젝트를 매핑해서 상호 변화해주는 기술을 OXM이라고 합니다. 기능이 같은 여러 가지 기술이 존재한다면 추상화를 적용할 수 있습니다. 스프링이 제공하는 OXM 추상 계층의 API를 이용해 XML 문서와 오브젝트 사이의 변환을 처리하게 하면 코드 수정 없이 OXM 기술을 자유 롭게 바꿔서 적용할 수 있습니다.
- 스프링에서 제공하는 OXM 추상 계층 API인, Unmarshaller interface
✅ OXM 서비스 추상화 적용
SqlReader에서 스프링의 XML 언마샬러를 사용해서 XML을 읽는 방법을 OXM으로 제한해서 사용성을 극대화하는게 목적입니다. 그래서 OxmSqlReader 구현체를 OxmSqlService에서 독점하는 구조로 만듭니다. 내장된 SqlReader 구현을 외부에서 사용하지 못하도록 제한하고 스스로 최적화된 구조로 만드 것입니다.
- OxmSqlService
package com.jhcode.spring.ch7.user.slqservice; import java.io.IOException; import javax.annotation.PostConstruct; import javax.xml.transform.Source; import javax.xml.transform.stream.StreamSource; import org.springframework.oxm.Unmarshaller; import com.jhcode.spring.ch7.user.dao.UserDao; import com.jhcode.spring.ch7.user.slqservice.exception.SqlRetrievalFailureException; import com.jhcode.spring.ch7.user.slqservice.jaxb.SqlType; import com.jhcode.spring.ch7.user.slqservice.jaxb.Sqlmap; public class OxmSqlService implements SqlService { // final로 변경 불가능하며, 두 개의 클래스는 강하게 결합되어 있다 private final OxmSqlReader oxmSqlReader = new OxmSqlReader(); // 디폴트 오브젝트로 만들어진 프로퍼티, 필요에 따라 setter으로 변경한다 private SqlRegistry sqlRegistry = new HashMapSqlRegistry(); public void setSqlRegistry(SqlRegistry sqlRegistry) { this.sqlRegistry = sqlRegistry; } //== OxmSqlService를 통해서 간접적으로 OxmSqlReader에게 DI한다 ==// public void setUnmarshaller(Unmarshaller unmarshaller) { this.oxmSqlReader.setUnmarshaller(unmarshaller); } public void setSqlmapFile(String sqlmapFile) { this.oxmSqlReader.setSqlmapFile(sqlmapFile); } //== SqlService의 구현 코드 ==// @PostConstruct public void loadSql() { this.oxmSqlReader.read(this.sqlRegistry); } @Override public String getSql(String key) throws SqlRetrievalFailureException { try { return this.sqlRegistry.findSql(key); } catch (Exception e) { throw new SqlRetrievalFailureException(e); } } //== 내부 클래스 ==// private class OxmSqlReader implements SqlReader{ private Unmarshaller unmarshaller; private final static String DEFAULT_SQLMAP_FILE = "sqlmap.xml"; private String sqlmapFile = DEFAULT_SQLMAP_FILE; public void setUnmarshaller(Unmarshaller unmarshaller) { this.unmarshaller = unmarshaller; } public void setSqlmapFile(String sqlmapFile) { this.sqlmapFile = sqlmapFile; } @Override public void read(SqlRegistry sqlRegistry) { try { Source source = new StreamSource( UserDao.class.getResourceAsStream(this.sqlmapFile)); Sqlmap sqlmap = (Sqlmap) this.unmarshaller.unmarshal(source); for(SqlType sql : sqlmap.getSql()) { sqlRegistry.registerSql(sql.getKey(), sql.getValue()); } } catch (IOException e) { throw new IllegalArgumentException(this.sqlmapFile + "을 가져올 수 없습니다."); } } } }
- TestDaoFactory
@Bean public OxmSqlService sqlService() { OxmSqlService oxmSqlService = new OxmSqlService(); oxmSqlService.setUnmarshaller(unmarshaller()); return oxmSqlService; } @Bean public Jaxb2Marshaller unmarshaller() { Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller(); jaxb2Marshaller.setContextPath("com.jhcode.spring.ch7.user.slqservice.jaxb"); return jaxb2Marshaller; }
OXM을 적용했지만 빈 설정은 여전히 단순하게 유지할 수 있습니다. OXM 언마샬러를 사용하는 SqlReader 그리고 SqlRegistry는 하나의 빈을 등록하는 것으로 충분하기 때문입니다. SqlRegistry는 필요에 따라 다른 구현으로 교체할 수도 있습니다.
- SqlService → OxmSqlService
- SqlRegistry → HashMapSqlRegistry
- SqlReader → OxmSqlService.OxmSqlReader
OXM을 적용함으로써 각각의 다른 OXM 기술의 구현체들을 유연하게 적용하여 사용할 수 있게 되었습니다.
✅ 위임을 이용한 BaseSqlService의 재사용
OxmSqlService는 SqlReader을 내부 클래스로 구현해 OXM에 특화된 형태로 재구성했기 때문에 설정은 간결해지고 의도되지 않는 방식으로 확장될 위험이 적어졌습니다. 하지만 SqlService의 핵심 메소드인 loadSql()
, getSql()
코드가 BaseSqlService와 동일하게 중복되고 있습니다. 현재는 코드가 간단하여 아무런 문제가 없지만, 자주 코드가 변경되어야 하고, 확장이 필요할 경우가 생긴다면 지금의 코드는 유지 보수성 측면에서 좋지 않습니다. 따라서 loadSql()
, getSql()
의 구현 로직은 BaseSqlService에 두고 OxmSqlService는 일종의 설정과 기본 구성을 변경해주기 위한 어댑터 같은 개념으로 BasSqlService 앞에 두는 설계가 가능합니다.
위 그림과 같이 OxmSqlService는 OXM 기술에 특화된 SqlReader를 멤버로 내장하고 있고, 그에 필요한 설정을 한 번에 지정할 수 있는 확장 구조만을 가지고 있습니다. 실제 SqlReader와 SqlService를 이용해 SqlService의 기능인 loadSql()과 getSql()은 내부에 BaseSqlService 객체를 만들어 위임하는 방식으로 수행하게 됩니다.
- OxmSqlService
public class OxmSqlService implements SqlService { // SqlService 로직을 위임할 객체 private final BaseSqlService baseSqlService = new BaseSqlService(); //... 생략 //== SqlService의 구현 코드 ==// @PostConstruct public void loadSql() { // 실제 작업을 위임할 대상에게 주입 this.baseSqlService.setSqlReader(this.oxmSqlReader); this.baseSqlService.setSqlRegistry(this.sqlRegistry); // 초기화 작업 위임 this.baseSqlService.loadSql(); } @Override public String getSql(String key) throws SqlRetrievalFailureException { return this.baseSqlService.getSql(key); }
✅ 리소스 추상화
OxmSqlReader나 XmlSqlReader은 SQL 매핑 정보가 담긴 XML 파일 이름을 프로퍼티로 외부에서 지정할 수는 있지만 USerDao 클래스와 같은 클래스패스에 존재하는 파일로 제한되는 문제점이 있습니다. 다양한 위치에 존재하는 리소스에 대해 접근하기 위해 리소스 추상화를 적용해 보겠습니다.
- OxmSqlService
//...생략 public void setSqlmap(Resource sqlmap) { this.oxmSqlReader.setSqlmap(sqlmap); } //== SqlService의 구현 코드 ==// @PostConstruct public void loadSql() { // 실제 작업을 위임할 대상에게 주입 this.baseSqlService.setSqlReader(this.oxmSqlReader); this.baseSqlService.setSqlRegistry(this.sqlRegistry); // 초기화 작업 위임 this.baseSqlService.loadSql(); } @Override public String getSql(String key) throws SqlRetrievalFailureException { return this.baseSqlService.getSql(key); } //== 내부 클래스 ==// private class OxmSqlReader implements SqlReader{ private Unmarshaller unmarshaller; private Resource sqlmap = new ClassPathResource("sqlmap.xml", UserDao.class); public void setUnmarshaller(Unmarshaller unmarshaller) { this.unmarshaller = unmarshaller; } public void setSqlmap(Resource sqlmap) { this.sqlmap = sqlmap; }
setmapFile 프로퍼티를 모두 Resource 타입으로 변경합니다. Resource 타입은 실제 소스가 어떤 것이든 상관없이
getInputStream()
메소드로 스트림을 가져올 수 있습니다.
- TestDaoFactory
@Bean public OxmSqlService sqlService() { OxmSqlService oxmSqlService = new OxmSqlService(); oxmSqlService.setSqlmap(new ClassPathResource ("/vol1/jhcode/ch7/user/dao/sqlmap.xml", UserDao.class)); oxmSqlService.setUnmarshaller(unmarshaller()); return oxmSqlService; } @Bean public Jaxb2Marshaller unmarshaller() { Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller(); jaxb2Marshaller.setContextPath("vol1.jhcode.ch7.user.slqservice.jaxb"); return jaxb2Marshaller; }
📖 토비 스프링 3.1 -p596~616
🚩jhcode33의 toby-spring-study.git으로 이동하기
Uploaded by N2T