[프로그래머스] 자바 중급 복습(2)

Seungjae·2021년 7월 16일
0

JAVA 공부하기

목록 보기
3/4

[프로그래머스] 자바 중급 복습 PART 5~8


PART 5

자바IO

크게 byte단위 입출력문자 단위 입출력클래스로 나뉜다. 이러한 입출력 클래스들은 데코레이터 패턴을 기반으로 만들어졌다.

데코레이터 패턴

데코레이터 패턴이란 객체의 결합을 통해 기능을 유연하게 확장할 수 있게 해주는 패턴이다. 일반적으로 어떠한 기능을 확장하는 방법을 생각하면 보통 상속을 떠올린다.

하지만 기본 기능에 추가할 수 있는 기능이 다양한 경우, 상속의 경우 기능의 확장별로 클래스를 추가해야하지만, 각 추가 기능을 Decorator 클래스로 정의하면 필요에 따라서 조합하여 유연하게 확장할 수 있다.

(Ref: https://gmlwjd9405.github.io/2018/07/09/decorator-pattern.html)

Byte단위 입출력

Byte단위 입출력 클래스는 클래스의 이름이 InputStream이나 OutputStream으로 끝난다.

예로 FileInputStream(파일로 부터 읽어온다.)과 FileOutputStream(파일에 쓴다.)가 있다. 이 클래스들을 이용해서 파일에서 1바이트씩 읽어들여 1바이트씩 저장할 수 있다.

read()메소드는 byte를 리턴하면 끝을 표현할 수 없기에 int를 리턴한다. 정수 4바이트 중 마지막 바이트에 읽어들인 1바이트를 저장한다. 더 이상 읽어들일 것이 없을 때, -1을 리턴한다.

일반적으로 데이터를 가져올 때 512크기의 byte 배열을 버퍼로 사용하여 읽고 쓰게된다. 이유는 대부분의 운영체제는 기본적으로 데이터를 512byte씩 읽어오기 때문이다. 즉 1바이트씩 읽어오려해도 한번 읽어 들일 때 마다 다시 512바이트를 운영체제에서 읽어오기에 511바이트는 버리게 된다. 따라서 이렇게 사용하는 것이 효율적인 것이다.

try-with-resources

java.io의 객체는 인스턴스를 만들고, 모두 사용하면 close() 메소드를 호출해야한다. 하지만 try-with-resources를 사용하면 예외가 발생하지 않았다면 자동으로 close()메소드를 실행시켜준다.

Char단위 입출력(Console)

Char단위 입출력 클래스는 클래스 이름이 Reader나 Writer로 끝난다. 이러한 클래스를 사용하여 키보드로 부터 한줄씩 입력받아 콘솔로 출력하는 작업 등을 할 수 있다.

또한 아래와 같은 형식으로 파일에서 한 줄씩 입력 받아서 다른 파일에 출력하는 작업 또한 할 수 있다.

BufferedReader br = new BufferedReader(new FileReader("..."));
PrintWriter pw = new PrintWriter(new FileWriter("..."));

PART 6

어노테이션

어노테이션은 Java5에 추가됬으며, 클래스나 메타코드에 붙인 후 클래스가 컴파일되거나 실행될 때 어노테이션의 유무나 어노테이션에 설정된 값을 통하여 클래스가 좀 더 다르게 실행되게 할 수 있다.

어노테이션은 기본적으로 자바가 제공하지만 사용자가 직접 만들 수도 있다.

PART 7

Thread

운영체제 입장에서는 자바도 하나의 프로세스이다. 그리고 이러한 프로세스 안에서도 여러 개의 흐름이 동작할 수 있다. 이것이 Thread이다. Thread를 사용하면 동시에 여러 작업을 수행하게 할 수 있다.

Java에서 Thread를 만드는 방법은 크게 Thread 클래스를 상속받는 방법과 Runnable인터페이스를 구현하는 방법이 존재한다. 우선 첫번째 방법인 Thread 클래스를 상속받는 방법은 말 그대로 Thread 클래스를 상속받고 가지고 있는 run()메소드를 오버라이딩하면 된다. 그리고 start()메서드를 호출하여 실행시키면 된다.

MyThread th1 = new MyThread();
MyThread th2 = new MyThread();

th1.start();
th2.start();

이때 알아둬야할 것은 main도 하나의 쓰레드이며 각 쓰레드는 서로 번갈아가며 수행되고 독립적으로 수행된다는 것이다. 즉 main쓰레드가 종료되어도 t1,t2쓰레드는 작업을 수행(일반 쓰레드일 경우)하게 된다.

두번째 방법은 Runnable인터페이스를 구현하는 방법이다. 이 방법도 말 그대로 Runnable 인터페이스를 구현하고 Runnable 인터페이스가 가지고 있는 run()메소드를 구현하면 된다. 하지만 이 경우 실행하는 방법에 차이가 있다. Runnable 인터페이스의 구현체는 엄밀히 따지만 Thread를 상속받지는 않았기에 Thread가 아니다. 즉 Thread를 생성하고 해당 생성자에 구현체를 넣어서 Thread를 생성해야한다. 그 뒤는 위와 같이 start()메소드를 사용하여 실행시키면 된다.

MyThread mt1 = new MyThread();
MyThread mt2 = new MyThread();

Thread th1 = new Thread(mt1);
Thread th2 = new Thread(mt2);

th1.start();
th2.start();

왜 굳이 첫번째 방법이 있는데 작업이 한번 더 필요한 Runnable을 구현하는 방법이 존재할까? 그 이유는 자바에서는 다중상속을 지원하지 않기 때문이다.

공유객체

쓰레드들이 공유 객체를 가질 경우, 공유 객체가 가진 메서드가 동시에 접근되면 문제가 생길 수 있다. 운영체제에서는 이러한 구역을 임계 구역, critical section이라고 한다.

즉 이러한 문제를 방지하기 위해 동기화 메서드, 동기화 블록을 사용한다. 이것들은 공유 객체가 가진 메소드가 동시에 호출 되지 않도록 해준다.

public synchronized void func() {
}

synchronized를 만나게 되면 먼저 호출한 쪽이 Monitoring Lock(객체의 사용권)을 얻게 된다. 즉 synchronized가 붙은 메서드가 모두 실행된 다음에야 다음 메서드가 실행될 수 있다. 모니터링 락은 메소드 실행이 종료되거나, wait()와 같은 메소드를 만나기 전까지 유지된다. 그리고 다른 쓰레드들은 모니터링 락을 놓을 때까지 대기하게 된다.

이때 주의점은 synchronized를 붙히지 않은 메소드는 다른 쓰레드들이 synchronized메소드를 실행하면서 모니터링 락을 획득했다 하더라도, 그것과 상관없이 실행된다는 것이다.

하지만 로직이 복잡한 메소드의 경우 synchronized를 사용해야하더라고 대기하는 쓰레드가 너무 오래 대기해야할 수 있기에 부담스러울 수 있다. 이러한 문제를 해결하기위해 메서드 내에서 synchronized가 꼭 필요한 부분만 synchronized 블록을 사용하여 처리할 수 있다.

synchronized(this){
    System.out.println("Hello");
}

쓰레드의 상태

쓰레드는 동시에 동작하지만 사실 동시에 동작하지 않는다. 무슨 말이냐면 예를 들어 쓰레드가 3개가 있다면 JVM은 시간을 잘게 쪼갠 후에 한번은 쓰레드1을, 한번은 쓰레드 2를, 한번은 쓰레드 3을 실행한다. 이러한 작업이 빠르게 일어나서 우리는 동시에 동작한다고 느끼게 되는 것이다.

쓰레드는 크게 4가지 Runnable(실행 가능)과 Running(실행)상태, Blocked상태, Dead상태로 나뉜다.

  • Runnable: 실행 가능 상태, 즉 실행 가능하여 실행되기를 기다리는 상태
  • Running: 실행 중인 상태
  • Blocked: wait(), sleep()메소드 등으로 인해 잠시 대기하는 블록 상태 => wait()메소드에 경우 다른 쓰레드가 notify()(임의의 한 개만 깨움)나 notifyAll()(다 깨움)메소드를 호출하기 전에는 블록상태에서 해제되지 않는다.
  • Dead: run메소드가 종료되면, 쓰레드는 종료된다. 즉 Dead상태이다.

추가적으로 Thread의 yeild메소드가 호출되면 해당 쓰레드는 다른 쓰레드에게 자원을 양보하게 되고, Thread가 가지고 있는 join메소드를 호출하게 되면 해당 쓰레드가 종료될 때까지 대기하게 된다.

그리고 wait과 notify는 꼭 동기화된 블록 안에서 사용해야한다. wait메소드를 만나게 되면 해당 쓰레드는 모니터링 락에 대한 권한을 내려놓고 대기하게 된다.

(Ref1: https://javaplant.tistory.com/29)
(Ref2: https://widevery.tistory.com/27)

데몬 쓰레드

데몬이란 보통 유닉스 계열의 OS에서 백그라운드로 동작하는 프로그램을 뜻한다. 데몬 쓰레드를 만드는 방법은 간단하다. 그냥 쓰레드에 데몬 설정을 해주면 된다. 이러한 데몬 쓰레드는 자바프로그램이 실행할 때 백그라운드에서 특별한 작업을 처리하게 하는 용도로 사용한다.

Thread th = new Thread(new DaemonThread());
th.setDaemon(true);
th.start();

데몬 쓰레드는 일반 쓰레드(main 등)가 모두 종료되면 강제적으로 종료되게 된다. 그 이유는 데몬 쓰레드는 일반 쓰레드를 보조하기 위해 사용하는 것이기 때문에 일반 쓰레드가 모두 종료되게 되면 데몬 쓰레드가 사실상 필요하지 않기 때문이다. 데몬 쓰레드의 예로는 가비지 컬렉션, 자동저장, 화면 자동 갱신 등이 있다고 한다.

(Ref: https://devbox.tistory.com/entry/Java-%EB%8D%B0%EB%AA%AC%EC%93%B0%EB%A0%88%EB%93%9C)

PART 8

람다식

자바는 메소드만 매개 변수로 전달할 방법이 없다. 인스턴스만 전달할 수 있다. 그래서 메소드가 하나 밖에 없는 인터페이스인 함수형 인터페이스의 메서드를 전달할 때에도 구현체를 만들어 인스턴스로 넘겨야한다. 예를 들자면 run() 메소드만 가지고 있는 Runnable 인터페이스가 있다.

new Thread(new Runnable(){public void run(){
                    System.out.println("hello");
            }}).start();

메소드만 전달할 수 있다면 훨씬 코드가 간단해질 것이다. 이런 부분을 해결한 것이 람다표현식이다. 람다식은 다른 말로는 익명 메소드라고도 한다.

 new Thread(() -> {System.out.println("hello");}).start();

해당 람다식의 동작은 아래와 같다.

  1. JVM은 Thread생성자를 보고 ()->{} 이 무엇인지 대상을 추론
  2. Thread생성자 api를 보면 통해 JVM은 Thread생성자가 Runnable인터페이스를 구현한 것을 받는 것을 인지
  3. 람다식을 Runnable을 구현하는 객체로 자동으로 만들어서 매개변수로 넣어줌
profile
코드 품질의 중요성을 아는 개발자 👋🏻

0개의 댓글