ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 7장) 7.1 SQL과 DAO의 분리 ~ 7.2 인터페이스의 분리와 자기참조 빈
    Java & Spring/토비의 스프링 3.1 2021. 8. 6. 14:09
    반응형

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

    • 스프링의 모든 기술은 객체지향적인 언어의 장점을 적극적으로 활용해서 코드를 작성하도록 도와줌
    • 앞서 학습한 3대 핵심기술인 IoC/DI, 서비스 추상화, AOP를 Application 개발에 활용해서 새로운 기능을 만들어보고, 스프링의 개발철학과 추구하는 가치, 스프링 사용자에게 요구되는 게 무엇인지 알아보자

    7.1 SQL과 DAO의 분리

    • DAO에서 SQL 분리하기
      • 반복적인 JDBC 작업 흐름은 템플릿, 트랜잭션과 예외처리 작업은 서비스 추상화와 AOP로 DAO로 부터 분리했다
      • DAO는 데이터를 가져오고 조작하는 작업의 인터페이스 역할
      • 데이터 엑세스 로직이 바뀌지 않더라도 DB 테이블, 필드 이름과 SQL 문장의 변경이 일어나는 경우, 현재 구조에서는 DAO를 수정해야함
        • 번거로울 뿐만 아니라, 여러 사이드 이펙트가 존재

    7.1.1 XML 설정을 이용한 분리

    • 개별 SQL 프로퍼티 방식
      • 필드 매핑을 위해 사용하는 userMapper도 SQL은 아니지만 필드 이름을 가지고 있음
        • 우선, userMapper를 제외하고 순수한 SQL 문장만 작업
        • 기존에 작업했던add() 메소드의 SQL을 외부로 빼내보기
          • public class UserDaoJdbc implemnets UserDao {
                private String sqlAdd;
            
                public void setSqlAdd(String sqlAdd) {
                    this.sqlAdd = sqlAdd;
                }
            }
            
            /// UserService
            public void add(User user) {
                this.jdbcTemplate.update(
                    this.sqlAdd,
                    user.getId(), //... data
                );
            }
          • <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(?,?,?,?,?,?,?)" />
            </bean>
          • sql 문장을 userDao 빈의 프로퍼티로 등록하고, DI 받아서 사용하도록 수정
        • 스트링 값을 외부에서 DI해오기 때문에 SQL을 분리해서 관리할 수 있지만, 매번 새로운 SQL이 필요할 때마다 프로퍼티를 추가하고 DI 변수와 setter 메소드를 만들어야하는 단점이 존재
    • SQL 맵 프로퍼티 방식
      • SQL이 많이질수록 DAO에 DI용 프로퍼티를 추가하기 힘들어짐
      • SQL을 하나의 컬레션으로 담아두는 방법을 이용해보자
        • public class UserDaoJdbc implements UserDao {
              private Map<String, String> sqlMap;
          
              public void setSqlMap(Map<String, String> sqlMap) {
                  this.sqlMap = sqlMap;
              }
          }
          // UserService
          public void add(User user) {
              this.jdbcTemplate.update(
                  this.sqlMap.get("add"),
                  // data
              );
          }
        • <bean id="userDao" class="springbook.user.dao.UserDaoJdbc">
              <property name="dataSource" ref="dataSource" />
              <property name="sqlMap">
                  <map>
                      <entry key="add" value="nsert into users(id, name, password, email, level, login, recommend) values(?,?,?,?,?,?,?)" />
                      <entry key="get" value="select * from users where id = ?" />
                  </map>
              </property>
          </bean>
      • Map은 하나 이상의 복잡한 정보를 담고 있어서, property의 value 애트리뷰트로만 정의할 수 없음
        • 스프링이 제공하는 <map> 태그를 사용해서 적용

    7.1.2 SQL 제공 서비스

    • 앞서 만든 SQL 맵 프로퍼티 방식에서 여전히 문제점이 존재
      • SQL과 DI 설정정보가 섞여있으면 보기에도 지저분하고 관리하기에도 좋지 않음
        • DAO의 일부인 SQL 문장을 Application 구성정보를 가진 설정정보와 함께 두는 것은 바람직하지 못함
      • 스프링의 설정파일로부터 생성된 오브젝트와 정보는 애플리케이션을 다시 시작하기 전에는 변경이 매우 어려움
        • 싱글톤인 DAO의 인스턴스 변수에 접근해서 실시간으로 내용을 수정하기에 복잡하기도 하고, 맵 내용을 수정할 경우 동시성 문제를 일으킬 수 있음
    • DAO가 사용할 SQL을 제공해주는 기능을 독립시켜보자
    • SQL 서비스 인터페이스
      • SQL에 대한 키 값을 전달하면 그에 해당하는 SQL을 반환하도록 설계
      • package springbook.user.sqlservice;
        
        public interface SqlService {
            String getSql(String key) throws SqlRetrievalFailureException; // 런타임 예외로, 복구할 필요가 없으면 무시
        }
        
        // sql 조회 실패 예외
        public class SqlRetrievalFailureException extends RuntimeException {
            public SqlRetrievalFailureException(String message) {
                super(message);
            }
        
            public SqlRetrievalFailureException(String message, Throwable cause) {
                super(message, cause); // 근본 원인이 되는 중첩 예외 저장
            }
        }
      • public class UserDaoJdbc implements UserDao {
            private SqlService sqlService;
        
            public void setSqlService(SqlService sqlService) {
                this.sqlService = sqlService;
            }
        
            public User get(String id) {
                return this.jdbcTemplate.queryForObject(this.sqlService.getSql("userGet"),
                new Object[] {id}, this.userMapper);
            }
        } 
        
    • 스프링 설정을 사용하는 단순 SQL 서비스
      • map을 이용한 sqlService 구현체 구현 및 설정 수정
      • 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 SqlRetrievalFailureException {
                Optional<String> sql = Optional.ofNullable(sqlMap.get(key));
                return sql.orElseThrow(() -> new SqlRetrievalFailureException(key + "에 대한 SQL을 찾을 수 없습니다"));
            }
        }
      • <bean id="userDao" class="springboot.user.dao.UserDaoJdbc">
            <property name="dataSource" ref="dataSource" />
            <property name="sqlService" ref="sqlService" />
        </bean>
        <bean id="sqlService" class="springbook.user.sqlservice.simpleSqlService">
            <property name="sqlMap">
                <map>
                    <entry key="add" value="nsert into users(id, name, password, email, level, login, recommend) values(?,?,?,?,?,?,?)" />
                    <entry key="get" value="select * from users where id = ?" />
                </map>
            </property>
        </bean>
      • 앞선 설정과 큰 차이가 없어보이지만, UserDao를 포함한 모든 DAO는 이제 SQL을 어디에 저장해두고 가져오는지에 대해 전혀 신경쓸 필요가 없어짐
        • 동시에 sqlService 빈에는 DAO에는 전혀 영향을 주지 않은 채, 다양한 방법으로 구현된 SqlService 타입 클래스를 적용할 수 있음

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

    • SqlService 인터페이스 구현 방법에 대한 고민

    7.2.1 XML 파일 매핑

    • 스프링의 XML 설정파일에서 <bean> 태그 안에 SQL 정보를 넣어놓고 활용하기보다, SQL을 저장해두는 전용 포맷을 가진 독립적인 파일을 이용하는 것이 더욱 바람직
    • JAXB
      • XML에 담긴 정보를 파일에서 읽어오는 방법 중 하나
      • JAVA 8까지 유지되었고, 9와 10에서 DEPRECATED, 11부터 더이상 지원 X
      • JAXB를 사용했던 이유
        • DOM과 같은 전통 XML API와 달리 XML 문서정보를 거의 동일한 구조의 오브젝트로 직접 매핑
        • XML 문서의 구조를 정의한 스키마를 이용해서, 매핑할 오브젝트의 클래스까지 자동으로 만들어주는 컴파일러 제공
        • 스키마 컴파일러를 통해 자동생성된 오브젝트에는 매핑정보가 애노테이션으로 담겨있음
        • JAXB API는 애노테이션에 담긴 정보를 이용해서 XML과 매핑된 오브젝트 트리 사이의 자동변환 작업을 수행
    • SQL 맵을 위한 스키마 작성과 컴파일
      • 키와 SQL 정보를 담은 <sql> , <sqlmap> 태그를 만들어보자
      • <sqlmap>
            <sql key="userAdd">insert into users(...) ...</sql>
            <sql key="userGet">select * from users ...</sql>
        </sqlmap>
    • <?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>
                      <elemnet name="sql" maxOccurs="unbounded" type="tns:sqlType" />
                      <!-- 필요한 개수만큼 <sql>을 포함할 수 있게하기 위한 maxOccurs="unbounded" 설정 -->
                  </sequence>
              </complexType>
          </element>
      
          <!-- <sql>에 대한 정의 -->
          <complexType name="sqlType"> 
              <simpleContent>
                  <extension base="string">
                      <attribute name="key" use="required" type="string" />
                  </extension>
              </simpleContent>
          </complexType>
      </schema>
    • JAXB 컴파일러로 위의 스키마 파일을 컴파일해보기
    - `xjc -p springboo.user.sqlservice.jaxb sqlmap.xsd -d src`
      - 생성할 클래스 패키지, 변환한 스키마 파일, 생성된 파일이 저장될 위치 순서
      - 컴파일 이후 내부적으로 List를 통해 sql들을 담게되고, 변환 작업에서 참고할 정보를 애노테이션으로 담고있음
      - 또한, sql 내부에는 key와 value가 저장되어 required 설정이 되어있는 클래스 파일을 확인할 수 있음
    • 언마샬링
      • XML 문서를 읽어서 자바 오브젝트로 변환하는 것
        • 반대로, 바인딩 오브젝트를 XML 문서로 변환하는 것을 마샬링이라고 부름
      • JAXB AIP의 사용법을 익히기 위한 학습 테스트
      • public class JaxbTest {
            @Test
            public void readSqlmap() throws JAXBException, IOException {
                String contextPath = Sqlmap.class.getPackage().getName();
                JAXBContext context = JAXBContext.newInstance(contextPath);
        
                Unmarshaller unmarshaller = context.createUnmarshaller();
        
                Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal(getClass().getResourceAsStream("sqlmap.xml"));
                List<SqlType> sqlList = sqlmap.getSql();
        
                assertThat(sqlList.size(), is(3));
                assertThat(sqlList.get(0).getKey(), is("add"));
                assertThat(sqlList.get(0).getValue(), is("insert"));
                assertThat(sqlList.get(1).getKey(), is("get"));
                assertThat(sqlList.get(1).getValue(), is("select"));
                assertThat(sqlList.get(2).getKey(), is("delete"));
                assertThat(sqlList.get(2).getValue(), is("delete"));
            }
        }
        

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

    • SQL 맵 XML 파일
      • 기존에 <map>으로 만들어뒀던 SQL을 <sqlmap>, <sql> 태그를 사용하도록 sqlmap.xml로 생성하기
      • <?xml version="1.0" encoding="UTF-8"?>
        <schema 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="userAdd">insert into users(...) ...</sql>
            <sql key="userGet">select * from users ...</sql>
        </schema>
    • XML SQL 서비스
      • 위에서 작업한 sqlmap.xml의 SQL을 가져와 DAO에 제공해주는 SqlService 인터페이스의 구현 클래스 구현하기
        • 작업에 앞선 고민
          • 언제 JAXB를 사용해서 XML 문서를 가져올지에 대한 고민
            • 매번 XML 파일을 읽는 것은 비효율적, 특별한 이유가 없는 한 XML 파일은 한번만 읽도록 해야함
            • XML 파일로 부터 읽은 내용을 어딘가에 저장해두고 DAO 요청이 올 때 마다 사용
            • 라이프사이클에 대한 이해가 부족하니, 먼저 생성자에 초기 작업을 구현
          • Sql 문장 조회 효율성
            • 매번 검색을 위해 List 모두를 검사하는 방법은 비효율적
            • Map 타입 오브젝트에 저장해두고 사용하도록 설정
      • 생성자에서 JAXB를 이용해 XML로 된 SQL 문서를 읽어들이고, 변환된 Sql 오브젝트를 맵으로 옮겨 저장해뒀다가, DAO의 요청에 따라 SQL을 찾아서 전달하는 방식으로 SqlService 구현
        • public class XmlSqlService implements SqlService {
              private Map<String, String> sqlMap;
          
              public XmlSqlService() {
                  String contextPath = Sqlmap.class.getPackage().getName();
                  try { 
                      JAXBContext context = JAXBContext.newInstance(contextPath);
                      Unmarshaller unmarshaller = context.createUnmarshaller();
                      InputStream is = UserDao.class.getResourceAsStream("sqlmap.xml"); // UserDao와 같은 클래스패스
                      Sqlmap unmarshalSqlmap = (Sqlmap) unmarshaller.unmarshal(is);
          
                      sqlMap = unmarshalSqlmap.getSql().stream()
                          .collect(Collectors.toMap(SqlType::getKey(), SqlType::getValue()));
                  } catch (JAXBException e) {
                      throw new RuntimeException(e);
                  }
              }
          
              public String getSql(String key) throws SqlRetrievalFailureException {
                  Optional<String> sql = Optional.ofNullable(sqlMap.get(key));
                  return sql.orElseThrow(() -> new SqlRetrievalFailureException(key + "에 대한 SQL을 찾을 수 없습니다"));
              }
          }
        • <bean id="sqlService" class="springbook.user.sqlservice.XmlSqlService">
          </bean>
          • sqlService 설정 변경

    7.2.3 빈의 초기화 작업

    • XmlSqlService의 개선사항
      • 생성자에서 예외가 발생할 수 있는 복잡한 초기화 작업을 다루는 것은 좋지 않음
        • 오브젝트 생성 중 생성자에서 발생하는 예외는 다루기 힘들고, 상속하기 불편하며, 보안 문제가 존재
        • 초기 상태를 가진 오브젝트를 만들어두고 별도의 초기화 메소드를 사용하는 것이 바람직
      • 읽어들일 파일의 위치와 이름이 코드에 고정되어 있음
        • 코드의 로직과 여타 이유로 바뀔 가능성 있는 내용은 외부에서 DI로 설정해줄 수 있게 하는 것이 바람직
      • 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);
                ...
            }
        }
        
      • 초기화 메소드인 loadSql() 메소드는 언제 실행돼야하고, 어떻게 실행할까?
        • XmlSqlService 오브젝트는 빈, 따라서 스프링이 제어권을 가지고 있음
        • 스프링은 빈 오브젝트를 생성하고 DI 작업을 수행해서 프로퍼티를 모두 주입한 뒤, 미리 지정한 초기화 메소드를 호출해주는 기능이 존재
        • 애노테이션을 이용한 빈 설정을 지원해주는 빈 후처리기를 사용
          • <bean>태그를 이용해 하나씩 등록할 수도 있지만, context 스키마의 annotation-config 태그로 편리하게 이용
          • <beans ...
                   xmlns:context="http://www.springframwork.org/schema/context"
                   xsi:schemaLocation="...
                                       http://www.springframework.org/schema/context
                                       http://www.springframework.org/schema/context/spring-context-3.0.xsd
                                       ">
                ... <!-- 트랜잭션 후처리기 등록 -->
            
                <context:annotation-config /> <!-- 코드의 애노테이션을 이용해서 부가적인 빈 설정 or 초기화 작업을 해주는 후처리기 등록 -->
            </beans>
            • context:annotation-config 태그에 의해 등록되는 빈 후처리기는 빈 설정에 사용되는 애노테이션 제공하는데, 그 중 @PostConstruct 애노테이션을 사용
        • @PostConstruct
          • JAVA 8까지 유지되었고, 9와 10에서 DEPRECATED, 11부터 더이상 지원 X
          • 빈 오브젝트의 초기화 메소드를 지정하는데 사용
          • @PostConstruct 를 초기화 작업을 수행할 메소드에 부여해주면 스프링은 XmlSqlService 클래스로 등록된 빈의 오브젝트를 생성하고 DI 작업을 마친 후, @PostConstruct가 붙은 메소드를 자동으로 실행
          • public class XmlSqlService implements SqlService {
                ...
                @PostConstruct
                public void loadSql() { ... }
            }
            
          • <bean id="sqlService" class="springbook.user.sqlservice.XmlSqlService">
                <property name="sqlmapFile" value="sqlmap.xml" />
            </bean>
            • sqlmapFile 프로퍼티 value는 XML 파일의 클래스 패스로, UserDao 인터페이스의 패키지로부터 상대 위치를 지정 가능
            • 여기서는 UserDao와 같은 클래스패스에 있으므로 파일 이름만 지정
        • 스프링 컨테이너의 초기 작업 순서
          1. xml 빈 설정을 읽음
          2. 빈의 오브젝트를 생성
          3. 프로퍼티에 의존 오브젝트 또는 값을 주입
          4. 빈이나 태그로 등록된 후처리기를 동작시킨다.
            코드에 달린 애노테이션에 대한 부가작업 진행

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

    • 위에서 작성한 코드는 SQL을 가져오는 방법에 대해 특정 기술에 고정되어 있음
      • 문제점
        • XML 대신 다른 포맷 파일에서 SQL을 읽어오는 경우
        • XML에서 SQL을 가져오는 방법은 변하지 않지만, 가져온 SQL 정보를 HashMap 타입 컬렉션이 아닌 다른 방식으로 저장해두고 이를 검색해서 가져오는 경우
        • 위의 두 경우 모두 코드를 고치거나 새로 만들어야함
      • XmlSqlService가 변경되는 이유가 두 가지라면 단일 책임 원칙을 위반
        • 그렇다고 한 가지 기술의 변화 때문에 아예 새로운 클래스를 만들면 상당 부분의 코드가 중복됨
    • 책임에 따른 인터페이스 정의
      • 독립적으로 변경 가능한 책임
        • SQL 정보를 외부의 리소스로부터 읽어오는 것
          • SQL이 담겨 있는 리소스가 어떤 것이든 상관없이 애플리케이션에서 활용 가능하도록 메모리에 읽어들이는 것은 하나의 책임
        • 읽어온 SQL을 보관해두고 있다가 필요할 때 제공해주는 것
        • 부가적인 책임
          • 서비스를 위해 한 번 가져온 SQL을 필요에 따라 수정할 수 있게 하는 기능
            • 시스템 운영 중, 서버를 재시작하거나 애플리케이션을 재설치하지 않고도 SQL을 긴급히 변경해야 하는 경우를 위한 경우
      • 변경 가능한 기능은 전략패턴을 적용
        • DAO 관점에서는 SqlService라는 인터페이스를 구현한 오브젝트에만 의존
        • SqlService의 구현 클래스가 변경 가능한 책임을 가진 SqlReader와 SqlRegistry 두 가지 타입의 오브젝트를 사용하도록 수정
      • 고려 사항
        • SqlReader가 읽어오는 SQL 정보를 다시 SqlRegistry에 전달해서 등록하는데, 어떻게 전달할까??
          • SqlService가 SqlReader에게 정보를 전달받은 뒤, SqlRegistry에 다시 전달해줄 필요가 없음
          • 정보를 전달하는 것이 전부라면, SqlService가 중간 과정에서 빠지는 방법도 생각 가능
            • SqlReader에게 SqlRegistry 전략을 제공해주어 SQL 정보를 SqlRegistry에 저장하는 것이 좋은 방법
        • SqlReader가 제공하는 메소드의 리턴 타입은 무엇으로 해야할까??
          • 기존에 전달하던 Map 방식은 정보 전달만을 위해 일시적으로 Map 타입의 형식을 갖도록 만들어야 한다는 것이 불편
        • SqlReader와 SqlRegistry는 각각 구현 방식을 독립적으로 유지하며 꼭 필요한 관계만 가지고 협력해서 일을 할 수 있는 구조로 구현
          • SqlReader가 사용할 SqlRegistry 오브젝트를 제공해주는 건 SqlService의 코드가 담당
          • SqlRegistry가 일종의 콜백 오브젝트처럼 사용된다고 생각
    • SqlRegistry 인터페이스
      • public interface SqlRegistry {
            void registerSql(String key, String sql);
        
            String findSql(String key) throws SqlNotFoundException;
        }
        • SQL을 등록하고 검색하는 두 가지 기능 정의
        • 레지스트리를 여러개 두는 방식을 사용한다면 한 레지스트리에서 검색이 실패할 경우, 다른 레지스트리에 검색을 시도할 수 있음
          • 하지만, 이번 예시에서는 하나의 레지스트리를 두기 때문에, 코드에 버그가 있거나 설정에 문제가 있을 때 발생하는 문제를 복구할 가능성이 적다고 판단되어 런타임 예외로 만듦
    • SqlReader 인터페이스
      • public interface SqlReader {
            void read(SqlRegistry sqlRegistry);
        }
        • SqlReader는 SqlRegistry 오브젝트를 메소드 파라미터로 DI받아 읽어들인 SQL을 등록하는 데 사용

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

    • 다중 인터페이스 구현과 간접 참조
      • SqlService의 구현 클래스는 SqlReader와 SqlRegistry 두 개의 프로퍼티를 DI 받는 구조로 구현
      • 모든 클래스가 인터페이스에 의존하고 있는데, 인터페이스에만 의존해야 스프링의 DI를 적용 가능
        • DI를 적용하지 않아도, 자신이 사용하는 오브젝트 클래스가 어떤 것인지 알지 못하게 만드는 것이 좋음
          • 자유롭게 확장할 기회 제공
      • 인터페이스 상속의 장점
        • 하나의 클래스가 여러 개의 인터페이스를 상속해서 여러 종류의 타입으로 존재 가능
          • 같은 타입으로 존재하지만 다른 구현을 가진 오브젝트를 만들 수 있다는 다형성을 활용할 수 있음
      • 같은 클래스 코드지만 책임이 다른 코드는 직접 접근하지 않고 인터페이스를 통해 간접적으로 사용하도록 코드 구현
    • 인터페이스를 이용한 분리
      • SqlReader와 SqlRegistry 두 개의 인터페이스 타입 오브젝트에 의존하는 구조로 구현
        • public class XmlSqlService implements SqlService {
              private SqlReader sqlReader;
              private SqlRegistry sqlRegistry;
          
              public void setSqlReader(SqlReader sqlReader) {
                  this.sqlReader = sqlReader;
              }
          
              public void setSqlRegistry(SqlRegistry sqlRegistry) {
                  this.sqlRegistry = sqlRegistry;
              }
          }
      • XmlSqlService 클래스가 SqlRegistry를 구현하도록 설정
        • public class XmlSqlService implements SqlService, SqlRegistry {
              private Map<String, String> sqlMap = new HashMap<>(); // sqlRegistry 구현의 일부로 외부에서 직접 접근 불가
          
              @Override
              public String findSql(String key) throws SqlNotFoundException {
                  String sql = sqlMap.get(key);
                  if (sql == null) {
                      throw new SqlNotFoundException("Not Found Sql From " + key);
                  }
                  return sql;
              }
          
              @Override
              public void registSql(String key, String sql) {
                  sqlMap.put(key, sql);
                  // HashMap 저장소를 사용하는 구체적인 구현 방법에서 독립되도록 인터페이스의 메소드로 접근하게 해줌
              }
          }
        • sqlMap은 SqlRegistry 구현의 일부이므로 SqlRegistry 구현 메소드가 아닌 메소드에서 직접 사용해서는 안됨
      • XmlSqlService 클래스가 SqlReader를 구현하도록 설정
        • public class XmlSqlService implements SqlService, SqlRegistry, SqlReader {
              ...
              private String sqlmapFile;
              public void setSqlmapFile(String sqlmapFile) { // sqlMapFile은 SqlReader 구현의 일부, SqlReader 구현 메소드를 통하지 않고는 접근 금지
                  this.sqlmapFile = sqlmapFile;
              }
          
              @Override
              public void read(SqlRegistry sqlRegistry) { // loadSql()에 있던 코드를 SqlReader 메소드로
                  String contextPath = Sqlmap.class.getPackage().getName();
          
                  try {
                      JAXBContext context = JAXBContext.newInstance(contextPath);
                      Unmarshaller unmarshaller = context.createUnmarshaller();
                      InputStream is = UserDao.class.getResourceAsStream(sqlmapFile);
                      sqlmap.getSql().forEach(sql -> sqlRegistry.registerSql(sql.getKey()), sql.getValue());
                  } catch (JAXBException e) {
                      throw new RuntimeException(e);
                  }
              }
          }
          • 어떻게 읽어오는지 SqlReader 메소드 뒤로 숨기고, 어떻게 저장해둘지 SqlRegistry 타입 오브젝트가 처리
      • @PostConstruct가 달린 빈 초기화 메소드와 SqlService 인터페이스에 선언된 getFinder()를 sqlReader와 sqlRegistry를 이용하도록 수정
        • public class XmlSqlService implements SqlService, SqlRegistry, SqlReader {
              ...
              @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);
                  }
              }
          }
          • sqlReader와 sqlRegistry 두 전략을 이용하도록 재구성
    • 자기참조 빈 설정
      • 클래스는 하나뿐이고 빈도 하나만 등록할 것이지만, 세 개의 빈이 등록된 것처럼 SqlService 빈이 SqlRegistry와 SqlReader를 주입받도록 설정
        • <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>
          • 위의 설정을 통해 sqlService를 구현한 메소드와 초기화 메소드는 외부에서 DI 된 오브젝트라고 생각하고 자신의 메소드에 접근
      • 자기 참조 빈은 흔히 사용되는 방법이 아님
        • 책임이 다르다면 클래스를 구분하고 각기 다른 오브젝트로 만드는 것이 자연스러움
        • 하지만, 책임과 관심사가 복잡하게 얽혀 있어서 확장이 힘들고 변경에 취약한 구조의 클래스를 유연한 구조로 만드려고 할 때, 처음 시도할 수 있는 방법

    7.2.6 디폴트 의존관계

    • 각 인터페이스를 구현한 코드를 분리해두고 DI로 조합하기
    • 확장 가능한 기반 클래스
      • 자기참조 빈으로 만들었던 XmlSqlService 코드에서 의존 인터페이스와 구현 코드 제거
      • public class BaseSqlService implements SqlService {
            protected SqlReader sqlReader;
            protected SqlRegistry sqlRegistry;  // BaseSqlService는 상속을 통해 확장해서 사용하기에 적합, 서브 클래스에서 필요한 경우 접근할 수 있도록 protected로 선언
        
            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);
                }
            }
        }
      • public class HashMapSqlRegistry implements SqlRegistry {
            private Map<String, String> sqlMap = new HashMap<>();
        
            public String findSql(String key) throws SqlNotFoundException {
                String sql = sqlMap.get(key);
        
                if (sql == null) {
                    throw new SqlNotFoundException("Not Found Sql From " + key);
                }
                return sql;
            }
        
            public void registerSql(String key, String sql) {
                sqlMap.put(key, sql);
            }
        }
        
        public class JaxbXmlSqlReader implements SqlReader {
            private String sqlmapFile;
        
            public void setSqlmapFile(String sqlmapFile) {
                this.sqlmapFile = sqlmapFile;
            }
        
            public void read(SqlRegistry sqlRegistry) {
                ... // jaxb api로 sql을 읽어오는 코드
            }
        }
      • <bean id="sqlService" class="springbook.user.sqlservice.XmlSqlService">
            <property name="sqlReader" ref="sqlReader" />
            <property name="sqlRegistry" ref="sqlRegistry" />
        </bean>
        
        <bean id="sqlReader" class="springbook.user.sqlservice.JaxbXmlSqlReader">
            <property name="sqlmapFile" value="sqlmap.xml" />
        </bean>
        
        <bean id="sqlRegistry" class="springbook.user.sqlservice.HashMapSqlRegistry">
        </bean>
        • 클래스 분리에 따른 각각 빈 설정
    • 디폴트 의존관계를 갖는 빈 만들기
      • 특정 의존 오브젝트가 대부분의 환경에서 거의 default라고 해도 좋을 만큼 기본적으로 사용될 가능성이 있다면, default 의존관계를 갖는 빈을 만드는 것을 고려
        • 디폴트 의존관계 = 외부에서 DI 받지 않는 경우, 기본적으로 자동 적용되는 의존관계
        • public classDefaultSqlService extends BaseSqlService {
              public DefaultSqlService() {
                  setSqlReader(new JaxbXmlSqlReader());
                  setSqlRegistry(new HashMapSqlRegistry());
              }
          }
        • <bean id="sqlService" class="springbook.user.sqlservice.DefaultSqlService" />
        • 위 처럼 설정해도 테스트에는 실패
        • DefaultSqlService 내부에서 생성하는 JaxbXmlSqlReader의 sqlmapFile 프로퍼티가 비어있기 때문
        • 디폴트 의존 오브젝트로 직접 넣어줄 때는 프로퍼티를 외부에서 직접 지정 불가능
          • 빈으로 등록되는 것은 DefaultSqlService 뿐
      • 문제 해결
        • sqlmapFile을 DefaultSqlService의 프로퍼티로 지정하는 방법
          • JaxbXmlSqlReader는 디폴트 의존 오브젝트에 불과하기 때문에 부적절
          • 명시적인 설정이 없는 경우에 기본적으로 사용하겠다는 의미인데, 반드시 필요하지도 않은 sqlmapFile을 프로퍼티로 등록해두는 것은 바람직하지 않음
        • sqlmapFile을 JaxbXmlSqlReader에 의해 기본적으로 사용될 만한 default 값을 가지도록 설정
          • SQL 파일 이름을 매번 바꿀 필요가 없고, 관례적으로 사용할 만한 이름으로 default 값을 정해준다면, default sqlmapFile 이름도 갖게 있게 되므로 별다른 설정 없이 그대로 사용 가능
        • 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;
              }
          }
          • DI를 사용한다고 해서 항상 모든 프로퍼티 값을 설정에 넣고, 모든 의존 오브젝트를 빈으로 일일이 지정할 필요가 없음
          • DefaultSqlService처럼 자주 사용되는 의존 오브젝트는 미리 지정한 디폴트 의존 오브젝트를 설정 없이도 사용할 수 있게 만드는 것은 좋은 방법
      • DefaultSqlService는 BaseSqlService를 상속
        • DefaultSqlService는 BaseSqlService의 sqlReader와 sqlRegistry 프로퍼티를 그대로 갖고 있고, 원한다면 일부 or 모든 프로퍼티 변경 가능
        • 디폴트 의존 오브젝트 대신 사용하고 싶은 구현 오브젝트가 있다면 설정에 프로퍼티를 추가하면 되고, 설정하지 않은 부분은 디폴트가 사용됨
        • <bean id="sqlService" class="springbook.user.sqlservice.DefaultSqlService">
              <propery name="sqlRegistry" ref="ultraSuperFastSqlRegistry" />
          </bean>
      • Default 의존 오브젝트의 단점
        • 설정을 통해 다른 구현 오브젝트를 사용하게 해도 DefaultSqlService는 생성자에서 일단 디폴트 의존 오브젝트를 다 만듦
        • 하지만, 장점이 더욱 많기 때문에 단점을 커버
        • 디폴트로 만드는 오브젝트가 매우 복잡하고 많은 리소스를 소모한다면 디폴트 의존 오브젝트가 아예 만들어지지 않게할 수도 있음
          • ex) @PostContructor 초기화 메소드에서 프로퍼티가 설정됐는지 확인하고, 없다면 default 오브젝트를 만드는 방법
    반응형

    댓글

Designed by Tistory.