토비의 스프링 | 7장 스프링 핵심 기술의 응용 (SQL 쿼리문을 코드에서 분리해 보자)

주싱·2022년 10월 30일
0

토비의 스프링

목록 보기
24/30

토비의 스프링 ‘7장 스프링의 핵심 기술의 응용’을 읽고 정리합니다. 6장까지 개선해왔던 UserDao에서 SQL 쿼리문을 분리하여 손쉽게 유지보수하고 확장가능한 구조를 만들어가는 과정에 집중합니다.

1. 시작하며

지금까지 해온 일

  • 반복적인 JDBC 작업 흐름은 템플릿을 이용해 DAO에서 완벽하게 제거했다.
  • 트랜잭션과 예외처리 작업도 서비스 추상화AOP 등을 이용해 처리했기 때문에 DAO 안에서 모두 제거할 수 있었다.
  • 데이터 엑세스 작업과 둘러싼 다양한 코드를 책임과 성격이 다른 부분끼리 분리하고, 독립적으로 확장하거나 변경할 수 있도록 인터페이스를 이용해 연결하고, DI로 다이나믹하게 관계를 설정할 수 있도록 만들어줬다.

해결해야 할 문제

  • UserDao 클래스에 DB 테이블과 필드 정보를 고스란히 담고 있는 SQL 문장이 있다.
  • 데이터 엑세스 로직은 바뀌지 않더라도 DB의 테이블, 필드 이름과 SQL 문장이 바뀔 수 있다.
  • 현재 동작하고 있는 애플리케이션에서 사용되는 SQL이 정확히 어떤 것인지 알려면 최종 빌드에 사용한 자바 코드를 가져와 일일이 코드를 뒤져봐야 한다.

2. 개별 SQL 프로퍼티 방식

구현개념

  • 각각의 SQL 쿼리문을 스프링의 XML 설정파일로 빼내는 것이다. 스프링은 설정을 이용해 빈에 값을 주입해줄 수 있다.

소스코드

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>

3. SQL 맵 프로퍼티 방식

문제

  • SQL이 필요할 때마다 매번 프로퍼티를 추가하고 DI를 위한 변수와 수정자 메서드를 만들어줘야 한다.
  • SQL이 점점 많아지면 그때마다 DAO에 DI용 프로퍼티를 추가하기가 상당히 귀찮다.

구현개념

  • SQL을 하나의 컬렉션으로 담아두는 방법을 시도해보자.
  • 맵을 이용하면 키 값을 이용해 SQL 문장을 가져올 수 있다. 맵을 이용하면 프로퍼티는 하나만 만들어도 되기 때문에 DAO의 코드는 더 간결해진다.

소스코드

맵 타입의 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>

개선한 점

  • 맵으로 만들어두면 새로운 SQL이 필요할 때 설정에 만 추가해주면 되니 모든 SQL을 일일이 프로퍼티로 등록하는 방법에 비해 작업량도 적고 코드도 간단해서 좋다.

단점

  • 메서드에서 SQL을 가져올 때 문자열로 된 키 값을 사용하기 때문에 오타와 같은 실수가 있어도, 해당 메서드가 실행되기 전에는 오류를 확인하기 힘들다는 단점이 있다.

3. 독립된 SQL 제공 서비스

문제

  • 스프링 설정파일에 SQL과 DI설정정보가 섞여 있어 보기에도 지저분하고 관리하기에도 좋지 않다. 데이터 엑세스 로직의 일부인 SQL 문장을 애플리케이션 구성정보를 가진 설정정보와 함께 두는 건 바람직하지 못하다.

구현개념

  • SQL을 따로 분리해둬야 독립적으로 SQL 문의 리뷰나 SQL 튜닝 작업을 수행하기도 편하다. 이를 위해 독립적인 SQL 제공 서비스가 필요하다.
  • 운영 중에 동적으로 갱신도 가능한 유연하고 확작성이 뛰어난 SQL 서비스를 만들어보자.
  • 단순하게 DAO가 사용할 SQL에 대한 키 값을 전달하면 그에 해당하는 SQL을 돌려주도록 만들어보자. 여러 DAO의 SQL문을 함께 관리할 것이기 때문에 키 이름에 DAO나 도메인 오브젝트 이름을 함께 사용한다.

소스코드

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>

개선된 점

  • 현재 코드와 설정만 놓고 보자면 앞에서 사용했던 방법과 별로 다를 게 없어 보이지만 큰 차이가 있다. 이제 UserDao를 포함한 모든 DAO는 SQL을 어디에 저장해두고 가져오는지에 대해서는 전혀 신경 쓰지 않아도 된다. 구체적인 구현 방법과 기술에 상관없이 SqlService 인터페이스 타입의 빈을 DI 받아서 필요한 SQL을 가져다 쓰기만 하면 된다.
  • 동시에 sqlService 빈에는 DAO에는 전혀 영향을 주지 않은 채로 다양한 방법으로 구현된 SqlService 타입 클래스를 적용할 수 있다.

4. SQL 전용 XML 파일

문제

  • 스프링의 XML 설정파일에서 태그 안에 SQL 정보를 넣어놓고 활용하는 건 좋은 방법이 아니다. 그보다는 SQL을 저장해두는 전용 포멧을 가진 독립적인 파일을 이용하는 편이 바람직하다.

구현개념

  • 검색용 키와 SQL 문장 두 가지를 담을 수 있는 XML 문서를 설계해보고, 이 XML 파일에서 SQL을 읽어뒀다가 DAO에게 제공해주는 SQL 서비스 구현 클래스를 만들어보자.
  • XML에 담긴 정보를 파일에서 읽어오는 다양한 방법 중 JAXB(Java Architecture for XML Binding)를 사용한다. JAXB는 XML의 정보를 그대로 담고 있는 오브젝트 트리 구조를 만들어주기 때문에 XML 정보를 오브젝트처럼 다룰 수 있어 편리하다.

소스코드

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 컴파일러로 컴파일 (아래 주의)

  • 최신 JDK에 포함되어 있지 않음 (JDK 1.8 설치 시 bin 폴더에 존재)
  • 경로등을 정확히 설정하지 않으면 까다로운 문제가 발생함
  • 아래의 각 옵션을 명확히 이해하고 사용할 필요가 있음
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;
        }
    }
}

개선한 점

  • 이제 SQL 문을 스프링의 빈 설정에서 완벽하게 분리하는 데 성공했다.
  • 독자적인 스키마를 갖는 깔끔한 XML 문서이므로 작성하고 검증하기에도 편리하고, 필요하다면 다른 툴에서도 불러서 사용할 수 있다. DBA에 의한 SQL 리뷰나 튜닝이 필요하다면 sqlmap.xml 파일만 제공해주면 된다.

5. 빈 초기화 적용

문제

  • SQL 문을 담고 있는 XML 파일 경로가 고정되어 있다.
  • 생성자에서 예외가 발생할 수 있는 복잡한 초기화 작업을 다루는 것은 좋지 않다. 객체를 생성하는 중에 발생하는 예외는 다루기 힘들고, 상속하기 불편하며, 보안에도 문제가 생길 수 있다.

구현 개념

  • 초기 상태를 가진 빈을 만든 후에 @PostConstruct 어노테이션을 사용해 빈 후처리기에 의해 빈 초기화를 수행하도록 한다.
  • 코드의 로직과 여타 이유로 바뀔 가능성이 있는 XML 설정 파일은 외부 DI로 설정해 줄 수 있게 하자.

소스코드

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>

6. 인터페이스 분리

문제

  • XmlSqlService는 특정 포맷의 XML에서 SQL 데이터를 가져오는 방법 그리고 이를 HashMap 타입의 맵 객체에 저장하는 방법, 두 가지 서로 다른 관심사가 내부 구현에 고정되어 있다. 그래서 각각에 변경이 생기면 지금까지 만든 코드를 직접 고치거나 새로 만들어야 한다.

구현개념

  • 서로 변하는 시기와 성질이 다른 것, 변하는 것과 변하지 않는 것을 함께 두는 것은 바람직한 구조가 이니다. 서로 관심이 다른 코드들을 분리하고, 서로 코드에 영향을 주지 않으면서 유연하게 확장 가능하도록 DI를 적용해 보자.
  • 기존의 SqlService를 SQL 정보를 외부의 리소스로부터 읽어오는 역할을 하는 SqlReader와 읽어온 SQL을 보관해두고 있다가 클라이언트의 요청이 있으면 제공해주는 역할을 하는 SqlRegistry 두 인터페이스로 분리한다.
  • SqlService가 SqlReader에서 가져온 SQL 데이터를 SqlRegistry로 어떻게 전달할 것인가를 고민해 본다면 간단히는 다음과 같이 할 수 있다. 그러나 이 둘 사이에 일시적으로 정보를 전달하기 위해 Map 타입의 형식을 갖도록 만들어야 한다는 건 불편한다. 대신 SqlReader에게 SqlRegistry라는 전략을 제공해 주면서 직접 저장하라고 요청하는 편이 낫겠다.
    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);
}

7. 자기참조 빈으로 시작하기

구현개념

  • 굳이 DI를 적용하지 않더라도 자신이 사용하는 객체의 클래스가 어떤 것인지 알지 못하게 만드는 것이 좋다. 그래야 구현 클래스를 바꾸고 의존 객체를 변경해서 자유롭게 확장할 기회를 얻을 수 있다. 클래스의 코드는 단지 인터페이스에 대해서만 알고 있고, 인터페이스를 통해서만 의존 객체에 접근한다.
  • 책임에 따라 분리되지 않았던 XmlSqlService 클래스를 일단 세분화된 책임을 정의한 인터페이스를 구현하도록 만들 것이다. 그래서 같은 클래스의 코드이지만 책임이 다른 코드는 직접 접근하지 않고 인터페이스를 통해 간접적으로 사용하는 코드로 벼경할 것이다.

소스코드

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>

8. BaseSqlService

구현개념

  • 이제 SqlReader와 SqlRegistry를 완전히 분리해서 구현해 보자.
  • 가장 기본이 되는 골격을 가진 기본 클래스인 BaseSqlService를 구현해 보자.

소스코드

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>

9. DefaultSqlService

문제

  • 유연성을 보장하려면 지금 까지 만들어온 구조가 꼭 필요하지만 반대로 적어도 3개의 빈을 등록해줘야 한다는 점이 귀찮게 느껴지기도 한다. 이렇게 확장을 고려해서 기능을 분리하고, 인터페이스와 전략 패턴을 도입하고, DI를 적용한다면 늘어난 클래스와 인터페이스 구현과 의존관계 설정에 대한 부담은 감수해야 한다.

구현개념

  • 특정 의존 객체가 대부분의 환경에서 거의 디폴트라고 해도 좋을 만큼 기본적으로 사용될 가능성이 있다면, 디폴트 의존관계를 갖는 빈을 만드는 것을 고려해볼 필요가 있다.
  • 이를 위해 외부에서 DI 받지 않은 경우 기본적으로 자동 적용되는 의존관계를 부여하는 것이다.
  • 구현의 핵심은 SqlServce를 직접 구현하는 것이 아니라 BaseSqlService를 상속받아서 구현하는 것이다. 그래야 필요에 따라 DI를 받을 수도 있고 그렇지 않은 경우는 디폴트 의존 관계를 주입해줄 수 있다.

소스코드

디폴트 의존관계를 생성자에서 주입하는 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>

더 생각해 볼 문제

  • 디폴트 의존관계를 주입해주기 때문에 편리함을 얻을 수 있지만 필요에 따라 의존관계를 주입받는 경우 불필요한 디폴트 의존객체가 생성되는 부분이 조금 꺼림칙하긴 하다.
  • 이럴 때는 생성자 대신 @PostConstruct에서 주입된 의존객체가 있는지 확인 후 없을 경우에만 디폴트 의존객체를 생성해 주도록 할 수도 있다.

10. OXM 서비스 추상화

문제

  • 자바에는 JAXB 외에도 다양한 XML과 자바오브젝트를 매핑하는 기술이 있다. 필요에 따라 다른 기술로 손쉽게 바꿔서 사용할 수 있는 추상화가 필요하다.

구현개념

  • XML과 자바오브젝트를 매핑해서 상호 변환해주는 기술을 간단히 OXM(Object-XML Mapping)이라고 한다.
  • 스프링은 트랜잭션, 메일 전송뿐 아니라 OXM에 대해서도 서비스 추상화 기능을 제공한다. 스프링이 제공하는 OXM 추상화 서비스 인터페이스에는 자바오브젝트를 XML로 변환하는 Marshaller와 반대로 XML을 자바오브젝트로 변환하는 Unmarshaller가 있다.
  • Unmarshaller 인터페이스는 아주 간단하다. XML 파일에 대한 정보를 담은 Source 타입의 오브젝트를 주면, 설정에서 지정한 OXM 기술을 이용해 자바오브젝트 트리로 변환하고, 루트 오브젝트를 돌려준다.

소스코드

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);
    }

11. 리소스 추상화

문제

  • XML 파일을 좀 더 다양한 소스에서 가져올 수 있게 만든다. 현재는 UserDao 클래스와 같은 클래스패스 안에서만 XML을 읽어올 수 있다. 이것을 임의의 클래스패스나 파일 시스템상의 절대위치 또는 HTTP 프로토콜을 통해 원격에서 가져오도록 확장할 수 있도록 한다.

구현개념

  • 스프링은 자바에 존재하는 일관성 없는 리로스 접근 API를 추상화해서 Resource라는 추상화 인터페이스를 정의했다.
  • Resource는 스프링에서 빈이 아니라 값으로 취급된다.

소스코드

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>
profile
소프트웨어 엔지니어, 일상

0개의 댓글