다음은 토비의 스프링 7.3~7.4장 을 공부하고 정리하였습니다.
7.1~7.2장 내용 간단 요약
- SQLReader - SQL을 어떻게 읽어오는지
- SqlRegistry - SQL을 어떻게 저장해둘 건지
- SqlService 이란?
- @PostConstruct 가 달린 빈 초기화 메소드와 SqlService 인터페이스에 선언된 메소드인 getFinder()을 sqlReader과 sqlRegistry를 이용하도록 변경
- 자기 자신을 참조하는 빈이다. → sqlService를 구현한 메소드와 초기화된 메소드는 외부에서 DI 된 오브젝트라고 생각하고 결국 자신의 메소드에 접근하므로
이 셋을 이용하여 디폴트 의존관계를 갖는 빈을 만들 수 있다. 디폴트 의존관계란 외부에서 DI 받지 않는 경우 기본적으로 자동 적용되는 의존관계를 말한다. 이전 내용에서는 생성자와 적절한 xml bean 설정을 통해 디폴트 의존관계를 설정하는 내용을 담고 있다.
7.3. 서비스 추상화 적용
1. OXM 서비스란?
ORM 서비스(Object Relational Mapping) 과 비슷하게, OXM(Object-XML Mapping)은 XML과 자바 오브젝트를 매핑해서 상호 변환해주는 기술을 말한다. (Castor XML, JiBX, XmlBeans, Xstream 등이 있다.)
Spring에서는 코드의 수정 없이도 OXM 기술을 자유롭게 바꾸어서 적용할 수 있는 ‘서비스 추상화’ 로직을 제공한다.
2. OXM 서비스 인터페이스
- Marshaller : Object → XML
- Unmarshaller : XML → Object
3. OXM예시 1 - Jaxb2Marshaller를 통해 언마샬링과 마샬링을 수행해보자!
- JAXB용 Unmarshaller 빈을 설정한다
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="unmarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<!--프로젝트 내 이전에 만든 jaxb 패키지 경로-->
<property name="contextPath" value="com.ksb.spring.jaxb" />
</bean>
</beans>
2. @Autowired 어노테이션으로 Unmarshaller를 부르고, Unmarshaller의 unmarshal() 메소드를 한 번 호출해주기만 하면 모든 번거로운 작업은 Jaxb2Marshaller 빈이 알아서 진행해준다!
4. OXM예시 2 - Castor를 통해 언마샬링과 마샬링을 수행해보자!
- 매핑 정보를 작성한다.
<!--mapping.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapping PUBLIC "-//EXOLAB/Castor Object Mapping DTD Version 1.0//EN"
"http://castor.org/mapping.dtd">
<mapping>
<class name="com.ksb.spring.jaxb.Sqlmap">
<map-to xml="sqlmap"/>
<field name="sql" type="com.ksb.spring.jaxb.SqlType"
required="true" collection="arraylist">
<bind-xml name="sql" node="element"/>
</field>
</class>
<class name="com.ksb.spring.jaxb.SqlType">
<map-to xml="sql"/>
<field name="key" type="string" required="true">
<bind-xml name="key" node="attribute"/>
</field>
<field name="value" type="string" required="true">
<bind-xml node="text"/>
</field>
</class>
</mapping>
2. 설정파일의 unmarshaller 빈의 클래스를 Castor용 구현 클래스로 설정한다. 이후 mappingLocation 프로퍼티에는 Castor용 매핑파일의 위치를 지정해준다!
OXM 서비스 추상화 적용 in (Oxm)SqlService
기능이 같은(sqlReader) 여러가지 기술(OxmSqlReader, BaseSqlReader)이 존재한다 면 서비스 추상화를 고민해보자!
SqlReader와 SqlRegistry 란? (아래 참고)
SqlReader와 SqlRegistry 란?
- SqlReader는 어떠한 리소스로 저장되어있는 XML을 읽어오는 인터페이스였다. 따라서 read()와 같은 메소드가 있었다.그리고 세 개의 인터페이스를 구현한 구현 클래스를 DI를 통해 간접적으로 연결함으로써, 자유로운 확장이 가능했다.
- SqlRegistry는 SqlReader가 읽어온 SQL을 맵이나 리스트와 같은 형태로 저장하고 관리해주는 역할이였다. 따라서 registerSql(), findSql()과 같은 메소드가 있었다.
- SqlService는 SQL을 사용자가 사용하기 위한 인터페이스였다. 따라서 SqlReader, SqlRegistry에 대한 참조를 갖고 초기화를 진행하였고, getSql()과 같은 메소드가 있었다.
SqlReader와 SqlRegistry의 두 개의 전략을 활용하면서, SqlReader 구현 오브젝트에 대한 의존관계를 고정시켜버리자. → static(private final)을 이용하여 마치 밖에서 볼 때에는 하나의 오브젝트로 보이지만, 내부에서는 의존관계를 가지는 관계로 만든다.
왜??
앞장에서 봤던 BaseSqlService의 경우 디폴트 의존관계를 갖는 빈을 사용했다. 하지만, OXM을 적용하는 경우 언마샬러를 비롯해서 설정을 통해 DI 해줄 것이 많기 때문에 SqlReader 클래스를 단순히 디폴트 오브젝트로 제공하지 않고, 하나의 빈 설정만으로 SqlService와 SqlReader이 필요한 프로퍼티 설정이 모두 가능하도록 만들 필요가 있다. 따라서 강한 결합 구조를 만드는 것이다!
아래와 같이 OxmSqlReader는 외부에 노출되지 않는다. 만약 DI를 통해 제공받아야 하는 property가 있다면 OxmSqlService의 공개된 프로퍼티를 통해 간접적으로 DI 받아야 한다.
코드로 보면 아래와 같다.
위임 구조를 이용한 코드의 중복 제거
상황은 이러하다.
우리는 BaseSqlService와 OxmSqlService 두개를 모두사용하고 있으며, loadSql과 getSql 메소드는 BaseSqlService와 OxmSqlService에 둘 다 존재한다. 두 메소드의 작업이 꽤나 복잡하고, 변경도 자주 일어난다면? → 코드의 중복 때문에 헷갈리는 경우가 많을 것이다.
그래서 이 둘을 묶을 거다!! 위임구조 방식을 사용한다.
위임(delegate)구조는 프록시를 만들 때 사용했었다. 간단히 설명하자면, 아래 그림처럼 우선 클라이언트의 요청을 직접 받는 빈(Subject)이 주요한 내용은 뒤의 빈(RealSubject)에게 전달해준다.
하지만 프록시 방법을 사용한답시고 OxmSqlService와 BaseSqlService를 위임구조로 만들기 위해 두 개의 빈을 등록하는 것은 불편하다. 또한 프록시처럼 많은 타깃에 적용할 것도 아니고, 특화된 서비스를 위해 한 번만 사용할 건데 굳이 이런 유연한 DI 방식을 사용하지 않아도 된다.
따라서 아래와 같이 OxmSqlService는 OXM 기술에 특화된 SqlReader를 멤버로 내장한다. 이후 실제 SqlReader와 SqlService를 이용해 SqlService의 기능을 구현하는 일은 내부에 BaseSqlService를 만들어서 위임하자.
코드로 나타내면 아래와 같이 BaseSqlService가 SqlReader와 SqlService에 있는 로직들을 OxmSqlService 대신 위임(delegate)해서 수행한다.
다양한 위치에 존재하는 리소스에 접근할 수 있는 통일된 방법?
문제 상황 → 리소스가 여러 곳에 산재되어 있을 수 있다.
- 클래스패스에 존재하는 파일이 아닌 클래스패스 루트로부터 각각에 있는 파일
- 서버나 개발 시스템의 특정 폴더에 있는 파일
- http, ftp 프로토콜 접근하는 웹상의 리소스 파일
그렇다면 리소스에 접근할 수 있는 통일된 방법을 사용하자!
OxmSqlReader가 sqlmapFile을 읽어오는 코드를 작성한다. 그러면 코드의 변경 없이도 다양한 소스로부터 SQL 맵 파일을 가져오게 할 수 있다.
리소스
Resource라는 추상화 인터페이스를 정의하자!
‘추상화’라는 것은, 일단 개념이나 구조를 단순화하고 일반화하는 과정을 의미한다.
public interface Resource extends InputStreamSource{
boolean exists();
boolean isReadable();
boolean isOpen();
URL getURL() throws IOException;
URI getURI() throws IOException;
File getFIle() throws IOException;
//이는 JDK의 URL, URI, File 형태로 전환 가능한 리소스에 사용된다.
Resource createRelative(String relativePath) throws IOException;
long lastModified() thrwos IOException;
String getFilename();
String getDescription();
}
public interface InputStreamSource{
InputStream getInputStream() thows IOException;
//모든 리소스는 InputStream 형태로 가져올 수 있다.
}
그렇다면 어떻게 임의의 리소스를 Resource 인터페이스 타입의 오브젝트로 가져올 수 있을까? Resource 추상화는 빈이 아닌 값으로 취급된다. 즉, 리소스는 OXM이나 트랜잭션처럼 서비스를 제공해주는 것이 아니라 단순한 정보를 가진 값으로 지정된다. ResourceLoader를 이용하여 쉽게 구현해볼 수 있다.
Resource 사용 방법
일단 스트링으로 되어 있던 sqlmapFile 프로퍼티를 모두 Resource 타입으로 바꾼다. 그리고 이름도 sqlmap으로 변경한다.
Resource 타입은 실제 소스가 어떤 것이든 상관없이 getInputStream() 메소드를 이용해 Stream으로 가져올 수 있다. 이를 StreamSource 클래스를 이용해서 OXM 언마샬러가 필요로 하는 Source 타입으로 만들어주면 된다.
public class OxmSqlService implements SqlService{
public void setSqlmap(Resource sqlmap){
this.oxmSqlReader.setSqlmap(sqlmap);
}
...
private class OxmSqlReader implements SqlReader{
private Resource sqlmap = new ClassPathResource("sqlmap.xml", UserDao.class);
public void setSqlmap(Resource sqlmap){
this.sqlmap = sqlmap;
}
public void read(SqlRegistry sqlregistry){
try {
Source source = new StreamSource(sqlmap.getInputStream());
} catch (IOException e){
throw new IllegalArgumentException(this.sqlmap.getFilename() + "을 가져올 수 없습니다.", e);
}
}
}
}
이후 sqlmap.xml 파일을 클래스패스 리소스로 지정해준다. sqlmap.xml 파일 안에 아래의 방법들을 채택하여 여러 property를 지정해주면 된다.
- classpath: 접두어를 이용해 지정한 리소스. 클래스패스 루투로부터의 상대적인 위치를 의미한다.
<property name="sqlmap" value="classpath:springbook/user/dao/sqlmap.xml"/> - file: 파일 시스템의 루트 티렉토리부터 시작하는 파일 위치
<property name="sqlmap" value="file:/opt/resources/sqlmap.xml" /> - http 로 접근 : 공개적인 웹 서버에서 가져오는 경우
<property name="sqlmap" value="https:/***/sqlmap.xml"/>
7.4 인터페이스 상속을 통한 안전한 기능확장
서버가 운영 중인 상태에서 서버를 재시작하지 않고 긴급하게 애플리케이션이 사용 중인 SQL을 변경해야 할 때?
결론부터 말하면 DI를 사용하자! → UpdatableSqlRegistry라는, SqlRegistry 인터페이스를 상속하고 SQL 수정 기능을 가진 확장 인터페이스를 새로 추가하자.
- 위에서 SqlRegistry는 SQL의 등록과 조회만 가능하다. UpdatableSqlRegistry는 SqlRegistry를 상속받아(extends) 만들었고, MyUpdatableSqlRegistry는 UpdatableSqlRegistry를 이용해 만들었다.
- BaseSqlService와 SqlAdminService는 각각 SqlRegistry와 UpdatableSqlRegistry라는 인터페이스에 의존하고 있다.
- MyUpdatableSqlRegistry은 DI를 통해 BaseSqlService와 SqlAdminService를 사용하고 있다.
여기에서 핵심은 기능을 추가했지만 BaseSqlService의 interface는 SqlRegistry라는 사실이 변하지 않았다는 것이다.
- BaseSqlService의 interface를 망치고 싶지 않다. → 이 SqlRegistry를 확장해 UpdatableSqlRegistry를 구현하고 직접적으로 ‘Sql 수정’ 역할을 하는 SqlAdminService에게 이 interface를 맡긴다.
- BaseSqlService도 UpdatableSqlRegistry를 사용하면 되잖아? → BaseSqlService는 본래 DAO를 위한 SQL 조회 서비스라 SqlRegistry 인터페이스가 제공하는 기능이면 충분하다.
- 따라서 MyUpdatableSqlRegistry를 그냥 DI 받아서 사용하고 기존 틀을 흐트러 뜨리지 않는다!
결론
"추상화" 개념은 SPRING에서 매우 중요하다! 주로 interface, abstract가 추상화라고만 알고 있는데, DI, IOP 개념 모두가 이 추상화에 큰 역할을 한다.
앞단에 JavaMailSender 챕터에서 이메일을 보내지 않고 테스트 코드 작성하기 부분에서도 이를 확인할 수 있었다.
📩 JavaMailSender 이메일을 보내지 않고 테스트 코드 작성하기
방법 : Mockito를 이용해서 가짜 객체를 주입해 "해당 메소드가 호출되었는지"를 확인한다.
1. 메인 class인 MailUtil을 추상화 -> 기존 MailUtil을 MailUtilImpl로 변경하고 MailUtil은 interface화한다.
2. Mockito에서 MailUtil 인터페이스를 Mocking한다.
3. Mockito.verify() 메소드로 해당 메소드 호출 여부만을 파악한다.
쉽게 말해 "추상화"란 '공통된 동작'을 뽑아내고 해당 동작의 '구체적인 구현'들을 상위클래스에 '최소한의 틀로' 나타내는 것이다. 이를 통해 데이터베이스, 메시지 브로커, 보안 메커니즘 등과 같은 다른 기술 스택이나 리소스 사용이 편리해진다. 대표적으로 Spring의 JDBC 추상화를 이용해 여러 다른 데이터베이스 시스템 간에 SQL 쿼리를 다르게 처리할 수 있다.
문제 시 삭제 하겠습니다 !
'BackEnd > JAVA\SPRING' 카테고리의 다른 글
[Java|Spring] Koala 프로젝트 구현 과정 중 이슈 정리 (0) | 2024.02.01 |
---|---|
[WAS/WS|Docker] Springboot war 파일 외장 tomcat 배포 (2) | 2024.01.10 |
[SPRING] AOP (0) | 2023.10.16 |
[SPRING] enum 타입, 역할과 책임의 분리 (0) | 2023.10.02 |
[SPRING] DI, 템플릿과 콜백 (0) | 2023.09.22 |