Skip to content

Latest commit

 

History

History
399 lines (277 loc) · 24.2 KB

chapter07_스프링핵심기술의응용.md

File metadata and controls

399 lines (277 loc) · 24.2 KB
기능이 같은 여러 가지 기술이 존재한다는 이야기가 나오면 "서비스 추상화"가 떠올라야 한다.
로우레벨의 구체적인 기술과 API에 종속되지 않고 추상화된 레이어와 API를 제공해서 구현 기술에 대해 
독립적인 코드를 작성할 수 있게 해주는 "서비스 추상화"가 필요하다. 

DI의 가치를 제대로 누리기가 쉽진 않다.
DI에 필요한 유연하고 확장성이 뛰어난 오브젝트 설계를 하려면 많은 고민과 학습, 훈련, 경험이 필요하다.
추천하고 싶은 한 가지가 있다면 DI를 의식하면서 설계하는 방식이다.
런타임시에 의존 오브젝트를 다이내믹하게 연결해줘서 유연한 확장을 꾀하는게 목적이기 때문에
항상 확장을 염두해두고 오브젝트 사이의 관계를 생각해야 한다.

"인터페이스 분리 원칙"이 주는 장점은 모든 클라이언트가 자신의 관심에 따른 접근 방식을 불필요한 간섭 없이 유지할 수 있다는 점이다.

어노테이션 같은 메타 정보를 활용하는 프로그래밍 방식은 프로그래밍 언어나 API 사용법 외에 미리 정의된 규칙과 관례를 기억해야하고
메타정보를 보고 프로그램이 어떻게 동작할지 이해해야 하는 부담을 주기도 한다.
"정책을 기억 못 하거나 잘못 알고 있을 경우 의도한 대로 동작하지 않는 코드가 만들어질 수 있다."

스프링 3.1은 XML에서 자주 사용되는 전용 태그를 "@Enable"로 시작하는 어노테이션을 대체할 수 있게 다양한 어노테이션을 제공한다.

7.1 SQL과 DAO의 분리

현재 SQL 변경이 필요한 상황이면 DAO 코드가 수정될 수 밖에 없다.
독립적인 SQL 서비스를 만들어보자.

SQL 서비스 인터페이스 예제

  1. SqlService 인터페이스 추가
  2. SqlRetrievalFailureException 커스텀 예외 추가
    • 조금 더 세세한 예외를 구분하고 싶다면 해당 예외의 서브클래스를 추가로 정의하자
  3. SqlService 구현체 SimpleSqlService 추가

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

JAXB 세팅 예제

XML 파일에서 SQL을 읽어뒀다가 DAO에게 제공해주는 SQL 서비스 구현 클래스를 만들어보자

JAXB
XML에 담긴 정보를 파일에서 읽어오는 방법은 다양하다.
Java Architecture for XML Binding JAXB를 이용하겠다.
JDK 6 이상이라면 java.xml.bind패키지가 포함되어 있다.

  • XML 문서 정보를 트리 구조의 오브젝트로 직접 매핑해준다.
  • XML 문서의 구조를 정의한 스키마를 이용해서 매핑할 오브젝트의 클래스까지 자동으로 만들어주는 컴파일러도 제공해준다.
  • 스키마 컴파일러를 통해 자동 생성된 오브젝트에는 매핑정보가 어노테이션으로 담겨 있다.

JAXB에서는
XML문서를 읽어서 자바의 오브젝트로 변환하는 것을 언마샬링이라고 부른다.
반대로 바인딩 오브젝트를 XML문서로 변환하는 것을 마샬링이라고 한다.

<?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>
                <element name="sql" maxOccurs="unbounded" type="tns:sqlType" />
            </sequence>
        </complexType>
    </element>
    <complexType name="sqlType">
        <simpleContent>
            <extension base="string">
                <attribute name="key" use="required" type="string" />
            </extension>
        </simpleContent>
    </complexType>
</schema>

위의 sqlmap.xsd를 프로젝트 루트에 두고 xjc -p main.java.springbook.chapter07.jaxb sqlmap.xsd -d src 명령어를 실행하면 아래와 같이 실행된다

  • jdk 11로는 되지 않아서 jdk 8로 변경하고 실행했다
    • jdk 11부터는 포함되지 않는다고 한다
    • implementation("javax.xml.bind:jaxb-api:2.3.1") gradle 의존성 추가
스키마의 구문을 분석하는 중...
스키마를 컴파일하는 중...
src/main/java/springbook/chapter07/jaxb/ObjectFactory.java
src/main/java/springbook/chapter07/jaxb/SqlType.java  // <sqlmap>과 <sql>에 대한 바인딩 클래스
src/main/java/springbook/chapter07/jaxb/Sqlmap.java   // <sqlmap>과 <sql>에 대한 바인딩 클래스
src/main/java/springbook/chapter07/jaxb/package-info.java

JAXB 학습 테스트 예제

스프링부트에서는 java 아래 소스 코드는 컴파일 되어서 .class 파일이 되고 그거랑 resources 아래 있는 파일이랑 합쳐서 클래스 패스로 잡히게 됩니다.

  • resources폴더 안에 작성하지 않고 다른 곳에 작성한다면 다 무시한다. 부트에서 하셨으면 자바 폴더와 리소스 폴더가 구분이 되어서 들어가야 합니다. 소스 폴더에 넣은 일반 파일은 클래스패스로 잡히지 않아요.

1. XML파일을 이용하는 SQL서비스 적용 예제

한 XML파일은 한 번만 읽도록 하여, XML파일로 부터 읽은 내용은 어딘가에 저장해두고 DAO에서 요청이 올 때 사용해야 한다.

2. XmlSqlService 빈의 초기화 작업 수정 예제

현재 XmlSqlService 생성자에 아래와 같이 예외가 발생할 수도 있는 복잡한 초기화 작업을 다루는 것은 좋지 않다.

  1. 오브젝트를 생성하는 중에 생성자에서 발생하는 예외는 다루기 힘들다.
  2. 상속하기 불편하다.
  3. 읽어들일 파일의 이름이 하드코딩 되어 있다.
public XmlSqlService() {
    String contextPath = Sqlmap.class.getPackage().getName();
    try {
        JAXBContext jaxbContext = JAXBContext.newInstance(contextPath);
        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
        InputStream is = ClassLoader.class.getResourceAsStream("/sqlmap.xml");
        Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal(is);

        for (SqlType sql : sqlmap.getSql()) {
            this.sqlMap.put(sql.getKey(), sql.getValue());
        }
    } catch (JAXBException e) {
        // JAXBException은 복구 불가능한 예외이기 때문에 RuntimeException으로 포장해서 던진다
        throw new RuntimeException(e);
    }
}

초기 상태를 가진 오브젝트를 만들어놓고 별도의 초기화 메소드를 사용하는 방법이 바람직하다.
외부에서 DI로 설정해줄 수 있게 만들어보자

  • sqlmap.xml SQL 파일 이름을 외부에서 지정하자
  • 생성자로 XML파일을 읽지 말고 별도의 메소드로 분리하여 XML파일을 읽어들이자
    • @Bean으로 등록하면서 loadSql()을 직접 호출해주는 방법
    • loadSql()의 메소드 레벨에 빈 후처리기 @PostConstruct를 통해 호출하는 방법
  1. @Configuration을 통해 XmlSqlService를 스프링 빈으로 등록
  2. 빈으로 등록하면서 sqlmap.xml파일 이름 전달
  3. @PostConstruct 빈 후처리기를 통해 빈으로 등록된 후 sqlmap.xml의 XML파일을 읽어 SQL 정보를 언마샬링
  4. 전역 필드에 SqlMap을 갱신

3. 책임에 따른 인터페이스 정의 (SqlRegistry, SqlReader)

현재 XmlSqlService는 특정 포맷의 XML에서 SQL데이터를 가져오고, 이를 Map타입 컬렉션에 저장한다.
SQL을 가져오는 방법에 있어서는 특정 기술에 고정되어 있다.
가져온 SQL 정보를 Map타입 컬렉션이 아니라 다른 방식으로 저장해두고 이를 검색해서 가져오려면 직접 고치거나 새로 수정해야 한다.

  • XmlSqlService가 변경되는 이유가 두 가지라면 이는 단일책임원칙을 위반하는 것이다

분리 가능한 관심사를 구분해보자. XmlSqlService구현을 참고해서 독립적으로 변경 가능한 책임을 뽑아보자.

  1. SQL 정보를 외부의 리소스로부터 읽어오는 것
  2. SQL을 보관하고 필요할 때 제공하는 것
  3. 부가적으로, 한 번 가져온 SQL을 필요에 따라 수정할 수 있게 하는것

4. 자기참조 빈 : XmlSqlService에서 모든 인터페이스 구현 예제

XmlSqlService클래스는 이 세 가지 관심과 책임을 가지고 있었는데 분리된 인터페이스를 다 구현하도록 먼저 만들어보자

@Bean
public SqlService sqlService() {
    XmlSqlService sqlService = new XmlSqlService();
    sqlService.setSqlmapFile("sqlmap.xml");
    sqlService.setSqlReader(sqlService);
    sqlService.setSqlRegistry(sqlService);
    return sqlService;
}

책임과 관심사가 복잡하게 얽혀 있어서 확장이 힘들고 변경에 취약한 구조의 클래스를 유연한 구조로 만들려고 할 떄 처음 시도해볼 수 있는 방법이다.

  • 실제로 스프링에서 제공하는 클래스 중에도 자신의 의존 오브젝트 인터페이스를 스스로 구현하고 자신을 DI하는 코드가 제법 있다

5. 의존성 분리 : HashMapSqlRegistry, JaxbXmlSqlReader 클래스 추가 예제

6. 디폴트 의존관계를 갖는 DefaultSqlService 추가 예제

이제 BaseSqlServicesqlReadersqlRegistry 프로퍼티의 DI를 통해 의존관계를 자유롭게 변경해가면서 기능을 확장할 수 있다.
하지만 3개의 빈을 등록해줘야 한다는 점이 귀찮게 느껴지기도 한다.
이렇게 확장을 고려해서 기능을 분리하고, 인터페이스와 전략 패턴을 도입하고, DI를 적용한다면 늘어난 클래스와 인터페이스 구현과 의존관계 설정에 대한 부담은 감수해야 한다.

디폴트 의존관계란?
외부에서 DI받지 않는 경우 기본적으로 자동 적용되는 의존관계를 말한다.

7.3 서비스 추상화 적용

JaxbXmlSqlReader는 좀 더 개선하고 발전시킬 부분이 있다.

  1. 자바에는 JAXB외에도 다양한 XML과 자바 오브젝트를 매핑하는 기술이 있어 필요에 따라 다른 기술로 손쉽게 바꿀 수 있어야 한다.
  2. XML 파일을 좀 더 다양한 소스에서 가져올 수 있게 만들어야 한다.
    • 임의의 클래스패스나 파일 시스템 상의 절대위치 또는 HTTP 프로토콜을 통해 원격에서 가져오도록 확장할 수는 없는가 하는 점이다.

Object-XML Mapping 서비스 추상화

  1. Castor XML
  2. JiBX
  3. XmlBeans
    • 아파치 XML 프로젝트의 하나
  4. Xstream

기능이 같은 여러 가지 기술이 존재한다는 이야기가 나오면 "서비스 추상화" 가 떠올라야 한다.
로우레벨의 구체적인 기술과 API에 종속되지 않고 추상화된 레이어와 API를 제공해서 구현 기술에 대해 독립적인 코드를 작성할 수 있게 해주는 "서비스 추상화"가 필요하다.

스프링이 제공하는 OXM 추상 계층의 API를 이용해 XML 문서와 오브젝트 사이의 변환을 처리하게 하면, 코드 수정 없이도 OXM 기술을 자유롭게 바꿔서 적용할 수 있다.

7. Spring Jaxb2Marshaller 테스트 예제

스프링이 제공하는 OXM 추상화 서비스 인터페이스에
자바 오브젝트 → XML Marshaller
XML → 자바 오브젝트 Unmarshaller가 있다

Spring Jaxb2MarshallerUnmarshaller인터페이스와 Marshaller인터페이스 둘 다 구현했다.

8. OXM 서비스 추상화 적용 예제

이제 스프링의 OXM 추상화 기능을 이용하는 SqlService를 만들어보자.
이름은 OxmSqlService라고 하고 SqlRegistry는 DI 받을 수 있게 만들지만 SqlReader는 스프링의 OXM 언마샬러를 이용하도록 OxmSqlService내에 고정시켜야 한다.

여기서 SqlReader구현체는 OxmSqlService의 내부 클래스로 선언한다.
현재 OxmSqlServiceBaseSqlService 이 두 클래스에서 중복되는 메소드가 있다.

  • loadSql()
  • getSql()

수정이 일어난다면 두 곳 다 수정해줘야한다.
중복이 된 두 메소드 실행을 BaseSqlService에게 위임하도록 수정했다

9. 리소스(SQL 매핑 정보 파일) 추상화 Resource, ResourceLoader 적용 예제

XML 파일을 좀 더 다양한 소스에서 가져올 수 있게 만들어야 한다.

  • 임의의 클래스패스나 파일 시스템 상의 절대위치 또는 HTTP 프로토콜을 통해 원격에서 가져오도록 확장할 수는 없는가 하는 점이다.

기존에는 클래스패스로 부터 resources안에 있는 파일을 읽기 위해 ClassLoader 클래스의 getResourceAsStream()을 사용했다.
이를 파일 시스템이나 웹상의 HTTP를 통해 접근 가능한 파일로 바꾸려면 URL클래스를 사용하도록 코드를 변경해야 한다.
또한 서블릿 컨텍스트 내의 리소스를 가져오려면 ServletContextgetResourceAsStream()을 사용해야 한다.

리소스의 위치와 종류에 따라서 다른 클래스와 메소드를 사용해야 한다는 점이 불편하다.
스프링은 자바에 존재하는 일관성 없는 리소스 접근 API를 추상화해서 Resource라는 추상화 인터페이스를 제공한다.

  • 스프링의 거의 모든 API는 외부의 리소스 정보가 필요할 때는 항상 이 Resource 추상화를 이용한다
  • Resource는 빈이 아니라 단순한 정보를 가진 값으로 취급된다

그래서 스프링에는 URL클래스와 유사하게 접두어를 이용해 Resource오브젝트를 선언하는 ResourceLoader를 제공한다.

ResourceLoader의 대표적인 예는 애플리케이션 컨텍스트다.

  • 따라서 모든 애플리케이션 컨텍스트는 리소스 로더이기도 하다.

스프링에서 파일을 읽거나 참조하는 기능을 만들때는 Resource타입의 추상화된 기능을 사용하자

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

SQL을 수정하거나 추가하기 위해 기능을 추가해야 한다면 기존 SqlRegistry인터페이스는 건드리지 마라.

public interface UpdatableSqlRegistry extends SqlRegistry {
    void updateSql(String key, String sql) throws SqlUpdateFailureException;

    void updateSql(Map<String, String> sqlmap) throws SqlUpdateFailureException; 
}

SqlRegistry의 서브 인터페이스로 기능을 추가할 수 있다.

실제 오브젝트 사이에서 일어나는 DI의 결과만 보자면 BaseSqlServiceSqlAdminService 오브젝트는 동일한 MyUpdatableSqlRegistry 오브젝트를 DI 받아서 사용한다.
동일한 오브젝트에 의존하고 있지만 각자의 관심과 필요에 따라서 다른 인터페이스를 통해 접근한다.

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

현재 SQL 정보를 읽어들여서 메모리에 두고 그대로 사용하기 때문에 SQL이 수정된다면 서버를 재시작해야 한다.
운영 중인 시스템에서 사용하는 정보를 실시간으로 변경하는 작업을 만들 때 가장 먼저 고려해야 할 사항은 동시성 문제다.

자바에서 제공되는 주요 기술을 이용해 간단한 방식으로 어느 정도 안전한 업데이트가 가능한 SQL 레지스트리를 구현해보자

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

내장형 데이터베이스를 이용한 SQL 레지스트리 예제

내장형 메모리 DB는 애플리케이션과 생명주기가 같다.

  • Derby,HSQL,H2

애플리케이션 내에서 DB를 가동시키고 초기화 SQL 스크립트를 실행시키는 등의 초기화 작업이 별도로 필요하다.
대신 스프링은 내장형 DB를 초기화하는 작업을 지원하는 편리한 내장형 DB 빌더를 제공한다.

기존 ConcurrentHashMap으로 구현한 클래스와 내장형 DB로 구현핰 클래스들의 테스트 클래스 AbstractUpdatableSqlRegistryTest추가한다.
추상 클래스의 createUpdatableSqlRegistry()를 통해 구현체를 다르게 하여 테스트를 한다.

내장형 DB 빌더 학습 테스트 예제

내장형 DB 빌더는 DB 엔진을 생성하고 초기화 스크립트를 실행해서 테이블과 초기 데이터를 준비한뒤에 DB에 접근할 수 있는 Connection을 생성해주는 DataSource오브젝트를 돌려주게 된다.

  • 정확히는 DB 셧다운 기능을 가진 EmbeddedDatabase타입 오브젝트다

DataSource인터페이스를 상속한 EmbeddedDatabase타입이기 때문에 일반적인 사용 방법을 그대로 적용할 수 있다.

SQL 업데이트 트랜잭션 적용 예제

7.6 스프링 3.1의 DI

스프링은 2.5 버전에서 DI와 웹 기능 일부에 어노테이션을 적용하기 시작했고, 스프링 3.0에서는 다양한 영역으로 어노테이션의 활용 범위를 넓혔다.
스프링 3.1에서는 어노테이션을 이용한 메타정보 작성 방식이 거의 모든 영역으로 확대돼서, 원한다면 스프링 애플리케이션을 XML없이도 작성할 수 있게 됐다.

빈 스캐닝과 Config 정보 수정 예제

  1. @ContextConfiguration 제거, @SpringJunitConfig로 수정
  2. DI Config 일부 제거 후 @Autowired 적용

@ContextConfiguration

스프링 테스트가 테스트용 DI 정보를 어디서 가져와야 하는지 지정할 때 사용하는 어노테이션이다.

@ImportResource

자바 클래스로 만들어진 DI 설정 정보에서 XML의 설정 정보를 지정하는 어노테이션이다.

목적에 맞는 컨텍스트 설정 분리 예제

지금은 TestDBConfig에 애플리케이션에 필요한 DI와 테스트에 필요한 DI 정보가 같이 작성되어 있다.

  1. 테스트용 DITestAppContext실제 필요한 DIAppContext 정보를 분리
  2. 싪제 필요한 DI 정보 중 SQL 서비스용 빈SqlServiceContext 분리

프로파일 ⭐️

MailSender빈이 실제 운영환경과 테스트 환경에서 중복된다.
ProductionAppContext로 분리하자

@Profile@ActiveProfiles 예제

  • 운영 환경 : AppContext + ProductionAppContext
  • 테스트 환경 : AppContext + TestAppContext

스프링 3.1은 프로파일이 지정되어 있지 않은 빈 설정은 default 프로파일로 취급한다.
프로파일을 적용하면 모든 설정 클래스를 부담없이 메인 설정 클래스에서 @Import해도 된다는 장점이 있다.

@Profile이 붙은 설정 클래스는 @Import로 가져오든 @ContextConfiguration에 직접 명시하든 상관없이 현재 컨테이너의 활성 프로파일 목록에 자신의 프로파일 이름이 들어있지 않으면 무시된다.

  • 활성 프로파일 : 스프링 컨테이너를 실행할 때 추가로 지정해주는 속성

@Profile의 빈 설정만 잘 적용되었을까?

@SpringJUnitConfig(classes = AppContext.class)
@ActiveProfiles("test")
class UserServiceTest {

    @Autowired
    private DefaultListableBeanFactory bf;

    @Test
    void beans() {
        for (String bean : bf.getBeanDefinitionNames()) {
            System.out.printf("%s : %s%n", bean, bf.getBean(bean));
        }
    }

//    mailSenderImpl : springbook.chapter06.DummyMailSender@7516e4e5
}
  • DummyMailSender가 빈으로 등록된 것을 볼 수 있다.

스태틱 중첩 클래스를 이용한 프로파일 적용 예제

TestAppContext와 ProductionAppContext를 AppContext에 스태틱 중첩 클래스로 옮겼다.
AppContext@Import도 줄어들고 설정 정보를 쉽게 한눈에 확인할 수 있다.

프로퍼티 소스 @PropertySource 예제

DB 연결 정보를 환경에 따라 다르게 설정될 수 있어야 하고 손쉽게 변경할 수 있어야 한다.
컨테이너가 프로퍼티 값을 가져오는 대상을 프로퍼티 소스라고 한다.
스프링 3.1은 빈 설정 작업에 필요한 프로퍼티 정보를 컨테이너가 관리하고 제공해준다.

  • 환경 변수나 시스템 프로퍼티처럼 디폴트로 프로퍼티 정보를 끌어오는 프로퍼티 소스도 있고,
  • 프로퍼티 파일이나 리소스의 위치를 지정해서 사용되는 프로퍼티 소스도 있다.

@PropertySource로 등록한 리소스로부터 가져오는 프로퍼티 값은 컨테이너가 관리하는 Environment타입의 환경 오브젝트에 저장된다.
환경 오브젝트는 빈 처럼 필드로 주입받을 수 있다.

프로퍼티 정보 @Value 예제

Environment오브젝트 대신 프로퍼티 값을 직접 DI 받는 방법도 가능하다.

  • 프로퍼티 소스로 부터 값을 주입받을 수 있게 치환자를 이용한다
  • 스프링이 타입 변환이 필요한 프로퍼티를 알아서 처리해준다는 장점

@Value치환자를 이용해 프로퍼티 값을 필드에 주입하려면 PropertySourcePlaceHolderConfigurer를 빈으로 등록해줘야 한다.
빈 팩토리 후처리기로 사용되는 빈을 정의하는 것이다. 이 빈 설정 메소드는 반드시 스태틱 메소드로 선언해야 한다.

빈 설정의 재사용과 @Enable* 예제

현재 SqlServiceContext는 다른 빈 설정 정보와 성격이 다르기 때문에 분리시켜 놓았다.
SQL 서비스 빈은 서비스 인터페이스, 즉 API인 SqlService만 DAO에 노출하면 되고 나머지 구현 기술이나 방법은 내부에 감춰두고 필요에 따라 자유롭게 변경할 수 있어야 한다.

sqlmap 파일 정보의 위치나 파일 이름을 외부에서 주입할 수 있게 먼저 수정하자

스프링 3.1은 SqlServiceContext처럼 모듈화된 빈 설정을 가져올 때 사용하는 @Import를 다른 어노테이션으로 대체할 수 있는 방법을 제공한다.
@Import(value = SqlServiceContext.class) 이 메타 어노테이션을 가진 @EnableSqlService를 추가해서 AppContext에 적용하자.