Spring - (29) : 스프링 핵심 기술의 응용

­이승환·2021년 12월 19일
0

spring

목록 보기
26/26

Overview


토비의 스프링을 읽고 정리한 내용이다. 이번 포스팅에서는 스프링 핵심 기술의 응용을 정리하고자 한다.

문제가 있을 시, lshn1007@hanyang.ac.kr 로 메일 주시면 삭제하겠습니다.

Goal

  • 스프링의 3대 핵심 기술인 IoC/DI + PSA + AOP 기술을 활용해서 새로운 기능을 만들고, 과정에서 스프링의 개발 철학과 추구하는 가치, 스프리에 사용자에게 요구되는 것이 무엇인지 확인하고자 함

XML 파일 매핑

  • SQL 정보를 스프링의 빈 설정으로 넣어놓고 활용하는 방법은 좋은 방법이 아니다. 그보다는 SQL을 저장하는 독립적인 XML 파일을 만들어 활용하는 편이 바람직하다. (SQL Mapping)
  • XML에 담긴 정보를 파일에서 읽어오는 방법은 다양한데 가장 간단하게 사용할 수 있는 방법은 JAXB(Java Architecture for XML Binding)를 이용하는 방법이다. JDK 6라면 java.xml.bind 패키지 안에서 JAXB의 구현 클래스를 찾을 수 있다.
  • JAXB는 XML 문서 정보를 거의 동일한 구조의 오브젝트로 직접 매핑할 수 있기 때문에 XML 정보를 오브젝트로 다룰 수 있어 편리하다.
  • JAXB는 XML 문서의 구조를 정의한 스키마를 이용해서 매핑할 오브젝트의 클래스까지 자동으로 만들어주는 컴파일러(XJC)도 제공해준다. 스키마 컴파일러를 통해 자동생성된 오브젝트에는 매핑 정보가 애노테이션으로 담겨 있다. JAXB API는 애노테이션에 담긴 정보를 이용해서 XML과 매핑된 오브젝트 트리 사이의 자동변환 작업을 수행해준다.

마샬링과 언마샬링

  • Marshalling : 자바 오브젝트를 XML로 변환하는 것 (JAXB)
  • Unmarshalling : XML을 자바 오브젝트로 변환하는 것 (JAXB)

@PostConstruct

  • 의존성 주입이 이루어진 후 별도의 메소드를 통해 추가로 초기화를 수행하고자 할 때 초기화 메소드에 지정하는 애노테이션이다.
  • 초기화 작업을 수행할 메소드에 이 애노테이션을 사용하면 빈 오브젝트를 생성하고 의존성 주입이 이루어진 후에 @PostConstruct 애노테이션이 지정된 메소드를 자동으로 실행해준다.
  • Spring에서 제공되는 애노테이션은 아니고, JavaEE 5나 JDK 6에 포함된 표준 애노테이션으로 스프링은 이 애노테이션이 붙은 메소드를 이용해 지정된 동작을 수행한다.

자기참조 빈 설정

  • 스프링은 프로퍼티의 ref 항목에 자기 자신을 넣는 것을 허용한다.
  • 아래와 같은 구조에서 실제 생성되는 빈은 하나지만, 마치 여러 개의 빈이 생성된 것처럼 의존성 주입이 가능하다. (생성자 주입을 통해 이를 시도하면, 순환 참조 문제로 인해 애플리케이션이 구동되지 않아 부득이하게 필드 의존성 주입 방식을 통해 의존성 주입을 하였다.)
@Service
public class XmlSqlService implements SqlService, SqlRegistry, SqlReader {
    @Autowired
    private SqlRegistry sqlRegistry;  // SqlRegistry의 구현체인 자기 자신을 주입 받음 
    
    @Autowired
    private final SqlReader sqlReader;      
    
    ...
}
  • 자기참조 빈을 만들어보는 것은, 책임과 관심사가 복잡하게 얽혀 있어서 확장이 힘들고 변경에 취약한 구조의 클래스를 유연한 구조로 만들려고 할 때 시도해볼 수 있는 방법 중 하나다.

디폴트 의존관계

  • 디폴트 의존관계란 외부에서 DI 받지 않는 경우 기본적으로 자동 적용되는 의존 관계를 말한다.
  • 특정 의존 오브젝트가 대부분의 환경에서 거의 디폴트라도 해도 좋을 만큼 기본적으로 사용될 가능성이 있다면, 디폴트 의존관계를 갖는 빈을 만드는 것을 고려해볼 수 있다.
  • DI 설정이 없을 경우 디폴트로 적용하고 싶은 의존 오브젝트를 생성자에서 넣어준다.

OXM 서비스 추상화

  • XML과 자바오브젝트를 매핑해서 상호 변환해주는 기술을 OXM(Object-XML Mapping) 이라고 하는데 OXM 기술에는 JAXB 외에도 다양한 기술이 있다. (Castor XML, JiBX, XmlBeans, Xstream)
  • OXM 프레임워크와 기술들은 모두 사용 목적이 동일하기 때문에 유사한 기능과 API를 제공한다. 따라서 로우레벨의 구체적인 기술과 API에 종속되지 않고 추상화된 레이어와 API를 제공해서 구현 기술에 대해 독립적인 코드를 작성할 수 있게 해주는 서비스 추상화를 적용해볼 수 있다.
  • 스프링에서는 OXM에 대해서 서비스 추상화 기능을 제공한다.

OXM 서비스 인터페이스

  • 스프링이 제공하는 OXM 추상화 서비스 인터페이스에는 자바오브젝트를 XML로 변환하는 Marshaller 와 반대의 변환을 수행하는 Unmarshaller 인터페이스를 제공한다.
/**
 * spring-oxm 모듈 안에 정의되어 있다.
 */
package org.springframework.oxm;

import java.io.IOException;
import javax.xml.transform.Source;

public interface Unmarshaller {
    // 해당 클래스로 언마샬링이 가능한지 확인해준다.
    boolean supports(Class<?> var1);
	
    // source를 통해 제공받은 XML을 Java Object tree로 변환해서 그 root Object를 돌려준다.
    // 매핑 실패 시 추상화된 예외(XmlMappingException)를 던진다. 서브클래스에 좀 더 세분화되어 있다.
    Object unmarshal(Source var1) throws IOException, XmlMappingException;
}

JAXB 구현 테스트

  • JAXB를 이용하는 Unmarshaller 구현체는 Jaxb2Marshaller 로, Jaxb2Marshaller 를 빈으로 등록한 다음 사용하면 된다. 여기서 Jaxb2MarshallerMarshallerUnmarshaller 를 둘 다 구현하여 Unmarshaller 의 역할도 수행할 수 있다.

  • 아래는 빈으로 등록한 Unmashaller 예제코드이다.

@Configuration
public class OxmConfig {
    @Bean
    public Jaxb2Marshaller unmarshaller() {
        Jaxb2Marshaller unmarshaller = new Jaxb2Marshaller();
        unmarshaller.setContextPath("com.corgi.example.xml");
        return unmarshaller;
    }
}
  • 아래는 테스트 코드이다.
@SpringBootTest
public class OxmTests {

    @Autowired
    private Unmarshaller unmarshaller;

    @Test
    void unmarshallSqlMap() throws IOException {
        Source xmlSource = new StreamSource(new ClassPathResource("sqlmap.xml").getInputStream());

        Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal(xmlSource);

        List<SqlType> sqlList = sqlmap.getSql();

        assertEquals(6, sqlList.size());
        assertEquals("userAdd", sqlList.get(0).getKey());
	...
    }
}
  • 다른 Object Xml Binding(OXM) 을 사용하고자 한다면 Unmarshaller 인터페이스를 구체 클래스로 확장해서 사용하면 된다.

멤버 클래스를 참조하는 통합 클래스

  • 외부에서 변경이 불가능한 멤버 클래스를 내부에 포함하는 형태로, 두 개의 클래스를 강하게 결합하고. 더 이상의 확장이나 변경이 불가능하도록 제한하는 방법을 사용할 수 있다.
  • 밖에서 볼 때는 하나의 오브젝트이지만, 내부에서는 의존 관계를 가진 두 개의 오브젝트가 깔끔하게 결합되서 사용된다.
  • 이 방법은 특정 기술을 사용하는 서비스 구조로 최적화할 수 있고, 하나의 클래스로 만들 수 있기 때문에 빈의 등록과 설정은 단순해지고 쉽게 사용할 수 있다는 장점이 있다.
@Service
public class OxmSqlService implements SqlService {
    private final OxmSqlReader oxmSqlReader = new OxmSqlReader();

    private class OxmSqlReader implements SqlReader {
        ...
    }
}

위임을 이용한 BaseSqlService의 재사용

  • 중복 코드를 가지는 두 Service class의 중복을 제거하기 위해 위임을 이용한 방법을 사용할 수 있다. (A, B class가 있다고 할 때 A class에서 B class의 instance를 내부적으로 가지고 있으면서, 외부로부터 요청이 들어올 때 B class에 위임하는 방식)
  • 위임을 위해 프록시를 활용한 방법도 생각해볼 수 있겠지만, 프록시 방식의 위임은 두 클래스를 각각 빈으로 등록해야하는 불편함이 따르고, 부가 기능의 적용을 위해 프록시처럼 많은 타깃에 적용해야될 것이 아니라면, 차라리 한 클래스로 묶어서 처리하는 방법이 유용할 수 있다.
@RequiredArgsConstructor
@Service
public class OxmSqlService implements SqlService {

    private final Unmarshaller unmarshaller;
    private final String sqlmapFile;
    private final SqlRegistry sqlRegistry;
    private final OxmSqlReader oxmSqlReader = new OxmSqlReader();

    // 중복 코드를 가지는 Service class를 내부 멤버 필드로 선언
    private final BaseSqlService baseSqlService = new BaseSqlService();

    @PostConstruct
    public void loadSql() {
        this.baseSqlService.setSqlReader(this.oxmSqlReader);
        this.baseSqlService.setSqlRegistry(this.sqlRegistry);
        this.baseSqlService.loadSql();
    }

    @Override
    public String getSql(String key) throws SqlRetrievalFailureException {
        return this.baseSqlService.getSql(key);
    }
}

리소스 추상화

  • 지금까지 만든 OxmSqlReader 나 XmlSqlReader 는 클래스 패스로부터만 리소스를 가져올 수 밖에 없다. 근데 만약 요구 사항 변경에 따라 웹상의 리소스를 가져오거나, 서버나 개발 시스템의 특정 폴더에 있는 파일을 가져오는 등 다양한 방식으로 리소스를 가져와야 한다면, 리소스에 접근할 수 있는 방법을 추상화해보는 것을 고려해볼 수 있다.
  • 자바에서는 리소스에 대해 접근할 수 있는 단일화된 인터페이스를 제공하지 않는 반면에 스프링에서는 리소스 추상화가 가능하도록 Resource 와 ResourceLoader 를 제공한다. 이를 이용하면 리소스의 위치와 접근 방법에 독립적인 코드를 쉽게 만들 수 있다. 스프링 애플리케이션에서 파일을 읽거나 참조하는 기능을 만들 때는 Resource 타입의 추상화 기능을 이용하는 것이 좋다.

Resource

  • 스프링에서는 자바에 존재하는 리소스 접근 API를 추상화해서 Resource 라는 추상화 인터페이스를 정의했다. 애플리케이션 컨텍스트가 사용할 설정정보 파일을 지정하는 것부터 시작해서 스프링의 거의 모든 API는 항상 이 Resource 추상화를 이용한다.
package org.springframework.core.io;
...

public interface Resource extends InputStreamSource {
    boolean exists();
    default boolean isReadable() { }
    default boolean isOpen() { }
}
  • 임의의 리소스를 Resource 인터페이스 타입의 오브젝트로 가져오기 위해 필요한 리소스를 빈으로 등록하는 방법은 사용해서는 안된다. 이유는 스프링에서 Resource 는 빈이 아니라 값으로 취급되기 때문이다. (애플리케이션에서 필요한 리소스를 매번 빈으로 등록할 수도 없는 노릇이므로)
  • Resource 를 사용할 때 주의할 점은 Resource 오브젝트가 실제 리소스는 아니라는 점이다. 단순히 리소스에 접근할 수 있는 추상화된 핸들러일 뿐. Resource 타입의 오브젝트가 만들어졌다고 해서 실제 리소스가 존재하지 않을 수 있다.

ResourceLoader

  • 스프링에서는 리소스의 종류와 위치를 문자열로 구성하여 Resource 오브젝트를 선언할 수 있다. 그리고 이렇게 문자열로 정의된 리소스를 실제 Resource 타입의 오브젝트로 변환해주는 ResourceLoader 인터페이스와 구현체를 제공한다.
package org.springframework.core.io;

public interface ResourceLoader {
    String CLASSPATH_URL_PREFIX = "classpath:";

    // location에 담긴 String 정보를 바탕으로 그에 적절한 Resource로 변환해준다.
    Resource getResource(String var1);

    @Nullable
    ClassLoader getClassLoader();
}
  • 리소스 로더의 대표적인 예는 Spring의 애플리케이션 컨텍스트다. 애플리케이션 컨텍스트는 ResourceLoader 인터페이스를 상속하고 있으므로 Spring의 모든 애플리케이션 컨텍스트는 리소스 로더라고 할 수 있다.
  • 스프링 컨테이너는 리소스 로더를 다양한 목적으로 사용하고 있는데, 예를 들어 애플리케이션 컨텍스트가 사용할 스프링 설정 정보가 담긴 XML 파일도 리소스 로더를 이용해 Resource 형태로 읽어온다. 이외에도 애플리케이션 컨텍스트가 외부에서 읽어오는 모든 정보는 리소스 로더를 사용하게 되어 있다. (애플리케이션 컨텍스트 자신이 리소스 로더로서 변환과 로딩 기능을 수행함)
  • 리소스 로더는 접두어를 이용해 지정한 경로의 리소스를 가져오는데, 접두어를 지정하지 않을 경우, 리소스 로더의 구현 방식에 따라 리소스를 가져오는 방식이 달라진다. 하지만 접두어를 붙여주면 리소스 로더의 종류와 상관없이 접두어가 의미하는 위치와 방법을 이용해 리소스를 읽어온다.

ResourceLoader가 처리하는 접두어

file:

  • ex. file:/C:/temp/file.txt
  • 파일 시스템의 C:/temp 폴더에 있는 file.txt 를 리소스로 만들어준다.

classpath:

  • ex. classpath:file.txt
  • 클래스패스의 루트에 존재하는 file.txt 리소스에 접근하게 해준다.

http:

  • ex. http://www.test.com/test.dat
  • HTTP 프로토콜을 사용해 접근할 수 있는 웹상의 리소스를 지정한다. (ftp: 도 사용할 수 있다.)

null

  • ex. WEB-INF/test.dat
  • 접두어가 없는 경우에는 ResourceLoader의 구현에 따라 리소스 위치가 결정된다. ServletResourceLoader 라면 서블릿 컨텍스트의 리소스를 기준으로 해석한다.

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

  • 운영 중인 시스템에서 사용하는 정보를 실시간으로 변경하는 작업을 만들 때 가장 고려해야 할 사항은 동시성 문제다.
  • HashMap 은 멀티스레드 환경에서 동시에 수정을 시도하거나, 수정과 동시에 요청하는 경우 예상하지 못한 결과가 발생할 수 있다. 멀티스레드 환경에서 안전하게 HashMap을 조작하려면 Collections.synchronizedMap() 등을 이용해 외부에서 동기화해줘야 한다. 하지만 이렇게 HashMap에 대한 전 작업을 동기화하면 DAO의 요청이 많은 고성능 서비스에서는 성능에 문제가 생길 수 있다.
  • 그래서 동기화된 해시 데이터 조작에 최적화되도록 만들어진 ConcurrentHashMap 을 사용하는 방법이 일반적으로 권장된다. ConcurrentHashMap 은 데이터 조작 시 전체 데이터에 대해 락을 걸지 않고, 조회는 락을 아예 사용하지 않는다. 그래서 어느정도 안전하면서 성능이 보장되는 동기화된 HashMap 으로 사용하기에 적당하다.

내장형 DB를 이용한 SQL 레지스트리 만들기

  • 내장형 DB(embedded database)는 애플리케이션에 내장되서 애플리케이션과 함께 시작되고 종료되는 DB를 말한다. 내장형 DB를 사용하면 애플리케이션 외부에 별도로 설치하고 셋업하는 번거로움을 없앨 수 있다.
  • 데이터는 메모리에 저장되기 때문에 IO로 인해 발생하는 부하가 적어서 성능이 뛰어나다.
  • 동시에 Map과 같은 컬렉션이나 오브젝트를 이용해 메모리에 데이터를 저장해두는 방법에 비해 매우 효과적이고 안정적인 방법으로 등록, 수정, 검색이 가능하다.(SQL을 이용해 데이터를 다룰 수 있으므로) 또한 최적화된 락킹, 격리수준, 트랜잭션을 적용할 수도 있다.

스프링의 내장형 DB 지원 기능

  • 자바에서 많이 사용되는 내장형 DB는 Derby, HSQL, H2 가 있다.
  • 내장형 DB는 애플리케이션 내에서 DB를 기동시키고 초기화 SQL 스크립트 등을 실행시키는 등의 초기화 작업이 별도로 필요하다. 이후에는 일반 DB와 마찬가지로 내장형 DB용 JDBC 드라이버를 이용해 DB에 접속, 사용할 수 있다.
  • 스프링은 내장형 DB를 손쉽게 이용할 수 있도록 내장형 DB 빌더(내장형 DB의 초기화 작업을 지원함)를 제공한다. JDBC 접속 URL을 통해 연결을 시도하면 JDBC 드라이버 내에서 내장형 DB 인스턴스를 생성해준다.
  • 또한 데이터 초기화를 위해 테이블 등을 생성하거나 초기 데이터를 삽입하는 SQL을 실행해주기도 한다. 모든 준비가 끝나면 내장형 DB에 대한 DataSource 오브젝트를 돌려준다.

내장형 DB 빌더 학습 테스트

  • 내장형 DB는 애플리케이션을 통해 DB가 시작될 때마다 매번 테이블을 새롭게 생성한다. 따라서 지속적으로 사용 가능한 테이블 생성 SQL 스크립트를 준비해야 한다. (테이블 생성, 데이터 Insert 쿼리 등을 담은 sql 확장자 파일)
  • 스프링이 제공하는 내장형 DB 빌더는 EmbeddedDatabaseBuilder 로, DB 엔진을 생성하고 초기화 스크립트를 실행해서 테이블과 초기 데이터를 준비한 뒤에 DB에 접근할 수 있는 Connection을 생성해주는 DataSource 오브젝트를 돌려주게 된다. (정확히는 DB 셧다운 기능을 가진 EmbeddedDatabase를 말한다.)
private EmbeddedDatabase database;
private NamedParameterJdbcTemplate template;    // JdbcTemplate을 더 편리하게 사용할 수 있게 확장한 템플릿(SimpleJdbcTemplate은 deprecated 되어 NamedParameterJdbcTemplate 사용)

@BeforeEach
void setUp() {
    // 내장형 DB 초기화
    database = new EmbeddedDatabaseBuilder()
          .setType(EmbeddedDatabaseType.HSQL)         // 내장형 DB 종류
          .addScript("classpath:sql/schema.sql")      // 초기화에 사용할 DB 스크립트 경로 지정(여러 개 지정 가능)
          .addScript("classpath:sql/data.sql")
          .build();

    template = new NamedParameterJdbcTemplate(database);
}

테스트 코드의 재사용

  • 테스트 코드의 내용이 크게 차이가 나지 않는다면 기존에 작성한 테스트 코드를 재사용해볼 수 있다. 방법은 이미 작성해놓은 테스트 클래스를 상속하는 것이다.
  • 공통 테스트 코드를 별도의 추상 클래스로 만들고, 이 클래스를 상속하면 된다. 그렇게 하면 추상 클래스를 상속한 클래스에서는 @Test 메소드를 모두 상속 받아서 자신의 테스트로 활용하게 된다.

애노테이션의 메타 정보 활용

  • 애노테이션은 코드의 동작에 직접 영향을 주지는 못하지만 메타정보로서 활용되는 데는 XML에 비해 유리한 점이 많다. 애노테이션을 부여하면, 부여한 클래스의 패키지, 클래스 이름, 접근 제한자, 상속한 클래스나 구현 인터페이스가 무엇인지 알 수 있으므로 애노테이션 하나를 단순히 자바 코드에 넣는 것만으로도 애노테이션을 참고하는 코드에서는 다양한 부가 정보를 얻어낼 수 있다는 장점이 있다.

  • 반면에 동일한 정보를 XML로 표현하려면 모든 내용을 명시적으로 나타내야 한다. 간단히 클래스가 무엇인지만 지정하려고해도 다음과 같이 작성해야 한다. 즉, 애노테이션 하나를 자바 코드에 넣는 것에 비해 작성할 정보의 양이 많다. 또한, 텍스트 정보이기 때문에 오타가 발생하기 쉽다.

<x:special target="type" class="com.mycompany.myproject.MyClass"/>
  • 리팩토링에서도 많은 차이점이 드러난다. 만약 리팩토링 중에 클래스의 이름을 변경하거나, 패키지를 변경하면 XML로 설정한 정보는 텍스트로 구성되어 있어서 직접 하나하나 수정해줘야 한다.

  • 자바 개발의 흐름은 점차 XML 같은 텍스트 형태의 메타정보 활용을 자바 코드에 내장된 애노테이션으로 대체하는 쪽으로 가고 있다.

  • 프레임워크의 발전과 함께 자바 코드와 프레임워크, XML 메타 정보의 형태로 진행되다가 스프링 3.1에 이르러서는 핵심 로직을 담은 자바 코드와 DI 프레임워크, 그리고 DI를 위한 메타데이터로서의 자바 코드로 재구성되고 있다.

@ImportResource

  • 자바 클래스로 만들어진 DI 설정 정보에서 XML의 설정 정보를 가져오게 만들 수 있다.
@Configuration
@ImportResource("/test-applicationContext.xml")
public class TestApplicationContext {
	...
}

<context:annotation-config />

  • ApplicationContext 안에 이미 등록된 bean들의 Annotation을 활성화 하기 위해 사용된다.
  • 이미 스프링 컨텍스트에 의해 생성되어 저장된 bean들에 대해서 @Autowired와 @Qualifier Annotation을 인식할 수 있다.
  • component-scan 또한 같은 일을 할 수 있는데, context:annotation-config는 bean을 등록하는 작업을 하지 않는다.
  • 이 설정에 의해 등록되는 빈 후처리기는 @PostConstruct 와 같은 표준 애노테이션을 인식해서 자동으로 메소드를 실행해준다. XML에 담긴 DI 정보를 이용하는 스프링 컨테이너를 사용하는 경우에는 @PostConstruct 와 같은 애노테이션의 기능이 필요하면 반드시 <context:annotation-config /> 를 포함시켜서 필요한 빈 후처리기가 등록되게 만들어야 한다.
  • 반면에 TestApplicationContext 처럼 @Configuration 이 붙은 설정 클래스를 사용하는 컨테이너가 사용되면 더 이상 <context:annotation-config /> 를 넣을 필요가 없다. 컨테이너가 직접 @PostConstruct 애노테이션을 처리하는 빈 후처리기를 등록해주기 때문이다.

@Resource

  • @Autowired 와 유사하게 필드에 빈을 주입할 때 사용한다.
  • 차이점은 @Autowired 는 필드의 타입을 기준으로 빈을 찾고, @Resource 는 필드의 이름을 기준으로 찾는다는 점이다.
@Resource
private Database embeddedDatabase;

@Bean
public SqlRegistry sqlRegistry() {
    EmbeddedDbSqlRegistry sqlRegistry = new EmbeddedDbSqlRegistry();
    sqlRegistry.setDataSource(this.embeddedDatabase);
    return sqlRegistry;
}

@EnableTransactionManagement

  • <tx:annotation-driven /> 을 대체하는 annotation 이다.
  • Spring 3.1은 XML에서 자주 사용되는 전용 태그를 @Enable 로 시작하는 애노테이션으로 대체할 수 있게 다양한 애노테이션을 제공한다.

빈 스캐닝과 자동와이어링

  • 스프링은 @Autowired 가 붙은 수정자 메소드가 있으면 파라미터 타입을 보고 주입 가능한 타입의 빈을 모두 찾는다. 주입 가능한 타입의 빈이 하나라면 스프링이 수정자 메소드를 호출해서 넣어준다. 만약 두 개 이상이 나오면, 그중에서 프로퍼티와 동일한 이름의 빈이 있는지 찾는다. 만약 타입과 이름을 모두 비교해도 최종 후보를 찾아내지 못하면 주입할 빈을 찾을 수 없다는 에러가 날 것이다.
  • @Autowired 와 같은 자동와이어링은 적절히 사용하면 DI 관련 코드를 대폭 줄일 수 있어서 편리하다. 반면에 빈 설정 정보를 보고 다른 빈과 의존관계가 어떻게 맺어져 있는지 한눈에 파악하기 힘들다는 단점도 있긴 하다.

@Component를 이용한 자동 빈 등록

  • @Component 는 클래스에 부여되는 어노테이션으로 @Component 가 붙은 클래스는 빈 스캐너를 통해 자동으로 빈으로 등록된다.
  • @Component 애노테이션이 달린 클래스를 자동으로 찾아서 빈을 등록해주게 하려면 빈 스캔 기능을 사용하겠다는 @ComponentScan 애노테이션을 사용해줘야 된다. 프로젝트 내의 모든 클래스패스를 다 뒤져서 @Component 애노테이션이 달린 클래스를 찾는 것은 부담이 많이 가는 작업이므로 특정 패키지 아래에서만 찾도록 기준이 되는 패키지(basePackages)를 지정해줄 수 있다.
@ComponentScan(basePackages="springbook.user")
public class TestApplicationContext {
	...
}

메타 애노테이션

  • 여러 개의 애노테이션에 공통적인 속성을 부여하려면 메타 애노테이션을 이용한다. 메타 애노테이션은 애노테이션 정의에 부여된 애노테이션을 말한다. 애노테이션이 빈 스캔을 통해 자동등록 대상으로 인식되게 하려면 애노테이션 정의에 @Component 를 메타 애노테이션으로 붙여주면 된다. (애노테이션은 상속, 구현이 불가능하므로 메타 애노테이션을 이용)

  • 예를 들어 SNS 서비스에 접속하는 기능을 제공하는 빈을 AOP 포인트컷으로 지정할 수 있도록 구분이 필요하다면 @SnsConnector 라는 애노테이션을 하나 만들어 사용할 수 있을 것이다. @SnsConnector 애노테이션을 정의할 때 메타 애노테이션으로 @Component 를 부여해주면 클래스마다 @Component 를 따로 붙여주지 않아도 자동 빈 등록 대상으로 만들 수 있다.

  • 본격적으로 엔터프라이즈 애플리케이션을 개발하게 되면 수십 개에서 수천 개의 애플리케이션 빈을 만들 수 있다. 이런 경우에 모든 빈을 XML이나 자바 코드에 일일이 설정해주려면 번거롭다. 대신 자동와이어링과 자동등록 방식을 적용하면 DI와 관련된 코드나 설정정보가 간결해질 것이다.

@Import

  • 자바 클래스로 된 설정정보를 가져올 때는 @ImportResource 대신 @Import 를 이용한다.
@Configuration
@Import(SqlServiceContext.class)
public class AppContext {
	...
}

프로파일(Profile)

  • 스프링 3.1은 환경에 따라서 빈 설정정보가 달라져야하는 경우에 파일을 여러 개로 쪼개고 조합하는 등의 번거로운 방법 대신 간단히 설정정보를 구성할 수 있는 방법을 제공한다. 실행 환경에 따라 빈 구성이 달라지는 내용을 프로파일로 정의해서 만들어두고, 실행 시점에 어떤 프로파일의 빈 설정을 사용할지 지정하는 것이다.
  • 프로파일은 간단한 이름과 빈 설정으로 구성된다. 프로파일을 적용하면 하나의 설정 클래스만 가지고 환경에 따라 다른 빈 설정 조합을 만들어낼 수 있다. 프로파일은 설정 클래스 단위로 지정한다. 아래와 같이 @Profile 애노테이션을 클래스 레벨에 부여하고 프로파일 이름만 넣어주면 된다.
  • 스프링 3.1은 프로파일이 지정되어 있지 않은 빈 설정은 default 프로파일로 취급한다. 이름 그대로 디폴트 빈 설정정보로 취급되어 항상 적용된다.
  • 프로파일을 적용하면 모든 설정 클래스를 부담없이 메인 설정 클래스에서 @Import 해도 된다는 장점이 있다.
    @Profile 이 붙은 설정 클래스는 @Import 로 가져오든 @ContextConfiguration 에 직접 명시하든 상관없이 현재 컨테이너의 활성 프로파일 목록에 자신의 프로파일 이름이 들어있지 않으면 무시된다. 활성 프로파일이란 스프링 컨테이너를 실행할 때 추가로 지정해주는 속성을 말한다.
  • 만약 테스트 코드가 실행될 때 @ActiveProfiles("test") 과 같이 테스트 클래스에 선언해주면 test 프로파일을 활성 프로파일로 사용하게 된다.

스프링 컨테이너에 등록된 빈 정보 확인

  • 스프링 컨테이너는 모두 BeanFactory 인터페이스를 구현하고 있다. 이 BeanFactory 구현 클래스 중에 DefaultListableBeanFactory 가 있는데 거의 대부분의 스프링 컨테이너는 이 클래스를 이용해 빈을 등록하고 관리한다. 스프링은 이 오브젝트를 @Autowired 로 주입받아서 이용하게 해준다.
  • DefaultListableBeanFactory 에는 getBeanDefinitionNames() 메소드가 있어서 컨테이너에 등록된 모든 빈 이름을 가져올 수 있고, 빈 이름을 이용해서 실제 빈과 빈 클래스 정보 등도 조회해볼 수 있다.
@Autowired
DefaultListableBeanFactory bf;

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

중첩 클래스를 이용한 프로파일 적용

  • 중첩 클래스를 이용해 여러 프로파일 클래스를 하나의 클래스로 통합할 수 있다.
  • 분리되어 있던 클래스들을 내부 static class로 선언하고 @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 {
        ...
    }
}

프로퍼티 소스

  • 외부 서비스 연결에 필요한 정보는 손쉽게 편집할 수 있으면서 빌드 작업이 따로 필요 없는 XML이나 프로퍼티 파일 같이 별도의 텍스트 파일에 저장해두는 편이 낫다.
  • 프로퍼티 파일의 확장자는 보통 .properties 이고, 내부에 key=value 형태로 프로퍼티를 정의한다.
  • 최근에는 yaml 을 이용하는 경우도 많다.
# database.properties
db.driverClass=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost/springbook?characterEncoding=UTF-8
db.username=spring
db.password=book
  • 스프링 3.1은 빈 설정 작업에 필요한 프로퍼티 정보를 컨테이너가 관리하고 제공해준다. 스프링 컨테이너가 지정된 정보 소스로부터 프로퍼티 값을 수집하고, 이를 빈 설정 작업 중에 사용할 수 있게 해준다.
  • 컨테이너가 프로퍼티 값을 가져오는 대상을 프로퍼티 소스(property source) 라고 한다. 환경 변수나 시스템 프로퍼티처럼 디폴트로 프로퍼티 정보를 끌어오는 프로퍼티 소스도 있고, 프로퍼티 파일이나 리소스의 위치를 지정해서 사용하는 프로퍼티 소스도 있다.
  • DB 연결 정보는 특정 파일에서 프로퍼티 값을 가져와야 하므로 프로퍼티 소스를 등록해줘야 한다. 프로퍼티 소스 등록에는 @PropertySource 애노테이션을 사용한다. 컨테이너가 프로퍼티 소스로 database.properties 파일의 내용을 사용하도록 @PropertySource 애노테이션을 넣어주면 된다.
@Configuration
...
@PropertySource("/database.properties")
public class AppContext {
	...
}
  • @PropertySource 로 등록한 리소스로부터 가져오는 프로퍼티 값은 컨테이너가 관리하는 Environment 타입의 환경 오브젝트에 저장된다. 환경 오브젝트는 빈처럼 @Autowired 를 통해 필드로 주입받을 수 있다. 주입받은 Environment 오브젝트의 getProperty() 를 이용하면 프로퍼티 값을 가져올 수 있다.
@Autowired
private 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

  • 앞에서는 Environment 오브젝트를 DI 받아서 프로퍼티 값을 사용했지만, @Value 어노테이션을 이용해 프로퍼티 값을 직접 DI 받는 방법도 가능하다. 프로퍼티 소스로부터 값을 주입받을 수 있게 치환자(placeholder)를 사용해서 값을 가져올 수 있다.
@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;
}
  • XML에서는 프로퍼티 값에 문자열로 된 치환자를 넣어주면, 컨테이너가 프로퍼티 파일 등에서 가져온 실제 값으로 바꿔치기하게 만들 수 있다.
<property name="driverClass" value="${db.driverClass}" />
  • XML에서는 치환자 자리의 값을 바꿔주는데, @Value 에서는 @Value 가 붙은 필드의 값을 주입해주는 방식으로 동작한다.
  • @Value 와 치환자를 이용해 프로퍼티 값을 필드에 주입하려면 프로퍼티 소스로부터 가져온 값을 @Value 필드에 주입하는 기능을 제공해주는 PropertySourcesPlaceholderConfigurer 를 빈으로 선언해줘야 한다.
  • 빈 팩토리 후처리기로 사용되는 빈을 정의해주는 것인데 이 빈 설정 메소드는 반드시 static 메소드로 선언해줘야 한다.
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
    return new PropertySourcesPlaceholderConfigurer();
}

빈 설정 클래스의 재사용과 @Enable*

  • @Configuration 애노테이션이 달린, 빈 설정으로 사용되는 AppContext 같은 클래스도 스프링에서는 하나의 빈으로 취급된다. 그래서 @Autowired 로 설정 클래스를 DI 받을 수 있다. (아래와 같이 @Configuration 애노테이션은 @Component 애노테이션을 메타 애노테이션으로 가지고 있어 빈으로 등록된다.)
@Component
public @interface Configuration {
	...
}
  • 스프링은 모듈화된 빈 설정을 가져올 때 사용하는 @Import 를 다른 애노테이션으로 대체할 수 있는 방법을 제공한다. 바로 Custom 애노테이션을 만들고 @Import 를 메타 애노테이션으로 넣어 사용하는 방법이다.
@Import(SqlServiceContext.class)
public @interface EnableSqlService {
	...
}
  • 이 애노테이션을 사용하면 SqlServiceContext 설정 클래스를 @Import 하는 것과 같은 효과를 제공한다.
  • 참고로, @EnableXXX 의 형식으로 만든 이 커스텀 애노테이션은 SqlService를 사용하겠다는 의미로 해석하면 된다.
profile
Mechanical & Computer Science

0개의 댓글