- SQL과 DAO의 분리
- 인터페이스의 분리와 자기참조 빈
- 서비스 추상화 적용
- 인터페이스 상속을 통한 안전한 기능확장
- DI를 이용해 다양한 구현 방법 적용하기
- 스프링 3.1의 DI
- Reference
SQL과 DAO의 분리
- 데이터 액세스 로직은 바뀌지 않더라도 SQL 문장은 바뀔 수 있기 때문에 분리한다.
XML 설정을 이용한 분리
- SQL을 XML 설정파일로 분리한다.
public class UserDaoJdbc implements UserDao {
private Map<String, String> sqlMap;//UserDAO DI 할때 Property 값으로 주입
public void setSqlMap(Map<String, String> sqlMap) {
this.sqlMap = sqlMap;
}
}
SQL 제공 서비스
- SQL과 DI 설정정보가 섞여 있으면 관리하기 좋지 않다.
- 이를 위해 DAO가 사용할 SQL을 독립시켜야한다.
public interface SqlService {
String getSql(String key) throws SqlRetrievalFailureException;
}
public class SimpleSqlService implements SqlService {
private final Map<String, String> sqlMap;
public SimpleSqlService(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을 찾을수 없습니다");
}
return sql;
}
}
<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>
- UserDAO 이외에 모든 DAO는 SQL을 어디에 저장해두고 가져오는지 신경 쓸 필요가 없어졌다.
- SqlService 빈만 DI 받아서 필요한 SQL을 가져다 쓰면 된다.
- SqlService의 내부 구현이 변경되어도 DAO에 영향이 없다.
인터페이스의 분리와 자기참조 빈
JAXB(Java Architecture for XML Binding)
- JAXB의 장점은 XML 문서정보를 거의 동일한 구조의 오브젝트로 매핑한다.
XML SQL 서비스
- DAO가 SQL을 요청할 때마다 매번 XML 파일을 읽어서 SQL을 찾는것은 비효율적이다.
- 따라서 XML 파일은 한번 읽고 해당 내용은 어딘가에 저장해둬야한다.
생성자에서 SQL을 읽어와 내부에 저장
@Service
public class XmlSqlService implements SqlService {
private final Map<String, String> sqlMap = new HashMap<>();
public XmlSqlService() {
String contextPath = Sqlmap.class.getPackage().getName();
try {
JAXBContext context = JAXBContext.newInstance(contextPath);
Unmarshaller unmarshaller = context.createUnmarshaller();
Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal(getXmlFile("mapper/sqlmap.xml"));
for (SqlType sql : sqlmap.getSql()) {
sqlMap.put(sql.getKey(), sql.getValue());
}
} catch (JAXBException e) {
throw new RuntimeException(e);
}
}
@Override
public String getSql(String key) throws SqlRetrievalFailureException {
String sql = sqlMap.get(key);
if (sql == null) {
throw new SqlRetrievalFailureException(key + "를 이용해서 SQL을 찾을 수 없습니다");
}
return sql;
}
private File getXmlFile(String fileName) {
ClassLoader classLoader = getClass().getClassLoader();
return new File(classLoader.getResource(fileName).getFile());
}
}
- SQL파일(sqlmap.xml)과 스프링 빈 설정을 분리했다.
- [단점 1] 생성자에서 복잡한 초기화 작업을 다루고 있다.
초기 상태를 가진 오브젝트를 만들어놓고 별도의 초기화 메소드를 사용하는 방법이 바람직하다. - [단점 2] 읽어들일 파일의 위치와 이름이 코드에 고정되어있다.
초기화 메소드 이용
@Service
public class XmlSqlService implements SqlService {
private final Map<String, String> sqlMap = new HashMap<>();
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();
Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal(getXmlFile(this.sqlMapFile));
for (SqlType sql : sqlmap.getSql()) {
sqlMap.put(sql.getKey(), sql.getValue());
}
} catch (JAXBException e) {
throw new RuntimeException(e);
}
}
...
}
- @PostConstruct를 사용함으로써 sqlMapFile을 setter로 주입할 수 있다.
인터페이스 분리
- 현재 SQL을 가져오는 방법에 있어서 특정 기술에 고정되어있다.(XML)
- 가져온 SQL을 저장할 타입이 고정되어있다.(HashMap)
책임에 따른 인터페이스 정의
- SqlReader와 SqlRegistry로 분리.
//SqlService 구현 클래스
Map<String, String> sqls = sqlReader.readSql();
sqlRegistry.addSqls(sqls);
- readSql의 내부 데이터 타입이 외부로 노출되며 강제된다.
public interface SqlRegistry {
void registerSql(String key, String vale);
String findSql(String key) throws SqlNotFoundException;
}
public interface SqlReader {
//SqlReader가 직접 SqlRegistry에 SQl 정보를 등록
void read(SqlRegistry sqlRegistry);
}
자기참조 빈으로 시작하기
- SqlService 클래스는 SqlReader와 SqlRegistry를 DI 받아야한다.
- SqlService의 구현 클래스가 SqlReader, SqlService, SqlRegistry 세개의 인터페이스를 구현하게 할 수도있다.
public class XmlSqlService implements SqlService, SqlRegistry, SqlReader {
// --------- SqlProvider ------------
private SqlReader sqlReader;
private 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);
}
}
// --------- 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);
}
// --------- 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.XmlSqlService">
<property name="sqlReader" ref="sqlService" />
<property name="sqlRegistry" ref="sqlService" />
<property name="sqlmapFile" ref="sqlmap.xml" />
</bean>
- 일반적으로 다른 오브젝트를 주입한다.
- 하지만 여기서는 자기참조 빈을 만들었다.
디폴트 의존관계
- 디폴트 의존관계는 외부에서 DI 받지 않는 경우 자동 적용되는 의존관계를 말한다.
//BaseSqlService
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);
}
}
}
//HashMapSqlRegistry
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);
}
}
//JaxbXmlSqlReader
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); }
}
}
public class DefaultSqlService extends BaseSqlService {
public DefaultSqlService() {
setSqlReader(new JaxbXmlSqlReader());
setSqlRegistry(new HashMapSqlRegistry());
}
}
서비스 추상화 적용
OXM(Object-XML Mapping)
- XML/자바오브젝트 바인딩 기술.
- 다양한 구현체가 있다.
- JAXB
- Castor XML
- JiBX
- XmlBeans
- Xstream
OXM 서비스 추상화 적용
public class OxmSqlService implements SqlService {
private final OxmSqlReader oxmSqlReader = new OxmSqlReader();//[1]
private SqlRegistry sqlRegistry = new HashMapSqlRegistry();
public void setSqlRegistry(SqlRegistry sqlRegistry) {
this.sqlRegistry = sqlRegistry;
}
public void setUnmarshaller(Unmarshaller unmarshaller) {//[2]
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);
}
}
}
}
<bean id="unmarshaller" class="org.springframework.oxm.castor.CastorMarshaller">
<property name="mappingLocation" value="springbook/learningtest/spring/oxm/mapping.xml" />
</bean>
- SQL을 읽는 방식을 OXM으로 고정하기 위해 외부에서 DI받지 않고 내부에서 객체를 직접 만들었다.
- OXM의 구현체를 바꾸고 싶으면 Unmarshaller을 외부에서 DI 받는다.
리소스 추상화
- Resource : 스프링은 리소스 접근 API를 추상화해서 Resource라는 인터페이스를 정의.
- ResourceLoader : 위치를 지정하면 실제 Resource 타입의 객체로 변환해주는 기능 제공.
- 예를 들어 ApplicationContext 역시 ResourceLoader를 상속하고 있다.
ApplicationContext가 사용할 스프링 설정정보를 가져와야하기 때문이다.
인터페이스 상속을 통한 안전한 기능확장
인터페이스 상속
- 위를 현재 구조라고 하자.
- 여기에 이미 등록된 SQL을 변경할 수 있는 새로운 기능을 넣어서 확장하고 싶다.
이미 SqlRegistry 인터페이스를 접근하는 클라이언트와 그 서브 클래스가 존재한다.
따라서 SqlRegistry 자체를 수정하는 것은 바람직하지 않다.
- 이때 인터페이스 상속을 이용한다.
public interface UpdatableSqlRegistry extends SqlRegistry {
void updateSql(String key, String value) throws SqlUpdateFailureException;
void updateSql(Map<String, String> sqls) throws SqlUpdateFailureException;
}
- BaseSqlService와 SqlAdminService는 동일한 오브젝트에 의존하고 있지만
각자의 관심과 필요에 따라서 다른 인터페이스를 통해 접근한다.
DI를 이용해 다양한 구현 방법 적용하기
ConcurrentHashMap을 이용한 수정 가능 SQL 레지스트리
- 멀티 스레드에 안전한 SQL 수정이 필요하다.
- Collections.synchronizedMap() 등을 이용할 수 있지만 HashMap에 대한 전 작업을 동기화하면 성능에 문제가 생길 수 있다.
- ConcurrentHashMap을 사용하면 데이터 조작시 전체 데이터에 대해 락을 걸지 않고 조회시에는 락을 아예 사용하지 않는다.
public class ConcurrentHashMapSqlRegistry implements UpdatableSqlRegistry {
private ConcurrentHashMap<String, String> sqlMap = new ConcurrentHashMap<>();
@Override
public String findSql(String key) throws SqlNotFoundException {
String value = sqlMap.get(key);
if (value == null) {
throw new SqlNotFoundException();
}
return value;
}
@Override
public void registerSql(String key, String value) {
sqlMap.put(key, value);
}
@Override
public void updateSql(String key, String value) throws SqlUpdateFailureException {
if(sqlMap.get(key) == null) {
throw new SqlUpdateFailureException();
}
sqlMap.put(key, value);
}
@Override
public void updateSql(Map<String, String> sqlMap) throws SqlUpdateFailureException {
sqlMap.forEach(this::updateSql);
}
}
내장형 데이터베이스를 이용한 SQL 레지스트리 만들기
- ConcurrentHashMap 대신 내장형 DB를 이용해본다.
- 스프링은 내장형 DB를 위한 서비스 추상화를 제공하고 있다.
- 스프링이 제공하는 내장형 DB 빌더는 EmbeddedDatabaseBuilder다.
new EmbeddedDatabaseBuilder()
.setType(내장형 DB종류)
.setScript(초기화에 사용할 DB 스크립트의 리소스)
...
.build();
- EmbeddedDbSqlRegistry
public class EmbeddedDbSqlRegistry implements UpdatableSqlRegistry {
SimpleJdbcTemplate jdbc;
public void setDataSource(DataSource dataSource) {
jdbc = new SimpleJdbcTemplate(dataSource);
}
public void registerSql(String key, String sql) {
jdbc.update("insert into sqlmap(key_, sql_) values(?,?)", key, sql);
}
public String findSql(String key) throws SqlNotFoundException {
try {
return jdbc.queryForObject("select sql_ from sqlmap where key_ = ?", String.class, key);
}
catch(EmptyResultDataAccessException e) {
throw new SqlNotFoundException(key + "에 해당하는 SQL을 찾을 수 없습니다", e);
}
}
...
}
트랜잭션 적용
- 이전 HashMap의 경우 트랜잭션을 적용하기 힘들다.
- 반면 내장형 DB를 사용하는 경우 트랜잭션 적용이 쉽다.
스프링 3.1의 DI
- 자바 언어의 변화에 따라 DI 프레임워크로서 스프링의 사용 방식에도 여러 가지 영향을 줬다.
- 대표적으로 두 가지가 있다.
[1] 애노테이션의 메타정보 활용
- 자바 코드가 실행의 목적보다 다른 자바 코드에 의해 데이터처럼 취급되기도 한다.
- 코드를 리플렉션 API 등을 이용해 살펴보고 그에 따라 동작하는 기능이 많이 사용되고 있다.
- 대표적으로 애노테이션이 있다.
- 리플렉션 API를 이용해 애노테이션의 메타정보를 조회하고, 애노테이션 내에 설정된 값을 가져와 참고한다.
- 애노테이션을 이용하면 코드의 양을 간략화 할 수 있다.
[2] 정책과 관례를 이용한 프로그래밍
- 코드를 이용해 명시적으로 동작 내용을 기술하는 대신
미리 약속한 규칙에 따라 프로그램이 동작하도록 만든다. - ex1) XML에 bean 태그를 정의해두면 하나의 오브젝트가 만들어진다.
- ex2) @Transactional의 우선순위.
@Import
- @Configuration을 이용한 설정 파일이 여러개 있다.
- 설정 파일을 추가할때마다 @ContextConfiguration의 내용을 수정해야할까?
- @Import 애노테이션을 이용한다.
@Configuration
@Import(SqlServiceContext.class)
public class AppContext {
}
@Enable*
- SqlServiceContext처럼 모듈화된 빈 설정을 가져올 때 @Import를 사용한다.
- 이를 대체하는 용도로 @Enable* 이 사용된다.
- @Import를 메타 애노테이션으로 넣어 만든다.
@Import(value=SqlServiceContext)
public @interface EnableSqlService {
}
- [장점 1] 의미가 더 잘 드러난다.
- [장점 2] 애노테이션에 엘리먼트를 넣어서 옵션을 지정할 수 잇다.
@EnableSqlService("classpath:/springbook/user/sqlmap.xml")