본문 바로가기

토비의 스프링 정리

토비의 스프링 - 7.3 서비스 추상화 적용

7.3.1 JaxbXmlSqlReader 개선 과제

  • JaxbXmlSqlReader를 개선할 두 가지 과제를 생각할 수 있음
    1. JAXB 이외의 XML과 자바 오브젝트 매핑 기술
    2. XML 파일을 다양한 소스에서 가져오게 함
    3. 💡 클래스 패스, 파일 시스템, 웹 등 다양한 위치에서 가져오게 함

7.3.2 OXM 서비스 추상화

  • OXM(Object-XML Mapping)은 XML 과 자바 오브젝트를 매핑해서 상호 변환하는 기술임
  • 자바에는 JAXB 외에 자주 사용되는 XML과 자바 오브젝트 매핑 기술이 존재함
    1. Castor XML
      • 설정파일이 필요없는 인트로스펙션 모드를 지원하기도 하는 간결하고 가벼운 바인딩 프레임워크
    2. JiBX
      • 뛰어난 퍼포먼스를 자랑하는 XML 바인딩 기술
    3. XmlBeans
      • 아파치 XML 프로젝트의 하나. XML의 정보셋을 효과적으로 제공
    4. Xstream
      • 관례를 이용해 설정이 없는 바인딩을 지원하는 XML 바인딩 기술의 하나
  • JAXB를 포함해서 다섯 가지 기술 모두 유사한 기능API를 제공함
  • 유사한 기능과 API를 제공하기 때문에, 이를 추상화 하는 서비스 추상화할 수 있음
  • 스프링에서 트랜잭션, 메일 전송뿐 아니라 OXM에 대해 서비스 추상화 기능을 제공함

7.3.3 OXM 서비스 인터페이스

  • OXM 추상화 서비스 인터페이스는 자바 오브젝트를 XML로 변환하는 Marshaller와, XML을 자바 오브젝트로 변환하는 Unmarsharller가 있음
  • XML에서 데이터를 읽어오는 서비스 추상화 기능을 사용할 것이기 때문에 Unmarsharller을 사용할 것임
  • OXM 기술에 따라 Unmarsharller 인터페이스를 구현한 다섯 가지 클래스가 있음

7.3.4 JAXB 구현 테스트

  • JAXB를 이용하도록 만들어진 Unmarsharller 구현 클래스는 Jaxb2Marsharller임
  • 이름이 Jaxb2Marsharller인 이유는, Marsharller와 Unmarsharller를 모두 구현하고 있기 때문임
  • Jaxb2Marsharller는 프로퍼티인 contextPath만 넣으면 됨
  • 테스트를 위해 OxmTest-context.xml 파일을 만들고 빈을 설정함
  • 테스트 코드는 OXM 추상화 API를 사용했으므로 XML을 읽어서 오브젝트로 변환하는 두 줄이면 충분함
  • 변수를 제거하면 한 줄로 만들 수 있음
  • OXM 서비스 추상화 API를 사용했기 때문에, 테스트 코드에 JAXB라는 구체적인 기술에 의존하는 부분이 없음
implementation group: 'org.springframework', name: 'spring-oxm', version: '3.0.4.RELEASE'
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="unmarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
        <!--프로젝트 내 이전에 만든 jaxb 패키지 경로-->
        <property name="contextPath" value="com.ksb.spring.jaxb" />
    </bean>
</beans>
//import 주의!
import org.springframework.oxm.Unmarshaller;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/OxmTest-context.xml")
public class OxmTest {
    //unmarshaller 타입의 빈을 찾아서 넣어줌
    @Autowired
    Unmarshaller unmarshaller;

    @Test
    public void unmarshallSqlMap() throws XmlMappingException, IOException {
        Source xmlSource = new StreamSource(
                getClass().getResourceAsStream("sqlmap.xml"));

        Sqlmap sqlmap = (Sqlmap) this.unmarshaller.unmarshal(xmlSource);

        List<SqlType> sqlList = sqlmap.getSql();

        assertThat(sqlList.size(), is(6));
        ...
    }
}

7.3.5 Castor 구현 테스트

  • Castor용 매핑 정보만 준비하고 unmashaller 빈 설정만 변경하면 JAXB에서 쉽게 변경 가능
  • Castor는 여러 XML오브젝트 변환 방법을 지원하는데, XML 매핑 파일을 이용할 것임
  • 매핑정보만 적절히 만들어 주면, 어떤 클래스와 필드로도 매핑이 가능하기 때문에 JAXB 컴파일러가 만든 Sqlmap, SqlType 클래스를 Castor 매핑용으로 사용할 수 있음
  • Castor용 매핑 XML은 mapping.xml

💡 현재 이 코드는 동작하지 않음. 스프링 4.3.13 부터 CastorMarshaller는 Deprecated 되었기 때문인 듯 싶음.

<!--mapping.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapping PUBLIC "-//EXOLAB/Castor Object Mapping DTD Version 1.0//EN"
        "http://castor.org/mapping.dtd">
<mapping>
    <class name="com.ksb.spring.jaxb.Sqlmap">
        <map-to xml="sqlmap"/>
        <field name="sql" type="com.ksb.spring.jaxb.SqlType"
               required="true" collection="arraylist">
            <bind-xml name="sql" node="element"/>
        </field>
    </class>
    <class name="com.ksb.spring.jaxb.SqlType">
        <map-to xml="sql"/>
        <field name="key" type="string" required="true">
            <bind-xml name="key" node="attribute"/>
        </field>
        <field name="value" type="string" required="true">
            <bind-xml node="text"/>
        </field>
    </class>
</mapping>
<bean id="unmarshaller" class="org.springframework.oxm.castor.CastorMarshaller">
    <property name="mappingLocation" value="/mapping.xml" />
</bean>

7.3.6 멤버 클래스를 찾조하는 통합 클래스

  • OXM 추상화 기능을 이용하는 SqlService의 이름을 OxmSqlService로 명명함
  • SqlReader는 스프링의 OXM Unmarshaller를 이용하도록 OxmSqlService에 고정해야함
  • 즉, SqlReader를 OxmlSqlService 내부에 구현하여 스태틱 멤버 클래스로 만듦
  • 이렇게 하는 이유는, SQL을 읽는 방법을 OXM으로 제한하여 사용성을 극대화 하는것이 목적

  • OxmSqlService가 내부에 OxmSqlReader를 포함하면서, 멤버로 갖고 있기 때문에 빈의 등록은 OxmlSqlService만 하면 됨
  • 이렇게 빈의 개수를 줄일 수 있음
  • 하지만, OxmSqlReader의 경우 외부에 노출되지 않지만, 스스로 빈으로 등록될 수 없음
  • 때문에 빈의 프로퍼티로 제공받는 값이 있을 경우 OxmlSqlService에서 받아 OxmlSqlReader로 제공해야 함
  • 즉, OxmlSqlReader는 OxmlSqlService의 공개된 프로퍼티를 통해 간접적 DI를 받아야 함

 

  • OxmlSqlReader는 OXM을 사용하므로 Unmarshaller가 필요하고, 매핑 파일 이름도 외부에서 지정해야 하기 때문에 두 가지 정보가 필요함
  • 이 두 가지 정보를 OxmlSqlService의 프로퍼티로 받고 OxmlSqlReader에게 전달해야 함

7.3.7 OxmSqlService

  • OxmSqlService는 내부에 OxmlSqlReader를 구현하고 생성해서 멤버로 가짐
  • 또한, 두 가지 정보를 DI 받아 OxmSqlReader에게 전달함
  • SqlRegistry의 경우 이전에 만든 HashMapSqlRegistry를 디폴트로 사용함
import org.springframework.oxm.Unmarshaller;
...
public class OxmSqlService implements SqlService{
    private final OxmSqlReader oxmSqlReader = new OxmSqlReader();

    private SqlRegistry sqlRegistry = new HashMapSqlRegistry();

    public void setSqlRegistry(SqlRegistry sqlRegistry){
        this.sqlRegistry = sqlRegistry;
    }

    //DI 받은 것을 oxmSqlReader로 전달
    public void setUnmarshaller(Unmarshaller unmarshaller){
        oxmSqlReader.setUnmarshaller(unmarshaller);
    }

    public void setSqlmapFile(String sqlmapFile){
        oxmSqlReader.setSqlmap(sqlmap);
    }

    @PostConstruct
    public void loadSql() {
        this.oxmSqlReader.read(this.sqlRegistry);
    }

    @Override
    public String getSql(String key) throws SqlRetrievalFailException {
        try{
            return this.sqlRegistry.findSql(key);
        }catch (SqlRetrievalFailException e){
            throw new RuntimeException(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(
                        getClass().getResourceAsStream(this.sqlmapFile)
                );
                Sqlmap sqlmap = (Sqlmap)this.unmarshaller.unmarshal(source);

                for(SqlType sql : sqlmap.getSql())
                    sqlRegistry.registrySql(sql.getKey(), sql.getValue());
            }catch (IOException e){
                throw new IllegalArgumentException(this.sqlmapFile +
                        "을 가져올 수 없습니다."+e);
            }
        }
    }

}
<beans>
    ...
    <bean id="sqlService" class="com.ksb.spring.OxmSqlService">
        <property name="unmarshaller" ref="unmarshaller"/>
    </bean>

    <bean id="unmarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
        <property name="contextPath" value="com.ksb.spring.jaxb"/>
    </bean>
</beans>

7.3.8 위임을 이용한 BaseSqlService의 재사용

  • OxmSqlService의 loadSql()과 getSql()이 BaseSqlService와 동일함
  • 재사용한다고 BaseSqlService을 상속 받으면 OxmSqlService를 생성하는 코드를 넣기 애매함
  • loadSql()과 getSql()을 BaseSqlService에 두고, OxmSqlService에서 BaseSqlService로 위임함
  • 위임의 경우 프록시를 만들 때 사용해 봤음
  • 프록시의 경우 애플리케이션 전반적으로 적용해야 하기 때문에 각각 빈으로 등록을 했음
  • 하지만, BaseSqlService와 OxmSqlService의 경우 한 번만 사용할 것이기 때문에 두 오브젝트를 빈으로 등록하기에 불편함
  • 따라서 OxmSqlService내부에서 BaseSqlService을 구현하고, loadSql()과 getSql()을 위임함

public class OxmSqlService implements SqlService{
    private final BaseSqlService baseSqlService = new BaseSqlService();
    ...

    @PostConstruct
    public void loadSql() {
        this.baseSqlService.setSqlReader(this.oxmSqlReader);
        this.baseSqlService.setSqlRegistry(this.sqlRegistry);

        this.baseSqlService.loadSql();
    }

    @Override
    public String getSql(String key) throws SqlRetrievalFailException {
        return this.baseSqlService.getSql(key);
    }
    ...
}

7.3.9 리소스

  • 현재 SQL 정보는 프로젝트 내부 리소스(Resource)인 xml 파일에서만 가져올 수 있음
private final static String DEFAULT_SQLMAP_FILE = "/sqlmap.xml";
  • 때문에, 웹과 같은 프로젝트 외부의 리소스를 불러 오려면 코드를 수정해야 함
  • 자바에서 다양한 위치에 존재하는 리소스에 대한 단일화된 접근 인터페이스를 제공하지 않음
  • ClassLoader 클래스의 경우 ClassLoader의 getResourceAsStream()을 사용해야 하고, URL 클래스의 경우 URL의 getResourceAsStream()를 사용해야 하고, ServletContext 클래스의 경우 ServletContext 클래스의 getResourceAsStream()을 사용해야 함
  • 이렇게 다른 클래스이더라도 같은 목적을 사용하기 때문에 서비스 추상화를 사용하여 프로젝트에 맞는 클래스를 DI 하는 방법을 고려할 수 있음
  • 하지만, Resource의 경우 스프링에서 이아닌 으로 취급되어 오브젝트 DI가 불가능 함
  • 즉, 빈 property의 value에 값이 객체일 경우 자동으로 오브젝트로 변환 하는데, Resource의 경우에는 객체가 아닌 스트링 값으로 취급함

7.3.10 리소스 로더

  • 스프링에서 Resource의 문제를 해결하기 위해 접두어를 이용한 Resource 오브젝트를 선언하는 방법을 제공함
  • 빈 property의 value에 접두어를 붙이면 스프링에서 Resource 오브젝트로 인식하게 할 수 있음

접두어 예 설명

file: file:/C:/temp/file.txt 파일 시스템에서 파일을 지정함
classpath: classpath:file.txt 클래스패스에 루트에 존재하는 파일을 지정함
없음 WEB-INF/test.dat 접두어가 없는 경우 ResourceLoader 구현에 따라 리소스 위치가 결정됨
http: http://www.ksb.com/test.dat http 프로토콜을 사용해 웹 상의 리소스를 지정함. ftp:도 사용가능

7.3.11 Resource를 이용해 XML 파일 가져오기

  • sqlmapFile의 프로퍼티를 Resource 타입으로 변경해야 함
  • 꼭 파일을 읽어오는 것이 아닐 수 있기 때문에 sqlmap으로 이름 변경
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;

public class OxmSqlService implements SqlService{
    ...
    public void setSqlmapFile(Resource sqlmap){
        oxmSqlReader.setSqlmap(sqlmap);
    }

    private class OxmSqlReader implements SqlReader{
        private Resource sqlmap = new ClassPathResource("/sqlmap.xml",
                UserDao.class);
        ...

        @Override
        public void read(SqlRegistry sqlRegistry) {
            try{
                Source source = new StreamSource(sqlmap.getInputStream());
                Sqlmap sqlmap = (Sqlmap)this.unmarshaller.unmarshal(source);

                for(SqlType sql : sqlmap.getSql())
                    sqlRegistry.registrySql(sql.getKey(), sql.getValue());
            }catch (IOException e){
                throw new IllegalArgumentException(this.sqlmap.getFilename() +
                        "을 가져올 수 없습니다."+e);
            }
        }
    }

}
<bean id="sqlService" class="com.ksb.spring.OxmSqlService">
    <property name="unmarshaller" ref="unmarshaller"/>
    <!--classpath는 디폴트이므로 생략 가능-->
    <property name="sqlmap" value="classpath:/sqlmap.xml"/>
</bean>