본문 바로가기
프로그래밍

Interface 를 활용한 Enum 리팩토링

by kdohyeon (김대니) 2023. 6. 21.
반응형

java interface

Java Enum 을 Interface 를 활용하여 리팩토링 하게 된 사례를 공유합니다.


상황

리팩토링 대상이 되는 코드의 특징은 다음과 같다.

  • 10개 이상의 Enum 클래스가 존재하며, Enum 클래스는 지속적으로 더 추가될 수 있음
  • 모든 Enum 클래스는 code, name 이라는 필드를 공통으로 포함하고 있으며, 각 Enum 클래스에 따라 별도 필드가 포함될 수 있음
  • 모든 Enum 클래스는 공통으로 NONE 이라는 Enum 타입을 가지고 있음

목표

위 조건에 해당하는 10개 이상의 Enum 클래스를 기반으로 특정 DTO 객체를 생성해야 한다.

  • 각 Enum 클래스에 대해 List<MetaTypeDto> 리스트 객체를 생성해야 한다.
public class MetaTypeDto {
  private String type;
  private String desc;
  private String List<MetaDetailTypeDto> list;
}
  • List<MetaDetailTypeDto> 리스트 객체는 각 Enum 클래스의 Enum 타입을 기준으로 설정해야 한다.
public class MetaDetailTypeDto {
  private String code;
  private String name;
}

예시

예를 들어보자. EnumA, EnumB 를 활용하여 List<MetaTypeDto> 를 생성해서 JSON 형식으로 표현한다면 다음과 같다.

[
    {
        "type": "EnumA 타입", // 하드코딩
        "desc": "EnumA 설명", // 하드코딩
        "list": [
            {
                "code": "NONE",
                "name": "없음"
            },
            {
                "code": "ENUM_A1",
                "name": "A1"
            },
            {
                "code": "ENUM_A2",
                "name": "A2"
            },
        ]
    },
    {
        "type": "EnumB 타입", // 하드코딩
        "desc": "EnumB 설명", // 하드코딩
        "list": [
            {
                "code": "NONE",
                "name": "없음"
            },
            {
                "code": "ENUM_B1",
                "name": "B1"
            }
        ]
    },
    ...
]

기존 코드

리팩토링을 하기 전에는 가장 간단한 방법으로 구현되어 있었다.

  • 각 Enum 클래스에 대한 MetaTypeDto 를 직접 생성
  • 생성한 MetaTypeDto 를 List 에 넣어 반환
public List<MetaTypeDto> buildMetaTypeDtos() {
  List<MetaTypeDto> result = Lists.newArrayList();

  MetaTypeDto a = MetaTypeDto.builder().type("aType").desc("타입 A").list(getAList()).build();
  MetaTypeDto b = MetaTypeDto.builder().type("bType").desc("타입 B").list(getBList()).build();
  MetaTypeDto c = MetaTypeDto.builder().type("cType").desc("타입 C").list(getCList()).build();
  ...

  result.add(a);
  result.add(b);
  result.add(c);
  ...

  return result;
}
  • getAList(), getBList(), getCList() 등 각 Enum 의 리스트를 만들기 위한 메소드를 각각 만들어 활용 (즉, Enum 클래스 개수만큼 생성됨)
  • getAList(), getBList(), getCList() 등 모든 메소드는 동일한 역할을 수행함 (.filter() -> .map() -> .collect())
public List<MetaDetailTypeDto> getAList() {
  return Arrays.stream(EnumA.values())
                  .filter(code -> !code.equals(EnumA.NONE))
                  .map(code -> MetaDetailTypeDto.builder()
                                        .code(code.getCode())
                                        .name(code.getName())
                                        .build())
                  .collect(Collectors.toList());
}

문제점

위 코드처럼 구현을 하면 몇 가지 문제점이 있다.

  • 문제점 1) 각 Enum 클래스가 생성될 때마다 새로운 getAList(), getBList() 를 만들어주어야 한다.
  • 문제점 2) MetaTypeDto 역시 새로운 Enum 클래스에 맞춰서 생성해주어야 한다.
  • 문제점 3) 중복 로직이 많다.

개선하기

중복 로직을 없애기 위해서 먼저 생각한 방식은 자바의 상속 개념을 활용하면 되지 않을까? 였다.
최상단의 부모 Enum 클래스를 만들어 공통으로 수행하는 로직을 정리해두고, 10개의 이상의 각 Enum 클래스가 그 부모 Enum 을 상속하도록 변경하면 리팩토링에 좀 유연하지 않을까 생각했다.
근데... 구글에 찾아보니 Enum 은 일반 클래스처럼 extends 를 할 수 없다고 한다.

Enum 은 왜 extends 를 할 수 없을까?

참고. https://www.baeldung.com/java-extending-enums

자바에서는 Enum 클래스를 컴파일 할 때, Enum 클래스를 final 클래스로 변경한다. 해당 Enum 클래스 내부의 각 Enum 타입은 static final 필드로 변경한다. 아래와 같은 예시처럼 컴파일이 되는 모습을 확인할 수 있는데, 자바에서는 final 클래스를 상속받을 수 없기 때문에 Enum 클래스를 extends 를 할 수 없다.

$ javap BasicStringOperation  
public final class com.baeldung.enums.extendenum.BasicStringOperation 
    extends java.lang.Enum<com.baeldung.enums.extendenum.BasicStringOperation> {
  public static final com.baeldung.enums.extendenum.BasicStringOperation TRIM;
  public static final com.baeldung.enums.extendenum.BasicStringOperation TO_UPPER;
  public static final com.baeldung.enums.extendenum.BasicStringOperation REVERSE;
 ...
}

그럼 어떻게? → Interface 활용

참고. https://wedul.site/289

이펙티브 자바를 보면 (규칙 34) 확장 가능한 enum 을 만들어야 한다면 인터페이스를 이용하면 된다는 이야기가 나온다.
그럼 10개의 enum 에서 공통으로 사용할 수 있는 인터페이스를 만들어보자.

public interface MetaCode {
    String getCode();
    String getName();
    List<MetaCode> except(); // 필터링 용

    static List<MetaDetailTypeDto> buildMetaDetailTypeDto(Class<? extends MetaCode> cls {
        MetaCode[] enumConstants = cls.getEnumConstants();
        return Arrays.stream(enumConstants)
                        .filter(code -> !code.except().contains(code))
                        .map(code -> MetaDetailTypeDto.builder()
                                        .code(code.getCode())
                                        .name(code.getName())
                                        .build())
                        .collect(Collectors.toList());
    }
}

interface 내부에 static 메소드를 만들어서 공통 로직을 수행할 수 있도록 한다.
Class<? extends MetaCode> cls 를 파라미터로 받게 해두었는데, MetaCode 인터페이스의 구현체만 파라미터로 전달될 수 있도록 제한한다.
Enum 과 interface 같이 사용할 때 핵심은 cls.getEnumConstants() 인데, 이 코드를 통해서 인입된 클래스의 enum 값들을 모두 가져올 수 있다.
참고. https://techblog.woowahan.com/2527/

각 Enum 클래스에 인터페이스 적용

기존에 사용하고 있던 각 Enum 클래스를 이제 MetaCode 인터페이스를 구현하도록 수정해야 한다.

@Getter
public enum EnumA implement MetaCode {

    NONE("NONE", "없음"),
    ENUM_1("ENUM_A1", "A1"),
    ENUM_2("ENUM_A1", "A2");

    private String code;
    private String name;

    EnumA(String code, String name) {
        this.code = code;
    }

    @Override
    List<MetaCode> except() {
        return Lists.newArrayList(NONE);
    }
}

각 구현체를 관리하는 별도 Enum 생성

MetaTypeDto 객체를 만들기 위해서는 type, desc 값이 추가적으로 필요한데, "EnumA 타입", "EnumA 설명" 등 하드코딩되어 있는 값들을 별도 Enum 으로 관리한다면 좀 더 개선된 코드를 만들 수 있다.

MetaTypeCode 라는 Enum 클래스를 만들어 관리해보자

public enum MetaTypeCode {
    ENUM_A_CODE("EnumA 타입", "EnumA 설명", EnumA.class),
    ;

    private String type;
    private String desc;
    private Class<? extends MetaCode> cls;

    MetaTypeCode(String type, String desc, Class<? extends MetaCode> cls) {
        this.type = type;
        this.desc = desc;
        this.cls = cls;
    }

    public static MetaTypeDto buildMetaTypeDto(MetaTypeCode code) {
        return MetaTypeDto.builder()
                .type(code.getType())
                .desc(code.getDesc())
                .list(MetaCode.buildMetaDetailTypeDto(code.getCls()))
                .build();
    }
}

기존 코드 리팩토링

MetaTypeCode 를 순회하면서 buildMetaTypeDto 를 실행시킨다.

private List<MetaTypeDto> metaTypeList = Lists.newArrayList();

private void init() {
    Arrays.stream(MetaTypeCode.values())
        .forEach(code -> metaTypeList.add(MetaTypeCode.buildMetaTypeDto(code)));
}
반응형

댓글