application.properties로 사용하는 분들도 계시지만 방법은 동일하기 때문에 저는 application.yml 기준으로 혼자 복습하며 기록해보려고 합니다.
만약 A, B, C라는 클래스가 존재할 때, 이 3개의 클래스 내부에서 companyName이라는 값을 모두 같은 "Fast Company"라는 값을 사용한다고 가정을 해봅시다.
이때 만약 회사 이름이 변경되어서 "Slow Company"로 바뀌었다고 생각해 봅시다. 그럼 우리는 A, B, C 클래스로 모두 이동해서 "Fast Company"라는 값을 "Slow Company"로 모두 변경해줘야 합니다.
물론 클래스가 3개 밖에 없어서 간단하게 할 수 있지만, 만약 클래스가 1000개가 넘는다고 생각하면 어떨까요? 무려 1000번을 변경해줘야 합니다. 이때 이런 공통적인 값들을 properties이나 yml(yaml) 파일에서 설정 파일의 값만 바꿔주면 모든 클래스에 적용시켜 줄 수 있습니다.
이렇게 클래스에서 프로퍼티 값을 가져오는 방법에는 여러 가지가 존재하는데, 코드를 작성하며 한번 확인해 봅시다.
일단 먼저 application.yml에 우리가 사용할 설정 값을 입력해 둡니다.
# application.yml
company:
name: "Fast Company"
1. @Value("${}")
제일 먼저 등장하는 것은 @Value 어노테이션입니다. SpEL(Spring Expression Langauge)로 내가 설정한 property 이름을 표현해 주면 해당 값을 가져올 수 있는 방법입니다.
A class에서는 @Value를 이용해서 application.yml의 company.name을 가져와볼 것입니다.
// A.java
@Component
public class A {
@Value("${company.name}")
private String companyName;
public A() {
}
public void printCompanyName() {
System.out.println("A class = " + companyName);
}
}
클래스 A를 @Component를 이용해서 빈으로 등록합니다. 그리고 @Value를 이용해서 우리가 원하는 설정 값을 가져와서 companyName에 넣을 것이고, main 함수가 있는 클래스에서 printCompanyName()을 이용해서 값이 잘 입력되었는지 확인해 볼 것입니다.
main에서는 생성자 주입을 통해서 A, B, C 클래스 빈에 대해서 DI를 받을 것이고, 모든 생성자 주입이 끝난 후에 실행시켜 줄 것이기 때문에 @PostConstruct를 사용할 것입니다. 아래 코드에 해당 과정이 작성되어 있습니다.
// MainApplication.java
@SpringBootApplication
public class MainApplication {
private final A a;
private final B b;
private final C c;
public MainApplication(A a, B b, C c) {
this.a = a;
this.b = b;
this.c = c;
}
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
@PostConstruct
public void init() {
a.printCompanyName();
b.printCompanyName();
c.printCompanyName();
}
}
코드를 실행시켜 보면 콘솔에 찍히는 값은 다음과 같습니다. 나머지 로그들은 모두 제외했습니다. 간단하게 출력 값만 적어봤습니다.
A class = Fast Company
B class = null
C class = null
결과를 보면 A class에서 Fast Company라는 값을 제대로 가져온 것을 확인할 수 있습니다. B와 C는 아직 아무 설정도 안 해줬기 때문에 null이 나오는 것입니다.
A class를 조금 더 멋스럽게 변경해 보겠습니다. 필드에 주입하는 것이 아닌 우리는 생성자를 사용해서 필드값을 초기화하는 방법을 더 많이 사용하기 때문에 이렇게 변경해보겠습니다.
// A.java
@Component
public class A {
private String companyName;
public A(@Value("${company.name}") String companyName) {
this.companyName = companyName;
}
public void printCompanyName() {
System.out.println("A class = " + companyName);
}
}
매우 간단하게 해냈습니다. 필드 값 위에 입력하는 것이 아닌 생성자의 매개변수에 @Value를 붙여서 넣어주면 됩니다. 조금 멋있어졌군요.
2. Enviroment Bean (ApplicationContext)
두 번째 방법은 Enviroment 빈을 사용해서 프로퍼티 값을 가져오는 방법입니다. Spring Bean으로 등록되어 있는 Enviroment Bean을 주입받아서 getProperty()를 이용하여 해당 프로퍼티 값을 가져올 수 있는 방법입니다.
B class는 이 방법을 사용해서 프로퍼티 값을 한번 가져와보겠습니다.
// B.java
@Component
public class B {
private String companyName;
private final Environment environment; // 생성자 주입을 통해서 DI
public B(Environment environment) {
this.environment = environment;
companyName = this.environment.getProperty("company.name");
}
public void printCompanyName() {
System.out.println("B class = " + companyName);
}
}
org.springframework.core.env에 있는 Enviroment를 생성자를 통해서 의존성 주입을 받아옵니다. 그런 다음 Enviroment의 getProperty() 메서드를 이용하여 원하는 프로퍼티 값을 가져와서 companyName에 초기화해 줬습니다.
다시 main 메서드를 돌려서 출력 값을 확인해 봅니다.
A class = Fast Company
B class = Fast Company
C class = null
B class에도 값이 잘 가져와진 것을 볼 수 있습니다. 위에서 작성한 Enviroment를 사용하는 코드는 직접 ApplicationContext를 가져와서 사용해 볼 수도 있습니다. 코드를 한번 리펙토링 해보겠습니다.
// B.java
@Component
public class B {
private String companyName;
private final ApplicationContext applicationContext;
public B(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
companyName = this.applicationContext.getEnvironment().getProperty("company.name");
}
public void printCompanyName() {
System.out.println("B class = " + companyName);
}
}
main을 다시 재시작해보니 출력되는 값은 동일하게 성공했습니다. 하지만 뭔가 코드 가독성면에서는 별로 좋아 보이진 않는다는 생각이 드는 코드입니다.
3. Configuration Properties
다음 방법은 자바 클래스로 따로 분리해서 값을 가져올 수 있는 방법입니다. @Value를 사용했을 때와는 다른 점은 클래스로 값을 매핑해서 가져올 수 있기 때문에 Type Safe 합니다. 그리고 각 프로퍼티 값에 대한 Meta 데이터를 작성할 수 있습니다.
사용하는 방법은 @ConfigurationProperties("") 애노테이션을 사용하여 작성할 수 있습니다. 해당 클래스를 Bean으로 등록해서 사용할 수 있게 하기 위해서는 @Configuration 애노테이션을 사용해야 합니다. 코드를 먼저 작성해 보겠습니다.
# @Configuration 애노테이션 사용
// CompanyProperties.java
@ConfigurationProperties("company")
@Configuration
public class CompanyProperties {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@ConfigurationProperties("company")를 이용하여 나는 application.yml에서 company라는 프로퍼티 값을 가져와서 매핑한다는 것을 의미한다.
@Configuration을 붙여주는 이유는 CompanyProperties 클래스를 스프링 빈으로서 등록하기 위함이다. 그래야 다른 곳에서 DI 받아서 사용할 수 있기 때문이다.
private String name에 company.name을 가져와서 값을 초기화시키는 것이다. 이때 우리는 setter를 정의해 주면 된다. getter는 외부에서 해당 값을 사용하기 위해서는 필요하다.
이제 스프링 빈으로 등록된 CompanyProperties를 주입받아서 C class에서 사용해 볼 것이다.
// C.java
@Component
public class C {
private CompanyProperties companyProperties;
public C(CompanyProperties companyProperties) {
this.companyProperties = companyProperties;
}
public void printCompanyName() {
System.out.println("C class = " + companyProperties.getName());
}
}
생성자 주입을 통해서 CompanyProperties를 주입받습니다. 그런 후 getName()을 통해서 해당 프로퍼티 값을 받아서 printCompanyName()에 넣어줍니다. 이제 main을 재시작해서 출력값을 확인해 보겠습니다.
A class = Fast Company
B class = Fast Company
C class = Fast Company
C class도 적절하게 값을 받아오는 것을 확인할 수 있습니다.
위에서 작성한 코드를 조금 리펙토링 해보겠습니다.
CompanyProperties에서 @Configuration을 떼어보겠습니다. 어 그럼 Bean으로 등록되지 않아서...주입을 받아오지 못하지 않나?라는 의문이 들어야 합니다.
그래서 MainApplication로 가서 @ConfigurationPropertiesScan 애노테이션을 붙여줍니다. @ConfigurationProperties가 붙은 클래스를 자동으로 Scan 하여 빈으로 등록해 줍니다.
# @Configuration을 빼고, @ConfigurationPropertiesScan을 사용
// CompanyProperties.java
@ConfigurationProperties("company")
public class CompanyProperties {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// MainApplication.java
@ConfigurationPropertiesScan
@SpringBootApplication
public class MainApplication {
/* 코드 생략 */
}
이렇게 설정해 두고 다시 main을 재시작해보면 적절하게 잘 출력되는 것을 확인해 볼 수 있습니다.
하지만 여기서 조금 생각해 볼 것이 있습니다. CompanyProperties를 보면 setName()을 통해서 해당 값을 초기화합니다. 이렇게 되면 언제든 setter를 통해서 값을 변경할 수 있다는 것은 조금 위험한 내용인 것 같습니다. 필드를 final로 선언해서 생성자를 통해서 해당 값을 초기화할 수 있도록 코드를 리펙토링 해봅시다. 이때는 @ConstructorBinding 애노테이션을 사용합니다. 생성자를 통해 프로퍼티 값을 바인딩받을 수 있도록 해주는 애노테이션입니다.
# Setter를 빼고, @ConstructorBinding을 사용하여 생성자 바인딩
// CompanyProperties.java
@ConstructorBinding
@ConfigurationProperties("company")
public class CompanyProperties {
private final String name;
public CompanyProperties(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
main을 다시 재시작해봅니다. 그럼 값이 적절하게 출력되는 것을 볼 수 있습니다. 리펙토링 성공이네요. 이렇게 불변의 값으로 설정하고 싶다면 @ConstructorBinding을 사용해서 해당 값을 다루는 것이 좋습니다.
이제 코드를 완성했으니 우리가 생각했던 시나리오대로 "Fast Company"를 "Slow Company"로 바꿔서 모든 출력값이 정상적으로 변경되는지 확인해 봅시다.
# application.yml
company:
name: "Slow Company"
마지막은 캡처로 깔끔하게 확인해 봤습니다. 모든 출력값이 정상적으로 Slow Company로 변경되었습니다. 좋습니다~ 🙄
이번 정리 글에서는 많은 애노테이션이 새롭게 나와서 하나씩 정리해 보는 시간을 가지는 것이 좋을 거 같습니다. 프로젝트에서 이 내용을 써보게 되는 날이 왔으면 좋겠습니다.
'🧑🏻💻 Dev > SpringBoot' 카테고리의 다른 글
[Spring | LetsEncrypt] Http 서버를 Https로 바꿔보자 (0) | 2023.08.07 |
---|---|
[Spring] 스프링에서 캐시를 사용하는 방법 (with. Redis) (0) | 2023.04.10 |
[Spring] ExceptionHandler가 작동안 되는 오류 (컴포넌트 스캔) (0) | 2023.04.03 |
[Spring/OAuth2.0] 스프링 페이스북 로그인 구현하기 (4) | 2023.03.16 |
[Spring] 프로젝트 실행 시 Please Sign in 페이지 해결 (0) | 2023.02.19 |