토비의 스프링 ‘7장 스프링의 핵심 기술의 응용’을 읽고 정리합니다. 6장까지 개선해왔던 UserDao에서 SQL 쿼리문을 분리하여 손쉽게 유지보수하고 확장가능한 구조를 만들어가는 과정에 집중합니다.
add() 메서드를 위한 SQL 필드
private String sqlAdd;
public void setSqlAdd(String sqlAdd) {
this.sqlAdd = sqlAdd;
}
주입받은 SQL 사용
@Override
public void add(User user) {
jdbcTemplate.update(sqlAdd,
user.getId(), user.getName(), user.getPassword(),
user.getLevel().intValue(), user.getLogin(), user.getRecommend(), user.getEmail()
);
}
설정파일에 넣은 SQL 문장
<bean id="userDao" class="dao.UserDaoJdbc">
<property name="dataSource" ref="dataSource"/>
<property name="sqlAdd" value="insert into users(id, name, password, level, login, recommend, email) values(?,?,?,?,?,?,?)"/>
</bean>
맵 타입의 SQL 정보 프로퍼티
private Map<String, String> sqlMap;
public void setSqlMap(Map<String, String> sqlMap) {
this.sqlMap = sqlMap;
}
sqlMap을 사용하도록 수정한 add()
@Override
public void add(User user) {
jdbcTemplate.update(sqlMap.get("add"),
user.getId(), user.getName(), user.getPassword(),
user.getLevel().intValue(), user.getLogin(), user.getRecommend(), user.getEmail()
);
}
맵을 이용한 SQL 설정
<bean id="userDao" class="dao.UserDaoJdbc">
<property name="dataSource" ref="dataSource"/>
<property name="sqlMap">
<map>
<entry key="add" value="insert into users(id, name, password, level, login, recommend, email) 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 = ?, level = ?, login = ?, recommend = ?, email = ? where id = ?"/>
</map>
</property>
</bean>
SqlService 인터페이스
public interface SqlService {
String getSql(String key) throws SqlRetrievalFailureException;
}
SQL 조회 실패 시 예외
public class SqlRetrievalFailureException extends RuntimeException {
@Serial
private static final long serialVersionUID = 4411008427154074018L;
public SqlRetrievalFailureException(String message) {
super(message);
}
public SqlRetrievalFailureException(String message, Throwable cause) {
super(message, cause);
}
}
SqlService 프로퍼티 추가
public class UserDaoJdbc implements UserDao {
...
private SqlService sqlService;
public void setSqlService(SqlService sqlService) {
this.sqlService = sqlService;
}
SqlService를 사용하도록 수정한 메서드
@Override
public void add(User user) {
jdbcTemplate.update(sqlService.getSql("userAdd"),
user.getId(), user.getName(), user.getPassword(),
user.getLevel().intValue(), user.getLogin(), user.getRecommend(), user.getEmail()
);
}
맵을 이용한 SqlService 구현
public class SimpleSqlService implements SqlService {
private Map<String, String> sqlMap;
public void setSqlMap(Map<String, String> sqlMap) {
this.sqlMap = sqlMap;
}
@Override
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="userDao" class="dao.UserDaoJdbc">
<property name="dataSource" ref="dataSource"/>
<property name="sqlService" ref="sqlService"/>
</bean>
<bean id="sqlService" class="sql.SimpleSqlService">
<property name="sqlMap">
<map>
<entry key="userAdd" value="insert into users(id, name, password, level, login, recommend, email) 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 = ?, level = ?, login = ?, recommend = ?, email = ? where id = ?"/>
</map>
</property>
</bean>
XML 문서의 구조를 정의하는 스키마
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.epril.com/sqlmap"
xmlns:tns="http://www.epril.com/sqlmap" elementFormDefault="qualified">
<element name="sqlmap">
<complexType>
<sequence>
<element name="sql" maxOccurs="unbounded" type="tns:sqlType" />
</sequence>
</complexType>
</element>
<complexType name="sqlType">
<simpleContent>
<extension base="string">
<attribute name="key" use="required" type="string" />
</extension>
</simpleContent>
</complexType>
</schema>
JAXB 컴파일러로 컴파일 (아래 주의)
xjc -p springbook.user.sqlservice.jaxb sqlmap.xsd -d src
Sqlmap 클래스 (JAXB 컴파일러에 의해 자동 생성된)
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = "sql")
@XmlRootElement(name = "sqlmap")
public class Sqlmap {
@XmlElement(required = true)
protected List<SqlType> sql;
public List<SqlType> getSql() {
if (sql == null) {
sql = new ArrayList<>();
}
return sql;
}
}
SqlType 클래스 (JAXB 컴파일러에 의해 자동 생성된)
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "sqlType", propOrder = "value")
public class SqlType {
@XmlValue
protected String value;
@XmlAttribute(name = "key", required = true)
protected String key;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getKey() {
return key;
}
public void setKey(String value) {
key = value;
}
}
SQL 문장을 담을 전용 설정 문서
<?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, level, login, recommend, email) values(?,?,?,?,?,?,?)</sql>
<sql key="userGet">select * from users where id = ?</sql>
<sql key="userGetAll">elect * 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 = ?, level = ?, login = ?, recommend = ?, email = ? where id = ?</sql>
</sqlmap>
생성자 초기화 방법을 사용하는 XmlSqlService 클래스
public class XmlSqlService implements SqlService {
private final Map<String, String> sqlMap = new HashMap<>();
public XmlSqlService() throws JAXBException {
String contextPath = Sqlmap.class.getPackage().getName();
System.out.println("contextPath = " + contextPath);
try {
JAXBContext context = JAXBContext.newInstance(contextPath);
Unmarshaller unmarshaller = context.createUnmarshaller();
InputStream is = getClass().getResourceAsStream("/sqlmap.xml");
Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal(is);
for (SqlType sql : sqlmap.getSql()) {
sqlMap.put(sql.getKey(), sql.getValue());
}
} catch (JAXBException ex) {
throw new RuntimeException(ex);
}
}
@Override
public String getSql(String key) throws SqlRetrievalFailureException {
String sql = sqlMap.get(key);
if (sql == null) {
throw new SqlRetrievalFailureException(key + "에 대한 SQL을 찾을 수 없습니다");
} else {
return sql;
}
}
}
Maven 의존 패키지 추가
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
@PostConstruct엥 의한 빈 초기화 및 XML 경로 외부 주입 적용
public class XmlSqlService implements SqlService {
...
private String sqlmapFile;
public void setSqlmapFile(String sqlmapFile) {
this.sqlmapFile = sqlmapFile;
}
@PostConstruct
public void loadSql() throws JAXBException {
...
InputStream is = getClass().getResourceAsStream('/' + sqlmapFile);
...
}
애플리케이선 설정정보
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
...
xmlns:context="http://www.springframework.org/schema/context"
...
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
...
<bean id="sqlService" class="sql.XmlSqlService">
<property name="sqlmapFile" value="sqlmap.xml"/>
</bean>
Map<String, String> sqls = sqlReader.readSql();
sqlRegistry.addSqls(sqls);
sqlReader.readSql(sqlRegistry);
SqlRegistry 인터페이스
public interface SqlRegistry {
void registerSql(String key, String sql);
String findSql(String key) throws SqlNotFoundException;
}
SqlReader 인터페이스
@FunctionalInterface
public interface SqlReader {
void read(SqlRegistry sqlRegistry);
}
XmlSqlService의 의존객체 DI 코드
public class XmlSqlService implements SqlService, SqlRegistry, SqlReader {
...
private SqlReader sqlReader;
private SqlRegistry sqlRegistry;
public void setSqlReader(SqlReader sqlReader) {
this.sqlReader = sqlReader;
}
public void setSqlRegistry(SqlRegistry sqlRegistry) {
this.sqlRegistry = sqlRegistry;
}
...
}
SqlRegistry 구현
public class XmlSqlService implements SqlService, SqlRegistry {
...
private SqlReader sqlReader;
private SqlRegistry sqlRegistry;
@Override
public String findSql(String key) throws SqlRetrievalFailureException {
String sql = sqlMap.get(key);
if (sql == null) {
throw new SqlRetrievalFailureException(key + "에 대한 SQL을 찾을 수 없습니다");
} else {
return sql;
}
}
@Override
public void registerSql(String key, String sql) {
sqlMap.put(key, sql);
}
...
}
SqlReader 구현
public class XmlSqlService implements SqlService, SqlRegistry, SqlReader {
...
private SqlReader sqlReader;
private SqlRegistry sqlRegistry;
@Override
public void read(SqlRegistry sqlRegistry) {
String contextPath = Sqlmap.class.getPackage().getName();
System.out.println("contextPath = " + contextPath);
try {
JAXBContext context = JAXBContext.newInstance(contextPath);
Unmarshaller unmarshaller = context.createUnmarshaller();
InputStream is = getClass().getResourceAsStream('/' + sqlmapFile);
Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal(is);
for (SqlType sql : sqlmap.getSql()) {
sqlRegistry.registerSql(sql.getKey(), sql.getValue());
}
} catch (JAXBException ex) {
throw new RuntimeException(ex);
}
}
...
}
SqlServie 인터페이스 구현
public class XmlSqlService implements SqlService, SqlRegistry, SqlReader {
...
private SqlReader sqlReader;
private SqlRegistry sqlRegistry;
...
@PostConstruct
public void loadSql() throws JAXBException {
sqlReader.read(sqlRegistry);
}
@Override
public String getSql(String key) throws SqlRetrievalFailureException {
return sqlRegistry.findSql(key);
}
...
}
자신을 참조하는 sqlService 빈 설정
<bean id="sqlService" class="sql.XmlSqlService">
<property name="sqlmapFile" value="sqlmap.xml"/>
<property name="sqlReader" ref="sqlService"/>
<property name="sqlRegistry" ref="sqlService"/>
</bean>
SqlReader와 SqlRegistry를 DI 받아서 사용하는 BaseSqlService 클래스
public class BaseSqlService implements SqlService {
private SqlReader sqlReader;
private SqlRegistry sqlRegistry;
public void setSqlReader(SqlReader sqlReader) {
this.sqlReader = sqlReader;
}
public void setSqlRegistry(SqlRegistry sqlRegistry) {
this.sqlRegistry = sqlRegistry;
}
@Override
public void loadSql() {
sqlReader.read(sqlRegistry);
}
@Override
public String getSql(String key) throws SqlRetrievalFailureException {
return sqlRegistry.findSql(key);
}
}
JAXB 기술을 사용해 XML 파일을 로딩하는 SqlReader 구현
public class JaxbXmlSqlReader implements SqlReader {
private String sqlmapFile;
public void setSqlmapFile(String sqlmapFile) {
this.sqlmapFile = sqlmapFile;
}
@Override
public void read(SqlRegistry sqlRegistry) {
String contextPath = Sqlmap.class.getPackage().getName();
try {
JAXBContext context = JAXBContext.newInstance(contextPath);
Unmarshaller unmarshaller = context.createUnmarshaller();
InputStream is = getClass().getResourceAsStream('/' + sqlmapFile);
Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal(is);
for (SqlType sql : sqlmap.getSql()) {
sqlRegistry.registerSql(sql.getKey(), sql.getValue());
}
} catch (JAXBException ex) {
throw new RuntimeException(ex);
}
}
}
HashMap을 사용해 로딩한 XML 데이터를 관리하는 SqlRegistry 구현
public class HashMapSqlRegistry implements SqlRegistry {
private final Map<String, String> sqlMap = new HashMap<>();
@Override
public String findSql(String key) throws SqlRetrievalFailureException {
String sql = sqlMap.get(key);
if (sql == null) {
throw new SqlRetrievalFailureException(key + "에 대한 SQL을 찾을 수 없습니다");
} else {
return sql;
}
}
@Override
public void registerSql(String key, String sql) {
sqlMap.put(key, sql);
}
}
애플리케이션 설정정보
<bean id="sqlReader" class="sql.JaxbXmlSqlReader">
<property name="sqlmapFile" value="sqlmap.xml"/>
</bean>
<bean id="sqlRegistry" class="sql.HashMapSqlRegistry"/>
<bean id="sqlService" class="sql.BaseSqlService">
<property name="sqlReader" ref="sqlReader"/>
<property name="sqlRegistry" ref="sqlRegistry"/>
</bean>
디폴트 의존관계를 생성자에서 주입하는 DefaultSqlService
public class DefaultSqlService extends BaseSqlService {
public DefaultSqlService() {
setSqlReader(new JaxbXmlSqlReader());
setSqlRegistry(new HashMapSqlRegistry());
}
}
디폴트 값을 갖는 JaxbXmlSqlReader
public class JaxbXmlSqlReader implements SqlReader {
private static final String DEFAULT_SQLMAP_FILE = "sqlmap.xml";
private String sqlmapFile = DEFAULT_SQLMAP_FILE;
...
}
디폴트 의존관계를 활용한 유연한 빈 설정
<bean id="sqlService" class="sql.DefaultSqlService">
<property name="sqlRegistry" ref="ultraSuperFastSqlRegistry"/>
</bean>
OxmSqlReader를 내부 클래스로 구현하고 있는 OxmSqlService
public class OxmSqlService implements SqlService {
private final OxmSqlReader sqlReader= new OxmSqlReader();
private SqlRegistry sqlRegistry = new HashMapSqlRegistry();
public void setUnmarshaller(Unmarshaller unmarshaller) {
sqlReader.setUnmarshaller(unmarshaller);
}
public void setSqlmapFile(String sqlmapFile) {
sqlReader.setSqlmapFile(sqlmapFile);
}
public void setSqlRegistry(SqlRegistry sqlRegistry) {
this.sqlRegistry = sqlRegistry;
}
@PostConstruct
@Override
public void loadSql() {
sqlReader.read(sqlRegistry);
}
@Override
public String getSql(String key) throws SqlRetrievalFailureException {
return sqlRegistry.findSql(key);
}
private class OxmSqlReader implements SqlReader {
private Unmarshaller unmarshaller;
private static final String DEFAULT_MAP_FILE = "sqlmap.xml";
private String sqlmapFile = DEFAULT_MAP_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("/oxm/" + sqlmapFile));
Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal(source);
for (SqlType sql : sqlmap.getSql()) {
sqlRegistry.registerSql(sql.getKey(), sql.getValue());
}
} catch (IOException e) {
throw new IllegalArgumentException(sqlmapFile + "을 가져올 수 없습니다.", e);
}
}
}
}
애플리케이션 설정정보
<bean id="unmarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="contextPath" value="sql.jaxb"/>
</bean>
<bean id="sqlService" class="sql.OxmSqlService">
<property name="unmarshaller" ref="unmarshaller"/>
</bean>
BaseSqlService를 사용한 중복 개선
public class OxmSqlService implements SqlService {
private final BaseSqlService baseSqlService = new BaseSqlService();
private final OxmSqlReader sqlReader= new OxmSqlReader();
private SqlRegistry sqlRegistry = new HashMapSqlRegistry();
...
@PostConstruct
@Override
public void loadSql() {
baseSqlService.setSqlReader(sqlReader);
baseSqlService.setSqlRegistry(sqlRegistry);
baseSqlService.loadSql();
}
@Override
public String getSql(String key) throws SqlRetrievalFailureException {
return baseSqlService.getSql(key);
}
OxmSqlService에 Resource 적용
public class OxmSqlService implements SqlService {
...
private final OxmSqlReader sqlReader= new OxmSqlReader();
...
public void setSqlmap(Resource sqlmap) {
sqlReader.setSqlmap(sqlmap);
}
@SuppressWarnings("InnerClassMayBeStatic")
private class OxmSqlReader implements SqlReader {
...
private Resource sqlmap = new ClassPathResource("/oxm/sqlmap.xml"); // 디폴트
public void setSqlmap(Resource sqlmap) {
this.sqlmap = sqlmap;
}
...
@Override
public void read(SqlRegistry sqlRegistry) {
try {
Source source = new StreamSource(sqlmap.getInputStream());
Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal(source);
...
} catch (IOException e) {
throw new IllegalArgumentException(sqlmap.getFilename() + "을 가져올 수 없습니다.", e);
}
}
}
}
설정파일
<bean id="sqlService" class="sql.OxmSqlService">
<property name="unmarshaller" ref="unmarshaller"/>
<property name="sqlmap" value="classpath:/oxm/sqlmap.xml"/>
</bean>