도서/자바의 신

[도서/자바의 신] #31 Java 7에 추가된 것들에는?

yulee_to 2023. 1. 2. 20:33

자바의 신

✔️이 글은 [자바의 신 - 이상민 지음] 도서를 바탕으로 정리한 글입니다. 


Fork/Join은 CPU를 더 쉽게, 효율적으로 사용하기 위해서 만들어진 거에요

Java 7에서 추가된 클래스 중에는 Fork/Join과 관련된 클래스들이 존재한다. 

Fork/Join은 어떤 계산 작업을 할 때 여러 개로 나누어(Fork) 계산한 후 결과를 모으는 작업(Join)을 의미한다.

Fork/Join에는 Work stealing이라는 개념이 있다. 여러개의 Dequeue가 있을 때 하나의 Dequeue는 바쁘고, 다른 Dequeue는 한가하다면 한가한 Dequeue가 바쁜 Dequeue의 일을 가져가서 해주는 것을 Working stealing이라 한다. 

Fork/Join을 사용하면 별도로 구현해주지 않아도 라이브러리에서 Work Steal 작업을 알아서 수행해준다. 

Work steal은 CPU가 많은 장비에서 계산 위주의 작업을 빠르게 해야할 필요가 있을 때 매우 유용하게 쓰인다. 

 

Fork/Join 작업의 기본 수행 개념

if ( 작업의 단위가 충분히 작을 경우 ) {
    해당 작업을 수행
} else {
    작업을 반으로 쪼개어 두 개의 작업을 나눔
    두 작업을 동시에 실행시키고, 두 작업이 끝날때까지 결과를 기다림 
}

보통 이 연산은 회귀적으로(Recursive하게) 수행될 때 많이 사용한다. 

 

Fork/Join 기능은 java.util.concurrent 패키지의 RecursiveAction과 RecursiveTask라는 abstract 클래스를 사용해야 한다. 

public abstract class RecursiveAction extends ForkJoinTask<Void> 
public abstract class RecursiveTask<V> extends ForkJoinTask<V>

RecursiveTask 클래스는 제네릭이고, 결과를 리턴도 해주지만 RecursiveAction은 제네릭도, 결과를 리턴해주지도 않는다. 

 

ForkJoinTask 클래스 선언부

public abstract class ForkJoinTask<V> extends Object
    implements Future<V>, Serializable

Future 인터페이스는 비동기적인(asynchronous) 요청을 하고 응답을 기다릴 때 사용되고, Java 5부터 추가되었다. 

 

Fork/Join 작업을 수행하기 위한 클래스로 RecursiveTask나 RecursiveAction 클래스를 확장해주고, 두 클래스의 공통된 메소드 compute()를 사용하여 재귀호출하고, 연산을 수행한다. 클래스를 만든 후에는 ForkJoinPool 클래스를 사용해 작업을 하면 된다.

 

ForkJoinPool 클래스의 주요 메소드

  Fork/Join 클라 밖에서 호출 Fork/Join 클라 내에서 호출
비 동기적 호출 수행시 execute(ForkJoinTask) ForkJoinTask.fork()
호출 후 결과 대기 invoke(ForkJoinTask) ForkJoinTask.invoke()
호출 후 Future 객체 수신 submit(ForkJoinTask) ForkJoinTask.fork()

 

Fork/Join 예제 

아래 for문을 Fork/Join 형태로 compute() 메소드를 Override

long tatal = 0;
for(long loop = from; loop <= to; loop++)
    total += loop;
public Long compute() {
	long gap = to - from;
    
    if( gap <= 3) {
    	long tempSum = 0;
        for(long loop = from; loop <= to; loop++) {
        	tempSum += loop;
        }
        return tempSum;
    }
    
    long middle = (from + to) / 2; // 작업을 나누기 위한 중간값 구하기
    GetSum sumPre = new GetSum(from, middle); // 작업 수행을 위한 객체 생성
    sumPre.fork();  //fork로 다른 쓰레드에서 작업 하나를 비동기로 수행하게 함
    GetSum sumPost = new GetSum(middle+1, to); // 작업 수행을 위한 객체 생성
    return sumPost.compute()+sumPre.join(); //또 다른 작업 수행한 후 fork()로 시작한 작업을 기다림
 }

이렇게 RecursiveTask 클래스를 확장한 클래스(GetSum)에 compute() 메소드를 Override해주고 이 클래스를 사용하기 위한 예제 클래스에서 GetSum 클래스를 생성하고, ForkJoinPool 클래스에 선언되어 있는 invoke() 메소드에 계산을 수행하는 객체를 넘겨주면 작업이 시작되고 그 결과를 받으면 된다. 

쓰레드 객체를 만들지 않고, 쓰레드 작업을 할당해주지 않아도 JVM에서 알아서 처리해준다.

NIO도 잘 모르는데 NIO2가 나왔다

NIO는 I/O처리를 더 빠르게 하기 위해 JDK 1.4에서부터 제공되었다.

 

NIO2는 Java 7에서부터 제공되었다. java.io.File 클래스를 보완하는 내용이 주로 포함되어 있다. 

파일의 속성과 심볼릭 링크(Symbolic link)까지 처리할 수 있는 기능과 어떤 파일이 변경되었는지 쉽게 확인할 수 있는 WatchService 클래스도 제공한고, 몇가지 채널들도 추가되었다. 

 

File 클래스 단점

  • 심볼릭 링크, 속성, 파일의 권한 등에 대한 기능 없음
  • 파일 삭제하는 delete() 수행 실패시 아무런 예외를 발생시키지 않고, boolean 타입의 결과만 리턴
  • 파일이 변경되었는지 확인하는 방법은 lastModified() 메소드에서 제공해주는 long 타입의 결과로 이전 시간과 비교하는 수 밖에 없으며, 이 메소드가 호출되면 연계되어 호출되는 클래스가 다수 존재하며 성능상 문제가 많음

NIO2에서 File 클래스 대체하는 클래스들 - java.nio.file 패키지에 있음

  • Paths : 이 클래스의 static한 get() 메소드로 Path라는 인터페이스의 객체를 얻을 수 있음. Path는 파일과 경로에 대한 정보를 갖고 있음
  • Files : 기존 File 클래스에서 제공되던 클래스의 단점을 보완한 클래스로 Path 객체를 사용해 파일을 통제하는 데 사용됨
  • FileSystems : 현재 사용중인 파일 시스템에 대한 정보를 처리하는 데 필요한 메소드 제공, 이 클래스에서 제공되는 static한 getDefault() 메소드를 사용하면 현재 사용중인 기본 파일 시스템에 대한 정보를 갖고 있는 FileSystem 객체를 얻을 수 있음 
  • FileStore : 파일을 저장하는 디바이스, 파티션, 볼륨 등에 대한 정보들을 확인하는 데 필요한 메소드 제공

 

Paths클래스는 생성자가 업속, 단지 두개의 static한 get() 메소드를 통해 Path 객체를 얻을 수 있다.

  • get(String first, String ... more) : 내부에서는 FileSystems.getDefault().getPath(first, more) 식으로 동작
  • get(URI uri) 

이 외에도 java.io.File의 toPath() 메소드를 통해서도 Path 객체를 얻을 수 있다. 

Paths의 get() 메소드 호출시 내부에서는 FileSystems.getDefault().getPath(first, more) 식으로 동작한다. 

 

Path의 주요 메소드

  • relativize() : 매개 변수로 넘긴 Path와 현재 Path와의 사앧 겨로를 리턴. 현재 Path에서 매개 변수로 넘긴 경로로 커맨드 창을 옮길 때 사용해주면 좋음
  • toAbsolutePath() : 상대 경로로 되어 있는 것을 절대 경로로 변경, 절대경로에는 ..과 같은 경로는 그대로 남음
  • normalize() : 경로 상에 있는 .이나 ..을 없앰
  • resolve() : 매개 변수로 넘어온 문자열을 하나의 경로로 생각하고, 현재 Path의 가장 마지막 path로 추가, Path를 매개변수로도 받음 

Files 클래스는 파일을 다루기 위한 클래스랍니다

Files 클래스의 주요 기능

  • copy(), move() : 복사 및 이동
  • createDirectories(), createDirectory(), createFile(), createLink(), createSymbolicLink(), createTempDirectory(), createTempFile() : 파일, 디렉터리 등 생성
  • delete(), deleteIfExsits() : 삭제
  • newBufferedReader(), newBufferedWriter(), newByteChannel(), newDirectoryStream(), newInputStream(), newOutputStream() : Stream 및 객체 생성
  • get이나 is로 시작하는 메소드 : 각종 확인

예제 1) 파일 쓰기

매개변수 path를 경로의 파일에 데이터를 저장하고, Path 객체를 리턴해주는 writeFile(Path path) 메소드 

1. Charset 객체(charset)에 Charset.forName() 메소드를 호출해 파일의 문자열 캐릭터 셋을 지정

2. 만들어둔 getContents() 메소드로 파일에 쓸 내용을 List<String> 타입으로 받아옴 

3. StandardOpenOption 객체(openOption)를 생성해 StandardOpenOption.CREATE 상수를 지정(파일을 열 때의 조건)

4. File.write()를 리턴해줌 

   4.1. write(Path path, byte[] bytes, OpenOption ... options) 

   4.2. write(Path path, Iterable <? extends CharSequence> lines, Charset cs, OpenOption... options) 

 

StandardOpenOption이라는 enum 클래스에는 APPEND, CREATE, CREATE_NEW, DELETE_ON_CLOSE, DSYNC(순차적으로 처리), READ, SPARSE(파일을 sparse), SYNC(메타 데이터까지 순차적으로 처리), TRUNCATE_EXISTING, WRITE 상수가 있다. 

 

예제 2) 파일 읽기

파일을 읽어서 콘솔에 그 결과를 출력하는 readFile() 메소드

1. Charset.forName()으로 파일의 문자열 캐릭터 셋을 지정

2. Files.readAlllines(path, charset)으로 파일의 내용을 List<String> 타입으로 읽어옴

3. for-each 문을 이용해 읽어온 데이터 출력

 

Files 클래스에서 파일을 읽는데 사용하는 메소드에는 readAllBytes()와 readAllLines()가 있다. 전자는 데이터를 바이트 배열로 받고, 후자는 List로 리턴된다. Mega Bytes나 Giga bytes 짜리 파일을 이 메소드로 읽으면 OutOfMemoryError가 발생한다. 

 

예제 3) 파일 복사/이동/삭제하기

copyMoveDelete(Path fromPath, String fileName) 메소드 구현

1. Path 객체(toPath)에 fromPath.toAbsolutePath().getParent() 메소드로 파일 명의 앞 단계의 경로까지만 정보를 받아옴

2. Path 객체(copyPath)에 toPath.resolve("copied")라는 새로운 경로를 만듦

3. copied 경로가 존재하지 않으면 디렉터리 새로 생성 Files.createDirectories(copyPath)

4. copyPath.resolve(fileName)으로 파일 경로를 지정하는 객체(copiedFilePath) 생성

5. StandardCopyOption으로 REPLACE_EXISTING 복사 옵션을 지정

6. Files.copy() 메소드로 파일 복사

7. Files.move() 메소드로 파일을 이동 혹은 이름 변경

8. Files.delete()로 파일과 경로 삭제, 기본적으로 delete()는 지우려는 디렉터리에 파일이 남아 있으면 예외를 발생시킴

 

StandardCopyOption이라는 enum 클래스에는 ATOMIC_MOVE(단일 파일 이동), COPY_ATTRIBUTES(속성 정보 포함해서 복사), REPLACE_EXISTING(기존 파일이 있으면 새 파일로 변경)이라는 상수가 선어되어 있다. 

 

Files 클래스에는 임시 디렉터리와 파일을 만들 수 있는 createTempDirectory(), createTempFile()이라는 메소드도 제공한다.

파일이 변경되었는지 확인하는 WatchService 클래스도 추가되었어요

어떤 프로그램을 작성하면, 파일이 변경되었는지 확인하는 작업이 필요하다. 

자바에서 파일이 변경되었는지 확인하려면 lastModified() 메소드로 최근 변경된 파일의 시간과 기존에 저장된 시간과 비교해 확인하는 수 박에 없었다. 이 방법은 주기적으로 메소드를 호출해줘야 하고, 이 메소드 한번 호출에 내부적으로 호출되는 연계 메소드가 많아 성능이 안좋다.

 

Java7부터는 이런 단점을 보완한 WatchService라는 인터페이스를 제공한다. 어떤 디렉터리에 누군가 파일을 생성하건, 수정하거나 삭제하면 WatchService를 구현한 클래스에 알려준다. lastModified()는 주기적으로 호출해줘야 했지만 WatchService를 사용하면 이슈가 발생했을 때에만 알려준다. 

 

WatchService 예제

package niosecond;

import static java.io.File.separator;
import static java.nio.file.StandardWatchEventKinds.*;

import java.io.IOException;
import java.nio.file.*;
import java.util.List;
public class WatcherSample extends Thread{
    String dirName;
    public static void main(String [] args) throws Exception{
        String dirName = ".."+ separator + "godofjava";
        String fileName = "WathcerSample.txt";
        WatcherSample sample = new WatcherSample(dirName);
        sample.setDaemon(true);
        sample.start();
        Thread.sleep(1000);
        for(int loop = 0; loop <10; loop++ ) {
            sample.fileWriteDelete(dirName, fileName+loop);
        }
    }
    public WatcherSample(String dirName){
        this.dirName = dirName;
    }
    public void run() {
        System.out.println("### Watcher thread is started ###");
        System.out.format("Dir=%s\n", dirName);
        addWatcher();
    }
    public void addWatcher() {
        try{
            Path dir = Paths.get(dirName);

            // FileSystems.getDefault()를 호출해 기본 파일 시스템 객체를 얻은 후에, 그 객체의 newWatchService() 메소드로 객체를 얻어온다.
            WatchService watcher = FileSystems.getDefault().newWatchService();
            // Path 클래스의 register 메소드로 어떤 작업에 대해서 감시를 할 것인지 지정, 이벤트 상수들은 StandardWatchEventKinds에 선언되어 있다.
            WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
            while( true) {
                // 선언된 3개의 이벤트 중 한 가지가 발생하면 watcher에 해당 이벤트가 등록되고, take() 메소드로 전달된다.
                // take() 호출 부분에는 이벤트가 발생할 때까지 기다리게 되고, 이벤트를 받으면 WatchKey객체가 리턴.
                key = watcher.take();
                // 객체에 여러개의 이벤트가 포함될 수 있어 pollEvents()를 호출하면 WatchEvent 객체가 들어있는 List 형태로 리턴
                List<WatchEvent<?>> eventList = key.pollEvents();

                // 이벤트가 두개 이상 저장되어 있을 수 있으니 하나씩 꺼내서 확인
                for(WatchEvent<?> event : eventList) {
                    Path name = (Path) event.context(); // 객체 선언시 선언했던 제네릭 타입을 리턴, 일반적으론 Path 리턴
                    if( event.kind() == ENTRY_CREATE)
                        System.out.format("%s created%n", name);
                    else if( event.kind() == ENTRY_DELETE)
                        System.out.format("%s deleted%n", name);
                    else if( event.kind() == ENTRY_MODIFY)
                        System.out.format("%s modified%n", name);
                }
                key.reset(); // 일반적으론 이벤트가 다시 발생할 떄까지 대기 상태로 넘어감
            }
        }
        catch(IOException|InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void fileWriteDelete(String dirName, String fileName){
        Path path = Paths.get(dirName, fileName);
        String contents = "Watcher sample";
        StandardOpenOption openOption = StandardOpenOption.CREATE;
        
        try{
            System.out.println("Write " + fileName);
            Files.write(path, contents.getBytes(), openOption);
            Files.delete(path);
            Thread.sleep(100);
        }catch(IOException|InterruptedException e){
            e.printStackTrace();
        }
    }
}

해당 예제로 지정해준 디렉터리를 감시하기 시작하고, 파일이 생성, 수정, 삭제 작업을 수행한 것을 볼 수 있다. 

WatcherSample 클래스가 쓰레드로 동작하는데, setDaemon(true)로 지정했기 때문에 while을 벗어나지 않아도 프로그램이 자동으로 멈춘다. 

파일과 관련된 다른 새로운 API에는 어떤 것들이 있을까?

Java 7의 NIO2에 추가된 기능

  • SeekableByteChannel(random access) : 이 인터페이스는 java.nio.channels 패키지에 선언되어 있으며, 바이트 기반의 채널을 처리하고, 현재의 위치를 관리, 해당 위치가 변경되는 것을 허용줘서 채널을 보다 유연하게 처리하는 데 사용한다. 
  • NetworkChannel : 네트워크 소켓을 처리하기 위한 채널로 네트워크 연결에 대한 바인딩, 소켓 옵션을 세팅하고, 로컬 주소를 알려주는 인터페이스이다. 
  • MulticastChannel : IP 멀티캐스트(IP를 그룹으로 묶고, 그 그룹에 데이터 전송하는 방식)를 지원하는 네트워크 채널이다. 
  • AsynchronousFileChannel : 비동기적으로 파일을 읽는데 사용된다. 처리한 결과는 java.util.concurrent 패키지의 Future 객체나 CompletionHandler 인터페이스(모든 데이터 성공적으로 처리되었을 때 수행되는 completed(), 실패했을 때 처리되는 failed() 메소드 제공)를 구현한 객체로 받을 수있 있다. 
  • AsynchronousChannelGroup : 비동기적인 처리를 하는 쓰레드 풀(thread pool)을 제공하여 더 안정적인 처리가 가능하다. 

 

채널은 디바이스, 파일, 네트워크 등과의 연결 상태를 나타내는 클래스이다. 파일을 읽거나 네트워크에서 데이터를 받는 작업을 위한 통로라고 생각하면 된다. 

Fork/Join과 NIO2 외에 추가 및 변경된 것들을 간단히 살펴보죠

Java 7에서 변경된 사항

  • JDBC 4.1 : JDBC가 1.4로 업그레이드되면서 RowSetFactory와 RowSetProvider라는 클래스가 추가되었다. JDK 1.4부터 제공된 RowSet이라는 인터페이스를 사용하면 Connection 및 Statement 객체를 생성할 필요 없이 SQL Query를 수행할 수 있는 데 RowSetFactory와 RowSetProvider로 RowSet 객체를 쉽게 생성할 수 있다. 
  • TransferQueue 추가 : java.util.concurrent 패키지에 추가된 인터페이스로 어떤 메시지를 처리할 떄 유용하게 사용할 수 있다. 쓰레드에서 Producer/Consumer 패턴으로 특정 타입의 객체를 처리하는 쓰레드 풀을 미리 만들어 놓고 처리하는 기능을 일반화해 SynchronousQueue 기능을 인터페이스로 끌어올리고, 좀더 일반화해서 BlockingQueue를 확장했다. 
  • Objects 클래스 추가 : java.util 패키지에 추가된 클래스로 compare(), equals(), hash(), hashCode(), toString() 등의 static한 메소드를 제공하는데 매개변수로 넘어온 객체가 null이여도 예외를 발생시키지 않는다. 

정리해 봅시다

1. Java 7에서 새로 추가된 Fork/Join은 어떤 것을 의미하나요?

계산을 나눠서 할 수 있는 것을 의미한다. 

2. Fork/Join에 있는 Work steal이라는 개념은 무엇인가요?

할 일이 없는 Dequeue가 바쁜 Dequeue에서 대기하고 있는 일을 가져와서 대신 일을 해주는 것을 의미한다. 

3. Path라는 인터페이스는 어떤 기능을 제공하나요?

파일과 경로에 대한 정보를 가지고 있다. 

4. Files 클래스는 어떤 기능을 제공하나요?

Path 객체를 사용하여 파일을 통제하고, File 클래스의 단점을 개선한 기능을 제공한다. 

5. WatchService라는 클래스는 어떤 기능을 제공하나요?

파일이 생성, 수정, 삭제 되었을 때 알려주는 기능을 제공한다. 

6. NIO에서 이야기하는 채널(Channel)은 무슨 역할을 하나요?

채널은 디바이스, 파일 네트워크 등과의 작업을 위한 통로 역할을 한다. 

7. java.util 패키지에 추가된 Objects라는 클래스는 어떤 메소드를 제공하나요?

compare(), equals(), hash(), hashCode(), toString() 등을 제공하는데 매개변수에 null인 객체를 넘겨줘도 예외를 발생시키지 않는다. 

 

728x90