개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

기초개념 › Spring

설정 파일

설정 파일은 무엇이고, 코드에 적는 것과 무엇이 다른가

설정 파일은 코드와 따로 둔 글 파일입니다. 스프링 부트에서는 application.properties라는 이름이고, 안에는 "이름=값" 줄을 적습니다(shop.name=kim-shop). 포트 번호나 접속 주소처럼 환경마다 달라지는 값을 코드에 직접 적으면 값이 바뀔 때마다 코드를 고쳐 다시 컴파일해야 하므로, 값은 이 파일에 두고 코드는 이름만 적습니다. 코드에서 값이 필요한 필드 위에 @Value("${이름}") 줄을 적으면, 스프링이 객체를 만들 때 설정 파일에서 그 이름의 값을 찾아 필드에 넣어 줍니다. ${이름:기본값} 꼴로 : 뒤에 기본값을 적어 두면 설정에 그 이름이 없을 때 그 값을 씁니다. 이 실습은 설정 파일의 shop.name 줄을 바꾸거나 지우고, @Value에 기본값을 붙이거나 떼면서 java 명령으로 프로그램을 실행해, name 필드에 어떤 값이 들어오는지, 프로그램이 제대로 시작하는지를 봅니다.

실행으로 대조 · 스프링 부트 4.1.1에서 여섯 가지 조합을 조합마다 소스를 컴파일해 자식 java 프로세스로 실행하고, 표준 출력·종료 코드·예외를 프로세스 밖에서 관찰해 대조

이 실습에 나오는 개념

application.properties
application.properties는 스프링 부트가 시작할 때 자동으로 찾아 읽는 설정 파일의 이름이다. 자바 코드가 아니라 "이름=값" 줄(예: shop.name=kim-shop)이 모인 글 파일이며, 줄 앞에 #를 붙이면 설정이 아닌 설명으로 읽힌다. 스프링 부트는 이 파일을 클래스패스의 맨 위(보통 src/main/resources), 클래스패스의 /config, 프로그램을 실행한 폴더, 그 폴더의 config 폴더에서 찾으며, 읽은 값은 스프링의 Environment에 설정 출처(PropertySource)로 들어간다. 같은 내용을 YAML 형식의 application.yaml로 적을 수도 있다. 값을 바꾸려면 코드를 고치지 않고 이 파일만 고치면 되므로, 환경마다 달라지는 값을 두는 곳으로 쓴다.
@Value
@Value는 스프링이 객체를 만들 때 필드나 생성자의 매개변수에 값을 넣어 달라고 표시하는 애너테이션이다. @Value("${shop.name}")처럼 ${ } 안에 설정의 이름을 적으면, 스프링이 설정에서 그 이름의 값을 찾아 넣는다. 우리가 그 필드에 값을 넣는 코드를 쓰지 않아도 된다. 스프링 부트에서는 설정에 그 이름이 없고 기본값도 적지 않으면 객체를 만들지 못해 애플리케이션이 시작하다 멈춘다. 이름이 없을 때를 대비하려면 ${shop.name:no-name}처럼 : 뒤에 기본값을 적는다.
${...} 자리 표시자
${이름}은 "여기에는 설정에 있는 그 이름의 값이 들어간다"고 표시하는 글이다. 스프링이 값을 쓰기 전에 설정에서 이름을 찾아 이 부분을 값으로 바꾸며, @Value("${이름}")뿐 아니라 application.properties의 값 안에서도 쓸 수 있다(예: app.description=${app.name} is an app). ${이름:기본값}처럼 : 뒤에 적은 값은 설정에 그 이름이 없을 때만 쓰이고, 이름이 있으면 쓰이지 않는다. 스프링 부트는 이 자리를 풀지 못하면 시작에 실패하도록 설정자(PropertySourcesPlaceholderConfigurer)를 기본으로 등록해 둔다. 스프링 프레임워크만 쓰고 이 설정자를 두지 않으면 풀지 못한 ${이름} 글자가 값으로 그대로 들어간다.
외부화 설정과 프로파일
외부화 설정은 DB 주소 같은 설정 값을 코드에 넣지 않고 설정 파일, 환경 변수, 명령행 인자 등으로 주입하는 것이다. 같은 빌드 결과물을 환경에 따라 다르게 설정해 실행할 수 있다. 프로파일은 개발·운영처럼 환경별로 설정 묶음을 나눠 두고 실행 시 선택하는 기능이다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 컴퓨터에서 java 명령으로 App 클래스를 실행하면, 자바가 App 클래스의 main 메서드를 부릅니다. main 메서드의 첫 줄 SpringApplication.run(App.class, args) 줄이 스프링 부트를 시작합니다.
  2. 스프링 부트가 시작하면서 클래스 파일이 있는 곳에서 application.properties 파일을 찾아 읽습니다. 이 파일은 자바 코드가 아니라 "이름=값" 줄이 모인 글 파일입니다. shop.name=kim-shop 줄을 읽어, 스프링의 설정 목록에 shop.name = kim-shop 한 쌍을 적어 둡니다.
  3. 다음으로 스프링은 App 클래스가 있는 패키지부터 클래스를 훑어 @Component가 붙은 ShopInfo 클래스를 찾고, 그 객체를 만들기 시작합니다. 우리가 쓴 코드에는 new ShopInfo()라는 줄이 없습니다. 객체는 스프링이 main 메서드의 SpringApplication.run 줄 안에서 만듭니다.
  4. 객체를 만드는 동안 name 필드 위의 @Value("${shop.name}") 줄을 읽습니다. ${ } 안은 "설정 목록에서 이 이름의 값을 찾아 오라"는 뜻입니다. shop.name 이름으로 설정 목록을 찾으니 kim-shop 값이 있어, 스프링이 name 필드에 그 값을 넣습니다. 우리가 쓴 코드에는 name에 값을 넣는 줄도 없습니다.
  5. SpringApplication.run 줄이 끝나고 main 메서드가 다음 줄로 갑니다. ctx.getBean 줄이 스프링이 만들어 보관해 둔 ShopInfo 객체를 꺼내 info에 담습니다. 이 객체의 name에는 이미 kim-shop 값이 들어 있습니다.
  6. System.out.println(info.name) 줄이 name에 들어 있는 글을 화면에 찍습니다. 화면에 kim-shop 한 줄이 찍힙니다.
  7. main 메서드가 끝납니다. 이 프로젝트에는 요청을 기다릴 웹 서버가 없으므로 java 프로세스도 그대로 끝납니다(종료 코드 0).

실행 결과

코드

# application.properties
# 설정: 이름=값 꼴로 코드 밖에 적는 값
shop.name=kim-shop

@Component
class ShopInfo {
    // 받기: 설정에서 shop.name 값을 찾아 name에 넣는다
    @Value("${shop.name}")
    String name;
}

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        // 스프링을 시작한다. ctx는 스프링이 만든 객체를 보관한 곳이다
        ApplicationContext ctx = SpringApplication.run(App.class, args);
        ShopInfo info = ctx.getBean(ShopInfo.class);
        System.out.println(info.name);
    }
}

이 모형의 전제

스프링 부트 4.1.1(스프링 프레임워크 7.0.9)을 JDK 21.0.12.1에서 실행한 결과입니다. 여섯 가지 조합을 모두, 화면의 코드와 같은 구조의 소스(패키지 app의 ShopInfo와 App, 그리고 클래스 파일과 같은 폴더에 둔 application.properties)를 컴파일하고 java 명령으로 app.App의 main 메서드부터 새 프로세스로 실행해 대조했습니다. 대조한 것은 결과 표의 세 항목입니다. name에 들어온 값은 프로세스가 표준 출력에 찍은 글로, 시작 성공 여부는 종료 코드(0과 1)와 표준 출력으로, 오류는 표준 오류에 나온 예외의 이름과 메시지로 정했습니다. 설정 파일을 지운 조합에서는 파일에서 shop.name 줄과 바로 위 설명 줄만 지우고 맨 위의 # application.properties 줄은 남겼습니다. 단계 문장이 기대는 사실도 실행으로 확인했습니다. 설정 파일을 둔 조합에서 스프링이 클래스패스의 application.properties를 설정 출처로 읽고(Config resource class path resource [application.properties]) 그 안의 shop.name 값이 스프링의 Environment에 들어 있는 것, SpringApplication.run이 끝났을 때 ShopInfo 객체가 이미 만들어져 name 필드에 값이 들어 있는 것, 우리 소스에 new ShopInfo()나 name에 값을 넣는 줄이 없는 것, shop.name 줄이 없고 기본값도 없을 때 예외(BeanCreationException, 원인은 PlaceholderResolutionException의 Could not resolve placeholder 'shop.name' in value "${shop.name}")가 SpringApplication.run 줄에서 나와 main이 다음 줄에 가지 못하고 종료 코드 1로 끝나며 표준 출력에 아무것도 찍히지 않는 것, 같은 컴파일 결과(ShopInfo.class 파일과 App.class 파일의 해시가 같음)에서 application.properties만 kim-shop에서 lee-shop으로 고쳐 다시 실행하면 컴파일 없이 lee-shop이 나오는 것입니다. shop.name 줄을 지우고 기본값을 적은 조합에서는 파일에 이름=값 줄이 하나도 없어 스프링의 설정 출처 목록에 application.properties가 오르지 않는 것을 관찰했습니다. 단계 문장은 그 사실을 "설정 목록에 shop.name 이름이 없다"까지만 말합니다. 클래스패스는 라이브러리 폴더 전체가 아니라 spring-boot-starter가 들여오는 21개 jar만 줬고(스프링 부트 기초개념 실습과 같은 목록, 웹 라이브러리 없음), 자식 프로세스의 작업 폴더는 빈 임시 폴더로 했습니다(스프링 부트는 작업 폴더의 application.properties와 config 폴더도 읽기 때문입니다). 환경 변수 중 SPRING_과 SHOP_으로 시작하는 것은 자식 프로세스에서 뺐습니다. 문서가 정한 것은 스프링 부트 문서 Externalized Configuration 절(application.properties를 시작할 때 클래스패스 맨 위, 클래스패스의 /config 패키지, 현재 폴더, 현재 폴더의 config 폴더에서 찾아 읽는다는 것, 같은 코드를 환경마다 다르게 쓰려고 설정을 코드 밖에 둔다는 것, ${이름:기본값}에서 : 뒤가 기본값이라는 것)과 스프링 프레임워크 문서 Using @Value 절(@Value("${...}")로 외부 설정값을 넣는다는 것, 기본값 문법, 스프링 부트가 application.properties의 값을 읽는 PropertySourcesPlaceholderConfigurer를 기본으로 등록한다는 것, 이 설정자가 있으면 ${} 자리를 풀지 못할 때 초기화가 실패한다는 것)입니다. 스프링 프레임워크만 쓰고 이 설정자를 두지 않으면 풀지 못한 ${shop.name} 글자가 값으로 그대로 들어간다고 문서에 적혀 있지만, 이 실습은 스프링 부트에서만 실행했습니다. 값이 없을 때 BeanCreationException과 PlaceholderResolutionException이 나온다는 것은 문서가 정한 것이 아니라 이 버전에서 관찰한 동작입니다. 화면의 코드는 package와 import 줄을 줄였고, ShopInfo와 App이 같은 패키지에 있다고 가정했습니다. 두 클래스는 실제로는 파일이 따로이고, application.properties도 보통은 src/main/resources에 두며 빌드가 클래스 파일 옆으로 복사합니다(여기서는 직접 그 폴더에 두었습니다). 이 실습이 다루지 않는 것은 application.yml, 여러 곳에 같은 이름이 있을 때의 우선순위, 환경 변수와 실행 인자로 값을 바꾸는 것, 프로파일별 파일, @ConfigurationProperties입니다. 우선순위는 "세부 동작 실습: 설정 우선순위"에서, 설정을 보고 스프링 부트가 알아서 빈을 만드는 일은 "세부 동작 실습: 스프링 부트 자동 설정"에서 다룹니다.

앞 실습: 컨트롤러 · 다음 실습: JDBC

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요