Java

[Java] Enum 뿌리부터 시작하여 개념 이해하기

BE_ranny 2024. 8. 16. 21:14

Introduction

개발 공부를 하면서 enum을 자주 사용하는 것을 보았다. enum이 편리하고 유용하다는 것은 알겠는데, 특징이나 사용법을 공부해도 왜 enum을 사용하는지 잘 이해되지 않았다.

 

어떤 이유로 enum을 사용하고, 언제 사용하는 걸까? 에 대한 의문이 풀리지 않았다. 

 

그런데 enum의 뿌리를 탐구하다 보니, 이해가 되기 시작했다. enum이 등장하기 전에는 상수를 어떻게 정의했는지,  enum을 사용하면 어떤 점이 더 편리한지를 정리하려고 한다.


Enum이란?

 

미리 정의된 상수들의 특별한 집합이다. enum은 enumeration 혹은 enumerate type의 줄임말로 계산, 열거라는 영어단어의 앞부분만 따서 만든 예약어이다.

 

먼저 Enum은 프로그래밍을 하다보면 배열이나, 리스트 등 여러 개의 묶음 데이터를 다루는 일이 빈번하다. 이 묶음 데이터 중에는 주제에 따라 한정된 값만 가지는 경우가 존재한다. 대표적으로 '계절'이나 '주사위' 같은 예시를 들 수 있다. 계절은 봄, 여름, 가을, 겨울 4가지로 한정되어 있고, 주사위는 1,2,3,4,5,6으로 6개로만 구성되어 있다.

 

이와 같이 정해져 있는 한정된 데이터 묶음을 열거형 타입인 Enum으로 묶어주면 보다 구조적인 프로그래밍을 할 수 있다.

 

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

 

Enum가 나오기 이전에는 상수를 어떻게 정의했을까?

final 상수

final 제어자를 한번 지정하면 바뀌지 않게 설정되고, static을 사용하여 메모리에 한번만 할당된다. 하지만 이 방법은 접근제어자들 때문에 가독성이 좋지 못하다는 단점이 있다.

public class EnumExample {
	private final static int SPRING = 1;
	private final static int SUMMER = 2;
	private final static int AUTUMN = 3;
	private final static int WINTER = 4;
	
	 public static void main(String[] args) {

        int season = EnumExample.SPRING;

        switch (season) {
            case EnumExample.SPRING:
                System.out.println("봄 입니다.");
                break;
            case EnumExample.SUMMER:
                System.out.println("여름 입니다.");
                break;
            case EnumExample.AUTUMN:
                System.out.println("가을 입니다.");
                break;
            case EnumExample.WINTER:
                System.out.println("겨울 입니다.");
                break;
        }
    }
}

 

 

인터페이스 상수

interface는 반드시 추상 메소드만 선언할 수 있는 것은 아니다. 인터페이스 내에서도 상수를 선언할 수 있는데, 인터페이스 멤버는 public static final 속성을 생략할 수 있는 특징을 사용해 코드를 조금 더 간결하게 작성할 수 있다.

interface SEASON {
    int SPRING = 1;
    int SUMMER = 2;
    int AUTUMN = 3;
    int WINTER = 4;
}

interface DAY {
    int MONDAY = 1;
    int TUESDAY = 2;
    int WEDNESDAY = 3;
    int THURSDAY = 4;
    int FRIDAY = 5;
    int SATURDAY = 6;
    int SUNDAY = 7;
}

public static void main(String[] args) {

    int day = DAY.MONDAY;

    // 상수를 비교하는 논리적으로 잘못된 행위를 함으로써 day 변수에 다른 상수값이 들어가버림
    if (DAY.MONDAY == SEASON.SPRING) {
        day = SEASON.SPRING;
    }

    // day 변수에 있는 상수는 SEASON 상수이기 때문에 조건문에서 걸러져야 되지만,
    // 결국 정수값이기 때문에 에러가 안나고 그대로 수행됨 -> 프로그램 버그 발생 
    switch (day) {
        case DAY.MONDAY:
            System.out.println("월요일 입니다.");
            break;
        case DAY.TUESDAY:
            System.out.println("화요일 입니다.");
            break;
        case DAY.WEDNESDAY:
            System.out.println("수요일 입니다.");
            break;
    }
}

 

하지만 인터페이스로 상수를 정의하는 방법도 결국 문제가 있다. 위 집합처럼 정의된 상수는 서로 비교하면 안 된다는 점이다. 인터페이스에서 정의된 상수 값들이 중복되어 있지만, 결국은 정수값이기 때문에 컴파일 에러 없이 실행이 가능하다. 규모가 작을 때는 문제가 없을 수 있겠지만, 프로그램 크기가 커지면 제약적이지 않은 요소 때문에 예기치 못한 문제를 발생시킬 수 있다.

 

 

자체 클래스 상수

위의 문제점들로 상수를 정수값으로 구성하는 것이 아닌 독립된 고유의 객체로 선언하고자 자체 클래스 인스턴스를 이용해 상수처럼 사용하는 방법도 나왔다. 자기 자신 객체를 인스턴스화 하고 final static 함으로써 고유의 객체를 얻게 된다.

 

하지만 가독성은 다시 안좋아졌다. 그리고 switch문의 조건에 들어가는 타입이 제한적이기 때문에 if문에서는 문제없지만, switch문에서는 사용할 수 없다는 단점이 있다.

//자기 자신 객체를 인스턴스화 하고, final static 하여 고유의 객체 상수를 얻게 됨 
class SEASON {
    public final static SEASON SPRING = new SEASON();
    public final static SEASON SUMMER = new SEASON();
    public final static SEASON AUTUMN = new SEASON();
    public final static SEASON WINTER = new SEASON();
}

class DAY {
    public final static DAY MONDAY = new DAY();
    public final static DAY TUESDAY = new DAY();
    public final static DAY WEDNESDAY = new DAY();
    /*
     * ... 생략 ..
     */
}
public class EnumExample {
	
	public static void main(String[] args) {

	    int day = DAY.MONDAY;

	    // if문에서는 문제 없지만
	    if (DAY.MONDAY == SEASON.SPRING) {
	        day = SEASON.SPRING;
	    }

		// switch문에서는 사용할 수 없음
	    switch (day) {
	        case DAY.MONDAY:
	            System.out.println("월요일 입니다.");
	            break;
	        case DAY.TUESDAY:
	            System.out.println("화요일 입니다.");
	            break;
	        case DAY.WEDNESDAY:
	            System.out.println("수요일 입니다.");
	            break;
                /*
                 * ... 생략 ..
                 */
	    }
	}
}

 

 


Enum 사용해 상수 정의하기

위 문제들처럼 상수 그룹 간의 중복 문제를 피하고, 코드의 가독성과 타입 안정성을 높이기 위한 방법 중 하나로 Enum을 사용하게 된 것이다.

 

위의 상수를 enum으로 바꾸면 아래와 같다.

enum SEASON{
	SPRING, SUMMER, AUTUMN, WINTER;
}

enum MONTH{
	JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, 
	AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER;
}

 

enum의 핵심은 이러한 상수를 객체 지향적으로 객체화해서 관리하자는 의미이다. enum은 인터페이스 동일하게 독립된 특수한 클래스로 구분된다.

 

일종의 객체이기 때문에 힙(heep) 메모리에 저장되고, 각 enum 상수들은 별개의 메모리 주소값을 가지기 때문에 독립된 상수로 구성될 수 있다.

 

Enum 장점

  • 코드가 단순해지고 가독성이 좋아진다
  • 허용 가능한 값들을 제한하여 유형 안전(type safe)을 제공한다
  • IDE의 지원을 받을 수 있다. (자동 완성, 오타 검증, 텍스트 리팩토리 등)
  • 리팩토리 시 변경 범위가 최소화 된다. 내용을 추가해도 enum 코드만 수정하면 된다
  • 문맥(context)을 담을 수 있다

Enum 활용 방법

if문 줄이기

어떤 값에 대한 반대의 값을 반환하여 수행하는 코드를 작성하는 경우를 사용하여 if문 사용을 자제해 보겠다.

 

예를 들어, 스위치 ON/OFF 하는 예제를 사용해 보겠다. ON이면 OFF 하고, OFF이면 ON을 한다. 이럴 때 if문을 통해 코드를 작성하는데, if ~ else구문이 많아지게 되면 코드가 지저분해져 가독성이 떨어진다. 그래서 enum을 활용하여 if ~ else구문을 최대한 줄여보고, 가독성 있게 코드를 작성해보려고 한다.

 

public enum PowerSwitch {
	ON("켜짐"),
	OFF("꺼짐");
	
	private String krName;
	
	private PowerSwitch() {
		//
	}

	public String getKrName() {
		return krName;
	}

	public void setKrName(String krName) {
		this.krName = krName;
	}
	
	public PowerSwitch opposite() {
		if(this == PowerSwitch.ON) {
			return PowerSwitch.OFF;
		} else {
			return PowerSwitch.ON;
		}
	}
}

 

enum을 통해 ON, OFF으로 각각 상수를 정의했다. opposite() 메서드 호출을 ON이나 OFF에 해당하는 반대의 값을 반환받을 수 있다. 

public class PowerSwitch {
	public static void main(String[] args) {
		PowerSwitch powerSwitch = PowerSwitch.ON;
		displayByPowerSwitch(powerSwitch.opposite());
	}
	
	public static void displayByPowerSwitch(PowerSwitch powerSwitch) {
		if(powerSwitch == PowerSwitch.ON) {
			System.out.println("전원을 on 한다.");
		} else {
			System.out.println("전원을 off 한다.");
		}
	}
}

 

직점 powerSwitch 변수에 ON을 할당했다. 그리고 powerSwitch 변수를 통해 opposite() 메서드를 호출하면 OFF 상수를 반환한다. 만약 opposite() 메소드가 없다면 if문으로 사용하여 powerSwitch 변수에 있는 값을 확인하는 작업을 수행해야 한다. 하지만 opposite() 메소드를 호출함으로써 한번에 알 수 있다. displayByPowerSwitch() 메소드를 통해 ON 또는 OFF에 따른 작업을 수행한다. enum을 통해 메인 메서드에서는 if문을 사용하지 않고도 간단하게 on/off 하는 작업을 구현할 수 있다.

 

결론

enum의 뿌리부터 찾아 공부하니 왜 enum이 탄생하게 되었으며, 어떻게 사용하는지 이해하게 되었다. 이렇게 이해한 enum을 바탕으로 다음 포스팅에서는 내가 개발한 코드에서는 어떻게 적용했는지 작성하도록 하겠다.

 

Reference

https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html?ref=nextree.co.kr

https://opentutorials.org/module/516/6091?ref=nextree.co.kr

https://velog.io/@red-sprout/Java-enum은-왜-쓰는걸까-feat.-우아한형제들-기술블로그