본문 바로가기
프로젝트 개발 관련 공부/JAVA

[JAVA] 알고리즘 풀 때 enum 써보기

by 이제ise이제 2022. 10. 22.

부제 : 입력받은 정수에 해당하는 enum 멤버로 할당 in 방향 of 시뮬레이션

 

0. 한 줄 요약
1. 발단 : java에서 갑분 enum?
2. 전개 : 어디에 써먹을까
3. 위기 : 이거 외않되?
4. 절정 : 도와줘요 구글에몽! (작성중)
5. 결말

 

 

요약

 

 

입력 받은 번호 위치(index)에 해당하는 enum객체의 원소를 바로 가져다 쓰고 싶으면
객체이름.values()[번호]을 사용하자

ex)

enum eDir{
	empty, up, down, right, left;
}

eDir dir = eDir.values()[idx];

 

 


발단 : java에서 갑분 enum?

 

 

SW Expert Academy의 5653. [모의 SW 역량테스트] 줄기세포배양을 풀다가 세포들 상태를 enum으로 구분해서 풀어봤다.

(결과적으로 3개 케이스가 시간초과나서 실패함 => 반복을 줄여서 성공. 관련 글은 여기로)

 

이때 이후로 간단한 숫자나 문자를 입력받아 케이스를 나누는 부분을 enum으로 쓰면 어떨까 생각했다.

 

 

 


전개 : 어디에 써먹을까

 

 

"유지보수할 필요없는, 문제 하나 풀 때 쓰는 용도인데 그런거 쓸 필요가 있을까? 시간초과 안 남?"

 

스스로에게도 하는 말이고 남한테도 들은 말이지만, 그럼에도 enum으로 쓰려는 이유는 심플했다. 입력값에 따라 switch나 if-else로 케이스 나누기가 귀찮았다.

 

알고리즘 문제 중 시뮬레이션에서 자주보이는 방향 이동이 특히 그랬다.

 

많고 많은 예제문제들이 있지만 당장 풀고 있는 [백준]17143. 낚시왕에서는 다음과 같다.

위 = 1, 아래 = 2, 오른쪽 = 3, 왼쪽 = 4

이런 경우 나는 대략 다음 코드와 같이 구현하는 편이었다.

 

- ⅰ) 먼저 기본 입력을 Shark라는 클래스에 입력을 받는다.

static class Shark {
    int r, c, s, d, z; // 상어위치, 속력, 이동 방향, 크기
    char num; // 몇 번째 상어인지(알파벳)

    public Shark(char num, int r, int c, int s, ind d, int z) {
        super();
        this.num = num;
        this.r = r;
        this.c = c;
        this.s = s;
        this.d = d;
        this.z = z;
    }
}


public static void main(String[] args) throws IOException {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    StringTokenizer st = new StringTokenizer(br.readLine());
    
    int r = Integer.parseInt(st.nextToken());
    int c = Integer.parseInt(st.nextToken());
    int s = Integer.parseInt(st.nextToken());
    int d = Integer.parseInt(st.nextToken()); // 상어의 이동 방향
    int z = Integer.parseInt(st.nextToken());

    Shark shark = new Shark(num, r, c, s, d, z);
}

 

- ⅱ) 이제 여기에 '낚시왕' 문제 기준으로 상어별로 이동 방향 값을 넣어주거나 이동시킨다.

    switch (d) {
        case 1: // 위
            // 상어를 위로 이동시킴
            // 벽이랑 만나면 d = 2로 바꾸고 계속 이동시킴
        break;
        case 2: // 아래
            // 상어를 아래로 이동시킴
            // 벽이랑 만나면 d = 1로 바꾸고 계속 이동시킴
        break;
        case 3: // 오른쪽
            // 상어를 오른쪽으로 이동시킴
            // 벽이랑 만나면 d = 4로 바꾸고 계속 이동시킴
        break;
        case 4: // 왼쪽
            // 상어를 왼쪽으로 이동시킴
            // 벽이랑 만나면 d = 3로 바꾸고 계속 이동시킴
        break;
    }

 

💬 if/else || switch 으로 방향 전환시 문제점
1. 주석 하나하나 달아줘야한다. 엄청 귀찮다
2. 주석 안 달면 가독성이 쓰레기다.
3. 주석 달아도 코드리뷰할 때 집중 안하면 기억 안 난다.

 

이 번호가.. 어느 방향이더라..

 

 

물론 어차피 enum으로 해도 나중에 조건문에서 값에 따라 분리하긴 해야한다.

 

	switch (dir) {
        case up:
            // 상어를 위로 이동시킴
            // 벽이랑 만나면 d = 2로 바꾸고 계속 이동시킴
        break;
        case down:
            // 상어를 아래로 이동시킴
            // 벽이랑 만나면 d = 1로 바꾸고 계속 이동시킴
        break;
        case right:
            // 상어를 오른쪽으로 이동시킴
            // 벽이랑 만나면 d = 4로 바꾸고 계속 이동시킴
        break;
        case left:
            // 상어를 왼쪽으로 이동시킴
            // 벽이랑 만나면 d = 3로 바꾸고 계속 이동시킴
        break;
    }

 

이런 식으로.

 

그래도 1이 위, 2가 아래, 3이 오른쪽, 4가 왼쪽이라고 굳이 표시할 필요가 없다.

 

 

 


위기 : 이거 외않되?

스물다섯 스물하나 : enum 좋아하면 않되..?

 

목표 입력받은 숫자(1~4)에 해당하는 enum 값(방향: 위, 아래, 오, 왼)을 자동할당하는 것이다.

그러면 방향 전환 같이 방향 값을 찾을 때 정수값의 의미를 아는 사람만 파악하는 수고도 덜고, 주석을 안달아도 되므로 가독성과 귀찮음을 해결할 수 있을 것이다.

 

 

 

enum을 접한게 Unity C#이었던지라 비슷한 방식으로 접근했다.

그런데 이게 문제였다.

 

Microsoft 문서 중 열거형-C# 참조에서 enum 타입 사용법으로 다음 방식을 알려준다.

 

enum in 마소 C# 문서

 

비슷하게 eDir dir = (eDir) d; 로 하면 되겠지?

금방 하겠는데?

 

.. 라고 생각했다. 플래그였다


 

전혀 안됐다.

 

응 안돼 돌아가

그딴 거 안된다고 이클립스가 경고해준다(56warnings 는 무시해주길.. 알고 풀면서 패키지에 사용 안한 변수가 많아서 그래요..)

 

 

이때부터는 오라클에서 제공하는 java 레퍼런스를 뒤졌다.

valueOf 라는 게 있던데 d를 string으로 받아서 넘겨볼까?

 

 

응 안돼 돌아가22

안된단다.

 

 

Enum.java의 238번째 줄에 가보니 문제는 d의 값(문자열 형태인 1, 2, 3, 4)에 해당하는게 enum eDir에 없다는 의미였다.(eDir의 값은 up, down, right, left니까)

Enum.java : 238번째 줄을 보자

 

 

이 에러를 해결하기 위해 현재 코드를 조금 수정하면 이렇게 된다.

 

일을 2번하는 매직. 일을 2번하는 매직.

(좋은 코드가 아니니까 코드 블록 대신 사진으로 올렸다. 이런 거 복붙하지 마세요)

switch에 1, 2, 3, 4를 쓰기 싫어서 하는 건데 쓰게 만든다.

 

심지어 저건 입력 코드라서 구현에서 또 switch로 up 등등 을 써야하는게 이게 무슨 멍청한 짓일까.

 

 

 


절정 : 도와줘요 구글에몽!

 

 

분명 C# 처럼 한 줄로 캐스팅하거나 바로 대입할 수 있는 방법이 있을 것 같았다.

구글에도 물어보고, 오라클에서 제공하는 java 레퍼런스의 다른 메서드도 찾아보고, 야생의 핑프가 되어 지인들에게도 물어봤다.

이때 약간 주변 사람들이 이걸 게으르다고 해야 할지 부지런하다고 해야 할지 모르겠는 미친놈으로 바라봤다. 

 

해치지 않아요..

 

 

아무튼 물어보면서 내 목적을 좀 더 분명하게 정리해봤다.

입력(=대입 혹은 매개변수) 정수형
return String 혹은 enum형
(내 코드에서는 eDir로 return)

 

 

그리고 정답은 킹갓제너럴 스택오버플로우에 있었다.

 

찬양하라 스택오버플로우

스택오버플로우 줄임말은 뭘까? SO? 스오플? 스플? 이거 줄임말 아는 분 댓글 좀 부탁드립니다. 쓰는데 너무 길다

 

첫 줄에도 말했던 객체이름.values()[정수]를 쓰면 된단다!

 

댓글에서는 캐시와 성능 관련해서 뭔가 있었지만 코드 돌려보고 한 줄로 바로 넣을 수 있다는게 완전 신났었다.

 

 

before / after

 

before22 / after22

 

사진 속 코드는 아래 더보기 클릭

더보기

!주의: 전체 코드 중 일부이므로 단순 복붙할 경우 에러가 발생할 수 있습니다.

static class Shark {
    int r, c, s, z; // 상어위치, 속력, 크기
    eDir dir; // 이동 방향
    char num; // 상어 번호(지만 알파벳)

    public Shark(char num, int r, int c, int s, eDir dir, int z) {
        super();
        this.num = num;
        this.r = r;
        this.c = c;
        this.s = s;
        this.dir = dir;
        this.z = z;
    }
}

static enum eDir {
    empty, up, down, right, left;
}

 

// 상어들 정보 넣기
for (int m = 0; m < M; m++) {
    char num = (char) (m + 'A');

    st = new StringTokenizer(br.readLine());
    int r = Integer.parseInt(st.nextToken());
    int c = Integer.parseInt(st.nextToken());
    int s = Integer.parseInt(st.nextToken());
    int d = Integer.parseInt(st.nextToken());
    int z = Integer.parseInt(st.nextToken());

    Shark shark = new Shark(num, r, c, s, eDir.values()[d], z);
    sharkList.add(shark);
    map[r][c] = num;
}
// 3. 상어가 이동한다.
for (int sIdx = 0; sIdx < sharkList.size(); sIdx++) {
    if (sharkList.get(sIdx).s != -1) {
        for (int s = 1; s <= sharkList.get(sIdx).s; s++) {
            eDir dir = sharkList.get(sIdx).dir;
            switch (dir) {
            case up:
                // 구현
                break;
            case down:
                // 구현
                break;
            case right:
                // 구현
                break;
            case left:
                // 구현
                break;
            }
        }
    }
} // ========== 이동 종료

 

 

 

크게 달라진 건 티끌이고 오히려 코드 줄 수도 늘었다. 1, 2, 3, 4 대신 알파벳으로 치는게 더 귀찮을 수 있다.

 

그래도 누군가에게 코드를 설명할 때,

"1이니까 위로 갈거고 조건에 해당하면 2로 바꿔서 방향을 아래로 가도록할거야" 대신

"up이던 걸 조건에 해당하면 down으로 바꿀거야" 라고 말할 수 있게 되었다.

 

 


결론 : 객체이름.values()[번호]을 사용하자

수미상관

 

 

static enum eDir{
    empty, up, down, right, left;
}
    
public static void main(String[] args) throws IOException {
    // -- 중략 --
    Shark shark = new Shark(num, r, c, s, eDir.values()[d], z);
    
    // -- 중략 --

 

실제 프로젝트에서는 캐시 사용 때문에 느려진다는 얘기가 있다. 하지만 작은 규모나 시뮬레이션 문풀 때는 주석보다 가독성도 높고, 정수에 따라 switch나 if-else의 조건을 매번 바꿔주지 않아도 되니 편한 만큼 높은 만큼 자주 써먹을 것 같다.

 

 

+) 이번 글을 쓰면서 클래스의 멤버, 객체, 요소, 원소가 조금 혼재되어 사용한 것 같다. 관련 용어를 한 번 정리할 필요를 느꼈다. 정리하게 되면 블로깅해야지(티스토리 제발 주식처럼 터지지 말아줘)

 

++) 개인적으로 자바 레퍼런스는 메서드가 일목요연하게 정리되어있어서 어떤 메서드가 있는지 찾기는 쉽지만 이를 활용하는 방법은 아직 모르겠다. C++ 레퍼런스는 예시도 제공해줘서 좋은데 아쉽다.