들어가기 전에
개발을 하다보면 다음과 같이 상수 값은 자주 사용된다.
public class FileService {
private static final String FILE_STORE_PATH = "/static/images";
public void saveImage(File file) {
String filePath = FILE_STORE_PATH + "/" + file.getName();
// ...
}
}
그런데 서버의 동작 환경 (ex. 테스트, 운영)이나 기타 상황에 따라 상수 값을 다르게 줘야 하는 경우가 있다. 이럴 때에는 어떻게 해야할까?
다음과 같이 매번 변수를 두 개 생성해서 이렇게 분기를 쳐야 하는걸까?
public class FileService {
// 운영 서버와 테스트 서버 간에 이미지 저장 경로를 다르게 해야 한다
private static final String REAL_FOLDER_PATH = "/static/images";
private static final String TEST_FOLDER_PATH = "/static/test/images";
public void saveImage(File file) {
String folderPath = Config.isReal ? REAL_FOLDER_PATH : TEST_FOLDER_PATH;
String filePath = FILE_STORE_PATH + "/" + file.getName();
// ...
}
}
이제부터 더 나은 방법이 있을지 고민해보도록 하자.
다형성
우리는 대부분 객체지향을 활용해서 프로그래밍을 진행하고 있다. 그리고 객체지향의 중요한 특성 중 한 가지, 다형성을 활용해보면 이 문제를 좀 더 멋있게 접근할 수 있을 것 같다.
어떤 함수를 실행할텐데, 상황에 따라 각각 실행되는 로직이 달라야 한다면 어떻게 할 것인가?
interface를 만든 후 이를 구현하는 클래스를 여럿 만들어서 다형성을 통해 로직을 처리하는 것이야말로 객체지향이 권장하는 바이다.
그렇다면 이런 상수 값을 가져다 쓰는 것도 다형성을 활용해볼 수 있지 않을까?
상황에 따라 다른 상수 값 사용하기
~ Spring의 경우 ~
public interface FileStorePathProvider {
String getFileStorePath();
}
다음과 같이, 우리에게 필요한 '역할'을 가진 인터페이스를 구성하고..
class RealFileStorePathProvider implements FileStorePathProvider {
@Override
public String getFileStorePath() {
return "/static/images";
}
}
class TestFileStorePathProvider implements FileStorePathProvider {
@Override
public String getFileStorePath() {
return "/static/test/images";
}
}
각각의 환경에 따른 상수 값을 제공하는 클래스를 만들어주자.
public class FileService {
private final FileStorePathProvider fileStorePathProvider;
public FileService(FileStorePathProvider fileStorePathProvider) {
this.fileStorePathProvider = fileStorePathProvider;
}
public void saveImage(File file) {
String folderPath = fileStorePathProvider.getFileStorePath();
String filePath = FILE_STORE_PATH + "/" + file.getName();
// ...
}
}
그 다음 이렇게, 사용할 곳에서 인터페이스를 주입 받아주자.
뭐를 주입 받아야할지 어떻게 아느냐고? 그건 Spring Profile을 활용해주자.
@Profile("real")
@Component
class RealFileStorePathProvider implements FileStorePathProvider {
@Override
public String getFileStorePath() {
return "/static/images";
}
}
@Profile("test")
@Component
class TestFileStorePathProvider implements FileStorePathProvider {
@Override
public String getFileStorePath() {
return "/static/test/images";
}
}
다음과 같이 @Profile
어노테이션을 붙여서, 특정 프로필에서 어떤 빈이 주입될 것인지 명시해주면 좋겠다.
~ Node.js의 경우 ~
Spring 만 사용하면 너무 정 없으니, Node.js 쪽도 어떻게 접근하면 좋을지 알아보자.
Node.js의 경우는 내가 알기로는 Profile 같은 기능이 없다. 하지만 .env
를 이용하면 비슷하게 서버 환경을 분기할 수 있다.
NestJS (or 의존성 주입 라이브러리) 사용하기
interface FileStorePathProvider {
getFilePath(): String
}
class RealFileStorePathProvider implements FileStorePathProvider {
public getFilePath(): String {
return "/static/images";
}
}
class TestFileStorePathProvider implements FileStorePathProvider {
public getFilePath(): String {
return "/static/test/images";
}
}
비슷하게 인터페이스와 클래스를 구성해주자.
@Injectable()
class InjectableFileStorePathProvider implements FileStorePathProvider {
private static providerMap: Record<string, FileStorePathProvider> = {
"real": new RealFileStorePathProvider(),
"test": new TestFileStorePathProvider(),
"local-real": new RealFileStorePathProvider(),
}
public getFilePath(): String {
return this.getProvider().getFilePath();
}
private getProvider(): FileStorePathProvider {
return InjectableFileStorePathProvider.providerMap[process.env.NODE_ENV];
}
}
그 후 이렇게 Map에서 .env를 이용해서 인스턴스를 얻어오는 식으로 하면 좋을 것 같다.
의존성 주입 라이브러리 없이 사용하기
export const fileStorePathProvider =
process.env.NODE_ENV === "test"
? new TestFileStorePathProvider()
: new RealFileStorePathProvider();
만약 의존성 주입 라이브러리가 없다면 그냥 이렇게 env 파일의 상태에 따라 변수를 구성 후 import 받아오는 식으로 구성하면 될 것 같다.