2020. 4. 18. 22:46ㆍJava
처음 자바의 Enum을 알고 난 뒤, 여기저기 유용하게 사용했다. Enum은 기본적으로 상수의 그룹을 나타낼 때 사용하는데, 자바의 Enum은 다른 언어의 Enum보다 강력한 기능을 여럿 제공한다. 이번 포스팅에서는 Enum을 활용하는 여러 방법에 대해 알아보려고 한다.
선언과 사용
public enum Rank {
FIRST_PLACE(6),
SECOND_PLACE(5),
NONE(-1);
private int matchingCount;
Rank(int matchingCount) {
this.matchingCount = matchingCount;
}
}
이 때, Rank
는 Enum 타입이 되고 FIRST_PLACE
, NONE
등은 Enum 상수가 된다. 각 상수들은 일반 클래스와 동일하게 필드를 가질 수 있다. 위 코드에서는 matchingCount
가 필드이고, 이는 상수 옆 괄호 안에 NONE(-1)
처럼 값을 넣어줄 수 있다. 이렇게 NONE
의 matchingCount
가 -1이라고 명시할 수 있다. 위 코드는 내부적으로 아래와 같이 class로 바뀐다.
public class Rank {
public static final Rank FIRST_PLACE = new Rank(6);
public static final Rank SECOND_PLACE = new Rank(5);
public static final Rank NONE = new Rank(-1);
}
Rank rank = Rank.FIRST_PLACE;
따라서, 각 Enum 상수의 사용은 위와 같이 할 수 있다.
values(), ordinal()
Rank.values()
는 Rank에 포함되는 모든 상수들의 배열을 리턴한다. 즉, Rank[] ranks = {Rank.FIRST_PLACE, Rank.SECOND_PLACE, Rank.NONE}
과 같다.ordinal()
은 해당 상수의 index를 리턴한다. Rank.FIRST_PLACE.ordinal()
은 0이고, Rank.NONE.ordinal()
은 2가 된다.
Comparable
자바의 Enum은 Comparable<T>
인터페이스를 구현하고 있다. 따라서 compareTo()
메서드를 사용할 수 있다. 또한 이 compareTo()
메서드는 final로 선언되어 있어서 오버라이드가 불가능하다. 따라서 Enum 상수의 순서는 상수들이 선언된 순서가 된다.
메서드
public enum Rank {
FIRST_PLACE(6),
SECOND_PLACE(5),
NONE(-1);
private int matchingCount;
Rank(int matchingCount) {
this.matchingCount = matchingCount;
}
public void printRankInfo() {
System.out.println(this.toString());
}
}
public static void main(String[] args) {
FIRST_PLACE.printRankInfo();
}
Enum도 클래스와 동일하게 메서드를 가질 수 있다. 위 코드에서 printRankInfo()
는 자기 자신의 문자열을 출력하는 메서드이다. 위 main 함수의 실행 결과는 아래와 같다.
output: FIRST_PLACE
Lambda
사칙연산은 아래와 같이 인터페이스로 구현할 수 있다.
public interface Operator {
double calculate(double preOperand, double postOperand);
}
class Plus implements Operator {
@Override
public double calculate(double preOperand, double postOperand) {
return preOperand + postOperand;
}
}
class Minus implements Operator {
@Override
public double calculate(double preOperand, double postOperand) {
return preOperand + postOperand;
}
}
class Multiply implements Operator {
@Override
public double calculate(double preOperand, double postOperand) {
return preOperand + postOperand;
}
}
class Divide implements Operator {
@Override
public double calculate(double preOperand, double postOperand) {
return preOperand + postOperand;
}
}
class operationChoicer {
public Operator findOperator(String operatorName) {
if (operatorName.equals("+")) {
return new Plus();
}
if (operatorName.equals("-")) {
return new Minus();
}
if (operatorName.equals("*")) {
return new Multiply();
}
if (operatorName.equals("/")) {
return new Divide();
}
return null;
}
}
하지만 위와 같이 구현하면 상태(+,- 등의 사칙연산의 기호)와 행위(실제로 덧셈, 뺄셈을 하는 것)가 따로 관리된다. 이를 Enum과 람다식을 사용해서 구현하면 아래와 같이 구현할 수 있다.
public enum Operator {
PLUS("+", (preOperand, postOperand) -> preOperand + postOperand),
MINUS("-", (preOperand, postOperand) -> preOperand - postOperand),
MULTIPLY("*", (preOperand, postOperand) -> preOperand * postOperand),
DIVIDE("/", (preOperand, postOperand) -> preOperand / postOperand);
private String name;
private BiFunction<Double, Double, Double> expression;
Operator(String name, BiFunction<Double, Double, Double> expression) {
this.name = name;
this.expression = expression;
}
public static Operator findOperator(String operatorName) {
return Stream.of(values())
.filter(operator -> operator.name.equals(operatorName))
.findFirst()
.orElse(null);
}
public double calculate(double preOperand, double postOperand) {
return expression.apply(preOperand, postOperand);
}
}
코드가 훨신 깔끔해지고 양도 줄어든 기분이다. 이 경우는 확실히 그렇지만, Enum이 가져야할 필드가 늘어나고 람다식이 복잡해지면 인터페이스로 분리하는 편이 훨신 나을 것이다.
활용기
처음 나온 코드의 Rank
는 복권 등수를 나타낸 Enum이다. 복권 등수는 6자리 번호 중 일치하는 번호의 갯수와 보너스 번호의 일치 여부로 결정된다. 이를 조금 더 구체적으로 적어보면 아래와 같다.
public enum Rank {
FIRST_PLACE(6, 2_000_000_000),
SECOND_PLACE(5, 300_000_000),
THIRD_PLACE(5, 15_000_000),
FOURTH_PLACE(4, 50_000),
FIFTH_PLACE(3, 5_000),
NONE(-1, 0);
private int matchingCount;
private int reward;
Rank(int matchingCount, int reward) {
this.matchingCount = matchingCount;
this.reward = reward;
}
public static Rank findRankBy(int matchingCount, boolean isBonusMatching) {
if (matchingCount == THIRD_PLACE.matchingCount && !isBonusMatching) {
return THIRD_PLACE;
}
return Stream.of(values())
.filter(rank -> rank.matchingCount == matchingCount)
.findFirst()
.orElse(NONE);
}
}
findRankBy()
메서드는 일치하는 숫자의 갯수와 보너스 번호 일치 여부를 받아서 해당하는 Rank
를 리턴하는 함수이다. 이 때, 2등과 3등은 일치하는 숫자의 갯수는 동일하고, 보너스 번호 일치 여부로 결정된다. 나와 페어는 이 코드가 마음에 들지 않았다. Enum의 각 상수들은 자신의 상태를 온전히 갖고 있어야 한다고 생각하는데, 위 코드의 상수들은 보너스 번호 일치 여부를 갖고 있지 않았기 때문이다. 따라서 아래와 같이 코드를 수정했다.
// Rank.java
public enum Rank {
FIRST_PLACE(6, BonusType.NO_MATTER, 2_000_000_000),
SECOND_PLACE(5, BonusType.SHOULD_MATCHING, 300_000_000),
THIRD_PLACE(5, BonusType.SHOULD_NOT_MATCHING, 15_000_000),
FOURTH_PLACE(4, BonusType.NO_MATTER, 50_000),
FIFTH_PLACE(3, BonusType.NO_MATTER, 5_000),
NONE(-1, BonusType.NO_MATTER, 0);
private int matchingCount;
private BonusType bonusType;
private int reward;
Rank(int matchingCount, BonusType bonusType, int reward) {
this.matchingCount = matchingCount;
this.bonusType = bonusType;
this.reward = reward;
}
public static Rank findRankBy(int matchingCount, boolean isBonusMatching) {
return Stream.of(values())
.filter(rank -> (rank.matchingCount == matchingCount)
&& (rank.bonusType.filter(isBonusMatching)))
.findFirst()
.orElse(NONE);
}
}
// BonusType.java
public enum BonusType {
NO_MATTER(noMatterBonusMatchingOrNot -> true),
SHOULD_MATCHING(isBonusMatching -> isBonusMatching.equals(true)),
SHOULD_NOT_MATCHING(isBonusMatching -> isBonusMatching.equals(false));
private Function<Boolean, Boolean> expression;
BonusType(Function<Boolean, Boolean> expression) {
this.expression = expression;
}
public boolean filter(boolean isBonusMatching) {
return expression.apply(isBonusMatching);
}
}
2, 3등을 제외한 나머지 등수들은 보너스 일치 여부와 상관이 없다(BonusType.NO_MATTER
). 2등은 보너스 번호가 일치해야하고(BonusType.SHOULD_MATCHING
), 3등은 일치하지 않아야한다(BonusType.SHOULD_NOT_MATCHING
). 새로운 BonusType
이란 Enum을 만듦으로써, Rank는 자신의 상태를 온전하게 표현할 수 있게 되었다.
사실, 처음 페어와 프로그래밍을 할 때는 보너스 일치 여부를 boolean
으로 표현했다. 하지만 코드가 마음에 들지 않았고, 오랜 고민 끝에 페어가 보너스 일치 여부가 boolean이 아니지 않을까? 하는 질문을 던졌다. 그 질문에 대한 답으로 BonusType
이 만들어졌다. 이번 주제와 별개지만 기존 사고 방식을 깨는게 얼마나 중요한지 깨닫게 되는 계기였다.
참고 사이트
'Java' 카테고리의 다른 글
[Spring Data JDBC] 객체(Object)와 개체(Entity) 대응시키기 (0) | 2020.05.11 |
---|---|
JPA vs JDBC, JPA vs Mybatis, JPA vs Spring Data JPA의 차이점과 Hibernate (0) | 2020.04.25 |
자바의 생성자와 정적 팩토리 메서드 (0) | 2020.04.20 |
Intelij 단축키 - 알아두면 생산성이 올라가는 단축키 모음 (0) | 2020.04.20 |
자바의 예외처리 (0) | 2020.04.18 |