티스토리 뷰
SQL - DAO 분리
- SQL 변경 작업은 생각보다 빈번히 일어남
- 그때마다 DAO 코드를 수정하고 이를 다시 컴파일해서 적용하는 것은 번거로울 뿐만 아니라 위험하기도 함
- 따라서 SQL 을 적절히 분리해서 DAO 코드와 다른 파일이나 위치에 두고 관리하는 것이 좋음
Configuration 을 이용한 분리
public class UserDaoJdbc implements UserDao {
private String sqlAdd;
public UserDaoJdbc(DataSource dataSource, String sqlAdd) {
jdbcTemplate = new JdbcTemplate(dataSource);
this.sqlAdd = sqlAdd;
}
public void add(final User user) {
this.jdbcTemplate.update(sqlAdd,
user.getId(), user.getName(), user.getPassword(), user.getLevel().intValue(), user.getLogin(), user.getRecommend());
}
}
@Configuration
public class DaoFactory {
@Bean
public UserDao userDao() {
UserDao userDao = new UserDaoJdbc(dataSource(), "insert into users(id, name, password, level, login, recommend) values(?, ?, ?, ?, ?, ?)");
return userDao;
}
}
- 코드 수정 없이 config 설정을 바꾸는 것만으로 SQL 수정 가능
- 한계
- SQL 수정할 때마다 DI를 위한 변수, 생성자를 함께 수정해야 함
SQL을 하나의 컬렉션에 담는 방법
@Configuration
public class DaoFactory {
@Bean
public UserDao userDao() {
Map<String, String> sqlMap = new HashMap<>();
sqlMap.put("add", "insert into users(id, name, password, level, login, recommend) values(?, ?, ?, ?, ?, ?)");
//put other sql to sqlMap
UserDao userDao = new UserDaoJdbc(dataSource(), sqlMap);
return userDao;
}
}
public class UserDaoJdbc implements UserDao {
private Map<String, String> sqlMap;
private JdbcTemplate jdbcTemplate;
public UserDaoJdbc(DataSource dataSource, Map<String,String> sqlMap) {
jdbcTemplate = new JdbcTemplate(dataSource);
this.sqlMap = sqlMap;
}
public void add(final User user) {
this.jdbcTemplate.update(sqlMap.get("add"),
user.getId(), user.getName(), user.getPassword(), user.getLevel().intValue(), user.getLogin(), user.getRecommend());
}
}
- 한계
- SQL과 DI 설정 정보가 섞여 있어서 보기에도 지저분하고 관리에도 좋지 않음
- 설정파일로부터 생성된 오브젝트와 정보는 어플리케이션을 다시 시작하기 전에는 변경이 매우 어려움
SQL 제공 서비스
- SQL 제공 기능을 본격적으로 분리해서 다양한 SQL 정보 소스를 사용할 수 있고, 운영 중에 동적으로 갱신도 가능한 SQL 서비스를 만들어보자
public interface SqlService {
String getSql(String key) throws SqlRetrievalFailureException;
}
public class SimpleSqlService implements SqlService{
private 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 을 찾을 수 없습니다.");
} else {
return sql;
}
}
}
@Bean
public UserDao userDao() {
UserDao userDao = new UserDaoJdbc(dataSource(), sqlService());
return userDao;
}
@Bean
public SqlService sqlService() {
Map<String, String> sqlMap = new HashMap<>();
// 여러 Dao 의 Sql을 구분하기 위해 key 값을 userAdd로 구체화
sqlMap.put("userAdd", "insert into users(id, name, password, level, login, recommend) values(?, ?, ?, ?, ?, ?)");
//put other sql to sqlMap
SqlService sqlService = new SimpleSqlService(sqlMap);
return sqlService;
}
public class UserDaoJdbc implements UserDao {
private SqlService sqlService;
private JdbcTemplate jdbcTemplate;
public UserDaoJdbc(DataSource dataSource, SqlService sqlService) {
jdbcTemplate = new JdbcTemplate(dataSource);
this.sqlService = sqlService;
}
public void add(final User user) {
this.jdbcTemplate.update(this.sqlService.getSql("userAdd"),
user.getId(), user.getName(), user.getPassword(), user.getLevel().intValue(), user.getLogin(), user.getRecommend());
}
}
- 장점
- DAO는 SQL을 어디에 저장해두고 가져오는지에 대해서 전혀 신경 쓰지 않아도 됨
- DAO에 영향을 주지 않고 다양한 방법으로 구현된 SqlService 타입 크래스 적용 가능
언마샬링
XML 문서를 읽어서 자바의 오브젝트로 변환하는 것
반대로 바인딩 오브젝트를 XML 로 변환하는 것은 마샬링
XML -> java Object 로 바꾸는 예제
public class JaxbTest {
@Test
void readSqlmap() throws JAXBException {
String contextPath = Sqlmap.class.getPackageName();
JAXBContext context = JAXBContext.newInstance(contextPath);
Unmarshaller unmarshaller = context.createUnmarshaller();
Sqlmap sqlmap = (Sqlmap)unmarshaller.unmarshal(
getClass().getResourceAsStream("sqlmap.xml")
);
List<SqlType> sqlList = sqlmap.getSql();
assertThat(sqlList.size()).isEqualTo(3);
}
}
SQL - 스프링 빈 설정 분리
- sqlmap.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>
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("sqlmap.xml");
Sqlmap sqlmap = (Sqlmap)unmarshaller.unmarshal(is);
// SQL 을 맵으로 저장
for (SqlType sql : sqlmap.getSql()) {
sqlMap.put(sql.getKey(), sql.getValue());
}
} catch (JAXBException e) {
// JAXBException 은 복구 불가능한 예외이므로 불필요한 throws를 피하도록 런타임 예외로 포장해서 던진다.
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 을 찾을 수 없습니다.");
} else {
return sql;
}
}
}
@Bean
public SqlService sqlService() {
// 빈 설정에서 SQL 문장이 사라졌다.
SqlService sqlService = new XmlSqlService();
return sqlService;
}
빈 초기화 작업
- 생성자에 두지 않고 초기화 메소드를 이용
- 생성자에서 발생하는 예외는 다루기 힘들고 상속하기 불편하며 보안 상 문제가 있음
- xml 파일 위치를 외부 설정 값으로 분리
@PostConstruct
- 스프링은 빈의 오브젝트를 생성하고 DI 작업을 마친 후에 @PostConstruct가 붙은 메소드를 자동으로 실행
- 생성자와는 달리 프로퍼티까지 모두 준비된 후에 실행됨
public class XmlSqlService implements SqlService{
@Value("${sqlmapFile}")
private String sqlmapFile;
private Map<String, String> sqlMap = new HashMap<String, String>();
@Override
public String getSql(String key) throws SqlRetrievalFailureException {
String sql = sqlMap.get(key);
if (sql == null) {
throw new SqlRetrievalFailureException(key + " 에 대한 SQL 을 찾을 수 없습니다.");
} else {
return sql;
}
}
@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("sqlmap.xml");
Sqlmap sqlmap = (Sqlmap)unmarshaller.unmarshal(is);
// SQL 을 맵으로 저장
for (SqlType sql : sqlmap.getSql()) {
sqlMap.put(sql.getKey(), sql.getValue());
}
} catch (JAXBException e) {
// JAXBException 은 복구 불가능한 예외이므로 불필요한 throws를 피하도록 런타임 예외로 포장해서 던진다.
throw new RuntimeException(e);
}
}
}
- 문제점 : XmlSqlService가 변경되는 이유가 두 가지
- Xml 대신 다른 포맷의 파일에서 SQL을 읽어오고 싶을 때
- 외부 파일에서 가져온 SQL 정보를 HashMap 이 아닌 다른 collection에 저장해두고 싶을 때
인터페이스 분리
public interface SqlReader {
//SQL을 외부에서 가져와 SqlRegistry에 등록. 다양한 예외가 발생할 수 있지만 대부분 복구 불가능한 예외이므로 예외를 선언해두지 않음.
void read(SqlRegistry sqlRegistry);
}
- SqlReader가 읽어오는 SQL 정보는 다시 SqlRegistry에 전달해서 등록되도록 해야 한다.
- SqlReader 에게 SqlRegistry 전략을 제공해주면서 이를 이용해 Sql 정보를 SqlRegistry에 저장하라고 요청
public interface SqlRegistry {
//SQL을 키와 함께 등록
void registerSql(String key, String sql);
//키로 SQL을 검색. 검색이 실패하면 예외를 던짐.
String findSql(String key) throws SqlNotFoundException;
}
public class XmlSqlService implements SqlService, SqlRegistry, SqlReader {
private final SqlReader sqlReader;
private final SqlRegistry sqlRegistry;
private Map<String, String> sqlMap = new HashMap<>();
@Value("${sqlMapFile}")
private String sqlMapFile;
public XmlSqlService(SqlReader sqlReader, SqlRegistry sqlRegistry) {
this.sqlReader = sqlReader;
this.sqlRegistry = sqlRegistry;
}
@Override
public void registerSql(String key, String sql) {
sqlMap.put(key, sql);
}
@Override
public String findSql(String key) throws SqlNotFoundException {
String sql = sqlMap.get(key);
if (sql == null) {
throw new SqlRetrievalFailureException(key + " 에 대한 SQL 을 찾을 수 없습니다.");
} else {
return sql;
}
}
@Override
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);
// SQL 을 맵으로 저장
for (SqlType sql : sqlmap.getSql()) {
//sqlMap.put(sql.getKey(), sql.getValue());
//전략 패턴..?
sqlRegistry.registerSql(sql.getKey(), sql.getValue());
}
} catch (JAXBException e) {
// JAXBException 은 복구 불가능한 예외이므로 불필요한 throws를 피하도록 런타임 예외로 포장해서 던진다.
throw new RuntimeException(e);
}
}
@Override
public String getSql(String key) throws SqlRetrievalFailureException {
try {
return this.sqlRegistry.findSql(key);
} catch (SqlNotFoundException e) {
throw new SqlRetrievalFailureException(e.getMessage());
}
}
@PostConstruct
public void loadSql() {
this.sqlReader.read(this.sqlRegistry);
}
}
- 자기참조 빈
- 흔히 쓰이는 방법은 아님. 책임과 관심사가 복잡하게 얽혀 있어서 확장이 힘들고 변경에 취약한 구조의 클래스를 유연한 구조로 바꿀 때 처음 시도해볼 수 있는 방법
- 일종의 임시 구현 단계 (?)
디폴트 의존 관계
확장 가능한 기반 클래스
public class BaseSqlService implements SqlService {
protected SqlReader sqlReader;
protected SqlRegistry sqlRegistry;
public BaseSqlService(SqlReader sqlReader, SqlRegistry sqlRegistry) {
this.sqlReader = sqlReader;
this.sqlRegistry = sqlRegistry;
}
@PostConstruct
public void loadSql() {
this.sqlReader.read(this.sqlRegistry);
}
@Override
public String getSql(String key) throws SqlRetrievalFailureException {
try {
return this.sqlRegistry.findSql(key);
} catch (SqlNotFoundException e) {
throw new SqlRetrievalFailureException(e.getMessage());
}
}
}
public class HashMapSqlRegistry implements SqlRegistry {
private Map<String, String> sqlMap = new HashMap<>();
@Override
public void registerSql(String key, String sql) {
sqlMap.put(key, sql);
}
@Override
public String findSql(String key) throws SqlNotFoundException {
String sql = sqlMap.get(key);
if (sql == null) {
throw new SqlRetrievalFailureException(key + " 에 대한 SQL 을 찾을 수 없습니다.");
} else {
return sql;
}
}
}
public class JaxbXmlSqlReader implements SqlReader{
private final String sqlmapFile;
public JaxbXmlSqlReader(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 = UserDao.class.getResourceAsStream(sqlmapFile);
Sqlmap sqlmap = (Sqlmap)unmarshaller.unmarshal(is);
// SQL 을 맵으로 저장
for (SqlType sql : sqlmap.getSql()) {
//sqlMap.put(sql.getKey(), sql.getValue());
//전략 패턴..?
sqlRegistry.registerSql(sql.getKey(), sql.getValue());
}
} catch (JAXBException e) {
// JAXBException 은 복구 불가능한 예외이므로 불필요한 throws를 피하도록 런타임 예외로 포장해서 던진다.
throw new RuntimeException(e);
}
}
}
@Bean
public SqlService sqlService() {
SqlService sqlService = new BaseSqlService(sqlReader(), sqlRegistry());
return sqlService;
}
@Bean
public SqlReader sqlReader() {
SqlReader sqlReader = new JaxbXmlSqlReader("sqlmap.xml");
return sqlReader;
}
@Bean
public SqlRegistry sqlRegistry() {
SqlRegistry sqlRegistry = new HashMapSqlRegistry();
return sqlRegistry;
}
디폴트 의존관계를 갖는 빈 만들기
- 외부에서 DI 받지 않는 경우 기본적으로 자동 적용되는 의존관계
- 특정 의존 오브젝트가 대부분의 환경에서 거의 디폴트라고 해도 좋을 만큼 기본적으로 사용될 가능성이 있다면 디폴트 의존관계를 갖는 빈을 고려해볼 필요가 있음
public class DefaultSqlService extends BaseSqlService {
private static final String DEFAULT_SQLMAP_FILE = "sqlmap.xml";
public DefaultSqlService() {
super(new JaxbXmlSqlReader(DEFAULT_SQLMAP_FILE), new HashMapSqlRegistry());
}
}
@Bean
public SqlService sqlService() {
// SqlService sqlService = new BaseSqlService(sqlReader(), sqlRegistry());
SqlService sqlService = new DefaultSqlService();
return sqlService;
}
- 만약 DI 오브젝트 중 일부를 바꾸고 싶다면 DefaultSqlService() 생성자를 추가해서 구현하면 된다.
서비스 추상화 적용
- 위 코드 개선 사항
- JAXB 이외에 다양한 XML - 자바오브젝트 매핑 기술을 적용하고 싶을 때 손쉽게 다른 매핑 기술을 적용할 수 있어야 한다.
- 서비스 추상화
- 기능이 같은 여러 가지 기술이 존재하고, 스프링에서 공통 인터페이스를 제공.
- XML 파일을 좀 더 다양한 소스에서 가져올 수 있게 만든다. (현재는 UserDao 클래스와 같은 클래스패스 안에서만 XML을 읽어 옴.)
- JAXB 이외에 다양한 XML - 자바오브젝트 매핑 기술을 적용하고 싶을 때 손쉽게 다른 매핑 기술을 적용할 수 있어야 한다.
public class OxmSqlService implements SqlService{
private final OxmSqlReader oxmSqlReader;
//디플토 오브젝트로 만들어진 프로퍼티. 필요에 따라 DI 를 통해 교체 가능.
private SqlRegistry sqlRegistry = new HashMapSqlRegistry();
public OxmSqlService(Unmarshaller unmarshaller) {
this.oxmSqlReader = new OxmSqlReader(unmarshaller);
}
@PostConstruct
public void loadSql() {
this.oxmSqlReader.read(this.sqlRegistry);
}
@Override
public String getSql(String key) throws SqlRetrievalFailureException {
try {
return this.sqlRegistry.findSql(key);
} catch (SqlNotFoundException e) {
throw new SqlRetrievalFailureException(e.getMessage());
}
}
private class OxmSqlReader implements SqlReader {
private Unmarshaller unmarshaller;
private final static String DEFAULT_SQLMAP_FILE = "sqlmap.xml";
private String sqlmapFile = DEFAULT_SQLMAP_FILE;
public OxmSqlReader(Unmarshaller unmarshaller) {
this.unmarshaller = unmarshaller;
}
@Override
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 (JAXBException e) {
throw new IllegalArgumentException(this.sqlmapFile + "을 가져올 수 없습니다.", e);
}
}
}
}
@Bean
public SqlService sqlService() {
SqlService sqlService = new OxmSqlService(unmarshaller());
return sqlService;
}
@Bean
public Unmarshaller unmarshaller() {
Unmarshaller unmarshaller = new Jaxb2Marshaller();
return unmarshaller;
}
구현 클래스를 OxmSqlService가 내장하는 방식으로 구현.
- 밖에서 볼 때는 하나의 오브젝트로 보이지만 내부에서는 의존관계를 가진 두 개의 오브젝트가 깔끔하게 결합돼서 사용
- 하나의 클래스로 만들어두기 때문에 빈의 등록과 설정이 단순해지고 쉽게 사용 가능.
- (강한 결합이 유용할 때도 있다!)
개선사항
loadSql()
,getSql()
코드가 BaseSqlService, OxmSqlService 에 중복되어 나타남.
위임을 이용한 BaseSqlService 재사용
public class OxmSqlService implements SqlService{
private final BaseSqlService baseSqlService;
private final OxmSqlReader oxmSqlReader;
//디플토 오브젝트로 만들어진 프로퍼티. 필요에 따라 DI 를 통해 교체 가능.
private SqlRegistry sqlRegistry = new HashMapSqlRegistry();
public OxmSqlService(Unmarshaller unmarshaller) {
this.oxmSqlReader = new OxmSqlReader(unmarshaller);
this.baseSqlService = new BaseSqlService(this.oxmSqlReader, this.sqlRegistry);
}
@PostConstruct
public void loadSql() {
//this.oxmSqlReader.read(this.sqlRegistry);
//SQL 을 등록하는 초기화 작업을 baseSqlService 에게 위임
this.baseSqlService.loadSql();
}
@Override
public String getSql(String key) throws SqlRetrievalFailureException {
// try {
// return this.sqlRegistry.findSql(key);
// } catch (SqlNotFoundException e) {
// throw new SqlRetrievalFailureException(e.getMessage());
// }
//SQL을 찾아오는 작업도 baseSqlService 에게 위임.
return this.baseSqlService.getSql(key);
}
private class OxmSqlReader implements SqlReader {
private Unmarshaller unmarshaller;
private final static String DEFAULT_SQLMAP_FILE = "sqlmap.xml";
private String sqlmapFile = DEFAULT_SQLMAP_FILE;
public OxmSqlReader(Unmarshaller unmarshaller) {
this.unmarshaller = unmarshaller;
}
@Override
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 (JAXBException e) {
throw new IllegalArgumentException(this.sqlmapFile + "을 가져올 수 없습니다.", e);
}
}
}
}
- 위임구조를 활용하여 OxmSqlService 에 있던 중복 코드를 깔끔하게 제거 가능.
- OxmSqlService 는 OXM 에 특화된 SqlReader를 멤버로 내장하고 있고 여기에 필요한 설정을 지정할 수 있는 구조. 실제 SqlService의 기능을 구현하는 일은 내부에 BaseSqlService를 만들어서 위임.
리소스 추상화
- 등장 배경
- OxmSqlReader, XmlSqlReader 에서 SQL Mapping 정보가 담긴 XML 파일 이름을 외부에서 지정할 수 있지만 UserDao 클래스와 같은 클래스패스에 존재하는 파일로 제한됨.
- e.g. 파일 시스템이나 웹상의 HTTP를 통해 접근 가능한 파일로 바꾸려면 URL 클래스를 사용하도록 Reader 코드를 변경해야 함.
Resource
- 스프링에서 제공하는 리소스 접근 API 추상화 인터페이스
ResourceLoader
- 문자열로 정의된 리소스를 실제 Resource 타입 오브젝트로 변환
public interface ResourceLoader{
// location 에 담긴 스트링 정보를 바탕으로 적절한 Resource로 변환
Resource getResource(String location);
//...
}
OxmSqlService 에 적용
public class OxmSqlService implements SqlService{
private final BaseSqlService baseSqlService;
private final OxmSqlReader oxmSqlReader;
//디플트 오브젝트로 만들어진 프로퍼티. 필요에 따라 DI 를 통해 교체 가능.
private SqlRegistry sqlRegistry = new HashMapSqlRegistry();
public OxmSqlService(Unmarshaller unmarshaller, Resource sqlmap) {
this.oxmSqlReader = new OxmSqlReader(unmarshaller, sqlmap);
this.baseSqlService = new BaseSqlService(this.oxmSqlReader, this.sqlRegistry);
}
@PostConstruct
public void loadSql() {
//SQL 을 등록하는 초기화 작업을 baseSqlService 에게 위임
this.baseSqlService.loadSql();
}
@Override
public String getSql(String key) throws SqlRetrievalFailureException {
//SQL을 찾아오는 작업도 baseSqlService 에게 위임.
return this.baseSqlService.getSql(key);
}
private class OxmSqlReader implements SqlReader {
private Unmarshaller unmarshaller;
// private final static String DEFAULT_SQLMAP_FILE = "sqlmap.xml";
// private String sqlmapFile = DEFAULT_SQLMAP_FILE;
private Resource sqlmap = new ClassPathResource("sqlmap.xml", UserDao.class);
public OxmSqlReader(Unmarshaller unmarshaller, Resource sqlmap) {
this.unmarshaller = unmarshaller;
this.sqlmap = sqlmap;
}
@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.registerSql(sql.getKey(), sql.getValue());
}
} catch (IOException | JAXBException e) {
throw new IllegalArgumentException(this.sqlmap.getFilename() + "을 가져올 수 없습니다.", e);
}
}
}
}
<bean id="sqlService" class="springbook.user.sqlservice.OxmSqlService">
<property name="sqlMap" value="classpath:springbook/user/dao/sqlmap.xml" />
...
</bean>
- 기존에는 UserDao.class 와 동일한 path에 있는 xml 파일만 읽어 들임
- 이제는 path를 자유롭게 설정할 수 있으며 HTTP 프로토콜과 같이 접근 방법을 다르게 할 수도 있음.
Load a Resource as a String in Spring | Baeldung
인터페이스 상속을 통한 안전한 기능 확장
- 목표
- 서버가 운영 중인 상태에서 서버를 재시작하지 않고 어플리케이션이 사용 중인 SQL을 변경.
- 객체지향 설계를 잘 하는 방법
- DI를 의식하면서 설계. DI를 잘 활용할 수 있는 방법을 생각하면서 오브젝트를 설계
- 항상 확장을 염두에 두고 오브젝트 사이의 관계를 생각
- DI란 결국 미래를 프로그래밍하는 것
- DI를 적용할 때는 가능한 인터페이스를 사용
- 다형성을 얻기 위해
- 인터페이스 분리 원칙 (ISP)
- 목적과 관심이 각기 다른 클라이언트가 있다면 인터페이스를 통해 이를 적절하게 분리
- 장점
- 기존 클라이언트에 영향을 주지 않은 채로 오브젝트의 기능을 확장하거나 수정 가능 -> 인터페이스를 상속해서 기능을 확장하는 경우도 마찬가지
인터페이스 상속
public interface UpdatableSqlRegistry extends SqlRegistry{
public void updateSql(String key, String sql) throws SqlUpdateFailureException;
public void updateSql(Map<String, String> sqlmap) throws SqlUpdateFailureException;
}
<bean id="sqlService" class="springbook.user.sqlservice.BaseSqlService">
...
<property name="sqlRegistry" ref="sqlRegistry" />
</bean>
<bean id="sqlRegistry" class="springbook.user.sqlservice.MyUpdatableSqlRegistry" />
<bean id="sqlAdminService" class="springbook.user.sqlservice.SqlAdminService" />
<property name="updatableSqlRegistry" ref="sqlRegistry" />
...
</bean>
- 그림 7-11 참고
- BaseSqlService와 SqlAdminService는 동일한 오브젝트에 의존하고 있지만 각자의 관심과 필요에 따라서 다른 인터페이스를 통해 접근
- 이렇게 인터페이스를 추가하거나 상속을 통해 확장하는 방식을 잘 활용하면 이미 기존의 인터페이스를 사용하는 클라이언트가 있는 경우에도 유연한 확장이 가능
DI를 이용해 다양한 구현 방법 적용
ConcurrentHashMap 을 이용한 수정 가능 SQL 레지스트리
public class ConcurrentHashMapSqlRegistry implements UpdatableSqlRegistry {
private 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 SqlNotFoundException {
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 SqlUpdateFailureException {
if (sqlMap.get(key) == null) {
throw new SqlUpdateFailureException();
}
sqlMap.put(key, sql);
}
@Override
public void updateSql(Map<String, String> sqlmap) throws SqlUpdateFailureException {
for (Map.Entry<String, String> entry : sqlmap.entrySet()) {
updateSql(entry.getKey(), entry.getValue());
}
}
}
@Bean
public SqlService sqlService() {
SqlService sqlService = new OxmSqlService(unmarshaller(), sqlRegistry());
return sqlService;
}
@Bean
public SqlRegistry sqlRegistry() {
SqlRegistry sqlRegistry = new ConcurrentHashMapSqlRegistry();
return sqlRegistry;
}
내장형 DB 를 이용한 SQL 레지스트리
- 내장형 DB 장점
- 데이터가 메모리에 저장되기 때문에 다른 DB 에 비해 성능 뛰어남
- Map 과 같은 collection에 저장하는 것에 비해 효과적이고 안정적인 방법으로 등록, 수정, 검색 가능
스프링의 내장형 DB 지원 기능
- 내장형 DB 초기화하는 작업을 지원하는 내장형 DB 빌더 제공
- URL, 드라이버 초기화
- 테이블 생성, 초기 데이터 삽입 등 SQL 을 실행시켜 줌
- 모든 준비가 끝나면 DataSource 오브젝트 반환 -> 일반적인 DB처럼 사용 가능
public class EmbeddedDbSqlRegistry implements UpdatableSqlRegistry {
JdbcTemplate jdbc;
public EmbeddedDbSqlRegistry(DataSource dataSource) {
jdbc = new JdbcTemplate(dataSource);
}
@Override
public void registerSql(String key, String sql) {
jdbc.update("insert into sqlmap(key_, sql) values(?,?)", key, sql);
}
@Override
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();
}
}
@Override
public void updateSql(String key, String sql) throws SqlUpdateFailureException {
int affected = jdbc.update("update sqlmap set sql_ = ? where key_ = ?", sql, key);
if (affected == 0) {
throw new SqlUpdateFailureException();
}
}
@Override
public void updateSql(Map<String, String> sqlmap) throws SqlUpdateFailureException {
for (Map.Entry<String, String> entry : sqlmap.entrySet()) {
updateSql(entry.getKey(), entry.getValue());
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<jdbc:embedded-database id="embeddedDatabase" type="HSQL">
<jdbc:script location="classpath:springbook/user/sqlservice/updatable/sqlRegistrySchema.sql"/>
</jdbc:embedded-database>
- jdbc 네임스페이스 선언
- jdbc:embedded-database 태그를 이용해 내장형 DB 등록
- jdbc:script 를 이용해 초기 테이블 생성위해 필요한 SQL 스크립트 지정
트랜잭션 적용
updateSql(Map<String, String> sqlmap)
- sqlmap에 있는 sql을 순차적으로 변경. -> 변경 중 에러가 발생하면 이전 변경 내용을 되돌려야 한다. (롤백 필요)
public class EmbeddedDbSqlRegistry implements UpdatableSqlRegistry {
JdbcTemplate jdbc;
//jdbcTemplate 과 트랜잭션을 동기화해주는 트랜잭션 템플릿.
TransactionTemplate transactionTemplate;
public EmbeddedDbSqlRegistry(DataSource dataSource) {
jdbc = new JdbcTemplate(dataSource);
//datasource로 transactionManager를 만들고 이를 이용해 TransactionTemplate 을 생성
this.transactionTemplate = new TransactionTemplate(
new DataSourceTransactionManager(dataSource)
);
}
@Override
public void registerSql(String key, String sql) {
jdbc.update("insert into sqlmap(key_, sql) values(?,?)", key, sql);
}
@Override
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();
}
}
@Override
public void updateSql(String key, String sql) throws SqlUpdateFailureException {
int affected = jdbc.update("update sqlmap set sql_ = ? where key_ = ?", sql, key);
if (affected == 0) {
throw new SqlUpdateFailureException();
}
}
@Override
public void updateSql(final Map<String, String> sqlmap) throws SqlUpdateFailureException {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
//트랜잭션 경계 안에서 동작할 코드를 콜백 형태로 만들고 TransactionTemplate의 execute() method 에 전달
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
for (Map.Entry<String, String> entry : sqlmap.entrySet()) {
updateSql(entry.getKey(), entry.getValue());
}
}
});
}
}
- SQL registry 라는 제한된 오브젝트 내에서 서비스에 특화된 간단한 트랜잭션이 필요한 것이므로 AOP를 활용하지 않고 트랜잭션 추상화 API를 직접 사용
'SPRING' 카테고리의 다른 글
토비의 스프링 10. 스프링이란 무엇인가 (0) | 2022.09.18 |
---|---|
토비의 스프링 9. 스프링 핵심 기술의 응용 (2) (0) | 2022.09.18 |
토비의 스프링 7. AOP (2) (0) | 2022.09.18 |
토비의 스프링 6. AOP (1) (0) | 2022.05.27 |
토비의 스프링 5. 서비스 추상화 (0) | 2022.05.27 |
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- ec2
- 토비
- 카카오
- 코테
- SOLID
- AOP
- 객체지향
- 예외처리
- c++
- 코딩테스트
- 자바스터디
- 데코레이터패턴
- 자바
- java
- 프록시패턴
- OOP
- 토비의봄TV
- 서비스추상화
- BOJ
- 김영한
- 프로그래머스
- 디자인패턴
- 백기선
- 템플릿콜백
- provider
- 토비의스프링
- gracefulshutdown
- 프록시
- 스프링
- 메서드레퍼런스
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
글 보관함