티스토리 뷰

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을 읽어 옴.)
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를 직접 사용
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2025/01   »
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
글 보관함