Codeit/스프린트 과제

[sprint3] 심화 2 - application.yaml로 Bean 구현하기 (File*Repository 구현체의 파일 저장 경로 설정하기

leejunkim 2025. 6. 28. 11:26

업무: File*Repository 구현체의 파일을 저장할 디렉토리 경로를 application.yaml 설정 값을 통해 제어해보세요.


먼저 심화 1부분의 파일들을 그대로 가져와서 설명하려고 한다.

application.yml

spring:
  application:
    name: discodeit


discodeit:
  repository:
    # jcf | file
    type: file
    file-directory: .discodeit
    extension: .ser

RepositorySettings.java

@ConfigurationProperties("discodeit.repository")
// need getters and setters for spring to inject values
@Getter
@Setter
public class RepositorySettings {
    // default values unless stated otherwise
    private String type = "jcf";
    private String fileDirectory = ".discodeit";
    private String extension = ".ser";
}

DiscodeitApplication.java (메인 파일)

@SpringBootApplication
@EnableConfigurationProperties(RepositorySettings.class)
public class DiscodeitApplication {

	public static void main(String[] args) {

 

이제 목표는 File*Repository에 이 설정들을 가져와서 사용하는 것이다.

 

예를 들면, File*Repository는 코드는 이렇다:

@Repository
public class FileBinaryContentRepository implements BinaryContentRepository {
    private final Path DIRECTORY;
    private final String EXTENSION = ".ser";

    public FileBinaryContentRepository() {
        this.DIRECTORY = Paths.get(System.getProperty("user.dir"),
                "file-data-map",
                        BinaryContent.class.getSimpleName());
        if (Files.notExists(DIRECTORY)) {
            try {
                Files.createDirectories(DIRECTORY);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private Path resolvePath(UUID id) {
        return DIRECTORY.resolve(id + EXTENSION);
    }

	... // 그 외 메서드들

 

목표는 DIRECTORY, EXTENSION, 그리고 System.getProperty의 "user.dir"을 application.yml 파일에서 받아오는 것이다.

@Value를 썼는데 안된 경우

아직 설정값으로 빈 등록이 익숙치 않아서 처음부터 많이 해매서, 일단 처음에는 강사님의 도움을 받고 @Value를 넣어서 해결하려고 했다.

FileChannelRepository.java

@Repository
public class FileBinaryContentRepository implements BinaryContentRepository {

    @Value("${discodeit.repository.file-directory}")
    private String fileDirectory;

    private final Path directory;
    private final String extension = ".ser";

    public FileBinaryContentRepository() {
        this.directory = Paths.get(System.getProperty("user.dir"),
            fileDirectory,
            "file-data-map",
            BinaryContent.class.getSimpleName());
        if (Files.notExists(directory)) {
            try {
                Files.createDirectories(directory);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    .....

 

이렇게 했더니.. 이런식으로 에러가 떴다!

 

문제가 뭔지 알아보니까..

@Value 어노테이션을 통해 클래스 필드에 의존성 주입을 할 경우, 객체가 완전히 생성이 되고 난 후 주입이 되므로, 객체 생성자가 실행되는 시점에서 @Value 값이 null이 된다는 것을 의미한다 (NullPointerException).

 

이 뜻은, FileBinaryContentRepository가 생성자를 통해 인스턴스화 될때, @Value를 쓰면 필드값은 생성이 완료된 후 주입이 되므로, 생성 시 안에 fileDirectory의 값이 아직 주입이 안됐어서 NPE가 뜨는 것이다.

 

스프링 빈의 생명주기를 간단하게 알아보자:

스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료

 

이때 의존관계 주입은

  • 생성자 주입 (스프링 생성시점)
  • 필드 주입 (의존관계 주입 시점),
  • setter 주입 (의존관계 주입 시점)이 있다.

이때 @Value는 필드 주입과 동일하게 의존관계 주입 시점에 동작하는데 이는 생성자 호출 시점 이후다!

해결책 1 - @Value 와 @PostConstruct

@PostConstruct는 초기화 콜백이다. 이 뜻은 의존관계 주입 시점인 @Value가 동작한 이후여서, @PostConstruct때 fileDirectory를 사용하는 방식으로 코드를 작성하면 일단 문제가 해결된다.

@Repository
public class FileBinaryContentRepository implements BinaryContentRepository {

    @Value("${discodeit.repository:file-directory}")
    private String fileDirectory;

    private Path DIRECTORY;
    private final String EXTENSION = ".ser";

    @PostConstruct
    public void initDirectory() {
        // 예: ~/discodeit/file-data-map/BinaryContent
        this.DIRECTORY = Paths.get(System.getProperty("user.dir"),
                fileDirectory,
                "file-data-map",
                BinaryContent.class.getSimpleName());

        try {
            if (Files.notExists(DIRECTORY)) {
                Files.createDirectories(DIRECTORY);
            }
        } catch (IOException e) {
            throw new RuntimeException("파일 저장 디렉토리 생성 실패", e);
        }
    }

    private Path resolvePath(UUID id) {
        return DIRECTORY.resolve(id + EXTENSION);
    }

	... // 그 외 다른 메서드

 

일단 이렇게 하고, 생성자는 없앴다.

하지만 멘토님한테 여쭤보니까 사실상 @Value를 쓰는 일은 거의 없어서 이 다음 방법으로 코드를 다시 썼다:

해결책 2 - 생성자에 RepositorySettings 주입

@Repository
public class FileBinaryContentRepository implements BinaryContentRepository {
    private final Path directory;
    private final String extension;

    public FileBinaryContentRepository(RepositorySettings repositorySettings) { // 여기
        this.extension = repositorySettings.getExtension();
        String fileDirectory = repositorySettings.getFileDirectory();
        this.directory = Paths.get(System.getProperty("user.dir"),
                fileDirectory,
                "file-data-map",
                BinaryContent.class.getSimpleName());

        try {
            if (Files.notExists(directory)) {
                Files.createDirectories(directory);
            }
        } catch (IOException e) {
            throw new RuntimeException("파일 저장 디렉토리 생성 실패", e);
        }
    }

    private Path resolvePath(UUID id) {
        return directory.resolve(id + extension);
    }

 

일단 @Value 어노테이션과 @PostConstruct를 지우고, 생성자를 다시 만들어서 RepositorySettings 값을 받아오는 걸로 바꿨다.

 

그러면 이제 FileRepositoryConfig.java:

@Configuration
@ConditionalOnProperty(
        prefix = "discodeit.repository",
        name="type",
        havingValue = "file"
)
public class FileRepositoryConfig {

    @Bean
    public BinaryContentRepository binaryContentRepository(RepositorySettings settings) {
        return new FileBinaryContentRepository(settings);
    }

    @Bean
    public ChannelRepository channelRepository(RepositorySettings settings) {
        return new FileChannelRepository(settings);
    }

	// ... 그 외 File*Repository 메서드들

 

그리고 다시 보는 RepositorySettings

@ConfigurationProperties("discodeit.repository")
// need getters and setters for spring to inject values
@Getter
@Setter
public class RepositorySettings {
    // default values unless stated otherwise
    private String type = "jcf";
    private String fileDirectory = ".discodeit";
    private String extension = ".ser";
}

FileRepositoryConfig은 application.yml의 설정값에 따라 토글되고, FileRepositoryConfig안에 있는 파라미터 값RepositorySettings는 application.yml 파일을 통해 값들을 받아온다.