본문 바로가기
2023 활동 - 4학년/[1월 ~ 4월] sw 아카데미 백엔드 과정

[2023.02.28 / CNU SW 아카데미] SpringBasic Part1 D-19

by 은행장 노씨 2023. 2. 28.

Spring Framework 시작하기(3)

1. Enviroment profile

이번 시간에는 Application 속성 관리를 Spring에서 어떻게 하는지 살펴보자. 

- Application Context에서 제공하는 중요 기능 중 하나이다. 

- EnvironmentCapable 인터페이스를 제공한다. 

//EnvironmentCapable.class
package org.springframework.core.env;

public interface EnvironmentCapable {
	Environment getEnvironment();
}
  • 어플리케이션이 처한 상황과 공간을 말하는 것이다. 
  • 개발 중 : 개발환경
    테스트 중 : 테스트 환경
    운영 중 : 운영 환경
  • IoC Context에서는 무슨 요인을 주는 환경이 존재할까?
    -> Bean 에게 어떤 영향을 주는 무언가
개발 중에는 H2 DB를 사용하도록 Bean이 등록된다. 
-> DataSource에 Connection 대상이 H2 Database 이다. 
운영 중에는 MySQL로 다시 바꾼다. 
-> DB가 바뀜 == 환경이 바뀐다. 

- profile, property가 바뀐다. 

 

Properties

: 어플리케이션을 개발하다 보면 DB 접속정보, 서버 호출정보 등 다양한 정보를 어플리케이션 속성으로 정의

- 코드 상에 정의하면 배포 시에 계속 변경해야 함

- 보안 상 코드에 노출 시키면 안됨

-> 외부로 빼서 읽어오게 한다. 

  • 많은 곳에 정의될 수 있음(file, jvm system file, os level 환경변수)
  • 속성을 객체로 정의해서 사용할 때도 있다. (단위 테스트)
  • resource -> application.properties
version = v1.0.0

kdt.version = v1.0.0

kdt.support_vendors = a, b, c, d

kdt.minimum-order-amount = 1

properties를 어노테이션하자. 

// AppConfiguration.java

@Configuration
@ComponentScan(
	basePackages = {"org.prgrms.kdt.order"}
)
@PropertySource("application.properties")
public class AppConfiguration {
}
// OrderTester.java

public class OrderTester {
	public static void main(String []args) {
    	var applicationContext = new AnnotationConfigapplicationContext(AppConfiguration.class);
        
        var environment = new applicationContext.getEnvironment();
        var version = environment.getProperty("kdt.version");
        var minimumOrderAmount = environment.getProperty("kdt.minimum-order-amount", Integer.class);
        var support = environment.getProperty("kdt.support-vendors", List.class);
        
        var customerId = UUID.randomUUID();
        var voucherRepository = applicationContext.getBean(VoucherRepository.class);
        var voucher = voucherRepository.insert(new FixedAmountVoucher(UUID.randomUUID(), 10L));
        
        var orderService = applicationContext.getBean(orderService.class);
        var orderItems = new ArrayList<orderItem>() {{
        	add(new OrderItem(UUID.randomUUID(), 100L, 1));
        }};
        var order = orderService.createOrder(customerId, orderItems, voucher.getVoucherId());
        Assert.isTrue(order.totalAmount() == 90L, MessageFormat.format("{0} is not 90L", order.totalAmount()));
    }
}

 

value annotation을 사용하면 값을 field에 주입시킬 수 있다. 

//OrderProperties.java

import org.springframework.stereotype.Component;

@Component
public class OrderProperties implelments InitializingBean {
	@Value("v1.1.1")
    // @Value("${kdt.verson}")
	private String version;	// 생성자로 값을 할당하지 않아도 됨
    
    @Value("0")
	private Integer minimumOrderAmount;		// Default : null
    
    @Value("d, a, b")
	private List<String> supportVendors; 	// Default : null
    
    @Value("${JAVA_HOME}")
    private String javaHome;
    
    @Override
    public void afterPropertiesSet() throws Exception {
    	// Bean을 등록하지 않고 바로 확인할 수 있다.
        // 디버깅
    }
}

- 없는 key를 전달한다면 key 값 자체가 value로 들어가게 된다. 

@Value("${kdt.verson:v0.0.0}") // : 다음은 디폴트 값이다. 

 

- 시스템 환경 변수를 쓸 수는 없을까? key 값을 쓸 수 있다.

- 시스템 환경 변수가 더 우선순위가 높다. (파일 환경보다)

 

클래스로 환경을 지정할 수 있다.

//VersionProvider.java

@Component
@PropertySource("version.properties")
public class VersionProvider {
	private final String version;
    
    public VersionProvider(@Value("${version:v0.0.0}")String version) {
    	this.version = version;
    }
    
    public String getVersion() {
    	return version;
    }
    
}
@Service
public class OrderService {
	private final VoucherService voucherService;
    private final OrderRepository orderRepository;
    private final VersionProvider versionProvider;
    
    public OrderService(VoucherService voucherService, OrderRepository orderRepository, VersionProvider versionProvider) {
    	this.voucherService = voucherService;
        this.orderRepository = orderRepository;
        this.versionProvider = versionProvider;
    }
    
    public Order createOrder(UUID customerId, List<OrderItem> orderItems) {
    	var order = new Order(UUID.randomUUID(), customerId, orderItems);
        orderRepository.insert(order);
        return order;
    }
    
    public Order createOrder(UUID customerId, List<OrderItem> orderItems, UUID voucherId) {
    	
        // setter가 없으니까 바꾸지 못한다. 
        versionProvider.getVersion();
        
    	var voucher = voucherService.getVoucher(voucherId);
    	var order = new Order(UUID.randomUUID(), customerId, orderItems);
        orderRepository.insert(order);
        voucherService.useVoucher(voucher);
        return order;
    } 
    
}

 


2. YAML로 Properties 작성

- "YAML은 마크업 언어가 아니다 (YAML Ain't Markup Language)”

- Yet Another Markup Language

- 핵심은 문서 마크업이 아닌 데이터 중심

- 특정한 객체의 형태를 직렬화

 

// application.yaml
kdt:
  version: "v1.0"
  minimum-order-amount: 1
  support-vendors: 
  	- a
    - b
    - c
    - d
  description: |-
  	line 1 hello world
    line 2 xxx
    line 3

스프링 부트는 yaml을 지원하지만 스프링 프레임워크는 yaml을 지원하지 않는다. 

factory로 구현해야 한다. 

// AppConfiguration.java

@Configuration
@ComponentScan(
	basePackages = {"org.prgrms.kdt.voucher", "org.prgrms.kdt.configuration"}
)
@PropertySource(value="application.yaml", factroy=YamlProperitesFactory.class)
public class AppConfiguration {}
// YamlProperitesFactoryBean.java
public class YamlProperitesFactory implements PropertySourceFactory{
	@Override
    public PropertySource<?> createPropertySource(String s, EncodeResource encodeResource) throws IException {
    	var yamlPropertyiesFactroyBean = new YamlPropertiesFactoryBean();
        yamlPropertiesFactoryBean.serResouces(encodeResource.getResource());
        
        var properties = YamlPropertiesFactoryBean.getObject();
        return new PropertiesPropertySource(encodeResource/getResource().getFilename(), properties);
    }
}

큰 프로젝트일 때에는 속성이 다양하기 때문에 그룹화를 시킨다. 

DB, Server, API 등등

 


3. Profile 

: 특정한 특징이나 공통점을 찾아서 그룹화된 것을 의미한다. 

- 설정이나 Bean을 그룹화해서 하나의 Profile로 정의할 수 있다. 

- 예를 들어, 환경별로 그룹화한다.(로컬, 운용환경, dev 등)

- @Profile("local")

public class OrderTester {
	public static void main(String []args) {
    	var applicationContext = new AnnotationConfigapplicationContext();
        applicationContext.register(AppConfiguration.class);
        var environment = applicationContext.getEnvironment();
        environment.setActiveProfiles("dev");
        applicationContext.refresh();
        
        
        var customerId = UUID.randomUUID();
        var voucherRepository = applicationContext.getBean(VoucherRepository.class);
        var voucher = voucherRepository.insert(new FixedAmountVoucher(UUID.randomUUID(), 10L));
        
        var orderService = applicationContext.getBean(orderService.class);
        var orderItems = new ArrayList<orderItem>() {{
        	add(new OrderItem(UUID.randomUUID(), 100L, 1));
        }};
        var order = orderService.createOrder(customerId, orderItems, voucher.getVoucherId());
        Assert.isTrue(order.totalAmount() == 90L, MessageFormat.format("{0} is not 90L", order.totalAmount()));
    }
}

 

yaml 파일도 profile에 의해서 동작하도록 코드를 짤 수 있다. 

// application.yaml

// 공통적으로 사용하는 서버 동작
servers:
  - dev.bar.com
  - foo.

--- 

spring.config.activate.on-profile: local

kdt:
  version: "v1.0"
  minimum-order-amount: 1
  support-vendors: 
  	- a
    - b
    - c
    - d
  description: |-
  	line 1 hello world
    line 2 xxx
    line 3
      
---

spring.config.activate.on-profile: dev

kdt:
  version: "v1.0"
  minimum-order-amount: 1
  support-vendors: 
  	- dev-a
    - dev-b
  description: |-
  	line 1 hello world
    line 2 xxx
    line 3

spring.config.activate.on-profile: local 은 스프링 부트에서 지원하는 기능이다. 

 


4. Resource

- 스프링 어플리케이션을 만들다보면 외부 리소스를 읽는 경우가 있다.

 

* 외부리소스 종류

  • 이미지 파일
  • 텍스트 파일
  • 암복호화 키파일 등

다양한 곳에서 가져올 수 있다.(file system, class path, url...)

- 호출할 API가 모두 다르다. 

스프링은 Resource와 ResourceLoader 인터페이스를 제공한다

- Resource는 다양한 구현체를 제공해준다. 

스프링 공식문서에도 Built-in Resource를 소개해준다.

모든 Application context가 ResourceLoader 인터페이스를 구현하고 있다. 

=> 모든 Application context에서 Resource 객체를 가져올 수 있다. 

 

// OrderTester.java
public class OrderTester {
	public static void main(String []args) throws IOEception {
    	var applicationContext = new AnnotationConfigapplicationContext();
        
        // file resource
    	var resource = applicationContext.getResource("application.yaml");		// class path에서 가져오기
        var file = resource.getFile();
        var strings = Files.readAllLines(file.toPath());		//전체 읽기 List
        System.out.println(strings.stream().reduce("", (a, b) -> a + "\n" + b));
        
        // url resource
    	var resourceURL = applicationContext.getResource("https://stackoverflow.com/");
        var readableByteChannel = Channels.newChannel(resourceURL.getURL().openStream());
        new BufferedReader(Channels.newChannel(readableByteChannel, StandardCharsets.UTF_8));
        var contents = bufferedReader.lines().collect(Collectors.joining("\n"));
        
        var customerId = UUID.randomUUID();
        var voucherRepository = applicationContext.getBean(VoucherRepository.class);
        var voucher = voucherRepository.insert(new FixedAmountVoucher(UUID.randomUUID(), 10L));
        
        var orderService = applicationContext.getBean(orderService.class);
        var orderItems = new ArrayList<orderItem>() {{
        	add(new OrderItem(UUID.randomUUID(), 100L, 1));
        }};
        var order = orderService.createOrder(customerId, orderItems, voucher.getVoucherId());
        Assert.isTrue(order.totalAmount() == 90L, MessageFormat.format("{0} is not 90L", order.totalAmount()));
    }
}

- file path가 기준이라면 working directory를 기준으로 가져온다.