goodbye

[Java] enum(enumeration) 본문

Java

[Java] enum(enumeration)

goodbye 2021. 7. 27. 22:36

1. Enum 이란?

관련이 있는 상수들의 집합을 의미한다. 자바에서는 final로 String과 같은 문자열이나 숫자들을 나타내는 기본 자료형의 값을 고정할 수 있는데 이렇게 고정된 값을 상수라고 한다. 어떤 클래스가 상수만으로 작성되어 있으면 반드시 class로 선언할 필요가 없기때문에 class 대신 enum이라고 선언하면 이 객체는 상수의 집합인 것을 명시적으로 나타낸다.

 

2. 상수를 정의하는 다양한 방법

Java 1.5 version 부터 추가된 enum이 나오기전에는 다양한 방법으로 상수를 정의했다.

 

a. 주석으로 상수 의미를 전달하는 방법

public class Color {
	public static void main(String[] args) {
    	/*
        하얀색 == 1
        파란색 == 2
        검정색 == 3
        */
        
        int color = 1;
        
        switch(color) {
        case 1:
        	System.out.println("하얀색입니다");
           	break;
        case 2:
        	System.out.println("파란색입니다");
            break;
        case 3:
        	System.out.println("검정색입니다");
            break;
        }
    }
}

위 방법은 주석이 사라지거나 주석 부분과 상수를 사용하는 부분이 분리되면 각각의 숫자들이 어떤것을 의미하는지 이해하기 어려운 단점이 존재한다.


 b. final static 예약어를 이용하는 방법

public class Color {
    private final static int WHITE = 1;
    private final static int BLUE = 2;
    private final static int BLACK = 3;
    
    public static void main(String[] args) {
    	int color = WHITE;
        
        switch(color) {
        case WHITE:
        	System.out.println("하얀색입니다");
           	break;
        case BLUE:
        	System.out.println("파란색입니다");
            break;
        case BLACK:
        	System.out.println("검정색입니다");
            break;
        }
    }
}

 

final 예약어를 사용하여 한번 지정하면 바뀌지 않게 설정하면서 static 예약어를 통해 동시에 메모리에 한번만 할당되도록 설정하는 방법으로 이름이 있어서 무엇을 의미하는지 한눈에 알 수 있는 방법이다. 하지만 필요한 상수가 생길때마다 계속 추가하다보면 상수가 너무 많아지고 각각의 상수의 집합에서 같은 이름으로 정의된 상수가 있다면 중복된 이름에 해당하여 컴파일 단계에서 오류가 발생하는 문제가 발생한다. 이를 해결하기 위한 방법으로 class 또는 인터페이스를 사용하여 각각의 집합끼리 상수가 정의되고 중복된 이름이 있어도 오류가 발생하지 않게 할수는 있다.


c. interface에 상수를 정의하는 방법

interface Color {
    private final static Color WHITE = new Color();
    private final static Color BLUE = new Color();
    private final static Color BLACK = new Color();
}

interface Shapes {
    private final static Shapes CIRCLE = new Shapes();
    private final static Shapes TRIANGLE = new Shapes();
    private final static Shapes SQUARE = new Shapes();
}

public class Example {
    public static void main(String[] args) {
    
    	if(Color.WHITE == shapes.CIRCLE) {
        	System.out.println("같음");
        }else {
        	System.out.println("다름");
        }
        
        Color color = Color.WHITE;
        
        switch(color) {
        case WHITE:
        	System.out.println("하얀색입니다");
           	break;
        case BLUE:
        	System.out.println("파란색입니다");
            break;
        case BLACK:
        	System.out.println("검정색입니다");
            break;
        }
    }
}

interface에 선언된 변수는 public static final 속성을 생략 할 수 있는 특징을 이용하여 코드를 조금 더 간결하게 작성 할 수 있다. 하지만 서로 다른 집합에 정의된 상수들을 서로 비교할 수 없는 단점이 존재한다. 즉 다른 집합의 상수를 비교하면 컴파일 단계에서 에러를 확인 할 수 있어야 하지만 위 코드는 확인 할 수 없으며, 런타임 단계에서 예기치 못한 문제를 발생 시킬수 있다.


d. class에 상수를 정의하는 방법

class Color {
    private final static Color WHITE = new Color();
    private final static Color BLUE = new Color();
    private final static Color BLACK = new Color();
}

class Shapes {
    private final static Shapes CIRCLE = new Shapes();
    private final static Shapes TRIANGLE = new Shapes();
    private final static Shapes SQUARE = new Shapes();
}

public class Example {
    public static void main(String[] args) {
    
    	if(Color.WHITE == Shapes.CIRCLE) {
        	System.out.println("같음");
        }else {
        	System.out.println("다름");
        }
        
        Color color = Color.WHITE;
        
        switch(color) {
        case WHITE:
        	System.out.println("하얀색입니다");
           	break;
        case BLUE:
        	System.out.println("파란색입니다");
            break;
        case BLACK:
        	System.out.println("검정색입니다");
            break;
        }
    }
}

interface로 작성된 상수들의 집합을 class로 바꾸고 각각의 상수들의 타입을 자신의 상수 집합의 이름으로 지정하여 자기 자신을 인스턴스화 한 값을 할당한다. 즉 각각의 상수들의 서로 다른 데이터를 갖는것을 의미하고, 같은 집합의 상수들은 같은 데이터 타입을 갖는다. 정리하면 상수들의 데이터 타입은 같지만 서로 다른 데이터 값을 가지고 있다. 

 

이 경우에도 서로 다른 데이터 타입은 비교 할 수 없다는 에러가 컴파일 단계에서 발생한다. 런타임에서 발생 할 수 있는 에러를 컴파일 단계에서 검출하도록 수정하여 예기치 못한 오류를 사전에 차단 할 수 있지만 switch문 조건에 들어가는 데이터 타입이 제한적이기 때문에 switch문에는 사용하지 못한다. 이러한 문제점을 enum은 해결 할 수 있다

 

3. 통칭 정수 열거 패턴(int enum pattern)의 단점

enum이 등장하기 이전에 상수값 관리를 static final로 불변의 상수값을 만들어 사용하고 네이밍 규칙은 대문자로 하며, 변수명을 의미있고 다른 상수들과 구분지을수 있도록 지어야 했다. 이러한 상수값 관리는 타입 안전을 보장할 수 없으며, 표현련도 좋지 않다. 즉 자바가 정수 열거 패턴을 위한 별도 이름공간(namespace)를 지원하지 않기 때문에 언더바(_)를 사용하여 접두어로 이름 충돌을 방지한다. 이러한 방식의 가장 큰 단점 중 하나는 정수 열거 패턴을 사용한 상수값(constant)은 컴파일을 하면 그 값이 클라이언트 파일에 그대로 새겨지므로, 상수 값이 바뀌면 컴파일을 다시 해야한다.

이러한 열거 패턴의 단점을 극복하기 위해서 열거 타입(Enum Type)이 생겨났는데 Enum은 int로 선언한 상수와 성능면에서 비슷하나, 자료형을 메모리에 올리고 초기화하는 시/공간적 비용때문에 약간 손해를 보지만 코드의 가독성이 높아지며 강력한 기능을 제공한다. 

 

4. enum을 사용하는 이유

enum은 열거형으로 불리며 서로 연관된 상수들의 집합을 의미한다. Java 1.5 version 부터 추가되었다. 

 

  a. 구현의 의도가 열거임을 분명하게 알 수 있다(키워드 enum)  

  b. 인스턴스 생성과 상속을 방지함으로 상수의 타입안정성이 보장된다

  c. 정의한 타입이외의 타입을 가진 테이터값을 컴파일시 체크한다(enum 클래스를 사용해 새로운 상수타입 정의)

  

5. enum 클래스의 원소에 추가 속성 부여

public enum Fruits {
    APPLE("1"),
    ORANGE("2"),
    BANANA("3");
    
    private String fruits;
    
    Fruits(String fruits) {
    	this.fruits = fruits;
    }
}

enum의 각 열거형 상수에 추가 속성을 부여 할 수 있다. 생성자의 파라미터를 통해 추가 속성을 enum클래스의 필드에 설정해주고, getter메소드를 통해 해당 속성을 필요할 때에 가져다 쓸 수 있게 한다. 이처럼 메소드나 필드를 enum 타입에 추가하면 enum 상수에 어떤 데이터를 연관 시킬 수 있다. 열거형 상수를 모아놓는 간단한 형태에서 시작하지만, 끊임없이 진화하여 완벽한 추상체가 될 수 있는 것이다.

 

6. enum의 생성자는 private가 default 이다

Java에서 enum타입은 열거형을 의미하는 특별한 형태의 클래스이다. 그렇기 때문에 일반 클래스와 같이 생성자가 있어야 한다. 물론 생성자를 만들어주지 않아도 Java가 default 생성자를 만들어주긴 하지만, enum의 경우에는 다른 클래스들과 달리 일반적으로 생성자의 접근제어자를 private로 지정해야한다. enum타입의 생성자를 일반적인 클래스의 생성자와 같이 public로 설정하거나 protected로 하게 되면 아래와 같은 에러가 발생한다.

Illegal modifier for the enum constructor, only private is permitted.

왜 생성자를 private로 지정하는 것일까? 그 이유는 enum은 서로 연관된 고정된 상수의 집합이기 때문이다. 즉, 외부에서 객체를 생성하지 못하게 막으며 생성자를 통해 값을 초기화하거나 동적으로 변환하지 못하게 되는것이다.

 

enum타입은 고정된 상수들의 집합으로써, 런타임이 아닌 컴파일타임에 모든값을 알고 있어야 한다. 즉 다른 패키지나 클래스에서 enum 타입에 접근해서 동적으로 어떤 값을 정해줄 수 없다. 따라서 컴파일 시 타입안정성이 보장된다. 즉 해당 enum 클래스내에서까지도 new 키워드로 인스턴스 생성이 불가능하며 newInstance(), clone() 등의 메소드도 사용이 불가능하다. 이때문에 생성자의 접근제어자를 privae으로 설정해야 한다. 이렇게 되면 외부에서 접근가능한 생성자가 없으므로 enum타입은 실제적으로 final과 다름이 없다. 클라이언트에서 enum의 인스턴스를 생성할 수 없고 상속을 받을 수도 없으므로, 클라이언트의 관점에서 보면 인스턴스는 없지만 선언된 enum 상수는 존재하는 셈이다. 결국 enum 타입은 인스턴스 생성을 제어하며, 싱글톤을 일반화한다. 이러한 특성때문에 enum타입은 싱글톤을 구현하는 하나의 방법으로 사용되기도 한다.

 

 

7. String을 enum 타입으로 변환후 비교

switch문의 조건에 String 타입을 넣게되면, case 문에서 enum 타입으로 비교할수가 없다. 그래서 문자열로 바꾸기 위해서 toString() 메서드를 쓰면 상수표현시깅 필요하다는 문구가 나온다. 사실상 여기서 toString() 메서드를 쓰는게 의미가 없는데 그 이유는 toString() 은 상수이름을 문자열로 반환하기 때문이다. 

 

이 문제를 해결하기 위해서 enum 클래스에서 static method인 values() 메서드를 사용한다. values() 메서드는 열거된 모든 원소를 배열에 담아 순서대로 반환한다. 

public enum Fruits {
    APPLE("1"),
    ORANGE("2"),
    BANANA("3");
    
    private String fruits;
    
    Fruits(String fruits) {
    	this.fruits = fruits;
    }
    
    public static Fruits findByFruits(String fruits) {
    	for(Fruits fruit : Fruits.values()) {
        	if(fruit.equals(fruits)) {
            	return fruit;
            }
        }
        throw new RuntimeException();
    }
}
@Controller
@RequriedArgsConstructor
@RequestMapping("/fruits")
public class FruitsController {

	@GetMappping
    public String list(@ModelAttribute("fruitsVo") FruitsVo FruitsVo, RedirectAttrubutes redirectAttributes) {
    	boolean flag = false;
        switch (findByFruits(fruitsVo.getFruits())) {
        	case BANANA :
            	flag = true;
                break;
            }
            
            if(!flag) {
                redirectAttributes.addFlashAttribute("message", "바나나가 아닙니다.");
                return "redirect://fruits.admin";
            }
            return "/fruits.admin";
        }
    }

 

8. 마무리

정수 열거 패턴(int enum pattern)을 사용하면 상수 값이 바뀌게 될 경우 컴파일도 다시 해야 클라이언트에 있는 상수값도 바뀌게 된다. 하지만 열거타입(Enum Type)을 사용하게 되면 열거타입을 삭제할 경우, 제거한 상수를 참조하지 않는 클라이언트에게는 아무런 영향이 없다. 그리고 제거된 상수를 참조하는 클라이언트에서는 다시 컴파일 하게 될 경우 제거된 상수를 참조하는 줄에서 디버깅에 유용한 메세지를 담은 컴파일 오류가 발생하고, 클라이언트를 다시 컴파일 하지 않을 경우 런타임에 같은 줄에서 유용한 예외가 발생할 것이다.

 

즉 요약하면 열거타입을 사용할때 각 상수를 특정 데이터와 연결짓기 위해서는 생성자와 메서드를 사용해야한다. 또한 하나의 메소드가 상수별로 다르게 동작해야 하는 경우에는 switch문보다 상수별 메서드 구현을 사용하는게 좋고, 열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하는게 좋다. 

 

Reference

Effective Java3
https://docs.oracle.com/javase/7/docs/api/java/lang/Enum.html
https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html
http://www.javapractices.com/topic/TopicAction.do?Id=1
https://opentutorials.org/module/516/6091
https://www.nextree.co.kr/p11686/
https://webdevtechblog.com/enum%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%83%81%EC%88%98%EA%B0%92-%EA%B4%80%EB%A6%AC-a3e3fb73eae1

 

 

'Java' 카테고리의 다른 글

Optional -1  (1) 2022.12.26
Java Stream & Function API  (0) 2022.12.21
Concurrency solution -1  (0) 2022.10.22
추상클래스와 인터페이스  (0) 2021.12.12
좋은 객체 지향 설계의 5가지 원칙 - SOLID  (0) 2021.10.25
Comments