토비의 스프링 ‘7장 스프링의 핵심 기술의 응용’을 읽고 정리합니다. SqlService에서 SQL 문을 동적으로 변경할 수 있는 기능 구현 과정에 집중합니다.
등록과 조회 기능만 가지는 인터페이스
public interface SqlRegistry {
void registerSql(String key, String sql);
String findSql(String key) throws SqlRetrievalFailureException;
}
SQL 수정 기능을 가진 확장 인터페이스
public interface UpdatableSqlRegistry extends SqlRegistry {
void updateSql(String key, String sql) throws SqlRetrievalFailureException;
void updateSql(Map<String, String> sqlmap) throws SqlRetrievalFailureException;
}
테스트 코드
public class ConcurrentHashMapSqlRegistryTest {
UpdatableSqlRegistry sqlRegistry;
@Before
public void setUp() {
sqlRegistry = new ConcurrentHashMapSqlRegistry();
sqlRegistry.registerSql("KEY1", "SQL1");
sqlRegistry.registerSql("KEY2", "SQL2");
sqlRegistry.registerSql("KEY3", "SQL3");
}
@Test
public void find() {
checkFind("SQL1", "SQL2", "SQL3");
}
@Test(expected = SqlRetrievalFailureException.class)
public void unknownKey() {
sqlRegistry.findSql("SQL9999!@#$");
}
@Test
public void updateSingle() {
sqlRegistry.updateSql("KEY2", "Modified2");
checkFind("SQL1", "Modified2", "SQL3");
}
@Test
public void updateMulti() {
Map<String, String> sqlMap = new HashMap<>();
sqlMap.put("KEY1", "Modified1");
sqlMap.put("KEY2", "Modified2");
sqlRegistry.updateSql(sqlMap);
checkFind("Modified1", "Modified2", "SQL3");
}
@Test(expected = SqlRetrievalFailureException.class)
public void updateWithNotExistingKey() {
sqlRegistry.updateSql("SQL9999!@#$", "Modified2");
}
private void checkFind(String expected1, String expected2, String expected3) {
assertEquals(expected1, sqlRegistry.findSql("KEY1"));
assertEquals(expected2, sqlRegistry.findSql("KEY2"));
assertEquals(expected3, sqlRegistry.findSql("KEY3"));
}
}
수정 가능한 SQL Registry
public class ConcurrentHashMapSqlRegistry implements UpdatableSqlRegistry {
private final Map<String, String> sqlMap = new ConcurrentHashMap<>();
@Override
public void registerSql(String key, String sql) {
sqlMap.put(key, sql);
}
@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 updateSql(String key, String sql) throws SqlRetrievalFailureException {
if (sqlMap.get(key) == null) {
throw new SqlRetrievalFailureException(key + "에 대한 SQL을 찾을 수 없습니다");
}
sqlMap.put(key, sql);
}
@Override
public void updateSql(Map<String, String> sqlmap) throws SqlRetrievalFailureException {
for (Map.Entry<String, String> entry : sqlmap.entrySet()) {
updateSql(entry.getKey(), entry.getValue());
}
}
}
애플리케이선 설정정보
<bean id="sqlRegistry" class="sql.ConcurrentHashMapSqlRegistry"/>
<bean id="sqlService" class="sql.OxmSqlService">
<property name="unmarshaller" ref="unmarshaller"/>
<property name="sqlmap" value="classpath:/oxm/sqlmap.xml"/>
<property name="sqlRegistry" ref="sqlRegistry"/>
</bean>
내장형 DB를 사용하는 SQL 레지스트리
public class EmbeddedDbSqlRegistry implements UpdatableSqlRegistry {
private JdbcTemplate jdbcTemplate;
// 인터페이스 분리 원칙을 위해 EmbeddedDatabase 타입이 아닌 상위 DataSource를 받는다.
// EmbeddedDatabase 타입을 통해 DB 종료 기능을 제공해 줄 필요는 없다.
public void setDataSource(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public void registerSql(String key, String sql) {
jdbcTemplate.update("insert into SQLMAP(KEY_, SQL_) values(?, ?)", key, sql);
}
@Override
public String findSql(String key) throws SqlRetrievalFailureException {
try {
return jdbcTemplate.queryForObject("select SQL_ from SQLMAP where KEY_= ?", String.class, key);
} catch (EmptyResultDataAccessException ex) {
throw new SqlRetrievalFailureException(key + "에 대한 SQL을 찾을 수 없습니다", ex);
}
}
@Override
public void updateSql(String key, String sql) throws SqlRetrievalFailureException {
int affected = jdbcTemplate.update("update SQLMAP set SQL_ = ? where KEY_ = ?", sql, key);
if (affected == 0 ) {
throw new SqlRetrievalFailureException(key + "에 대한 SQL을 찾을 수 없습니다");
}
}
@Override
public void updateSql(Map<String, String> sqlmap) throws SqlRetrievalFailureException {
for (Map.Entry<String, String> entry : sqlmap.entrySet()) {
updateSql(entry.getKey(), entry.getValue());
}
}
}
UpdatableSqlRegistry 테스트 코드의 재활용
public abstract class AbstratUpdatableSqlRegistryTest {
UpdatableSqlRegistry sqlRegistry;
@Before
public void setUp() {
sqlRegistry = createUpdatableSqlRegistry();
sqlRegistry.registerSql("KEY1", "SQL1");
sqlRegistry.registerSql("KEY2", "SQL2");
sqlRegistry.registerSql("KEY3", "SQL3");
}
protected abstract UpdatableSqlRegistry createUpdatableSqlRegistry();
...
}
ConcurrentHashMapSqlRegistryTest의 재작성
public class ConcurrentHashMapSqlRegistryTest extends AbstratUpdatableSqlRegistryTest {
@Override
protected UpdatableSqlRegistry createUpdatableSqlRegistry() {
return new ConcurrentHashMapSqlRegistry();
}
}
EmbeddedDbSqlRegistryTest의 작성
public class EmbeddedDbSqlRegistryTest extends AbstratUpdatableSqlRegistryTest {
EmbeddedDatabase db;
@Override
protected UpdatableSqlRegistry createUpdatableSqlRegistry() {
db = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:/sql/sqlRegistrySchema.sql")
.build();
EmbeddedDbSqlRegistry embeddedDbSqlRegistry = new EmbeddedDbSqlRegistry();
embeddedDbSqlRegistry.setDataSource(db);
return embeddedDbSqlRegistry;
}
@After
public void tearDown() {
db.shutdown();
}
}
애플리케이션 설정파일
<beans ...
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
...
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"
>
...
<bean id="sqlRegistry" class="sql.EmbeddedDbSqlRegistry">
<property name="dataSource" ref="embeddedDatabase"/>
</bean>
...
<jdbc:embedded-database id="embeddedDatabase" type="H2">
<jdbc:script location="classpath:/sql/sqlRegistrySchema.sql"/>
</jdbc:embedded-database>
실패하는 트랜잭션 테스트 코드
public void transactionalUpdate() {
checkFind("SQL1", "SQL2", "SQL3");
Map<String, String> sqlmap = new HashMap<>();
sqlmap.put("KEY1", "Modifeid1");
sqlmap.put("KEY9999!@#$", "Modifeid9999"); // 업데이트에 실패하는 케이스 만들기
try {
sqlRegistry.updateSql(sqlmap);
fail();
} catch (SqlRetrievalFailureException exception) {}
checkFind("SQL1", "SQL2", "SQL3");
}
테스트를 성공시키는 트랜잭션 코드 추가
public class EmbeddedDbSqlRegistry implements UpdatableSqlRegistry {
private JdbcTemplate jdbcTemplate;
private TransactionTemplate transactionTemplate;
public void setDataSource(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
transactionTemplate = new TransactionTemplate(
new DataSourceTransactionManager(dataSource));
}
...
@Override
public void updateSql(Map<String, String> sqlmap) throws SqlRetrievalFailureException {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
for (Map.Entry<String, String> entry : sqlmap.entrySet()) {
updateSql(entry.getKey(), entry.getValue());
}
}
});
}
}