[Java] 11주차 과제: Enum
by Roel Downey
스터디 링크 : 링크
Enum
열거형(enumerated type)이라고 부르며 서로 연관된 상수들의 집합이다. 기존에 상수를 사용하면서 발생했던 문제(typesafe)를 개선하고자 jdk1.5 부터 추가 된 기능이다.
Enum 정의
- enum 키워드를 이용하여 정의한다.
- 열거형 필드의 이름은 상수이기 때문에 대문자로 표기한다.
- 기본적으로 0부터 시작하는 정숫값이 연속적으로 부여된다.
enum Day {
// 0부터 연속적인 정수값 부여
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY;
}
Enum의 사용
위의 코드 Day 라는 enum을 정의한 후 아래의 코드를 작성하여 사용해보자.
public class EnumTest {
public static void main(String[] args) {
Day day = Day.FRIDAY;
System.out.println("오늘은" + day + "입니다.");
}
}
output
오늘은 FRIDAY 입니다.
주의사항
- 열거형 상수의 비교에는 ==와 compartTo() 사용가능 (등가비교연산자 가능)
- =, >, >=, <, <=, <> 같은 비교연산자는 사용할 수 없음(컴파일 에러)
enum은 왜 만들어졌는가?(효용성)
Enum을 잘 사용하면 코드의 가독성을 높이고 논리적인 오류를 줄일 수 있다. Enum을 잘 사용하기 위해 우선 Enum이 왜 탄생했는지 먼저 알아보자.
결론부터 말하자면 상수를 클래스로 정의해서 관리할 때 얻을 수 있는 이점들을 모두 취하면서 상수들을 더욱 간단히 선언할 수 있도록 하기 위해 만들어졌다.
예제를 살펴보면서 자세히 알아보자. 다음 예제는 과일 이름을 입력받으면 가격을 출력하는 프로그램이고, 과일의 이름은 숫자를 붙여 다음과 같이 상수로 관리한다.
public class EnumEx {
public static final int APPLE = 1;
public static final int BANANA = 2;
public static final int COCONUT = 3;
public static void main(String[] args) {
int type = APPLE;
switch (type) {
case APPLE:
System.out.println("360원");
break;
case BANANA:
System.out.println("6000원");
break;
case COCONUT:
System.out.println("2000원");
break;
}
}
}
위 코드를 읽다보면 각각의 상수에 1, 2, 3이라는 리터럴을 부여하여 구분을 하는데 이것은 논리적으로 아무런 의미가 없다. APPLE은 정수 1과 아무런 관련도 없고 굳이 1이어야 할 이유도 없다는 것이다.
두번째 문제는 이름의 충돌이 발생할 수 있다는 것이다. 만약 이 프로그램이 커져서, 기업의 이름을 추가하고 뭐가 추가되고 하다보니 IT 회사의 정보가 추가되었고 회사 이름을 상수로 관리하려 한다 해보자.
public class EnumEx {
public static final int APPLE = 1;
public static final int BANANA = 2;
public static final int COCONUT = 3;
...
public static final int APPLE = 1;
public static final int GOOGLE = 2;
public static final int FACEBOOK = 3;
...
}
과일인 ‘APPLE’과 회사 이름인 ‘APPLE’은 이름은 같지만 서로 다른 의미를 가진다. 이러한 상황에 위의 예시처럼 사용하려면 이름이 중복되기 때문에 컴파일 에러가 발생한다.
이름의 중복은 아래처럼 이름을 다르게 해주거나 인터페이스로 만들어서 구분할 수 있다.
서로 다른 이름 예제
public class EnumEx {
public static final int FRUIT_APPLE = 1;
public static final int FRUIT_BANANA = 2;
public static final int FRUIT_COCONUT = 3;
...
public static final int COMPANY_APPLE = 1;
public static final int COMPANY_GOOGLE = 2;
public static final int COMPANY_FACEBOOK = 3;
...
}
인터페이스 예제
interface Fruit {
int APPLE = 1, BANANA = 2, COCONUT = 3;
}
interface Company {
int APPLE = 1, GOOGLE = 2, FACEBOOK = 3;
}
하지만 상수를 인터페이스로 관리하는 것은 Anti-Pattern이다. 인터페이스는 규약을 정하기 위해 만든 것이지, 이런 식으로 사용하라고 만든 개념이 아니기 때문이다.
여전히 문제가 남아있다. fruit와 company 모두 int 타입의 자료형이기 때문에 아래와 같은 코드가 가능하다.
if(Fruit.APPLE == Company.APPLE) {
...
}
하지만 ‘과일’과 ‘회사’는 서로 비교조차 되어서는 안되는 다른 개념이다. 따라서 위와 같은 코드는 애초에 작성할 수 없게 컴파일 과정에서 막아줘야 한다.
둘이 애초에 비교를 하지 못하도록 하려면 서로 다른 객체로 만들어주면 된다.
class Fruit {
public static final Fruit APPLE = new Fruit();
public static final Fruit BANANA = new Fruit();
public static final Fruit COCONUT = new Fruit();
}
class Company {
public static final Company APPLE = new Company();
public static final Company GOOGLE = new Company();
public static final Company FACEBOOK = new Company();
}
public class EnumEx {
public static void main(String[] args) {
if (Fruit.APPLE == Company.APPLE) {} // 컴파일 에러 발생
}
}
이렇게 하면 위에서 언급했던 문제들
- 상수와 리터럴이 논리적인 연관이 없음
- 서로 다른 개념끼리 이름이 충돌할 수 있음
- 서로 다른 개념임에도 비교하는 코드가 가능함
이 모두 해결된다.
하지만 또! 다른 문제가 발생한다. 사용자 정의 타입은 switch문의 조건에 들어갈 수 없다. (switch문의 조건으로 들어갈 수 있는 데이터 타입은 byte, short, char, int, enum, String, Byte, Short, Character, Integer이다.)
public class EnumEx {
public static void main(String[] args) {
Fruit type = Fruit.APPLE;
switch (type) { // 컴파일 에러
case Fruit.APPLE:
System.out.println("360원");
break;
case Fruit.BANANA:
System.out.println("6000원");
break;
case Fruit.COCONUT:
System.out.println("2000원");
break;
}
}
}
Enum은 이렇게 상수를 클래스로 정의해서 관리할 때 얻을 수 있는 이점들을 모두 취하면서 상수들을 더욱 간단히 선언할 수 있도록 하기 위해 만들어진 것이다.
java.lang.Enum
모든 enum은 내부적으로 java.lang.Enum 클래스를 부모 클래스로 가진다.
Class getDeclaringClass() | 열거형의 Class객체를 반환 |
String name() | 열거형 상수의 이름을 문자열로 반환 |
int ordinal() | 열거형 상수가 정의된 순서를 반환(0부터 시작) |
T valueOf(Class enumType, String name) | 지정된 열거형에서 name과 일치하는 열거형 상수를 반환 |
compareTo(E o) | 지정된 객체보다 작은 경우 음의 정수, 동일한 경우 0, 크면 양의정수를 반환 |
values(), valueOf()
java.lang.Enum 즉 열거형의 부모 클래스에선 values(), valueOf() 메소드에 대한 내용을 자세히 찾아볼 수 없다.
그 이유는 컴파일러가 자동으로 추가해 주는 메소드이기 때문이다.
static E values() | 해당 열거체의 모든 상수를 저장한 배열을 생성하여 반환 |
static E valuesOf(String name) | 전달된 문자열과 일치하는 해당 열거체의 상수를 반환 |
EnumSet
Set
Set은 객체(데이터)를 중복해서 저장할 수 없다. 또한 저장된 객체(데이터)를 인덱스로 관리하지 않기 때문에 저장 순서가 보장되지 않는다 Set 컬렉션을 구현하는 대표적인 클래스들은 HashSet, TreeSet, LinkedHashSet 등이 있다. 주로 공통적으로 사용하는 메소드들은 add, iterator, size, remove, clear 들이 있다.
EnumSet
Set 인터페이스를 기반으로 하면서 Enumeration type을 사용하는 방법이다.
allOf(Class elementType) | 인자로 들어온 enum을 그대로 enum set 생성 |
complementOf(EnumSet s) | 인자로 들어온 enum set에서 없는 요소들로 만 다시 enum set 생성 |
of(E e1, E e2, E e3, E e4, E e5) | 초기값으로 지정한 값들로 enum set 생성 |
range(E fro,, E to) | 처음과 끝을 입력하면 그 사이에 있는 값들로 enum set 생성 |
EnumSet은 다른 컬렉션들과 달리 new 연산자를 사용할 수 없다. 단지 정적 팩토리 메서드(static factory method)만으로 EnumSet의 구현 객체를 반환 받을 수 있다. 왜 그럴까?
EnumSet의 내부를 살펴보면 abstract 클래스, 추상클래스라는 것을 알 수 있다. 즉, EnumSet은 추상클래스이기 때문에 객체로써 생성 및 사용이 불가능한 것이다.
왜 이렇게 만들었을고?
- 사용자 편의성 - (사용자는 어떤 구현 객체가 적합한지 몰라도 상관없다)
RegularEnumSet은 원소의 갯수가 적을 때 적합하고, JumboEnumSet은 원소의 갯수가 많을때 적합하지만, 이는 EnumSet의 구현체들을 모두 알고 있는 사용자가 아니라면 난해한 선택지가 될 수도 있다. 하지만 EnumSet을 가장 잘 알고 있는 EnumSet 개발자가 적절한 구현 객체를 반환해준다면 EnumSet을 사용하는 입장에서는 어떤 구현체가 적합한지 고려하지 않아도 된다. - 사용자 편의성2 - (사용자는 빈번하게 발생되는 EnumSet초기화 과정을 간단히 진행할 수 있다.)
EnumSet이 다루는 Enum의 모든 원소들을 Set에 담는 행위는 빈번하게 수행될 것으로 예상되는 일이다. 이러한 경우를 대비하여서 EnumSet의 allOf라는 메소드를 사용하면 모든 Enum 원소가 담겨있는 EnumSet을 쉽게 반환받고 사용 할 수 있다. - EnumSet의 확장성과 유지보수의 이점 EnumSet을 유지보수하는 과정에서 RegularEnumSet과 JumboEnumSet 이외에 다른 경우를 대비하는 구현 클래스가 추가 된다고 하여도 내부에 감추어져 있기 때문에, EnumSet을 사용하던 기존의 코드에는 전혀 영향이 없다. 심지어 RegularEnumSet이 삭제된다 하더라도 사용자에게 영향이 없다. 이는 EnumSet의 확장성의 큰 이점으로 작용할 수 있다.
이펙티브 자바
switch의 대안으로 상수별로 다르게 동작하는 코드 구현
- switch 문을 이용해 상수의 값에 따라 분기처리 하는 방법
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
// 상수가 뜻하는 연산을 수행한다
// 새로운 상수가 추가되면 case 문도 추가해야한다.
public double apply(double x, double y) {
switch (this) {
case PLUS:
return x + y;
case MINUS:
return x - y;
case TIMES:
return x * y;
case DIVIDE:
return x / y;
}
throw new AssertionError("Unknown op: " + this);
}
}
상수별 메소드 구현
- 열거 타입은 상수별로 다르게 동작하는 코드를 구현하는 코드를 제공 해준다.
public enum 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;
}
},
DIVIED {
public double apply(double x, double y) {
return x / y;
}
};
public abstract double apply(double x, double y);
}
데이터와 메소드가 있는 형태
- 열거 타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장 하면 된다.
// 어떤 객체의 지구에서의 무게를 입력받아 여덞 행성에서의 무게를 출력하는 예제이다.
enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6),
MARS (6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN (5.685e+26, 6.027e7),
URANUS (8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass; // 질량(단위: 킬로그램)
private final double radius; // 반지름(단위: 미터)
private final double surfaceGravity; // 표면중력(단위: m / s^2)
// 중력상수(단위: m^3 / kg s^2)
private static final double G = 6.67300E-11;
// 생성자
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() { return mass; }
public double radius() { return radius; }
public double surfaceGravity() { return surfaceGravity; }
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
public class WeightTable {
public static void main(String[] args) {
double earthWeight = Double.parseDouble("200");
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("%s에서의 무게는 %f이다.%n",
p, p.surfaceWeight(mass));
}
}
Anti Pattern
ordinal()
- 열거형 상수가 정의된 순서(0부터 시작)를 정수로 반환한다.
enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET, SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() {
return ordinal()+1;
}
}
public class Test {
public static void main(String[] args) {
Ensemble ensemble = Ensemble.valueOf("NONET");
System.out.println(ensemble.numberOfMusicians());;
}
}
Output
9
상수 선언 순서를 바꾸는 순간 numberOfMusicians() 메소드는 우리가 생각했던 방식으로 동작하지 않을 것이다. 또한 값을 중간에 비울 수도 없다. 더 이상 값을 추가하지 않을려면 일종의 더미 상수를 집어넣어야 한다. 이렇게 되면 코드가 깔끔하지 못하기 때문에 자바 Enum API 문서 상에도 ordinal() 메소드 사용을 권장하진 않는다.
type-safety
type-safety는 숫자만 해당하는게 아니라 문자열도 마찬가지이다. QueryDSL과 같은 라이브러리가 각광 받는 이유는 타입세이프티 때문이다.
문자열은 타입 세이프티가 보장되지 않는다. 따라서 문자열로 sql을 작성하는 것보다 QueryDSL과 같이 클래스에서 축출한 정보를 이용해 작성하면 훨씬 수월하고, 컴파일 타임에 오타가 날 일도 없고 특정한 타입 기반으로 컴파일을 하기 때문에 다 처리된다. 런타임에 문자열 오타로 발생하는 sql에러를 미연에 방지할 수 있다.
예제를 보자! 우리가 반드시 hello를 출력해야 한다고 할 때 다음과 같은 코드를 작성했다.
public class TypeSafetyEx {
public static void main(String[] args) {
System.out.println("hello");
}
}
위 코드는 type-safety하지 않은 코드이다. 오타가 발생해서 hellp가 출력될 수도 있고 hell0가 출력될 수도 있다. 그럼 어떻게 해야 할까?
public class TypeSafetyEx {
enum Greet {
Hello("hello");
Greet(String message) {
this.message = message;
}
String message;
public String getMessage() {
return message;
}
}
public static void main(String[] args) {
System.out.println(Greet.Hello.getMessage());
}
}
이렇게 Enum을 통해서 hello라고 정의해두면 type-safety가 되는 것이다. 코드는 길어졌지만 출력할 때 편하고 오타가 나더라도 컴파일러가 알려주기 때문에 오타 방지가 된다.
그래서 type-safety가 뭔데?
타입이 일치해야 안전하다. 즉, String 타입에는 String 타입이 와야 한다는 것이다. 같은 이름을 가진 상수라도 타입이 다르면 막아내는 것이 type-safety라고 볼 수 있다.
'Java' 카테고리의 다른 글
[Java] 13주차 과제: I/O (0) | 2021.02.17 |
---|---|
[Java] 12주차 과제: 애노테이션 (0) | 2021.02.16 |
[Java] 10주차 과제: 멀티쓰레드 프로그래밍 (0) | 2021.01.23 |
[Java] 9주차 과제: 예외 처리 (0) | 2021.01.18 |
[Java] 8주차 과제: 인터페이스 (0) | 2021.01.05 |
블로그의 정보
What doing?
Roel Downey