ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 7장) 7.3 서비스 추상화 적용
    Java & Spring/토비의 스프링 3.1 2021. 8. 8. 17:27
    반응형

    7.3 서비스 추상화 적용

    • JaxbXmlSqlReader 개선 과제
      • JAXB 외에 다양한 XML과 자바오브젝트를 매핑하는 기술이 있음
        • 필요에 따라 다른 기술로 변경해서 사용
      • XML 파일을 좀 더 다양한 소스에서 가져올 수 있게 만든다
        • 앞서 작성한 내용은 UserDao 클래스와 같은 class path 안에서만 XML을 읽어올 수 있음
        • 임의의 class path나 파일 시스템 상의 절대위치 or HTTP 프로토콜을 통해 원격에서 가져오도록 확장하는 방법 생각

    7.3.1 OXM 서비스 추상화

    • OXM (Object-XML Mapping)
      • JAXB 외에 실전에서 자주 사용되는 다양한 XML과 자바오브젝트를 매핑하는 기술(OXM)
        • Castor XML
          • 설정파일 필요 X
          • 인트로스펙션 모드를 지원하기도 하며 간결하고 가벼운 바인딩 프레임워크
            • 인트로스펙션
              • 특정 클래스가 어떤 클래스로부터 파생되었는지, 혹은 어떤 메소드가 구현되어 있는지, 객체에는 어떤 속성이 있는지에 대한 상세한 정보를 런타임에 얻거나 조작하는 기술
        • JibX
          • 뛰어난 퍼포먼스를 자랑하는 XML 바인딩 기술
        • XmlBeans
          • 아파치 XML 프로젝트의 하나로 XML 정보셋을 효과적으로 제공
        • Xstream
          • 관례를 이용해서 설정이 없는 바인딩을 지원하는 XML 바인딩 기술 중 하나
      • 상호 호환성
        • JAXB를 포함한 총 다섯가지의 OXM 프레임워크는 사용 목적이 동일하기 때문에 유사한 기능과 API를 제공
        • 기능이 같기 때문에 로우레벨의 구체적인 기술과 API에 종속되지 않고 추상화된 레이어와 API를 제공하여, 구현 기술에 대해 독립적인 코드를 작성할 수 있게 해주는 서비스 추상화가 필요
    • OXM 서비스 인터페이스
      • 스프링이 제공하는 OXM 추상화 서비스 인터페이스에는 자바오브젝트를 XML로 변환하는 Marshaller와 반대인 Unmarshaller가 있음
        • SqlReader는 Unmarshaller를 이용
        • public interface Umarshaller {
              boolean supports(Class<?> clazz);
          
              Object unmarshal(Source source) throws IOException, XmlMappingException;
          }
          
          • Unmarshaller 인터페이스를 살펴보면 XML 파일에 대한 정보를 담은 Source 타입의 오브젝트를 제공받고, 설정에서 지정한 OXM 기술을 이용해 자바오브젝트 트리로 변환하고 그 루트 오브젝트를 리턴함
          • OXM 기술에 따라 Unmarshaller 인터페이스를 구현한 클래스가 다섯가지 있고, 필요에 따라 추가 정보를 빈 프로퍼티로 지정 가능
    • JAXB 구현 테스트
      • 앞서 만든 JaxbTest 학습 테스트를 스프링 OXM 서비스 추상화 인터페이스를 이용하도록 수정
      • Jaxb2Marshaller 클래스는 Unmarshaller와 Marshaller 인터페이스를 모두 구현하고 있음
        • Jaxb2Marshaller 클래스를 빈으로 등록하고 바인딩 클래스 패키지 이름을 지정하는 contextPath 프로퍼티만 넣어주면 언마샬러로 사용 가능
      • <beans xmlns="http://www.springframwork.org/schema/beans"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://www.springframework.org/schema/benas
                                   http://www.springframework.org/schema/benas/spring-beans-3.0xsd">
            <bean id="unmarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
                <property name="contextPath" value="springbook.user.sqlservice.jaxb" />
            </bean>
        </beans>
        
      • 스프링의 서비스 추상화가 적용됐으므로 로우레벨의 JAXB API를 사용해서 컨텍스트를 만들어 언마샬러를 생성하는 등의 복잡한 코드 작성 필요 X
        • 추상 인터페이스인 Unmarshaller의 unmarshal() 메소드를 호출하면 모든 작업을 Jaxb2Marshaller 빈이 알아서 진행
      • @RunWith(SpringRunner.class)
        @ContextConfiguration
        public class OxmTest {
            @Autowired Unmarshaller unmarshaller;
        
            @Test
            public void unmarshallSqlMap() throws XmlMappingException, IOException {
                SOurce xmlSource = new StreamSource(
                getClass().getResourceAsStream("sqlmap.xml"));
        
                Sqlmap sqlmap = (Sqlmap) this.unmarshaller.unmarshal(xmlSource);
        
                List<SqlType> sqlList = sqlmap.getSql();
                assertThat(sqlList.size(), is(3));
                assertThat(sqlList.get(0).getKey(), is("add"));
                ...
                assertThat(sqlList.get(2).getValue(), is("delete"));
            }
        }
        • OXM 추상화 계층을 이용하여 구체적인 기술에 의존하는 부분 없이 XML 설정에 의해 기술을 변경할 수 있게 됨
    • Castor 구현 테스트
      • Castor로 OXM 기술을 바꾸는 수정
        • <!DOCTYPE mapping PUBLIC "-//EXOLAB/Castor Mapping DTD Version 1.0/EN"
              "http://castor.org/mapping.dtd">
          <mapping>
              <class name="springbook.sqlservice.jaxb.Sqlmap">
                  <map-to xml="sqlmap" />
                  <filed name="sql" type="springbook.sqlservice.jaxb.SqlType" required="true" collection="arraylist">
                      <bind-xml name="sql" node="element" />
                  </filed>
              </class>
              <class name="springbook.sqlservice.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>
          • key - value 값의 매핑 정보 설정 ~> mapping.xml로 이름 명명
        • <bean id="unmarshaller" class="org.springframework.oxm.CastorMarshaller">
              <property name="mappingLocation" value="springbook/learningtest/spring/oxm/mapping.xml" />
          </bean>
          • 언마샬러로 Castor용 구현 클래스로 변경하면서 매핑 설정을 제공하면 Castor로 OXM 기술을 바꿀 수 있음
        • 서비스 추상화를 활용하여 로우레벨의 기술을 필요에 따라 변경해서 사용 ~> 애플리케이션 코드는 유지

    7.3.2 OXM 서비스 추상화 적용

    • SqlService를 OXM 추상화 기능을 사용하여 만들자
      • SqlRegistry는 DI를 받지만, SqlReader는 스프링의 OXM 언마샬러를 사용하도록 고정
      • OXM 기술에 의존적이라고해서 OXM 코드를 직접 가지고 있을 필요는 없음
      • 앞서 SqlReader와 SqlRegistry 두 전략을 유지하면서 SqlReader 구현 오브젝트에 대한 의존 관계를 고정시키기
    • 멤버 클래스를 참조하는 통합 클래스
      • BaseSqlService와 유사하게 SqlReader 타입의 의존 오브젝트를 사용하되 스태틱 멤버 클래스로 내장하고 자신만 사용하도록하는 OxmSqlService 구현
        • SqlRegistry는 가장 단순한 HashMapSqlRegisty를 default 의존 오브젝트로 등록
      • public class OxmSqlService implements SqlService {
            private final OxmSqlReader oxmSqlReader = new OxmSqlReader();
        
            private class OxmSqlReader implements SqlReader {
                ...
            }
        }
        • OxmSqlReader를 private으로 감추고, final로 선언하여 DI하거나 변경하지 못하도록 설정
          • OXM을 이용하는 서비스 구조로 최적화
            • 스프링의 OXM 서비스 추상화를 사용하면 언마샬러를 빈으로 등록해야하는데, SqlService를 위해 등록할 빈이 계속 늘어나게 됨
            • 계속 발전시키기 위해서 가능한 한 분리하고 확장 가능하게 만들어야 하지만, 실제로 이를 적용해서 DAO를 개발하는 입장에서는 부담
            • 빈의 개수를 줄이고 설정을 단순하게 하는 방법에는 BaseSqlService를 확장해서 디폴트 설정을 두는 방법도 있지만, 디폴트로 내부에서 만드는 오브젝트의 프로퍼티를 외부에서 지정해주기 힘듦
              • OXM 적용의 경우, 언마샬러를 비롯한 설정을 통해 DI 해줄게 많기 때문에 부적합
          • 하나의 빈 설정으로 SqlService와 SqlReader에 필요한 프로피터 설정이 가능하도록 해야함
            • SqlService의 구현이 SqlReader의 구체적인 구현 클래스 정보를 알고, 자신의 프로퍼티를 통해 필요한 설정정보를 넘겨주고, 멤버 클래스로 소유하고 있는 강한 결합구조를 사용함
            • OxmSqlReader는 OxmSqlService에 의해 만들어지기기 때문에, DI 받을 정보가 있다면 OxmSqlService의 공개된 프로퍼티를 통해 간접적으로 DI 받음
      • public class OxmSqlService implements SqlService {
            private final OxmSqlReader oxmSqlReader = new OxmSqlReader();
        
            public void setUnmarshaller(Unmarshaller unmarshaller) {
                this.oxmSqlReader.setUnmarshaller(unmarshaller);
            }
        
            public void setSqlmapFile(String sqlmapFile) {
                this.oxmSqlReader.setSqlmapFile(sqlmapFile);
            }
        
            private class OxmSqlReader implenets SqlReader {
                private Unmarshaller unmarshaller;
                private String sqlmapFile;
                // ... setter methods
            }
        }
        • 앞서 UserDaoJdbc에서 JdbcTemplate을 만들 때, Datasource를 전달하는 방식과 비슷
          • JdbcTemplate은 자체 독립 빈으로 만들 수도 있고 여러 DAO에서 사용 가능한 최상위 레벨 클래스지만, OxmSqlReader는 OxmSqlService에서만 사용하도록 제한한 멤버 클래스
      • public class OxmSqlService implements SqlService {
            private final OxmSqlReader oxmSqlReader = new OxmSqlReader();
            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);
            }
        
            @PostConstruct
            public void loadSql() {
                this.oxmSqlReader.read(this.sqlRegistry);
            }
        
            public String getSql(String key) throws SqlRetrievalFailureException {
                try {
                    return this.sqlRegistry.findSql(key);
                } catch (SqlNotFoundException e) {
                    throw new SqlRetrievalFailureException(e);
                }
            }
        
            private class OxmSqlReader implenets SqlReader {
                private final static String DEFAULT_SQLMAP_FILE = "sqlmap.xml";
                private Unmarshaller unmarshaller;
                private String sqlmapFile;
        
                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);
                        Sqlmap sqlmap = (Sqlmal)this.unmarshaller.unmarshal(source);
                        sqlmap.getSql().forEach(sql -> {
                            sqlRegistry.registerSql(sql.getKey(), sql.getValue());
                        });
                    } catch (IOException e) {
                        throw new IllegalArgumentException(this.sqlmapFile + "을 가져올 수 없습니다.", e);
                    }
                }
            }
        }
        • OXM을 적용했어도 빈 설정을 단순하게 유지할 수 있는 구조로 수정됨
          • unmarshaller 빈만 따로 설정해주면, SqlService와 OXM 언마샬러를 사용하는 SqlReader, SqlRgistry는 하나의 빈을 등록하는 것으로 충분
          • SqlRegistry는 필요에 따라 다른 구현으로 교체 가능
        • <bean id="sqlService" class="springbook.user.sqlservice.OxmSqlService">
              <property name="unmarshaller" ref="unmarshaller" />
          </bean>
          
          <bean id="unmarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
              <propery name="contextPath" value="springbook.user.sqlService.jaxb" />
          </bean>
          • OxmSqlServic와 Jaxb2Marshaller를 사용하는 unmarshaller만 빈으로 등록하면, 프로퍼티를 내부적인 간접 DI를 통해 이용되므로 설정이 간단
    • 위임을 이용한 BaseSqlService의 재사용
      • loadSql()과 getSql() 메소드가 BaseSqlService와 동일
        • 그렇다고 BaseSqlService 코드를 재사용한다고 상속해서 OxmSqlService를 만들면 멤버 클래스로 통합시킨 OxmSqlReader를 생성하는 코드를 넣기 애매해짐
        • 중복을 제거하기 위해 loadSql()과 getSql()메소드를 추출해서 슈퍼클래스로 분리하는 방법도 있지만, 이 정도 코드로는 복잡한 계층구조로 만들기에도 부담스러움
        • 앞서 작성한 로직처럼 간단한 중복이라면 그냥 허용할 수 있지만, 복잡해진다면 심각한 문제
          • 위임 구조를 이용해서 코드의 중복을 제거
          • loadSql()과 getSql() 구현 로직은 BaseSqlService에만 두고 OxmSqlService는 일종의 설정과 기본 구성을 변경해주기 위한 어댑터 같은 개념으로 BaseSqlService의 앞에 두는 설계가 가능
          • OxmSqlService의 외형 틀을 유지한 채, SqlService의 기능 구현은 BaseSqlService로 위임
        • 위임 구조로 만들기 위해 두 개의 빈으로 등록하는 것은 불편
          • 프록시처럼 많은 타깃에 적용하는게 아닌 특화된 서비스 한번만 사용하기 때문에, 유연한 DI는 포기하고 OxmSqlService와 BaseSqlService를 한 클래스로 묶기
          • OxmSqlService는 OXM 기술에 특화된 SqlReader를 멤버로 내장, 필요한 설정을 한번에 지정할 수 있는 확장구조
          • 실제 SqlReader와 SqlService를 이용해 SqlService 기능을 구현하는 일은 내부의 BaseSqlService를 만들어서 위임
        • public class OxmSqlService implenets SqlService {
              private final BaseSqlService baseSqlService = new BaseSqlService();
              ...
          
              @PostContruct
              public void loadSql() {
                  this.baseSqlService.setSqlReader(this.oxmSqlReader);
                  this.baseSqlService.setSqlRegistry(this.sqlRegistry);
          
                  this.baseSqlService.loadSql(); // SQL 등록 초기화 위임
              }
          
              public String getSql(String key) throws SqlRetirevalFailureException {
                  return this.baseSqlService.getSql(key);
              }
          }
          • 중복 코드가 제거되고 SqlReader와 SqlRegistry와 관련 로직에 변경점이 생기면 BaseSqlService만 수정해주면 되게끔 수정됨

    7.3.3 리소스 추상화

    • OxmSqlReader와 XmlSqlReader의 공통 문제점
      • SQL 매핑 정보가 담긴 XML 파일 이름을 프로퍼티로 외부에서 지정할 수는 있지만, UserDao 클래스와 같은 classpath에 존재하는 파일로 제한됨
        • classpath 루트 등에 있는 XML 파일을 읽는 경우나 서버나 개발 시스템의 특정 디렉토리에 있는 파일을 읽어오지 못하는 등 문제점 존재
        • URL 클래스를 사용해서 원격 리소스에 접근이 가능하지만, 클래스패스 안에 존재하는 리소스나 서블릿 컨텍스트의 리소스 등을 지정할 수 없고 존재 여부를 미리 확인할 수 없음
      • 위의 문제점(리소스 접근법)을 추상화를 통해 해결하기
    • 리소스
      • 리소스 접근 API를 추상화한 Resource라는 추상화 인터페이스 사용
      • public interface Resource extends InputStreamSource {
            boolean exists();
            boolean isReadable();
            boolean isOpen();
        
            Url getURL() throws IOException;
            Url getURI() throws IOException;
            File getFile() throws IOException; // JDK의 URL, URI, FIle 형태로 전환 가능한 리소스에 사용
        
            Resource createRelative(String relativePath) throws IOException;
        
            long lastModified() throws IOException;
            String getFileName();
            String getDescription(); // 이상 3개의 메소드는 리소스에 대한 이름 및 부가정보 조회
        }
        
        public interface InputStreamSource {
            InputStream getInpuStream() throws IOException; // 모든 리소스를 InputStream 형태로 가져옴
        }
        • Resource는 스프링에서 빈이 아닌 값으로 취급됨
          • OXM이나 트랜잭션처럼 서비스를 제공해주는 것이 아닌 단순한 정보를 가진 값으로 지정
    • 추상화 적용 방법에 대한 고민
      • 빈으로 등록하면 리소스 타입에 따라 각기 다른 Resource 인터페이스의 구현 클래스를 지정해주면 됨
        • HTTP로 가져올 리소스라면 HttpResource 같은 클래스를 빈의 클래스로 지정하여 사용하면 됨
      • Resource는 빈으로 등록되지 않기 때문에 불가능
        • property의 value 애트리뷰트에 넣으려해도 value는 단순한 String 값만 가능하여 불가능
    • 리소스 로더
      • ResourceLoader
        • 문자열 안에 리소스 종류와 리소스의 위치를 함께 표현하여 Resource 오브젝트를 선언
        • 문자열로 정의된 리소스를 실제 Resource 타입 오브젝트로 변환해줌
        • public interface ResourceLoader {
              Resource getResource(String location);
          }
          • 접두어(리소스 종류)의 여부에 따른 리소스를 가져오는 방식
            • 접두어가 없는 경우
              • 리소스 로더의 구현 방식에 따라 달라짐
            • 접두어가 있는 경우
              • 리소스 로더의 종류와 상관없이 접두어가 의미하는 위치와 방법을 이용해 리소스를 가져옴
        • 접두어 설명
          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.dat HTTP 프로토콜을 사용해 접근할 수 있는 웹상의 리소스를 지정
          ftp: 또한 사용 가능
        • 스프링의 애플리케이션 컨텍스트
          • ResourceLoader의 대표적인 예
          • ApplicationContext 인터페이스는 ResourceLoader 인터페이스를 상속
          • 모든 애플리케이션 컨텍스트는 리소스 로더
            • ex) 스프링 설정 정보가 담긴 XML 파일도 리소스 로더를 이용해 Resource 형태로 Load
            • ex) 애플리케이션 컨텍스트가 외부에서 읽어오는 모든 정보는 리소스 로더를 사용하게 되어있음
            • property 태그의 value 문자열로 된 리소스 정보 또한 Resource 오브젝트로 변환해서 프로퍼티에 주입할 때도애플리케이션 컨텍스트 자신이 리소스 로더로 변환과 로딩을 담당
    • Resource를 이용해 XML파일 가져오기
      • OxmSqlService에 Resource 적용하기
        • SQL 매핑정보가 담긴 파일을 다양한 위치에서 가져오도록 수정
        • sqlmapFile 프로퍼티를 String 타입에서 Resource 타입으로 수정
        • sqlmapFile 이름을 sqlmap으로 수정
          • 꼭 파일에서 읽어오지 않을 수 있기 때문
        • Resource 타입은 실제 소스가 어떤 것이든 getInputStream() 메소드로 스트림을 가져올 수 있음
          • SteramSource 클래스를 사용하여 OXM 언마샬러가 필요로 하는 Source 타입으로 만들어 사용
      • public class OxmSqlService implenets 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.getInpuStream());
                    } catch (IOException e) {
                        throw new IllegalArgumentException(this.sqlmapFile + "을 가져올 수 없습니다.", e);
                    }
                }
            }
        }
        
        • Resource 오브젝트는 실제 리소스가 아닌, 리소스에 접근할 수 있는 추상화된 핸들러
          • 오브젝트가 생성이 되어도 실제로 리소스가 존재하지 않을 수 있음
        • ClassPathResource를 사용하여 코드에서 클래스패스 리소스를 바로 지정(default 값 지정)
          • 문자열로 지정하는 경우는 리소스 로더가 인식할 수 있는 문자열로 표현
          • <bean id="sqlService" class="springbook.user.sqlservice.OxmSqlService">
                <property name="unmarshaller" ref="unmarshaller" />
                <!-- 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="http://www.epril.com/resources/sqlmap.xml" />
            </bean>
          • classpath:
            • 클래스패스 루트로부터 상대적 위치
          • file:
            • 파일 시스템의 루트 디렉토리부터 시작하는 파일 위치
          • http://
            • HTTP 프로토콜로 접근하여 웹 리소스 접근
    반응형

    댓글

Designed by Tistory.