토비의 스프링 정리
토비의 스프링 - 7.3 서비스 추상화 적용
ksb-dev
2022. 10. 25. 23:18
7.3.1 JaxbXmlSqlReader 개선 과제
- JaxbXmlSqlReader를 개선할 두 가지 과제를 생각할 수 있음
- JAXB 이외의 XML과 자바 오브젝트 매핑 기술
- XML 파일을 다양한 소스에서 가져오게 함
- 💡 클래스 패스, 파일 시스템, 웹 등 다양한 위치에서 가져오게 함
7.3.2 OXM 서비스 추상화
- OXM(Object-XML Mapping)은 XML 과 자바 오브젝트를 매핑해서 상호 변환하는 기술임
- 자바에는 JAXB 외에 자주 사용되는 XML과 자바 오브젝트 매핑 기술이 존재함
- Castor XML
- 설정파일이 필요없는 인트로스펙션 모드를 지원하기도 하는 간결하고 가벼운 바인딩 프레임워크
- JiBX
- 뛰어난 퍼포먼스를 자랑하는 XML 바인딩 기술
- XmlBeans
- 아파치 XML 프로젝트의 하나. XML의 정보셋을 효과적으로 제공
- Xstream
- 관례를 이용해 설정이 없는 바인딩을 지원하는 XML 바인딩 기술의 하나
- Castor 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>