본문 바로가기
  • 개발하는 곰돌이
Programming Language/Common

[Kotlin/Java] 개인적인 Enum 활용기

by 개발하는 곰돌이 2023. 2. 4.

목차

    클린 코드(로버트 C. 마틴 著)를 읽거나 개발 강의를 듣다 보면 리터럴 대신 Enum을 사용하는 것을 권장하는 경우가 많다. 최근 프로젝트를 진행할 때 Enum을 사용하면서 느낀 점에 대해서 정리해보고자 한다.


    Enum?

    Enum이란 관련된 요소들을 모아놓은 집합 또는 단어 의미 그대로 열거해 놓은 것이라고 볼 수 있다. 사실 자바를 처음 배울 당시에만 해도 Enum에 대한 내용은 뭔가 잘 와닿지 않았다. 대부분 Enum에 대해서는 짧고 간단하게 다루는 경우가 많았고, 심지어 아예 Enum을 다루지 않은 책들도 있었다.

     

    그러던 도중 배민 기술 블로그에서 Enum을 다룬 글과 최근 수강한 Spring Boot 강의에서 Enum을 활용하는 것을 보고 어떻게 Enum을 활용할 수 있을지 와닿은 점이 있어서 진행중이던 프로젝트에 적용해봤고 Enum을 활용하는 방법을 어느정도 감을 잡은 것 같다.


    허용값 제한 및 의미 파악이 쉽다

    Enum을 사용하면 메소드의 파라미터의 범위를 손쉽게 제한할 수 있고, 그 파라미터의 의미를 쉽게 파악할 수 있다. 예를 들어 게시판의 글을 구분하기 위한 범주가 일반, 질문, 정보, 공지로 나뉘어 있다. 새로운 글을 작성하거나 기존 글을 수정하고자 할 땐 이 범주에 해당하는 것만 입력해야 한다. 가령, 글의 정보를 저장하는 클래스가 아래와 같이 존재한다.

     

    Kotlin

    class Post(
        var title: String,
        var category: String,
        var content: String,
        val writer: String
    ) {
        ...
    }

    Java

    public class Post {
        private String title;
        private String category;
        private String content;
        private final String writer;
        
        ...
    }

    이 경우 category의 타입이 String이기 때문에 정확하게 일반, 질문, 정보, 공지라는 값만 들어오는지 확인하려면 별도의 메소드를 작성해야 한다. 또한 오탈자로 인해 런타임에서 에러가 발생하거나 전혀 엉뚱한 값이 들어갈 수도 있고, 이 클래스를 처음 보는 경우라면 category에 무슨 값을 넣어야 하는지 알기 어렵다. 뿐만 아니라 category를 별도의 숫자 코드로 구분하는 경우라면 이 숫자가 무슨 의미인지 이해할 수도 없다. 이럴 때 아래와 같이 Enum을 활용하여 코드를 개선할 수 있다.

     

    Kotlin

    enum class Category(val value: String) {
        GENERAL("일반"),
        QUESTION("질문"),
        INFORMATION("정보"),
        NOTICE("공지")
    }
    class Post(
        var title: String,
        var category: Category,
        var content: String,
        val writer: String
    ) {
        ...
    }

    Java

    @RequiredArgsConstructor
    @Getter
    public enum Category {
        GENERAL("일반"),
        QUESTION("질문"),
        INFORMATION("정보"),
        NOTICE("공지");
        
        private final String key;
    }
    public class Post {
        private String title;
        private Category category;
        private String content;
        private final String writer;
        
        ...
    }

    Post 클래스의 category의 타입을 새로 생성한 enum으로 변경했다. Enum인 Category 타입에서는 GENERAL, QUESTION, INFORMATION, NOTICE만 선택할 수 있으므로 이제 Post 객체를 생성할 때 오탈자로 인한 런타임 에러나 엉뚱한 값 삽입에 대한 걱정이 사라졌다. 게다가 IDE의 자동완성 기능이 어떤 값을 삽입해야 하는지 알려주기 때문에 클래스를 처음 보는 사람도 어떤 값을 삽입해야할 지 알기 쉬워졌으며, 별도의 숫자 코드로 구분하더라도 Enum에 code 필드를 만들어서 사용하면 어떤 숫자 코드로 구분하는지 알 필요 없이 Enum에서 정해진 값으로 글의 범주를 구분할 수 있게 된다.


    별도의 코드가 어떤 내용인지 파악하기 쉽다.

    DB에서는 데이터를 구분하기 위한 코드를 사용하는 경우가 많다. 예를 들면 아래와 같이 사용자 정보를 관리하는 테이블에선 사용자의 권한을 관리자와 일반 사용자로 구분하기 위해, 또 글의 범주를 구분하기 위해 숫자 코드를 사용하는 경우를 들 수 있다. 이런 경우에는 아래와 같이 코드에 대한 정보를 별도의 테이블로 관리하게 된다. 

    이렇게 데이터 상태값을 코드로 관리하게 될 때는 코드를 사용하는 컬럼에 각 코드에 대한 설명을 주석으로 추가할 수 있긴 하지만 주석의 내용을 모두가 알 수는 없는 노릇이라서 특히 프론트로 이러한 코드를 사용하는 데이터를 보내줄 때 상태값을 코드 그대로 보내주면 프론트 개발자 입장에서는 이 상태코드가 무엇을 의미하는 것인지 알 방도가 없다. 따라서 응답에는 각 상태코드가 나타내는 상태를 담아서 보내줘야 하는데, CODE_KEY에 따라서 같은 코드가 다른 상태를 나타내고 있다. 이 때 백엔드에서는 CODE_KEY를 Enum으로 수월하게 관리할 수 있다.

     

    실제로 현재 진행중인 프로젝트에서는 아래와 같은 방식으로 앱 실행 시 DB에서 코드를 받아와서 정적 HashMap에 저장한 후 상태코드를 실제 상태로 변환하거나 상태 코드의 유효성 검사를 할 때 사용하고 있었다.

     

    Kotlin

    @Component
    class CodeConverter(private val utilMapper: UtilMapper) {
        @Scheduled(initialDelay = 1000L, fixedDelay = Long.MAX_VALUE)
        fun initCodeMap() {
            for (code in utilMapper.getCodes()) {
                codeMap[code.codeKey] = codeMap.getOrDefault(code.codeKey, HashMap()).also { it[code.code] = code.name }
            }
        }
    
        companion object {
            private val codeMap: MutableMap<String, MutableMap<String, String?>> = HashMap()
            
            @JvmStatic
            fun isValidCode(key: String, code: String?) = codeMap.getOrDefault(key, emptyMap()).containsKey(code)
    
            @JvmStatic
            fun getCodeName(key: String, code: String?) = codeMap.getOrDefault(key, emptyMap())[code]
        }
    }

    Java

    @Component
    @RequiredArgsConstructor
    public class CodeConverter {
        private final UtilMapper utilMapper;
        private static Map<String, Map<String, String>> codeMap = new HashMap<>();
    
        @Scheduled(initialDelay = 1000L, fixedDelay = Long.MAX_VALUE)
        public void initCodeMap() {
            for (Code code : utilMapper.getCodes()) {
                Map<String, String> map = codeMap.getOrDefault(code.getCodeKeys(), new HashMap<>());
                map.put(code.getCode(), code.getName());
                codeMap.put(code.getCodeKeys(), map);
            }
        }
    
        public static boolean isValidCode(String key, String code) {
            return codeMap.getOrDefault(key, Collections.emptyMap()).containsKey(code);
        }
    
        public static String getCodeName(String key, String code) {
            return codeMap.getOrDefault(key, Collections.emptyMap()).get(code);
        }
    }

    그런데 문제는 이렇게 key를 String으로 사용하면 이전 예제에서 category에 String을 사용할 때의 문제와 동일한 문제가 발생한다. DB를 보지 않는 한 정확히 어떤 CODE_KEY를 넣어야 하는지 알 수 없고, 마찬가지로 codeKey를 입력하더라도 오탈자로 인해 코드의 유효성 검사나 코드의 실제 상태값을 가져올 때 원하는 결과를 얻지 못할 수 있다. 물론 해당 기능을 사용하는 CODE_KEY를 String 상수로 선언하여 사용할 수도 있지만 클래스마다 상수를 정의해야해서 번거롭고, 이 경우에도 지정된 값 이외의 key가 전송되는지 확인할 수 없다. Enum을 사용하면 이러한 문제들을 손쉽게 해결할 수 있다.

     

    Kotlin

    enum class CodeKeys(val key: String) {
        CATEGORY("CATEGORY"),
        USER_TYPE("USER_TYPE"),
        USER_STATUS("USER_STATUS")
    }
    @Component
    class CodeConverter(private val utilMapper: UtilMapper) {
        @Scheduled(initialDelay = 1000L, fixedDelay = Long.MAX_VALUE)
        fun initCodeMap() {
            for (code in utilMapper.getCodes()) {
                codeMap[code.codeKey] = codeMap.getOrDefault(code.codeKey, HashMap()).also { it[code.code] = code.name }
            }
        }
    
        companion object {
            private val codeMap: MutableMap<String, MutableMap<String, String?>> = HashMap()
            
            @JvmStatic
            fun isValidCode(key: CodeKeys, code: String?) = codeMap.getOrDefault(key.key, emptyMap()).containsKey(code)
    
            @JvmStatic
            fun getCodeName(key: CodeKeys, code: String?) = codeMap.getOrDefault(key.key, emptyMap())[code]
        }
    }

    Java

    @RequiredArgsConstructor
    @Getter
    public enum CodeKeys {
        CATEGORY("CATEGORY"),
        USER_TYPE("USER_TYPE"),
        USER_STATUS("USER_STATUS");
        
        private final String key;
    }
    @Component
    @RequiredArgsConstructor
    public class CodeConverter {
        private final UtilMapper utilMapper;
        private static Map<String, Map<String, String>> codeMap = new HashMap<>();
    
        @Scheduled(initialDelay = 1000L, fixedDelay = Long.MAX_VALUE)
        public void initCodeMap() {
            for (Code code : utilMapper.getCodes()) {
                Map<String, String> map = codeMap.getOrDefault(code.getCodeKeys(), new HashMap<>());
                map.put(code.getCode(), code.getName());
                codeMap.put(code.getCodeKeys(), map);
            }
        }
    
        public static boolean isValidCode(CodeKeys key, String code) {
            return codeMap.getOrDefault(key.getKey(), Collections.emptyMap()).containsKey(code);
        }
    
        public static String getCodeName(CodeKeys key, String code) {
            return codeMap.getOrDefault(key.getKey(), Collections.emptyMap()).get(code);
        }
    }

    이렇게 isValidCode()getCodeName()의 파라미터로 사용하던 key를 Enum으로 변경함으로써 이 메소드를 사용할 다른 개발자들은 Enum에 선언되어 있는 코드키만 사용할 수 있기 때문에 원하는 코드를 검증하거나 코드의 상태값을 가져올 때 어떤 코드키를 넣어야하는지 알 필요가 없어졌고, 엉뚱한 코드키가 파라미터로 보내지는 경우에도 컴파일 단계에서 걸러줄 수 있게 되었다. 또한, DB에 새로운 코드키가 추가될 경우에는 CodeKeys에 해당 코드키만 추가하면 그대로 사용할 수 있다.


    다양한 외부 API를 호출하는 경우

    하나의 앱에서 다수의 외부 API를 호출하여 그 응답값을 활용해야 하는 경우가 종종 있다. Enum을 사용하지 않으면 이 uri가 어떤 API를 호출하는지 알기 어렵고, API를 호출하는 별도의 메소드를 작성해서 쓰더라도 새로운 외부 API를 호출할 일이 생긴다면 새로운 메소드를 작성하는 등 여러가지 번거로운 점이 많다. 이 때도 Enum을 사용하면 앱에서 사용하는 외부 API를 손쉽게 관리할 수 있다. 

     

    Kotlin

    enum class ExternalApiGroup(val url: Stirng, val authKey: String, val method: HttpMethod) {
        NAVER_API("https://www.naver.com/blabla/api", "인증키", HttpMethod.GET),
        KAKAO_API("https://www.kakao.com/blabla/api", "인증키", HttpMethod.GET),
        GOOGLE_API("https://www.google.com/blabla/api", "인증키", HttpMethod.POST)
    }

    Java

    @RequiredArgsConstructor
    @Getter
    public enum ExternalApiGroup {
        NAVER_API("https://www.naver.com/blabla/api", "인증키", HttpMethod.GET),
        KAKAO_API("https://www.kakao.com/blabla/api", "인증키", HttpMethod.GET),
        GOOGLE_API("https://www.google.com/blabla/api", "인증키", HttpMethod.POST);
        
        private final String url;
        private final String authKey;
        private final HttpMethod method;
    }

    여기까지만 해도 코드를 작성할 때 API를 호출하기 위한 url의 주소와 인증키를 모르더라도 API 호출을 손쉽게 할 수 있고, 어떤 API를 호출할지 알기도 쉬워졌으며, 추가적인 API를 호출해야 할 때도 Enum에 추가만 하면 되니 관리하기도 수월해진다.

     

    하지만 Enum이 클래스의 범주에 포함되는 코틀린과 자바의 특성을 이용하면 ExternalApiGroup을 더욱 개선할 수도 있다.

     

    Kotlin

    enum class ExternalApiGroup(val url: Stirng, val authKey: String, val method: HttpMethod) {
        NAVER_API("https://www.naver.com/blabla/api", "인증키", HttpMethod.GET),
        KAKAO_API("https://www.kakao.com/blabla/api", "인증키", HttpMethod.GET),
        GOOGLE_API("https://www.google.com/blabla/api", "인증키", HttpMethod.POST)
        
        fun getResponse(params: Map<String, String>) {
            val webClient = WebClient.create()
            val formData = LinkedMultiValueMap<String, String>()
            formData.setAll(params)
            val response: String?
            when (method) {
                HttpMethod.GET -> response = webClient.get()
                    .uri(url) { it.queryParams(formData).build() }
                    .header(HttpHeaders.ACCEPT, "*/*")
                    .header(HttpHeaders.AUTHORIZATION, "Bearer $authkey")
                    .retrieve()
                    .bodyToMono(String::class.java)
                    .block()
    
                HttpMethod.POST -> response = webClient.post()
                    .uri(url)
                    .accept(MediaType.APPLICATION_JSON)
                    .body(BodyInserters.fromFormData(formData))
                    .retrieve()
                    .bodyToMono(String::class.java)
                    .block()
                else -> response = ""
            }
            assert(response != null)
            return JsonParser.parseString(response).asJsonObject
        }
    }

    Java

    @RequiredArgsConstructor
    @Getter
    public enum ExternalApiGroup {
        NAVER_API("https://www.naver.com/blabla/api", "인증키", HttpMethod.GET),
        KAKAO_API("https://www.kakao.com/blabla/api", "인증키", HttpMethod.GET),
        GOOGLE_API("https://www.google.com/blabla/api", "인증키", HttpMethod.POST);
        
        private final String url;
        private final String authKey;
        private final HttpMethod method;
        
        public JsonObject getSettlementData(Map<String, String> params) {
            WebClient webClient = WebClient.create();
            MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
            formData.setAll(params);
            String response = "";
            switch (method) {
                case GET:
                    response = webClient.get()
                            .uri(url, (uriBuilder -> uriBuilder.queryParams(formData).build()))
                            .header(HttpHeaders.ACCEPT, "*/*")
                            .header(HttpHeaders.AUTHORIZATION, "Bearer " + authkey)
                            .retrieve()
                            .bodyToMono(String.class)
                            .block();
                    break;
                case POST:
                    response = webClient.post()
                            .uri(url)
                            .accept(MediaType.APPLICATION_JSON)
                            .body(BodyInserters.fromFormData(formData))
                            .retrieve()
                            .bodyToMono(String.class)
                            .block();
                    break;
            }
    
            assert response != null;
            return JsonParser.parseString(response).getAsJsonObject();
        }
    }

    이제 다른 클래스에서 외부 API를 호출할 때 별도의 함수를 작성할 필요도 없어졌다. 그저 API의 요청 파라미터만 Map에 담아서 ExternalApiGroup.NAVER_API.getResponse()와 같이 메소드의 파라미터로 전달하여 호출하면 된다.


    결론

    여기까지 현재 진행중인 프로젝트에서 Enum을 활용해본 방법과 그로 인해 정리해보았다. Enum을 활용하면 메소드의 파라미터에 대한 유효성 검사를 굳이 할 필요가 없어지고 잘못된 값이 들어가면 컴파일 시점에서 걸러주니 한층 수월해진다. 게다가 추가적인 요구사항이 발생하더라도 Enum에 추가만 하면 다른 코드는 수정할 필요가 없으니 확장도 간단해진다.

     

    물론 아직 주니어 개발자인 입장에서 Enum의 활용법이 이게 전부가 아닐 수 있고 미숙한 점이 있을 수도 있다. 하지만 이전에는 어떻게 활용할지 전혀 감이 잡히지 않던 Enum을 다른 분들이 어떻게 활용하는지 보고 배우면서 실제 프로젝트에 적용해보면서 Enum을 활용하는 것으로 얻을 수 있는 많은 이점들을 확실히 느낄 수 있었다.

    댓글