2020-5-28 토비의 스프링 7장

스프링 핵심기술 응용

  • 반복적인 작업은 JdbcTemplate을 이용해서 중보글 제거했지만, SQL을 DAO에서 하지는 못했다.
  • 데이터액세스 로직은 바뀔 수 있지만 DB의 테이블, 필드이름, SQL문장은 바뀔 수 있다. SQL변경이 발생하면 DAO코드도 수정될 수 밖에 없다. SQL 코드를 DAO에서 분리하는 작업이 필요하다.

DAO SQL분리

  • SQL파일을 XML로 분리한다.
public class UserDaoJdbc implements UserDao {
	private String sqlAdd;

	public void setSqlAdd(String sqlAdd) {
		this.sqlAdd = sqlAdd;
	}
}
  • xml로 분리하는건 sql이 필요할때마다 프로퍼티를 추가하고 di를 위한 변수와 setter메서드를 만들어야되서 불편…
  • SQL 맵 프로퍼티방식
    • sql을 하나의 컬렉션으로 담는 방식. Map<String, String>으로 담아서 관리.
    • 새로운 sql이 필요할때마다 <entry>만 sql에 추가하면 되지만 sql을 가져올때 오타가 있어서 안된다.

SQL 제공서비스

  • sql을 꼭 xml방식으로 담을 필요은 없다. xml을 통해 가져오면 애플리케이션을 다시 시작하기 전에는 변경이 어렵다.

  • sql을 db에 담아두거나 리모트 등 외부시스템에서 가져올 수도 있다. 어떤 이슈가 있는지는 모르겠다.

  • SQL맵 오브젝트를 변경할 수 있지만 DAO가 싱글톤이라 실시간으로 접근해서 변경하는건 쉽지 않다.

  • 결론 : 확정성이 뛰어난 SQL서비스 인터페이스를 만들자.

  • public interface SqlService {
        String getSql(String key) throws SqlRetrievalFailureException;
    }
    
    public class SimpleSqlService implements SqlService {
      private Map<String, String> sqlMap;
    
      public void setSqlMap(Map<String, String> sqlMap) {
        this.sqlMap = sqlMap;
      }
    
      @Override
      public String getSql(String key) throws SqlRetrievalFailureException {
        String sql = sqlMap.get(key);
        if (sql == null)
          throw new SqlRetrievalFailureException(key +"에 대한 SQL을 찾을 수 없습니다.");
        else
          return sql;
      }
    }
    
  • <bean id="sqlService" class="toby.dao.SimpleSqlService">
      <property name="sqlMap">
        <map>
          <entry key="userAdd" value="insert into users (id, name, password, email, level, login, recommend) values(?,?,?,?,?,?,?)" />
          <entry key="userGet" value="select * from users where id = ?" />
          <entry key="userGetAll" value="select * from users order by id" />
          <entry key="userDeleteAll" value="delete from users" />
          <entry key="userGetCount" value="select count(*) from users" />
          <entry key="userUpdate" value="update users set name = ?, password = ?, email = ?, level = ?, login = ?, recommend = ? where id = ?" />
        </map>
      </property>
    </bean>
    
  • SqlService인터페이스를 정의하고 구현한다음. Sql쿼리들이 들어있는 값을 XML로 주입한다. 여기서 에러가 매우 잘 발생 ㅠㅠ bean태그 안에 sql정보를 넣는건 좋은 방법이 아니다. sql을 저장해두는 전용포맷 파일을 만들자. JAXB를 쓸 수 있다.

JAXB

  • XML문서정보를 동일한 구조의 오브젝트로 직접매핑해준다. XML정보를 오브젝트처럼 다룰 수 있어서 편하다.
  • 언마샬링 : xml => java object // 마샬링 : java object => xml

빈초기화

  • UserDaoJdbc에서 사용한 XML문서를 만들어야한다.
<?xml version="1.0" encoding="UTF-8"?>
<sqlmap
        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(id, name, password, email, level, login, recommend) values(?,?,?,?,?,?,?)</sql>
  <sql key="userGet">select * from users where id = ?</sql>
  <sql key="userGetAll">select * from users order by id</sql>
  <sql key="userDeleteAll">delete from users</sql>
  <sql key="userGetCount">select count(*) from users</sql>
  <sql key="userUpdate">update users set name = ?, password = ?, email = ?, level = ?, login = ?, recommend = ? where id = ?</sql>
</sqlmap>
  • UserDao와 같은 패키지에 넣자.
  • XML SQL Service
    • sqlmap.xml에 있는 SQL을 가져와 DAO에 제공해주는 SqlService인터페이스를 구현하자. JAXB api를 쓰면된다. 언제 xml문서를 가져와야되나? 요청할때마다 가져오는게 아니라 한번만 가져와야된다. SqlServie의 구현체가 스프링빈이고 초기화할때 읽어야한다.
    • jaxb로 xml문서를 언마샬링하면 sql문장은 하나의 sql클래스의 오브젝트에 담긴다. 이 객체를 Map을 이용해 key-value값으로 담아두자.
    • @PostContruct를 초기화 작업 메서드에 부여하면 XmlSqlService로 등록된 오브젝트를 생성하고 DI를 마친뒤 @PostContrcut가 붙은 메서드를 자동으로 실행한다.

서비스추상화

  • jaxb말고 xml을 매핑하는 기술이 자바에도 있다.
  • xml파일을 다양한 소스에서 가져올 수있게한다. 현재 UserDao는 클래스패스 안에서만 xml을 읽을 수 있다.

기능이 같은 여러가지 기술이 존재한다는 이야기가 나오면 서비스 추상화를 할 수 있다.

리소스추상화

  • 이전에 만든 OxmSqlReader, XmlSqlReader는 SQL매핑 정보가 담긴 XML파일 이름을 프로퍼티로 외부에서 지정할 수 있지만 UserDao클래스와 같은 클래스패스에 존재하는 파일로 제한된다. 외부에서 파일을 불러오기는 힘들다.
  • 스프링에서는 Resource인터페이스를 사용한다. 스프링의 대부분의 API는 외부의 리소스 정보가 필요할 때는 항상 Resource추상화를 이용한다.
  • 서비스 추상화의 오브젝트와는 달리, Resource는 스프링에서 빈이 아니라 값으로 취급. 적용하는 방법이 문제
  • 빈으로 등록하면 리소스의 타입에 따라 Resource 인터페이스의 구현클래스를 지정하면 되는데, 값이라서 그것도 힘들다.

리스소로더

  • 접두어를 이용해 Resource오브젝트를 선언할 수 있다. 문자열 안에 리소스의 종류와 위칠르 함께 표현. 문자열로 정의된 리소스를 Resource타입 오브젝트로 변환하는 ResourceLoader를 제공
접두어 설명
file: file:/C:/temp/file.txt 파일 시스템의 C:/temp폴더에 있는 file.txt를 리소스로 만들어준다.
classpath: classpath:file.txt 클래스패스 루트에 존재하는 file.txt 리소스에 접근하게 해준다.
없음 WEB-INF/test.dat 접두어가 없으면 ResourceLoader구현에 따라 리소스의 위치가 결정
http: http://www.myserver.com/test.dat HTTP 프로토콜을 사용해 접근할 수 있는 웹상의 리소스를 지정
  • 리소스 로더의 대표적인 예가 스프링 애플리케이션 컨텍스트다.

    • 스프링 컨테이너는 설정정보가 담긴 xml파일을 리소스 로더를 이용해 Resource형태로 읽어온다.

    • 그밖에 외부에서 읽어오는 모든 정보는 리소스로더를 사용.

    • 빈의 프로퍼티값을 변환할때도 리소스로더를 사용

    • 빈으로 등록한 클래스에 파일을 지정해주는 프로퍼티가 존재하면 대부분 Reousrce타입

    • Resource타입은 빈으로 등록하지 않고 <property>태그의 value를 사용해 문자열로 값을 넣는다. 이 문자열이 Resource오브젝트로 변환한다.

    • <property name="myFile" value="classpaht:com/epril/myproject/myfile.txt" />
      <property name="myFile" value="file:data/myfile.txt" />
      <property name="myFile" value="http://www.myserver.com/myfile.txt" />
      
  • OxmSqlService에 Resource를 적용해서 xml파일을 가져올 수 있다.

  • Resource오브젝트가 실제 리소스가 아니라는 점을 주의. Resource는 리소스에 접근할 있는 추상화된 핸들러이고 오브젝트가 만들어졌어도 실제로는 존재하지 않을 수가 있다.

DI와 인터페이스 프로그래밍

  • 설계를 할때 DI를 의식적으로 생각하면서 설계를 해야된다.
  • DI를 DI답게 만들려면 두개의 오브젝트가 인터페이스를 통해 느슨하게 연결되어야한다.
  • 인터페이스를 사용하는 이유
    • 다형성때문이다. 하나의 인터페이스를 통해 여러개의 구현을 바꿀 수 있다.
    • 다형성 때문에 인터페이슬르 쓰는건 아니다. final클래스만 아니라면 상속을 통해서도 여러가지 방식으로 구현을 확장할 수 있다. 템플릿메서드패턴
    • 인터페이스 분리 원칙을 통해 클라이언트와 오브젝트 사이의 관계를 명확하게 할 수 있다.
    • 인터페이스를 사용하는 클라이언트 A와 인터페이스B가 있을때, A가 B를 바라볼때 인터페이스라는 창을 통해서만 바라본다. 구현체가 여러개인 이유는 다른 종류의 클라이언트들이 존재하기 때문
    • 오브젝트가 그 자체로 응집도가 높은 작은 단위로 설계됬더라도. 목적과 관심이 다른 클라이언트들이 있다면 인터페이스를 통해 분리시켜라. 인터페이스 분리원칙

스프링 3.1

  • 자바5에서 어노테이션이 등장하고 자바 코드의 메타정보를 이용한 프로그래밍방식을 많이 활용한다.
  • xml은 어노테이션에 비해 작성할 정보가 많다. 텍스트정보라 오타가 발생하기 쉽다.
  • xml은 다시 빌드할 필요도 없고 어느환경에서 편집이 쉬운데, 어노테이션은 자바 코드에 존재해서 변경할때마다 새로 컴파일하고 변경이 불편한 단점이 있다.
  • 스프링 3.1에서는 xml을 쓰지않고 개발할 수 있다.

xml to java

  • xml설정파일을 자바 설정파일로 변환한다.
//Refacting
@Configuration
@ImportResource(locations = "/test-applicationContext.xml")
public class TestApplicationContext {
  @Bean
  public DataSource dataSource() {
    SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
    dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
    dataSource.setUrl("jdbc:mysql://localhost/test?characterEncoding=UTF-8&useSSL=false");
    dataSource.setUsername("jimmy");
    dataSource.setPassword("12345");
    return dataSource;
  }
}
// 메서드의 이름은 bean id로 해주면된다. 리턴값은 인터페이스로 해라.

//Before
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
  <property name="driverClass" value="com.mysql.jdbc.Driver" />
  <property name="url" value="jdbc:mysql://localhost/test?useSSL=false" />
  <property name="username" value="jimmy" />
  <property name="password" value="12345" />
</bean>

빈스캐닝과 자동와이어링

  • @Autowired는 자동 와이어링을 이용해 조건에 맞는 빈을 자동으로 찾아 setter()나 필드를 통해 넣어준다.

    • public class UserDaoJdbc implements UserDao {
      
          @Autowired
          public void setDataSource(DataSource dataSource) {
              this.jdbcTemplate = new JdbcTemplate(dataSource);
          }
          ...
          @Autowired
          private SqlService sqlService;
      }
      
    • 컨테이너가 이름, 타입을 기준으로 주입될 빈을 찾는다.

    • 주입가능한 타입의 빈이 2개이상나오면 프로퍼티와 동일한 이름의 빈이 있는지 찾는다. Datasource의 빈이 2개가 있다. 하나는 dataSource 다른 하나는 embeddedDatasource빈. datasource빈이 수정자 메서드의 프로퍼티 이름과 같기때문에 이를 넣어주고 만약에 타입과 이름 모두 다 비교해도 찾지 못하면 에러발생

    • 필드주입을 해도 상관없다. 자바에서는 private필드에는 클래스 외부에서 값을 넣을 수 없게 되어있지만 리플렉션을 이용해서 제약조건을 우회했다.

    • setDatasource()를 필드주입으로는 안된다. setDatasource()는 주어진 오브젝트를 필드에 저장하는것이 아니라 JdbcTemplate()을 생성해서 저장해주기 때문이다.

@component를 이용한 등록

  • @Component가 붙은 클래스는 빈스캐너를 통해 자동으로 빈으로 등록된다.

  • @Configuration
    @EnableTransactionManagement
    @ComponentScan(basePackages = "toby")
    public class TestApplicationContext {
    
        @Autowired
        UserDao userDao;
    	...
    }
    
    • UserDaoJdbc에 @Component를 붙인다. 빈자동등록이 디폴트가 아니기때문에 @ComponentScan을 붙여서 basepackage를 지정해줘야한다.
    • @Componenet가 붙은 클래스가 발견되면 새로운 빈으로 등록한다. 빈의 아이디를 지정하지 않으면 클래스이름의첫글자를 소문자로 바꿔서 사용한다. 되도록 @Autowired를 사용해라
    • @Component(“value”)를 사용하면 value가 id가 된다.
    • dao에 대해서는 되도록 @repository를 써라

컨텍스트분리와 @Import

  • 다양한 환경분리를 해야된다. 테스트용과 일반용.

  • userDao, userService빈은 애플리케이션 운영과 테스트에 둘다 필요하다. testUserService는 테스트에서만 필요하다. 이를 위해 DI설정정보를 분리해야한다.

  • @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = {TestAppContext.class, domain.AppContext.class})
    public class UserDaoTest {
    
        @Autowired
        UserDao dao;
    ...
    }
    
    • 위의 방법으로 설정을 분리할 수 있다.

@Import

  • @Configuration을 추가한 클래스를 만들면 빈 정보를 분리할 수 있다.

  • xml설정정보는 @ImportResource를 통해 가져왔지만 자바클래스정보는 @Import를 이용하면된다.

  • @Configuration
    @EnableTransactionManagement
    @Import(value = SqlServiceContext.class)
    @ComponentScan(basePackages = "toby")
    public class AppContext {
    
        @Autowired
      ...
    }
    
    @Configuration
    public class SqlServiceContext {
        //    sql service
        @Bean
      ....
    }
    

프로파일

  • TestAppContext없이 AppContext만으로 설정정보를 지정하면 어떨까? 의존성있는 userService에서 에러가 발생.

  • AppContext에 MailSender를 구현하면 테스트에서도 구현된 부분이 있어서 충돌한다.

  • @Configuration
    public class ProductionAppContext {
        @Bean
        public MailSender mailSender() {
            JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
            mailSender.setHost("localhost");
            return mailSender;
        }
    }
    
    
    • ProductionAppContext도 SqlServiceContext처럼 @Import를 통해 AppContext에서 불러올 수 있지만 @TestAppContext가 AppContext를 사용하기 때문에 충돌이 발생한다. 스프링에서는 실행환경에 따라 빈 구성이 달라지는 내용을 프로파일로 정의해서 만들어지고 사용할 수 있다. 실행시점에 어떤 프로파일의 빈설정을 지정하면 된다.

    • @Configuration
      @Profile("test")
      public class TestAppContext {
          @Autowired
          private UserDao userDao;
      }
      
      @Configuration
      @EnableTransactionManagement
      @Import({SqlServiceContext.class, TestAppContext.class, ProductionAppContext.class})
      @ComponentScan(basePackages = "toby")
      public class AppContext {
      
       ...
      }
      
      // profile active
      @RunWith(SpringJUnit4ClassRunner.class)
      @ContextConfiguration(classes = {domain.AppContext.class})
      @ActiveProfiles("test")
      public class UserDaoTest {
      
          @Autowired
          UserDao dao;
      }
      
      
    • @Profile(“value”)처럼 value를 통해 지정하면 된다. TestAppContext는 test프로파일의 빈설정정보를 담은 클래스가 됬다.

    • 저 상태로 테스트 돌리면 실패한다. @Profile이 붙은 설정클래스는 @ContextConfiguration, @Import로 가져오든 현재 컨테이너의 @Active프로파일 목록에 자신의 프로파일 이름이 없으면 무시된다.

    • @ActiveProfiles(“value”)를 지정해야한다.

    • TestAppContext의 빈설정은 포함되고, ProductionContext의 설정은 production으로 선언되서 무시된다. 일정의 필터다

중첩클래스 이용

  • 설정파일들이 너무 많아졌다. 프로파일에 따라 분리된 설정클래스들을 static 클래스를 활용하여 하나로 합친다.

  • @Configuration
    @EnableTransactionManagement
    @Import({SqlServiceContext.class, AppContext.TestAppContext.class, AppContext.ProductionAppContext.class})
    @ComponentScan(basePackages = "toby")
    public class AppContext {
        @Autowired
        UserDao userDao;
       ...
        @Configuration
        @Profile("production")
        public static class ProductionAppContext {
            ...
        }
        @Configuration
        @Profile("test")
        public static class TestAppContext {
            ...
        }
    }
    
    

프로퍼티소스

  • db정보를 database.properties에 저장하자. 스프링은 빈설정 작업에 필요한 프로퍼티정보를 컨테이너가 관리하고 제공해준다.

  • 컨테이너가 프로퍼티 값을 가져온느 대상을 프로퍼티 소스라고 한다.

  • @Configuration
    @EnableTransactionManagement
    @Import({SqlServiceContext.class})
    @ComponentScan(basePackages = "toby")
    @PropertySource("/database.properties")
    public class AppContext {
        @Autowired
        UserDao userDao;
    
        @Autowired
        private Environment env;
    
        @Bean
        public DataSource dataSource() {
            SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
            try {
                dataSource.setDriverClass((Class<? extends java.sql.Driver>)Class.forName(env.getProperty("db.driverClass")));
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
            dataSource.setUrl(env.getProperty("db.url"));
            dataSource.setUsername(env.getProperty("db.username"));
            dataSource.setPassword(env.getProperty("db.password"));
            return dataSource;
        }
    }
    
  • @PropertyResource로 등록한 리소스로부터 가져오는 프로퍼티 값은 컨테이너가 관리하는 Environment타입의 환경오브젝트에 저장

  • Environment getProperty()를 통해 값을 가져올 수 있다.

propertysourcesPlaceholderConfigurer

  • Environment오브젝트 대신 직접 주입받을 수도 있다.

  • @Value를 쓰면된다.

    • @Configuration
      @EnableTransactionManagement
      @Import({SqlServiceContext.class})
      @ComponentScan(basePackages = "toby")
      @PropertySource("/database.properties")
      public class AppContext {
        	@Value("${db.driverClass")
          private Class<? extends Driver> driverClass;
      
          @Value("${db.url}")
          private String url;
      
          @Value("${db.username}")
          private String usernmae;
      }
      
      
    • @Value를 이용하면 driverClass처럼 문자열 그대로 사용하지 않고 타입변환이 가능한 장점과 리플렉션이나 try-catch가 없어서 깔끔하다. 반면 값을 주입받도록 클래스에 필드를 선언하는것이 부담스럽다.
Written on July 1, 2020