[자바의 정석] 열거형(enum)

June·2021년 1월 6일
0

자바

목록 보기
32/36

자바의 타입에 안전한 열거형에서는 실제 값이 같아도 타입이 다르면 조건식의 결과가 false가 된다. 이처럼 값뿐만 아니라 타입까지 체크하기 때문에 타입에 안전하다고 하는 것이다.

열거형의 정의와 사용

enum 열거형이름 {상수명1, 상수명2, ...}

이 열거형에 정의된 상수를 사용하는 방법은 '열거형이름.상수명'이다.

열거형 상수간의 비교에는 '=='를 사용할 수 있다. equals()가 아닌 '=='로 비교가 가능하다는 것은 그만큼 빠른 성능을 제공한다는 얘기다. 그러나 '<','>'와 같은 비교연산자는 사용할 수 없고 compareTo()는 사용가능하다. compareTo()는 두 비교대상이 같으면 0, 왼쪽이 크면 양수, 오른쪽이 크면 음수를 반환한다.

열거형 Direction에 정의된 모든 상수를 출력하려면

Direction[] dArr = Direction.values();

for (Direction d : dArr) {
	System.out.printf("%s = %d%n", d.name(), d.ordinal());
}

values()는 열거형읨 모든 상수를 배열에 담아 반환한다. 이 메서드는 모든 열거형이 갖고 있는 것으로 컴파일러가 자동으로 추가해 준다. 그리고 ordinal()은 모든 열거형의 조상인 java.lang.Enum 클래스에 정의된 것것으로, 열거형 상수가 정의된 순서를 정수로 반환한다.

Enum 클래스에 정의된 ordinal()이 열거형 상수가 정의된 순서를 반환하지만, 이 값을 열거형 상수의 값으로 사용하지 않는 것이 좋다. 이 값은 내부적은 용도로만 사용되기 위한 것이기 때문이다.

열거형 상수의 값이 불연속적인 경우에는 다음과 같이 열거형 상수의 이름 옆에 원하는 값을 괄호()와 함께 적어주면 된다.

enum Direction {EAST(1), SOUTH(5), WEST(-1), NORTH(10)}

그리고 지정된 값을 저장할 수 있는 인스턴스 변수와 생성자를 새로 추가해 주어야 한다.

enum Direction {
	EAST(1, ">"), SOUTH(5, "V"), WEST(-1, "<"), NORTH(10, "^");	//끝에 ';'를 추가해야 한다.
    private final int value;	//정수를 저장할 필드(인스턴스 변수) 추가
    private final String symbol;
    
    Direction(int value, String symbol) {	//생성자 추가
    	this.value = value;
        this.symbol = symbol;
    }
    
    public int getValue() {
    	return value;
    }
    
    public String getSymbol() {
    	reutrn symbol;
    }
}

열거형의 생성자는 제어자가 묵시적으로 private이다.

열거형의 이해

enum Direction {EAST, SOUTH, WEST, NORTH}

사실은 열거형 상수 하나하나가 Direction 객체이다. 위의 문장을 클래스로 정의한다면 다음과 같다.

class Direction {
	static final Direction EAST = new Direction("EAST");
    static final Direction SOUTH = new Direction("SOUTH");
    static final Direction WEST = new Direction("WEST");
    static final Direction NORTH = new Direction("NORTH");
    
    private String name;
    
    private Direction(String name) {
    	this.name = name;
    }
}

Direction클래스의 static상수 EAST, SOUTH, WEST, NORTH의 값은 같은 객체의 주소이고, 이 값은 바뀌지 않는 값이므로 '=='로 비교가 가능한 것이다.

valueOf 메소드

valueOf() 메소드
valueOf() 메소드는 전달된 문자열과 일치하는 해당 열거체의 상수를 반환합니다.

출처: http://tcpschool.com/java/java_api_enum

사용 예

public enum HttpMethod {
    GET("GET"), POST("POST"), PUT("PUT"), PATCH("PATCH"), DELETE("DELETE");

    private String httpMethod;

    HttpMethod(String httpMethod) {
        this.httpMethod = httpMethod;
    }

    public static HttpMethod compare(String method) {
        return valueOf(method.toUpperCase());
    }
}
        switch (HttpMethod.compare(method)) {
            case GET:
                todoHttpMethods.handleBasicGetMethod(path, exchange, taskMap);
            case POST:
                todoHttpMethods.handleGetMethodWithParameter(exchange, body, taskMap);
            case PUT:
                todoHttpMethods.handlePutMethod(path, exchange, body, taskMap);
            case PATCH:
                todoHttpMethods.handlePatchMethod(path, exchange, body, taskMap);
            case DELETE:
                todoHttpMethods.handleDeleteMethod(path, exchange, taskMap);
        }

자바의 신

enum

enum 생성자는 package-private이나 priavte만 가능하다. 컴파일할 때 생성자를 자동으로 넣어준다.

enum 클래스의 경우 변경되면 자바 프로그램을 수정 후 다시 컴파일해서 실행 중인 자바 프로그램을 중지했다가 다시 시작해야 한다는 단점이 있다.

java.lang.Enum

enum 클래스는 무조건 java.lang.Enum 클래스의 상속을 받는다. 컴파일러가 알아서 추가하는 것이다.

enum 클래스의 부모에는 생성자가 protected로 선언되어 있다. Enum(String name, int ordinal)로 되어있는데 name은 상수의 이름이고, ordinal은 순서이며 상수가 선언된 순서대로 1씩 증가한다.

Enum클래스의 부모는 Object 클래스이기 때문에 Object 메서드들을 사용할 수 있지만 4개는 override 하지 못하게 막아놨다.

  • clone()
  • finalize()
  • hashCode()
  • equals()

Object 클래스의 메소드를 Overriding한 마지막 메소드는 toString 메소드인데, 기본적으로 enum 변수에 이 메소르르 호출하면 상수 이름을 출력한다. toString() 메서드는 Object 클래스 메소드 중에서 유일하게 final로 선언되어 있지 않다.

강의

핵심

  1. enum이 뭔지 알고 있다.
  2. enum을 활용해 조건문을 제어할 수 있다

상수로 하면 단점

public class StringCalculator {
    public static final String PLUS = "+";
    public static final String MINUS = "-";
    public static final String MULTIPLY = "*";
    public static final String DIVIDE = "/";
    
    public static int execute(String symbol, int a, int b) {
        if (PLUS.equals(symbol) {
            return a + b;
        }
        if (MINUS.equals(symbol) {
            return a + b;
        }
        if (MULTIPLY.equals(symbol) {
            return a + b;
        }
        if (DIVIDE.equals(symbol) {
            return a + b;
        }
        throw new IllegalArgumentException();
    }
}

추가 요구사항이 생길 때마다 내부코드가 수정이 발생한다. 예를들어 PLUS만 구현되어있을 떄 MINUS를 구현하려고하면 if문과 상수를 추가해야 한다. 필요한 기능이 생길 때마다 조건문이 추가될 것이다.

또 없는 값을 넣으면 예외 처리를 해줘야 한다.

enum으로 변경

Enum은 상수 이상의 역할을 담당한다. 행위와 상태를 같은 위치에서 관리해서 응집도가 높아진다. 또한 싱글톤이다(?)

public class StringCalculator {
    public static final String PLUS = "+";
    public static final String MINUS = "-";
    public static final String MULTIPLY = "*";
    public static final String DIVIDE = "/";
    
    public static int execute(Operator operator, int a, int b) {
        if (operateor == Operator.PLUS) {
            return a + b;
        }
        if (operateor == Operator.MINUS) {
            return a - b;
        }
        if (operateor == Operator.MULTIPLY) {
            return a * b;
        }
        if (operateor == Operator.DIVIDE) {
            return a / b;
        }
        throw new IllegalArgumentException();
    }
}

StringCalculatorTest

@Test
void add() {
    int result = StringCalculator.execute(Operator.PLUS, 1, 2);

이렇게 코드를 하면 호출하는 쪽에서 예상하지 않는 심볼을 던질 확률은 없어진다.

2차 개선

PLUS를 알아야할까? 문자열로 넣고 싶을 수도 있다.
Operator에서 문자열을 받으면 Operator로 변환해주자.

Operator

public enum Operator {
    PLUS("+"),
    MINUS("-"),
    MULTIPLY("*"),
    DIVIDE("/"),
    ;
    
    private final String symbol;
    
    Operator(final String symbol) {
        this.symbol = symbol;
    }
    
    public static Operator of(String symbol) {
        return Arrays.stream(values())
            .filter(it -> it.symbol.equals(symbol))
            .findAny()
            .orElseThrow(() -> new UnsupportedOperationException("지원하지 않는 연산입니다.");
    }

StringCalculator

public class StringCalculator {
    
    pulblic static int execute(String symbol, int a, int b) {
        return execute(Operator.of(symbol), a, b);
    }
    
    public static int execute(Operator operator, int a, int b) {
        if (operator == Operator.PLUS) {
            return a + b;
        }
        if (operator == Operator.MINUS) {
            return a + b;
        }
        if (operator == Operator.MULTIPLY) {
            return a + b;
        }
        if (operator == Operator.DIVIDE) {
            return a + b;
        }
        throw new IllegalArgumentException();
    }
}

강의 코드 보고

else를 쓰지 말라는 이유?
기능이 추가될 때마다 메서드 내의 구현이 변화한다. 다형성, 추상화를 통해서 문제를 해결해보라는 뜻이다.

위의 if나 switch문을 없애보자.

3차 개선

Operator

public enum Operator {
    PLUS("+") {
        @Override
        int execute(int a, int b) {
            return a+b;
        }
    },
    MINUS("-") {
        @Override
        int execute(int a, int b) {
            return a-b;
        }
    },
    MULTIPLY("*") {
        @Override
        int execute(int a, int b) {
            return a*b;
        }
    },
    DIVIDE("/") {
        @Override
        int execute(int a, int b) {
            return a/b;
        }
    },
    ;
    
    private final String symbol;
    
    abstract int execute(int a, int b);
    
    Operator(final String symbol) {
        this.symbol = symbol;
    }
    
    public static Operator of(String symbol) {
        return Arrays.stream(values())
            .filter(it -> it.symbol.equals(symbol))
            .findAny()
            .orElseThrow(() -> new UnsupportedOperationException("지원하지 않는 연산입니다.");
    }

StringCalculator

public class StringCalculator {
    
    pulblic static int execute(String symbol, int a, int b) {
        return execute(Operator.of(symbol), a, b);
    }

    public static int execute(Operator operator, int a, int b) {
        operator.execute(a, b);
    }
}

다형성으로 극복한 예시다.

4차 개선

람다를 사용해보자.

Operator

public enum Operator {
    PLUS("+", (a,b) -> a + b),
    MINUS("-", (a,b) -> a - b),
    MULTIPLY("*", (a,b) -> a * b),
    DIVIDE("/", (a,b) -> a / b),
    ;
    
    private final String symbol;
    private final IntBinaryOperator operator; //람다를 받는 함수형 인터페이스?
    
    Operator(final String symbol, IntBinaryOperator operator) {
        this.symbol = symbol;
        this.operator = operator;
    }
    
    int execute(int a, int b) {
        return operator.applyAsInt(a, b);
    }
    
    public static Operator of(String symbol) {
        return Arrays.stream(values())
            .filter(it -> it.symbol.equals(symbol))
            .findAny()
            .orElseThrow(() -> new UnsupportedOperationException("지원하지 않는 연산입니다.");
    }

싱글톤

개념

Operator

public enum Operator {
    PLUS("+", (a,b) -> a + b),
    MINUS("-", (a,b) -> a - b),
    MULTIPLY("*", (a,b) -> a * b),
    DIVIDE("/", (a,b) -> a / b),
    
    ...
}

어플리케이션에서 클래스 하나당 인스턴스가 하나만 존재한다. 그래서 여기서는 4개가 메모리에 존재한다.

1차 구현

public class SpecialClass {
    private static SpecialClass instance = new SpecialClass();
    
    private SpecialClass() {
    }
    
    public static SpecialClass getInstance() {
        return instance;
    }

이렇게 하면 항상 바로 메모리에 올라가기 때문에 성능이 안좋다.

2차 구현

public class SpecialClass {
    private static SpecialClass instance;
    
    private SpecialClass() {
    }
    
    public static SpecialClass getInstance() {
        if (Object.isNull(instance) {
            instance = new SpecialClass();
        }
        return instance;
    }

이렇게 하면 멀티스레드 환경에서 두 개가 만들어질 수도 있다.

3차 구현

public class SpecialClass {
    private static SpecialClass instance;
    
    private SpecialClass() {
    }
    
    public static synchronized SpecialClass getInstance() {
        if (Object.isNull(instance) {
            instance = new SpecialClass();
        }
        return instance;
    }

이렇게 하면 느리다.

4차 구현

public class SpecialClass {
    private static SpecialClass instance;
    
    private SpecialClass() {
    }
    
    public static synchronized SpecialClass getInstance() {
        if (Objects.isNull(instance)) {
	        synchronized (instance) {
    	        instance = new SpecialClass();
	        }
     }
     
     return instance;
}

이것 역시 조건문과 선언이 나눠져있어서 문제가 있다. 그래서 한번더 감싸줘야한다.

5차 구현

public class SpecialClass {
    private static SpecialClass instance;
    
    private SpecialClass() {
    }
    
    public static synchronized SpecialClass getInstance() {
        synchronized (instance) {
	        if (Objects.isNull(instance)) {
		        synchronized (instance) {
    		        instance = new SpecialClass();
	       	    }
            }
         }
     
     return instance;
    }
}

코드가 매우 지저분하다. 그래서 분리를 해보자.

6차 구현

public class SpecialClass {
    private static SpecialClass instance;
    
    private SpecialClass() {
    }
    
    public static synchronized SpecialClass getInstance() {
        return Holder.instance;
    }
}

static class Holder {
    private static SpecialClass instance = new SpecialClass();
}

7차 구현

public enum SpecialClass {
    
    INSTACE
    ;
    
    public int some() {
        return 1;
    }

enum을 쓰면 자바 언어레벨 싱글톤 보장 가능하다.

네오의 생각
사실 이정도는 개발자의 자기만족이다. 실무에서 클래스가 두개 세개 생긴다고 해서 그렇게 성능이 차이나지도 않고 뭐라하는 사람도 없다.

결론은 enum은 싱글톤이므로, setter를 통해서 값을 바꾸면 다른 곳에서 사용되다가 문제가 생길 수 있다.

enum에서 상수 사용하는 방법

Enum에서 상수를 사용하는 방법

0개의 댓글