📂 목차
📚 본문
Enum
enum
으로 선언되어 있지만 사실상 class 이다.
배경
public class Direction {
public static final int NORTH = 0;
public static final int SOUTH = 1;
public static final int EAST = 2;
public static final int WEST = 3;
}
자바 1.5 이전에 상수를 보통 static final 로 정의했지만, 이에 대한 문제는
- 타입 안전성 보장 X
- 디버깅 시 상수 값만 보일 수 있음
- 유지보수가 어려움
이러한 문제점 등으로 타입 안전성과 가독성을 해결하기 위해 enum 이 탄생했다.
enum 선언
public enum Direction {
NORTH, SOUTH, EAST, WEST
}
이는 컴파일러 차원에서 잘못된 값은 넣지 못하기 때문에 사전에 타입 안전성을 가져갈 수 있다.
java.lang.Enum
자바 enum 은 위 클래스를 상속하는 클래스이며, 사실상 위 Direction 의 코드는 컴파일 시 다음과 같다.
public final class Direction extends Enum<Direction> {
public static final Direction NORTH = new Direction("NORTH", 0);
public static final Direction SOUTH = new Direction("SOUTH", 1);
public static final Direction EAST = new Direction("EAST", 2);
public static final Direction WEST = new Direction("WEST", 3);
private Direction(String name, int ordinal) {
super(name, ordinal);
}
public static Direction[] values() { ... } // 모든 enum 반환
public static Direction valueOf(String name) { ... } // 이름으로 검색
}
필드와 생성자
실제로 클래스이기 때문에 필드와 생성자 또한 정의 할 수 있다.
public enum Season {
SPRING("꽃이 피는 계절"),
SUMMER("더운 계절"),
FALL("단풍이 드는 계절"),
WINTER("눈이 오는 계절");
private final String description;
Season(String description) { // private 생성자
this.description = description;
}
public String getDescription() {
return description;
}
}
또한 아래와 같이 abstract 메서드로 오버라이딩도 가능하다. 즉, 각 상수가 서로 다른 동작을 가질 수 있는 다형성까지 가져갈 수 있다.
public enum Operation {
PLUS {
public int apply(int x, int y) { return x + y; }
},
MINUS {
public int apply(int x, int y) { return x - y; }
};
public abstract int apply(int x, int y);
}
상속이 된다면 interface 도 집어넣을 수 있게 된다.
public interface Printable {
void print();
}
public enum Color implements Printable {
RED, GREEN, BLUE;
@Override
public void print() {
System.out.println("Color: " + this.name());
}
}
Enum 은 보통 switch 가독성을 위해 쓰이게 된다.
주의점
ordinal()
사용 지양: 상수의 순서가 바뀌면 로직이 깨지며 대신 name 과 같은 별도의 필드를 사용한다.- 직렬화 시 주의해야 한다. enum 은 싱글턴 보장이라 역직렬화해도 같은 인스턴스가 유지된다.
- 상속 불가 enum 은 상속이 안된다(public final class 이기 때문).
ordinal() 은 enum 내부에 구현되어 있는 것이고 로직에 직접 사용되지 않기 때문에 보지도 않을 것이다.
자주 사용하는 클래스
StringBuffer 와 String Builder
StringBuffer
- 동기화를 지원함
- 성능이 느림
- 스레드 세이프
- 멀티 스레드 환경에서 사용하면 좋다.
Serializable
이라는 인터페이스를 구현하지만 이는 메타데이터 용도로만 쓰이고
StringBuilder
- 비동기
- 성능이 빠름
- 쓰레드 세이프하지 않음
- 단일 스레드 환경에서 사용하면 좋다.
String Builder 의 thread unsafety 살펴보기
이번엔 StringBuilder 가 두 개의 쓰레드를 사용했을때 데이터 일관성을 가져가지 못하는 상황을 재현해보자.
public class Main {
static final StringBuilder SB = new StringBuilder();
public static void main(String[] args) throws InterruptedException {
Runnable run1 = () -> {
for (int i = 0; i < 1_000_000; i++) {
SB.append("a");
}
};
Runnable run2 = () -> {
for (int i = 0; i < 1_000_000; i++) {
SB.append(1);
}
};
Thread t1 = new Thread(run1);
Thread t2 = new Thread(run2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(SB);
long a = SB.chars().filter(c -> c == 'a').count();
long one = SB.chars().filter(c -> c == '1').count();
long na = SB.chars().filter(c -> c == '\0').count();
System.out.println(a);
System.out.println(one);
System.out.println(na);
System.out.println(a+one+na);
System.out.println(SB.length());
System.out.println(SB.capacity());
}
}
위를 출력해보면 a가 먼저 출력이 된 후에 1이 출력이 되어야 하는데, a와 1이 번갈아가면서 되어지기도 하고, 어쩔때는 null 값이 들어가버리기도 한다..
이 이유는 append
작업이 원자적이지 않은 작업이기에 StringBuffer
자체가 a 혹은 1을 저장하려고 할 때, 할당된 String 메모리가 부족하면 이를 더 늘리는 작업을 하는데 이 늘리는 작업 때문에 실제 문자열의 길이와 할당된 메모리의 크기가 달라 null 이 생성되게 된다.
또한 a와 1의 저장된 숫자도 다른 것을 알 수 있는데,
...
1111111111111111111
505014
944207
31755
1480976
1480976
2359294
와 같이 출력됨을 볼 수 있다. 이는 출력할 때마다 바뀔 것이다(전체 capacity 는 일정한 값으로 증가하기 때문에 맨 마지막 값은 왠만해서는 안바뀐다).
따라서 Spring 은 multi-thread 를 우선 지원하기에 어떤 컴포넌트가 어떤 방식으로(단일, 다중) 요청을 넣는지 잘 살피고 맞는 구현을 하면 될 것이다.