[JAVA] BitMask에 관한 자그마한 고찰, 그리고 EnumSet

개요

BitMask 를 사용하는 이유와, 적용하게 된 계기를 적어 보고자 한다. 그러나 결국 EnumSet 을 사용하기로 결정하였기 때문에, BitMask 대신 EnumSet 사용을 권장한다.

단, 이 포스팅에서 BitMaskEnumSet 을 디테일하게 다루지는 않을 것이다. 이 글은 ‘왜’ 사용해야 하는가와 '어떤 장점’이 있는지에 초점을 맞추어 작성하였다.


BitMask요? 갑자기요?

스무 살, C언어 강의를 듣던 언젠가, 어렴풋이 들었던 것도 같다. (심지어 그 당시 교수님도 그리 중요하게 다루지 않으셨고, 나는 A+이지렁!) 지금도 잘 모르지만 그땐 더욱 몰랐으니 논외로 치고, 처음 BitMask 를 이해해야만 했던 것은 약 1년 전이다. 우리 앱[1]이 특정 앱에게 intent로 정보를 요청하고, 수신해야 했는데 이때 수신한 값이 BitMask 형태라는 것이다. 헤헤. 청천벽력 같았다. ‘어차피 intentdata 담아서 줄 거면 그 값 그대로 주면 되지 굳이?’ 라는 생각이 들었지만, 그렇게 준다는 걸 어떡해. 앞으로 쓸 일도 없을 것 같고, 대충 이런 게 있구나 정도로만 이해하고 넘어갔다.


코드 구경, 이해, 깨달음, 적용

다른 업무로 옆 파트 과장님과 의견 교환 중에 우연히 과장님의 코드를 볼 기회가 생겼다. 업무 관련해서 조언을 구했는데, 과장님께서 본인 코드[2] 를 적절히 고치면 웬만해서 해결될 거라고 하셨다. 그렇다. 예상했겠지만, 이 코드 안에 BitMask 를 사용한 비즈니스 로직이 포함되어 있었다.

후. 이렇게 다시 만날 줄 몰랐다. 그러나 더는 피할 곳이 없었다. 이전엔 이미 마스킹된 값을 풀어내는 것에 그쳤지만, 이번엔 이것을 제대로 적용해 볼 수 있는 기회였다. 과장님께서 코드까지 주셨는데 두려울 게 뭐가 있겠습니까.


그래서 BitMask가 뭔데?

말 그대로 bit 에 관련된 것이다. bit 는 이진 숫자를 뜻하는 말로, 컴퓨터에서 사용되는 데이터의 최소 단위이다. 맞다, 0과 1! 그런데 왜 bitmask 가 붙는 걸까? BitMask 는 0과 1로만 이루어진 bit 의 특성을 이용한 테크닉의 일종이다. 결론만 말하자면, 이 테크닉은 ‘조건 지옥’ 에서 조금이나마 벗어날 수 있도록 해 준다. 아래 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class HobbyManager {
// 0000 0000 부터 1000 0000 까지 총 9가지의 기준을 가질 수 있다.
public static final int NONE = 0x0; // 0000 0000
public static final int WALKING = 0x1; // 0000 0001
public static final int READING = 0x2; // 0000 0010
public static final int SINGING = 0x4; // 0000 0100
public static final int RUNNING = 0x8; // 0000 1000
public static final int PROGRAMMING = 0x10; // 0001 0000
public static final int SWIMMING = 0x20; // 0010 0000
public static final int DANCING = 0x40; // 0100 0000
public static final int SLEEPING = 0x80; // 1000 0000

private static int hobbyFlags;

public static boolean hasHobby(int flag) {
return (flag & hobbyFlags) == flag;
}

public static void addHobby(int flag) {
hobbyFlags |= flag;
}

public static void setHobbyFlags(int flags) {
hobbyFlags = flags;
}
}

작명이 영 별로이긴 하지만, 위 코드는 누군가의 취미를 설정하고, 추가하고, 특정 취미 보유 여부를 판단할 수 있도록 한다. 사용 방법은 간단하다. 모든 함수가 static 함수로 선언되어 있기에 코드 어느 곳에서나, 필요할 때 위의 함수를 호출하기만 하면 된다.

1
2
3
4
5
6
7
8
// 초기 취미 Setting
HobbyManager.setHobbyFlags(HobbyManager.WALKING |
HobbyManager.SINGING |
HobbyManager.DANCING);
// 신규 취미 추가
HobbyManager.addHobby(HobbyManager.PROGRAMMING);
// 취미 보유 여부 확인
HobbyManager.hasHobby(HobbyManager.SINGING);

사실, HobbyManager 를 작성한 본질적인 이유는 취미 보유 여부 확인 이라고 해도 과언이 아니다. 만약, BitMask 를 사용하지 않는다면 어떤 코드를 작성할 수 있을까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ArrayList<String> hobbies = new ArrayList<>();
hobbies.add("WALKING");
hobbies.add("SINGING");
hobbies.add("DANCING");
hobbies.add("PROGRAMMING");

public boolean hasHobby(@NonNull String hobby) {
for (i = 0; i < hobbies.length; i++) {
if (hobby.equals(hobbies.get(i))) {
return true;
}
}
return false;
}

적당히 못난 코드로 작성해 보았다. hasHobby 에서 판단하고 싶은 hobby 를 전달받고, 유저의 취미 리스트를 0부터 하나씩 비교하여 존재할 경우 true, 아니면 false 를 반환하게 된다. 물론, 이렇게 개발해도 크리티컬한 문제는 없다. 이런 코드들이 쌓이고 쌓이면 문제가 될 수는 있겠지만 말이다. 그렇지만 코드가 예쁘고, 조금 더 효율적이면 기분이 조크든요. 기왕 하는 거 예쁘게 작성하면 모두가 행복합니다!


다시 돌아와서

막상 적용하니 문제가 생겼다. 위의 코드를 예시로 들면, 우리 앱에서 지원하고 싶은 취미가 9가지가 넘는단 사실이다. 이 경우에 int 형 대신 long 을 사용해서 더 많은 기준을 잡을 수 있다고는 하는데, 관련 예시가 없어 선뜻 적용하기가 어려웠다. 방법을 찾기 위해 열심히 구글링을 하다가, BitMask 대신 사용할 수 있는 엄청난 것을 발견했다. (자바로 안드로이드 개발하지만 자바를 잘 모르는, 또 나만 몰랐겠지?) 나와 같은 고민을 하는 사람이 없도록, Java는 위대했던 거시다.


결론

EnumSet 쓰세요. 두 번 쓰세요. 특징을 정리하면 다음과 같다.

  • EnumSet 에서 제공하는 모든 메서드는 산술 비트 연산을 사용하여 구현되므로 일반적인 연산 속도가 굉장히 빠르다.
  • HashSet 과 같은 다른 Set 구현체와 비교했을 때, 데이터가 예상 가능한 순서로 저장되어 있다.
    • 계산시 하나의 비트만이 필요하므로 더 빠르다.
    • HashSet 처럼 데이터 저장 버킷을 찾을 때 hash code를 계산할 필요가 없다.
  • EnumSet 은 내부적으로 big vector로 표현된다.
    • 비트 벡터의 특성상 더 작은 메모리를 사용한다.

bit flag, BitMask는 고전적인 방법(…)이라고 한다. 굳이 쓰지 말고, 내부적으로 bit flag를 사용하는 EnumSet 을 사용하자. 그럼, 위의 예시 코드를 바꿔 봐야지!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HobbyManager {
public enum Hobbies {
NONE, WALKING, READING,
SINGING, RUNNING, PROGRAMMING,
SWIMMING, DANCING, SLEEPING,
// 신규 취미 2종 추가
DRAWING, BAKING
}

private static EnumSet<Hobbies> hobbyFlags;

public static boolean hasHobby(Hobbies flag) {
return hobbyFlags.contains(flag);
}

public static void addHobby(Hobbies flag) {
hobbyFlags.add(flag);
}

public static void setHobbyFlags(EnumSet<Hobbies> flags) {
hobbyFlags = flags;
Log.i(TAG, "hobbyFlags Flags: " + hobbyFlags);
}
}

다음과 같이 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
// 초기 취미 Setting
EnumSet<HobbyManager.Hobbies> hobbyEnumSet = EnumSet.of(HobbyManager.Hobbies.WALKING,
HobbyManager.Hobbies.SINGING,
HobbyManager.Hobbies.DANCING);
HobbyManager.setHobbyFlags(hobbyEnumSet);

// 신규 취미 추가
HobbyManager.addHobby(HobbyManager.Hobbies.PROGRAMMING);
// 취미 보유 여부 확인
HobbyManager.hasHobby(HobbyManager.Hobbies.SINGING);

이번 개발 건에서는 '보유 여부 확인’만 하면 되어서 이정도 코드로 충분했으나, 만일 ‘제거’ 가 필요하다면 EnumSet 에서 지원하는 removeHobbyManager 에 적용하여 확장하면 된다. Easy!


참고



  1. 모 기업에서 Android Application 개발을 하고 있음. 다만, 일반적인 Android App과는 약간 다른 형태로, 특성 상 타 앱들과 intent를 주고받는 일이 잦다. ↩︎

  2. 같은 팀이기에 서로의 코드는 언제든지 열람 가능하다. ↩︎

Share