백엔드 개발자 블로그

Enum 본문

Java

Enum

backend-dev 2024. 4. 29. 21:00

Enum 클래스란?

  • 연관된 상수들의 집합이다. (public static final 형태로 사용)
  • 각 엘리먼트는 대부분 대문자 형태로 정의하며 중복되지 않아야 한다.

Enum을 사용하는 이유

특정 클래스에 상수형 필드를 정의하는 것에 문제점들이 존재하기에 사용한다. 

이펙티브 자바 아이템 34의 내용에서 제기된 문제점들

  • 타입 안전을 보장할 방법이 없으며 표현력도 좋지 않다. 오렌지를 건네야 할 메서드에 사과를 보내고, 동등 연산자로 비교하더라도 컴파일러는 아무런 경고 메시지를 출력하지 않는다.
  • 상수의 값이 바뀌면 클라이언트도 반드시 다시 컴파일해야 한다. 다시 컴파일하지 않은 클라이언트는 실행이 되더라도 엉뚱하게 동작할 것이다.
  • 정수 상수문자열출력하기가 다소 까다롭다. 심지어 그 안에 상수가 몇 개 인지도 알 수 없다.
  • 정수 대신 문자열 상수를 사용하여 변형하는 패턴도 있지만, 상수의 의미를 출력할 수 있다는 점은 좋지만 문자열 상수의 이름 대신 문자열 값을 그대로 사용하면 오타가 있어도 컴파일러는 확인할 길이 없으니 자연스럽게 런타임 버그가 생긴다.

언제 사용하는게 좋을까?

1. 상수로 표현된 그룹

과일을 표현한다고 해보자.

 

단순 상수형으로 표현한 경우

public class Fruits {
    public static final String APPLE = "apple";
    public static final String BANANA = "banana";
    public static final String ORANGE = "orange";
    public static final String PINEAPPLE = "pineapple";
    public static final String MANGGO = "manggo";
}

public static void main(String[] args) {
		String myFruit = "apple";

		switch(myFruit){
		    case APPLE:
		        System.out.println("this is apple");
		        break;
		    case BANANA:
		        System.out.println("this is banana");
		        break;
		    default:
		        throw new IllegalStateException("Unexpected value: " + myFruit);
		}
}

 

문제점 : 컴파일 타임에는 오류없이 정상적으로 동작하여 실제로 문제가 있어도 넘어간다.

  • 오타 문제 : ORANGE 상수를 사용하는데 실수로 "orang2"로 사용해도 넘어감
  • 중복 문제 : ORANGE에 "apple"값을 중복해서 사용해도 넘어감

 

enum으로 표현한 경우

enum Fruits {
    APPLE("apple"),
    BANANA("banana"),
    ORANGE("orange"),
    PINEAPPLE("pineapple"),
    MANGGO("manggo");

    private String value;

    Fruits(String value) {
        this.value = value;
    }
}

public static void main(String[] args) {
		Fruits myFruit = Fruits.APPLE;

		switch(myFruit){
		    case APPLE :
		        System.out.println("this is apple!!");
		        break;
		    case BANANA :
		        System.out.println("this is banana!!");
		        break;
		    default:
		        throw new IllegalStateException("Unexpected value: " + myFruit);
		}
}
  • enum 클래스 내에서 엘리먼트에 대한 고유 문자열을 가지기 때문에 열거형에서 가지는 문자열의 의미가 명확해졌다.
  • 외부에는 enum type이 노출되기 때문에 중복되거나 오타에 대한 염려를 하지 않아도 된다.
  • enum class 를 사용하는 것만으로 런타임에서 발생할 수 있는 오류를 해결해주므로 더 높은 안전성을 가진다.

 

2. 타입별 다른 연산식

연산식을 사용할 때 나오는 대표적인 예이다.

public enum BasicOperation implements Operation {
    PLUS("+") {
        public double apply(double x, double y) { return x + y; }
    },
    MINUS("-") {
        public double apply(double x, double y) { return x - y; }
    },
    TIMES("*") {
        public double apply(double x, double y) { return x * y; }
    },
    DIVIDE("/") {
        public double apply(double x, double y) { return x / y; }
    };
    private final String symbol;
    BasicOperation(String symbol) {
        this.symbol = symbol;
    }
    @Override public String toString() {
        return symbol;
    }
}

이처럼 열거 타입을 사용하여 구현 로직을 제공할 수도 있다. 열거 타입으로 로직을 제공하게 되면 사용자는 간결하게 사용할 수 있으며 기능을 확장하기에도 용이하다.

3. @Enumurated

데이터베이스에서 열거 타입을 지정할 수 없기에 @Enumurated 애노테이션을 사용한다. 테이블에 바인딩 되는 엔티티에 열거 타입으로 관리되는 필드 위에 해당 애노테이션만 붙여주면 그 필드는 열거 타입으로 관리되면서 데이터베이스에는 필요한 코드값으로 관리할 수 있게 된다.

다음의 Category 열거 타입을 사용하는 Product 엔티티를 살펴보자.

public enum Category {
    CLOTHES("CA0001"),
    SHOES("CA0002"),
    JACKET("CA0003"),
    DRESS("CA0004");

    private String code;

    Category(String code) {
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

@Entity
@Getter
@Setter
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    @Enumerated(EnumType.STRING)   // 요기
    private Category category;
}

 

EnumType에는 ORDINALSTRING 두개의 속성을 선택할 수 있다.

  • ORDINAL : 단순히 열거 타입의 해당 엘리먼트의 순번을 저장
  • STRING은 열거타입의 이름 자체를 저장

 

위 코드에서는 STRING을 사용했으니 실제 데이터베이스에 저장되는 데이터를 확인해보면 열거 타입의 이름이 저장되는 것을 확인할 수 있다.

 

4. @Converter

열거타입의 코드값으로 저장하고 싶을 때는 @Converter를 사용한다.

@Converter
public class EnumConverter implements AttributeConverter<Category, String> {
    private static Logger log = LoggerFactory.getLogger(EnumConverter.class);

    @Override
    public String convertToDatabaseColumn(Category category) {
        return category.getCode();
    }

    @Override
    public Category convertToEntityAttribute(String code) {
        if (code == null) {
            return null;
        }

        try {
            return Category.valueOf(code);
        } catch (IllegalArgumentException e) {
            log.error("failure to convert cause unexpected code [{}]", code, e);
            throw e;
        }
    }
}

위의 코드를 살펴보면 데이터 베이스에 저장할 때(convertToDatabaseColumn)는 Code 값을 리턴하고 반대로 데이터베이스에 저장된 데이터를 추출할 경우(convertToEntityAttribute)에는 코드 값을 사용하여 코드에 해당하는 열거 타입을 리턴하는 것을 확인할 수 있다. converter를 생성하였으니 엔티티에 적용해보자

@Entity
@Getter
@Setter
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    @Convert(converter = EnumConverter.class)
    private Category category;
}

이제 실제 저장되는 데이터를 확인해보면 열거 타입에 해당하는 코드 값이 저장되는것을 확인할 수 있다.


Enum에서 제공하는 메소드

values()

열거된 모든 원소를 배열에 담아 순서대로 반환한다.

for(Category category : Category.values()) {
    System.out.println(category);
}

// 결과값
// CLOTHES
// SHOES
// JACKET
// DRESS
// UNKNOWN

ordinal()

원소에 열거된 순서를 정수 값으로 반환한다.

System.out.println(Category.CLOTHES.ordinal())

// 결과값
// 0

valueOf()

매개변수로 주어진 String과 열거형에서 일치하는 이름을 갖는 원소를 반환한다.

System.out.println(Category.valueOf("CA0001"));

// 결과값
// DRESS

name()

해당 원소의 이름을 반환한다.

System.out.println(Category.DRESS.name());

// 결과값
// DRESS

compareTo()

기준이 되는 원소 기준으로 순서를 비교하여 리턴한다.


Enum 조회는 어떻게 하는게 좋을까?

Arrays.stream

원시 타입일 경우 원시타입에 해당하는 Stream 타입을 반환(ex. IntStream)

public Category findWithArraysStream(String code) {
    return Arrays.stream(values())
                 .filter(accountStatus -> accountStatus.getCode().equals(code))
                 .findAny()
                 .orElse(UNKNOWN);

}

Steams.of

원시 타입일 경우 Stream 내부에 원시 타입 배열로 반환(ex. Stream<int[]>)

public Category findWithStreamOf(String code) {
    return Stream.of(values())
                 .filter(accountStatus -> accountStatus.getCode().equals(code))
                 .findAny()
                 .orElse(UNKNOWN);

}

HashMap

성능상 가장 우수하다고 함

private static final Map<String, Category> descriptions = 
			Collections.unmodifiableMap(Stream.of(values())
        .collect(Collectors.toMap(
            Category::getCode,
            Function.identity())));

public static Category findWithHashMap(String code) {
    return Optional.ofNullable(descriptions.get(code)).orElse(UNKNOWN);
}

참고

'Java' 카테고리의 다른 글

Thread  (0) 2024.05.04
STATIC  (1) 2024.05.02
JIT Compiler  (0) 2024.04.29
G1 GC vs ZGC  (0) 2024.04.29
heap dump 분석하기 (feat. OOM)  (0) 2024.04.29