• Feed
  • Explore
  • Ranking
/
/
    🖥 백엔드

    서버 환경에 따라 상수 값 분기하기

    객체지향을 활용해 상수 값 분기하기!
    Backend객체지향Spring
    O
    Octoping
    2024.07.18
    ·
    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 받아오는 식으로 구성하면 될 것 같다.







    - 컬렉션 아티클