[Spring] 토비의 스프링 Vol.1 7장 스프링 핵심 기술의 응용

Shiba·2023년 9월 22일
0

🍀 스프링 정리

목록 보기
9/21
post-thumbnail

📗 스프링 핵심 기술의 응용

❗ 토비의 스프링 3.1 vol 1 정리입니다.
책을 읽지 않으셨다면 이해가 어려울 수 있습니다!

7장에서는 지금까지 살펴봤던 3가지 기술(DI, 서비스 추상화, AOP)을 활용해 새로운 기능을 만들어보고 이를 통해 스프링의 개발철학과 추구하는 가치, 스프링 사용자에게 요구되는 것에 대해 알아볼 것이다.

📖 SQL과 DAO의 분리

UserDao로 돌아가 SQL과 DAO코드를 분리하는 작업에 도전해볼 것이다.

📝 XML 설정을 이용한 분리

가장 쉽게 생각할 수 있는 방법으로, SQL을 스프링의 XML 설정파일로 빼내는 것이다.

◼ 개별 SQL 프로퍼티 방식

각각의 SQL문장을 프로퍼티로 만들어 XML에서 지정하도록하는 방식

add() 메소드 수정

// add() 메소드를 위한 SQL필드
public class UserDaoJdbc implements UserDao {
	private String sqlAdd;
    
    public void setSqlAdd(String sqlAdd) {
    	this.sqlAdd = sqlAdd;
    }
}

//주입받은 SQL 사용
public void add(User user) {
	this.jdbcTemplate.update(
    	this.sqlAdd,
        user.getId(), user.getName(), user.getPassword(), user.getEmail(),
        user.getLevel().intValue(), user.getLogin(), user.getRecommend());
}

XML설정

<bean id="userDao" class="springbook.user.dao.UserDaoJdbc">
	<property name="dataSource" ref="dataSource" />
    <property name="sqlAdd" value="insert into users(id, name, password,
    	email, level, login, recommend) values(?,?,?,?,?,?,?)" />

◼ SQL 맵 프로퍼티 방식

위의 방식은 SQL이 많아지면 상당히 번거로움
이번에는 SQL을 하나의 컬렉션으로 담아두는 방법을 시도해보자. - 맵을 이용

개별적으로 정의한 프로퍼티는 모두 제거. Map타입의 sqlMap 프로퍼티를 대신 추가

public class UserDaoJdbc implements UserDao {
	...
    private Map<String, String> sqlMap;
    
    public void setSqlMap(Map<String, String> sqlMap) {
    	this.sqlMap = sqlMap;
    }
}

//SQL 맵의 키값을 메소드 이름으로 정함
//sqlMap을 사용하도록 수정한 add()
public void add(User user) {
	this.jdbcTemplate.update(
    	//프로퍼티로 제공받은 맵으로부터 키를 이용해서 필요한 SQL가져옴
    	this.sqlMap.get("add"), 
    	user.getId(), user.getName(), user.getPassword(), user.getEmail(),
        user.getLevel().intValue(), user.getLogin(), user.getRecommend());
}

XML설정

<bean id="userDao" class="springbook.user.dao.UserDaoJdbc">
	<property name="dataSource" ref="dataSource" />
    <property name="sqlMap">
    	<map>
    <entry key="add" value="insert into users(id, name, password,
    	email, level, login, recommend) 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 = ?,
    	email = ?, level = ?, login = ?, recommend = ? where id = ?" />

📝 SQL 제공 서비스

SQL을 DI설정정보와 같이 두는건 바람직하지 못함. 또한, 꼭 SQL을 XML에 담아둘 이유도 없다
스프링의 설정파일로부터 생성된 오브젝트와 정보는 애플리케이션을 다시 시작하기전에는 변경이 매우 어렵다는 점도 문제다.
=> 독립적인 SQL 제공 서비스가 필요하다!

◼ SQL 서비스 인터페이스

먼저 인터페이스를 설계해보자
- SQL서비스의 기능은 키값을 전달하면 그에 해당하는 SQL을 돌려주는 것

인터페이스 작성

package springbook.user.sqlservice;

public interface SqlService {
	//실패하는 경우 SqlRetrievalFailureException 예외 던지기
	String getSql(String key) throws SqlRetrievalFailureException; 
}

SQL조회 실패시 예외 클래스 작성

package springbook.user.sqlservice;
...

public class SqlRetrievalFailureException extends RuntimeException {
	public SqlRetrievalFailureException(String message) {
    	super(message);
    }
    
    public SqlRetrievalFailureException(String message, Throwable cause) {
    	super(message, cause);
    }
}

DI받을 수 있도록 SqlService 프로퍼티 추가

public class UserDaoJdbc implements UserDao {
	...
    private SqlService sqlService;
  	
    public void setSqlService(SqlService sqlService){
    	this.sqlService = sqlService;
    }
}

sqlService를 사용하도록 수정한 UserDao의 메소드들

public void add(User user) {
	this.jdbcTemplate.update(this.sqlService.getSql("userAdd"),
    		user.getId(), user.getName(), user.getPassword(), user.getEmail(),
            user.getLevel().intValue(), user.getLogin(), user.getRecommend());
}

public User get(String id) {
	return this.jdbcTemplate.queryForObject(this.sqlServicegetSql("userGet"),
    		new Object[] {id}, this.userMapper);
}

public List<User> getAll() {
	return this.jdbcTemplate.query(this.sqlService.getSql("userGetAll"),
    	this.userMapper);
}

public void deleteAll() {
	this.jdbcTemplate.update(this.sqlService.getSql("userDeleteAll"));
}

public int getCount() {
	return this.jdbcTemplate.queryForInt(this.sqlService.getSql("userGetCount"));
    
public void update(User user) {
	this.jdbcTemplate.update(this.sqlService.getSql("userUpdate"),
    		user.getName(), user.getPassword(), user.getEmail(),
            user.getLevel().intValue(), user.getLogin(), user.getRecommend()
            user.getId());
}

◼ 스프링 설정을 사용하는 단순 SQL 서비스

가장 간단한 방법으로 SqlService를 구현해보자.

package springbook.user.sqlservice;
...

public class SimpleSqlService implements SqlService {
	private Map<String, String> sqlMap;
    
    pubilc void setSqlMap(Map<String, String> sqlMap) {
    	this.sqlMap = sqlMap;
    }
    
    public String getSql(String key) throws SqlRetrievalFailureException {
    	String sql = sqlMap.get(key);
        if(sql == null) //get()에 실패하면 예외 던지기
        	throw new SqlRetrievalFailureException(key + 
            	"에 대한 SQL을 찾을 수 없습니다");
        else
        	return sql;
    }
}

📖 인터페이스의 분리와 자기참조 빈

📝 XML 파일 매핑

<bean> 태그안에 SQL을 넣는건 바람직하지 않다. 그보다는 SQL을 저장해두는 전용 포멧을 가진 독립적인 파일을 사용하는 편이 바람직하다. 가장 편리한 포맷은 XML이다.

◼ JAXB (Java Architecture for XML Binding)

  • XML 문서정보를 거의 동일한 구조의 오브젝트로 직접 매핑해줌
  • XML 문서의 구조를 정의한 스키마를 이용해서 매핑할 오브젝트의 클래스까지 자동으로 만들어주는 컴파일러 제공

SQL 맵을 위한 스키마 작성과 컴파일

SQL 정보를 담은 <sql> 태그를 가진 XML 문서를 작성하고 그 XML문서의 구조를 정의하는 스키마를 만든 뒤 JAXB로 컴파일 해보자.

맵 XML 문서 작성

<sqlmap>
	<sql key="userAdd">into users(...) ...</sql>
    <sql key="userGet">select * from users ...</sql>
    ...
</sqlmap>

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">
                	//검색을 위한 키 값은 <sql>의 key 애트리뷰트에 넣는다. 필수 값이다.
                	<attribute name="key" use="required" type="string" />
                </extension>
        </simpleContent>
    </complexType>
</schema>

JAXB로 컴파일하기
DOS창(cmd) 프로젝트 루트 폴더로 이동(cd 경로)
다음 명령을 사용해 컴파일

xjc -p springbook.user.sqlservice.생성할 클래스의 패키지 변환할 스키마 파일 이름.xsd -d 생성된 파일이 저장될 위치

위 명령으로 만들어진 XML 문서를 바인딩하기위한 클래스

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "sqlmapType", propOrder = { "sql" })
@XmlRootElement(name = "sqlmap")
public class Sqlmap {
	@XmlElement(required = true)
    //<sql> 태그의 정보를 담은 SqlType오브젝트를 리스트로 가짐
    protected List<SqlType> sql; 
    
    public Lsit<SqlType> getSql() {
    	if (sql == null) {
        	sql = new ArrayList<SqlType>();
        }
        return this.sql;
    }
}

<sql> 태그의 정보를 담을 SqlType 클래스

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "sqlType", propOrder = { "value" })
public class SqlType {
	@XmlValue
    protected String value; //SQL값을 저장할 스트링 타입의 필드
    @XmlAttribute(required = true)
    protected String key; //key애트리뷰트에 담긴 검색용 키값을 위한 스트링 타입의 필드
    
    public String getValue() {
    	return value;
    }
    
    public void setValue(String value) {
    	this.value = value;
    }
    
    public String getKey() {
    	return key;
    }
    
    public void setKey(String value) {
    	this.key = value;
    }
}

언마샬링

JAXB에서 XML문서를 읽어서 자바의 오브젝트로 변환하는 것
마샬링 : 바인딩 오브젝트를 XML문서로 변환하는 것

📝 XML파일을 이용하는 SQL 서비스

앞서 사용한 JAXB를 SqlService에 적용해보자

◼ SQL맵 XML 파일

map과 entry로 만들었던 SQL을 모두 <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="add" value="insert into users(id, name, password,
    	email, level, login, recommend) values(?,?,?,?,?,?,?)" />
    <sql key="get" value="select * from users where id = ?" />
    <sql key="getAll" value="select * from users order by id" />
    <sql key="deleteAll" value="delete from users" />
    <sql key="getCount" value="select count(*) from users" />
    <sql key="update" value="update users set name = ?, password = ?,
    	email = ?, level = ?, login = ?, recommend = ? where id = ?" />
</sqlmap>

◼ XML SQL 서비스

생성자에서 JAXB를 이용해 XML로 된 SQL문서를 읽어들여 맵으로 저장해두었다가, DAO의 요청에 따라 SQL을 찾아서 전달하는 방식으로 SqlService를 구현해보자

package springbook.user.sqlservice;
...

public class XmlSqlService implements SqlService {
	private Map<String, String> sqlMap = new HashMap<String, String>();
    
    public XmlSqlService() {
    	//JAXB API를 이용해 XML문서를 오브젝트 트리로 읽어옴
    	String contextPath = Sqlmap.class.getPackage().getName();
        try{
        	JAXBcontextPath = JAXBContext.newInstance(contextPath);
            Unmarshaller unmarshaller = context.createUnmarshaller();
            //USerDao와 같은 클래스패스의 sqlmap.xml파일을 변환
            InputStream is = UserDao.classgetResourceAsStream("sqlmap.xml");
            Sqlmap sqlmap = (Sqlmap)unmarshaller.unmarshal(is);
            
            //읽어온 SQL을 맵으로 저장
            for(SqlType sql : sqlmap.getSql()) {
            	sqlMap.put(sql.getKey(), sql.getValue());
            }
        } catch (JAXBException e) {
        	throw new RuntimeException(e); //JAXBException은 복구 불가능 예외
        }
    }
    
    public String getSql(String key) throws SqlRetrievalFailureException {
    	String sql = sqlMap.get(key);
        if(sql == null) //get()에 실패하면 예외 던지기
        	throw new SqlRetrievalFailureException(key + 
            	"에 대한 SQL을 찾을 수 없습니다");
        else
        	return sql;
    }
}

이제 SQL문장을 스프링의 빈 설정에서 완벽하게 분리하는 데 성공했다. DAO로직이나 파라미터가 바뀌지않는 한 SQL내용을 변경하더라도 애플리케이션의 코드나 DI설정은 전혀 수정할 필요가 없어짐.

📝 빈의 초기화 작업

위 코드의 몇 가지 개선점이 존재한다

  • 생성자에서 예외가 발생할 수도 있는 복잡한 초기화 작업을 다루는건 좋지 않음.
    - 초기상태를 가진 오브젝트를 만들어두고 별도의 초기화 메소드 사용
  • 읽어들일 파일의 위치와 이름이 고정되어있음
    - 변경가능성이 존재한다면 외부에서 DI하도록 만드는게 바람직

파일 이름을 외부에서 지정할 수 있도록 프로퍼티 추가

private String sqlmapFile;

public void setSqlmapFile(String sqlmapFile){
	this.sqlmapFile = sqlmapFile;
}

생성자에서 진행하던 작업을 별도의 초기화 메소드로 만들어 옮기기

public void loadSql() {
	String contextPath = Sqlmap.class.getPackage().getName();
    try {
    	...
        //프로퍼티로 설정을 통해 제공받은 파일이름 사용 
        InputStream is = UserDao.class.getResourceAsStream(this.sqlmapFile);
        ...
    }
}

@PostConstruct

초기화 작업을 수행할 메소드에 부여해주면 스프링은 빈의 오브젝트를 생성하고 DI작업을 마친 뒤, @PostConstruct가 붙은 메소드를 자동실행한다

public class XmlSqlService implements SqlService {
	...
    @PostConstruct //빈의 초기화 메소드로 지정
    public void loadSql() { ... }
}

sqlmapFile 프로퍼티의 값을 sqlService 빈의 설정에 넣어주기

<bean id="sqlService" class="springbook.user.sqlservice.XmlSqlService">
	<property name="sqlmapFile" value="sqlmap.xml" />
</bean>

이제 UserDaoTest를 돌리면 애플리케이션 컨택스트는 다음과 같이 동작한다

📝 변화를 위한 준비: 인터페이스 분리

현재 XmlSqlServiceXML이 아닌 다른 포맷의 파일에서 SQL을 읽어올 수 없음
- 해당 기능을 구현하기 위해서는 완전히 새로운 클래스를 만들어야한다...
- SQL을 가져오는 것과 보관해두고 사용하는 것은 서로 다른 관심


=> 관심이 다른 코드를 분리하고 유연하게 확장가능하도록 DI를 적용해보자!

◼ 책임이 따른 인터페이스 정의

먼저 관심을 서로 분리해보자!
1. SQL 정보를 외부에서 읽어옴
2. 읽어온 SQL을 보관해두고 있다가 필요할 때 제공

  • SqlService 서비스 오브젝트 구조

SqlReader가 읽어온 SQL정보를 SqlRegistry가 저장해야하는데 어떻게 전달해야할까?

  • SqlReader에서 온 정보를 SqlService가 받아 맵으로 만든 뒤 SqlRegistry에게 전달
//SqlService 구현 클래스 코드
Map<String, String> sqls = sqlReader.readSql(); //Map이라는 구체적 전송타입 강제
sqlRegistry.addSqls(sqls);

- 둘 사이에 정보 전달을 위해 Map형식을 만들어야한다는건 불편하다
  - 받아온 SQL정보를 다시 Map형식으로 포장하여 전달해주어야하는 번거로움

  • 발상의 전환! SqlService가 굳이 중간에 존재할 필요가 없으니 SqlReader에게 SqlRegistry 전략을 제공해주어 SqlRegistry에 저장하라고 요청하기
//SqlService코드 변경
sqlReader.readSql(sqlRegistry);

//등록 기능을 제공하는 SqlRegistry 메소드
interface SqlRegistry {
	//SqlReader는 읽어들인 SQL을 레지스트리에 저장
	void registerSql(String key, String sql); 
    ...
}

- 불필요하게 SqlService 코드를 통해 특정 포맷으로 변환한 SQL정보를 주고받을 필요가 없음

◼ SqlRegistry 인터페이스

SqlRegistry 인터페이스를 작성해보자.

package springbook.user.sqlservice;
...
public interface SqlRegistry {
	void registerSql(String key, String sql); //SQL을 키와 함께 등록
    
    String findSql(String key) throws SqlNotFoundException; //키로 SQL검색 실패시 예외
}

◼ SqlReader 인터페이스

SqlReader 인터페이스를 작성해보자.

public interface SqlReader {
	void read(SqlRegistry sqlRegistry); //SQL을 외부에서 가져와 SqlRegistry에 등록
    //예외가 발생할 수 있지만, 대부분 복구 불가능 예외라서 예외선언을 하지않음
}

📝 자기참조 빈으로 시작하기

◼ 다중인터페이스 구현과 간접참조

SqlService 구현클래스는 앞서 만든 SqlReader, SqlRegistry를 DI받을 수 있는 구조여야한다. 인터페이스가 총 3개이므로, 인터페이스를 구현한 클래스 3개가 있어야 한다.

  • 클래스는 인터페이스에 대해서만 알고있고 인터페이스를 통해서만 의존 오브젝트에 접근한다.
    - 오브젝트가 어디서 만들어진 것인지는 알 필요가 없다

=> 그렇다면 세 개의 인터페이스를 하나의 클래스가 모두 구현한다면 어떨까?
- 인터페이스 타입의 상속을 통한 다형성을 활용해보자

◼ 인터페이스를 이용한 분리

먼저, SqlReader와 SqlRegistry를 DI받을 수 있도록 프로퍼티를 정의하자

public class XmlSqlService implements SqlService {
	//DI받을 수 있도록 인터페이스 타입 프로퍼티 선언
	private SqlReader sqlReader;
    private SqlRegistry sqlRegistry;
    
    public void setSqlReader(SqlReader sqlReader) {
    	this.sqlReader = sqlReader;
    }
    
    public void setSqlRegistry(SqlRegistry sqlRegistry) {
    	this.sqlRegistry = sqlRegistry;
    }
}

앞서 만든 HashMap을 사용하여 키를 검색하는 코드는 유지하되, SqlRegistry를 구현하는 메소드를 만들어두자

public class XmlSqlService implements SqlService, 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 SqlNotFoundException(key +
        	"에 대한 SQL을 찾을 수 없습니다");
        else return sql;
    }
    
    public void registerSql(String key, String sql) {
    	sqlMap.put(key, sql);
    }
    ...
} 

❗ sqlMap은 SqlRegistry 구현의 일부가 됐으므로 SqlRegistry 구현 메소드가 아닌 곳에서는 사용하면 안된다.

  이제 SqlReader를 구현해보자. 어떻게 읽어오는지는 SqlReader의 메소드 뒤로 숨기고, 어떻게 저장해둘지는 SqlRegistry 타입 오브젝트가 알아서 처리하도록 코드를 변경하자

public class XmlSqlService implements SqlService, SqlRegistry, 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()) {
            	//독립적인 인터페이스 메소드를 통해 읽어들인 SQL과 키를 전달한다
            	sqlRegistry.registerSql(sql.getKey(), sql.getValue()); 
            }
        } catch (JAXBException e) {
        	throw new RuntimeException(e);
        }
    }
}

마지막으로 SqlService 구현을 마무리하자!

public class XmlSqlService implements SqlService, SqlRegistry, SqlReader {
	...
    @PostConstruct
    public void loadSql() {
    	this.sqlReader.read(this.sqlRegistry);
    }
    
    //SqlService인터페이스 메소드
    public String getSql(String key) throws SqlRetrievalFailureException {
    	try {
        	return this.sqlRegistry.findSql(key);
        }
        catch (SqlNotFoundException e) {
        	throw new SqlRetrievalFailureException(e);
        }
    }
}

◼ 자기참조 빈 설정

이제 빈 설정을 통해 실제 DI가 일어나도록 해야 한다
- 클래스도 빈도 1개이지만 마치 3개의 빈이 등록된 것처럼 수행되어야한다

<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가능하도록 만들었다! 이제 이를 완전히 분리해두고 DI로 조합해 사용하게 만드는 단계이다

◼ 확장가능한 기반 클래스

SqlRegistry와 SqlReader를 이용하는 가장 간단한 Sql구현 클래스를 만들어보자

package springbook.user.sqlService;
...
public class BaseSqlService implements SqlService {
	//상속을 통해 확장하기때문에 protected로 접근자 변경
	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);
        }
    }
}

SqlRegistry를 구현했던 코드를 독립 클래스로 분리하자

package springbook.user.sqlservice;
...
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 SqlNotFoundException(key + "에 대한 SQL을 찾을 수 없습니다");
        else return sql;
    }
    
    public void registerSql(String key, String sql) {sqlMap.put(key, sql);}
}

SqlReader도 독립 클래스로 만들어두자

public class XmlSqlService 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()) {
            	//독립적인 인터페이스 메소드를 통해 읽어들인 SQL과 키를 전달한다
            	sqlRegistry.registerSql(sql.getKey(), sql.getValue()); 
            }
        } catch (JAXBException e) {
        	throw new RuntimeException(e);
        }
    }
}

클래스를 분리했으니 빈 설정도 수정해야한다

<bean id="sqlService" class="springbook.user.sqlservice.BaseSqlService">
	<property name="sqlReader" ref="sqlReader" /> //프로퍼티는 자기자신 참조가능
    <property name="sqlRegistry" ref="sqlRegistry" /> //프로퍼티는 자기자신 참조가능
</bean>

<bean>
	<property name="sqlmapFile" ref="sqlmap.xml" />
</bean>

<bean id="sqlRegistry" class="spring.user.sqlservice.HashMapSqlRegistery">
</bean>

◼ 디폴트 의존관계를 갖는 빈 만들기

디폴트 의존관계란?

외부에서 DI 받지 않는 경우 기본적으로 자동 적용되는 의존관계

DI설정이 없는 경우 디폴트로 적용하고 싶은 의존 오브젝트를 생성자에서 넣어준다

package springbook.user.sqlservice;
...

public class DefaultSqlService extends BaseSqlService {
	public DefaultSqlService() {
    	setSqlReader(new JaxbXmlSqlReader());
        setSqlRegistry(new HashMapSqlRegistry());
    }
}

디폴트 의존관계의 빈 설정

<bean id="sqlService" class="springbook.user.sqlservice.DefaultSqlService" />

수정 후, 테스트를 돌리면 모두 실패한다...
이유는 바로 DefaultSqlService내부에 생성하는 JaxbXmlSqlReader의 sqlmapFile 프로퍼티가 비어있기 때문이다

이 문제를 해결하려면 어떻게 해야할까?
두 가지 방법이 있다

  • sqlmapFile을 DefaultSqlService의 프로퍼티로 정의하는 방법
    • DefaultSqlService에 적용하기에는 바람직하지 않음.
      - JaxbXmlSqlReader는 디폴트 의존 오브젝트에 불과(사용할수도 안할수도 있음). 따라서 JaxbXmlSqlReader에서 사용되는 sqlmapFile을 프로퍼티로 등록하는건 바람직하지 않음


      => JaxbXmlSqlReader에 sqlmapFile을 디폴트 의존 오브젝트로 선언하는 방법

JaxbXmlSqlReader에 디폴트 값 선언

public class JaxbXmlSqlReader implements SqlReader {
	private static final String DEFAULT_SQLMAP_FILE = "sqlmap.xml";
    
    private String sqlmapFile = DEFAULT_SQLMAP_FILE;
    
    public void setSqlmapFile(String sqlmapFile) { this.sqlmapFile = sqlmapFile; }
}

디폴트 오브젝트의 단점

설정을 통해 다른 구현 오브젝트를 사용하게 해도 생성자에서 일단 디폴트 의존 오브젝트를 만들어버림

📖 서비스 추상화 적용

JaxbXmlSqlReader는 좀 더 개선하고 발전시킬 부분이 존재

  • 자바는 JAXB이외에도 다양한 XML과 자바오브젝트를 매핑하는 기술 존재
    - 다른 기술로 손쉽게 변환 가능해야함
  • XML파일을 좀 더 다양한 소스에서 가져올 수 있게 만든다
    - 현재는 클래스패스 안에서만 XML을 읽어옴. 이것을 파일시스템 또는 http프로토콜을 통해 원격으로 가져올 수는 없을까?

📝 OXM 서비스 추상화

◼ OXM (Object XML Mapping)

XML과 자바 오브젝트를 매핑해서 상호 변환해주는 기술

JAXB이외에도 실전에서 자주 사용되는 XML과 자바오브젝트 매핑 기술(OXM) 존재

  • Castor XML : 설정 파일이 필요 없는 인트로스펙션 모드를 지원하기도 하는 매우 간결하고 가벼운 바인딩 프레임워크
  • JiBX : 뛰어난 퍼포먼스를 자랑하는 XML 바인딩 기술
  • XmlBeans : 아파치 XML 프로젝트의 한 종류. XML의 정보셋을 효과적으로 제공
  • XStream : 관례를 이용해서 설정이 없는 바인딩을 지원하는 XML 바인딩 기술의 한 종류.

OXM 프레임 워크와 기술들은 기능면에서 상호 호환성이 있다.
- DB에서 했던 것처럼 서비스 추상화를 사용할 수 있지 않을까?

◼ OXM 서비스 인터페이스

스프링은 OXM 추상화 서비스 인터페이스 제공
- Marshaller와 Unmarshaller 존재 (SqlReader는 Unmarshaller 사용)

Unmarshaller 인터페이스

package org.springframework.oxm;
...
import javax.xml.transform.Source;

public interface Unmarshaller {
	boolean supports(Class<?> clazz);
    
    Object unmarshal(Source source) throws IOException, XmlMappingException;
}

테스트는 생략!

📝 OXM 서비스 추상화 적용

스프링의 OXM 추상화 기능을 이용하는 OXMSqlService를 만들어보자.
SqlReader는 스프링의 OXM 언마샬러를 이용하도록 OXMSqlService 내에 고정해야한다.(SQL을 읽는 방법을 OXM으로 제한함으로서 사용성을 극대화)

◼ 멤버 클래스를 참조하는 통합 클래스

OxmSqlService는 BaseSqlService와 유사하게 SqlReader 타입의 의존 오브젝트를 사용하되 이를 스태틱 멤버 클래스로 내장하고 자신만이 사용할 수 있도록 만들어보자
(의존 오브젝트를 자신만이 사용하도록 독점하는 구조)

OxmSqlService 기본 구조

package springbook.user.sqlservice;
...
public class OxmSqlService implements SqlService {
	private final OxmSqlReader oxmSqlReader = new OxmSqlReader();
    ...
    
    private class OxmSqlReader implements SqlReader { //private 멤버 클래스로 정의
    	...
    }
}

스프링의 OXM 서비스 추상화를 사용하면 언 마샬러를 빈으로 등록해야한다
- 기능이 늘어날때마다 SqlService를 위해 등록한 빈은 늘어남

  • 빈 갯수를 줄이고 단순하게 하는 방법
    • BaseSqlService를 확장해서 디폴트 설정을 두는 방법
      - 디폴트로 내부에서 만드는 오브젝트의 프로퍼티를 외부에서 지정해주기 힘듦

=> 하나의 빈 설정만으로 SqlService와 SqlReader의 필요한 프로퍼티 설정이 모두 가능하도록 하기 (강한 결합 구조로 만들기)

OxmSqlReader외부에 노출되지 않아서 OxmSqlService에 의해서만 만들어지고, 스스로 빈으로 등록될 수 없음.
- 자신이 DI로 제공받아야하는 정보가 있다면 OxmSqlService를 통해 간접적으로 받아야함

public class OxmSqlService implements SqlService {
	private final OxmSqlReader oxmSqlReader = new OxmSqlReader();
    ...
    
    //OxmSqlService가 받은 것을 그대로 멤버 클래스의 오브젝트에 전달
    public void setUnmarshaller(Unmarshaller unmarshaller) { 
    	this.oxmSqlReader.setUnmarshaller(unmarshaller);
    }
    
    //OxmSqlService가 받은 것을 그대로 멤버 클래스의 오브젝트에 전달
    public void setSqlmapFile(String sqlmapFile) {
    	this.oxmSqlReader.setSqlmapFile(sqlmapFile);
    }
    ...
    
    private class OxmSqlReader implements SqlReader{
    	private Unmarshaller unmarshaller;
        private String sqlmapFile;
        //setter 메소드 생략
        ...
    }
}

완성된 OxmSqlService 클래스

public class OxmSqlService implements SqlService {
	private final OxmSqlReader oxmSqlReader = new OxmSqlReader();
    
    // 디폴트 오브젝트로 만들어진 프로퍼티. DI로 교체 가능
    private SqlRegistry sqlRegistry = new HashMapSqlRegistry();
    
    public void setSqlRegistry(SqlRegistry sqlRegistry) {
    	this.sqlRegistry = sqlRegistry;
    }
    
    public void setUnmarshaller(Unmarshaller unmarshaller) { 
    	this.oxmSqlReader.setUnmarshaller(unmarshaller);
    }
    
    public void setSqlmapFile(String sqlmapFile) {
    	this.oxmSqlReader.setSqlmapFile(sqlmapFile);
    }
    
    //BaseSqlService와 같음
    @PostConstruct
    public void loadSql() {
    	this.oxmSqlReader.read(this.sqlRegistry);
    }
    
    //SqlService인터페이스 메소드
    public String getSql(String key) throws SqlRetrievalFailureException {
    	try {
        	return this.sqlRegistry.findSql(key);
        }
        catch (SqlNotFoundException e) {
        	throw new SqlRetrievalFailureException(e);
        }
    }
    
    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));
                // 전달받은 OXM 인터페이스 구현 오브젝트를 가지고 언 마샬링 작업 수행
                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);
            }
        }
    }
}

◼ 위임을 이용한 BaseSqlService의 재사용

OxmSqlService의 문제점이 존재한다

  • loadSql(), getSql()이라는 SqlService의 핵심 메소드 구현 코드가 BaseSqlService와 동일
    - 그리 복잡한 코드가 아니기 때문에 넘겨도 상관은 없음
    하지만 코드의 양이 많고, 변경이 자주일어난다면 매번 양쪽을 함께 변경해야함
    => 미래를 대비한다는 의미에서 중복된 코드를 제거할 방법을 생각해보자.

OxmSqlService의 외향적인 틀은 유지한 채로 SqlService의 기능구현은 BaseService로 위임하는 구조로 만들자
- 두개의 빈을 등록하는건 불편하다.
=> 하나의 클래스로 묶는 방법을 생각해보자

  • OxmSqlService가 SqlReader를 내장
  • 실제 SqlReader와 SqlService를 이용해 SqlService를 구현하는 일은 BaseSqlService가 수행

BaseSqlService로 SqlService의 일을 위임해보자

public class OxmSqlService implements SqlService {
	private final BaseSqlService baseSqlService = new BaseSqlService();
    ...
    
    //BaseSqlService에 위임하기
    @PostConstruct
    public void loadSql() {
    	//실제로 일을 수행할 baseSqlService에 DI해주기
    	this.baseSqlService.setSqlReader(this.sqlReader);
        this.baseSqlService.setSqlRegistry(this.sqlRegistry);
        
        this.baseSqlService.loadSql(); //초기화 작업을 baseSqlService에 위임
    }
    
    //SqlService인터페이스 메소드
    public String getSql(String key) throws SqlRetrievalFailureException {
    	//SQL을 찾아오는 작업도 baseSqlService에 위임
    	return this.baseSqlService.getSql(key); 
    }
    ...
}

📝 리소스 추상화

지금까지 만든 OxmSqlReader나 XmlSqlReader에 존재하는 공통적인 문제점

  • Xml파일 이름을 외부에서 지정할 수는 있지만 범위가 클래스패스에 존재하는 파일로 제한
    - 자바에는 다양한 위치에 존재하는 리소스에 대해 단일화된 접근 인터페이스를 제공해주는 클래스가 없다.
    -- URL을 통해 웹상의 리소스에 접근하는 java.net.URL클래스가 존재
    -- 기존에 사용하던 getResourceAsStream() 사용하기
    => 목적은 동일하지만 사용법이 다른 기술이 존재한다고 볼수 있으니 서비스 추상화를 적용할 수 있지 않을까?

◼ 리소스

자바에 존재하는 일관성 없는 리소스 접근 API를 추상화해서 Resource라는 추상화 인터페이스 정의

package org.springframework.core.io;
...

public interface Resource extends InputStreamSource {
	//리소스의 존재나 읽기 가능한지 여부를 확인가능
	boolean exist();
    boolean isReadable();
    boolean isOpen();
    
    URL getURL() throws IOException;
    URL getURL() throws IOException;
    File getFile() throws IOException;

    Resource createRelative(String relativePath) throws IOExcepton;

    long lastModified() throws IOException;
    String getFilename();
    String getDescription();
}

public interface InputStreamSource {
	//모든 리소스는 InputStream형태로 가져옴
	InputStream getInputStream() throws IOException; 
}

스프링의 거의 모든 API는 외부의 리소스 정보가 필요할 때는 항상 이 Resource 추상화를 이용
- 다른 추상화와 달리 Resource는 빈이 아닌 값으로 취급
-- 추상화를 적용하는 방법이 문제

◼ 리소스 로더

스프링에는 URL클래스와 유사하게 접두어를 이용해 Resource 오브젝트를 선언하는 방법 존재
- 문자열 안의 리소스의 종류와 리소스의 위치를 함께 표현하게 해주면
실제 Resource 타입 오브젝트로 변환해주는 ResourceLoader제공
(ResourceLoader의 대표적인 예 : 애플리케이션 컨택스트)

ResourceLoader 인터페이스

pakcage org.springframework.core.io;

public interface ResourceLoader {
	//location에 담긴 스트링 정보 바탕으로 그에 적절한 Resource로 변환
	Resource getResource(String location); 
    ...
}

ResourceLoader가 처리하는 접두어의 예

접두어설명
file:file:C:/temp/file.txt파일 시스템의 C:/temp 폴더에 있는 file.txt를 리소스로 만들어준다.
classpath:classpath:file.txt클래스패스의 루트에 존재하는 file.txt 리소스에 접근하게 해준다.
없음WEB-INF/test.dat접두어가 없는 경우에는 ResourceLoader구현에 따라 리소스의 위치가 결정. ServletResourceLoader라면 서블릿 컨텍스트의 루트를 기준으로 해석.
http:http://www.myserver.com/test.datHTTP프로토콜을 사용해 접근할 수 있는 웹상의 리소스를 지정. ttp:도 사용가능.

◼ Resource를 이용해 XML파일 가져오기

OxmSqlService에 Resource를 적용해서 SQL매핑정보가 담긴 파일을 다양한 위치에서 가져올 수 있도록 만들어보자.

스트링으로 되어있던 sqlmapFile 프로퍼티를 모두 Resource타입으로 바꾼 뒤, 이름을 sqlmap으로 변경한다. 그 후, StreamSource클래스를 이용해 Source타입으로 만들어주는 코드를 작성해보자.

public class OxmSqlService implements SqlService 
{
	public void setSqlmap(Resource sqlmap) {
    	this.oxmSqlReader.setSqlmap(sqlmap);
    }
    ...
    
    private class OxmSqlReader implements SqlReader{
    	//Resource 구현 클래스인 ClassPathResource 사용
    	private Resource sqlmap = new ClassPathResource("sqlmap.xml",
        	UserDao.class);
        
        public void setSqlmap(String sqlmap) {
        	this.sqlmap = sqlmap;
        }
        
        public void read(SqlRegistry sqlRegistry) {
        	try{
            	//리소스 종류에 상관없이 스트림으로 들고올 수 있음.
            	Source source = new StreamSource(sqlmap.InputStream());
                
                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.sqlmap.getFilename() +
                	"을 가져올 수 없습니다.", e);
            }
        }
    }
}

Resource 오브젝트가 실제 오브젝트가 아니라는 점을 주의!
단지 추상화된 핸들러일 뿐. 오브젝트가 만들어져도 실제로는 리소스가 존재하지 않을 수 있음.

📖 인터페이스 상속을 통한 안전한 기능확장

애플리케이션을 재시작하지 않고 특정 SQL의 내용만을 변경하고 싶다면 어떻게 해야할 지 생각해보자.

📝 DI와 기능의 확장

지금까지 적용한 DI는 일종의 디자인 패턴의 관점이였다. 이는 DI를 바르게 활용하고 있다고 볼 수는 없다.

◼ DI를 의식하는 설계

DI의 가치를 제대로 얻기 위해 DI에 적합한 오브젝트 설계가 필요하다.

책에서 추천하는 한가지 방식은 DI를 의식하면서 설계하는 방식이다.

  • DI를 적용하려면 최소한 두 개 이상의, 의존관계를 가지고 서로 협력해서 일하는 오브젝트가 필요하다.
  • 오브젝트를 적절한 책임에 따라 분리해주어야한다.
  • 항상 의존 오브젝트는 자유롭게 확장될 수 있다는 점을 염두에 두어야 한다.

    "DI는 미래를 프로그래밍 하는 것이다."

◼ DI와 인터페이스 프로그래밍

DI를 적용할 때에는 가능한 인터페이스를 사용하게 해야 한다.

인터페이스를 사용하는 이유

  • 다형성을 얻기 위함.
    - 다양한 목적을 위해 인터페이스를 이용한 다형성이 활용됨
  • 인터페이스 분리 원칙(Interface Separate Principle)을 통해 클라이언트와 의존 오브젝트 사이의 관계가 명확하게 해줄 수 있음
    - 하나의 오브젝트가 여러 인터페이스를 구현할 수 있으므로 각기 다른 관심과 목적을 가지고 어떤 오브젝트에 의존하고 있을 수 있음.
    (B1과는 연관되어있고, B2와는 연관이 없는 A가 B1,B2를 둘다 구현한 클래스 B에 직접 의존할 필요는 없음)
    - 모든 클라이언트가 자신의 관심에 따른 접근 방식을 불필요한 간섭없이 유지가능

📖 DI를 이용해 다양한 구현방법 적용하기

📝 ConcurrentHashMap을 이용한 수정 가능 SQL 레지스트리

기존에 사용하던 HashMap은 멀티쓰레드 환경에서 동시성 문제가 존재.
- Collections.synchronizedMap()해주면 되지만 성능이 저하됨
=> ConcurrentHashMap을 사용하는 방법 권장

◼ 수정 가능 SQL 레지스트리 테스트

ConcurrentHashMap을 이용해 UpdatableSqlRegistry를 구현해보자.

ConcurrentHashMap을 이용한 SQL 레지스트리 테스트

public class ConcurrentHashMapSqlRegistryTest {
	UpdatableSqlRegistry sqlRegistry;
    
    @Before
    public void setUp() {
    	sqlRegistry = new ConcurrentHashMapSqlRegistry();
        //각 테스트 메소드에서 사용할 초기 SQL정보 미리 등록
        sqlRegistry.registerSql("KEY1", "SQL1");
        sqlRegistry.registerSql("KEY2", "SQL2");
        sqlRegistry.registerSql("KEY3", "SQL3");
    }
    
    @Test
    public void find() {
    	checkFindResult("SQL1", "SQL2", "SQL3");
    }
    
    private void checkFindResult(String expected1, String expected2, String
    		expected3) {
    	assertThat(sqlRegistry.findSql("KEY1"), is(expected1));
        assertThat(sqlRegistry.findSql("KEY2"), is(expected2));
        assertThat(sqlRegistry.findSql("KEY3"), is(expected3));
    }
    
    //주어진 키에 해당하는 SQL이 없을 떄, 예외 발생 여부 체크
    @Test(expected= SqlNotFoundException.class)
    public void unknownKey() {
    	sqlRegistry.findSql("SQL9999!@#$");
    }
    
    //하나의 SQL을 변경하는 기능에 대한 테스트
    @Test
    public void updateSingle() {
    	sqlRegistry.updateSql("KEY2", "Modified2");
        checkFindResult("SQL1", "Modified2", "SQL3");
    }
    
    @Test
    public void updateMulti() {
    	Map<String, String> sqlmap = new HashMap<String, String>();
        sqlmap.put("KEY1", "Modified1");
        sqlmap.put("KEY3", "Modified3");
        
        sqlRegistry.updateSql(sqlmap);
        checkFindResult("Modified1", "SQL2", "Modified3");
    }
    
    //존재하지 않는 키의 SQL을 변경하려고할 때 예외 발생 여부 체크
    @Test(expected= SqlUpdateFailureException.class)
    public void updateWithNotExistingKey() {
    	sqlRegistry.updateSql("SQL9999!@#$", "Modified2");
    }
}

◼ 수정 가능 SQL레지스트리 구현

이제 테스트를 모두 성공시키도록 하는 ConcurrentHashMap코드를 작성해보자

public class ConcurrentHashMapSqlRegistry implements UpdatableSqlRegistry {
	private Map<String, String> sqlMap = new ConcurrentHashMap<String, String>();
    
    public STring findSql(String key) throws SqlNotFoundException {
    	String sql = sqlMap.get(key);
        if (sql == null) throw new SqlNotFoundException(key +
        	"를 이용해서 SQL을 찾을 수 없습니다");
       	else return sql;
    }
    
    public void registerSql(String key, String sql) { sqlMap.put(key, sql); }
    
    public void registerSql(String key, String sql) throws
    		SqlUpdateFailureException {
        if (sqlMap.get(key) == null) {
        	throw new SqlUpdateFailureException(key +
        	"를 이용해서 SQL을 찾을 수 없습니다");
        } 
        
        sqlMap.put(key, sql);
    }
    
    public void updateSql(Map<String, String> sqlmap) throws
    		SqlUpdateFailureException {
    	for(Map.Entry<String,String> entry : sqlmap.entrySet()) {
        	updateSql(entry.getKey(), entry.getValue());
        }        
    }
}

📝 내장형 데이터베이스를 이용한 SQL 레지스트리 만들기

이번엔 ConcurrentHashMap대산 내장형 DB를 이용해 SQL을 저장하고 수정하도록 만들어보자.
- ConcurrentHashMap은 잦은 조회와 변경이 일어나는 환경에서는 한계가 존재
- 내장형 DB란 애플리케이션에 내장되어 함께 시작되고 종료되는 DB

◼ 스프링의 내장형 DB 지원 기능

자바에서 많이 사용되는 내장형 데이터베이스는 Derby,HSQL,H2를 꼽을 수 있다.
- 모두 JDBC 드라이버를 제공하고 표준 DB와 호환되는 기능을 제공
- 스프링은 내장형 DB를 초기화하는 작업을 지원하는 내장형 DB 빌더를 제공
내장형 DB는 직접 DB 종료를 요청할 수도 있어야 함
- 스프링은 인터페이스를 상속하여 shutdown() 메소드를 추가한 인터페이스 제공

◼ 내장형 DB 빌더 테스트

내장형 DB 지원 기능이 어떻게 동작하는지 보기 위한 학습테스트를 만들어보자!

테이플 생성 SQL 스크립트

//schema.sql
CREATE TABLE SQLMAP(
	KEY_VARCHAR(100) PRIMARY KEY,
    SQL_VARCHAR(100) NOT NULL
);

초기 데이터 등록 SQL

//data.sql
INSERT INTO SQLMAP(KEY_, SQL_) values('KEY1', 'SQL1');
INSERT INTO SQLMAP(KEY_, SQL_) values('KEY2', 'SQL2');

내장형 DB 실행 시 위의 두 스크립트가 실행되어야 한다.

스프링의 제공하는 내장형 DB 빌더 EmbeddedDatabaseBuilderdml 사용법을 알아보자.

new EmbeddedDatabaseBuilder()
	.setType(내장형DB종류) //HSQL, Derby, H2 중 1택
    .addScript(초기화에 사용할 DB스크립트의 리소스) //SQL초기화 문장을 담은 스크립트의 위치
    ...
    .build(); //주어진 조건에 맞는 내장형 DB를 준비하고 초기화 스크립트를 모두 실행 
    		 //그 후, 이에 접근할 수 있는 EmbeddedDatabase를 돌려줌

위 내용을 토대로 학습 테스트를 살펴보도록 하자

package springbook.learningtest.spring.embeddeddb;
import static
	org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.HSQL;
...
public class EmbeddedDbTest {
	EmbeddedDatabase db;
    SimpleJdbcTemplate template; //JdbcTemplate를 더 편리하게 사용할 수 있게 확장한 템플릿
    
    @Before
    public void setUp() {
    	db = new EmbeddedDatabseBuilder()
        .setType(HSQL)
        .addScript("classpath:/springbook/learningtest/spring/embeddeddb/schema.sql")
        .addScript("classpath:/springbook/learningtest/spring/embeddeddb/data.sql")
        .build();
        
        template = new SimpleJdbcTemplate(db);
    }
    
    @After
    public void tearDown() {
    	db.shutdown();
    }
    
    @Test
    publci void intitData() {
    	assertThat(template.queryForInt("select count(*) from sqlmap"), is(2));
        
        List<Map<String, Object>> list = template.queryForList("select * from sqlmap order by key_");
        assertThat((String)list.get(0).get("key_"), is("KEY1"));
        assertThat((String)list.get(0).get("sql_"), is("SQL1"));
        assertThat((String)list.get(1).get("key_"), is("KEY2"));
        assertThat((String)list.get(1).get("sql_"), is("SQL2"));
    }
    
    @Test
    public void insert() {
    	template.update("insert into sqlmap(key_, sql_) values(?,?)", "KEY3",
        	"SQL3");
            
        assertThat(template.queryForInt("select count(*) from sqlmap"), is(3));
    }
}

◼ 내장형 DB를 이용한 SqlRegistry 만들기

EmbeddedDatabaseBuilder는 직접 빈으로 등록한다고 바로 사용불가
- 적절한 메소드를 호출해주는 초기화 코드가 필요
- 초기화 코드는 팩토리 빈으로 만드는 것이 좋으나 번거로움
=> 스프링은 팩토리 빈을 만드는 작업을 대신 해주는 전용 태그가 존재
(jdbc 스키마에 정의)

HSQL을 사용한다면 다음과 같이 정의

<jdbc:embedded-database id="embeddedDatabase" type="HSQL">
  <jdbc:script location="classpath:schema.sql"/>
</jdbc:embedded-database>

내장형 DB의 DataSource를 DI받아서 UpdatableSqlRegistry를 구현

package springbook.issuetracker.sqlservice.updatable;
...
public class EmbeddedDbSqlRegistry implements UpdatableSqlRegistry {
	SimpleJdbcTemplate jdbc;
    
    public void setDataSource(DataSource dataSource) {
    	jdbc = new SimpleJdbcTemplate(dataSource); // DI
    }
    
    public void registerSql(String key, String sql) {
    	jdbc.update("insert into sqlmap(key_, sql_) values(?,?)", key, sql);
    }
    
    public String findSql(String key) throws SqlNotFoundExceptioin {
    	try {
        	return jdbc.queryForObject("select sql_ from sqlmap where key_?", 
            	String.class, key);
        } catch(EmptyResultDataAccessException e) {
        	throw new SqlNotFoundException(key +
            	"에 해당하는 SQL을 찾을 수 없습니다", e);
        }
    }
    
    public void updateSql(String key, String sql) throws
    		SqlUpdateFailureException {
    	int affected = jdbc.update("update sqlmap set sql_= ? where key_=?",
        	sql, key);
        if(affedted == 0){
        	throw new SqlUpdateFailureException(ket +
            	"에 해당하는 SQL을 찾을 수 없습니다");
        }
    }
    
    public void updateSql(Map<String, String> sqlmap) throws
    SqlUpdateFailureException {
    	for(Map.Entry<String, String> entry : sqlmap.entrySet()) {
        	updateSql(entry.getKey(), entry.getValue());
        }
    }
}

◼ UpdatableSqlRegistry 테스트 코드의 재사용

위 코드를 테스트하기 위해서 앞서 작성한 구조가 유사한 ConcurrentHashMap의 테스트 코드를 공유하는 방법, 상속으로 작성해보자.

앞서 작성한 테스트 코드에서 의존하는 부분은

public class ConcurrentHashMapSqlRegistryTest {
	UpdatableSqlRegistry sqlRegistry;
    
    @Before
    public void setUp() {
    	sqlRegistry = new ConcurrentHashMapSqlRegistry(); <<= 특정 클래스에 의존

저 부분을 분리한다면 나머지 테스트 코드는 모두 공유 가능하다! 따라서, 바뀌는 부분을 별도의 메소드로 분리하고, 테스트 코드를 추상메소드로 전환해보자

public abstract class AbstractUpdatableSqlRegistryTest {
	UpdatableSqlRegistry sqlRegistry;
    
    @Before
    public void setUp() {
    	sqlRegistry = createUpdatableSqlRegistry();
        ...
    }
    
    //테스트 픽스처를 생성하는 부분만 추상메소드로 만들어두고 서브클래스에서 구현하도록 함
    abstract protected UpdatableSqlRegistry createUpdatableSqlRegistry();
    
    protected void checkFind(String expected1, String expected2, String 
    	expected3) {
       	
        ...
    }
    
    @Test
    public void find() {
    	...
    }
    
    //나머지 테스트 메소드 모두 생략
    
}

기존의 ConcurrentHashMapSqlRegistryTest는 위 추상 클래스를 상속해서 추상 메소드인 createUpdatableSqlRegistry()를 다음과 같이 구현

public class ConcurrentHashMapSqlRegistryTest extends AbstractUpdatableSqlRegistry
		Test{
    protected UPdatableSqlRegistry createUpdatableSqlRegistry() {
    	return new ConcurrentHashMapSqlRegistry();
    }
}

위 클래스 안에는 @Test가 붙은 메소드가 하나도 보이지 않지만 슈퍼클래스인 AbstractUpdatableSqlRegistry의 메소드를 모두 상속받아서 자신의 테스트로 활용

이제 위 방법대로 EmbeddedDbSqlRegistry에 대한 테스트를 만들어보자

//테이블 생성 SQL 스크립트 이름을 sqlRegistrySchema로 변경

package springbook.user.sqlservice.updatable;
...
public class EmbeddedDbSqlRegistryTest extends AbstractUpdatableSqlRegistryTest {
	EmbeddedDatabase db;
    
    protected UpdatableSqlRegistry createUpdatableSqlRegistry() {
    	db = new EmbeddedDatabseBuilder()
        	.setType(HSQL).addScript(
            "classpath:springbook/user/sqlservice/updatable/sqlRegistrySchema.sql")
            .build();
            
        EmbeddedDbSqlRegistry embeddedDbSqlRegistry = new
        	EmbeddedDbSqlRegistry();
        embeddedDbSqlRegistry.setDataSource(db);
        
        return embeddedDbSqlRegistry;
    }
    
    @After
    public void tearDown() {
    	db.shutdown();
    }
}

◼ XML 설정을 통한 내장형 DB의 생성과 적용

SqlService에 새롭게 만든 EmbeddedDbSqlRegistry를 적용해보자

XML설정

//jdbc 네임스페이스 선언
<beans xmlns="http://www.springframework.org/schema/beans"
       ...
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xsi:schemaLocation="http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
                           http://www.springframework.org/schema/jdbc
          ...">
  
//내장형 DB 등록
<jdbc:embedded-database id="embeddedDatabase" type="HSQL">
  <jdbc:script location=
       "classpath:springbook/user/sqlservice/updatable/sqlRegistryschema.sql"/>
</jdbc:embedded-database>
  
//EmbeddedDbSqlReistry 클래스를 이용한 빈 등록
<bean id="sqlService" class="springbook.user.sqlservice.OxmSqlService">
	<property name="unmarshaller" ref="unmarshaller" /> 
    <property name="sqlRegistry" ref="sqlRegistry" /> 
</bean>

<bean id="sqlRegistry" 
      class="spring.user.sqlservice.updatable.EmbeddedDbSqlRegistery">
  	<property name-"dataSource" ref="embeddedDatabase" />
</bean>

📝 트랜잭션 적용

EmbeddedSqlRegistry는 하나 이상의 SQL을 맵으로 전달받아 한 번에 수정해야 하는 경우심각한 문제가 발생할 수도 있다.
- 따라서, 여러 개의 SQL을 수정할 때에는 트랜잭션 안에서 수정이 일어나야 한다.
- HsahMap과 달리 내장형 DB는 트랜잭션 적용이 쉽다.

◼ 다중 SQL 수정에 대한 트랜잭션 테스트

트랜잭션이 적용되면 성공, 아니라면 실패하는 테스트를 만들자.
- 현재의 EmbeddedDbSqlRegistry코드가 테스트 조건을 만족하지 못해 실패함.
- 트랜잭션 기능을 추가해 테스트를 성공하도록 만들면 됨.

트랜잭션 기능을 점검하는 테스트 추가

public class EmbeddedDbSqlRegistryTest extends AbstractUpdatableSqlRegistryTest {
	...
    @Test
    public void transactionalUpdate() {
    	checkFind("SQL1", "SQL2", "SQL3");
        
        Map<String, String> sqlmap = new HashMap<String, String>();
        sqlmap.put("KEY1", "Modified1");
        sqlmap.put("KEY9999!@#$","Modified9999");
        
        try {
        	sqlRegistry.updateSql(sqlmap);
            fail();
        }
        catch(SqlUpdateFailureException e) {}
        
        // 트랜잭션이 롤백되므로 원래 상태로 돌아와야함
        checkFind("SQL1", "SQL2", "SQL3"); 
    }
}

◼ 코드를 이용한 트랜잭션 적용

테스트가 실패하는 것을 확인했다면 이제 테스트가 성공하도록 트랜잭션 기능을 추가해보자
- 여기서는 TransactionTemplate을 사용할 것이다.

public class EmbeddedDbSqlRegistry implements UpdatableSqlRegistry {
	SimpleJdbcTemplate jdbc;
    TransactionTemplate transactionTemplate;
    
    public void setDataSource(DataSource dataSource) {
    	jdbc = new SimpleJdbcTemplate(dataSource);
        transactionTemplate = new TransactionTemplate(
        					new DataSourceTransactionManager(dataSource));
    }
    
    ...
    
   	public void updateSql(final Map<String, String> sqlmap) throws
    		SqlUpdateFailureException {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        	//트랜잭션 경계 안에서 동작할 코드를 콜백 형태로 만들어 excute()에 전달
        	protected void doInTransactionWithoutResult(TransactionStatus status) {
            	for(Map.Entry<String, String> entry : sqlmap.entrySet()) {
                	updateSql(entry.getKey(), entry.getValue());
                }
            }
        });
    }   
}

📖 스프링 3.1의 DI

스프링 프레임워크 자체도 DI 원칙을 충실하게 따라서 만들어졌기 때문에 기존 설계와 코드에 영향을 주지 않고도 꾸준히 새로운 기능의 추가,확장이 가능했다

자바 언어의 변화와 스프링

자바 언어의 변화가 스프링 사용방식에도 영향을 주었다

🔷 애노테이션의 메타정보 활용

애노테이션

@Special 
public class MyClass {
	...
}
  • 장점
    • 애노테이션을 사용하는 것 만으로 다양한 부가정보를 획득할 수 있다
    • 리팩토링시 패키지나 클래스이름 변경이 있어도 자동으로 변경됨
  • 단점
    • 변경할 때마다 매번 클래스를 새로 컴파일해야함

XML

<x:special target="type" class="com.mycompany.myproject.MyClass" />
  • 장점
    • 어느 환경에서나 손쉽게 편집이 가능
    • 내용 수정이 있어도 빌드를 거칠 필요가 없음
  • 단점
    • 모든 부가정보를 명시적으로 작성해야함
    • 텍스트로 작성되어있어 리팩토링시 번거롭고, 오타가 발생하기 쉬움

🔷 정책과 관례를 이용한 프로그래밍

미리 정의한 규칙을 따라서 프레임워크가 작업을 수행
- 작성해야할 내용이 줄어듦
- 정책을 제대로 기억하고 사용해야한다.

책에서는 XML로 설정했던 부분을 모두 애노테이션으로 바꾸는 작업을 진행함!
더 자세한 내용을 알고싶으신 분들은 책을 보시는걸 추천드립니다!

profile
모르는 것 정리하기

0개의 댓글