1. Enum이 뭔가요
한마디로 "정적 클래스"라고 부를 수 있다. enum 안에 정의된 클래스들은 싱글톤이다. 예를들어, Game이라는 enum 클래스 안에 여러 개의 게임을 정의한다고 해보자. 기본 문법은 다음과 같다. 클래스 이름을 ","로 구분하는 것이다.
public enum Game {
MAPLE_STORY,
LOST_ARK,
ANIMAL_CROSSING,
OVER_WATCH,
LEAGUE_OF_LEGEND;
}
Game이라는 enum 타입 안에 MAPLE_STORY, LOST_ARK, ..., LEAGUE_OF_LEGEND라는 클래스가 정의되었다. 각각의 enum 객체들은 전술했듯 싱글톤이기 때문에 자바 프로세스가 실행되는 동안 한 개 존재하며, 여기저기서 상수처럼 쓰일 수 있다. 기본적으로 toString()은 enum 클래스 이름을 return한다.
public class Client {
public static void main(String[] args) {
// ANIMAL_CROSSING
System.out.println(Game.ANIMAL_CROSSING);
}
}
enum 안에 있는 클래스 역시 클래스이기 때문에 필드 값을 가질 수 있다. 영어로 된 게임 enum type에 대해 한국어 이름의 필드값을 추가하고자 한다. 다음과 같이 만든다.
public enum Game {
MAPLE_STORY("메이플 스토리"),
LOST_ARK("로스트 아크"),
ANIMAL_CROSSING("동물의 숲"),
OVER_WATCH("오버워치"),
LEAGUE_OF_LEGEND("리그 오브 레전드");
final String koreanName;
Game(String koreanName) { this.koreanName = koreanName; }
}
추가하고 싶은 필드와 관련된 생성자를 선언한다. 그리고 enum 클래스를 작성할 때 생성자를 호출하는 형식이다. enum의 필드값은 바뀌는 경우가 잘 없기 때문에, (값이 자주 바뀐다면 enum을 사용하는 이유가 없어지게 된다. 설계 미스) 보통 필드값은 final로 많이 사용한다.
enum타입의 기본 메소드인 values(), name()은 각각 enum class 배열 자체와 특정 enum class의 이름을 return한다. enum 객체의 필드를 직접 접근할 수 있다.
public class Client {
public static void main(String[] args) {
for (Game game : Game.values()) { // values() : enum class 자체를 return
System.out.println("game.name() : " + game.name()); // name() : 클래스 이름 return
System.out.println("game.koreanName : " + game.koreanName); // 필드값 접근 가능
}
}
}
혹은 아래처럼 메소드로 getter을 작성한 뒤 외부에서 호출할 수 있다.
public enum Game {
MAPLE_STORY("메이플 스토리"),
LOST_ARK("로스트 아크"),
ANIMAL_CROSSING("동물의 숲"),
OVER_WATCH("오버워치"),
LEAGUE_OF_LEGEND("리그 오브 레전드");
private final String koreanName;
Game(String koreanName) { this.koreanName = koreanName; }
// getter
String getKoreanName() {
return koreanName;
}
}
public class Client {
public static void main(String[] args) {
for (Game game : Game.values()) { // values() : enum class 자체를 return
System.out.println("game.name() : " + game.name()); // name() : 클래스 이름 return
// 메소드 호출
System.out.println("game.koreanName : " + game.getKoreanName());
}
}
}
물론! 필드 값을 여러개 둘 수 있다. 이 경우, 생성자 역시 수정해주어야 한다. 롬복을 이용했다.
@RequiredArgsConstructor
public enum Game {
MAPLE_STORY("메이플 스토리",20),
LOST_ARK("로스트 아크",5),
ANIMAL_CROSSING("동물의 숲",22),
OVER_WATCH("오버워치",7),
LEAGUE_OF_LEGEND("리그 오브 레전드",14);
private final String koreanName;
private final int year;
}
2. Enum엔 메소드도 작성 가능하다
enum에 대해 충분히 알고있다고 생각했는데, 이 사실을 몰랐다. 아니 어렴풋이 알고 있었는데(생성자가 있다는 사실에서부터 이미) 유용하게 사용될 수 있다는 사실을 몰랐다. 이 글을 쓴 이유이기도 하다
Game enum 클래스에 printInfo() 메소드를 추가했다.
void printInfo() {
System.out.println("출시된지 " + year + "년 된 유명한 게임, " + koreanName + "!");
}
클라이언트 프로그램에서 각 enum 객체의 printInfo()를 자연스럽게 호출할 수 있다.
public class Client {
public static void main(String[] args) {
for (Game game : Game.values()) {
game.printInfo();
}
}
}
혹은, enum 클래스 안에 abstract 메소드를 두고 각 enum 객체가 이를 구현하도록 할 수 있다. 익명 클래스/인터페이스를 구현하는 방법과 같다.
@RequiredArgsConstructor
public enum Game {
MAPLE_STORY("메이플 스토리",20) {
void printInfo() {
System.out.println("엔젤릭버스터 출동!");
}
},
LOST_ARK("로스트 아크",5) {
void printInfo() {
System.out.println("나는 97돌 언제 만들어보나..");
}
},
ANIMAL_CROSSING("동물의 숲",22) {
void printInfo() {
System.out.println("최애 주민은 쭈니");
}
},
OVER_WATCH("오버워치",7) {
void printInfo() {
System.out.println("사실 사놓고 몇 판 안함");
}
},
LEAGUE_OF_LEGEND("리그 오브 레전드",14) {
void printInfo() {
System.out.println("여기에 쏟아부은 시간동안 다른걸 했다면..");
}
};
private final String koreanName;
private final int year;
abstract void printInfo();
}
위와 같이 분리하면 각 enum 객체에 독립적인 로직을 수행하게 할 수 있다. 당연히! 구현하게 할 메소드 역시 여러개를 둘 수 있다.
3. 그래서 Enum을 사용하면 뭐가 좋냐?
좋으니까 닥치고 써!!!!
가 아니라 , 하나의 enum 클래스 자체에 여러가지 메타 정보를 담을 수 있다는 장점이 있다. 완벽하진 않지만 조금 더 상세히 설명하자면.. 클래스 자체에 대한 정보와 더불어 클래스가 가진 고유의 동작까지도 한번에 저장 가능하다.
위에 예시로 썼던 Game enum을 계속 사용하겠다. 만약 enum 타입 없이 Game 객체를 선언했다면 전반적 내용은 다음과 같을 것이다.
@AllArgsConstructor
public class Game {
String name;
String koreanName;
int year;
}
그리고 Game 객체를 생성하기 위해 런타임에 new를 통해 객체를 하나하나 생성해야 할 것이다. 그럼 그 객체느 싱글톤이 아니게 되고.. 싱글톤으로 구현하려면 동시성 처리를 따로 해줘야하고.. 기타 신경쓸게 많지만 그런건 상관하지 말고 로직 자체를 보자.
Game 객체의 이름에 따라 장르를 나누는 상황이라고 가정해보자. 메이플과 로스트아크는 rpg, 오버워치는 fps, 동물의 숲은 시뮬레이션, 롤은.. 모르겠다. 아무튼 각 게임의 이름에 따라 장르를 출력하는 프로그램을 만들어봤다.
public class Client {
public static void main(String[] args) {
Game [] games = {new Game("MAPLE_STORY","메이플스토리",20),
new Game("LOST_ARK","로스트 아크", 5),
new Game("ANIMAL_CROSSING","동물의 숲", 22),
new Game("OVER_WATCH","오버워치", 7),
new Game("LEAGUE_OF_LEGEND","리그 오브 레전드",14)};
for (Game game : games) {
if ("MAPLE_STORY".equals(game.name) || "LOST_ARK".equals(game.name)) {
System.out.println("RPG 장르");
} else if ("OVER_WATCH".equals(game.name)) {
System.out.println("FPS 장르");
} else if ("ANIMAL_CROSSING".equals(game.name)) {
System.out.println("시뮬레이션 장르");
} else {
System.out.println("기타 장르");
}
}
}
}
만약? games에 객체가 늘어난다면? if로 분기해야하는 장르의 개수가 늘어난다면? if 안에 or로 추가해야하는 게임들이 자꾸만 늘어난다면?!
enum을 사용하면 더 간단해진다. 기존에 사용했던 Game enum은 그대로 둔 채, Genre enum을 추가하자!
@RequiredArgsConstructor
public enum Genre {
RPG("rpg 장르", Arrays.asList("MAPLE_STORY","LOST_ARK")),
FPS("fps 장르", Arrays.asList("OVER_WATCH")),
SIMULATION("시뮬레이션 장르", Arrays.asList("ANIMAL_CROSSING")),
OTHER("기타 장르", Arrays.asList("LEAGUE_OF_LEGEND")),;
final String description;
final List<String> gameList;
public static Genre getGenre(String gameName) {
return Arrays.stream(Genre.values()) // 각 장르에 대해
.filter(genre -> genre.hasGame(gameName)) // gameName을 포함하는 장르가 있다?
.findAny() // return 해
.orElse(OTHER);
}
boolean hasGame(String gameName) {
// gameName에 해당하는 장르 enum return
return gameList.stream().anyMatch(gameName::equals);
}
public static void printGenre(String gameName) {
System.out.println(gameName + "의 장르는 " + getGenre(gameName) + "입니다.");
}
}
장르에 대한 enum을 추가했다. 각 enum 클래스 내부엔 장르가 포함하는 game을 추가했다. Client main 코드는 다음과 같이 바뀐다.
public class Client {
public static void main(String[] args) {
Genre.printGenre("MAPLE_STORY");
Genre.printGenre("LOST_ARK");
Genre.printGenre("ANIMAL_CROSSING");
}
}
같은 기능을 하지만, if문이 없으니 훨씬 깔끔하다!
이외에, 앞서 추상 메소드인 printInfo() 메소드를 각 enum 클래스에서 정의했던 것을 생각해보자. 하나의 추상 메소드를 구상 클래스에서 구현.. 그리고 그것을 호출한 구상 클래스의 타입에 따라 다른 메소드 호출..? 이거 완전 다형성 아닌가?
그렇다. 구상 클래스에 따라 호출되는 메소드가 달라지고, 이것을 클라이언트 측에서 신경쓰지 않는 객체지향의 특성, 다형성!!! 을 구현하기 위해 상위 인터페이스/클래스를 이용한 상속만이 사용되는 것은 아니다!
상속을 통한 다형성 구현에 비해 enum 자체가 가지고 있는 장점이라고 본다면,, JPA를 사용했을 때, @Enumerate를 통해 DB에 enum 값과 더불어 enum 내의 메소드 자체도 저장할 수 있다는 점일듯 하다. 이미 산더미처럼 테이블이 쌓여있는 프로젝트에서 "특정 필드 값이 xx일때 XX로직을 해주세요~" 했을 때 생각없이 짜면 수없이 늘어날 if문 때문에 머리아플 것 같다. 그러나 "enum 타입이 xx일때 XX로직 수행하기" 라고 정해두고 enum 클래스에 각 메소드 구현체를 두면, 특정 값과 특정 로직이 관련있다 라는 코드 가독성도 늘어나고 if문 쭉 쓰거나 간지나게 상속 써보겠다고 인터페이스 새로 만드는것보다 훨씬 간단하게 기능을 구현할 수 있지 않을까???
REFERENCE
https://techblog.woowahan.com/2527/
https://bcp0109.tistory.com/334