ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 7장) 7.4 인터페이스 상속을 통한 안전한 기능확장 ~ 7.5 DI를 이용해 다양한 구현 방법 적용하기
    Java & Spring/토비의 스프링 3.1 2021. 8. 20. 14:09
    반응형

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

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

    • 애플리케이션을 새로 시작하지 않고 특정 SQL의 내용만을 변경하고 싶다면 어떻게 해야 할지 생각해보자

    7.4.1 DI와 기능의 확장

    • 지금까지 적용한 DI는 일종의 디자인 패턴 or 프로그래밍 모델이라는 관점에서 이해하는 것이 자연스러움
      • 스프링과 같은 DI 프레임워크를 적용하고 빈 설정파일로 애플리케이션을 구성했다고 해서 DI를 바르게 활용하고 있다고 볼 수 없음
      • DI의 장점은 DI에 적합한 오브젝트 설계가 요구됨
    • DI를 의식하는 설계
      • 다양한 기능 확장이 가능했던 이유
        • SqlService 내부 기능을 적절한 책임과 역할에 따라 분리
        • 인터페이스 정의로 느슨하게 연결
        • DI를 통해 유연하게 의존관계를 지정
      • DI 덕분에 오브젝트들이 서로 세부적인 구현에 얽매이지 않고 유연한 의존관계로 독립적으로 발전
      • DI 적용 조건
        • 최소한 두 개 이상의 의존관계를 가지고 서로 협력해서 일하는 오브젝트가 필요
        • 적절한 책임에 따라 오브젝트를 분리
      • DI는 런타임 시 의존 오브젝트를 다이내믹하게 연결해줘서 유연한 확장이 목적
    • DI와 인터페이스 프로그래밍
      • 가능한 인터페이스를 사용하고, 두 개의 오브젝트가 인터페이스를 통해 느슨하게 연결
      • 인터페이스 사용 이유
        • 다형성을 얻기 위해
          • 하나의 인터페이스를 통해 여러 개의 구현을 바꿔가면서 사용할 수 있게 하기 위함
        • 인터페이스 분리 원칙을 통해 클라이언트와 의존 오브젝트 사이의 관계를 명확하게 해주기 위해
          • 인터페이스 분리 원칙
            • 오브젝트가 그 자체로 충분히 응집도가 높은 작은 단위로 설계됐더라도, 목적과 관심이 각기 다른 클라이언트가 있다면 인터페이스를 통해 이를 적절하게 분리
          • ex) B오브젝트가 B1과 B2라는 인터페이스를 구현하고 있고, A 오브젝트가 B1을 통해 B 오브젝트를 사용하고 있고있는 경우
            • A는 B1에만 관심이 있는데 B2 인터페이스의 메소드까지 모두 노출되어 B 클래스에 직접 의존할 이유가 없음
            • 따라서 B1을 통해 B를 의존

    7.4.2 인터페이스 상속

    • 오브젝트 기능이 발전하는 과정에서 다른 종류의 클라이언트가 등장하기 때문에, 하나의 오브젝트가 구현하는 인터페이스를 여러 개 만들어서 구분하여 사용
      • 인터페이스를 여러 개 만드는 대신 기존 인터페이스를 상속하여 확장하는 방법이 가끔 사용됨
    • 인터페이스 분리 원칙의 장점
      • 모든 클라이언트가 자신의 관심에 따른 접근 방식을 불필요한 간섭 없이 유지
        • 기존 클라이언트에 영향을 주지 않은 채로 오브젝트의 기능을 확장하거나 수정 가능
      • 또 다른 제 3의 클라이언트를 위한 인터페이스를 가질 수 있음
        • 앞서 작성한 코드를 보면 SqlRegistry의 구현 클래스인 MySqlRegistry의 오브젝트가 또 다른 제 3의 클라이언트를 위한 인터페이스를 가질 수 있음
    • SqlRegistry에 SQL을 변경하는 기능을 넣어 확장하기
      • BaseSqlService 클래스와 그 서브클래스가 존재 ~> SqlRegistry 인터페이스 수정은 바람직하지 않음
        • SQL 조회 서비스인 BaseSqlService 입장에서 SQL 업데이트 기능을 이용하는 클라이언트가 될 이유가 없음
      • 기존의 SqlRegistry 인터페이스를 상속하고 메스드를 추가해서 새로운 인터페이스를 정의하기
      • public interface UpdatableSqlRegistry extends SqlRegistry {
            public void updateSql(String key, String sql) throws SqlUpdateFailureException;
        
            public void updateSql(Map<String, String> sqlmap) throws SqlUpdateFailureException;
        }
      • SQL 변경 요청을 담당하는 SQL 관리용 오브젝트가 있다고 하고, 클래스 이름을 SqlAdminService로 정해 UpdatableSqlRegistry라는 인터페이스를 통해 SQL 레지스트리 오브젝트에 접근하게 하자
      • public class SqlAdminService implements AdminEventListner {
            private UpdatableSqlRegistry updatableSqlRegistry;
        
            public void setUpdatableSqlRegistry(UpdatableSqlRegistry updatableSqlRegistry) {
                this.updatableSqlRegistry = updatableSqlRegistry;
            }
        
            public void updateEventListener(UpdateEvent event) {
                this.updatableSqlRegistry.updateSql(event.get(KEY_ID), event.get(SQL_ID));
            }
        }
      • <bean id="sqlService" class="springbook.user.sqlservice.BaseSqlService">
            ...
            <property name="sqlRegistry" ref="sqlRegistry" />
        </bean>
        <bean id="sqlRegistry" class="springbook.user.sqlservice.MyUpdatableSqlRegistry" />
        
        <bean id="sqlAdminService" class="springbook.user.sqlservice.SqlAdminService">
            <property name="updatableSqlRegistry" ref="sqlRegistry" />
        </bean>
      • BaseSqlService와 SqlAdminService는 동일한 오브젝트에 의존하지만, 각자의 관심과 필요에 따라 다른 인터페이스를 통해 접근
        • 인터페이스를 사용한 DI라 가능

    7.5 DI를 이용해 다양한 구현 방법 적용하기

    • 운영 중인 시스템에서 사용하는 정보를 실시간 변경하는 작업을 만들 때는, 동시성 문제를 가장 먼저 고려할 것

    7.5.1 ConcurrentHashMap을 이용한 수정 가능

    • Default로 사용하던 HashMapRegistry는 멀티스레드 환경에서 동시에 수정을 시도하거나 동시에 요청하는 경우 예상치 못한 결과가 발생할 수 있음
      • 멀티 스레드 환경에서는 Collections.synchronizedMap() 등을 이용해 외부에서 동기화 해줘야 함
      • 하지만, HashMap의 전 작업을 동기화하면 DAO의 요청이 많은 고성능 서비스에서는 성능 문제 존재
      • 동기화 해시 데이터 조작에 최적화된 ConcurrentHashMap을 사용 권장
        • 데이터 조작 시 전체 데이터에 락을 걸지 않고, 조회는 락을 아예 사용 X
    • 수정 가능 SQL 레지스트리 테스트
      • public class ConcurrentHashMapSqlRegistryTest {
            UpdatableSqlRegistry sqlRegistry;
        
            @BeforeEach
            public void setUp() {
                sqlRegistry = new ConcurrentHashMapSqlRegistry();
                sqlRegistry.registerSql("KEY1", "SQL1");
                sqlRegistry.registerSql("KEY2", "SQL2");
                sqlRegistry.registerSql("KEY3", "SQL3");
            }
        
            @Test
            public void find() {
                checkFindResult("SQL1", "SQL2", "SQL3");
            }
        
            private void checkFindResult(String excepted1, String excepted2, String excepted3) {
                assertThat(sqlRegistry.findSql("KEY1"), is(expected1));
                ...
            }
        
            @Test
            public void updateSingle() { // 하나의 SQL 수정 기능 검증
                sqlRegistry.updateSql("KEY2", "Modified2");
                checkFindResult("SQL1", "Modified2", "SQL3");
            }
        
            @Test
            public void updateMulti() { // 동시에 여러개 SQL 수정 기능 검증
                Map<String, String> sqlmap = new HashMap<>(){{
                    put("KEY1", "Modified1");
                    put("KEY3", "Modified3");
                }};
        
                sqlRegistry.updateSql(sqlmap);
                checkFindResult("Modified1", "SQL2" ,"Modified3");
            }
        
            @Test(expected = SqlNotFoundException.class)
            public void unknownKey() { // 주어진 키로 찾지 못하는 예외
                sqlRegistry.findSql("ASDioqwje");
            }
        
            @Test(expected=SqlUpdateFailureException.class)
            public void updateWithNotExistingKey() { // 존재하지 않는 키 SQL 변경 예외
                sqlRegistry.updateSql("SQL9999!@#$", "Modified2");
            }
        }
    • 수정 가능 SQL 레지스트리 구현
      • public class ConcurrentHashMapSqlRegistry implements UpdatableSqlRegistry {
            private Map<String, String> sqlMap = new ConcurrentHashMap<>();
        
            public String findSql(String key) throws SqlNotFOundException { ... }
        
            public void registerSql(String key, String sql) { ... }
        
            public void updateSql(String key, String sql) throws SqlUpdateFailureException {
                if (sqlMap.get(key) == null) {
                    throw new SqlUpdateFailureException("not found sql from key : " + key);
                }
                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());
                }
            }
        }
      • <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" />

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

    • 내장형 DB를 이용해 SQL을 저장하고 수정하게 만들기
      • ConcurrentHashMap은 저장되는 데이터 양이 많아지고 잦은 조회와 변경이 일어날 때, 한계점이 존재
      • 데이터베이스는 인덱스를 이용한 최적화된 검색 지원, 동시에 많은 요청을 처리하면서 안정적인 변경 작업 가능
      • 내장형 DB는 IO로 인해 발생하는 부하가 적어 성능이 뛰어남
        • 메모리에 데이터를 저장하는 방법에 비해 효과적이고 안정적인 방법으로 CRUD 및 최적화 락킹, 격리수준, 트랜잭션 적용 가능
    • 스프링의 내장형 DB 지원 기능
      • 자바 내장형 DB
        • Derby, HSQL, H2를 가장 많이 사용
          • JDBC 드라이버를 제공하고 표준 DB랑 호환 가능
        • 내장형 DB를 위해 다른 서비스 추상화처럼 별도의 레이어와 인터페이스를 제공하지 않지만, 내장형 DB 빌더를 제공
          • URL과 드라이버 등 초기화 기능
          • 초기화 쿼리 실행 기능
          • 모든 준비가 끝나면 내장형 DB에 대한 DataSource 오브젝트를 반환
          • 이후 DataSource를 통해 일반 DB처럼 사용 가능
        • 애플리케이션 안에서 직접 DB를 종료 요청이 가능해야함
          • DataSource 인터페이스를 상속해 shutdown() 메소드를 추가한 EmbeddedDatabase 인터페이스 제공
    • 내장형 DB 빌더 학습 테스트
      • 테이블 생성 쿼리를 담은 schema.sql, 초기화 데이터 생성 쿼리를 담은 data.sql이 준비되어있다고 가정
      • EmbeddedDatabase db;
        SimpleJdbcTemplate template;
        
        @BeforeEach
        public void setUp() {
            db = new EmbeddedDatabaseBuilder()
                .setType(HSQL)
                .addScript("classpath:/springbook/learningtest/spring/embeddeddb/schema.sql")
                .addScript("classpath:/springbook/learningtest/spring/embeddeddb/data.sql")
                .build();
            template = new SimpleJdbcTemplate(db);
        }
        
        @AfterEach
        public void tearDown() {
            db.shutdown();
        }
        • 테스트 메소드마다 EmbeddedDatabaseBuilder가 최종적으로 DataSource 를 상속한 EmbeddedDatabase 타입의 오브젝트를 리턴하여, template 사용에 적용
    • 내장형 DB를 이용한 SqlRegistry 만들기
      • EmbeddedDatabaseBuilder는 직접 빈으로 등록한다고 바로 사용할 수 없음
        • 적절한 메소드를 호출해주는 초기화 코드 필요
        • 초기화 코드가 필요하다면 팩토리 빈으로 만드는 것이 좋음
      • public class EmbeddedDbSqlRegistry implements UpdatableSqlRegistry {
            SimpleJdbcTemplate jdbc;
        
            public void setDataSource(DataSource dataSource) {
                jdbc = new SImpleJdbcTemplate(dataSource);
            }
        
            public void registrySql(String key, String sql) {
                jdbc.update("insert into sqlmap(key_, sql_) values(?,?)", key, sql);
            }
        
            public String findSql(String key) throws SqlNotFoundException {
                try {
                     return jdbc.queryForObject("select sql_ from sqlmap where key_ = ?", String.class, key);
                } catch (EmptyResultDataAcessException e) {
                    throw new SqlNotFoundException("not found sql from key : " + key);
                }
            }
        
            public void updateSql(String key, String sql) throws SqlUpdateFailureException {
                int affected = jdbc.update("update sqlmap set sql_ = ? where key_ = ?", sql, key);
                if (affected == 0) {
                    throw new SqlUpdateFailureException("not found sql from key : " + key);
                }
            }
            public void updateSql(Map<String, String sqlmap) throws SqlUpdateFailureException { ... }
        }
        • 내장형 DB는 EmbeddedDatabase 타입이라고 했는데, 위에서는 DataSource 타입의 오브젝트로 DI
          • 인터페이스 분리 원칙을 지키기 위함
          • SQL 레지스트리는 JDBC를 이용해 DB에 접근할 수만 있으면 되기 때문에 DataSource 인터페이스가 적합
            • 자신이 필요로 하는 기능만 가진 인터페이스를 의존
    • UpdatableSqlRegistry 테스트 코드의 재사용
      • 인터페이스가 같은 클래스라도 구현 방식에 따라 검증 내용 / 테스트 방법이 달라질 수 있고, 의존 오브젝트 구성에 따라 mock이나 stub을 사용
      • ConcurrentHashMapSqlRegistry는 의존 오브젝트가 아예 없고, EmbeddedDbSqlRegisry도 내장형 DataSource 빈을 의존하기는 하지만 테스트 대역으로 대체하기 힘듦
      • UpdatableSqlRegistry 구현 클래스의 오브젝트 생성 부분을 분리하면, 나머지 테스트 코드가 공유 가능
    • XML 설정을 통한 내장형 DB의 생성과 적용
      • jdbc 스키마의 전용 태그 사용
      • <!-- 네임스페이스 선언 -->
        <beans xmlns="http://www.springframework.org/schema/beans"
               ...
               xmlns="http://www.springframework.org/schema/jdbc"
               xsi:schemaLocation="http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
                                   http://www.springframework.org/schema/jdbc"
               ...""></beans>
      • <!-- 내장형 DB 등록 -->
        <jdbc:embedded-database id="embeddedDatabase" type="HSQL">
            <jdbc:script location="classpath:springbook/user/sqlservice/updatable/sqlRegistrySchema.sql" />
        </jdbc:embedded-database>
      • <!-- embeddedDbSqlRegistry 클래스를 이용한 빈 등록 -->
        <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.EmbeddedDbSqlRegistry" >
            <property name="dataSource" ref="embeddedDatabase" />
        </bean>

    7.5.3 트랜잭션 적용

    • 하나의 SQL을 수정할 때는 문제가 없지만 하나 이상의 SQL을 맵으로 전달받아 한 번에 수정해야하는 경우, 심각한 문제 발생
      • 존재하지 않는 키에 대한 예외처리는 되어있지만, 트랜잭션이 적용되어있지 않음
      • 중간에 예외 발생 ~> 수정 사항은 반영
      • HashMap과 같은 컬렉션은 트랜잭션 개념을 적용하기 힘들지만, 내장형 DB는 쉬움
    • 트랜잭션 경계가 DAO 밖에 있고 범위가 넓다면 AOP를 이용
      • SQL 레지스트리라는 제한된 오브젝트 내에서 서비스에 특화된 경우는 트랜잭션 API를 사용
    • 다중 SQL 수정에 대한 트랜잭션 테스트
      • public class EmbeddedDbSqlRegistryTest extends AbstractUpdatableRegistryTest {
            ...
            @Test
            public void transactionalUpdate() {
                checkFind("SQL1", "SQL2", "SQL3");
        
                Map<String, String> sqlmap = new HashMap<>(){{
                    put("KEY1", "Modified");
                    put("KEY9999!@#$", "Modified999"); // 존재하지 않는 키 ~> 실패
                }};
        
                try {
                    sqlRegistry.updateSql(sqlmap);
                    fail(); // 예외가 발생하지 않은 경우 실패
                } catch (SqlUpdateFailureException e) {}
        
                checkFind("SQL1", "SQL2", "SQL3"); // 롤백이 되면 초기 값과 같음
            }
        }
      • 트랜잭션을 아직 적용하지 않아서 실패
    • 코드를 이용한 트랜잭션 적용
      • public class EmbeddedDbSqlRegistry extends AbstractUpdatableRegistryTest {
            SimpleJdbcTemplate jdbc;
            TransactionTemplate transactionTemplate;
        
            public void setDataSource(DataSource dataSource) {
                this.jdbc = new SimpleJdbcTemplate(dataSource);
                transactionTemplate = new TransactionTemplate(new DataSourceTransactionManager(dataSource));
            }
            ...
        
            public void updateSql(final Map<String, String> sqlmap) throws SqlUpdateFailureException {
                transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                    protected void doInTransactionWithoutResult(TransactionStatus status) {
                        for(Map.Entry<String, String> entry : sqlmap.entrySet()) {
                            updateSql(entry.getKey(), entry.getValue());
                        }
                    }
                });
            }
        }
        • PlatformTransactionManager를 직접 사용하기보다 트랜잭션 적용 코드에 템플릿/콜백 패턴을 적용한 TransactionTemplate을 쓰는 것이 더욱 간결
        • 일반적으로는 트랜잭션 매니저를 싱글톤 빈으로 등록
          • 여러 AOP를 통해 만들어지는 트랜잭션 프록시가 같은 트랜잭션 매니저를 공유해야 하기 때문
        • EmbeddedDbSqlRegistry는 내부에서 직접 만들어 사용하는 것이 좋음
          • 내장형 DB에 대한 트랜잭션 매니저를 공유할 필요가 없음
    반응형

    댓글

Designed by Tistory.