신규 서비스에 Java 17 버전을 사용할까 싶어 이참에 다시 한 번 자바 버전별 특징에 대해 정리해보기로 했다. 

 

 

Java 8 (2014년 3월 출시) - LTS

  • Lambda 표현식 추가 (익명 함수를 생성할 수 있는 기능)
  • Stream API 추가 (컬렉션 요소를 다루는 기능)
  • Date and Time API(Joda Time 기반) 추가 (thread-safe)
  • PermGen 메모리 영역 삭제, 메모리 구조 변경
  • Optional 클래스 (null 값을 처리)
  • 메서드 참조
  • 인터페이스 변경 (default 메서드와 static 메서드를 추가)
  • Completable Future (멀티 스레드 프로그래밍)

Java 9 (2017년 9월 출시)

  • 모듈 시스템 추가(필요한 모듈만 로드하여 불필요한 모듈을 로드하지 않아 보다 경량화된 애플리케이션 개발 가능)
  • 컬렉션 팩토리 메서드 강화
    • Set, List, Map 인터페이스에 Immutable 객체를 생성할 수 있는 새로운 메서드가 추가되었다.
      • List.of("a", "b", "c"), Set.of("a","b"), Map.of("a","abc", "가", "가나다")
  • 인터페이스의 private 메소드 지원
  • G1 GC 기본 GC로 설정 

Java 10 (2018년 3월 출시)

  • 지역 변수 형 추론(var) 추가
  • GC 인터페이스 개선 (Java 10에서는 G1 GC의 최대 힙 크기에 대한 기본값이 변경)

Java 11 (2018년 9월 출시) - LTS

  • HTTP 클라이언트 API 추가 (자바에서도 http로 api 호출이 가능했음 이전에는 라이브러리를 이용해야지만 가능했음)
  • String 클래스에 새로운 메소드 추가 (isBlank(), strip() 등)
  • Files 클래스에 새로운 메소드 추가 (writeString(~), readString() 등)
  • 컬렉션 인터페이스에 새로운 메소드 추가 (.toArray()) -> 원하는 타입의 배열을 선택하여 반환 가능해짐
    • String[] sampleArray = Arrays.asList("Java", "Kotlin").toArray(String[]::new);

Java 12 (2019년 3월 출시)

  • Switch 표현식 개선
  • G1 GC 개선

Java 13 (2019년 9월 출시)

  • Switch 표현식 개선(불필요한 break 문 없이 사용 가능)
String fruit = "banana";
int numLetters = switch (fruit) {
    case "apple", "pear" -> 5;
    case "banana", "kiwi", "orange" -> 6;
    case "avocado", "mango" -> 7;
    default -> 0;
};
  • Text Blocks 추가 (이후 14에서 공식 지원됨)
    • 문자열을 더욱 가독성 있게 작성할 수 있습니다. Text Blocks는 따옴표 대신 백틱(backtick) 기호(`)를 사용하고, 여러 줄의 문자열을 쉽게 작성할 수 있습니다.
String xml = """
    <document>
        <content>
            Text Blocks Example
        </content>
    </document>
""";

Java 14 (2020년 3월 출시)

  • Switch 표현식 개선(화살표 연산자 사용 가능)
  • Record 클래스 추가(데이터 저장 클래스로 final로 선언, getter 메서드와 equals() 및 hashCode() 메서드 자동 생성)
  • 패턴 매칭 instanceof 연산자
if (obj instanceof String) {
    String str = (String) obj;
    // str 객체를 사용합니다.
}

14버전에서는.. 아래와 같이 변경됨으로써 캐스팅 과정을 따로 거치지 않을 수 있게 되었다. 

if (obj instanceof String str) {
    // str 객체를 사용합니다.
}
  • NullPointerException 개선 (발생한 객체의 이름과 값이 함께 출력되어 오류 해결데 도움됨)

Java 15 (2020년 9월 출시)

  • Sealed 클래스 도입(클래스의 상속 제한 가능)
  • Hidden Classes 추가 (Reflection API를 통해 접근할 수 없는 클래스)

Java 16 (2021년 3월 출시)

  • (14의 기능으로부터 확정) instanceof 패턴 매칭 강화
  • Records 클래스 확장
  • Vector API 추가

Java 17 (2021년 9월 출시) - LTS

  • Sealed 클래스 확장
  • 패턴 매칭과 Switch 문 확장
  • AOT(Head of Time) 컴파일러 추가
  • ZGC, Shenandoah GC 개선 등 성능 향상

 

현재 11버전을 사용하고 있고, Optional이나 Stream, 디폴트인터페이스, 람다 등은 이제는 굉장히 유용하게, 빈번하게 사용하는 지라 17로 업그레이드를 할 경우 많은 부분에서 업그레이드가 되지 않을까 하는 기대감이 드는데 더욱 자세히 알아봐야겠다..

 

HashMap: {1=Python, 2=JavaScript}
HashMap after put():
{1=Python, 2=JavaScript, 3=Java}
HashMap after replace():
{1=Python, 2=JavaScript}

 

put() 및 replace() 메서드의 구문은 HashMap에서 매우 유사합니다.

// syntax of put()
hashmap.put(key, value)

// syntax of replace()
hashmap.replace(key, value)

그리고 해시맵에 지정된 키에 대한 매핑이 포함되어 있으면 두 메서드 모두 지정된 키와 연결된 값을 바꿉니다. 그러나 해시맵에 지정된 키로 찾을 수 있는 값이 없는 경우

* put() 메서드는 지정된 Key와 Value에 대한 새 데이터를 삽입합니다.

* replace() 메서드는 null을 반환합니다.

import java.util.HashMap;

class Main {
  public static void main(String[] args) {

    // create an HashMap
    HashMap<Integer, String> languages1 = new HashMap<>();

    // insert entries to HashMap
    languages1.put(1, "Python");
    languages1.put(2, "JavaScript");

    // create another HashMap similar to languages1
    HashMap<Integer, String> languages2 = new HashMap<>();

    // puts all entries from languages1 to languages2
    languages2.putAll(languages1);
    System.out.println("HashMap: " + languages1);

    // use of put()
    languages2.put(3, "Java");
    System.out.println("HashMap after put():\n" + languages2);

    // use of replace()
    languages1.replace(3, "Java");
    System.out.println("HashMap after replace():\n" + languages1);

  }
}

Output

put() 메서드는 HashMap에 새 매핑(3 = Java)을 추가합니다. replace() 메서드는 어떤 작업도 수행하지 않습니다.
 (Key가 없어도 에러는 나지 않네요?)

HashMap: {1=Python, 2=JavaScript}
HashMap after put():
{1=Python, 2=JavaScript, 3=Java}
HashMap after replace():
{1=Python, 2=JavaScript}

 

 

출처

https://www.programiz.com/java-programming/library/hashmap/replace

인터페이스에서 디폴트 메소드를 작성할 수 있게 됨으로써 추상 클래스와 언뜻 보면 차이가 없어 보인다.

실제로 인터페이스에 디폴트 메소드를 작성해서 코드를 작성해보려니 추상으로 풀 수도 있는거 아냐? 라는 생각이..

다시금 추상클래스와 인터페이스와의 차이를 알아보도록 하자.

 

인터페이스와 추상 클래스는 존재 목적이 다르다.

=> 추상 클래스는 추상 클래스를 상속 받아서 기능을 이용하고, 확장하는 데에 있다.

=> 인터페이스는 큰 틀을 사전에 짜놓는 것으로써 그 틀에 맞추어 구현을 하라는 데 의의가 있다. 이를 통해 인터페이스를 구현한 객체들은 동일한 동작을 약속할 수 있게 됩니다.

  • 추상 클래스, 인터페이스 둘 다 불완전 한 것이기 때문에 인스턴스화가 불가, extends하거나 implements해야 합니다.
  • 인터페이스는 여러개 인터페이스를 함께 구현 가능, 추상클래스는 다중 상속이 불가하다.
  • 추상 클래스에는 public, protected, private 메소드를 가질 수 있습니다. 반면에 인터페이스는 public만 허용됩니다.
  • 추상 클래스에는 멤버변수 선언이 가능하지만 인터페이스는 public static final 이어야 한다. 

=> 각각 다른 추상클래스를 상속하는데 공통된 기능이 필요하다면? 해당 기능을 인터페이스로 작성해서 구현

 

 

+ 학원에서 교육 받을 때에는 아래와 같이 교육받았다... 

추상클래스는 IS - A "~이다".

인터페이스는 HAS - A "~을 할 수 있는".

 

출처

https://codechacha.com/ko/java8-default-methods/

https://myjamong.tistory.com/150

현재 오픈 예정인 Admin 서비스의 경우 사용자 활동에 대한 로그를 S3에 남겨야 한다.

처음에 욕심으로 RequestBody에 담기 내용도 올려야지~ 하고는 Interceptor에서 HttpServletRequest에 담긴

Body 내용을 기록했는데 Body 데이터가 Controller단에서 사용이 불가한 것 아니겠는가.

 

HttpServletRequest 인터페이스는 본문을 읽기 위해 getInputStream() 메소드를 갖고 있는데 기본적으로

이 InputStream의 데이터는 한 번만 읽을 수 있도록 설계가 되어 있다.

해당 데이터를 재활용하려면 Wrapper를 이용해야 했는데 개인적인 생각으로 이는 Too much라 생각되었고,

그냥 URL에 담겨있는 ip, method, url 같은 간단한 정보만 기록하는 것으로 일보 후퇴하였다.

이게 데이터를 어쨌든 복사하는 개념이니 Body에 데이터가 많은 경우

매 Request마다 이를 수행하는 것은 성능에도 좋을 수 없겠다는 판단이 들어서이기도 하다.

(never ever 귀찮아서가 아니었음)

 

그로부터 일년이 지났을까..ㅋㅋ 팀장님께서 다른 서버는 몰라도

Admin 서버의 경우에는 Body 데이터를 기록해야 된다는 의견을 주셨고, 결국 이를 수행하기로......

(그래, Admin은 성능보다는 정확성과 기록이 중요한거 아니겠어?!)

 

나는 아래와 같이 설정하였고, 잘 수행되는 것까지 확인하였다.

파일과 같은 멀티파트 타입의 데이터를 보내는 경우에는 

StandardMultipartHttpServletRequest가 Cast 불가하다는 에러를 발생시키기 때문에 

"application/json" 콘텐트 타입만 처리하도록 만들었다. 

 

 

Filter단

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;


@Slf4j
@Component
public class InitSettingFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        CachedBodyHttpServletWrapper cachedBodyHttpServletWrapper = new CachedBodyHttpServletWrapper(httpRequest);
        chain.doFilter(cachedBodyHttpServletWrapper, response);
    }

    @Override
    public void destroy() {
    }
}

 

HttpServletRequestWrapper구현

import lombok.SneakyThrows;
import org.springframework.util.StreamUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class CachedBodyHttpServletWrapper extends HttpServletRequestWrapper {

    private byte[] cachedBody;

    public CachedBodyHttpServletWrapper(HttpServletRequest request) throws IOException {
        super(request);
        InputStream requestInputStream = request.getInputStream();
        this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new CachedBodyServletInputStream(this.cachedBody);
    }

    @Override
    public BufferedReader getReader() throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
        return new BufferedReader(new InputStreamReader(byteArrayInputStream, "UTF-8"));
    }

    /*
    Inner Class
     */
    public class CachedBodyServletInputStream extends ServletInputStream {

        private InputStream cachedBodyInputStream;

        public CachedBodyServletInputStream(byte[] cachedBody) {
            this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
        }

        @SneakyThrows
        @Override
        public boolean isFinished() {
            return cachedBodyInputStream.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readListener) {
        }

        @Override
        public int read() throws IOException {
            return cachedBodyInputStream.read();
        }
    }
}

 

Interceptor단

preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 메소드에 아래의 로직을 추가하였다. 

if (request.getContentType() != null && request.getContentType().contains("application/json")) {
    final CachedBodyHttpServletWrapper cachingRequest = (CachedBodyHttpServletWrapper) request;
    String requestBody;
    if ((requestBody = cachingRequest.getReader().readLine()) != null) {
        // S3로 업로드!
    }
} else {
	// Body 제외한 상태로 S3에 업로드!
}

 

끝!

 

참고

https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times

https://www.geeksforgeeks.org/static-synchronization-in-java/

위 글을 번역한 내용입니다.

 


 

동기화는 공유 리소스에 대한 멀티 스레드의 액세스를 컨트롤 할 수 있는 가능성입니다.

Java에서 동기화는 스레드 간의 안정적인 통신에 필수적이며, 이를 위해 Java에선 synchronized 키워드를 사용합니다.

 

Important Points Regarding Synchronization

  • Object level에 있는 메서드에만 해당됩니다.
  • 메서드 또는 블록이 동기화된 경우 object-level 잠금이 필요합니다.
  • 동기화는 deadlock 상태의 유일한 이유이기 때문에 Java에서 가장 위험한 단어입니다.
  • 필요할 땐 동기화된 키워드를 사용하고 동기화된 블록도 한번 사용해 보십시오.

Static Synchronization

동기화된 메서드는 순서가 지정된 출력을 가져오는 동작이 원하는대로 동작하지 않을 수 있습니다. 

클래스의 객체가 더 많을 경우 특정 인스턴스의 잠금만 획득합니다.

Synchronized을 유지하려면 정적 동기화로 달성할 수 있는 instance-level lock이 아닌  class-level lock이 필요합니다.

 

Static Synchronized 메소드는 두 개의 스레드가 synchronized 메소드에 대해 동시에 정적으로 작동할 수 없도록 Java에서 메소드를 동기화하는 메소드이기도 합니다. 유일한 차이점은 Static Synchronized를 사용하는 것입니다. 하나의 스레드만 메서드에서 작동하도록 class-level lock을 달성하고 있습니다. 스레드는 하나의 스레드만 정적 동기화 메서드에서 작동할 수 있도록 Java 클래스의 class level lock 을 획득합니다.

 

Syntax:

synchronized static return type class name{}

 

6개의 쓰레드가 있다고 가정하자. 실행 순서는 아래와 같다.

 

The complete declarations of  methods are:
method1: public static synchronized void method1()
method2: public static synchronized void method2()
method3: public static void method3()
method4: public synchronized int method4()
method5: public String method5()

 Threads and Classes

  1. 여기서 t1,t2… t6은 스레드 이름입니다.t1.method1()은 Manager 클래스의 클래스 수준 잠금을 획득하면서 실행을 시작합니다.
  2. t2.method2()는 실행 시작 시간을 기다립니다. 정적 동기화 메서드이므로 t1이 이미 클래스 수준 잠금을 획득했기 때문에 클래스 수준 잠금이 필요합니다. t2는 t1이 실행될 때까지 기다려야 합니다.
  3. t3.method2()는 클래스 수준 잠금이 필요하므로 대기하므로 t1이 잠금을 해제할 때까지 기다려야 합니다.
  4. t4.method3()은 잠금이 필요 없는 정적 메서드이므로 실행을 시작합니다.
  5. t5.method4()는 인스턴스 또는(일반) 수준의 동기화 메서드로 실행을 시작하고 개체 수준 잠금이 필요하므로 개체 수준 잠금을 얻습니다.
  6. t6.method5()는 인스턴스 메서드 또는 일반 메서드이므로 실행을 시작합니다.

 

Difference between Synchronized and Static Synchronized in Java

object-level lock. class-level lock.
메서드를 정적으로 선언할 필요는 없습니다.
해당 메서드는 정적으로 선언해야 합니다.
필요시 자주 사용됨 자주 사용 X
각 개체에 대해 다른 인스턴스가 생성됩니다.
전체 프로그램에 대해 단 하나의 인스턴스입니다.

MapStruct를 사용하는 이유
Controller, Service, Repository 등 레이어 간 데이터를 주고받을 때나 비즈니스 로직에서, 하나의 객체를 타입이 다른 객체로 형(Type) 변환하거나 여러 객체를 다른 객체로 합치는 일은 매우 빈번한여 결국 생산성을 떨어뜨리고, 비즈니스 로직에 섞이게 되면 코드가 복잡해집니다.

 

이 API에는 두 Java Bean 간에 자동으로 매핑되는 기능이 포함되어 있습니다.

MapStruct를 사용하여 인터페이스만 생성하면 라이브러리는 컴파일 시간 동안 구체적인 구현을 자동으로 생성합니다.

엔티티와 클라이언트 측으로 나가는 DTO 간에 변환이 발생하는 경우가 많은데 이때 수동으로 빈 mapper를 만드는 것은 시간이 많이 걸리고, 이 부분을 MapStruct가 해결합니다.

 

MapStruct 라이브러리는 자동으로 빈 매퍼 클래스를 생성합니다. MapStruct라이브러리를 추가하고, @Mapper 어노테이션이 선언된 interface를 선언합니다. 구현 클래스를 생성하지 않았도 MapStruct가 가 자동생성해줍니다. mvn clean install을 실행하여 MapStruct 처리를 trigger할 수 있고, 그러면 /target/generated-sources/annotations/ 아래에 구현 클래스가 생성됩니다.

 

Lombok과 함께 사용하는 경우

Lombok과 MapStruct를 함께 사용하면 런타임시 에러가 발생하는 것을 확인할 수 있었습니다.

Project Lombok은 (무엇보다도) 컴파일된 빈 클래스의 AST(추상 구문 트리)에 getter와 setter를 추가하는 주석 처리기인데
AST 수정은 Java 주석 처리 API에서 예측할 수 없으므로 Lombok과 MapStruct 내에서 둘 다 함께 작동하도록 하려면 별도 작업을 해줘야합니다. 기본적으로 MapStruct는 Lombok이 enhanced-bean에 대한 매퍼 클래스를 생성하기 전에 Lombok이 모든 수정을 완료할 때까지 기다립니다. Lombok 1.18.16 이상을 사용하는 경우 Lombok과 MapStruct가 함께 작동하도록 하기 위해 lombok-mapstruct-binding을 추가해야 합니다. MapStruct 또는 Lombok의 이전 버전을 사용하는 경우 솔루션은 Lombok에서 수정할 JavaBeans와 MapStruct에서 처리할 매퍼 인터페이스를 프로젝트의 두 개의 개별 모듈에 넣는 것입니다. 그런 다음 Lombok은 첫 번째 모듈의 컴파일에서 실행되어 두 번째 모듈을 컴파일하는 동안 MapStruct가 실행될 때 빈 클래스가 완료되도록 합니다.

 

작업내용 
mapstruct 라이브러리와 함께 lombok-mapstruct-binding을 추가하고,

아래와 같이 플러그인을 추가해주니 정상 작동 되는 것을 확인할 수 있었습니다.

    <properties>
        <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
        <org.projectlombok>1.18.20</org.projectlombok>
        <lombok-mapstruct-binding>0.2.0</lombok-mapstruct-binding>
    </properties>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok-mapstruct-binding</artifactId>
            <version>${lombok-mapstruct-binding}</version>
        </dependency>
 <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source> <!-- depending on your project -->
                    <target>${java.version}</target> <!-- depending on your project -->
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${org.projectlombok}</version>
                        </path>
                        <!-- This is needed when using Lombok 1.18.16 and above -->
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok-mapstruct-binding</artifactId>
                            <version>${lombok-mapstruct-binding}</version>
                        </path>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                        <!-- other annotation processors -->
                    </annotationProcessorPaths>
                </configuration>
            </plugin>

 

 

@Mapper(componentModel = "spring")
public interface TestMapper {
    Test toTestEntity(TestForm testForm);
}

 

 

참고 

https://mapstruct.org/faq/

 

Frequently Asked Questions (FAQ) – MapStruct

The strategies were developed over time and hence the naming / behavior deserves attention in future versions of MapStruct to getter better allignment. This would introduce backward incompatibillties, so we cannot not do this in the 1.x versions of MapStru

mapstruct.org

https://medium.com/uphill-engineering-design/deep-dive-into-mapstruct-spring-7ddd8dac3d6d

 

Deep dive into Mapstruct @ Spring

How to take advantage of Mapstruct to greatly reduce the amount of boilerplate code that would regularly be written by hand.

medium.com

 

 

 

https://openjdk.java.net/jeps/8277131?fbclid=IwAR1A4nFUHY58UBPisTmhNucUP9ZIKKo1UvhSDdVh1Y63HWLWsih5vISghJE 

 

 

JEP draft: Virtual Threads (Preview)

JEP draft: Virtual Threads (Preview) AuthorsRon Pressler, Alan BatemanOwnerAlan BatemanTypeFeatureScopeSEStatusDraftComponentcore-libsCreated2021/11/15 16:43Updated2021/11/18 14:02Issue8277131 Summary Drastically reduce the effort of writing, maintaining,

openjdk.java.net

위의 글을 번역한 내용입니다.

 

Created 2021/11/15 16:43
Updated 2021/11/18 14:02

JEP : JDK Enhancement Proposal

 

 

(먼저 요약부터)

기존의 자바 스레드(Thread)는 운영체제 레벨에서 관리되는 네이티브 스레드로, 스레드 생성 시 운영체제에서 새로운 스레드를 생성하고, 스레드 스케줄링 및 동기화를 위해 운영체제의 자원을 사용합니다. 따라서 운영체제에서 생성 가능한 스레드의 수에 제한이 있습니다. 이로 인해 자바 애플리케이션에서 너무 많은 스레드를 생성하면 운영체제의 자원 부족으로 성능 저하나 애플리케이션 충돌 등의 문제가 발생할 수 있습니다. JVM 내부에서 스레드 스케줄링과 동기화를 처리하기 때문에 가벼워지고, 더 많은 수의 스레드를 생성할 수 있습니다.

 

반면 자바 가상 스레드는 JVM 내부에서 스레드 스케줄링과 동기화를 처리하기 때문에 운영체제의 자원을 사용하지 않습니다. 이로 인해 운영체제에서 생성 가능한 스레드의 수에 제한이 없어지며, 대규모 서버 애플리케이션에서 높은 스레드 수가 필요한 경우에도 안정적으로 처리할 수 있습니다.


자바 가상 스레드는 더 적은 메모리와 더 높은 확장성을 제공합니다. 이는 여러 개의 가상 스레드를 생성하더라도 운영체제에서 새로운 스레드를 생성하지 않아도 되기 때문입니다. 이러한 이점은 높은 스레드 수가 필요한 대규모 서버 애플리케이션에서 특히 유용합니다.
자바 16부터는 가상 스레드를 지원하는 Project Loom이 추가되어, 자바 가상 스레드를 보다 쉽게 사용할 수 있도록 개선되었습니다.

 

 

Summary

가상의 경량 스레드를 통해 사용할 수 있는 하드웨어를 최대한 활용하고 고처리가 가능한 Concurrent한 응용 프로그램을 작성할 수 있으며 유지 보수 하는데에 드는 공수를 줄입니다.

 

Goals

  • 가상 스레드로 지정된 java.lang.Thread을 추가하면 몇 기가 바이트의 heap에서 수백만 개의 활성 인스턴스로 확장되는데 이는 기존 스레드와 거의 동일한 동작을 나타낸다.
  • 가상 스레드의 트러블슈팅, 디버깅, 프로파일링을 지원하는데 이때 플랫폼 스레드와 최대한 유사한 방식으로 기존 JDK 도구 및 툴링 인터페이스를 이용합니다.

Non-Goals

  • 플랫폼 스레드의 구조를 바꾸는 것은 아니다 (기존 스레드)
  • 자바 메모리 구조를 바꾸는 것이 아님
  • 새로운 데이터 병렬 구조를 제시하는 것이 아님 (parallel stream)

Motivation

개발자는 지난 수십 년 동안 Java를 광범위하게 사용하여 동시성(Concurrent) 응용 프로그램을 작성했고 이때 java.lang.Thread가 core한 역할을 수행했습니다. 스레드는 일부 동시성 단위의 응용 프로그램 단위를 나타낼 때는 잘 작동했습니다. (예를 들면 Transaction). Exception에 대해선 트러블 슈팅을 할 때면 스레드의 스택을 추적하고, 스레드 덤프를 떠서 모든 스레드의 스택을 가져와 프로그램이 수행하는 작업의 Snapshot을 얻는 방식으로 진행했죠. 

 

하지만 이는 어디까지나 어플리케이션(동시 트랜잭션 모음)에 대한 개발자의 논리적 관점에 스레드가 반응하는 경우에 한해서 입니다.  불행히도, Thread을  구현하고 있는 현재 방식은 각 Java 스레드에 대해 OS 스레드를 소비하는데 문제는 OS스레드는 무려 소켓보다도 귀중하고 비용이 많이 드는 자원이라는 것입니다. 이는 최신식의 서버가 OS 스레드보다 훨씬 더 많은 동시 트랜잭션을 처리할 수 있음을 뜻합니다. 높은 처리량을 필요로 하는 서버를 개발하는 개발자들은 하드웨어를 효과적으로 사용하여 낭비를 줄여야 했기 때문에 트랜잭션 간에 스레드를 공유해야 했습니다. 먼저는 스레드 생성비용을 줄이기 위해 스레드풀을 사용했고, 이것이 충분하지 않은 경우에는 I/O를 기다리는 트랜잭션 중간에도 스레드 풀에 스레드를 반환하기 시작했습니다. 이는 비동기식 프로그래밍 방식으로 이어 졌는데 단순히 API 세트를 분리하는 수준이 아니라 플랫폼이 아예 어플리케이션의 logical unit을 알지 못하는 수준으로까지 이어지길 바랬습니다. 결과적으로 플랫폼의 컨텍스트(스레드)가 더 이상 트랜잭션을 나타내지 않아 스레드가 그다지 유용하지 않게 되었기 때문에 문제 해결, 관찰, 디버깅 및 프로파일링이 매우 어려워지는 결과를 초래하게 됩니다. 더 나은 하드웨어 활용도 때문에 개발 및 유지보수가 어려워지게 되었는데 이것 또한 낭비로 볼 수 있겠죠. 그러면 개발자는 논리적 동시성 단위를 스레드로 직접 모델링하는 방식과 하드웨어가 지원할 수 있는 상당한 처리량을 낭비하는 것 중에서 선택해야 하는 경우에 처합니다. 

 

(스레드의 역할 UP vs 스레드의 역할 DOWN)

(OS 스레드 자원을 막 사용 but 개발 및 유지보수 편함 vs 하드웨어 활용도를 높임 but 개발 및 유지보수 어렵)

 

가상 스레드(java.lang.Thread의 구현)는 두 가지 장점을 모두 제공합니다. 가상 스레드에 동일한 동기 API를 사용할 경우 저렴한 스레드는 차단(block)합니다. (우리의 귀중한 OS 스레드를 블록하지 않은 상태로).  이렇게 되면 하드웨어 활용도는 최적화가 되고 높은 수준의 동시성과 높은 처리량을 허용하는 동시에 스레드 기반의 Java 플랫폼 및 해당 도구의 설계와 조화를 유지합니다. 가상 스레드는 플랫폼 스레드에 대한 것이며 가상 메모리는 물리적 RAM에 대한 것입니다. 이것은 물리적 리소스에 대한 자동 및 효율적인 mapping을 통해 가상 리소스에 환상적인 매커니즘을 가져옵니다. 가상 스레드는 저렴하고 풍부하기 때문에 스레드 사용 패턴이 바뀔 것으로 예상할 수 있겠죠? 예를 들면 두 개의 원격 서비스를 참조하여 동시에 응답을 기다리는 오늘날의 서버의 경우 일부 스레드 풀에 두 개의 차단 HTTP 클라이언트 작업을 제출하거나 완료 시 일부 콜백을 알리는 두 개의 비동기 HTTP 클라이언트 작업을 시작할 수 있습니다. 그 대신 각각 트랜잭션을 대신하여 HTTP 클라이언트 호출을 수행하는 것 외에는 아무것도 하지 않는 두 개의 가상 스레드를 생성할 수 있습니다. 이것은 비동기식 옵션만큼 효율적입니다. 스레드 풀 옵션과 달리 요청 기간 동안 두 개의 소중한 OS 스레드를 유지하지 않기 때문이죠. 코드는 스레드 풀 옵션만큼 친숙하고 단순할 뿐만 아니라 스레드가 여러 작업에서 공유되지 않아 스레드 로컬 오염의 위험이 있기 때문에 더 안전하기도 하죠. 

 

가상 스레드를 사용하기 위해 새로운 프로그래밍 모델을 배울 필요가 없습니다. 오늘날 Java를 사용하여 동시 응용 프로그램을 작성하는 사람은 누구나 이 모델을 이미 알고 있으며 이 모델은 Java's original programming model과 같습니다. 우리는 이제 오래된 습관을 버려야 될 것이며 특히 스레드 풀의 사용은 풀링 중인 리소스가 부족하거나 생성하는 데 비용이 많이 드는 경우에만 유용합니다.

 


http://gunsdevlog.blogspot.com/2020/09/java-project-loom-reactive-streams.html

=> 기존 스레드는 자원 사용면에서 효율적이지 못했고, 비동기로 개발할 경우에는 작성 및 디버깅이 어려웠음

=> 이를 가상 스레드를 통해 해결하고자 함.

 

기존 비동기 프로그래밍의 단점은? 

=>  비동기 프로그래밍을 개선한 Future, Promise, Reactive stream을 사용하더라도 로직 제어를 위해 부가적인 코드를 많이 필요로 하기 때문에 기본적으로 비동기 프로그래밍은 제어 흐름을 복잡하게 가져가야 하는 단점이 있다.

=> 스택 트레이스가 유용하지 않아서 스레드에 대한 정보를 파악하기가 어렵다.

=> 어떤 메소드가 Future를 반환하면 다른 메소드들도 마찬가지로  Future을 반환해야 하는데 이는 특정 패러다임을 강제하게 한다. 

 


Description

가상 스레드를 사용하면 동일한 프로세스에서 많은 활성 인스턴스가 공존할 수 있도록 하는 방식으로 JDK에 의해 구현된 java.lang.Thread의 인스턴스입니다.

Thread thread = Thread.ofVirtual().name("duke").unstarted(runnable);

스레드가 가상인지 아닌지는 Thread::isVirtual 메서드로 쿼리할 수 있습니다.

실제로 오늘날과 같이 개발자는 빌더를 사용하여 가상 스레드를 직접 구성하는 경우가 거의 없지만 대신 스레드 생성을 추상화하는 구성을 사용하여 빌더로 생성된 ThreadFactory의 인스턴스를 다음과 같이 사용할 수 있습니다.

ThreadFactory factory = Thread.ofVirtual().factory();

Java 코드에 관한 한 가상 스레드의 의미는 모두 단일 ThreadGroup에 속하고 열거할 수 없다는 점을 제외하면 플랫폼 스레드의 의미와 동일합니다. 그러나 이러한 스레드에서 호출된 Native Code는 다른 동작이 관찰될 수 있습니다. 예를 들어, 동일한 가상 스레드에서 여러 번 호출되면 각 인스턴스에서 다른 OS 스레드 ID를 관찰할 수 있습니다. 또한 OS 수준 모니터링에선 프로세스가 생성된 가상 스레드보다 적은 OS 스레드를 사용하는 되는 것을 관찰할 수 있습니다. 가상 스레드는 OS가 존재를 인식하지 못하기 때문에 OS 수준 모니터링에 보이지 않습니다. JDK는 스택을 포함한 상태를 Java 힙에 저장하여 가상 스레드를 구현합니다. 가상 스레드는 자바 라이브러리의 스케쥴러를 통해 스케쥴링 되는데 작업자 스레드는 가상 스레드가 실행될 때 등에 가상 스레드를 탑재하여 캐리어가 됩니다. 가상 스레드가 고정되면(예: 일부 I/O 작업이나 java.util.concurrent 동기화 구성에서 차단될 때) 가상 스레드는 일시 중단되고 가상 스레드의 캐리어는 다른 작업을 자유롭게 실행할 수 있습니다. 가상 스레드가 풀리게(비고정) 되면(예: I/O 작업 완료에 의해) 스케줄러에 제출되며, 이전에 실행했던 것과 반드시 ​​같을 필요는 없는 일부 캐리어 스레드에서 가상 스레드를 마운트하고 재개합니다. 이런 식으로 가상 스레드가 blocking operation을 수행할 때 OS 스레드를 점유하는 대신 JVM과 그 자리에 예약된 다른 스레드에 의해 일시 중단되는데 모두 OS 스레드를 차단하지 않습니다.

 

가상 스레드를 업고 있는 캐리어 스레드가 해당 OS 스레드를 공유하지만 Java 코드의 관점에서 보면 캐리어와 가상 스레드는 완전히 별개입니다. 캐리어의 신원은 가상 스레드에 알려지지 않고 두 스레드의 스택 추적은 독립적으로 가능합니다. JVM Tool Interface는 플랫폼 스레드와 마찬가지로 가상 스레드를 관찰하고 조작할 수 있지만 아래에 요약한 내용 처럼 일부 작업은 지원되지 않습니다. 특히 JVM TI는 모든 가상 스레드를 열거할 수 없습니다. 마찬가지로 디버거 인터페이스 JDI는 가상 스레드에서 대부분의 작업을 지원하지만 열거할 수는 없습니다. JFR은 가상 스레드에서 발생하는 이벤트를 가상 스레드와 연결합니다. 일반 스레드 덤프는 실행 중인 모든 플랫폼 스레드와 탑재된 가상 스레드를 표시하지만 새로운 종류의 스레드 덤프가 추가되었기 때문에 이는 나중에 설명합니다.


기존에는 스레드가 운영 체제에 의해 관리 및 예약되는 반면 가상 스레드는 가상 머신에서 관리 및 예약 됩니다. 

 

 

java.lang.Thread API

+ Platform 스레드는 현재 Java 플랫폼 버전에서 우리 모두에게 친숙한 스레드를 일컫는다.

 

java.lang.Thread API가 다음과 같이 업데이트되었습니다.

  • Thread.ofVirtual() 및 Thread.ofPlatform과 함께 Thread.Builder가 가상 및 플랫폼 스레드를 생성하는 새로운 API로 추가되었습니다. Thread.Builder를 사용하여 ThreadFactory를 만들 수도 있습니다.
  • Thread.startVirtualThread(Runnable)는 가상 스레드를 시작하기 위한 편리한 방법으로 추가되었습니다.
  • 스레드가 가상 스레드인지 테스트하기 위해 Thread::isVirtual이 추가되었습니다.
  • Thread.join 및 Thread.sleep의 추가되어 wait/sleep 시간을 java.time.Duration으로 제공할 수 있습니다.
  • Thread.getAllStackTraces()는 모든 스레드가 아닌 모든 플랫폼 스레드의 맵을 반환하도록 다시 지정됩니다.

이 외에는 동일하며 생성장에도 변함은 없습니다.

가상 스레드와 플랫폼 스레드 간의 API 차이점은 다음과 같습니다.

  • public constructor는 가상 스레드를 만드는 데 사용할 수 없습니다.
  • 가상 스레드는 데몬 스레드이므로 Thread::setDaemon 메서드를 사용하여 가상 스레드를 데몬이 아닌 스레드로 변경할 수 없습니다.
  • 가상 스레드는 Thread::setPriority 메서드로 변경할 수 없는 고정된 우선 순위인 Thread.NORM_PRIORITY를 갖습니다. 이 제한 사항은 향후 릴리스에서 다시 확인할 수 있습니다.
  • 가상 스레드는 스레드 그룹의 활성 구성원이 아닙니다. Thread::getThreadGroup은 비어 있는 자리 표시자 "VirtualThreads" 스레드 그룹을 반환합니다. Thread.Builder API는 가상 스레드에 대한 스레드 그룹을 설정하는 데 사용할 수 없습니다.
  • SecurityManager 세트로 실행할 때 가상 스레드에는 권한이 없습니다.
  • 가상 스레드는 stop, suspend 또는 resume 메소드를 지원하지 않습니다. 이러한 메서드는 가상 스레드에서 호출되는 경우 예외를 throw하도록 지정됩니다.

 

ThreadFactory virtualThreadFactory = Thread.builder().virtual().factory();
Thread virtualThread = virtualThreadFactory.newThread(printThread);
virtualThread.start();

 

Thread locals

가상 스레드는 플랫폼 스레드와 마찬가지로 스레드 로컬 및 상속 가능한 스레드 로컬을 지원하므로 스레드 로컬을 사용하는 기존 코드를 실행할 수 있습니다. 가상 스레드를 위해  java.base 모듈에서 많이 사용되고 있단 스레드로컬들을 제거하였습니다. 이렇게 하면 수백만 개의 가상 스레드로 실행할 때 메모리 공간에 대한 우려를 줄일 수 있습니다. Thread.Builder API는 스레드를 생성할 때 스레드 로컬을 opt-out하는 방법을 정의합니다. 또한 상속 가능한 스레드 로컬의 초기 값 상속을 거부하는 메서드를 정의합니다. 스레드 로컬을 지원하지 않는 스레드에서 호출될 때를 말하는데 ThreadLocal::get 메서드는 초기 값을 반환하고 ThreadLocal::set 메서드는 예외를 던지게 되어 있습니다. 

JEP: Scope Locals(Preview)는 일부 사용 사례에 대해 스레드 로컬에 대한 더 나은 대안으로 Scope Locals의 추가를 제안합니다.

 

java.util.concurrent APIs

잠금을 지원하는 기본 API인 LockSupport가 가상 스레드를 지원하도록 업데이트되었습니다. 가상 스레드가 고정되면 다른 작업을 수행하기 가능한 상태일 때  캐리어 스레드를 할당해줍니다. 가상 스레드가 unpark되게 되면 스케쥴러에게 반납되게 되어 계속 진행할 수 있게 합니다. LockSupport에 대한 업데이트를 통해 이를 사용하는 모든 API(Locks, Semaphores, blocking queues 등)를 가상 스레드에서 사용할 때 정상적으로 park할 수 있습니다.

 

소수의 API가 추가되었습니다.

  • 완료된 작업의 결과 또는 예외를 얻기 위해 새로운 메서드가 Future에 추가되었습니다. 또한 enum 값으로 작업 상태를 가져오는 새로운 방법으로 업데이트되었습니다. 이러한 추가 기능을 결합하면 Future 객체를 스트림의 요소로 쉽게 사용할 수 있습니다(필터링으로 상태를 테스트할 수 있고 맵을 사용하여 결과 스트림을 얻을 수 있음). 이러한 방법은 구조적 Concurrency를 위해 제안된 API 추가와 함께 사용됩니다.
  • Executors.newThreadPerTaskExecutor 및 Executors.newVirtualThreadPerTaskExecutor가 추가되어 각 작업에 대해 새 스레드를 생성하는 ExecutorService를 반환합니다. 이는 스레드 풀 및 ExecutorService를 사용하는 기존 코드와의 마이그레이션 및 상호 운용성을 위해 사용할 수 있습니다.

블로킹 구간에서 진입하는 경우 실제 스레드가 blocking되는 것이 아니고 LockSupport의 park()가 호출된다. 이때, Continuation.yield()를 통해 캐리어 스레드를 반납하게 되고, IO작업이 끝나면 다시 unpark()가 호출되고 스케줄러가 새로운 캐리어 스레드를 할당하고 scheduler.submit(Continuation)를 통해 계속 진행하게 된다. 

JVM에서 Project Loom을 위한 지원

  • Socket의 블로킹 부분 -> 논블로킹으로 변경
  • java.util.concurrent 논블로킹으로 변경
  • Thread.sleep() 논블로킹으로 변경

가상 스레드는 일반적으로 캐리어 스레드로 사용되는 작은 플랙폼 스레드 세트를 사용합니다. 가상 스레드에서 실행되는 코드는 일반적으로 기본 캐리어 스레드를 인식하지 못합니다. blocking 및 I/O 작업은 캐리어 스레드가 한 가상 스레드에서 다른 가상 스레드로 다시 스케줄링되는 포인트가 됩니다. 가상 스레드가 park되어 schdeuling이 불가능할 수 있는데 parked된 가상 스레드는 unpark하여 스케쥴링을 다시 활성화 시킬 수 있습니다.

 


Networking APIs

java.net 및 java.nio.channels API 패키지에 정의된 네트워킹 API 구현이 가상 스레드와 함께 작동하도록 업데이트되었습니다. block하는 작업(예: 네트워크 연결을 설정하거나 소켓에서 읽는 것)은 다른 작업을 수행하기 위해 캐리어 스레드를 해제합니다.

interruption및 cancellation를 허용하기 위해 java.net.Socket, java.net.ServerSocket 및 java.net.DatagramSocket에 의해 정의된 blocking I/O 메소드는 컨텍스트에서 가상 스레드가 호출될 때 인터럽트 가능하도록 다시 지정되었습니다. 소켓에서 차단된 가상 스레드를 중단하면 스레드가 풀리고, 소켓을 닫습니다.


정의에 따르면 기존 비동기 API는 시스템 호출을 차단하지 않으므로 가상 스레드에서 실행할 때 특별한 처리가 필요하지 않습니다. 동기 API 예를 들어 Socket / ServerSocket/ DatagramSocket, java.nio.channels SocketChannel/ ServerSocketChannel / DatagramChannel같은 API의 경우 비동기 API와 마찬가지로 가상 스레드 안에서 동작하는 데 별다른 조치를 하지 않아도 됩니다. 왜냐면 I/O 동작이 blocking system 자체를 호출하지 않고 selector에 맡겨지기 때문입니다. 하지만 java.net Socket 유형과 NIO 채널은 아닙니다... 동기 네트워킹 Java API는 가상 스레드에서 실행될 때 기본 소켓을 비차단 모드로 전환합니다. Java코드에서 호출된 I/O 작업이 즉시 완료되지 않으면 기본 네이티브 소켓이 JVM 전체 이벤트 알림 메커니즘(poller)에 등록되며 가상 스레드는 parking됩니다. 그리고 그 I/O 작업이 준비되면 (이벤트가 Poller에 도착하면) 가상 스레드는 unparked 상태가 되고 기본 소켓 작업은 재시도됩니다. 

java.io APIs

java.io 패키지는 바이트 및 문자 스트림에 대한 API를 제공합니다. 이러한 API의 구현은 무겁게 동기화되며 가상 스레드에서 이러한 API를 사용할 때 고정되지 않도록 반드시 변경해야 합니다. 원래 바이트 지향 입/출력 스트림은 스레드로부터 안전하지 않았으며 스레드가 read 혹은 write 메소드로 부터 차단되는 동안 close가 호출되었을 때 예상되는 동작을 명시하지 않았습니다. 대부분의 경우에선 concurrent한 스레드에서 input 혹은 output 스트림을 사용하는 게 이해되지 않을 수 있습니다. 문자 지향 reader/writer는 스레드로부터 안전하도록 지정되지 않았지만 하위 클래스에 대한 lock object를 노출시킵니다. 고정하는 것 외에도 동기화는 일관성이 없고 문제가 많습니다, 예를 들면 InputStreamReader와 OutputStreatWriter에서 사용되는 encoder/decoder 스트림은 잠금 개체가 아니라 스트림 단위에서 동기화합니다. 

고정을 피하기 위해 구현은 다음과 같이 변경됩니다.

  • BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter, PrintStream 및 PrintWriter는 직접 사용할 때 모니터가 아닌 명시적 잠금을 사용하도록 변경됩니다. 이러한 클래스는 하위 클래스로 분류될 때 이전과 같이 동기화됩니다.
  • InputStreamReader 및 OutputStreamWriter에서 사용하는 스트림 인코더/디코더는 둘러싸는 InputStreamReader 또는 OutputStreamWriter와 동일한 잠금을 사용하도록 변경됩니다.
  • PushbackInputStream::close는 기본 입력 스트림을 닫을 때 잠금을 유지하지 않도록 변경됩니다.

locking을 변경하는 것 외에도 BufferedOutptuStream, BufferedWriter 및 OutputStreamWriter 구현을 위한 기본 스트림 인코더에서 사용하는 버퍼의 초기 크기가 변경되어 heap에 많은 output스트림 또는 writer가 있는 경우 메모리 사용량을 줄입니다.

 

 

Scheduler

가상 스레드용 스케줄러는 ForkJoinPool을 모방하였는데, First-on-first-out(비동기) 모드에서 작동하며 병렬 처리는 사용 가능한 프로세서 수로 설정됩니다. 일부 blocking API는 대부분의 파일 I/O 작업과 같이 캐리어 스레드를 일시적으로 고정합니다. 이러한 API의 구현은 ForkJoinPool "managed blocker" 메커니즘을 통해 병렬 처리를 일시적으로 확장하여 고정되는 것을 보완합니다. 결과적으로 캐리어 스레드의 수가 사용 가능한 프로세서의 수를 일시적으로 초과할 수 있습니다.
스케줄러는 조정을 위해 두 가지 시스템 속성으로 구성할 수 있습니다.

  • 병렬 처리를 설정하기 위해 jdk.defaultScheduler.parallelism을 사용하며 이것의 기본값은 사용 가능한 프로세서 수로 설정됨
  • 병렬 처리가 확장될 때 캐리어 스레드 수를 제한하는 jdk.defaultScheduler.maxPoolSize. 기본값은 256입니다.

Java Native Interface (JNI)

JNI는 개체가 가상 스레드인지 테스트하기 위해 하나의 새로운 함수인 IsVirtualThread를 정의하도록 업데이트되었습니다. 다른 JNI 사양은 변경되지 않았습니다.

Debugger

debugger 아키텍처는 JVM 도구 인터페이스(JVM TI), 자바 디버그 와이어 프로토콜(JDWP) 및 자바 디버그 인터페이스(JDI)의 세 가지 인터페이스로 구성됩니다. 세 가지 인터페이스 모두 가상 스레드를 지원하도록 업데이트되었습니다.

  • jthread(Thread 객체에 대한 JNI 참조)로 호출되는 대부분의 함수는 가상 스레드에 대한 Thread 객체에 대한 참조로 호출할 수 있습니다. PopFrame, ForceEarlyReturn, StopThread, AgentStartFunction 및 GetThreadCpuTime과 같은 소수의 함수는 가상 스레드에서 지원되지 않습니다. SetLocalXXX 기능은 제한된 경우에만 가상 스레드에서 지원됩니다.
  • GetAllThreads 및 GetAllStackTraces 함수는 모든 스레드가 아닌 모든 플랫폼 스레드를 반환하도록 다시 지정되었습니다.

기존 JVM TI 에이전트는 대부분 이전과 같이 작동하지만 가상 스레드에서 지원되지 않는 기능을 호출하는 경우 오류가 발생할 수 있습니다. 이것은 "가상 스레드를 인식하지 못하는" 에이전트가 가상 스레드를 사용하는 애플리케이션과 함께 사용될 때 발생합니다. 플랫폼 스레드만 포함하는 배열을 반환하도록 GetAllThreads로 변경하는 것도 일부 에이전트의 문제일 수 있습니다. 이벤트를 플랫폼 스레드로만 제한하는 기능이 없기 때문에 ThreadStart/ThreadEnd 이벤트를 활성화하는 기존 에이전트에 대한 성능 문제도 있을 수 있습니다.

JDWP는 다음과 같이 업데이트됩니다.

  • 스레드가 가상 스레드인지 debugger가 테스트할 수 있도록 프로토콜에 새 명령이 추가되었습니다.
  • debugger가 스레드 시작/종료 이벤트를 플랫폼 스레드로 제한할 수 있도록 EventRequest 명령에 새로운 수정자가 추가되었습니다.

JDI는 다음과 같이 업데이트됩니다.

위에서 언급했듯이 가상 스레드는 스레드 그룹의 활성 스레드로 간주되지 않습니다. 결과적으로 스레드 그룹의 플랫폼 스레드 목록을 반환하면 가상 스레드 목록은 반환하지 않습니다.

 

Degrade java.lang.ThreadGroup API

java.lang.ThreadGroup은 가상 스레드를 그룹화하는 데 적합한 API가 아니라 최신 애플리케이션에서는 거의 사용되지 않는, 스레드 그룹화를 위한 deprecated된 레거시 API입니다. ThreadGroup API는 JDK 1.0부터 시작되었고, 스레드에 대한 작업을 제어하는 형식으로 의도된 것입니다. (예를 들면 "stop all threads") 최신 코드는 Java 5부터 java.util.concurrent API에서 제공하는 스레드 풀 API를 사용할 가능성이 더 높습니다. ThreadGroup은 초기 JDK 릴리스에서 애플릿의 격리를 지원했습니다. ThreadGroup은 진단 목적으로도 유용하게끔 의도되었지만 이것은 Java 5 이후 모니터링 및 관리 지원 및 java.lang.management API로 대체되었습니다. 이 외에도 일단 ThreadGroup에는 많은 문제들이 있습니다.. (이와 관련된 문제점은 생략하겠습니다)


Limitations

VM이 가상 스레드를 일시 중단할 수 없는 상황이 있는데 이런 상황을 가지고 가상 스레드를 고정이라고 일컫습니다. 현재로선 두가지의 상황이 있습니다. 

- native 메소드가 현재 가상 스레드에서 실행 중인 경우(Java로 다시 호출하는 경우에도)

- native 모니터가 가상 스레드에 의해 유지되는 경우, 즉 현재 동기화된 블록 또는 메서드 내에서 native monitor가 실행되고 있을 때

첫 번째 제한 사항은 그대로 가겠지만 두 번째 제한 사항은 향후 제거될 수 있습니다. 가상 스레드가 예를 들어 blocking I/O 작업을 수행하여 고정을 시도하면 고정된 상태에서 해제되지 않고 그 밑의 OS 스레드가 작업 기간 동안 차단됩니다. 이러한 이유로 장기간 자주 고정되는 일이 발생하면 가상 스레드의 확장성이 손상될 수 있습니다. 따라서 가상 스레드를 최대한 활용하려면 자주 실행되고 잠재적으로 긴 I/O 작업을 보호하는 동기화된 블록 또는 메서드를 java.util.concurrent.ReentrantLock으로 교체해야 합니다.


Risks and Assumptions

이 제안의 주요 위험은 기존 API 및 구현의 변경으로 인한 호환성 위험입니다.

  • java.io 패키지의 여러 API에서 사용하는 내부 locking이 변경되었습니다. 구체적으로 BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter, PrintStream 및 PrintWriter의 locking이 변경되었습니다. 이는 I/O 작업이 스트림에서 동기화된다고 가정하는 코드에 영향을 줄 수 있습니다. 이 변경은 이러한 클래스를 확장하고 상위 클래스에 의한 locking을 가정하는 코드에는 영향을 미치지 않습니다. 또한 java.io.Reader 또는 java.io.Writer를 확장하고 해당 API에 의해 노출된 lock object를 사용하는 코드에 영향을 주지 않습니다.
  • Legacy인 java.lang.ThreadGroup이 크게 변경되었습니다. ThreadGroup을 명시적으로 없앨 수 있는 기능이 제거 되었고, 데몬 ThreadGroup의 개념도 제거되었습니다. ThreadGroup 일시 중단, 재개 및 중지 메서드는 예외를 던지게끔 변경되었습니다.

가상 스레드나 새 API를 사용하는 신규 코드를 사용하는 기존 코드를 활용 할 때 플랫폼 스레드와 가상 스레드 사이에 몇 가지 동작 차이를 발견할 수 있습니다.

  • Thread stop, suspend 및 resume 메소드는 가상 스레드에서 호출될 때 UnsupportedOperationException을 발생시키도록 지정됩니다.
  • Thread setPriority 메서드는 가상 스레드에서 호출될 때 작동하지 않습니다(가상 스레드의 우선 순위는 항상 Thread.NORM_PRIORITY이므로).
  • 스레드 setDaemon 메서드는 가상 스레드를 데몬이 아닌 스레드로 변경하기 위해 호출되는 경우 UnsupportedOperationException을 발생시킵니다.
  • Thread local을 지원하지 않는 스레드 생성이 가능하도록 업데이트 되었습니다. ThreadLocal::set 및 Thread::setContextClassLoader는 스레드 로컬을 지원하지 않는 스레드 컨텍스트에서 호출되는 경우 UnsupportedOperationException을 발생시키도록 변경되었습니다.
  • Thread.getAllThreadStacks는 모든 스레드가 아닌 모든 플랫폼 스레드의 맵을 반환하도록 다시 지정되었습니다.
  • java.net.Socket, java.net.ServerSocket 및 java.net.DatagramSocket에 의해 정의된 blocking I/O 메소드는 가상 스레드 컨텍스트에서 호출될 때 인터럽트가 가능하게끔 바뀌었습니다. 
  • 가상 스레드는 ThreadGroup의 활성 구성원이 아닙니다. 가상 스레드에서 Thread::getThreadGroup을 호출하면 비어 있는 더미 "VirtualThreads" 그룹이 반환됩니다.
  • SecurityManager 세트로 실행할 때 가상 스레드에는 권한이 없습니다.

 

 

 

 


가상 스레드는 운영 체제가 아닌 Java 가상 머신에 의해 예약된 user-mode 스레드입니다. 가상 스레드는 리소스가 거의 필요하지 않으며 단일 Java 가상 머신은 수백만 개의 가상 스레드를 지원할 수 있습니다. 가상 스레드는 대부분의 시간을 차단하고 I/O 작업이 완료될 때까지 기다리는 작업을 실행하는 데 적합합니다.

 

 

 

결론.

하드웨어 자원을 효율적으로 사용하는 한편, 동시 프로그래밍을 훨씬 더 쉽게 만드는 것이 목표인 가상스레드이다. 처리량이 많은 동시 애플리케이션을 작성, 유지관리, 모니터링하는 데 필요한 리소스를 ‘크게’ 줄이기 위한 자바용 가상 스레드가 세상에 출현하게 된 것이다.  가상 스레드는 JVM에 의해 관리되므로 할당하는 데에 있어 시스템 호출이 필요하지 않고, Context Switching이 없다는 장점을 가지고 있으며, 기존 비동기 프로그래밍보다 디버깅이 쉽다.

가상 스레드는 내부적으로 사용되는 실제 커널 스레드인 캐리어스레드에서 실행되기 때문에 아주 많이 생성할 수 있으며 가상 스레드는 캐리어 스레드를 차단하지 않는다(하지만 네이티브 메소드에서 blocking IO를 수행하면 블로킹되긴 함). 가상스레드를 구성하는 것은 'continuation'과 'scheduler'이다. 

 

 

+ 추가

JEP 444 내용 (https://openjdk.org/jeps/444?fbclid=IwAR2zOZjrxeDCvWHcgnVOkpBLWNKPlrO1DaGU_gjeppWxv7XPfWP1CKpduH8&mibextid=S66gvF)

 

JEP 444에서는 가상 스레드 실행 시스템을 구현하는 내용에 대한 개요를 다루고 있습니다. 구현에 대한 구체적인 내용은 자세히 다루지 않았지만, 구현 과정에서 고려해야 할 몇 가지 주요 개념들을 언급하고 있습니다.

첫째, 가상 스레드 실행 시스템은 기존의 자바 스레드 실행 시스템과 유사하지만, 자바 가상 스레드가 네이티브 스레드와 1:1 대응되지 않으므로, 스레드 풀이나 스레드 생성/제거 등의 동작을 다르게 처리해야 합니다.

둘째, 가상 스레드의 실행 시간과 생명 주기를 관리하기 위해, 새로운 개념인 "Continuation"이 도입됩니다. Continuation은 일시 중지된 가상 스레드의 상태를 저장하고, 이를 다시 로드하여 실행을 재개할 수 있는 객체입니다. Continuation을 사용하면, 더 효율적인 가상 스레드 실행 시간 관리 및 스레드 상태 저장/로드가 가능합니다.

셋째, 가상 스레드 실행 시스템은 기존의 자바 스레드와 호환성을 유지해야 합니다. 이를 위해, 가상 스레드와 자바 스레드가 혼합되어 실행될 수 있도록 구현되어야 합니다.

마지막으로, 가상 스레드 실행 시스템은 높은 확장성과 성능을 보장해야 합니다. 이를 위해, 다양한 스레드 스케줄링 알고리즘과 I/O 관리 방식 등이 고려되어야 합니다.

이러한 개념들을 고려하여, JEP 378에서는 가상 스레드 실행 시스템을 구현하는 내용이 다루어집니다. JEP 378은 JEP 376과 함께 자바 가상 스레드에 대한 API와 실행 시스템을 제공하는 데 필요한 JEP 중 하나입니다.

 

 

 

참고 

http://gunsdevlog.blogspot.com/2020/09/java-project-loom-reactive-streams.html

오랜만에 신규 프로젝트를 생성하여 개발하고 있는데, 설계단에서 조금 어려움을 겪고 있다. 기존 프로젝트보다 더 나은 구현을 해보고 싶은데 생각보다 손이 나가질 않고 있다. 그래서 '개발자가 반드시 정복해야 할 객체 지향과 디자인패턴' 책을 펼쳤고, 책 내용 중 SOLID 부분을 다시금 정리하고 가보려 한다. 

 

 

단일책임원칙 (Single Responsibility Principle)

  • 클래스는 단 한 개의 책임을 가져야 한다. 
  • 예를 들면, 
    • 어떤 클래스에 HttpClinet() 클래스에서 데이터를 로드하는 메소드, 로드된 데이터로 파싱하는 메소드가 있다고 치자.
    • 그런데 HTTP 프로토콜에서 소켓 시반의 프로토콜로 변경되었다.
    • 그렇다면 데이터를 로드하는 메소드, 그리고 파싱하는 메소드 두개 다 변경을 해야 하는 상황인 것이다
    • 이러한 연쇄적은 코드 수정은 두 개의 책임이 한 클래스에 있기 떄문이라고 볼 수 있다. 
    • 데이터를 읽는 것과 데이터를 파싱해서 화면에 보여주는 책임을 분리해야 한다. 
  • 단일 책임 원칙을 잘 지키려면? 
    • 메서드를 실행하는 것이 누구인지 확인해 보는 것 
    • 어떤 클래스에 두개의 메서드가 있는데 각각의 메서드가 A,B 클래스 즉 2개의 클래스에서 사용되는 것이라면 책임 분리 후보가 될 수 있다.

 

개방폐쇄원칙 (Open-closed Principle)

  • 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.  
    • 기능을 변경하거나 확장할 수 있으면서 그 기능을 사용하는 코드는 수정하지 않는다. 
  • 한 인터페이스를 사용하는 클래스는 인터페이스를 구현한 클래스가 추가되더라도 변경되지 않을 것이다. 

 

리스코프 치환 원칙 (Liskov Substitution Principle)

  • 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다. 
  • 리스코프 치환 원칙이 지켜지지 않은 대표적인 예  
    •  이런 코드가 있는데 특수 Item은 무조건 할인을 해주지 않는 정책이 추가되었다고 하자. 이를 반영하기 위해 Coupon 클래스를 아래와 같이 수정할 수 있을 것이다.
    • public class Coupon { public int claculateDiscountAmount(Item item) { return item.getPrice(). * discountRate; } }
    •  위 코드는 아주 흔한 리스코프 치환 원칙 위반 사례이다. Item 타입을 사용하는 코드는 SpecialItem 타입이 존재하는지 알 필요 없이 오직 Item 탗입만 사용해야 하는데 SpecialItem 타입인지의 여부를 확인하고 있다는 것은 SpecialItem이 상위 타입인 Item을 완벽하게 대체하지 못하는 상황이라고 볼 수 있는 것이다. 
    • public class Coupon { public int calculateDiscoutnAmount(Item item) { if (item instanceof SpecialItem) // LSP 위반 발생 return 0; return item.getPrice() * discountRate; } }
    • 타입을 확인하는 기능 (instanceof연산자 같은..)을 사용하는 것은 전형적인 리스코프 치환 언칙을 위반할 때 발생하는 증상이다. 새로운 종류의 하위 타입이 생길 때마다 상위 타입을 사용하는 코드를 수정해줘야 할 가능성을 높이는 것은 개방 폐쇄 원칙을 지킬 수도 없게 하는 것이다. 
    • public class Item { 
      	public boolean isDiscountAbailable() {
          	return true;
          }
      }
      
      public class SpecialItem extends Item {
      	@Override
          public boolean isDiscountAbailable() {
          	return false;
          }
      }
      Item 클래스에 가격 할인 가능 여부를 판단하는 기능을 추가하고, SpecialItem 클래스는 이 기능을 알맞게 재정의 했다. 이렇게 함으로써 Item 클래스만 사용하도록 구현할 수 있게 되었다.
    • public class Coupon {
      	public int calculateDiscountAmount(Item item) {
          	if (!item.isDiscountAvailable()) // instanceof 연산자 사용 제거 
              	return 0;
                  
              return item.getPrice() * discountRate;
          }
      }
      리스코프 치환 원칙이 지켜지지 않으면 쿠폰 예제에서 봤듯이 개방 폐쇄 원칙을 지킬 수 없게 된다. 개방 폐쇄 원칙을 지키지 않으면 기능 확장을 위해 더 많은 부분을 수정해야 하므로, 리스코프 치환 원칙을 지키지 않으면 기능을 확장하기가 어렵게 된다.

 

인터페이스 분리 원칙 (Interface Segregation Priciple) 

  • 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다. 
    • 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙으로 말할 수 있다.
    • 예를 들어
      • AServiceInterface에 읽기, 쓰기, 삭제가 구현되어 있다고 치자. 그런데 읽기 부분에 변경이 발생했다고 치면 쓰기/삭제 등 변경이 필요 없는 소스 코드도 다시 컴파일해야 하는 경우가 생기는 것이다. 이럴 때에는 쓰기/읽기/삭제를 각각의 인터페이스들로 분리함으로써 각 클라이언트가 사용하지 않는 인터페이스에는 변경이 발생하더라도 영향을 받지 않도록 해야 한다. 
      • 자바의 경우 사용하지 않는 인터페이스 변경에 의해 발생하는 소스재컴파일 문제가 발생하진 않지만 인터페이스 분리 원칙은 재컴파일 문제만 관련된 것이 아니라 용도에 맞게 인터페이스를 분리하는 것, 즉 단일 책임 원칙과도 연결된다.

 

의존 역전 원칙 (Dependency Inversion Priciple)

  • 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.
  • 저수준 모듈이 변경되더라도 고수준 모듈은 변경되지 않는 것! 
    • 고수준 모듈 : 어떤 의미 있는 단일 기능을 제공하는 모듈
    • 저수준 모듈 : 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현 
    • 예를 들어, 
      • 1)
        • 암호화 예의 경우 바이트 데이터를 암호화한다는 것이 이 프로그램의 의미 있는 단일 기능으로서 고수준 모듈에 해당된다. 고수준 모듈은 데이터 읽기, 암호화, 데이터 쓰기라는 하위 기능으로 구성되는데, 저수준 모듈은 이 하위 기능을 실제로 어떻게 구현할지에 대한 내용을 다룬다.
      • 2) 
        • '쿠폰을 적용해서 가격 할인을 받을 수 있다.' '쿠폰은 동시에 한 개만 적용 가능하다' --> 고수준
        • '금액 할인 쿠폰', '비율할인쿠폰' 등 다영한 쿠폰이 존재 --> 저수준 
        • 쿠폰을 이용한 가격 계산 모듈이 개별적인 쿠폰 구현에 의존하게 되면 새로운 쿠폰이 추가되거나 변경될 때마다, 가격 계산 모듈이 변경되는 상황이 초래된다. 
  • 의존 역전 원칙은 앞서 리스코프 치환 원칙과 함께 개방 폐쇄 원칙을 따르는 설계를 만들어 주는 기반이 된다. 

 

 

출처 

책 - 개발자가 반드시 정복해야 할 객체 지향과 디자인패턴 (최범균 지음)

+ Recent posts