본문 바로가기
책 정리/토비의 스프링

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

by 이석준석이 2021. 2. 14.

1. Sql 과 Dao의 분리

 

현재의 구조는 Dao 내부에서 Sql을 정의하여 사용하도록 되어있다.

  • 토비의 스프링에서는 이를 JAXB api를 사용하여 xml에 따로 sql을 관리하도록 하여 저장하도록 구현한다.
  • 변경된 현재구조는 아래와 같다.

public class XmlSqlService implements SqlService {

    private Map<String, String> sqlMap = new HashMap<>();

    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");
            Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal(is);

            for(SqlType sqlType : sqlmap.getSql()) {
                sqlMap.put(sqlType.getKey(), sqlType.getValue());
            }
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
    }
}

생성자 내에서 예외가 발생할 수 있는 초기화작업은 좋지않다.

  • 생성자 생성중 발생 예외는 다루기 힘들다.
  • 상속하기 불편하다.
  • 함수를 하나 만들어서 수행하도록 변경한다.

생성자에서 파일을 읽어오는 구조에서

  • 파일을 읽어들이는 함수를 하나 구현한 뒤
  • @PostConstruct(빈 오브젝트를 생성하고, DI 가 끝난 뒤에 메소드를 자동 수행) 빈 후처리기 어노테이션을 이용해서 등록한다.
public class XmlSqlService implements SqlService {

    private Map<String, String> sqlMap = new HashMap<>();
    private String sqlmapFile;

    public void setSqlmapFile(String sqlmapFile) {
        this.sqlmapFile = sqlmapFile;
    }

    @PostConstruct
    public void loadSql() {
        String contextPath = Sqlmap.class.getPackage().getName();

        try {
            JAXBContext context = JAXBContext.newInstance(contextPath);
            Unmarshaller unmarshaller = context.createUnmarshaller();
            InputStream is = UserDao.class.getResourceAsStream(sqlmapFile);
            Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal(is);

            for(SqlType sqlType : sqlmap.getSql()) {
                sqlMap.put(sqlType.getKey(), sqlType.getValue());
            }
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
    }
 }

현재 XmlSqlService의 구조를 보면

  • SQL 파일을 읽어들이는 기능
  • SQL 문을 등록하는 기능
  • SQL 문을 조회하는 기능

이를 인터페이스로 분리하여

  1. SQL 파일을 읽어들이는 기능
  2. SQL 파일을 등록 / 조회하는 기능으로 나눠서 변경해보자

해당 코드는 현재 하나의 관심사 (Xml을 읽어서 저장하고 반환하는 기능) 을 갖고있기 때문에 인터페이스 다중상속을 이용하여 아래와같이 구조를 잡을 수 있다.

public class XmlSqlService implements SqlService, SqlRegistry, SqlReader {

    private SqlReader sqlReader;
    private SqlRegistry sqlRegistry;
    private String sqlmapFile;
    private Map<String, String> sqlMap = new HashMap<>();

    public void setSqlReader(SqlReader sqlReader) {
        this.sqlReader = sqlReader;
    }

    public void setSqlRegistry(SqlRegistry sqlRegistry) {
        this.sqlRegistry = sqlRegistry;
    }

    @PostConstruct
    public void loadSql() {
        sqlReader.read(sqlRegistry);
    }

    /**
     * SqlService Implementation
     */
    @Override
    public String getSql(String key) throws SqlRetrievalFailureException {
        try {
            return sqlRegistry.findSql(key);
        } catch(SqlNotFoundException e) {
            throw new SqlRetrievalFailureException(e);
        }
    }

    /**
     * SqlRegistry Implementation
     */
    @Override
    public void registerSql(String key, String sql) {
        sqlMap.put(key, sql);
    }

    @Override
    public String findSql(String key) throws SqlNotFoundException {
        String sql = sqlMap.get(key);
        if(StringUtils.isEmpty(sql)) {
            throw new SqlNotFoundException(key + "를 이용해서 SQL을 찾을 수 없습니다.");
        }
        return sql;
    }

    /**
     * SqlReader Implementation
     */
    @Override
    public void read(SqlRegistry sqlRegistry) {
        String contextPath = Sqlmap.class.getPackage().getName();

        try {
            JAXBContext context = JAXBContext.newInstance(contextPath);
            Unmarshaller unmarshaller = context.createUnmarshaller();
            InputStream is = UserDao.class.getResourceAsStream(sqlmapFile);
            Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal(is);

            for(SqlType sqlType : sqlmap.getSql()) {
                registerSql(sqlType.getKey(), sqlType.getValue());
            }
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
    }
}

위의 구조로 성격이 다른 코드들을 인터페이스를 통해 분리해냈다.

  • 빈 등록을 하려면 어떻게해야할까? 
  • 자신이 구현했으면서, 자신을 빈으로 등록하는 경우에는 아래와같이 자기자신을 참조하도록 설정한다.
@Bean
public SqlService sqlService() {
    XmlSqlService sqlProvider = new XmlSqlService();
    sqlProvider.setSqlmapFile("/sqlmap.xml");
    sqlProvider.setSqlReader(sqlProvider);
    sqlProvider.setSqlRegistry(sqlProvider);
    return sqlProvider;
}

2. 서비스 추상화 적용

 

두가지 기능을 더 추상화한다.

  • JAXB 외에도 XML을 자바 오브젝트로 매핑할수 있기에, 이를 추상화한다.
    • OXM (Object-XML Mapping)
  • 클래스패스 안에서만 XML을 읽어오는 기능을 추상화한다.

OXM 추상화

  • 스프링에서 제공하는 Unmarshaller 인터페이스를 사용해서 추상화 할 수 있다.
  • 과정은 토비의스프링에서..

리소스 추상화

  • 스프링 Resource 인터페이스를 사용한다.
  • ApplicationContext가 ResourceLoader를 상속하고 있기에
    • ResourceLoader 을 사용하여 Prefix 기반으로 오브젝트를 반환하는 기능을 사용한다.
package org.springframework.core.io;

public interface Resource extends InputStreamSource {

    boolean exists();

    default boolean isReadable() {
        return exists();
    }

    default boolean isOpen() {
        return false;
    }

    default boolean isFile() {
        return false;
    }

    URL getURL() throws IOException;

    URI getURI() throws IOException;

    File getFile() throws IOException;

    default ReadableByteChannel readableChannel() throws IOException {
        return Channels.newChannel(getInputStream());
    }

    long contentLength() throws IOException;

    long lastModified() throws IOException;

    Resource createRelative(String relativePath) throws IOException;

    @Nullable
    String getFilename();

    String getDescription();

}

 

prefix desc
file: C:/temp 폴더 기준
classpath: 클래스패스 루트 기준
없음 ResourceLoader 구현에 따라 달라진다.
http: Http 프로토콜을 사용해서 웹상의 리소스를 가져온다. / ftp: 도 가능하다.

3. 인터페이스 상속

 

현재 구조에서 SqlRegistry 에 업데이트 하는 기능이 추가된다면?

  • 지금까지 추상화를 이용한 분리를 잘 해왔기에 간단하게 인터페이스 상속을 통해 다른 클래스들은 변경하지않고 추가할 수 있다.
public interface UpdatableSqlRegistry extends SqlRegistry {
    void updateSql(String key, String sql) throws SqlUpdateFailureException;
    void updateSql(Map<String, String> sqlMap) throws SqlUpdateFailureException;
}

4. 스프링 3.1의 DI

 

두가지 변화

  • 어노테이션 메타정보 활용
    • Reflection API 를 이용하여 자바클래스 / 인터페이스 / 필드 / 메소드 등 메타정보를 살펴보기 시작
    • 자바 5에서 어노테이션이 나타난 이후, Reflection API 를 이용하여 어노테이션의 메타정보를 조회하고, 설정된 값을 사용
    • 기존에는 xml을 이용해서 했으나
      • ex) <bean id="userService", class="www.sjlee.kr.UserService.class"/ >
    • 어노테이션을 통해서 간단하게
      • 클래스에 대한 메타정보
      • 클래스의 패키지, 이름, 접근제한자, 메소드 등을 알 수 있다.

<tx:annotation-driven /> 은 아래와 같은 클래스들을 빈으로 등록해준다.

  • InfrastructureAdvisorAutoProxyCreator
  • AnnotationTransactionAttributeSource
  • TransactionInterceptor
  • BeanFactoryTransactionAttributeSourceAdvisor

해당 <tx:annotation-driven/>@EnableTransactionManagement 로 대체하여 위의 빈들을 등록하게한다.

  • XML 에서 자주 사용되는 설정들을 @Enable+... 으로 스프링은 해결하도록 해놓았다.

@Component 는

  • @ComponentScan 을 통해 스캔할 수 있도록 지정해줘야 한다.

 

@Import

  • 빈을 등록하는 Configuration 클래스가 2개인 경우에
    • 1. 전체를관리하는 AppContext (메인 설정정보)
    • 2. sql 연결쪽을 관리하는 SqlServiceContext (보조 설정정보)
@Configuration
@Import(SqlServiceContext.class)
public class AppContext {
}

 

@Profile

  • 특정 프로파일에만 적용돼야 하는 빈 설정을 갖고있는다.

 

@ActiveProfiles

  • 해당 테스트를 해당 프로파일로 실행하도록 한다.

 

@PropertySource

  • 등록한 리소스로부터 프로퍼티의 값을 가져올 수 있다.
  • @Bean 에서 등록할 때에는 npe 발생 ..ㅠ
  • 아래와 같이 사용
# database.properties
db.driverClass=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/tobyspring?serverTimezone=UTC&characterEncoding=UTF-8
db.username=root
db.password=123123
@Getter
@Component
@PropertySource("classpath:database.properties")
public class DatabaseProperty {

    @Value("${db.driverClass}")
    String driverClass;
    @Value("${db.url}")
    String url;
    @Value("${db.username}")
    String username;
    @Value("${db.password}")
    String password;
}
  • @Value(${}) 방식을 placeholder 방식이라고 하며, 이를 사용하기 위해서는 
    • PropertySourcePlaceHolderConfigurer 을 등록해줘야 한다. (Springboot는 알아서 등록이 되는 것 같다.)

@EnableTransactionManagement 어노테이션

  • @Import(TransactionManagementConfigurationSelector.class)
    • TransactionManagementConfigurationSelector 클래스의 설정정보를 임포트하겠다는 뜻이다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {
..
}

'책 정리 > 토비의 스프링' 카테고리의 다른 글

8. 스프링이란 무엇인가  (0) 2021.02.20
6.2. 스프링 AOP  (0) 2021.02.10
6.1. AOP  (0) 2021.02.02
5. 서비스 추상화  (0) 2021.02.01
4. 예외  (0) 2021.01.30