본문 바로가기
챕터정리방

[7장] 스프링 핵심 기술의 응용

by jaee_ 2022. 1. 16.

XML 설정을 이용한 분리

SQL문을 스프링의 XML 설정파일로 빼내는 방법

SQL Map 프로퍼티 방식

  • Map을 이용하여 Key 값으로 value에 해당하는 SQL 문장을 가져도오록 한다.
// UserDaoJdbc.java
public class UserDaoJdbc implements UserDao {
    ...
    private Map<String,String> sqlMap;

    public void setSqlMap(Map<String,String> sqlMap) {
        this.sqlMap = sqlMap;
    }
    ...
    public void add(User user) {
		this.jdbcTemplate.update(
                // 프로퍼티로 제공받은 맵으로부터 key 값을 이용해 필요한 SQL가져오기
				this.sqlMap.get("add"), 
					user.getId(), user.getName(), user.getPassword(), user.getEmail(), 
					user.getLevel().intValue(), user.getLogin(), user.getRecommend());
	}
}
<!--  applicationContext.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 = ?"  />
        </map>
    </property>
</bean>

 

  • map형식으로 등록할 때는 스프링이 제공해주는 <map> 태그를 사용한다.
  • <entry> 태그를 사용하여 키와 값을 담는다.
  • Map 으로 프로퍼티를 만들어 사용하면 <entry> 만 추가하면 되니 비교적 간단하다.

--> 문제점

  • 데이터 액세스 로직의 일부인 SQL과 애플리케이션 구성정보를 가진 DI설정정보가 섞여있다.
  • 꼭 xml아니고 다른 포맷 파일에 저장해둘 수 있으면 사용/관리 편함
  • sql이 스프링 설정파일로부터 생성되면 runtime에서 변경이 어렵다.

SQL 제공 서비스 - SQL 서비스 인터페이스

  • xml을 통해 가져오면 애플리케이션을 다시 시작하기 전에는 변경이 어렵다.
  • sql을 db에 담아두거나 리모트 등 외부시스템에서 가져올 수도 있다. 어떤 이슈가 있는지는 모르겠다.
  • SQL맵 오브젝트를 변경할 수 있지만 DAO가 싱글톤이라 실시간으로 접근해서 변경하는건 쉽지 않다.
  • SQL 을 독립적으로 분리하여 확장성이 뛰어난 SQLSservice 인터페이스를 만들어보자.

Sqlservice 인터페이스

public interface Sqlservice {
    String getSql(String key) throw SqlRetrievalFailureException;
}

조회 실패 시 예외

public class SqlRetrievalFailureException extends RuntimeException {
    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;
    }

    ...

    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 class SimpleSqlService implements SqlService {
    private Map<String,String> sqlMap;

    public void setSqlMap(Map<String,String> sqlMap) {
        this.sqlMap = sqlMap;
    }

    public String getSql(String key) throws SqlRetruesvalFailureException {
        String sql = sqlMap.get(key);
        if(sql = null) {
            throw new SqlRetruesvalFailureException(key + "에 대한 SQL을 찾을 수 없습니다.");
        } else {
            return sql;
        }
    }
}
<!--  applicationContext.xml -->
<bean id="userDao" class="springbook.user.dao.UserDaoJdbc">
    <property name="dataSource" ref="dataSource" />
    <property name="sqlService" ref="sqlService" />
</bean>

<bean id="sqlService" class="springbook.user.sqlService.sqlService">
    <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 = ?"  />
        </map>
    </property>
</bean>

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

sql을 저장해두는 독립적인 파일을 이용하자.

  • JAXB는 xml에 담긴 정보를 파일에서 읽어오는 방법 중 하나이다.
    • xml 정보를 오브젝트처럼 다룰 수 있어 편리하다.
      언마샬링(unmarchalling) : XML to 자바 오브젝트

      마샬링 : 자바 오브젝트 to XML

sql맵 xml과 sql 맵을 위한 스키마를 jaxb 컴파일러로 컴파일하면, 바인딩용 클래스가 생성된다. 언제 JAXB를 사용해 XML문서를 가져올까? DAO가 sql 요청할 때마다 매번 xml파일을 다시 읽는건 비효율적인 방법이다.


한 번 읽은건 어딘가에 저장해두고 DAO에서 요청이 올 때 사용해야한다. 우선은 생성자에서 SQL을 읽어와 내부에 저장해두는 초기작업을 해보자.

public class XmlSqlService implements SqlService {
    private Map<String, String> sqlMap = new HashMap<String, String>(); // 읽어온 SQL을 저장해둘 맵

    public XmlSqlService() {    // 생성자에서 xml읽어오기
        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);
            for(SqlType sql : sqlmap.getSql()) {
                sqlMap.put(sql.getKey(), sql.getValue()); // 읽어온 SQL을 맵으로 저장해둔다.
            }
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
    }
    ...

}

빈의 초기화 작업

  • 위 코드의 문제점 및 개선방법
    • 생성자에서 발생하는 예외는 다루기 힘들다, 상속하기 불편, 보안에도 문제가 생길 수 있다.
      • 생성자 대신에 loadSql()처럼 초기화 메서드를 사용하고, 파일이름과 위치는 외부에서 DI로 설정하게끔 수정
      • @PostConstruct 애노테이션을 통해 초기화 메서드로 지정해줄 수 있다.
        • 생성자와는 달리 프로퍼티까지 모두 준비된 후에 실행되게끔 해주므로, XmlSqlServicesqlmapFile프로퍼티 값이 주입되고 나서 해당 xml파일을 읽는 로직이 수행될 수 있음.
    • 읽어들일 파일 위치와 이름이 코드에 고정되어 있다.
      • DI를 통해 파일이름을 주입받을 수 있도록 수정한다.

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

  • 개선이 필요한 점
    • SQL을 가져오는 방법이 XML 방식에만 종속되어 있다. 다른 포맷에서 SQL을 읽어오게 하려면 SmlSqlService를 다 고쳐야 한다.
    • 가져온 정보를 Map 형식이 아닌 다른 형식으로 저장하고 가져오려면 다시 만들어야 한다.
  • 해결책 (관심사를 나눠서 독립적인 책임을 갖도록 하자)
    1. SQL 정보를 외부 리소스에서 읽어오는 책임
    2. 읽어온 SQL을 보관해두고 있다가 필요할 때 제공해주는 책임
    3. 한 번 가져온 SQL을 필요에 따라 수정할 수 있게 하는 책임

image

위 그림에선 SqlReader가 읽어오는 정보는 SqlRegistry로 전달해서 등록되게 해야한다.

  • 그림에서의 방식은 SqlService를 거치는 형태이다.
    • SqlReader에게 SqlRegistry 전략 오브젝트를 전달해서 저장하라고 요청하도록 수정하는 것이 좋다.
sqlReader.readSql(sqlRegistry);

image

자기참조 빈으로 구현을 시작하기

  • 책임에 따라 분리되지 않았던 XmlSqlService 클래스
    → 세분화된 책임을 정의한 인터페이스(SqlReader, SqlService, SqlRegistry)를 구현하게 하자.
  • XmlSqlService클래스 하나가 세 개의 인터페이스를 다 구현하게 구현.
    • 단, 책임이 다른 코드는 다른 클래스의 코드이지만 직접 접근하지 않고 인터페이스를 통해 간접적으로 사용하게 변경한다.

자신을 참조하는 sqlService 빈 설정

    <!-- sql service -->
    <bean id="sqlService" class="springbook.user.sqlservice.XmlSqlService">
        <property name="sqlReader" ref="sqlService" />
        <property name="sqlRegistry" ref="sqlService" />
        <property name="sqlmapFile" value="sqlmap.xml" />
    </bean>
  • sqlService를 하나만 선언했으므로 실제 빈 옵젝트도 한 개만 만들어진다.
  • 스프링은 프로퍼티의 ref에 자시자신을 넣는 것을 허용한다.
  • 자기참조 빈은 확장이 힘들고 변경에 취약한 구조의 클래스를 유연한 구조로 만들려고 할 때 처음 시도할 수 있는 방법이다.
    • 이를 통해 기존의 복잡하게 얽혀 있던 코드를 책임을 가진 단위로 구분해낼 수 있다.

디폴트 의존관계

// SqlService 구현체
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;
    }

    @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 구현체
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 SqlRetrievalFailureException(key + "를 이용해서 SQL을 찾을 수 없습니다");
        else return sql;
    }

    public void registerSql(String key, String sql) { sqlMap.put(key, sql);    }
}
// SqlReader 구현체
public class JaxbXmlSqlReader implements SqlReader {
    private String sqlmapFile;

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

    public void read(SqlRegistry sqlRegistry) {
        String contextPath = Sqlmap.class.getPackage().getName(); 
        ...
    }         
}
  • 위의 자기참조 빈에서 독립적인 빈으로 나누었다.
  • 이렇게 빈을 나눠놓으면 클래스가 늘어나고 의존관계 설정도 다 해줘야하는 부담이 있다.
    • 특정 의존 오브젝트가 기본으로 사용된다면 디폴트 의존관계를 갖는 빈을 만들어보자.
      • 외부에서 DI받지 않는 경우 자동 적용되도록.

디폴트 의존관계란 외부에서 DI 받지 않는 경우 기본적으로 자동 적용되는 의존관계를 말한다. 다음 코드는 디폴트 의존 오브젝트를 갖는 DefaultSqlService 이다.

public class DefaultSqlService extends BaseSqlService{
    public DefaultSqlService() { 
        // 생성자에서 자신이 사용할 디폴트 의존 오브젝트를 스스로 DI.
        setSqlReader(new JaxbXmlSqlReader()); 
        setSqlRegistry(new HashMapSqlRegistry());
    }
}

이와 같이 사용한다면 프로퍼티의 값도 다음과 같이 명료하게 변경할 수 있다.

    <!-- sql service -->
    <bean id="sqlService" class="springbook.user.sqlservice.DefaultSqlService">
    </bean>

하지만 다음과 같이 변경했을 경우 문제점이 하나 있다. 바로 JaxbXmlSqlReader에서 생성하는 sqlmapFile이 비어있기 떄문이다.

그렇다면 어떻게 해야할까?

  1. sqlmapFile을 DefaultSqlService의 프로퍼티로 정의한다.
  2. sqlmapFile도 디폴트 값을 준다.
public class JaxbXmlSqlReader implements SqlReader {
	private final String DEFAULT_SQLMAP_FILE = "sqlmap.xml";
    // 상수값으로 표현함으로써 의도를 명확히한다.
	private String sqlmapFile = DEFAULT_SQLMAP_FILE;

	public void setSqlmapFile(String sqlmapFile) { this.sqlmapFile = sqlmapFile; }
}
  • DI를 사용할 때 자주 사용되는 오브젝트는 디폴트로 설정한 뒤 나중에 사용하고싶은 구현체가 있으면 설정에 프로퍼티를 추가하면 된다.
  • 이 방법의 단점
    • 설정을 통해 다른 구현체를 사용한다고 해도 생성자에서 일단 디폴트 의존 오브젝트를 다 만들어버린다.
    • 사용되지 않는 오브젝트가 만들어지는 것은 이게 복잡하고 무거운 오브젝트일때는 많은 리소스를 소모한다.
      • 이럴 땐 @PostConstruct 초기화 메서드를 이용해 프로퍼티 설정여부를 확인하고 없는 경우에만 디폴트 오브젝트를 만드는 방법을 쓰면 된다.

 


서비스 추상화 적용

JaxbXmlSqlReader를 더 발전시켜보자

  • JAXB외에도 다양한 xml과 자바오브젝트 매핑 기술로 바꿔서 사용할 수 있게 한다.
  • XML파일을 클래스패스 외에도 절대경로, http 프로토콜 등 다양한 소스에서 가져올 수 있게 한다.

OXM 서비스 추상화

OXM은 Object Xml Mapping 의 약자로 XML과 자바 오브젝트를 매핑해서 상호 변환해주는 기술을 말한다.

  • 스프링은 OXM에 대해서도 서비스 추상화 기능을 제공한다.
  • Unmarshaller 인터페이스를 통해 JAXB 외의 로우 레벨의 구체적인 기술과 API에 종속되지 않는 독립적인 코드를 작성할 수 있다.

Unmarshaller OXM 추상화 인터페이스

public interface Unmarshaller {
    boolean supports(Class<?> clazz);

    Object unmarshal(Source source) throws IOException, XmlMappingException;
}

OXM 서비스 추상화 적용

  • OXM 추상화 기능을 이용하는 SqlService를 만들어보자.
    • SqlRegistry는 DI 받을 수 있게 만들지만 SqlReader는 스프링의 언마샬러를 이용하도록 OxmSqlService 내에 고정시켜야한다.
      • why? SQL을 읽는 방법을 OXM으로 제한해서 사용성을 극대화하는 것이 목적이다.
        Copycopy code to clipboard
        // SqlReader를 SqlService안에 포함시켜 하나의 빈으로 등록. 
        // SqlReader 구현을 외부에서 사용 못하도록 제한하고 스스로 최적화된 구조로 만들기
        public class OxmSqlService implements SqlService {
        private final OxmSqlReader oxmSqlReader = new OxmSqlReader();
        private class OxmSqlReader implements SqlReader {
        ...
        }
        }
  • 서비스 추상화를 이용할 때 계속 빈이 늘어나고 반복되는 DI구조가 불편하다.
  • 디폴트 의존 오브젝트는 외부에서 프로퍼티를 지정해주기 어려움이 있다.
    • 하나의 빈 설정만으로 SqlService와 SqlReader의 필요한 프로퍼티 설정이 모두 가능하도록 만든다.
  • OxmSqlReader는 외부에 노출되지 않기 떄문에 OxmSqlService에 의해서만 만들어지고, 스스로 빈으로 등록될 수 없다.
    • 자신이 DI를 통해 제공받아야하는 프로퍼티가 있다면 이를 OxmSqlService의 공개된 프로퍼티를 통해 간접적으로 DI 받아야 한다.

위임을 이용한 BaseSqlService 재사용

  • loadSql()getSql() 의 핵심 메서드 구현 코드가 BaseSqlService와 중복된다.
    loadSql()getSql() 구현 로직은 BaseSqlService에만 두고 OxmSqlService는 설정과 기본 구성을 변경해주기 위한 어댑터 처럼 BaseSqlService 앞에 두기

image

public class OxmSqlService implements SqlService{
    private final BaseSqlService baseSqlService = new BaseSqlService();

    @PostConstruct
    public void loadSql() {
        // OxmSqlService의 프로퍼티를 통해서 초기화된 SqlReader와 SqlREgistry를 실제 작업 대상인 baseSqlService에게 주입한다. 
        this.baseSqlService.setSqlReader(this.oxmSqlReader);
        this.baseSqlService.setSqlRegistry(this.sqlRegistry);

        this.baseSqlService.loadSql();
    }

리소스 추상화

  • java.net.URL 클래스엔 classpath 를 기준으로 리소스를 읽어오는 기능이 존재하지 않는다.
  • 리소스를 가져오면 최종적으로 InputStream 형태로 변경해서 사용하지만, 리소스의 종류와 위치에 따라서 다른 클래스와 메소드를 사용한다 --> 추상화 적용이 필요하다.
  • 스프링은 이런 자바의 일관성없는 리소스 접근에 대해서 Resource 라는 추상화 인터페이스를 제공한다.
  • Resource 는 스프링에서 빈이 아닌 값으로 취급된다.
    • 트랜잭션처럼 서비스를 제공해주는 것이 아닌 단순히 정보를 가진 값으로 지정된다.애플리케이션 컨텍스트가 사용할 설정정보 파일을 지정하는 것부터 스프링의 거의 모든 API는 외부의 리소스 정보가 필요할 떄는 항상 이 Resource 추상화를 이용한다.

리소스 로더

public interface ResourceLoader {

    // location에 담긴 스트링 정보를 바탕으로 그에 적절한 Resource로 변환해준다. 
    Resource getResource(String location);

    ClassLoader getClassLoader();
}
  • 문자열로 정의된 리소스를 실제 Reousrce타입 오브젝트로 변환해주는 클래스가 ResourceLoader 이다.
  • ResourceLoader가 처리하는 접두어의 예는 다음의 표와 같다.
접두어 설명
file: file:/CL/temp/file.txt 파일 시스템의 C:/temp 폴더에 있는 file.txt를 리소스로 만들어준다.
classpath: classpath:file.txt 클래스패스의 루트에 존재하는 file.txt 리소스에 접근하게 해준다.
http: http://www.myserver.com/test.dat HTTP 프로토콜을 사용해 접근할 수 있는 웹상의 리소스를 지정한다. ftp:도 사용가능하다.
  • ResourceLoader의 대표적인 예로는 스프링의 ApplicationContext(ResourceLoader 인터페이스를 상속)가 있다.
    • 스프링 설정정보가 담긴 XML 파일도 리소스 로더를 이용해 Resource 형태로 읽어온다.

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

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

  • String 형식의 sqlmapFile 프로퍼티 -> Resource 타입으로 변경 및 이름 변경
<!-- sql service -->
<bean id="sqlService" class="springbook.user.sqlservice.OxmSqlService">
  <property name="unmarshaller" ref="unmarshaller" /> 
  <property name="sqlmap" value="classpath:/springbook/user/dao/sqlmap.xml" />
</bean>

 

public class OxmSqlService implements SqlService {
	...
	
	public void setSqlmap(Resource sqlmap) {
		this.oxmSqlReader.setSqlmap(sqlmap);
	}

    ...

    private class OxmSqlReader implements SqlReader {
		private Unmarshaller unmarshaller;
        // Resource 로 변경해준다.
		private Resource sqlmap = new ClassPathResource("sqlmap.xml", UserDao.class);

		public void setUnmarshaller(Unmarshaller unmarshaller) {
			this.unmarshaller = unmarshaller;
		}

		public void setSqlmap(Resource sqlmap) {
			this.sqlmap = sqlmap;
		}

		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 e) {
				throw new IllegalArgumentException(this.sqlmap.getFilename() + "을 가져올 수 없습니다", e);
			}
		}
	}
}

 

  • Resource 는 실제 리소스가 아니라 단지 리소스에 접근할 수 있는 추상화된 핸들러라는 것을 명심하자
    • Resource 타입으로 오브젝트가 생성이 되었다고 해도 실제 리소스가 존재하지 않을 수도 있다.
  • classpath:는 디폴트로 생략이 가능하다.

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

  • 문제 : SQL 테이블 또는 조건을 급하게 변경해야하는 경우
    • 현재까지 만들어진 SqlService 구현 클래스들은 초기에 리소스로부터 SQL 정보를 읽어 메모리에 두고 사용한다.
    • -> SQL 테이블 또는 조건을 변경한다고 해도 메모리상의 SQL 이 갱신되지 않는다.

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

  • DI는 런타임 시에 의존 오브젝트를 다이내믹하게 연결해줘서 유연한 확장을 꾀하는 것이 목적이다.
  • DI를 적용할 때는 가능한 한 인터페아스를 사용 해야 한다.
    • DI를 DI답게 만들려면 두 개의 오브젝트가 인터페이스를 통해 느슨하게 연결되어야 한다. 
💡 인터페이스를 사용했을 떄 장점
1. 다형성을 얻기 위해서.
  • 하나의 인터페이스를 통해 여러 개의 구현을 바꿔가면서 사용할 수 있게 하는 것이 DI가 추구하는 첫 번째 목적이다.
  • 다형성의 예로, 프록시, 데코레이터, 어댑터, 테스트 대역 등이 있다.
2. 인터페이스 분리 원칙을 통해 클라이언트와 의존 오브젝트 사이의 관계를 명확하게 해줄 수 있다.
  • 오브젝트가 그 자체로도 충분히 응집도가 높은 작은 단위로 설계되었더라도, 목적과 관심이 각기 다른 클라이언트가 있다면 인터페이스를 통해 이를 적절하게 분리해줄 필요가 있다.
    • 이를 객체지향 설계 원칙에서는 인터페이스 분리 원칙이라고 부른다.

인터페이스 상속

  • 인터페이스 분리 원칙이 주는 장점은 모든 클라이언트가 자신의 관심에 다른 접근 방식을 불필요한 간섭 없이 유지할 수 있다는 점이다.
    • 기존 클라이언트는 자신이 사용하던 인터페이스를 통해 동일한 방식으로 접근할 수만 있다면 변경에 영향을 받지 않는다.DI를 이용해 다양한 구현 방법 적용하기
  • 운영 중인 시스템에서 사용하는 정보를 실시간으로 변경하는 작업을 만들 때 가장 먼저 고려해야 할 사항은 동시성 문제다.
  • 따라서 자바에서 제공되는 기술을 이용하여 어느정도 안전한 업데이트가 가능하도록 변경해보자.

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

  • ConcurrentHashMap은 데이터 조작 시 전체 데이터에 락을 걸지 않고 조회는 아예 락을 걸지 않는다. 따라서 안전하면서 성능적으로 보장되는 동기화된 HashMap으로 이용하기에 적당하다.
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 updateSql(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 service -->
<bean id="sqlService" class="springbook.user.sqlservice.OxmSqlService">
    <property name="unmarshaller" ref="unmarshaller" /> 
    <property name="sqlRegistry" ref="sqlRegistry" />
</bean>

<bean id="sqlRegistry" class="springbook.user.sqlservice.updatable.ConcurrentHashMapSqlRegistry">
</bean>

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

내장형 DB는 애플리케이션에 내장돼서 애플리케이션과 함께 시작되고 종료되는 DB를 말한다.

  • 내장형 DB(embedded DB)를 이용해 SQL을 저장하고 수정해보자.
  • 데이터베이스는 인덱스를 이용한 최적화된 검색을 지원하고 동시에 많은 요청을 처리하면서 안정적인 변경 작업이 가능한 기술이다.
  • 데이터는 메모리에 저장되기 때문에 IO로 인해 발생하는 부하가 적어서 성능이 뛰어나다.
  • 또한 Map 과 같은 컬렉션이나 오브젝트를 이용해 메모리에 데이터를 저장해두는 방법에 비해 매우 효과적이고 안정적인 방법으로 검색, 수정, 등록, 최적화된 락킹, 격리수준, 트랜잭션을 적용할 수 있다.
  • 자바에서 많이 지원되는 내장형 데이터베이스는 Derby, HSQL, H2 를 뽑을 수 있다.
  • 스프링은 내장형 DB를 초기화하는 작업을 지원하는 내장현 DB 빌더를 제공한다.

내장형 DB 인스턴스는 보통 고유한 JDBC 접속 URL을 통해 연결을 시도하면 JDBC 드라이버 내에서 이를 생성해준다.

new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(H2)  // 내장형 DB 종류
.setScriptEncoding("UTF-8")   
.ignoreFailedDrops(true)
.addScript("schema.sql") // 테이블 생성과 데이터 초기화를 위해 사용할 SQL 문장을 담은 SQL 스크립트의 위치 지정. SQL 스크립트는 하나 이상을 지정할 수 있음 
.addScripts("user_data.sql", "country_data.sql")
.build(); // 주어진 조건에 맞는 내장형 DB를 준비하고 초기화 스크립트를 모두 실행한 뒤 이에 접근할 수 있는 EmbeddedDatabase를 돌려준다. 
내장형 DB의 트랜잭션 격리수준 지원

트랜잭션 격리수준이 READ_UNCOMMITED라고 불리는 레벨 0은 한 트랜잭션이 종료되기 전의 작업 내용을 다른 트랜잭션이 읽을 위험성이 있다. 만약 다른 트랜잭션이 끝나기 전에 변경한 정보를 읽어버렸는데 해당 트랜잭션이 롤백되어버리면 실제로는 DB에 반영되지 않은 유효하지 않은 데이터를 사용하는 문제가 발생한다.

 

자바 언어의 변화와 스프링

애노테이션의 메타정보 활용
  • 자바 코드의 메타정보를 이용한 프로그래밍 방식
  • 애노테이션은 자바코드가 실행되는 데 직접 참여하지 못한다.
  • 애노테이션의 활용이 늘어난 이유
    • 애플리케이션을 핵심 로직을 담은 자바 코드와 이를 지원하는 IoC 방식의 프레임워크, 그리고 프레임워크가 참조하는 메타정보라는 세 가지로 구성하는 방식에 잘 어울린다.
    • 단점으로는 자바 코드에 존재하므로 변경할 때마다 매번 클래스를 새로 컴파일 해줘야 한다.
정책과 관례를 이용한 프로그래밍
  • 코드를 이용해 명시적으로 동작 내용을 기술하는 대신 코드 없이도 미리 약속한 규칙 또는 관례를 따라서 프로그램이 동작하도록 만드는 프로그래밍 스타일을 적극적으로 포용하게 만들어왔다.

테스트 컨텍스트의 변경

  • XML을 사용하지 않는 것이 최종 목적이다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=TestApplicationContext.class)
public class UserDaoTest {
  • @ContextConfiguration이 XML 위치 대신 DI 정보를 담고 있는 자바 클래스를 이용하게 만든다.
  • TestApplicationContext의 내용이 없어 에러가 나므로, XML의 도움을 받도록 하는 방법 : @ImportResource 애노테이션을 사용하여 자바 클래스로 만들어진 DI 설정정보에서 XML의 설정정보를 가져오게 만든다.
@Configuration
@ImportResource("/text-applicationContext.xml")
public class TestApplicationContext {
}
  • 이제부터 XML의 DI 정보를 단계적으로 TestApplicationContext로 옮길 수 있다.

<context:annotation-config /> 제거

  • <context:annotation-config /> 제거를 해도 문제가 없는 이유
    • @PostConstruct를 붙인 메소드가 빈이 초기화된 후에 자동으로 실행되도록 사용한다.
    • @PostConstruct와 같은 표준 애노테이션을 인식해서 자동으로 메소드를 실행해준다.
    • 스프링 컨테이너가 참고하는 DI 정보의 위치가 XML에서 TestApplicationContext라는 자바 클래스로 바꼈기 때문에 제거해도 문제가 없다.
    • 따라서 XML에 담긴 DI 정보를 이용하는 스프링 컨테이너를 사용하는 경우에는 포함하여야 한다. 

<bean>의 전환

  • @Bean은 @Configuration이 붙은 DI 설정용 클래스에서 주료 사용되는 것으로, 메소드를 이용해서 빈 오브젝트의 생성과 의존관계 주입을 직접 자바 코드로 작성할 수 있게 해준다.
  • @Bean이 붙은 public 메소드로 만들고, 메소드 이름은 <bean>의 id 값으로 한다.
  • 생성할 빈 오브젝트의 클래스는 <bean>의 class에 나온 것을 그대로 사용하면 된다.
@Bean
public DataSource dataSource() {
	SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
    
    dataSource. ...
	
    return dataSource;
}

 

@Autowired를 이용한 자동와이어링

  • 지금까지 사용했던 @Autowired는 스프링 테스트 클래스나 DI 설정용 @Configuration 클래스에서 스프링 컨테이너가 생성한 빈을 클래스의 멤버 필드로 주입받기 위해 사용했다.
  • 자동와이어링을 이용하면 컨테이너가 이름이나 타입을 기준으로 주입된 빈을 찾아주기 때문에 빈의 프로퍼티 설정을 직접 해주는 자바 코드나 XML 양을 대폭 줄일 수 있다.
@Autowired
public void setDataSource(DataSource dataSource) {
    this.jdbcTemplate = new JdbcTemplate(dataSource);
}
  • 스프링은 DataSource 타입의 빈을 모두 찾는다. 이후 주입 가능한 타입의 빈이 하나라면 스프링이 수정자 메소드를 호출해서 넣어주고, 두개 이상이 나오면 그중에서 프로퍼티와 동일한 이름의 빈이 있는지 찾는다.
  • setDataSource() 수정자 메소드를 없애고 필드에 @Autowired를 적용하는건 불가능하다. (주어진 오브젝트를 그대로 필드에 저장하는게 아닌, JdbcTemplate을 생성해서 저장하기 때문)
  • 자동와이어링은 DI 관련 코드를 대폭 줄일 수 있지만, 빈 설정정보를 보고 다른 빈과 의존관계가 어떻게 맺어져 있는지 한눈에 파악하기 힘들다는 단점이 있다.

@Component를 이용한 자동 빈 등록

  • @Component가 붙은 클래스는 빈 스캐너를 통해 자동으로 빈으로 등록된다.
  • @Component는 빈으로 등록될 후보 클래스에 붙여주는 일종의 마커라고 보면 된다.
@Component
public class UserDaoJdbc implements UserDao {
  • @Component 애노테이션이 달린 클래스를 자동으로 찾아서 빈을 등록해주게 하려면 빈 스캔 기능을 사용하겠다는 애노테이션 정의가 필요하다.
@ComponentScan(baskPackages="springbook.user")
public class TestApplicationContext {
  • basPackages 앨리먼트는 @Component가 붙은 클래스를 스캔할 기준 패키지를 지정할 때 사용한다.
  • 자동 빈 등록을 이용하는 경우 빈의 의존관계를 담은 프로퍼티를 따로 지정할 방법이 없으므로, 프로퍼티 설정에 @Autowired와 같은 자동와이어링 방식을 적용해야 한다. 
  • 여러 개의 애노테이션에 공통적인 속성을 부여하려면 메타 애노테이션(애노테이션의 정의에 부여된 애노테이션)을 이용해야 한다. 

@Component 메타 애노테이션을 가진 애노테이션 정의

 

테스트용 컨텍스트 분리

  • 성격이 다른 DI 정보를 분리해야 한다.
  • 운영 시스템에서는 AppContext 참조, 테스트에서는 AppContext와 TestAppContext 두 개의 DI 정보를 함께 사용하도록 분리

@Import

  • SQL 서비스는 다른 애플리케이션에서도 사용될 수 있기 때문에 독립적인 모듈처럼 취급하는게 좋다.
  • AppContext가 메인 설정정보가 되고, SqlServiceContext를 보조 설정정보로 사용한다.
  • 밑의 코드로 인해 AppContext가 설정 클래스로 사용되면 SqlServiceContext도 함께 적용된다.
@Import(SqlServiceContext.class)
public class AppContext {

 

@Profile과 @ActiveProfiles

  • 테스트환경과 운영환경에서 각기 다른 빈 정의가 필요한 경우가 있다. 따라서 운영환경에서 반드시 필요하지만 테스트 실행 중에는 배제돼야 하는 빈 설정을 별도의 설정 클래스를 만들어 따로 관리할 필요가 있다.
  • 실행환경에 따라 빈 구성이 달라지는 내용을 프로파일로 정의해서 만들어두고, 실행 시점에 어떤 프로파일의 빈 설정을 사용할지 지정해서 사용한다.
@Configuration
@Profile("test")
public class TextAppContext {
  •  서비스가 실행될 때 활성 프로파일로 test 프로파일을 지정하려면 @ActiveProfiles 애노테이션을 사용하면 된다.
@ActiveProfiles("test")
@ContextConfiguration(classes=AppContext.class)
public class UserServiceTest {

 

 

컨테이너의 빈 등록 정보 확인

  • 스프링 컨테이너에 등록된 빈 정보를 조회하는 방법

중첩 클래스를 이용한 프로파일 적용

  • 프로파일에 따라 분리했던 설정정보를 하나의 파일로 모아보는 방법이다.
  • 프로파일이 지정된 독립된 설정 클래스의 구조는 그대로 유지한 채로 단지 소스코드의 위치만 통합한다. (스태틱 중첩 클래스를 이용하면 된다.)

@PropertySource

  • 컨테이나 프로퍼티 값을 가져오는 대상을 프로퍼티 소스라고 한다.
  • DB 연결정보 정도들은 환경에 따라 다르게 설정될 수 있어야 한다. 
  • 그래서 이런 외부 서비스 연결에 필요한 정보는 자바 클래스에서 제거하고 손쉽게 편집할 수 있고빌드 작업이 따로 필요 없는 XML이나 프로퍼티 파일 같은 텍스트 파일에 저장해두는 편이 낫다.
  • 프로퍼티 파일의 확장자는 보통 properties이고, 내부에 키=값 형태로 프로퍼티를 정의한다.
@PropertySource("/database.properties")
public class AppContext {
  • @PropertySouce로 등록한 리소스로부터 가져오는 프로퍼티 값은 컨테이너가 관리하는 Environment 타입의 환경 오브젝트에 저장된다.

PropertySoucresPlaceholderConfigurer

  • 앞의 방법과 달리 Environment 오브젝트 대신 프로퍼티 값을 직접 DI 받는 방법도 가능하다. > @Value 애노테이션 사용

빈 설정자

  • SQL 서비스를 재사용 가능한 독립적인 모듈로 만들어야 한다.
  • UserDao 위치로 고정되어있는 SQL 매핑파일의 위치를 직접 지정할 수 있도록 수정해주어야 한다.
@Bean
public SqlService sqlService() {
  OxmSqlService sqlService = new OxmSqlService();
  sqlService.setUnmarshaller(unmarshaller());
  sqlService.setSqlRegistry(sqlRegistry());
  sqlService.setSqlmap(new ClassPathResource("/sql/sql-map.xml", UserDao.class));
  return sqlService;
}

@Enable* 애노테이션

  • @Import를 대체할 수 있는 애노테이션
@Import(value = SqlServiceContext.class)
public @interface EnableSqlService {
}

 

정리

스프링 DI와 서비스 추상화 등을 응용해 새로운 SQL 서비스 기능을 설계하고 개발한 뒤에 이를 점진적으로 확장, 발전시켰다.

 

'챕터정리방' 카테고리의 다른 글

[9장] 스프링 프로젝트 시작하기  (0) 2022.02.09
[8장] 스프링이란 무엇인가?  (0) 2022.01.25
[6장] AOP  (0) 2022.01.03
[5장] 서비스 추상화  (0) 2021.12.29
[5장] 서비스 추상화  (0) 2021.12.26

댓글