업무: 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 파일을 통해 값들을 받아온다.
'Codeit > 스프린트 과제' 카테고리의 다른 글
| [sprint8] S3 관련 코드 + S3Client와 Presigned Url (3) | 2025.08.25 |
|---|---|
| [sprint8] 어플리케이션 컨테이너화 - dockerfile, docker compose, docker volume (1) | 2025.08.22 |
| [sprint5] 과제에서 사용한 swagger 간단하게 정리 (0) | 2025.07.14 |
| [sprint4] IOException과 @Transactional (멘토님 피드백) (0) | 2025.07.14 |
| [sprint3] 심화 1 - application.yaml로 Repository 구현체 선택하기 (File/JCF) (2) | 2025.06.27 |