Developer.

[멋사 백엔드 19기] TIL 9일차

📂 목차


📚 본문

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 값이 들어가버리기도 한다..

stringbuilder-thread-unsafety

이 이유는 append 작업이 원자적이지 않은 작업이기에 StringBuffer 자체가 a 혹은 1을 저장하려고 할 때, 할당된 String 메모리가 부족하면 이를 더 늘리는 작업을 하는데 이 늘리는 작업 때문에 실제 문자열의 길이와 할당된 메모리의 크기가 달라 null 이 생성되게 된다.

또한 a와 1의 저장된 숫자도 다른 것을 알 수 있는데,

...
1111111111111111111
505014
944207
31755
1480976
1480976
2359294

와 같이 출력됨을 볼 수 있다. 이는 출력할 때마다 바뀔 것이다(전체 capacity 는 일정한 값으로 증가하기 때문에 맨 마지막 값은 왠만해서는 안바뀐다).

따라서 Spring 은 multi-thread 를 우선 지원하기에 어떤 컴포넌트가 어떤 방식으로(단일, 다중) 요청을 넣는지 잘 살피고 맞는 구현을 하면 될 것이다.