✅ Java-Configuration 설정을 이용한 분리
책에서는 xml 방식을 통해 DAO 객체와 SQL을 분리하고 있지만, 현재는 잘 쓰이지 않는 방식이기 때문에 Java-Configuration 방식을 사용해서 분리하도록 변경하였습니다.
- TestDaoFactory
@Bean public UserDaoJdbc userDao() { UserDaoJdbc userDaoJdbc = new UserDaoJdbc(); userDaoJdbc.setDataSource(dataSource()); userDaoJdbc.setSqlMap(sqlMap()); //sqlMap Bean 객체 주입 return userDaoJdbc; } //== xml 방식이 아닌 java-configuration 방식을 사용 ==// @Bean public Map<String, String> sqlMap() { Map<String, String> sqlMap = new HashMap<>(); sqlMap.put("add", "insert into users(id, name, password, email, level, login, recommend) values(?,?,?,?,?,?,?)"); sqlMap.put("get", "select * from users where id = ?"); sqlMap.put("getAll", "select * from users order by id"); sqlMap.put("deleteAll", "delete from users"); sqlMap.put("getCount", "select count(*) from users"); sqlMap.put("update", "update users set name = ?, password = ?, email = ?, level = ?, login = ?, recommend = ? where id = ?"); return sqlMap; }
- UserDaoJdbc
public class UserDaoJdbc implements UserDao { private JdbcTemplate jdbcTemplate; private Map<String, String> sqlMap; // setter을 사용한 sqlMap 주입 public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public void setSqlMap(Map<String, String> sqlMap) { this.sqlMap = sqlMap; } //... 생략 @Override public void add(User user) { this.jdbcTemplate.update( this.sqlMap.get("add"), user.getId(), user.getName(), user.getPassword(), user.getEmail(), user.getLevel().intValue(), user.getLogin(), user.getRecommend()); } @Override public Optional<User> get(String id) { try (Stream<User> stream = jdbcTemplate.queryForStream(this.sqlMap.get("get"), this.userMapper, id)) { return stream.findFirst(); } catch (DataAccessException e) { return Optional.empty(); } } @Override public void deleteAll() { this.jdbcTemplate.update(this.sqlMap.get("deleteAll")); } @Override public int getCount() { List<Integer> result = jdbcTemplate.query(this.sqlMap.get("getCount"), (rs, rowNum) -> rs.getInt(1)); return (int) DataAccessUtils.singleResult(result); } @Override public List<User> getAll() { return this.jdbcTemplate.query(this.sqlMap.get("getAll"), this.userMapper ); } @Override public void update(User user) { this.jdbcTemplate.update( this.sqlMap.get("update"), user.getName(), user.getPassword(), user.getEmail(), user.getLevel().intValue(), user.getLogin(), user.getRecommend(), user.getId()); }
👉 Map 타입으로 sql문을 식별할 key와 실제 sql 쿼리문을 가진 value로 구성되어 UserDaoJdbc에 주입함으로써 UserDaoJdbc에서 더 이상의 쿼리문이 보이지 않게 되었습니다. SQL과 DAO를 분리함으로써 SQL을 따로 관리할 수 있기 때문에 유지 보수에 용이합니다. 이는 책에서 진행한 XML 방식도 유사합니다. test는 잘 수행되었습니다.
✅ SQL 서비스 제공
스프링 설정파일 안에 SQL을 두고 이를 DI해서 DAO가 사용하게 손쉽게 SQL을 분리해낼 수 있었습니다. 하지만 SQL과 DI가 섞여 있기 때문에 지저분하며, SQL 수정, 튜닝 작업하기엔 불편합니다. 따라서 SQL을 편리하게 관리해주는 툴이 있고, 해당 툴에서 SQL 정보가 담긴 파일을 읽어서 관리하는 방법, 내장 DB에 저장해서 가져오는 방법이 더 선호됩니다. 차근차근 책을 따라가며 이를 분리해내는 방법을 알아보겠습니다.
- SqlService
package com.jhcode.spring.ch7.user.slqservice; public interface SqlService { // 런타임 예외를 던지도록 설정한다 String getSql(String key) throws SqlRetrievalFailureException; }
- SqlRetrievalFailureException
package com.jhcode.spring.ch7.user.slqservice; public class SqlRetrievalFailureException extends RuntimeException { public SqlRetrievalFailureException(String message) { super(message); } public SqlRetrievalFailureException(String message, Throwable cause) { super(message, cause); } }
- SimpleSqlService
package com.jhcode.spring.ch7.user.slqservice; import java.util.Map; 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; } } }
지금까지는 Map 타입의 Bean을 등록해서 DAO에 직접 주입하였지만 SqlService interface를 만들고, 이를 구현한 클래스를 통하여 DAO에서 SQL문을
getSql()
메소드를 통해 사용하도록 만듭니다. 이제 SqlService Bean을 생성하고, 관계 설정을 해보겠습니다.
- UserDaoJdbc
public class UserDaoJdbc implements UserDao { private JdbcTemplate jdbcTemplate; // SqlService 타입으로 주입 private SqlService sqlService; public void setSqlService(SqlService sqlService) { this.sqlService = sqlService; } //...생략 @Override public void add(User user) { this.jdbcTemplate.update( this.sqlService.getSql("add"), user.getId(), user.getName(), user.getPassword(), user.getEmail(), user.getLevel().intValue(), user.getLogin(), user.getRecommend()); } @Override public Optional<User> get(String id) { try (Stream<User> stream = jdbcTemplate.queryForStream(this.sqlService.getSql("get"), this.userMapper, id)) { return stream.findFirst(); } catch (DataAccessException e) { return Optional.empty(); } } @Override public void deleteAll() { this.jdbcTemplate.update(this.sqlService.getSql("deleteAll")); } @Override public int getCount() { List<Integer> result = jdbcTemplate.query(this.sqlService.getSql("getCount"), (rs, rowNum) -> rs.getInt(1)); return (int) DataAccessUtils.singleResult(result); } @Override public List<User> getAll() { return this.jdbcTemplate.query(this.sqlService.getSql("getAll"), this.userMapper ); } @Override public void update(User user) { this.jdbcTemplate.update( this.sqlService.getSql("update"), user.getName(), user.getPassword(), user.getEmail(), user.getLevel().intValue(), user.getLogin(), user.getRecommend(), user.getId()); }
더 이상 주입받은 Map 타입의 변수에서
get()
메소드를 통해서 SQL에 맞는 Key로 SQL 문을 찾는 것이 아니라 SqlService를 통해서 SQL문을 찾습니다. 기존의 Map 변수와 setter을 통한 의존성 주입을 제거하고, SqlService 객체를 주입 받도록 설정합니다. 또한 주입 받은 객체를 통해서getSql()
메소드로 알맞은 쿼리를 찾게 합니다.
- TestDaoFactory
@Bean public UserDaoJdbc userDao() { UserDaoJdbc userDaoJdbc = new UserDaoJdbc(); userDaoJdbc.setDataSource(dataSource()); userDaoJdbc.setSqlService(sqlService()); // Map이 아닌 SqlService 주입 return userDaoJdbc; } //== 여전히 SQL을 스프링 Bean에서 설정하지만 UserDao는 SQL이 어디에서 오는 건지 모르게 된다 ==// @Bean public SqlService sqlService() { SimpleSqlService sqlService = new SimpleSqlService(); Map<String, String> sqlMap = new HashMap<>(); sqlMap.put("add", "insert into users(id, name, password, email, level, login, recommend) values(?,?,?,?,?,?,?)"); sqlMap.put("get", "select * from users where id = ?"); sqlMap.put("getAll", "select * from users order by id"); sqlMap.put("deleteAll", "delete from users"); sqlMap.put("getCount", "select count(*) from users"); sqlMap.put("update", "update users set name = ?, password = ?, email = ?, level = ?, login = ?, recommend = ? where id = ?"); sqlService.setSqlMap(sqlMap); return sqlService; }
여기서 왜 굳이 이렇게할까?라고 생각이 들 수도 있습니다. 여전히 SQL문은 스프링 Bean 내부에서 관리하고 있기 때문입니다. 그러나 중요한 것은 더 이상 DAO는 SQL이 어디에 저장되는지 모르게 된다는 것입니다. 즉, 앞으로 우리는 SQL문을 파일 혹은 내장 DB에 저장된 데이터를 읽어와서 관리할 수 있게 만들 수도 있다는 것입니다. 지금은 그 과정에 있습니다. DAO는 단지 SqlService를 통해서 필요한 SQL을 호출할 수 있다는 사실만 알게 된다는 것이 중요합니다.
✅ JAXB(Java Architecture for XML Binding)
Java에서 XML에 담긴 정보를 파일에서 읽어 올때 사용할 API입니다. JAXB의 장점은 XML 문서 정보를 거의 동일한 구조의 오브젝트로 직접 매핑해준다는 것입니다.
책에서는 sqlmap.xsd 스키마 파일을 만들어서 shell이나 도스창에서 명령어를 통해 바인딩용 자바 클래스와 팩토리 클래스를 생성하였습니다. 그렇지만 toby github에서 해당 xsd 스키마 파일을 찾을 수 없어 코드를 보고 직접 구성하였습니다.
- pom.xml
<!-- xml 바인딩 --> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <!-- JAXB API ContextFactory --> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.1</version> </dependency>
xml 바인딩에 필요한 의존성을 추가합니다.
- ObjectFactory
package com.kitec.springframe.ch7.study2_1.springframe.sqlservice.jaxb; import javax.xml.bind.annotation.XmlRegistry; /** * This object contains factory methods for each * Java content interface and Java element interface * generated in the springbook.issuetracker.sqlprovider.jaxb package. * <p>An ObjectFactory allows you to programatically * construct new instances of the Java representation * for XML content. The Java representation of XML * content can consist of schema derived interfaces * and classes representing the binding of schema * type definitions, element declarations and model * groups. Factory methods for each of these are * provided in this class. * */ @XmlRegistry public class ObjectFactory { /** * Create a new ObjectFactory that can be used to create new instances of schema derived classes for package: springbook.issuetracker.sqlprovider.jaxb * */ public ObjectFactory() { } /** * Create an instance of {@link SqlType } * */ public SqlType createSqlType() { return new SqlType(); } /** * Create an instance of {@link Sqlmap } * */ public Sqlmap createSqlmap() { return new Sqlmap(); } }
- package-info
// // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vhudson-jaxb-ri-2.1-548 // See <a href="http://java.sun.com/xml/jaxb">http://java.sun.com/xml/jaxb</a> // Any modifications to this file will be lost upon recompilation of the source schema. // Generated on: 2009.08.12 at 05:59:18 ¿ÀÈÄ EST // @javax.xml.bind.annotation.XmlSchema(namespace = "http://www.epril.com/sqlmap", elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED) package com.kitec.springframe.ch7.study2_1.springframe.sqlservice.jaxb;
- Sqlmap
package com.kitec.springframe.ch7.study2_1.springframe.sqlservice.jaxb; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; import java.util.ArrayList; import java.util.List; import javax.xml.bind.annotation.XmlAccessType; /** * <p>Java class for anonymous complex type. * * <p>The following schema fragment specifies the expected content contained within this class. * * <pre> * <complexType> * <complexContent> * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType"> * <sequence> * <element name="sql" type="{http://www.epril.com/sqlmap}sqlType" maxOccurs="unbounded"/> * </sequence> * </restriction> * </complexContent> * </complexType> * </pre> * * */ @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "sqlmapType", propOrder = { "sql" }) @XmlRootElement(name = "sqlmap") public class Sqlmap { @XmlElement(required = true) protected List<SqlType> sql; /** * Gets the value of the sql property. * * <p> * This accessor method returns a reference to the live list, * not a snapshot. Therefore any modification you make to the * returned list will be present inside the JAXB object. * This is why there is not a <CODE>set</CODE> method for the sql property. * * <p> * For example, to add a new item, do as follows: * <pre> * getSql().add(newItem); * </pre> * * * <p> * Objects of the following type(s) are allowed in the list * {@link SqlType } * * */ public List<SqlType> getSql() { if (sql == null) { sql = new ArrayList<SqlType>(); } return this.sql; } }
- SqlType
package com.kitec.springframe.ch7.study2_1.springframe.sqlservice.jaxb; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlType; import javax.xml.bind.annotation.XmlValue; /** * <p>Java class for sqlType complex type. * * <p>The following schema fragment specifies the expected content contained within this class. * * <pre> * <complexType name="sqlType"> * <simpleContent> * <extension base="<http://www.w3.org/2001/XMLSchema>string"> * <attribute name="key" use="required" type="{http://www.w3.org/2001/XMLSchema}string" /> * </extension> * </simpleContent> * </complexType> * </pre> * * */ @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "sqlType", propOrder = { "value" }) public class SqlType { @XmlValue protected String value; @XmlAttribute(required = true) protected String key; /** * Gets the value of the value property. * * @return * possible object is * {@link String } * */ public String getValue() { return value; } /** * Sets the value of the value property. * * @param value * allowed object is * {@link String } * */ public void setValue(String value) { this.value = value; } /** * Gets the value of the key property. * * @return * possible object is * {@link String } * */ public String getKey() { return key; } /** * Sets the value of the key property. * * @param value * allowed object is * {@link String } * */ public void setKey(String value) { this.key = value; } }
👉 위 클래스들은 원래 명령어를 통해 xsd 파일을 컴파일해야 하지만 xsd에 대한 정보가 없어서 직접 코드로 옮겨서 만들었습니다. xml 파일에 대한 정보를 읽어서 자바 클래스로 매핑할 때 필요한 클래스들 입니다.
- ObjectFactory
✅ Unmarshalling
생성된 매핑 클래스를 적용하기 전에 JAXB API의 사용법을 익힐 수 있도록 학습테스트를 진행해보겠습니다.
- 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 ../../../../../sqlmap.xsd "> <sql key="add">insert</sql> <sql key="get">select</sql> <sql key="delete">delete</sql> </sqlmap>
JABX를 테스트할 sqlmap.xml을 작성합니다. ch7.learningtest.jdk.jaxb 패키지 안에서 작성하였습니다.
- JaxbTest
package com.jhcode.spring.ch7.learningtest.jdk.jaxb; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; import java.util.List; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import org.junit.jupiter.api.Test; import com.jhcode.spring.ch7.user.slqservice.jaxb.SqlType; import com.jhcode.spring.ch7.user.slqservice.jaxb.Sqlmap; public class JaxbTest { @Test public void readSqlmap() throws JAXBException, IOException { // 바인딩용 클래스들의 위치, context를 생성할 때 필요하다 String contextPath = Sqlmap.class.getPackage().getName(); // context 생성 JAXBContext context = JAXBContext.newInstance(contextPath); // 언마샬러 생성 Unmarshaller unmarshaller = context.createUnmarshaller(); // xml 파일을 읽어서 언마샬을 진행하면 매핑된 오브젝트를 리턴한다 Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal( getClass().getResourceAsStream("sqlmap.xml")); List<SqlType> sqlList = sqlmap.getSql(); //assertEquals(sqlList.size(), 6); assertEquals(sqlList.get(0).getKey(), "add"); assertEquals(sqlList.get(0).getValue(), "insert"); assertEquals(sqlList.get(1).getKey(), "get"); assertEquals(sqlList.get(1).getValue(), "select"); assertEquals(sqlList.get(2).getKey(), "delete"); assertEquals(sqlList.get(2).getValue(), "delete"); } }
xml 파일을 언마샬리하여 JAXB가 매핑된 클래스로 제대로 바인딩하는지 확인하는 테스트입니다.
📖 토비 스프링 3.1 -p557~573
🚩jhcode33의 toby-spring-study.git으로 이동하기
Uploaded by N2T