개요#

7.1. SQL과 DAO의 분리#

  • DAO는 지속적인 추상화를 통해 순수한 데이터 엑세스 코드만 남았다.
  • 하지만 SQL은 DB에 따라 달라질수 밖에 없는 상황이다. 이는 DAO코드를 변경하게 하는 유일한 이유가 된다.

7.1.1. XML설정을 이용한 분리#

개별 SQL 프로퍼티 방식#

  • SQL을 외부에서 주입받는다.
public class UserDaoJdbc implements UserDao {
	private String sqlAdd;
	
	public vo id setSqlAdd(String sqlAdd) {
		this.sqlAdd = sqlAdd;
	}

	public void add(User user) {
		this.jdbcTemplate.update(
			this.sqlAdd,	// "insert into user.." 를 제거하고 외부에서 주입받은 SQL을 사용한다.  
			user.getId(), user.getName(), user.getPassword(), user.getEmail(), 
			user.getLevel().intValue(), user.getLogin(), user.getRecommend());
	}
...
}
<bean id="userDao" class="">
	<property name="dataSource" ref="dataSource" />
	<property name="sqlAdd" value="insert into users(id, name, password, email, level, login, recommend) values(?,?,?,?,?,?,?)" />
</bean>

SQL 맵 프로퍼티 방식#

public class UserDaoJdbc implements UserDao {
	...
	
	private Map<String, String> sqlMap;	
	public void setSqlMap(Map<String, String> sqlMap) {
		this.sqlMap = sqlMap;
	}

	public void add(User user) {
		this.jdbcTemplate.update(
			this.sqlMap.get("add"), 
			user.getId(), user.getName(), user.getPassword(), user.getEmail(), 
			user.getLevel().intValue(), user.getLogin(), user.getRecommend());
	}
<bean id="userDao" class="springbook.user.dao.UserDaoJdbc">
	<property name="dataSource" ref="dataSource" />
	<property name="sqlMap">
		<map>
			<entry key="add" value="insert into users(id, name, password, email, level, login, recommend) values(?,?,?,?,?,?,?)" />			
			<entry key="get" value="select * from users where id = ?" />
			<entry key="getAll" value="select * from users order by id" />
			<entry key="deleteAll" value="delete from users" />
			<entry key="getCount" value="select count(*) from users" />
			<entry key="update" value="update users set name = ?, password = ?, email = ?, level = ?, login = ?, recommend = ? where id = ?"  />
		</map>
	</property>
</bean>

7.1.2. SQL 제공 서비스#

  • 위처럼 SQL을 DI할 경우 분리한다는 점에서는 좋다.
  • 하지만 SQL과 DI설정정보가 혼재되어 있는게 지저분해보이고 관리하기에 좋지 않다.
  • SQL을 아에 분리하는게 향후 튜닝하는데도 편하다.

SQL서비스 인터페이스#

  • 이를 위해 SQL을 제공하는 별도의 서비스 인터페이스를 만든다.
public interface SqlService {
	String getSql(String key) throws SqlRetrievalFailureException;
}
public class SimpleSqlService implements SqlService {
	private Map<String, String> sqlMap;
	
	public void setSqlMap(Map<String, String> sqlMap) {
		this.sqlMap = sqlMap;
	}

	public String getSql(String key) throws SqlRetrievalFailureException {
		String sql = sqlMap.get(key);
		if (sql == null)  
			throw new SqlRetrievalFailureException(key + "를 이용해서 SQL을 찾을 수 없습니다");
		else
			return sql;
	}
}
public class UserDaoJdbc implements UserDao {	
	private SqlService sqlService;

	public void setSqlService(SqlService sqlService) {
		this.sqlService = sqlService;
	}

	public void add(User user) {
		this.jdbcTemplate.update(
			this.sqlService.getSql("userAdd"), 
			user.getId(), user.getName(), user.getPassword(), user.getEmail(), 
			user.getLevel().intValue(), user.getLogin(), user.getRecommend());
	}
<bean id="userDao" class="springbook.user.dao.UserDaoJdbc">
	<property name="dataSource" ref="dataSource" />
	<property name="sqlService" ref="sqlService" />
</bean>

<bean id="sqlService" class="springbook.user.sqlservice.SimpleSqlService">
	<property name="sqlMap">
		<map>
			<entry key="userAdd" value="insert into users(id, name, password, email, level, login, recommend) values(?,?,?,?,?,?,?)" />			
			<entry key="userGet" value="select * from users where id = ?" />
			<entry key="userGetAll" value="select * from users order by id" />
			<entry key="userDeleteAll" value="delete from users" />
			<entry key="userGetCount" value="select count(*) from users" />
			<entry key="userUpdate" value="update users set name = ?, password = ?, email = ?, level = ?, login = ?, recommend = ? where id = ?"  />
		</map>
	</property>
</bean>

7.2. 인터페이스의 분리와 자기참조 빈#

7.2.1. XML 파일 매핑#

  • 스프링 XML설정파일에서 <bean> 태그안에 SQL정보를 넣어놓고 활용하는 건 좋은 방법이 아니다.
  • 그보다는 SQL을 저장하는 전용 포맷을 가진 독립적인 파일을 이용하는게 바람직하다.

JAXB#

언마샬링#

7.2.2. XML파일을 이용하는 SQL서비스#

SQL맵 XML파일#

  • UserDaoJdbc 가 사용할 SQL이 담긴 XML문서를 만들어보자.
<?xml version="1.0" encoding="UTF-8"?>
<sqlmap xmlns="http://www.epril.com/sqlmap" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.epril.com/sqlmap http://www.epril.com/sqlmap/sqlmap.xsd ">
	<sql key="userAdd">insert into users(id, name, password, email, level, login, recommend) values(?,?,?,?,?,?,?)</sql>
	<sql key="userGet">select * from users where id = ?</sql>
	<sql key="userGetAll">select * from users order by id</sql>
	<sql key="userDeleteAll">delete from users</sql>
	<sql key="userGetCount">select count(*) from users</sql>
	<sql key="userUpdate">update users set name = ?, password = ?, email = ?, level = ?, login = ?, recommend = ? where id = ?</sql>
</sqlmap>

XML SQL 서비스#

  • sqlmap.xml 에 있는 SQL을 가져와서 DAO에 제공해주는 SqlService 인터페이스의 구현체를 만들어보자.
public class XmlSqlService implements SqlService {
	private Map<String, String> sqlMap = new HashMap<String, String>();
	
	public XmlSqlService() {
		String contextPath = Sqlmap.class.getPackage().getName(); 
		try {
			JAXBContext context = JAXBContext.newInstance(contextPath);
			Unmarshaller unmarshaller = context.createUnmarshaller();
			InputStream is = UserDao.class.getResourceAsStream(this.sqlmapFile);
			Sqlmap sqlmap = (Sqlmap)unmarshaller.unmarshal(is);

			for(SqlType sql : sqlmap.getSql()) {
				sqlMap.put(sql.getKey(), sql.getValue());
			}
		} catch (JAXBException e) {
			throw new RuntimeException(e);
		} 
	}

	public String getSql(String key) throws SqlRetrievalFailureException {
		String sql = sqlMap.get(key);
		if (sql == null)  
			throw new SqlRetrievalFailureException(key + "를 이용해서 SQL을 찾을 수 없습니다");
		else
			return sql;
	}
}
<bean id="sqlService" class="springbook.user.sqlservice.XmlSqlService">
</bean>

7.2.3. 빈의 초기화 작업#

  • 생성자에서 예외가 발생할 수 있는 복잡한 초기화 작업을 다루는 것은 좋지 않다.
  • 오브젝트 생성 중 발생하는 예외를 다루기 힘들다
  • 상속하기도 불편한다.
public class XmlSqlService implements SqlService {
	private Map<String, String> sqlMap = new HashMap<String, String>();

	private String sqlmapFile;

	public void setSqlmapFile(String sqlmapFile) {
		this.sqlmapFile = sqlmapFile;
	}

	@PostConstruct
	public void loadSql() {
		String contextPath = Sqlmap.class.getPackage().getName(); 
		try {
			JAXBContext context = JAXBContext.newInstance(contextPath);
			Unmarshaller unmarshaller = context.createUnmarshaller();
			InputStream is = UserDao.class.getResourceAsStream(this.sqlmapFile);
			Sqlmap sqlmap = (Sqlmap)unmarshaller.unmarshal(is);

			for(SqlType sql : sqlmap.getSql()) {
				sqlMap.put(sql.getKey(), sql.getValue());
			}
		} catch (JAXBException e) {
			throw new RuntimeException(e);
		} 
	}

	public String getSql(String key) throws SqlRetrievalFailureException {
		String sql = sqlMap.get(key);
		if (sql == null)  
			throw new SqlRetrievalFailureException(key + "를 이용해서 SQL을 찾을 수 없습니다");
		else
			return sql;
	}
}
  • @PostConstruct 애노테이션은 스프링빈의 후처리기를 사용하는 애노테이션이다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
						http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
						http://www.springframework.org/schema/aop 
						http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
						http://www.springframework.org/schema/context 
						http://www.springframework.org/schema/context/spring-context-3.0.xsd
						http://www.springframework.org/schema/tx 
						http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
	<tx:annotation-driven />	<!-- @Transactional 애노테이션을 사용해서 트랜잭션 제어가 가능하도록 한다. -->				
	<context:annotation-config />	<!-- 코드의 애노테이션을 이용해서 부가적인 빈 설정 또는 초기화 작업을 해주는 후처리기를 등록 --> 

	<bean id="sqlService" class="springbook.user.sqlservice.XmlSqlService">
		<property name="sqlmapFile" value="sqlmap.xml" />
	</bean>

7.2.4. 변화를 위한 준비 : 인터페이스 분리#

책임에 따른 인터페이스 정의#

  • XmlSqlService 는 두가지 관심사를 가진다.
    1. SQL정보를 외부의 리소스로부터 읽어온다.
    2. 읽어온 SQL을 보관해두고 있다가 필요할때 제공한다.
  • SqlService 서비스 오브젝트 구조
그림 7-3
public interface SqlRegistry {
	void registerSql(String key, String sql);
	String findSql(String key) throws SqlNotFoundException;
}
public interface SqlReader {
	void read(SqlRegistry sqlRegistry);
}
  • SqlRegistry에 의존하는 SqlReader구조
그림 7-4

7.2.5. 자기참조 빈으로 시작하기#

다중 인터페이스 구현과 간접참조#

  • SqlService 서비스의 클래스와 의존관계
그림 7-5
  • 클래스의 코드는 인터페이스를 사용해서 인터페이스만을 안다.
  • 클래스를 여러개의 인터페이스를 구현할수 있다.
  • 그림 7-6처럼 하나의 클래스인 XmlSqlService 는 SqlService, SqlRegistry, SqlReader 의 3가지 인터페이스를 구현하는 형태로 가능하다.
그림 7-6

인터페이스를 이용한 분리#

  • 3가지 인터페이스인 SqlService, SqlRegistry, SqlReader 를 각각 구현하는 클래스를 만들자.
  • 하지만 향후 3가지 인터페이스의 구현체를 별도로 분리하더라도 쉽게 할수 있도록 파라미터로 객체를 전달한다.

자기참조 빈 설정#

  • XmlSqlService 클래스안에 혼재되어 있던 성격이 다른 코드를 3가지 인터페이스를 구현하는 방법을 통해 깔끔히 분리했다.
  • 빈은 하나만 선언했기 때문에 실제로 빈 객체도 한개만 만들어진다.
  • 스프링은 프로퍼티의 ref항목에 자기 자신을 넣는것을 허용한다. 이를 통해 sqlService를 구현한 메소드와 초기화 메소드는 외부에서 DI된 객체라고 생각하고 결국 자신의 메소드에 접근한다.
  • 자기참조빈을 지원하긴 하지만 흔히 쓰지않고 보기에도 쉽지 않다.
<bean id="sqlService" class="springbook.user.sqlservice.XmlSqlService">
	<property name="sqlReader" ref="sqlService" />
	<property name="sqlRegistry" ref="sqlService" />
	<property name="sqlmapFile" ref="sqlmap.xml" />
</bean>

7.2.6. 디폴트 의존관계#

확장가능한 기반 클래스#

  • SqlRegistry와 SqlReader를 이용하는 가장 간단한 SqlService 구현 클래스를 만들어보자.
  • SqlReader와 SqlRegistry를 사용하는 SqlService 구현 클래스
public class BaseSqlService implements SqlService {
	protected SqlReader sqlReader;
	protected SqlRegistry sqlRegistry;
		
	public void setSqlReader(SqlReader sqlReader) {
		this.sqlReader = sqlReader;
	}

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

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

	public String getSql(String key) throws SqlRetrievalFailureException {
		try {
			return this.sqlRegistry.findSql(key);
		} 
		catch(SqlNotFoundException e) {
			throw new SqlRetrievalFailureException(e);
		}
	}
}
  • HashMap을 이용하는 SqlRegistry클래스
public class HashMapSqlRegistry implements SqlRegistry {
	private Map<String, String> sqlMap = new HashMap<String, String>();

	public String findSql(String key) throws SqlNotFoundException {
		String sql = sqlMap.get(key);
		if (sql == null)  throw new SqlRetrievalFailureException(key + "를 이용해서 SQL을 찾을 수 없습니다");
		else return sql;
	}

	public void registerSql(String key, String sql) { sqlMap.put(key, sql);	}
}
  • JAXB를 사용하는 SqlReader클래스
public class JaxbXmlSqlReader implements SqlReader {
	private String sqlmapFile;

	public void setSqlmapFile(String sqlmapFile) { this.sqlmapFile = sqlmapFile; }

	public void read(SqlRegistry sqlRegistry) {
		String contextPath = Sqlmap.class.getPackage().getName(); 
		try {
			JAXBContext context = JAXBContext.newInstance(contextPath);
			Unmarshaller unmarshaller = context.createUnmarshaller();
			InputStream is = UserDao.class.getResourceAsStream(sqlmapFile);
			Sqlmap sqlmap = (Sqlmap)unmarshaller.unmarshal(is);
			for(SqlType sql : sqlmap.getSql()) {
				sqlRegistry.registerSql(sql.getKey(), sql.getValue());
			}
		} catch (JAXBException e) { throw new RuntimeException(e); } 		
	}
}
  • SqlReader와 SqlRegistry의 독립적인 빈 설정
<bean id="sqlService" class="springbook.user.sqlservice.BaseSqlService">
	<property name="sqlReader" ref="sqlReader" />
	<property name="sqlRegistry" ref="sqlRegistry" />
</bean>

<bean id="sqlReader" class="springbook.user.sqlservice.JaxbXmlSqlReader">
	<property name="sqlmapFile" value="sqlmap.xml" />
</bean>

<bean id="sqlRegistry" class="springbook.user.sqlservice.HashMapSqlRegistry">
</bean>

디폴트 의존관계를 갖는 빈 만들기#

  • 디폴트 의존관계란 외부에서 DI 받지 않는 경우 기본적으로 자동 적용되는 의존관계를 말한다.
  • DI설정이 없을 경우 디폴트로 적용하고 싶은 의존 객체를 생성자에게 넣어준다.

7.3. 서비스 추상화 적용#

JaxbXmlSqlReader를 조금더 개선해보자.

  • 자바에는 JAXB외에도 다양한 XML과 자바객체를 매핑하는 기술이 있다. 필요에 따라 다른 기술로 손쉽게 바꿔서 사용할수 있게 해야 한다.
  • XML파일을 좀더 다양한 소스에서 가져올수 있게 만든다. 임의의 클래스패스나 파일시스템상의 절대위치 또는 HTTP프로토콜을 통해 원격에서 가져올수 있도록 확장한다.

7.3.1. OXM 서비스 추상화#

  • Castor XML : 설정파일이 필요없는 인트로스펙션 모드를 지원하기도 하는 매우 간결하고 가벼운 바인딩 프레임워크
  • JiBX : 뛰어난 퍼포먼스를 자랑하는 XML바인딩 기술이다.
  • XmlBeans : 아파치 XML 프로젝트의 하다다. XML의 정보셋을 효과적으로 제공해준다.
  • XStream : 관례를 이용해서 설정이 없는 바인딩을 지원하는 XML바인딩 기술이다.

이런 기술을 OXM(Object XML Mapping) 이라고 한다.

OXM 서비스 인터페이스#

  • 스프링 OXM의 Unmarshaller인터페이스
public interface Unmarshaller {
	boolean supports(Class<?> clazz);
	Object unmarshal(Source source) throws IOException, XmlMappingException;
}

JAXB 구현 클래스#

  • 스프링의 Marshaller 클래스는 마샬링과 언마샬링 기능을 모두 처리한다.
  • JAXB용 Unmarshaller 빈 설정
<bean id="unmarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
	<property name="contextPath" value="springbook.user.sqlservice.jaxb" />
</bean>

Castor 구현 클래스#

  • Castor 매핑정보
<?xml version="1.0"?>
<!DOCTYPE mapping PUBLIC "-//EXOLAB/Castor Mapping DTD Version 1.0//EN" "http://castor.org/mapping.dtd">
<mapping>
    <class name="springbook.user.sqlservice.jaxb.Sqlmap">
        <map-to xml="sqlmap" />
        <field name="sql"
               type="springbook.user.sqlservice.jaxb.SqlType"
               required="true" collection="arraylist">
            <bind-xml name="sql" node="element" />
        </field>
    </class>
    <class name="springbook.user.sqlservice.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>
  • Castor기술을 사용하는 언마샬려 설정
<bean id="unmarshaller" class="org.springframework.oxm.castor.CastorMarshaller">
	<property name="mappingLocation" value="springbook/learningtest/spring/oxm/mapping.xml" />
</bean>

7.3.2. OXM 서비스 추상화 적용#

멤버 클래스를 참조하는 통합 클래스#

  • OxmSqlReader를 멤버로 갖는 OxmSqlService클래스 구조
그림 7-7
  • SqlService 구현체는 OxmSqlService 로 정한다.
  • SqlRegistry는 간단한 HashMapSqlRegistry로 한다.
  • SqlReader는 스프링의 OXM을 이용한다.
public class OxmSqlService implements SqlService {
	private final OxmSqlReader oxmSqlReader = new OxmSqlReader();
	private SqlRegistry sqlRegistry = new HashMapSqlRegistry();
	
	public void setSqlRegistry(SqlRegistry sqlRegistry) {
		this.sqlRegistry = sqlRegistry;
	}	
	public void setUnmarshaller(Unmarshaller unmarshaller) {
		this.oxmSqlReader.setUnmarshaller(unmarshaller);
	}	
	public void setSqlmapFile(String sqlmapFile) {
		this.oxmSqlReader.setSqlmapFile(sqlmapFile);
	}	
	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;
		}

		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 + "을 가져올 수 없습니다", e);
			}
		}
	}
}

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

  • loadSql()과 getSql()이라는 SqlService의 핵심 메소드는 BaseSqlService 와 동일해서 BaseSqlService의 해당 메소드로 처리를 위임한다.
  • 위임을 이용한 BaseSqlService 의 재사용
그림 7-9
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();
	}
	public String getSql(String key) throws SqlRetrievalFailureException {
		return this.baseSqlService.getSql(key);
	}
	...
}

7.3.3. 리소스 추상화#

리소스#

  • 스프링은 자바에 존재하는 일관성 없는 리소스 접근 API를 추상화해서 Resource라는 추상화 인터페이스를 제공한다.

리소스 로더#

  • 위치를 지정하면 실제 Resource 타입의 객체로 변환해주는 기능은 ResourceLoader가 제공한다.
  • ResourceLoader가 처리하는 접두어의 예
표 7-1

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

public class OxmSqlService implements SqlService {	
	public void setSqlmap(Resource sqlmap) {
		this.oxmSqlReader.setSqlmap(sqlmap);
	}	
	private class OxmSqlReader implements SqlReader {
		private Resource sqlmap = new ClassPathResource("sqlmap.xml", UserDao.class);
		public void setSqlmap(Resource sqlmap) {
			this.sqlmap = sqlmap;
		}
		public void read(SqlRegistry sqlRegistry) {
			try {
				Source source = new StreamSource(sqlmap.getInputStream());
				...
			} catch (IOException e) {
				throw new IllegalArgumentException(this.sqlmap.getFilename() + "을 가져올 수 없습니다", e);
			}
		}
	}
}
<bean id="sqlService" class="springbook.user.sqlservice.OxmSqlService">
	<property name="unmarshaller" ref="unmarshaller" /> 
	<property name="sqlmap" value="classpath:/springbook/user/dao/sqlmap.xml" />
</bean>

Add new attachment

Only authorized users are allowed to upload new attachments.
« This page (revision-3) was last changed on 04-Dec-2014 23:19 by DongGukLee