The Results Shocked Me — I Tried Writing Java Like It’s 2025

Legacy who? I pushed Java to the future — and it pushed back.

medium.com

 

자바를 2025년 답게 쓰자는 글 입니다.

몇몇 공감가고, 도입예정인 부분들에 대해서만 코멘트 남기려 합니다.

✨ Switched to Virtual Threads — Everything Changed

Virtual Thread (이하 VT)에 대한 사용이 가장 먼저 나오는데, 기본적인 개념은 있지만 사용해 본적은 없어
Dawn-Cs-Study 프로젝트를 하면서 사용해볼까 합니다.

VT의 개념자체가 자바가 사용하는 OS Thread를 VT를 핸들링하는 Carrier Thread로 사용하여 
한정된 스레드 갯수로 더 많은 스레드를 태우겠다는 것으로 알고있습니다.

 

Self Study • masiljangajji

Self Study

github.com

아래는 VT 도입 이전, Thread, Executor 관련해 공부하면서 남긴 내용들입니다.

Thead 쓸 때마다 생성해서 쓰면 안되나요?

기존에는 Thread 객체를 직접 생성해서 start() 해야 했음

Thread thread = new Thread(() -> {
    System.out.println("Hello World!");
    });

thread.start();  // 새로운 스레드 생성 후 실행 
thread.run();  // 기존 요청을 물고온 스레드로 실행 (스레드 생성 X)

이 방법은 조금 머리아픈게.. new Thread() 할 때마다 스레드가 생성 됨
start() 실행이 끝나면 OS Thread는 즉시 반납되지만, 자바 Thread 객체는 힙에남아서 공간을 점유함

또 한 번 실행이 끝난 Thread 객체는 다시 start() 못하며, 몇 개의 스레드가 만들어졌는지 예상하기 어려움.
실행자체는 큰 문제가 없을 수 있으나, 관리의 측면에서 생산성이 매우 떨어진다.

Executor Vs Executors

Thread 실행 정책을 추상화 하여 만든 Interface
new Thread() 를 직접 다루지 않도록 하기 위해 도입됨

public interface Executor {
    void execute(Runnable command);
}

스레드 생성, 스케줄링 전략은 구현체에 위임합니다.
ThreadPoolExecutor, ScheduledThreadPoolExecutor 등이 대표적인 구현체

        return new ThreadPoolExecutor(
                core,
                max,
                60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(queueCapacity),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

그런데.. ThreadPoolExecutor는 생성자 파라미터가 너무 많아서 설정이 귀찮습니다.
그렇기 때문에 자주 쓰는 패턴을 간단히 팩토리 메서드로 제공하기 위해 Executors 유틸 클래스가 만들어집니다.

Executors.newFixedThreadPool(int nThreads);   // 고정 크기 스레드풀
Executors.newCachedThreadPool();              // 필요시 스레드 생성, 유휴 시 종료
Executors.newSingleThreadExecutor();          // 1개 스레드만 사용
Executors.newScheduledThreadPool(int core);   // 주기적 실행 지원

하지만.. 실무에서는 보통 세부적인 커스텀을 하기 때문에 ThreadPoolExecutor 만들어 쓰는 경우가 더 많습니다.

여기서 한발자국 더 나아가면 이제는 OS Thread 하나에 여러 VT를 태워서 쓰는 방식으로 갑니다.
더 적은 Thread로 더 많은 작업을 수행할 수 있겠죠?

🧬 Embraced Records + Sealed Classes

레코드를 사용하자! 너무나 맞는 말입니다.
보통 DTO를 다룰때 Record 사용하거나 VO 같은 녀석들을 다룰때 사용하지 않나 싶습니다.

개인적으로는 불변성을 지켜줘야 한다!, 얘는 불변해!, 이런 의미를 내포하고 싶을때도 사용합니다.

sealed 키워드는 사용은 안해봤는데요, 이번 기회에 알아봤습니다.

이 타입이 어떤 클래스/인터페이스가 상속할 수 있는지를 명시적으로 제한합니다.

public sealed interface Event 
    permits LoginEvent, LogoutEvent {}

public final class LoginEvent implements Event {
    // 더 확장 불가
}

public final class LogoutEvent implements Event {
    // 더 확장 불가
}

이런식으로 구현이 되어지는데, sealed의 구현체는 항상 final, sealed, non-sealed 중 하나여야 합니다.

언제 유용한가?

sealed는 하위 타입의 집합이 닫혀있음을 의미합니다.
따라서...

public sealed interface Event permits LoginEvent, LogoutEvent {}

public final class LoginEvent implements Event {}
public final class LogoutEvent implements Event {}

LoginEvent,LogoutEvent 외에 다른 이벤트가 나오면 안되는 경우, 컴파일 타임때 Event 를 구현하는 상황을 막을 수 있습니다.
DDD에서는 클래스들을 집합화 하고, 이를 Context로 묶는 경향이 있기 때문에 DDD를 도입한다면 명시적으로 나타내기 좋지 않을까? 생각도 듭니다.

또한 switch문과 결합시키는 패턴도 유용합니다.

sealed interface Event permits LoginEvent, LogoutEvent, SignUpEvent {}

record LoginEvent(String email) implements Event {}
record LogoutEvent(Long userId) implements Event {}
record SignUpEvent(String email, String nickname) implements Event {}

public class EventHandler {

    public String handle(Event e) {
        return switch (e) {
            case LoginEvent login   -> "로그인 처리: " + login.email();
            case LogoutEvent logout -> "로그아웃 처리: " + logout.userId();
            case SignUpEvent signUp -> "회원가입 처리: " + signUp.email();
        };
    }

    public static void main(String[] args) {
        EventHandler handler = new EventHandler();

        System.out.println(handler.handle(new LoginEvent("user@test.com")));
        System.out.println(handler.handle(new LogoutEvent(42L)));
        System.out.println(handler.handle(new SignUpEvent("new@test.com", "승재")));
    }
}

또한 Pattern Matching for switch문법은 JDK 17은 preview고 21부터 정식지원입니다.

+ Recent posts