Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8주차](이승철, 김동하, 양재승) #10

Open
sunwootest opened this issue Aug 14, 2023 · 3 comments
Open

[8주차](이승철, 김동하, 양재승) #10

sunwootest opened this issue Aug 14, 2023 · 3 comments
Assignees
Labels

Comments

@sunwootest
Copy link
Collaborator

sunwootest commented Aug 14, 2023

  • 8주차
    • 7.5 ~ 7.7장
    • 8장
    • 9장
@d11210920
Copy link

토비의 스프링 7장 정리

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

운영중인 시스템에서 사용하는 정보를 실시간으로 변경하는 작업을 만들 때 가장 먼저 고려해야 할 사항은 동시성 문제이다.
이미 UpdatableSqlRegistry 라는 인터페이스를 정의해서 수정 가능한 SQL로 레지스트리 구현을 다양하게 DI로 적용할 수 있게 만들었으니
이를 구현해보자.

  1. 동기화된 해시 데이터 조작에 최적화되게 만들어진 ConcurrentHashMap을 사용해서 구현
  • 기존 HashMap을 ConcurrentHashMap으로 변경하고, UpdatableSqlRegistry에 추가된 메소드를 그에 맞게 구현해준다.
public class ConcurrentHashMapSqlRegistry implements UpdatableSqlRegistry {
    private Map<String, String> sqlMap = new ConcurrentHashMap<String, String>();
    
    
    @Override
    public String findSql(String key) throws SqlNotFoundException {
        String sql = sqlMap.get(key);
        if(sql == null) throw new 
                SqlNotFoundException(key + "를 이용해서 SQL을 찾을 수 없습니다");
        else return sql;
    }
    @Override
    public void registerSql(String key, String sql) {
        sqlMap.put(key, sql);
    }
    @Override
    public void updateSql(String key, String sql) throws SqlUpdateFailureException {
        if(sqlMap.get(key) == null) {
            throw new SqlUpdateFailureException(key + "에 해당하는 SQL을 찾을 수 없습니다.");
        }
        sqlMap.put(key, sql);
    }
    @Override
    public void updateSql(Map<String, String> sqlmap) throws SqlUpdateFailureException {
        for(Map.Entry<String, String> entry : sqlmap.entrySet()) {
            updateSql(entry.getKey(), entry.getValue());
        }
    }
}
  1. 내장형 데이터베이스를 이용한 SQL 레지스트리 만들기
  • ConcurrentHashMap은 저장되는 데이터의 양이 많아지고 잦은 조회와 변경이 일어나는 환경이라면 한계가 있다.
  • 별도의 DB를 구성하면 배보다 배꼽이 더 큰일이다.
  • 따라서 DB의 장점과 특징은 그대로 갖고 있으면서 애플리케이션 외부에 별도 설치하고 셋업하는 번거로움이 없는 내장형 DB를 사용.
    • 내장형 DB는 애플리케이션에 내장되어서 애플리케이션과 함께 시작-종료됨. 메모리에 저장되므로 IO로 인한 부하가 적다.
  • 내장형 DB는 애플리케이션 내에서 DB를 기동시키고 초기화 SQL스크립트를 실행시키는 초기화 작업이 별도로 필요하다.
    • 스프링은 내장형 DB를 초기화하는 작업을 지원하는 편리한 내장형 DB 빌더를 제공한다.
      • 내장형 DB빌더는 DB엔진을 생성하고 초기화 스크립트를 실행해서 테이블과 초기 데이터를 준비한 뒤에 DB에 접근 가능한 Connection을 생성해주는 DataSource 오브젝트, 정확히는 DB 셧다운 기능을 가진 EmbeddedDatabase 타입 를 돌려준다.

스프이 제공하는 내장형 DB 빌더는 EmbeddedDatabaseBuilder이다. 다음은 EmbeddedDatabaseBuilder를 사용하는 전형적인 방법이다.

new EmbeddedDatabaseBuilder()
    .setType(내장형DB종류)
    .addScript(초기화에 사용할 DB 스크립티의 리소스)
    ...
    .build();

EmbeddedDatabaseBuilder는 직접 빈으로 등록한다고 바로 사용할수 있는 게 아니라 적절한 메소드를 호출해주는 초기화 코드가 필요하다.
초기화 코드가 필요하다면 팩토리 빈으로 만드는 것이 좋다.
스프링에는 팩토리 빈을 만드는 작업을 대신해주는 전용 태그가 있다.

public class EmbeddedDbSqlRegistry implements UpdatableSqlRegistry{
    SimpleJdbcTemplate jdbc;
    public void setDataSource(DataSource dataSource) {
        jdbc = new SimpleJdbcTemplate(dataSource); 
        //DataSource를 DI 받아서 SimpleJdbcTemplate 형태로 저장해두고 사용한다.
    }

    @Override
    public String findSql(String key) throws SqlNotFoundException {
        try{
            return jdbc.queryForObject("select sql_from sqlmap where key_ = ?", String.class, key);
        }
        catch(EmptyResultDataAccessException e) {
            throw new SqlNotFoundException(key + "에 해당하는 SQL을 찾을 수 없습니다", e);
        }
    }
    @Override
    public void registerSql(String key, String sql) {
        jdbc.update("insert into sqlmap(key_, sql_) values(?,?)",key, sql);
    }
    
    @Override
    public void updateSql(String key, String sql) throws SqlUpdateFailureException {
        // update()는 SQL 실행 결과로 영향을 받은 레코드의 개수를 리턴한다. 
        // 이를 이용하면 주어진 키(key)를 가진 SQL이 존재했는지를 간단히 확인할 수 있다.
        int affected = jdbc.update("update sqlmap set sql_ = ? where key_ = ?", sql, key);
        if(affected == 0) {
            throw new SqlUpdateFailureException(key + "에 해당하는 SQL을 찾을 수 없습니다.");
        }
    }
}
  1. 트랜잭션 적용
    EmbeddedSqlRegistry는 내장형 DB를 사용하기 때문에 조회가 빈번하게 일어나는 중에도 데이터가 깨지는 일 없이 안전하게 SQL을 수정하도록 보장해준다.
    하지만 여러개의 SQL을 맵으로 전달받아 수정하는 경우에는 수정을 진행하는 중에 존재하지 않는 키가 발견될 경우 예외가 발생하고 작업이 중단된다.
    이때 트랜잭션이 적용되어있지 않기 때문에 이미 수정한 SQL은 그대로 DB에 반영되고 예외 발생 이후부터는 적용되지 않은 채로 작업을 마치게 된다.
    따라서 여러개의 SQL을 수정하는 작업은 반드시 트랜잭션 안에서 일어나야 한다.
    HashMap보다 내장형 DB가 트랜잭션 적용이 훨씬 쉽다.
    • 스프링에서 트랜잭션 적용 시 트랜잭션 경계가 DAO 밖에 있고 범위가 넓으면 AOP를 사용한다.
    • 하지만 SqlRegistry라는 제한된 오브젝트 내에서 간단한 트랜잭션이므로 트랜잭션 추상화 API를 직접 사용해보자.

트랜잭션의 적용은 수동 테스트 따위로 검증하기는 매우 어렵다. 특별한 예외상황이 아니라면 트랜잭션의 적용 여부가 결과에 별 영향을 주지 않기 때문이다.
따라서 트랜잭션이 적용되면 성공하고 아니면 실패하는 테스트를 만들자.

    @Test
    public void transactionalUpdate() {
        checkFindResult("SQL1", "SQL2", "SQL3"); // 초기 상태를 확인한다.
        
        Map<String, String> sqlmap = new HashMap<String, String>();
        sqlmap.put("KEY1", "Modified1");
        sqlmap.put("KEY9999!@#$", "Modified9999"); 
        // 두 번째 SQL의 키를 존재하지 않는 것으로 지정한다. 이때문에 테스트는 실패하고, 롤백이 일어나는지 확인한다.
        try {
            sqlRegistry.updateSql(sqlmap);
            fail();
        }catch (SqlUpdateFailureException e) {}
        checkFindResult("SQL1", "SQL2", "SQL3");
        // 첫번째 SQL은 정상적으로 수행했지만 트랜잭션이 롤백되기때문에 다시 변경 이전 상태로 돌아와야 한다.
        // 트랜잭션이 적용되지 않는다면 변경된 채로 남아서 테스트는 실패한다.
    }

아직 트랜잭션 적용을 하지 않았기에 이 테스트는 실패한다.

  • 코드를 이용한 트랜잭션 적용

    • 간결하게 트랜잭션 적용 코드에 템플릿/콜백 패턴을 적용한 TransactionTemplate을 사용
    • 일반적으로 트랜잭션 매니저를 싱글톤 빈으로 등록해서 사용하는데, 그 이유는 여러 AOP를 통해 만들어지는 트랜잭션 프록시가 같은 트랜잭션 매니저를 공유해야 하기 때문이다.
    • 하지만 EmbeddedDbSqlRegistry가 사용할 내장형 DB에 대한 트랜잭션 매니저는 공유할 필요가 없기에 EmbeddedDbSqlRegistry 내에서 직접 만들어 사용하는게 낫다.
       public class EmbeddedDbSqlRegistry implements UpdatableSqlRegistry {
           SimpleJdbcTemplate jdbc;
           TransactionTemplate transactionTemplate;
       
           public void setDataSource(DataSource dataSource) {
               jdbc = new SimpleJdbcTemplate(dataSource);
               transactionTemplate = new TransactionTemplate(
                                   new DataSourceTransactionManager(dataSource));
           }
       
    . . .
           public void updateSql(final Map<String, String> sqlmap) throws
                   SqlUpdateFailureException {
              transactionalTemplate.excute(new TransactionalCallbackWithoutResult() {
                   prptected void doInTransactionWithoutResult(TransactionStatus status) {
                       for(Map.Entry<String, String> entry : sqlmap.entrySet()) {
                           updateSql(entry.getKey(), entry.getValue());
                       }                  
                   }      
               });
           }
       }

    매우 분주하게 동작하는 서버환경이라면 트랜잭션 작업의 격리수준도 신경을 써야 한다.

7.6 스프링 3.1의 DI

자바 언어의 변화와 스프링

자바 언어와 관련 기술이 다양한 변화를 겪었고, 그에따라 스프링도 꾸준히 발전하고 변신해왔다.
하지만 이러한 변화에도 객체지향언어인 자바의 특징과 장점을 극대화하는 프로그래밍 스타일과 이를 지원하는 도구로서의 스프링 정체성은 변하지 않았다.
구버전 스프링을 이용해 개발했던 코드와 설정파일을 최신 스프링에서도 수정없이 그대로 사용할 수 있다.
스프링의 근본이 변하지 않은 덕이다.

  1. 애노테이션의 메타정보 활용
  • 자바 코드의 메타정보를 이용한 프로그래밍 방식의 대표적인게 애노테이션이다.
  • 애노테이션은 자바코드가 실행되는데 직접 참여하지 못한다.
애노테이션이 늘어난 이유 1. 핵심로직을 담은 자바코드와 IOC방식의 플레임워크, 프레임워크가 참조하는 메타정보 이 세가지로 구성하는 방식에 잘 어울린다. 2. 애플리케이션을 구성하는 많은 오브젝트와의 관계를 설정할 때 단순 자바코드로 만들어두면 불편하기에 XML로 전환을 했고 그 후 애노테이션이 등장을 했다. 애노테이션은 자바코드의 일부로 사용되기 때문에 XML과 비교했을때 여러 이점이 많다.
  • 애노테이션의 단점은 변경이 있을때 마다 매번 새로 컴파일을 해줘야 한다는 단점이 있다.
  • 하지만 흐름은 애노테이션으로 가고있고, 스프링 3.1부터는 XML을 완전히 배제한 설정이 가능하다.
  1. 정책과 관례를 이용한 프로그래밍
  • 메타정보를 활용하는 프로그래밍 방식이다. (명시적으로 동작 내용을 기술하는 대신 코드 없이도 미리 약속한 규칙/관례를 따라서 프로그램이 동작하도록 만드는 프로그래밍 스타일을 적극 포옹한다.)
  • 태그를 작성해두면 그에따라 하나의 오브젝트가 만들어지고, new 키워드를 이용한 인스턴스 생성 코드가 동작한다.
  • 는 프로퍼티 주입을 통해 오브젝트 의존 관계가 설정되는 코드가 동작하게 된다.
  • 하지만 프로그래밍 언어, API외에 이러한 정책을 잘못 안다면 의도대로 동작하지 않는 코드를 만들수 있다.
  • 간결하고 빠른 개발을 가능하게 해주기 때문에 이러한 개발 스타일이 늘어났다.

이 장에서는 지금까지의 예제 코드를 스프링 3.1의 DI스타일로 바꾸는 과정을 설명한다.

7.6.1 자바 코드를 이용한 빈 설정

테스트 컨텍스트의 변경

@Configuration
public class TestAppicationContext{

}

// UserDaoTest.java
//...
@ContextConfiguration(classes=TestApplicationContext.class) // 이제 XML 대신 클래스에서 DI 설정을 찾는다.
public class UserDaoTest{
}

자바 클래스로 만들어진 DI설정에 XML의 설정정보를 가져올 수도 있다.

@Configuration
@ImportResource("/test-applicationContext.xml") // xml 설정정보를 가져온다.
public class TestAppicationContext{

}

<context:annotation-config/> 제거

XML을 쓸때는 이 태그에 의해 등록되는 빈 후처리기가 @PostConstruct 와 같은 표준 애노테이션을 인식해서 자동으로 메서드를 실행해 주었지만,
@configuration이 붙은 설정 클래스를 사용하는 컨테이너가 사용되면, 컨테이너가 직점 @PostConstruct를 처리하는 빈 후처리기를 등록해준다.

<bean>의 전환

<bean> 으로 정의된 DI 정보는 @bean이 붙은 메서드와 1:1로 매핑된다. (메서드 이름이 의 id이다.)
@bean@configuration이 붙은 DI 설정용 클래스에서 사용된다.
메서드를 이용해 빈 오브젝트의 생성과 의존관계 주입을 코드로 직접 작성할 수 있게 해준다.
리턴값은 구현클래스보다 인터페이스로 해야 DI에 따라 구현체를 자유롭게 변경할 수 있지만, 메서드 내부에서는 빈의 구현 클래스에 맞는 프로퍼티 값 주입이 필요함.

@Bean
public DataSource dataSource() {
        SimpleDriverDataSource dataSource = new SimplerDriverDataSource();

        dataSource.setDriverClass(Driver.class);
        dataSource.setUrl("jdbc:mysql://localhost:port/....");
        dataSource.setUsername("myid");
        dataSource.setPassword("1q2w3e!");
        
        return dataSource;
}

@Autowired

XML의 프로퍼티를 이용해 자바 코드로 작성한 DI 정보를 참조할 수 있었지만, XML에 작성된 DI 정보를 자바 코드에서 참조할 수 있게 해준다.
만약 이 애노테이션이 붙은 필드의 타입과 같은 타입의 빈이 있다면 자동으로 필드에 주입해준다.

@Autowired
SqlService sqlService;

@Bean
public UserDao userDao() {
        . . .
        dao.setSqlService(this.sqlService);
        . . .
}

전용 태그 전환

XML에는 리스트 7-96에 나온 두 개의 빈 설정만 남았다.

<jdbc:embedded-database id=”embeddedDatabasetype=”HSQL”>
    <jdbc:script location=”classpath:schema.sql”/> <!-- 초기화 SQL 스크립트 등록 -->
</jdbc:embedded-database>

<tx:annotation-driven />

지금까지 전환엔 문제가 없었지만, 특수한 태그를 이용한 위 두 개의 빈은 내부에서 어떤 과정을 거쳐 빈이 만들어지는지 파악하기 어렵다.

스프링 서비스에서 사용하는 내장형 DB를 생성하는 /jdbc:embedded-database 전용 태그는 type에 지정한 내장형 DB를 생성하고 <jdbc:script> 로 지정한 스크립트로 초기화 한뒤, DataSource 타입 DB 커넥션 오브젝트를 빈으로 등록해준다.
<jdbc:embedded-database> 태그는 위와 같은 복잡한 과정을 거치는데, 자바 코드에선 학습 테스트를 만들때 보았던 EmbeddedDatabaseBuilder가 비슷한 역할을 한다.

트랜잭션 AOP를 적용하려면 수많은 빈이 필요한데, <tx:annotation-driven /> 태그는 기본적으로 아래 4가지 클래스를 빈으로 등록한다.

InfrastructureAdvisorAutoProxyCreator
AnnotationTransactionAttributeSource
TransactionInterceptor
BeanFactoryTransactionAttributeSourceAdvisor

하지만 스프링은 @enable로 시작하는 애노테이션으로 대체할 수 있도록 애노테이션을 제공한다.
따라서 위의 <tx:annotation-driven /> 코드는 @EnableTransactionManagement으로 대체할 수 있다.

7.6.2. 빈 스캐닝과 자동 와이어링

@Autowired를 이용한 자동와이어링

  • @Autowired는 자동와이어링 기법을 이용해서 조건에 맞는 빈을 찾아 자동으로 수정자 메소드나 필드에 넣어준다.
    컨테이너가 이름/타입 기준으로 주입될 빈을 찾아주기에 프로퍼티 설정을 직접 해주는 코드를 줄일 수 있다.
  • setter에 @Autowired 붙이면 파라미터 타입을 보고 주입 가능한 타입의 빈을 모두 찾음. 주입 가능한 빈이 1개일땐 스프링이 setter를 호출해서 넣고, 2개 이상일때는 그 중에서 프로퍼티와 동일한 이름의 빈을 찾아 넣고 없으면 에러를 발생시킨다.
@Autowired
public void setDataSource(DataSource dataSource) {
    this.jdbcTemplate = new JdbcTemplate(dataSource);
}

setter에서 필드 그대로 넣는다면 필드에 직접 @Autowired 를 적용할 수 있다.

자동 와이어링은 DI 관련 코드를 줄일 수 있는 장점이 있지만, 다른 빈과 의존관계가 어떻게 맺어져 있는지 파악하기가 힘들다.

@component 를 이용한 자동 빈 등록

클래스에 붙이면 빈 스캐너를 통해 자동으로 등록될 대상이 된다.
이 애노테이션이 달린 클래스를 자동으로 찾아 등록해주게 하려면 빈 스캔 기능을 사용하겠다는 애노테이션 정의가 필요하므로 아래와 같이 정의해준다.
@componentscan(basePackages="springbook.user")
스프링 부트에서는 @SpringBootApplication@component가 포함되어있다.

@component로 추가되는 빈의 id는 별도로 지정이 없으면 클래스 이름의 첫글자를 소문자로 바꿔서 사용한다.

  • 별도 지정은 @component("userDao")와 같이 할수 있다.

그런데 스프링에선 @component 이외의 애노테이션을 부착하여도 자동으로 빈 등록이 가능하다. 빈 스캔 검색 대상으로 만들 뿐 아니라 다른 의미의 마커로도 사용할 수 있도록 하기 위함이다.
AOP 적용대상 포인트컷을 패키지나 클래스로도 지정할 수 있지만, 애노테이션 기준으로 부가기능을 부여하는 것도 가능하다. @transactional이 가장 대표적인 예이다.
이 때 편의상 AOP 적용 대상에 부착할 애노테이션을 만들고 싶은데 이 애노테이션은 빈 자동등록 대상임을 알리는 용도로도 쓰고 싶다.
애노테이션은 상속도 불가능하고 인터페이스 구현도 불가능하다.

이럴때 메타 애노테이셭을 사용한다.

메타 애노테이션 메타 애노테이션이란 애노테이션의 정의에 부여된 애노테이션을 말한다. 애노테이션끼리는 상속도 안되고, 인터페이스 구현도 안되기 때문에 여러개의 애노테이션에 공통 속성을 부여하려면 메타 애노테이션을 사용한다.

다음은 메타 애노테이션으로 만들어진 @SnsConnector 애노테이션이다.

@Component
public @interface MyAnnotation{
    . . .
}

7.6.3 컨텍스트 분리와 @import

테스트용 컨텍스트 분리

지금까지 태스트용 testUserService 빈과 userService 빈을 한곳의 XML로 담아 두었다. 성격이 다른 DI 정보를 분리해보자.

기존에는 하나였던 설정을 1. 테스트애만 쓰이는 TestAppContext와 실제 앱의 동작에 쓰이는 AppContext 로 분리하고,
실제 로직에는 AppContext 를 import 하고, 테스트에는 두개 모두 import 하도록 해보자.

@ContextConfiguration(classes={TestAppContext.class, AppContext.class})
public class UserDaoTest {

@import

지금까지 만들었던 SqlService는 AppContext 내의 빈들과 구분되는 특징이 있고, 다른데에서도 충분히 쓰일 수 있기 때문에
SqlServiceContext 클래스로 분리하여 모듈처럼 관리하는게 낫다. 빈 내용은 수정할 것 없이 다른 @configuration 클래스를 하나 더 만들어서 옮겨주면 된다.

@Configuration
public class SqlServiceContext {
    @Bean
    public SqlService sqlService() {
        0xmSqlService sqlService = new 0xmSqlService();
        sqlService.setUnmarshaller(unmarshaller());
        sqlService.setSqlRegistry(sqlRegistry());
        return sqlService;
    }
    @Bean
    public SqlRegistry sqlRegistry() {
        EmbeddedDbSqlRegistry sqlRegistry = new EmbeddedDbSqlRegistry();
        sqlRegistry.setDataSource(embeddedDatabase());
        return sqlRegistry;
    }
    @Bean
    public Unmarshaller unmarshaller(){
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setContextPath("springbook.sqlservice.jaxb");
        return marshaller;
    }
    @Bean
    public DataSource embeddedDatabase() {
        return new EmbeddedDatabaseBuilder()
                .setName("embeddedDatabase")
                .setType(HSQL)
                .addScript(
                "classpath:springbook/user/sqlservice/updatable/sqlReistrySchema.sql")
                .build();
    }
}

SQL 서비스와 관련된 빈 설정은 별도로 분리하긴 했지만 애플리케이션이 동작할 때 항상 필요한 정보다. 따라서 파일을 구분했더라도 애플리케이션 설정정보의 중심이 되는
AppContext와 긴밀하게 연결해주는 게 좋다. 따라서 AppContext의 클래스 레벨에 @import를 추가해서 SqlServiceContext를 가져오게 만들자.

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages="springbook.user")
@Import(SqlServiceContext.class) 
public class AppContext {

7.6.4 프로파일

테스트 환경과 운영 환경에서 각각 다른 빈 정의가 필요한 경우가 있다. 테스트를 실행할때 DummyMailSender 라는 테스트용 클래스를 만들어 사용했는데,
운영 시스템에서는 실제 동작하는 메일 서버를 통해 메일을 발송하는 기능이 있는 메일 발송 서비스 빈이 필요하다.

이때 사용하는 것이 @Profile@activeprofiles 이다.

@Profile@activeprofiles

실행환경에 따라 빈 구성이 달라지는 내용을 프로파일로 정의해서 만들어두고, 실행시점에 지정해서 사용한다.

@Configuration
@Profile("test")
public class TestAppContext {
    //@Profile을 지정한 TestAppContext
}
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages="springbook.user")
@Import({SqlServiceContext.class, TestAppContext.class, ProductionAppContext.class}) 
public class AppContext {
    //@Import에 모든 설정 클래스 추가
}
@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles("test") 
@ContextConfiguration(classes=AppContext.class)
public class UserServiceTest {
    //활성 프로파일을 지정한 UserServiceTest

컨테이너의 빈 등록 정보 확인

정말 활성 프로파일이 제대로 적용돼서 지정한 프로파일의 빈 설정만 적용되고 나머지는 무시됐는지, 의도한 빈 정보만 적용됐는지를 알고싶다면
다음과 같이 간단히 스프링 컨테이너에 등록된 빈 정보를 조회하는 방법이 있다.

@Autowired
DefaultListableBeanFactory bf;

@Test
public void beans(){
        for(String str : bf.getBeanDefinitionNames()){
                System.out.println(str + "\t" + bf.getBean(str).getClass().getName());
        }
}

스프링 컨테이너는 BeanFactory 인터페이스를 구현하고, 그 중 DefaultListableBeanFactory 구현 클래스는 대부분의 스프링 컨테이너에서 사용된다.
getBeanDefinitionNames()는 컨테이너에 모든 빈 이름을 가져올 수 있다.

중첩 클래스를 이용한 프로파일 적용
거대한 하나의 빈 설정을 @import를 이용해 나누고, 프로파일을 적용하여 상황에 따른 빈 설정이 가능하게 했다.
근데 파일이 많아지면 이 모든 것 들을 한 눈에 보기 어려워 분리했던 설정정보를 중첩 클래스를 이용해 하나로 모아보자.

프로파일 설정과, 목적이 다른 빈을 다른 클래스로 분리해 놓은것은 여전히 유효하도록 하면서, 가독성을 높이는 방법이다.

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages="springbook.user")
@Import({SqlServiceContext.class, 
                    AppContext.TestAppContext.class, AppContext.ProductionAppContext.class}) 
public class AppContext {
    . . .


    @Configuration
    @Profile("production")
    public static class ProductionAppContext {
        . . .
    }
        
    @Configuration
    @Profile("test")
    public static class TestAppContext {
        . . .
    }
}

두 클래스에 static만 붙여서 AppContext로 가져온 뒤 @import에 지정했던 클래스를 내부로 이동시킨 클래스로 바꿔주기만 하면 된다.

이제 AppContext만 열어보면 디폴트로 적용될 빈이 무엇인지, 프로파일을 지정하기에 따라 어떤 빈이 등록될지, 또 프로파일에 따라
같은 빈의 구현 클래스가 어떻게 달라지는지를 좀 더 쉽게 확인할 수 있게 됐다.

7.6.5 프로퍼티 소스

AppContext에는 여전히 테스트 환경에 종속되는 정보가 남아있다. dataSource의 DB 연결 정보이다.
드라이버 클래스, URL, 계정 정보는 환경에 따라 달라진다.

그래서 이런 정보들은 자바 코드에 직접 하드코딩 하는 대신, XML이나 프로퍼티 파일같은 텍스트 파일에 저장해 두는 것이 좋다.

db.driverClass=com.mysql.jdbc.Driver
db.url=jdbc:mysql://....
db.username=spring
db.password=book

빈 설정에 필요한 프로퍼티를 외부 정보로 부터 가져올 수 있다.
이렇게 프로퍼티 값을 가져오는 대상을 프로퍼티 소스라고 부른다.

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages="springbook.user")
@Import(SqlServiceContext.class)
@PropertySource("/database.properties")
public class AppContext {

이렇게 아래처럼 등록해두면 컨테이너가 관리하는 Environment 타입의 환경 오브젝트에 프로퍼티가 저장된다.

@Autowired Environment env;

@Bean
public DataSource dataSource {
    SimpleDriverDataSource ds = new SimpleDriverDataSource();
    try {
        ds.setDriverClass((Class<? 
            extends java.sql.Driver>)Class.forName(env.getProperty("db.driverClass")));
    } 
    catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
    ds.setUrl(env.getProperty("db.url"));
    ds.setUsername(env.getProperty("db.username"));
    ds.setPassword(env.getProperty("db.password"));
    
    return ds;
}

PropertySourcesPlaceholderConfigurer에서 @value 애노테이션을 통해 치환자로 프로퍼티를 소스로부터 직접 주입받을 수 있다.

@PropertySource("/database.properties")
public class AppContext {
    @Value("${db.driverClass}") Class<? extends Driver> driverClass;
    @Value("${db.url}") String url;
    @Value("${db.username}") String username;
    @Value("${db.password}") String password;

@value와 치환자로 프로퍼티 값을 필드에 주입하려면 PropertySourcesPlaceholderConfigurer 빈을 정의해주어야 한다.

@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
    return new PropertySourcesPlaceholderConfigurer();
}

7.6.6 빈 설정의 재사용과 @enable*

SqlServiceContext는 조금 특별한 특징이 있어 AppContext와 분리하도록 했다.
그래서 여러 프로젝트에서 재사용이 쉽고, 빈 설정을 깔끔하게 유지할 수 있었다.

그러나 여전히 SQL 서비스는 특정 위치의 특정 파일에 의존적인 문제가 있다.

private class OxmSqlReader implements SqlReader {
    private Unmarshaller unmarshaller;
    private Resource sqlmap = new ClassPathResource("sqlmap.xml", UserDao.class);
}

UserDao의 클래스 패스의 sqlmap.xml로 고정되어있다.

SQL 서비스를 재사용 가능한 독립적인 모듈로 만들려면 UserDao 위치로 고정되어있는 SQL매핑파일의 위치를 직접 지정할 수 있도록 수정해주어야 한다.
따라서 SqlMapConfig 인터페이스를 하나 정의하고 매핑파일 리소스를 돌려주는 간단한 메소드를 정의한다.

아래는 위 인터페이스를 구현한 클래스이다.

public class UserSqlMapConfig implements SqlMapConfig{
    @Override
    public Resource getSqlMapResource() {
        return new ClassPathResource("/sqlmap.xml", UserDao.class);
    }
}

그 후 SqlServiceContext가 변하지 않는 SqlMapConfig 라는 인터페이스에만 의존하게 만들고, 구현 클래스는 빈의로 정의해 런타임 시 주입되게 만든다.
SqlMapConfig 타입 빈을 @Autowired를 이용해 필드로 주입받아 사용하도록 수정한다.

@Configuration
public class SqlServiceContext {
    @Autowired SqlMapConfig sqlMapConfig;

    @Bean
    public SqlService sqlService() {
      OxmSqlService sqlService = new OxmSqlService();
      sqlService.setUnmarshaller(unmarshaller());
      sqlService.setSqlRegistry(sqlRegistry());
      sqlService.setSqlmap(new ClassPathResource("/sql/sql-map.xml", UserDao.class));
      return sqlService;
    }

그 후 SqlMapConfig을 구현한 UserSqlMapConfig 클래스를 빈으로 등록 해준다.

public class AppContext {
    . . .
    
    @Bean
    public SqlMapConfig sqlMapConfig() {
        return new UserSqlMapConfig();
    }
}

@enable*애노테이션
이제는 SqlServiceContext는 모듈화가 되어 빈 설정에 재사용될 수 있다.
@import를 이용해야 하지만, 애노테이션을 보다 가독성을 높이는 방향으로 쓰기 위해 @enable* 애노테이션을 구현하자.

@Import(value = SqlServiceContext.class) 
public @interface EnableSqlService {
}
// 메타 애노테이션으로 넣은 애노테이션 정의 
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages="springbook.user")
@EnableSqlService
@PropertySource("/database.properties")
public class AppContext implements SqlMapConfig{

이렇게 하니 전보다 가독성이 더 좋아졌다.

7.7 정리

  1. SQL처럼 변경될 수 있는 텍스트로 된 정보는 외부 리소스에 담아두고 사용하면 편리하다.
  2. 성격이 다른 코드가 섞인 클래스는 인터페이스별로 분리하는게 좋다.
  3. 자주 사용되는 의존 객체는 디폴트로 미리 정의해두면 편리하다.
  4. XML과 객체 매핑은 스프링의 OXM 추상화 기능을 활용한다.
  5. 특정 의존 객체를 고정시켜 기능을 특화하려면 멤버 클래스로 만드는 것이 편리하고 기존 기능과 중복은 위임을 통해 제거하는 것이 좋다.
  6. 외부 파일이나 리소스를 사용하는 코드에서는 스프링의 리소스 추상화와 리소스 로더를 사용한다.
  7. DI를 의식하면서 개발하면 객체지향 설계에 도움이 된다.
  8. DI에는 인터페이스를 사용한다.
  9. 클라이언트에 따라 새로운 인터페이스를 만드는 방법이나 인터페이스를 상속하는 방법 두 가지를 사용할 수 있다.
  10. 내장 DB를 사용할 때는 스프링의 내장형 DB추상화 기능과 전용 태그를 사용하면 편리하다.

@sheepseung
Copy link

chapter 9. 스프링 프로젝트 시작하기

  • 📗자바 엔터프라이즈 플랫폼과 스프링 애플리케이션

    클라이언트와 백엔드 시스템

    가장 많이 사용되는 구조는 클라이언트가 웹 브라우저이고 백엔드 시스템이 DB인 구성이다.

    간단히 ’DB를 사용하는 웹 애플리케이션’ 이라고 한다. 웹 클라이언트와 DB가 사용되지 않는 시스템은 거의 없으니, 이를 스프링이 사용되는 애플리케이션의 기본구조라고 생각할 수도 있다.

    그렇다고 꼭 클라이언트는 웹 브라우저여야 하며 백엔드 시스템은 DB를 이용해야 하는 것만은 아니다. HTML을 사용하는 표준 웹 클라이언트 외에도 Flex나 X 인터넷 제품처럼 독립적으로 강력한 기능을 가진 RIA 클라이언트가 사용되기도한다.

    또는 HTTP 프로토콜을 이용해 통신하는 다른 엔터프라이즈 시스템일 때도 있다.

    자바 서버가 받아들일수 있는 방식으로 요청을 보내기만 한다면 어떤 종류의 클라이언트이든 상관없다.

    !https://blog.kakaocdn.net/dn/dp498D/btqGkgws7kf/JRa4CVyQ9ULdBjdVoGf7I1/img.png

    애플리케이션 서버

    스프링으로 만든 애플리케이션을 자바 서버환경에 배포하려면 JavaEE 서버가 필요하다.

    JavaEE 표준을 따르는 애플리케이션 서버는 크게 두 가지로 구분할 수 있다.

    • JavaEE의 대부분의 표준 기술을 지원하고 다양한 형태의 모듈로 배포 가능한 완전한 웹 애플리케이션 서버(WAS)
    • 웹 모듈의 배포만 가능한 경량급 WAS또는 서블릿/JSP 컨테이너다.

    경량급 WAS/서블릿 컨테이너

    스프링은 기본적으로 톰캣이나 제티 같은 가벼운 서블릿 컨테이너만 있어도 충분하다.

    EJB나 리소스 커넥터, WAS가 제공하는 분산 서비스 등이 굳이 필요하지 않다면 서블릿 컨테이너로도 엔터프라이즈 애플리케이션에 필요한 핵심기능을 모두 이용할 수 있다.

    WAS

    성능면에서 대단히 낫지 않더라도 미션 크리티컬한 시스템에서 요구하는 고도의 안정성이나 고성능 시스템에서 안정적인 리소스 관리등 필요가 있다면 상용/오픈소스 WAS를 이용할 수 있다.

    또 상대적으로 관리 기능이나 모니터링이 기능이 뛰어나서 여러 대의 서버를 동시에 운영할 때 유리한 점이 많다.

    스프링 애플리케이션의 배포단위

    • 독립 웹 모듈

    톰캣 같은 서블릿 컨테이너를 쓴다면 독립 웹 모듈이 유일한 방법이다.

    WAS로 배포한다고 하더라도 독립 웹 모듈을 사용하는 경우가 대부분일 것이다. EJB 모듈을 함께 사용한다거나 여러 개의 웹 모듈을 묶어서 하나의 웹 애플리케이션 모듈로 만들지 않는 한 독립 웹 모듈이 가장 단순하고 편리한 배포 단위다.

    • 엔터프라이즈 애플리케이션

    경우에 따라선 확장자가 ear인 엔터프라이즈 애플리케이션으로 패키징해서 배포할 수도 있다. 하나 이상의 웹 모듈과 별도로 구분된 공유 가능한 스프링 컨텍스트를 엔터프라이즈 애플리 케이션으로 묶어주는 방법이다.

    • 백그라운드 서비스 모듈

    이 두가지 방법 외에도 J2EE 1.4에서 등장한 rar 패키징 방법도 있다. rar는 리소스 커넥터를 만들어 배포할 때 사용하는 방식인데, 만약 스프링으로 만든 애플리케이션이 UI를 따로 가질 필요는 없고 서버 내에서 백그라운드 서비스처럼 동작할 필요가 있다면 rar 모듈로 만들어서 배포할 수 있다.

  • 📗애플리케이션 아키텍쳐

    계층형 아키텍처

    DI를 이용하여 관심사의 분리를 원칙으로 코드를 짜듯 애플리케이션을 구성하는 오브젝트들 또한 하나의 관심사로 나눌 수 있다.

    책임과 성격이 다른 것을 크게 그룹으로 만들어 분리해두는 것을 아키텍처 처원에서는 계층형 아키텍처라고 부른다.

    보통의 웹 기반의 엔터프라이즈 애플리케이션은 일반적으로 3계층 구조를 갖는다.

    • 데이터 엑세스 계층(Repository)

      DAO 계층이라고도 불리며 DB, ERP, 레거시 시스템 등에 접근하는 역할을 하는 계층으로 대게는 장기적인 데이터 저장을 목적으로 하는 DB 이용이 주된 책임을 가지는 계층.

    • 서비스 계층(Service)

      POJO로 만들어지는 비지니스 로직의 핵심을 담는 계층 다만, 비지니스 로직을 담은 서비스 계층과 엔터프라이즈 서비스를 제공하는 기반 서비스 계층은 이름 때문에 혼동하기 쉬우니 주의가 필요함.

      기반 서비스 계층은 주로 트랜잭션, 보안, 리모팅, 메일, 메세징 등의 서비스를 제공.

    • 프레젠테이션 계층(Controller)

      HTTP 프로토콜을 주로 처리하는 엔터프라이즈 어플리케이션의 특성상 서블릿 기술이 바탕이 되어 다른 계층과 달리 클라이언트까지 그 범위가 확장 될 수 있는 계층으로 초기엔 클라이언트 영역을 처리 했지만 지금은 RIA 나 SOFEA 로 프레젠테이션 계층이 이동하고 있음.

      • RIA : HTML5, JS 를 통해 구성되는 아키텍처로 좀 넓은 범위
      • SOFEA : 프레젠테이션 계층에 해당하는 코드를 클라이언트로 완전히 넘어가 분리되는 형태

    이런 계층을 설계 할때 주의점으로는 각 계층이 자신의 역할과 책임에만 집중해야 하고, 다른 계층과는 인터페이스를 통해 최대한 특정 계층의 기술이 덜 드러나도록 구현해야 한다.

    애플리케이션 정보 아키텍처

    어플리케이션에 흘러다니는 정보를 어떻게 다룰지를 결정하는 아키텍처

    데이터 중심 아키텍처

    데이터를 단순히 값이나 값을 담기 위한 목적의 오브젝트 형태로 취급

    • DB/SQL 중심의 로직 구현 방식 : DB 에서 돌려주는 내용을 그대로 맵이나 단순 결과 저장용 오브젝트에 넣어서 전달해 서비스 계층과 프레젠테이션 계층에서도 이를 사용.

    • 하나의 기능 트랜잭션에 모든 계층의 코드가 종속되는 경향이 있음.

    • 자바 코드는 단순히 DB 와 웹 화면을 연결해주는 인터페이스 도구로만 사용.

    • 거대한 서비스 계층 방식 : 상대적으로 단순한 DAO 로직을 사용하며 비지니스 로직의 대부분을 서비스 계층에 집중하는 방식

      ⇒비지니스 로직이 어플리케이션의 코드에 담기기 때문에 자바 코드를 활용해 로직을 구현해 테스트 하기 쉽고 자바 코드의 장점을 살릴 수 있음.

    오브젝트 중심 아키텍처

    도메인 모델을 반영하는 오브젝트 구조를 만들어두고 그것을 각 계층 사이에서 정보를 전송하는 데 사용

    • 도메인 오브젝트 : 도메인 모델의 구조를 반영해 만들어진 오브젝트로 어플리케이션의 전 계층에서 동일한 의미를 갖기에 SQL이나 웹 페이지의 출력 포맷, 입력 폼 등에 종속되지 않는 일관된 형식의 어플리케이션 정보를 다룰 수 있음. SQL은 필요한 내용에 따라 별도의 SQL 을 계속 작성해서 필드 값을 가져와 Map 형태로 만들어서 사용됨. 다만 최적화된 SQL 을 사용하지 않기 때문에 성능면에서 떨어짐. 다만 지연된 로딩 등을 사용하고 JPA나 하이버네이트 등 RDB 매핑 기술을 사용하면 좀 더 간단하게 사용할 수 있음.
    • 빈약한 도메인 오브젝트 방식 : 정보만 담겨 있고 정보를 활용하는 아무런 기능도 갖고 있지 않을 경우
      • 주된 비지니스 로직은 서비스 계층에 집둥 되기 때문에 어떻게보면 거대한 서비스 계층 방식의 하나라고 볼 수 있음.
    • 풍성한 도메인 오브젝트 방식 : 정보와 정보를 활용하는 비지니스 로직들이 담겨 있는 오브젝트
      • 다만 어디서든 비지니스 로직에 접근 할 수 있기 때문에 사용에 주의.

    스프링 애플리케이션을 위한 아키텍처 설계

    상태 관리와 빈 스코프

    아키텍처 설계에서 한 가지 더 신경 써야 할 사항은 상태 관리다. 크게는 사용자 로그인 세션 관리부터, 작게는 하나의 단위 작업이지만 여러 페이지에 걸쳐 진행되는 위저드 기능까지 애플리케이션은 하나의 HTTP 요청의 범위를 넘어서 유지해야 하는 상태정보가 있다.

    엔터프라이즈 애플리케이션은 특정 사용자가 독점해서 배타적으로 사용되지 않는다. 하나의 애플리케이션이 동시에 수많은 사용자의 요청을 처리하게 하기 위해 매번 간단한 요청을 받아서 그 결과를 돌려주는 방식으로 동작한다.

    따라서 서버의 자원이 특정 사용자에게 일정하게 할당되지 않는다. 그래서 서버 기반의 애플리케이션은 원래 지속적으로 유지되는 상태를 갖지 않는다(stateless)는 특징이 있다.

    하지만 어떤 식으로든 애플리케이션의 상태와 장시간 진행되는 작업정보는 유지돼야 한다. 이를위해 웹 클라이언트에 URL, 파라미터, 폼 히든 필드, 쿠키 등을 이용해 상태정보 또는 서버에 저장된 상태정보에 키 값 등을 전달해야 한다.

    이렇게 상태를 저장, 유지하는데 어떤 방식을 사용할지 결정하는 일은 매우 중요하다. 스프링은 기본적으로 상태가 유지되지 않는 빈과 오브젝트를 사용하는 것을 권장한다. 반면에 웹 클라이언트에 폼 정보를 출력하고 이를 수정하는 등의 작업을 위해서는 HTTP 세션을 적극 활용하기도 한다.

@tmdcheol
Copy link

8장 정리


스프링의 정의

자바로의 개발을 편하게 해주는 오픈소스 경량급 ** 애플리케이션 프레임워크 **
라이브러리, 프레임워크는 목표를 가지고 만들어진다.

애플리케이션 프레임워크
특정 계층이나, 기술, 업무 분야에 국한되지 않는 범용적인 프레임워크.


스프링의 기원

스프링은 자바 엔터프라이즈 개발 전략의 핵심 을 그대로 이어 개발 되었다.
스프링의 일차적인 목적은 핵심 기술에 담긴 프로그래밍 모델을 일관되게 적용해서 엔터프라이즈 애플리케이션 전 계층과 전 영역에 전략과 기능을 제공해줌으로써 애플리케이션을 편리하게 개발하게 해주는 애플리케이션 프레임워크로 사용되는 것
스프링은 EJB에 반하여 개발되었다.


편리한 애플리케이션 개발

  • 로우레벨 기술에 많은 신경 X
  • 핵심인 비즈니스 로직을 빠르고 효과적으로 구현

=> 스프링은 애플리케이션 비지니스 로직에 집중하도록 도와줌.


스프링의 목적

  • 정확한 목적을 이해하고, 잘 활용해야 제대로 된 가치를 얻을 수 있음.
  • 자바의 근본적인 목적은 객체지향 프로그래밍을 통해 유연하고 확장성 좋은 애플리케이션을 빠르게 만드는 것
  • 기존 개발은 기술적인 제약조건, 요구사항이 늘어나서 복잡성이 계속 증가함. 자주 수정사항이 발생한다.
  • 특정 환경에 종속적이지 않게 코드를 짤 수 있게하자
    • => 인터페이스를 분리하고, 환경에 독립적인 인터페이스를 제공한다.
  • 기술적인 처리를 담당하는 코드를 분리하자
    • aop
      • 트랜잭션, 로깅 등

DI를 사용해서, 비즈니스 로직은 최대한 순수하게 가져가자.
DI를 의식하다 보면, 변경 가능한 후보가 없을지 생각해보게 되고, 객체지향적 장점을 잘 살린 설계가 나올 수 있다.


POJO 프로그래밍

POJO의 조건

  • 특정 규약과 환경에 종속되지 않는다.
  • 객체지향적 원리에 충실하면서, 환경과 기술에 종속되지 않고 재활용될 수 있는 방식으로 설계된 오브젝트.
    => 이런 POJO에 핵심로직과 기능을 담아 설계, 개발하는 방법이 POJO 프로그래밍

스프링의 기술

POJO 프로그래밍을 위해 spring이 지원하는 세가지 가능기술: IoC/DI, AOP, PSA

IoC/DI

DI를 하는 이유

사용할 대상은 DI를 통해 외부에서 지정한다.
이를 통해 유연한 확장이 가능한 것이다.

핵심 기능의 변경

실제 의존하는 대상이 가진 핵심 기능을 DI설정을 통해 변경
ex) service는 repository를 의존한다. 그리고 repository는 JDBC, JPA, 하이버네이트 구현체로 이루어져 있다.
이 repository가 변경되기 위해서, interface를 중간에 두고, 갈아 끼울 수 있게 하는 것이다.

인터페이스의 변경

오브젝트가 가진 인터페이스가 클라이언트와 호환되지 않는 경우, 어댑터 패턴 적용

자주 바뀌는 부분을 정리해서 템플릿, 콜백으로 만들자.
자주 변경 -> 콜백, 템플릿은 계속 재사용

싱글톤과 오브젝트 스코프
스프링은 싱글톤 컨테이너가 클래스를 싱글톤으로 만들어 관리해준다.
ex) http request, session 같은 오브젝트도 스코프를 설정하여 DI에 사용 가능하다.

테스트

의존성이 있다면, mock object 주입.
DI가 있어서 가능한 것이다.

AOP

보통 IoC/DI 을 통해 POJO 프로그래밍을 하지만,
일부 서비스는 순수한 객체지향기법만으로는 POJO를 유지하기 힘들다.
⇒ 이 때 AOP가 필요함.

AOP의 적용 기법

AspectJ 사용 - 메서드 호출, 필드 액세스, 인스턴스 생성 등에도 부가 기능 제공 가능
클래스가 메모리로 로딩될 때 바이트코드를 조작해서 이루어진다.

AOP의 적용

트랜잭션(@transactional)

보안, 로깅, 데이터 추적, 성능 모니터링 같은 기능에 적용하면 유용하다.
비즈니스 로직을 순수하게 가져가서 변경사항이 있더라도 코드를 바꾸지 않도록 하는 것이다.
스프링을 통해서 AOP의 자유로운 이용이 가능하다. 스프링을 통해 비즈니스 로직에 세부적인 AOP 적용해서 사용하면 된다.


PSA

구체적 기술, 설정은 xml파일로 지정하자.
스프링이 지원하지 않더라도, 서비스 추상화를 위해 DI를 적용하자.
직접 추상 레이어를 도입하고, DI를 도입하자.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants