avatar
Octoping Blog
서버 환경에 따라 상수 값 분기하기
객체지향을 활용해 상수 값 분기하기!
backend객체지향Spring
Jul 19
·
6 min read

들어가기 전에

개발을 하다보면 다음과 같이 상수 값은 자주 사용된다.

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 받아오는 식으로 구성하면 될 것 같다.


- 컬렉션 아티클







반갑습니다 :)