Java - 상수와 enum

Yuri Lee·2020년 10월 7일
0

상수

상수는 변하지 않는 값이다. 아래에서 좌항이 변수이고 우항이 상수이다.

package com.yuri.javatutorials.constant2;

public class ConstantDemo {
	public static void main(String[] args) {
		/*
		 * 1. 사과 2. 복숭아 3. 바나나
		 */
		int type = 1;
		switch (type) {
		case 1:
			System.out.println(57);
			break;
		case 2:
			System.out.println(34);
			break;
		case 3:
			System.out.println(93);
			break;
		}
	}

}

/*
57
*/

위 코드에서 숫자 1에 해당하는 과일은 언제나 사과여야 한다. 그러므로 변하지 않는 값인 상수값에 따라서 그 값에 해당하는 과일의 의미를 고정하고 있다. 그런데 주석으로 상수의 의미를 전달하고 있지만 주석이 없어졌거나, 주석이 상수를 사용하는 코드와 멀어진다면 각 숫자에 해당하는 과일이 무엇을 나타내는지 알아보기거 어렵거나 불가능해질 수 있다.

이런 때는 이름이 있다면 더 좋을 것이다. 변수도 상수가 될 수 있다. 변수를 지정하고 그 변수를 final로 처리하면 한번 설정된 변수의 값은 더 이상 바뀌지 않는다. 또한 바뀌지 않는 값이라면 인스턴스 변수가 아니라 클래스 변수(static)로 지정하는 것이 더 좋을 것이다.

package com.yuri.javatutorials.constant2;

public class ConstantDemo {
	private final static int APPLE = 1; //전역변수
	private final static int PEACH = 2;
	private final static int BANANA = 3;

	public static void main(String[] args) {
		int type = APPLE;
		switch (type) {
		case APPLE:
			System.out.println(57 + " kcal");
			break;
		case PEACH:
			System.out.println(34 + " kcal");
			break;
		case BANANA:
			System.out.println(93 + " kcal");
			break;
		}
	}
}

private final static int APPLE = 1; //전역변수
private은 여기서는 별 의미가 없다. final이 전역변수를 선언해준다는 것을 기억
static은 이 ConstantDemo 클래스의 변수라는 것이다.
왜냐하면 apple이 1, peach가 2, banana가 3이라는 것은 인스턴스마다 달라질 필요가 있는 정보가 아니기 때문이다. 따라서 그냥 클래스 소속으로 한 것이다.

그런데 프로그램이 커지면서 누군가 기업에 대한 상수가 필요해졌다. 해서 아래와 같이 코드를 변경했다.

package com.yuri.javatutorials.constant2;

public class ConstantDemo {
	// fruit
	private final static int APPLE = 1;
	private final static int PEACH = 2;
	private final static int BANANA = 3;

	// company
	private final static int GOOGLE = 1;
	// private final static int APPLE = 2;
	private final static int ORACLE = 3;

	public static void main(String[] args) {
		int type = APPLE;
		switch (type) {
		case APPLE:
			System.out.println(57 + " kcal");
			break;
		case PEACH:
			System.out.println(34 + " kcal");
			break;
		case BANANA:
			System.out.println(93 + " kcal");
			break;
		}
	}
}

과일 APPLE과 기업 APPLE이 서로 같은 이름을 가진다. 이렇게 되면 APPLE의 값이 2에서 1로 바뀐다. 프로그램은 오동작 할 것이다. 다행인 것은 final로 선언했기 때문에 컴파일이 되지 않고 이름이 중복되는 문제를 방지 할 수 있다. 그런데 이미 기업에 대한 상수를 작성했고 이것이 이미 다양한 영역에서 사용되고 있는 상태에서 APPLE을 추가하려면 낭패가 될 것이다. 이러한 문제를 회피하기 위해서 접두사를 붙여보자.

package com.yuri.javatutorials.constant2;

public class ConstantDemo {
	// fruit
	private final static int FRUIT_APPLE = 1;
	private final static int FRUIT_PEACH = 2;
	private final static int FRUIT_BANANA = 3;

	// company
	private final static int COMPANY_GOOGLE = 1;
	private final static int COMPANY_APPLE = 2;
	private final static int COMPANY_ORACLE = 3;

	public static void main(String[] args) {
		int type = FRUIT_APPLE;
		switch (type) {
		case FRUIT_APPLE:
			System.out.println(57 + " kcal");
			break;
		case FRUIT_PEACH:
			System.out.println(34 + " kcal");
			break;
		case FRUIT_BANANA:
			System.out.println(93 + " kcal");
			break;
		}
	}
}

이름이 중복될 확률을 낮출 수 있다. 이러한 기법을 네임스페이스라고 한다. 😀 그런데 상수가 너무 지저분하다. 좀 깔끔하게 바꿀 수 없을까? 이럴 때 인터페이스를 사용할 수 있다.

package com.yuri.javatutorials.constant2;

interface FRUIT {
	int APPLE = 1, PEACH = 2, BANANA = 3;
}

interface COMPANY {
	int GOOGLE = 1, APPLE = 2, ORACLE = 3;
}

public class ConstantDemo {

	public static void main(String[] args) {
		int type = FRUIT.APPLE;
		switch (type) {
		case FRUIT.APPLE:
			System.out.println(57 + " kcal");
			break;
		case FRUIT.PEACH:
			System.out.println(34 + " kcal");
			break;
		case FRUIT.BANANA:
			System.out.println(93 + " kcal");
			break;
		}
	}
}

훨씬 깔끔하다. 접미사(FRUIT, COMPANY)로 이름을 구분했던 부분이 인터페이스로 구분되기 때문에 언어의 특성을 보다 잘 살린 구조가 되었다. 인터페이스를 이렇게 사용할 수 있는 것은 인터페이스에서 선언된 변수는 무조건 public static final의 속성을 갖기 때문이다.

우리 코드는 과일과 기업이라는 두 개의 상수 그룹이 존재한다. 위의 코드는 서로 다른 상수그룹의 비교를 시도했고 양쪽 모두 값이 정수 1이기 때문에 오류를 사전에 찾아주지 못하고 있다. 컴파일러가 이런 실수를 사전에 찾아줄 수 있게 하려면 어떻게 해야 할까?

package com.yuri.javatutorials.constant2;

class Fruit {
	public static final Fruit APPLE = new Fruit();
    // 자기 자신의 인스턴스를 이 상수의 값으로 저장
    // Apple의 값으로 자기 자신을 인스턴스화 한 값을 apple에다가 넣음
	public static final Fruit PEACH = new Fruit();
	public static final Fruit BANANA = new Fruit();
    
    // Apple, peach, banana 는 모두 다른 데이터라는 뜻이다. 왜냐? 다른 인스턴스이기 때문에..! 하지만 이 3개의 값들은 각각 Fruit라고 하는 같은 데이터 타입을 갖는다. 데이터타입은 같지만 각각이 갖는 값은 서로 다른 인스턴스를 갖고 있다는 것이다. 
}

class Company {
	public static final Company GOOGLE = new Company();
	public static final Company APPLE = new Company();
	public static final Company ORACLE = new Company();
}

public class ConstantDemo {

	public static void main(String[] args) {
		if (Fruit.APPLE == Company.APPLE) {
			System.out.println("과일 애플과 회사 애플이 같다.");
		}
	}
}

에러발생! Incompatible operand types Fruit and Company

위 코드 실행결과 다음과 같은 에러가 발생한다.

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
	Incompatible operand types Fruit and Company

	at com.yuri.javatutorials.constant2.ConstantDemo.main(ConstantDemo.java:18)

Fruit와 Company 클래스를 만들고 클래스 변수로 해당 클래스의 인스턴스를 사용하고 있다. 각각의 변수가 final이기 때문에 불변이고, Static이므로 인스턴스로 만들지 않아도 된다. 결과는 Fruit.APPLE == Company.APPLE에서 에러가 발생한다. 서로 다른 카테고리의 상수에 대해서는 비교조차 금지하게 된 것이다. 언제나 오류는 컴파일 시에 나타나도록 하는 것이 바람직하다. 실행 중에 발생하는 오류는 찾아내기가 더욱 어렵다.

그런데 위의 코드는 두 가지 문제점이 있는데 하나는 switch 문에서 사용할 수 없고 또 하나는 선언이 너무 복잡하다는 것이다.

package com.yuri.javatutorials.constant2;

class Fruit {
	public static final Fruit APPLE = new Fruit();
	public static final Fruit PEACH = new Fruit();
	public static final Fruit BANANA = new Fruit();
}

class Company {
	public static final Company GOOGLE = new Company();
	public static final Company APPLE = new Company();
	public static final Company ORACLE = new Company();
}

public class ConstantDemo {
    
    public static void main(String[] args) {
    	Fruit type = Fruit.APPLE;
        switch(type){
            case Fruit.APPLE:
                System.out.println(57+" kcal");
                break;
            case Fruit.PEACH:
                System.out.println(34+" kcal");
                break;
            case Fruit.BANANA:
                System.out.println(93+" kcal");
                break;
        }
    }
}

switch(type){ 이 부분에서 에러가 발생하는데 .. 왜?
switch문을 사용할 때 한가지 주의 할 것은 switch의 조건으로는 몇가지 제한된 데이터타입만을 사용할 수 있다. byte, short, char, int, enum, String, Character, Byte, Short, Integer !

enum

enum은 열거형(enumerated type)이라고 부른다. 열거형은 서로 연관된 상수들의 집합이라고 할 수 있다. 위의 예제에서는 Fruit와 Company가 말하자면 열거인 셈이다.

package com.yuri.javatutorials.constant2;

/*
 	public static final Fruit APPLE = new Fruit();
	public static final Fruit PEACH = new Fruit();
	public static final Fruit BANANA = new Fruit();
    	private Fruit(){}
 */

enum Fruit {
	APPLE, PEACH, BANANA;
}

enum Company {
	GOOGLE, APPLE, ORACLE;
}

public class ConstantDemo {

	public static void main(String[] args) {
		/*
		 * if(Fruit.APPLE == Company.APPLE){ System.out.println("과일 애플과 회사 애플이 같다."); }
		 */
		Fruit type = Fruit.APPLE;
		switch (type) {
		case APPLE:
			System.out.println(57 + " kcal");
			break;
		case PEACH:
			System.out.println(34 + " kcal");
			break;
		case BANANA:
			System.out.println(93 + " kcal");
			break;
		}
	}
}

enum은 class, interface와 동급의 형식을 가지는 단위다. 하지만 enum은 사실상 class이다. 편의를 위해서 enum만을 위한 문법적 형식을 가지고 있기 때문에 구분하기 위해서 enum이라는 키워드를 사용하는 것이다. enum은 많은 곳에서 사용하던 디자인 패턴을 언어가 채택해서 문법적인 요소로 단순화시킨 것텍스트이라고 할 수 있다.

enum을 사용하는 이유

  • 코드가 단순해진다.
  • 인스턴스 생성과 상속을 방지한다.
  • 키워드 enum을 사용하기 때문에 구현의 의도가 열거임을 분명하게 나타낼 수 있다.

enum과 생성자

package com.yuri.javatutorials.constant2;

enum Fruit {
	APPLE, PEACH, BANANA;

	Fruit() {
		System.out.println("Call Constructor " + this); // this 현재 이 각각의 인스턴스들, 생성자가 만들어졌다는 의미 
	}
}

enum Company {
	GOOGLE, APPLE, ORACLE;
}

public class ConstantDemo {

	public static void main(String[] args) {

		/*
		 * if(Fruit.APPLE == Company.APPLE){ System.out.println("과일 애플과 회사 애플이 같다."); }
		 */
		Fruit type = Fruit.APPLE;
		switch (type) {
		case APPLE:
			System.out.println(57 + " kcal");
			break;
		case PEACH:
			System.out.println(34 + " kcal");
			break;
		case BANANA:
			System.out.println(93 + " kcal");
			break;
		}
	}
}

/*
Call Constructor APPLE
Call Constructor PEACH
Call Constructor BANANA
57 kcal
*/

Fruit()가 실행되었다는 것은 생성자가 만들어졌다는 것이고, 생성자에 this를 하게 되면 생성자를 대표할 수 있는 어떤 정보를 자바가 준다. APPLE, PEACH, BANANA 이다.

Call Constructor APPLE
Call Constructor PEACH
Call Constructor BANANA
57 kcal

생성자를 가졌다면 이 Fruit라고 하는 것은 내부적으로 데이터를 가질 수 있을까? 다시 말해서 필드값을 가질 수 있을까? 못가질 이유가 없다!

public String color; 를 추가해서 각각의 과일에 대한 컬러를 준다고 하자.

package com.yuri.javatutorials.constant2;

enum Fruit {
	APPLE, PEACH, BANANA;
	
	public String color;
	
	Fruit(String color) {
		System.out.println("Call Constructor " + this); // this 현재 이 각각의 인스턴스들, 생성자가 만들어졌다는 의미 
		this.color = color;
		// this라는 것은 인스턴스를 의미, color은 전역변수를 가리킴. 
		// 생성자가 만들어 질때마다 color라는 매개변수를 갖는 것!
		// 앞에는 this가 있고, 뒤에 color는 this가 없는데 .. 앞의 this.color는 전역변수를 의미하고 this가 없는 경우에는 매개변수(지역변수)를 가리킨다. 
		// 지역변수가 전역변수보다 우선순위가 높다. 
		// this를 명시하지 않은 경우에는 지역변수를 쓰고 명시할 경우 전역변수를 사용한다. 
	}
}

enum Company {
	GOOGLE, APPLE, ORACLE;
}

public class ConstantDemo {

	public static void main(String[] args) {

		/*
		 * if(Fruit.APPLE == Company.APPLE){ System.out.println("과일 애플과 회사 애플이 같다."); }
		 */
		Fruit type = Fruit.APPLE;
		switch (type) {
		case APPLE:
			System.out.println(57 + " kcal");
			break;
		case PEACH:
			System.out.println(34 + " kcal");
			break;
		case BANANA:
			System.out.println(93 + " kcal");
			break;
		}
	}
}

/*
Call Constructor APPLE
Call Constructor PEACH
Call Constructor BANANA
57 kcal
*/

생성자가 호출될 때마다 color 값이 셋팅되어 this.color값을 갖도록 하려면 Fruit(String color)생성자가 호출될때 color 값을 부여해야 한다.

enum Fruit {
	APPLE("red"), PEACH("pink"), BANANA("yellow");

다음과 같이 색깔을 추가해주었다. 이후에

package com.yuri.javatutorials.constant2;

enum Fruit {
	APPLE("red"), PEACH("pink"), BANANA("yellow");
	
	public String color;
	
	Fruit(String color) {
		System.out.println("Call Constructor " + this); // this 현재 이 각각의 인스턴스들, 생성자가 만들어졌다는 의미 
		this.color = color;
	}
}

enum Company {
	GOOGLE, APPLE, ORACLE;
}

public class ConstantDemo {

	public static void main(String[] args) {

		/*
		 * if(Fruit.APPLE == Company.APPLE){ System.out.println("과일 애플과 회사 애플이 같다."); }
		 */
		Fruit type = Fruit.APPLE;
		switch (type) {
		case APPLE:
			System.out.println(57+" kcal, "+Fruit.APPLE.color);
			break;
		case PEACH:
			System.out.println(57+" kcal, "+Fruit.PEACH.color);
			break;
		case BANANA:
			System.out.println(57+" kcal, "+Fruit.BANANA.color);
			break;
		}
	}
}

/*
Call Constructor APPLE
Call Constructor PEACH
Call Constructor BANANA
57 kcal, red
*/

다음과 같이 호출하면 red가 출력된다.

변수가 생겼으므로, 메소드 또한 만들 수 있다.

package com.yuri.javatutorials.constant2;

enum Fruit {
	APPLE("red"), PEACH("pink"), BANANA("yellow");
	
	public String color;
	public String getColor() {
		return this.color;
	}
	Fruit(String color) {
		System.out.println("Call Constructor " + this); // this 현재 이 각각의 인스턴스들, 생성자가 만들어졌다는 의미 
		this.color = color;
	}
}

enum Company {
	GOOGLE, APPLE, ORACLE;
}

public class ConstantDemo {

	public static void main(String[] args) {

		/*
		 * if(Fruit.APPLE == Company.APPLE){ System.out.println("과일 애플과 회사 애플이 같다."); }
		 */
		Fruit type = Fruit.APPLE;
		switch (type) {
		case APPLE:
			System.out.println(57+" kcal, "+Fruit.APPLE.getColor());
			break;
		case PEACH:
			System.out.println(57+" kcal, "+Fruit.PEACH.getColor());
			break;
		case BANANA:
			System.out.println(57+" kcal, "+Fruit.BANANA.getColor());
			break;
		}
	}
}

/*
Call Constructor APPLE
Call Constructor PEACH
Call Constructor BANANA
57 kcal
*/

결과는 똑같다!

클래스로 상수 정의 vs enum 사용

클래스로 상수를 정의하게 되면 그 클래스가 가지고 있는 각각의 멤버를 마치 배열처럼 열거할 수 없다는 단점이 있다. Fruit가 어떠한 상수를 가지고 있는지를 코드를 작성하는 사람이 알고 있어야 한다.

enum은 그 안에 어떠한 데이터가 있는지 몰라도 마치 배열처럼 하나씩 꺼내서 처리할 수 있다. 그때 사용할 수 있는 메소드가 values()이다.

package com.yuri.javatutorials.constant2;

enum Fruit {
	APPLE("red"), PEACH("pink"), BANANA("yellow");

	private String color;

	Fruit(String color) {
		System.out.println("Call Constructor " + this);
		this.color = color;
	}

	String getColor() {
		return this.color;
	}
}

enum Company {
	GOOGLE, APPLE, ORACLE;
}

public class ConstantDemo {

	public static void main(String[] args) {
		for (Fruit f : Fruit.values()) {
			System.out.println(f + ", " + f.getColor());
		}
	}
}

for (Fruit f : Fruit.values()) {

: 뒤쪽에는 어떠한 데이터들의 집합이 오는 것이고, 앞에는 하나하나 꺼내주는 것이다. f라는 변수를 통해~
Fruit라고 하는 위에서 정의한 이넘의 values라는 메소드를 호출하면 values가 호출되면서 Fruit이 가지고 있는 데이터들 (APPLE, PEACH, BANANA)라고 하는 각각의 데이터들을 하나씩 꺼내서 f에 담는다.

열거형의 특성을 정리해보자. 열거형은 연관된 값들을 저장한다. (정확하게 말하자면 상수) 또 그 값들이 변경되지 않도록 보장한다. 뿐만 아니라 열거형 자체가 클래스이기 때문에 열거형 내부에 생성자, 필드, 메소드를 가질 수 있어서 단순히 상수가 아니라 더 많은 역할을 할 수 있다.


이 글은 생활코딩의 자바 강좌를 바탕으로 정리한 내용입니다.

profile
Step by step goes a long way ✨

0개의 댓글