ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스프링 5 레시피] 2장: 스프링 코어 (2-1 ~ 2-6)
    Spring 2021. 2. 26. 13:16
    반응형

    스프링5 레시피

     

    개요

     

    이 장의 주제는 스프링의 주요 기능입니다. IoC는 스프링 프레임워크의 심장부라고 할 수 있는데요.

     

    IoC가 무엇인지 간단히 살펴보겠습니다.

     

    IoC 란?
    Inversion of Control의 약자로 해석하면 제어의 역전이라는 뜻인데요,

    스프링을 사용하기 전에는 객체를 생성하고 객체 간의 의존 관계를 연결하는 등 제어권이 개발자에게 있었습니다. 

    무슨 의미인지 간단한 코드를 통해 알아보겠습니다.

     

    public class A {
    
        private B b;
        
        public A() {
        	this.b = new B(); // 자기가 사용할 의존성을 직접 만들어서 사용
        }
    }

     

    A 객체는 B객체에게 의존하고 있는데요, 개발자는 A 객체를 생성할 때 B 객체를 함께 생성해서 의존하고 있는 객체를 직접 주입하고 있습니다.

     

    다음은 스프링 프레임워크를 사용했을 때의 코드입니다.

     

    @Component
    public A {
    	
        private B b;
        
        public A(B b) {
        	this.b = b; // 누군가 B 객체를 생성하여 주입해줌. 제어권이 역전됨.
        }
        
    }

     

    A 객체가 의존하고있는 B 객체를 직접 생성하여 주입해주지 않고, 밖에서 누군가가 B 객체를 생성하고 A 객체가 생성될 때 생성자 parameter로 넘겨주어 의존 객체를 주입해주고 있습니다.

     

    이처럼 '객체의 라이프 사이클과 객체들의 의존성을 관리해주는 제어권이 넘어갔다'라는게 IoC의 개념입니다.

     

    그리고 해당 제어권을 가지고 있는 주체를 IoC 컨테이너 또는 스프링 컨테이너라고 말합니다.

     

     

     

    2-1. 자바로 빈 구성하기

     

    @Configuration과 @Bean을 붙인 자바 구성 클래스를 만들거나 @Component 등 스테레오 타입 애너테이션을 붙인 자바 컴포넌트를 구성면 됩니다. IoC 컨테이너는 이렇게 애너테이션을 붙인 자바 클래스를 스캐닝해서 빈을 구성합니다.

     

    빈을 구성하기 위해 간단한 시퀀스 생성기 클래스 설계하였는데요,

    @Setter
    public class SequenceGenerator {
    
        private String prefix; // 접두어
        private String suffix; // 접미어
        private int initial; // 초깃값
        private final AtomicInteger counter = new AtomicInteger(); // 시퀀스 숫자
    
        public String getSequence() {
            StringBuilder builder = new StringBuilder();
            builder.append(prefix)
                    .append(initial)
                    .append(counter.getAndIncrement())
                    .append(suffix);
    
            return builder.toString();
        }
    }
    

     

    이 클래스를 빈으로 등록 해주기 위해 @Configuration으로 자바 구성 클래스를 정의하였구요, @Bean을 붙여 SequenceGenerator 인스턴스를 빈으로 등록해주었습니다.

    @Configuration // 설정 클래스로 선언
    public class SequenceGeneratorConfiguration {
    
        @Bean // 빈으로 등록
        public SequenceGenerator sequenceGenerator() {
            SequenceGenerator sequenceGenerator = new SequenceGenerator();
            sequenceGenerator.setPrefix("30");
            sequenceGenerator.setSuffix("A");
            sequenceGenerator.setInitial(100000);
    
            return sequenceGenerator;
        }
    }

     

    * 실제 적용 사례 *

    springframework의 RestTemplate 같은 외부에서 제공해주는 라이브러리는 @Component 같은 스테레오 타입 애너테이션을 붙일 수 없기 때문에, @Bean으로 객체의 옵션을 커스텀해서 빈으로 등록할 수 있습니다.
        @Bean
        public RestTemplate restTemplate(RestTemplateBuilder builder) {
            return builder
                    .requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()))
                    .setReadTimeout(ofSeconds(5))
                    .setConnectTimeout(ofSeconds(5))
                    .additionalMessageConverters(new StringHttpMessageConverter(StandardCharsets.UTF_8))
                    .additionalInterceptors(new RestTemplateLoggingInterceptor())
                    .build();
        }

     

    스프링은 이처럼 @Configuration이 붙은 클래스를 보면 그 안에 정의된 @Bean이 달린 메서드를 찾아 빈으로 등록합니다. 이 때 빈의 이름은 메서드 명을 따라가게 되고, @Bean(name = "빈의 이름 정의")을 사용해 빈의 이름을 변경할 수 있습니다.

     

    IoC 컨테이너를 인스턴스화 해야 빈들을 스캐닝하고 빈 인스턴스를 가져올 수 있는데요, 스프링은 BeanFactory와 ApplicationContext라는 두 가지 인터페이스를 제공합니다. 일반적으로 BeanFactory를 확장하는 ApplicationContext를 사용하는 게 좋습니다.

     

    애노테이션을 이용해 빈 설정을 하였기 때문에 위에서 정의한 자바 구성 클래스를 스캔 대상으로 포함하여  AnnotationConfigApplicationContext를 인스턴스화 해서 SequenceGenerator 빈을 가져와 보겠습니다.

     

    ApplicationContext의 getBean() 메서드로 빈을 조회할 수 있는데요, 

    public class Main {
    
        public static void main(String[] args) {
            // IoC 컨테이너를 생성할 때 SequenceGeneratorConfiguration 설정 파일을 스캔
            ApplicationContext context = new AnnotationConfigApplicationContext(SequenceGeneratorConfiguration.class);
    
            // 첫 번째는 빈의 이름으로 조회하는데 타입을 특정할 수 없기 때문에 명시적으로 캐스팅을 합니다.
            SequenceGenerator generator1 = (SequenceGenerator) context.getBean("sequenceGenerator");
            // 두 번째는 빈의 이름과 타입으로 조회하기 때문에 캐스팅하지 않아도 됩니다.
            SequenceGenerator generator2 = context.getBean("sequenceGenerator", SequenceGenerator.class);
            // 세 번째는 타입으로만 조회하는데 해당 타입의 빈이 한 개만 있다면 빈의 이름을 생략할 수 있습니다.
            SequenceGenerator generator3 = context.getBean(SequenceGenerator.class);
    
            System.out.println("generator1 == generator2 = " + (generator1 == generator2)); // true
            System.out.println("generator1 == generator2 = " + (generator2 == generator3)); // true
            System.out.println("generator1 == generator2 = " + (generator3 == generator1)); // true
        }
    
    }
    

    뒤에서 설명 하겠지만 빈은 기본 스코프가 singleton이기 때문에 빈 1, 2, 3은 모두 같은 인스턴스라는 것을 동일성 비교를 했을 때 모두 true 인 것으로 확인할 수 있습니다.

     

     

    다음으로 @Component를 붙여 DAO 빈을 생성해볼 텐데요, 그에 앞서서 테스트에 필요한 Domain 클래와 DAO 클래스를 정의해 보겠습니다.

     

    Sequence 도메인 클래스

    @Getter
    @RequiredArgsConstructor
    public class Sequence {
        private final String id;
        private final String prefix;
        private final String suffix;
    }

     

    DAO 클래스

    @Component("sequenceDao")
    public class SequenceDao {
    
        // DB를 구성하는 대신 Map 으로 대체합니다.
        private final Map<String, Sequence> sequences = new HashMap<>();
        private final Map<String, AtomicInteger> values = new HashMap<>();
    
        public SequenceDao() {
        	// DAO 생성하며 테스트 값 같이 생성
            sequences.put("IT", new Sequence("IT", "30", "A"));
            values.put("IT", new AtomicInteger(10000));
        }
    
        // ID로 Sequnce 조회
        public Sequence getSequence(String sequenceId) {
            return sequences.get(sequenceId);
        }
    
        // ID로 스퀀스의 다음 값 조회
        public int getNextValue(String sequenceId) {
            AtomicInteger value = values.get(sequenceId);
            return value.getAndIncrement();
        }
    
    }

    클래스에 @Component를 붙이면 스프링은 해당 클래스를 스캔해 빈으로 생성하는데요, 괄호 안의 값은 빈의 이름을 정의하는데, 생략할 경우 소문자로 시작하는 클래스명(sequenceDao)을 빈의 이름으로 할당합니다.

     

    사실 @Component는 범용적으로 사용하지만, Dao와 같이 DB와 밀접해서 데이터 처리를 담당하는 Persistence Layer에는 @Repository를 사용하고 비즈니스 로직을 처리하는 Service Layer에는 @Service을, MVC를 담당하는 Presentation Layer에는 @Controller를 사용합니다.

     

    애노테이션마다 확장된 기능이 있지만 공통적으로는 모두 내부적으로 @Component를 사용하고 있습니다.

     

    @Respository

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Component
    public @interface Repository {
    
    	/**
    	 * The value may indicate a suggestion for a logical component name,
    	 * to be turned into a Spring bean in case of an autodetected component.
    	 * @return the suggested component name, if any (or empty String otherwise)
    	 */
    	@AliasFor(annotation = Component.class)
    	String value() default "";
    
    }

     

    기본적으로 스프링은 앞에서 배운 애너테이션을 모두 스캐닝하는데요, @ComponentScan의 다양한 속성을 적용하여 스캐닝 과정을 커스터마이징 할 수 있습니다.

     

    - basePackages

    basePackages는 스프링이 스캔할 대상 패키지를 설정합니다. @ComponentScan의 기본 속성이고 해당 패키지를 포함하여 하위 패키지까지 모두 스캔하는데요, basePackages만 사용한다면 다음과 같이 생략 가능합니다.

     

    @Configuration
    @ComponentScan(basePackages = "com.mins")
    public class SequenceGeneratorConfiguration {
    	...
    }
    @Configuration
    @ComponentScan("com.mins")
    public class SequenceGeneratorConfiguration {
    	...
    }

     

     

    - useDefaultFilters

    useDefaultFilters의 기본값이 true인데요, 유효한 패키지 경로에서 @Bean, @Component와 같은 스테레오 타입 애노테이션이 붙은 클래스를 빈으로 등록하는 속성입니다.

     

    해당 속성을 false로 바꾸면 어떻게 될까요?

    @Configuration
    @ComponentScan(basePackages = "com.mins", useDefaultFilters = false)
    public class SequenceGeneratorConfiguration {
    	...
    }
    public class Main {
    
        public static void main(String[] args) {
            ApplicationContext context = new AnnotationConfigApplicationContext(SequenceGeneratorConfiguration.class);
    
    	// NoSuchBeanDefinitionException 발생
    	// No qualifying bean of type 'com.mins.springrecipes.sequence.SequenceDao' available
            SequenceDao dao = context.getBean(SequenceDao.class);
    
            System.out.println("generator = " + dao.getNextValue("IT"));
        }
    
    }

    IoC 컨테이너에서 SequenceDao 빈을 꺼낼 때 빈을 찾을 수 없다는 예외가 발생합니다.

     

     

    - includeFilters, excludFilters

    다시 SequenceDao를 불러오고 싶다면 includeFilters 설정을 사용하면 되는데요, 특정 조건을 만족하는 클래스만 스캔하기 때문에 자바 패키지가 수십, 수백 개에 달할 때도 유용하게 사용할 수 있습니다.

     

    스프링이 지원하는 필터는 총 네 종류인데요, 하나씩 알아보도록 하겠습니다.

     

     

    FilterType.REGEX : 정규표현식으로 매치

    @Configuration
    @ComponentScan(
            basePackages = "com.mins", 
            useDefaultFilters = false, 
            includeFilters = {
                    @ComponentScan.Filter(
                            type = FilterType.REGEX,
                            pattern = {".*sequence.*Dao"})
            }
    )
    public class SequenceGeneratorConfiguration {
    	...
    }

    FilterType.REGEX 필터를 사용해서 정규표현식으로 sequence 패키지 아래에 이름이 Dao로 끝나는 클래스를 스캐닝 대상에 포함시켰습니다.

     

    FilterType.ASSIGNABLE_TYPE : 클래스/인터페이스 타입으로 매치

    @Configuration
    @ComponentScan(
            basePackages = "com.mins", 
            useDefaultFilters = false, 
            includeFilters = {
                    @ComponentScan.Filter(
                            type = FilterType.ASSIGNABLE_TYPE,
                            classes = {SequenceDao.class})
            }
    )
    public class SequenceGeneratorConfiguration {
    	...
    }

    FilterType.ASSIGNABLE_TYPE 필터를 사용해서 SequenceDao 타입 클래스를 스캐닝 대상에 포함시켰습니다.

     

    FilterType.ANNOTATION : 애노테이션으로 매치

    @Configuration
    @ComponentScan(
            basePackages = "com.mins", 
            useDefaultFilters = false, 
            includeFilters = {
                    @ComponentScan.Filter(
                            type = FilterType.ANNOTATION,
                            classes = org.springframework.stereotype.Repository.class)
            }
    )
    public class SequenceGeneratorConfiguration {
    	...
    }

    FilterType.ANNOTATION 필터를 사용해서 Respository 애너테이션을 사용하는 클래스를 스캐닝 대상에 포함시켰습니다.

     

    FilterType.ASPECTJ : AspectJ 포인트 컷 표현식으로 매치

    @Configuration
    @ComponentScan(
            basePackages = "com.mins", 
            useDefaultFilters = false, 
            includeFilters = {
                    @ComponentScan.Filter(
                            type = FilterType.ASPECTJ,
                            pattern = "com.mins.springrecipes.sequence.*Dao")
            }
    )
    public class SequenceGeneratorConfiguration {
    	...
    }

    FilterType.ASPECTJ 필터를 사용해서 sequence 패키지 아래에 이름이 Dao로 끝나는 클래스를 스캐닝 대상에 포함시켰습니다.

     

     

    includeFilters를 사용하게 되면 useDefaultFilters를 false로 지정했지만 SequenceDao 클래스는 스캐닝 대상이 되므로 빈으로 등록되는 것을 확인할 수 있습니다.

    public class Main {
    
        public static void main(String[] args) {
            ApplicationContext context = new AnnotationConfigApplicationContext(SequenceGeneratorConfiguration.class);
    
            SequenceDao dao = context.getBean(SequenceDao.class);
    
            System.out.println("generator = " + dao.getNextValue("IT"));
        }
    
    }
    
    
    결과 ==> generator = 10000

     

    * 주의사항
    includeFIlters에 포함된 클래스는 스테레오 타입 애너테이션이 달려있지 않아도 빈으로 등록됩니다.

     

     

    2-2. 생성자 호출해서 빈 생성하기

     

    우리는 앞에서 이미 SequenceGenerator의 생성자를 호출해서 빈으로 등록해보았기 때문에 간단히 살펴보겠습니다.

     

    온라인 쇼핑몰 애플리케이션을 개발한다고 가정할 때, 쇼핑몰은 다양한 상품을 취급하므로 Product 추상 클래스를 만들어서 여러 하위 클래스가 상속하는 구조로 설계합니다.

     

    Product 추상 클래스

    @Getter
    @Setter
    @ToString
    public abstract class Product {
    
        private String name;
        private double price;
    
        public Product() {
        }
    
        public Product(String name, double price) {
            this.name = name;
            this.price = price;
        }
    
    }

     

    Battery 클래스

    @Getter
    @Setter
    public class Battery extends Product {
    
        private boolean rechargeable;
    
        public Battery() {
            super();
        }
    
        public Battery(String name, double price) {
            super(name, price);
        }
    
    }

     

    Disc 클래스

    @Getter
    @Setter
    public class Disc extends Product{
    
        private int capacity;
    
        public Disc() {
            super();
        }
    
        public Disc(String name, double price) {
            super(name, price);
        }
    
    }

     

    Shop 자바 설정 파일

    @Configuration
    public class ShopConfiguration {
    
        @Bean
        public Product aaa() {
            Battery battery = new Battery("AAA", 2.5);
            battery.setRechargeable(true);
            return battery;
        }
    
        @Bean
        public Product cdrw() {
            Disc disc = new Disc("CD-RW", 1.5);
            disc.setCapacity(700);
            return disc;
        }
    
    }

     

    Battery와 Disc 클래스의 생성자를 호출해서 빈을 생성해주었는데요,

     

    Main 클래스에서 둘이 잘 등록되었는지 확인해보겠습니다.

    public class Main {
    
        public static void main(String[] args) {
            ApplicationContext context = new AnnotationConfigApplicationContext(ShopConfiguration.class);
    
            Product aaa = context.getBean("aaa", Product.class);
            Product cdrw = context.getBean("cdrw", Product.class);
    
            System.out.println("aaa = " + aaa);
            System.out.println("cdrw = " + cdrw);
        }
    
    }
    
    -- 결과 --
    aaa = Product(name=AAA, price=2.5)
    cdrw = Product(name=CD-RW, price=1.5)

     

     

     

    2-3. POJO 레퍼런스와 자동 연결을 이용해 다른 POJO와 상호 작용하기

     

    쉽게 말해서 DI하는 방법입니다.

     

    첫 번째 방법은 자바 구성 클래스에서 POJO 참조하기입니다.

    @Configuration
    public class SequenceGeneratorConfiguration {
    
        @Bean
        public DatePrefixGenerator datePrefixGenerator() {
            DatePrefixGenerator dpg = new DatePrefixGenerator();
            dpg.setPattern("yyyyMMdd");
            return dpg;
        }
    
        @Bean
        public SequenceGenerator sequenceGenerator() {
            SequenceGenerator sequenceGenerator = new SequenceGenerator();
            sequenceGenerator.setPrefix("30");
            sequenceGenerator.setSuffix("A");
            sequenceGenerator.setInitial(100000);
            
            // 스프링 사용 없이 표준 자바 코드를 이용하여 datePrefixGenerator 빈 레퍼런스를 참조
            sequenceGenerator.setPrefixGenerator(datePrefixGenerator());
    
            return sequenceGenerator;
        }
    
    }

     

     

    두 번째 방법은 @Autowired를 사용하는 방법입니다.

     

    @Autowired는 여러 가지 방법으로 사용할 수 있는데요, 하나씩 알아보겠습니다.

     

    - 필드 인젝션

    @Component
    public class SequenceService {
    
        @Autowired
        private SequenceDao sequenceDao;
    
    }

     

     

    - 세터 메서드 인젝션

    @Component
    public class SequenceService {
    
        private SequenceDao sequenceDao;
    
        @Autowired
        public void setSequenceDao(SequenceDao sequenceDao) {
            this.sequenceDao = sequenceDao;
        }
    
    }

    주입받고 싶은 빈을 메서드의 파라미터에 정의하고 @Autowired를 붙여주면 자동 연결이 됩니다.

     

     

    - 생성자 인젝션

    @Component
    public class SequenceService {
    
        private SequenceDao sequenceDao;
    
        @Autowired
        public SequenceService(SequenceDao sequenceDao) {
            this.sequenceDao = sequenceDao;
        }
    }

    메서드와 비슷하게 생성자 파라미터에 주입받고 싶은 빈을 정의하고 @Autowired를 붙여주면 되는데요, 스프링 4.3 버전부터 생성자가 한 개일 경우 @Autowired를 생략 가능합니다.

     

    최근에는 여러가지 장점으로 생성자 주입을 사용하고있습니다. (필드 final 사용가능, 테스트 용이 등)

     

     

    @Primary, @Qualifier

     

    @Primary와 @Qualifier는 타입이 같은 빈이 여럿일 때 사용하는 애너테이션인데요, 

     

    @Primary는 빈에 우선권을 부여합니다.

    @Primary
    @Component
    public class DatePrefixGenerator implements PrefixGenerator {
        private String pattern;
    }

    PrefixGenerator를 구현한 빈이 여러 개여도 스프링은 @Primay가 붙은 DatePrefixGenerator 빈을 우선적으로 연결합니다.

     

    @Qualifier는 빈의 이름을 명시하여 빈을 찾는데요, 같은 타입의 빈이 여러개여도 이름은 하나이기 때문에 사용할 수 있습니다.

    @Component
    public class SequenceService {
    
        @Autowired
        @Qualifier("datePrefixGenerator")
        private SequenceDao sequenceDao;
    }

     

    생성자나 메서드로 빈 주입을 받을 경우 인수 앞에 작성합니다.

    @Component
    public class SequenceService {
    
        private SequenceDao sequenceDao;
    
        @Autowired
        public SequenceService(@Qualifier("datePrefixGenerator") SequenceDao sequenceDao) {
            this.sequenceDao = sequenceDao;
        }
    }

     

     

    @Import

     

    @Import 애노테이션은 자바 구성 파일이 나뉘어 있을 때 다른 구성 클래스의 POJO를 모두 현재 구성 클래스의 스코프로 가져올 수 있는데요, 다음은 @Value 애노테이션과 스프링 표현식을 써서 가져오는 코드입니다.

     

    Prefix 구성 클래스에 정의되어있는 빈의 이름을 사용해서 SequenceGenerator 구성 클래스의 스코프로 가져와서 사용하였습니다.

    @Configuration
    @Import(PrefixConfiguration.class)
    public class SequenceGeneratorConfiguration {
    
        @Value("#{datePrefixGenerator}")
        private PrefixGenerator prefixGenerator;
    
        @Bean
        public SequenceGenerator sequenceGenerator() {
            SequenceGenerator sequenceGenerator = new SequenceGenerator();
            sequenceGenerator.setPrefix("30");
            sequenceGenerator.setSuffix("A");
            sequenceGenerator.setInitial(100000);
            sequenceGenerator.setPrefixGenerator(prefixGenerator);
    
            return sequenceGenerator;
        }
    }
    
    
    @Configuration
    public class PrefixConfiguration {
    
        @Bean
        public DatePrefixGenerator datePrefixGenerator() {
            DatePrefixGenerator dpg = new DatePrefixGenerator();
            dpg.setPattern("yyyyMMdd");
            return dpg;
        }
    
    }

     

     

    2-4. @Resource와 @Inject 를 붙여 POJO 자동 연결하기

     

    @Resource와 @Inject은 Java에서 지원한다는 점을 제외하고 @Autowired와 거의 유사한 기능을 하기 때문에 생략하도록 하겠습니다.

     

     

     

    2-5. @Scope를 붙여 POJO 스코프 지정하기

     

    @Scope는 빈의 스코프를 지정하는 애너테이션인데요, 스프링은 기본적으로 IoC 컨테이너에 빈을 단 하나만 생성합니다. 그렇기때문에 어디서 빈을 호출하든지 그 빈은 모두 하나의 인스턴스를 가리키는데, 이 스코프가 모든 빈의 기본 스코프인 singleton입니다.

     

     

    singleton (default)

    - IoC 컨테이너당 빈 인스턴스를 하나 생성합니다.

     

    prototype

    - 요청할 때마다 빈 인스턴스를 생성합니다.

     

    request

    - HTTP 요청당 하나의 빈 인스턴스를 생성합니다.

     

    session

    - HTTP 세션당 하나의 빈 인스턴스를 생성합니다.

     

     

     

    다음 코드를 보면서 차이점을 알아보겠습니다.

    @Configuration
    @ComponentScan("com.mins.springrecipes.shop")
    public class ShopConfiguration {
    
        @Bean
        public Product aaa() {
            Battery battery = new Battery("AAA", 2.5);
            battery.setRechargeable(true);
            return battery;
        }
    
        @Bean
        @Scope("prototype")
        public Product cdrw() {
            Disc disc = new Disc("CD-RW", 1.5);
            disc.setCapacity(700);
            return disc;
        }
    
    }

     

    public class Main {
    
        public static void main(String[] args) {
            ApplicationContext context = new AnnotationConfigApplicationContext(ShopConfiguration.class);
    
            Product aaa1 = context.getBean("aaa", Product.class);
            Product aaa2 = context.getBean("aaa", Product.class);
    
            System.out.println("(aaa1 == aaa2) = " + (aaa1 == aaa2)); // true
    
            Product cdrw1 = context.getBean("cdrw", Product.class);
            Product cdrw2 = context.getBean("cdrw", Product.class);
    
            System.out.println("(cdrw1 == cdrw2) = " + (cdrw1 == cdrw2)); // false
        }
    
    }

     

    aaa 빈은 몇 번을 꺼내던지 같은 인스턴스를 반환하기 때문에 aaa1과 aaa2를 동일성 비교해보면 true이지만, cdrw는 빈의 스코프를 prototype으로 설정했기 때문에 빈을 꺼낼 때마다 새로운 인스턴스를 생성해서 반환합니다.

     

    즉 cdrw1과 cdrw2는 동일성 비교를 했을 때 false를 출력합니다.

     

     

     

     

     

    2-6. 외부 리소스(텍스트, XML, 프로퍼티, 이미지 파일)의 데이터 사용하기

     

    파일 시스템이나 클래스 패스, URL과 같이 외부에 있는 리소스(예: 텍스트, XML, 프로퍼티, 이미지 파일)를 각자 알맞은 API로 읽어들여야 할 때가 있는데요, 스프링이 제공하는 @PropertySource를 이용하면 .properties나 .yml 파일을 읽어들일 수 있고, Resource라는 인터페이스를 사용해서 경로만 지정하면 리소스를 로드할 수 있는 메커니즘이 마련되어 있습니다.

     

    먼저 properties 파일 데이터를 이용해서 빈의 초깃값을 설정하겠습니다.

     

    discounts.properties

    specialcustomer.discount=0.1
    summer.discount=0.15
    endofyear.discount=0.2

     

     

    @Configuration
    @ComponentScan("com.mins.springrecipes.shop")
    @PropertySource("classpath:discounts.properties")
    public class ShopConfiguration {
    
        @Getter
        @Value("${endofyear.discount:0}")
        private double specialEndofyearDiscountField;
        
    }

    Shop 자바 설정 클래스에서 @PropertySource 애너테이션을 이용해서 classpath에 있는 discounts.properties 파일을 로드하였는데요, endofyear.discounts 값을 읽어서 필드에 할당 하였습니다.

     

    public class Main {
    
        public static void main(String[] args) {
            ApplicationContext context = new AnnotationConfigApplicationContext(ShopConfiguration.class);
    
            ShopConfiguration bean = context.getBean(ShopConfiguration.class);
    
            System.out.println("bean.getSpecialEndofyearDiscountField() = " + bean.getSpecialEndofyearDiscountField());
        }
    
    }
    
    -- 결과 --
    bean.getSpecialEndofyearDiscountField() = 0.2

    필드 값을 출력해보면 0.2가 출력되는 것을 확인할 수 있습니다.

     

     

     

    다음은 POJO에서 외부 리소스 파일 데이터를 가져와서 사용하기에 대한 설명인데요, IoC 컨테이너가 로드될 때 banner.txt라는 텍스트 파일 안에 문구를 콘솔에 출력해보겠습니다.

     

    banner.txt

    ********************
    ** Spring Study **
    ********************

     

    빈으로 등록할 BannerLoader 클래스는 생성될 때 파일을 한 줄씩 읽어서 콘솔에 출력합니다.

    public class BannerLoader {
    
        @Setter
        private Resource banner;
    
        @PostConstruct
        public void showBanner() throws IOException {
            Files.lines(Paths.get(banner.getURI()), StandardCharsets.UTF_8)
                    .forEachOrdered(System.out::println);
        }
    
    }

     

    @Value 애너테이션으로 classpath의 banner.txt 파일을 읽어서 Resource 타입으로 초기화합니다.

    그리고 bannerLoader 빈 프로퍼티에 banner를 설정합니다.

    @Configuration
    @ComponentScan("com.mins.springrecipes.shop")
    @PropertySource("classpath:discounts.properties")
    public class ShopConfiguration {
    
        @Value("classpath:banner.txt")
        private Resource banner;
        
        @Bean
        public BannerLoader bannerLoader() {
            BannerLoader bannerLoader = new BannerLoader();
            bannerLoader.setBanner(banner);
            return bannerLoader;
        }
        
    }

     

    테스트 하기 위해 ApplicationContext를 생성하면 banner.txt 파일의 내용이 콘솔에 출력됨을 확인할 수 있습니다.

    public class Main {
    
        public static void main(String[] args) {
            ApplicationContext context = new AnnotationConfigApplicationContext(ShopConfiguration.class);
        }
    
    }
    
    
    -- 콘솔 --
    ******************
    ** Spring Study **
    ******************

     

     

    배너 파일은 자바 클래스패스에 있기때문에 리소스 경로는 접두어 classpath: 로 시작합니다.

    banner.txt 파일이 파일 시스템에 위치해 있다면 접두어 file:  로 시작하고, URL로도 리소스의 위치를 특정할 수 있습니다. (URL의 예 http://springrecipes.apress.com/shop/banner.txt) 

     

     

    애플리케이션을 개발할 때 외부 리소스를 사용해야할 경우도 있는데요, 예시로 애플리케이션이 종료될 때 discounts.properties 파일에 나열된 할인율을 출력한다고 가정하겠습니다.

     

     

    ClassPathResource

    public class Main {
    
        public static void main(String[] args) throws IOException {
            ApplicationContext context = new AnnotationConfigApplicationContext(ShopConfiguration.class);
    
            Resource classPathResource = new ClassPathResource("discounts.properties");
            Properties properties = PropertiesLoaderUtils.loadProperties(classPathResource);
            
            System.out.println("properties = " + properties);
    }
    
    -- 결과 --
    properties = {specialcustomer.discount=0.1, endofyear.discount=0.2, summer.discount=0.15}

    스프링이 제공하는 ClassPathResource 클래스로 discouns.properties 파일 데이터를 가져와서 Resource 객체로 캐스팅하였고, PropertiesLoaderUtils 클래스를 이용해서 Properties 객체로 바꾸었습니다.

     

    properties의 데이터가 출력되는것을 볼 수 있습니다.

     

     

    스프링은 ClassPathResource 뿐만 아니라 FileSystemResource와 UrlResource도 지원하는데요, 하나씩 살펴 보겠습니다.

     

     

    FileSystemResource

    public class Main {
    
        public static void main(String[] args) throws IOException {
            ApplicationContext context = new AnnotationConfigApplicationContext(ShopConfiguration.class);
    
            Resource fileSystemResource = new FileSystemResource("c:/shop/banner.txt");
    
            Files.lines(Paths.get(fileSystemResource.getURI()), StandardCharsets.UTF_8)
                    .forEachOrdered(System.out::println);
        }
    
    }
    
    -- 결과 --
    ******************
    ** Spring Study **
    ******************

    FileSystemResource 클래스를 사용해서 파일 시스템에 있는 리소스를 쉽게 가져와서 사용할 수 있습니다. 

     

     

    UrlResource

    public class Main {
    
        public static void main(String[] args) throws IOException {
            ApplicationContext context = new AnnotationConfigApplicationContext(ShopConfiguration.class);
    
            Resource urlResource = new UrlResource("http://localhost:9090/static/banner.txt");
    
            Scanner scanner = new Scanner(urlResource.getInputStream());
    
            while (scanner.hasNext()) {
                System.out.println(scanner.nextLine());
            }
        }
    }
    
    -- 결과 --
    ******************
    ** Spring Study **
    ******************

    UrlResource 클래스도 마찬가지로 url로 리소스를 로드해서 쉽게 사용할 수 있습니다.

    반응형
Designed by Tistory.