back_Java_05

입출력

콘솔이란 콘솔은 사용자의 입력을 받거나 사용자에게 문자열을 출력해주는 역할을 하는 것을 말한다.

터미널, 콘솔, 쉘, 명령줄의 차이

터미널, 콘솔의 차이

  • 터미널

    • 텍스트 입력 및 출력의 환경이며, 명령을 처리하고 출력을 뱉는 것은 쉘이 하는 역할이다.

    • 터미널은 데이터를 입력하거나 처리 결과를 출력하는 단말기다.

    • 터미널은 쉘을 실행하고 명령을 입력하게 해주는 포장(Wrapper) 프로그램이다.

    • 터미널은 그래픽 인터페이스를 표시하고 쉘과 상호작용할 수 있는 프로그램이며, 용어 자체는 일반적으로 키보드와 디스플레이를 통해 사용자가 컴퓨터와 상호작용할 수 있도록 하는 장치를 말한다.

  • 콘솔

    • 물리적 터미널을 콘솔이라고 한다.(하드웨어 형태의 터미널)

    • 콘솔은 물리적 장치라면, 터미널은 원격제어 환경까지 포함한 더 넓은 개념이라 볼 수 있다.

    • 콘솔은 일종의 터미널이며, 텍스트 모드 프로그램이 활성화된 창이다.

    • 콘솔은 운영체제와의 저수준의 직접적 통신을 위해 컴퓨터 전용직렬 콘솔 포트에 연결된 단일 키보드와 모니터로 구성된다.

    • 콘솔은 시스템에 직접 연결된 기본적인 터미널이자 물리적 터미널이다.

06-01 콘솔 입출력

콘솔 입력

  • 문자열을 얻기 위해서는 자바의 System.in 을 사용한다.

import java.io.IOException;
import java.io.InputStream;

public class Sample {
	public static void main(String[] args) throws IOException {
		InputStream in = System.in;//InputStream 객체

		int a;
		a = in.read(); // 1바이트 사용자 입력을 정수로 받게되며, 이 때 system.out은 그대로 정수로 출력해준다. 
		System.out.println(a);
	}
}
  • InputStream 자바 내장 클래스, 자바 내장 클래스 중 java.lang패키지에 속하지 않은 경우, 지정 내용을 import 시켜주어야 한다. 반대로 java.lang에 속한 클래스는 별도의 import를 요구하지 않는다.

  • IOException : InputStream의 경우 예외 처리가 발생할 수 있어서 해당 라이브러리와 예외처리로 throw 키워드가 존재하게 된다.

InputStream

  • 위의 예시 코드는 1바이트씩 읽어들이다보니, 한 글자씩 입력을 받을 수 밖에 없다. 따라서 이러한 경우 스트림으로 된 데이터의 형태를 읽지 못하고, 다른 방식을 사용해야 한다.

import java.io.IOException;
import java.io.InputStream;

public class Sample {
	public static void main(String[] args) throws IOException {
		InputStream in = System.in;//InputStream 객체

		bytep[] a = new byte[3];
		in.read(a);
		for (int i = 0; idx < 3; i++)
			System.out.println(a[i]);
	}
}
abc (입력) 
97 (출력) 
98 (출력) 
99 (출력)

InputSrtreamReader

  • 그러나 위의 방식은 읽어들인 값을 아스키 코드로 해석해야 하고, 형변환이 필요하다. 바이트 단위로 읽는 것도 제약이 크다. 이에 문자로 입력스트림을 받는 방법에 InputStreamReader를 사용하면된다.

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Console {
	public static void main(String[] args) throws IOException {
		InputStream in = System.in;
		InputStreamReader reader = new InputStreamReader(in);
		char[] a = new char[3];
		reader.read(a);

		for (int i = 0; idx < 3; i++)
			System.out.println(a[i]);
	}
}

BufferedReader

  • 그러나 여전히 3바이트로 길이를 유한하게 해야만 받아들이는 구조는 제약이 있다. 온전히 들어오는 내용을 전체다 받아 들이는 예제는 다음과 같이 가능하다.

import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.ImputStreamReader;

public class Console {
	public static void main(String[] args) throws IOException {
		InputStream in = System.in;
		InputStreamReader reader = new InputStreamReader(in);
		BufferedReader br = new BufferedReader(reader);

		String a = br.readLine();
		System.out.println(a);
	}
}

Scanner

  • J2SE(Java Special Edition) 5.0 부터 java.util.Scanner 클래스가 새로 추가되어 콘솔 입력이 훨씬 쉬워졌다.

import java.util.Scanner;

public class Sample {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		System.out.println(sc.next());
	}
}

콘솔 출력

  • System.out.println 메서드는 PrintStream 클래스의 객체로 콘솔에 값을 출력할 때 사용하고, 디버깅시 많이 사용한다.

  • System.err의 경우 에러 출력을 위한 역할을 한다.

06-02 파일 입출력

파일 쓰기

FileOutputStream

import java.io.FileOutputStream;
import java.io.IOException;

public class FileInOut {
	public static void main(String[] args) throws IOException {
		FileOutputStream output = new FileOutputStream("./testfile");
		output.close();
	}
}
  • 파일을 생성하기 위한 FileOutputStream 클래스, close() 메소드는 기존의 다른 언어들의 close와 유사한 역할을 하며, 자동으로 닫아주긴 하나 직접 사용한 파일의 경우 추가적인 오류를 막고자 미사용시 닫기를 권장한다.

  • 아래와 같이 write 메소드를 활용해 내용을 넣어줄수 있다.

import java.io.FileOutputStream;
import java.io.IOException;

public class FileInOut {
	public static void main(String[] args) throws IOException {
		FileOutputStream output = new FileOutputStream("./testfile");

		for(int i = 1; i < 11; i++) {
			String data = i + "번째 줄입니다.\n";
			output.write(data.getBytes());
		}

		output.close();
	}
}
  • InputStream 과 마찬가지로 OutputStream 은 바이트 단위로 데이터를 처리하는 클래스다. FileOutputStream은 이를 상속받아 만들었고, 바이트 단위로 처리하게 되어 있다.

  • 그러다보니 한글이 깨지는 현상이 발생하는 경우가 있다고 한다. 왜 깨지는 걸까?

FileWriter

  • 문자열을 쓸 때, 위의 예시는 다소 불편하여 이를 해소한 방식이다.

import java.io.FileWriter;
import java.io.IOException;

public class FileInOut {
	public static void main(String[] args) throws IOException {
		FileWriter fw = new FileWriter("./test2.md");
		for(int i = 1; i < 11; i++) {
			String data = i + "번째 줄입니다.";
			fw.write(data);
		}
		fw.close();
	}
}
  • 이 방식의 특징은 byte 배열이 아닌 문자열로 사용할 수 있어 편리한 면을 가지고 있다.

PrintWriter

  • 그러나 여기서 FileWriter 도 개행문자를 넣어야 한다는 등의 불편한 부분이 존재한다.

  • 따라서 PrintWriter 를 사용하는 형태가 되면, 메소드가 개행을 지원하는 경우

import java.io.PrintWriter;
import java.io.IOException;

public class FileInOut {
	public static void main(String[] args) throws IOException {
		PrintWriter pw = new PrintWriter("./test2.md");
		for(int i = 1; i < 11; i++) {
			String data = i + "번째 줄입니다.";
			pw.println(data);
		}
		pw.close();
	}
}

파일에 내용 추가하기

  • 프로그래맴을 만들고, 쓴 내용에 추가로 내용을 적고 싶을 때 추가모드로 열어서 추가 내용을 작성해야 한다.

import java.io.FileWriter;
import java.io.IOException;

public class FileInOut {
	public static void main(String[] args) throws IOException {
		FileWriter fw = new FileWriter("./test3.md");
		for(int i = 1; i < 11; i++) {
			String data = i + "번째 줄입니다.";
			fw.write(data);
		}
		fw.close();

		FileWriter fw2 = new FileWriter("./test3.md", true);
		for(int i = 11; i < 21; i++) {
			String data = i + "번째 줄입니다.";
			fw2.write(data);
		}
		fw2.close();
	}
}
  • PrintWriter 를 사용할 경우에도 생성자 파라미터 파일명 대신, 추가모드로 열린 FileWriter의 객체를 전달하면 된다.

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class Sample {
    public static void main(String[] args) throws IOException {
        PrintWriter pw = new PrintWriter("c:/out.txt");
        for(int i=1; i<11; i++) {
            String data = i+" 번째 줄입니다.";
            pw.println(data);
        }
        pw.close();


        PrintWriter pw2 = new PrintWriter(new FileWriter("c:/out.txt", true));
        for(int i=11; i<21; i++) { // 다형성을 이용한 트릭
            String data = i+" 번째 줄입니다.";
            pw2.println(data);
        }
        pw2.close();
    }
}

파일 읽기

  • 바이트 단위로 읽고 처리하기

import java.io.FileInputStream;
import java.io.IOException;

public class Sample {
    public static void main(String[] args) throws IOException {
        byte[] b = new byte[1024];
        FileInputStream input = new FileInputStream("c:/out.txt");
        input.read(b);
        System.out.println(new String(b));  // byte 배열을 문자열로 변경하여 출력
        input.close();
    }
}
  • 라인 단위로 읽고 처리하기

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class Sample {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader("c:/out.txt"));
        while(true) {
            String line = br.readLine();
            if (line==null) break;  // 더 이상 읽을 라인이 없을 경우 while 문을 빠져나간다.
            System.out.println(line);
        }
        br.close();
    }
}

자바 날개 달기

07-01 패키지

  • 패키지 : 비슷한 성격의 자바 클래스들을 모아놓은 자바 디렉토리이다.

패키지

package house; // 이렇게 지정만 해주면 된다. 

public class HouseKim {
	(... 생략 ...)
}

서브패키지

  • 패키지 안에 또 다른 하위 패키지를 생성하는 방식이다.

package house.person;

public class EungYoungPark {
}

패키지 사용하기

  • 외부에서는 import 키워드를 활용해서 패키지를 인클루드할 수 있고, 모든 클래스를 사용할 수 있다.

  • 패키지 내부에 포함된

패키지 사용하는 이유?

  • 패키지를 사용하면 클래스의 분류가 용이하다. 클래스명이 동일한 경우가 생기거나 하면, 문제가 생길수 있는데, 이러한 상황을 구분하기 위해 패키지로 묶어서 사용이 가능하다.

07-02 접근제어자

접근제어자

  • 변수나 메소드의 사용권한을 제어하는 역할을 한다.

  • 다음과 같은 제어자가, 변수나 메소드 사용권한으로 제공된다.

    • private

    • default

    • protected

    • public

  • 위에서부터 아래로 갈 수록 보다 많은 접근성이 생긴다.

private

  • 접근 제어자로 사용되면 해당 클래스에서만 사용이 가능하다.

public class Sample {
	private String secret;
	private String getSecret() {return this.secret;}
}

default

  • 접근 제어자를 별도로 설정하지 않았다면, 접근 제어자가 없는 변수나 메소드는 기본적으로 해당 접근 제어자가 연결되어, 해당 '패키지' 내에서만 접근이 가능하다.

house/HouseKim.java

package house;  // 패키지가 동일하다.

public class HouseKim {
    String lastname = "kim";  // lastname은 default 접근제어자로 설정된다.
}

house/HousePark.java

package house;  // 패키지가 동일하다.

public class HousePark {
    String lastname = "park";

    public static void main(String[] args) {
        HouseKim kim = new HouseKim();
        System.out.println(kim.lastname);  // HouseKim 클래스의 lastname 변수를 사용할 수 있다.
    }
}
  • 동일 패키지 안에서의 서로 다른 클래스는 접근이 가능하다.

protected

  • 해당 클래스를 상속 받은 다른 패키지의 클래스에서만 접근이 가능하다.

house/HousePark.java

package house;  // 패키지가 서로 다르다.

public class HousePark {
    protected String lastname = "park";
}

house/person/EungYongPark.java

package house.person;  // 패키지가 서로 다르다. 즉, 기본적으로 접근이 불가하다.

import house.HousePark; // 패키지 안의 클래스를 임포트했다. 

public class EungYongPark extends HousePark {  // HousePark을 상속했다.
    public static void main(String[] args) {
        EungYongPark eyp = new EungYongPark();
        System.out.println(eyp.lastname);  // 상속한 클래스의 protected 변수는 접근이 가능하다.
    }
}
  • 상속이 되었으므로, 사용이 가능해졌다.

public

  • 어떤 클래스나 메소드라도 접근이 가능하다.

package house;

public class HousePark {
    protected String lastname = "park";
    public String info = "this is public message.";
}

클래스, 메소드의 접근 제어자

  • 접근제어자를 사용하면 위험요소를 제거하고, 코딩의 실수를 줄이는 역할을 한다는 것을 명심할 것.

07-03 정적(static) 변수 와 메소드

static 변수

  • C++ 과 동일하게 메모리 상에서 한번만 할당하는 형태가 되고 공유가 된다.

  • 따라서 클래스 전체에서 하나를 공유하는 경우, 전체를 관리하는 등의 용도로 사용하기 편리하다.

static 메소드

  • 해당 키워드를 사용하는 메소드는, 클래스를 이용하여 호출하는 것이 가능하다. 이는 기능적으로 객체가 필요 없는 메소드들을 위함이라고 생각하면 된다.

class Counter  {
    static int count = 0;
    Counter() {
        count++;
        System.out.println(count);
    }

    public static int getCount() {
        return count;
    }
}

public class Sample {
    public static void main(String[] args) {
        Counter c1 = new Counter();
        Counter c2 = new Counter();

        System.out.println(Counter.getCount());  // 스태틱 메서드는 클래스를 이용하여 호출
    }
}
import java.text.SimpleDateFormat;
import java.util.Date;

class Util {
    public static String getCurrentDate(String fmt) {
        SimpleDateFormat sdf = new SimpleDateFormat(fmt);
        return sdf.format(new Date());
    }
}

public class Sample {
    public static void main(String[] args) {
        System.out.println(Util.getCurrentDate("yyyyMMdd"));  // 오늘 날짜 출력
    }
}

싱글톤 패턴(Singleton Pattern)

  • 특정 클래스에서 단 하나의 객체만을 생성하도록 강제하는 패턴이다.

Sample.java

class Singleton {
    private Singleton() {
    }
}

public class Sample {
    public static void main(String[] args) {
        Singleton singleton = new Singleton();  // 컴파일 오류가 발생한다.
    }
}
  • 기본적으로 위 코드로 작성시 컴파일의 에러가 발생한다. 이는 private 키워드로 생성자 접근을 막아기 때문이다. new 키워드로 생성이 불가능하다.

class Singleton {
    private Singleton() {
    }

    public static Singleton getInstance() {
        return new Singleton();  // 같은 클래스이므로 생성자 호출이 가능하다.
    }
}

public class Sample {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
    }
}
  • 생성자가 private 키워드로 되어 있으면, 해당 클래스 생성자에 접근하는 것이 불가능하다. 하지만 거기서 get함수를 만들고, 여기서 생성자를 호출하는 방식으로 한다면, 클래스 생성자 없이 생성이 가능해진다.

  • 하지만 보면 알다시피, getInstance 메소드를 호출할 때마다 문제가 생기는데 이는 계속 새로운 인스턴스를 생성한다는 점이다.

  • 따라서 이러한 상황을 개선한 singleton 기법은 다음 처럼 짜여진 경우를 의미한다.

class Singleton {
    private static Singleton one;
    private Singleton() {
    }

    public static Singleton getInstance() {
        if(one==null) {
            one = new Singleton(); // 클래스 객체 자체를 이미 갖고 있는 방식으로 하고, null 여부를 판단하여 생성을 한다. 즉 한번 생성 이후에는 더이상 new 키워드를 쓰지 않고 해당 객체를 그대로 호출한다. 
        }
        return one;
    }
}

public class Sample {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1 == singleton2);  // true 출력
    }
}

⛔️ 주의

해당 방식은 `Thread Safe` 하지 않다고 한다... 주의하자.

07-04 예외 처리(Exception)

  • 오류가 발생하고 이에 대해 무시하거나 적절한 처리를 할 수 있는데, 이때 try ... catch 와 throw 구문을 통한 오류의 적절한 처리를 해볼 수 있다.

예외는 언제 발생하는가?

  • 없는 대상에 접근하는 경우

  • 연산이 불가능한 연산(4 /0 등...)

  • 접근 불가능한 배열 위치

  • ... 등

예외 처리하기

  • 기본적으로 try... catch 을 통해 처리된다.

  • 기본적인 방법과 예외의 표준은 자바 내부 처리된 클래스 객체를 확인하면 될 것으로 보이며 C++ 과 동일한 구조를 가진다.

try {
	... // 특정 조건문 등 실행
} catch(예외 조건 1) {
	... // 에러 처리
} catch(예외 조건 2) {
	...
} ...

finally

  • 프로그램 수행 중 예외 발생시 예외 처리하여 catch 구문이 실행되지만, 어떤 예외처리가 발생한다고 해도 반드시 실행되도록 만드는 것을 원할 수 있습니다. 이 경우, fianlly 키워드를 활용해서 예외처리 후 공통적으로 할 작업들을 정리해 둘 수 있다.

public class Sample {
    public void shouldBeRun() {
        System.out.println("ok thanks.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        int c;
        try {
            c = 4 / 0;
        } catch (ArithmeticException e) {
            c = -1;
        } finally {
            sample.shouldBeRun();  // 예외에 상관없이 무조건 수행된다.
        }
    }
}

RuntimeException 과 Exception

  • 직접 만드는 예외를 보며 참고하자!

public class Sample {
	public void sayNick(String nick) {
		if ("fool".equals(nick)) { return; }
		System.out.println("당신의 별명은 "+ nick +" 입니다.")
	}
	public static void main(String[] args) {
		Sample sample = new Sample();
		sample.sayNick("fool");
		smaple.sayNick("genious");
	}
}

RuntimeException

  • 적극적으로 예외를 발생시켜버리는 방식으로 할 수 있다.

Sample .java

class FoolException extends RuntimeException {}

public class Sample {
	public void sayNick(String nick) {
		if ("fool".equals(nick)) {
			throw new FoolException();
		}
		System.out.println("당신의 별명은 "+ nick +" 입니다.")
	}
	public static void main(String[] args) {
		Sample sample = new Sample();
		sample.sayNick("fool");
		smaple.sayNick("genious");
	}
}
  • 위의 예시 프로그램을 실행해보면, sayNick 메소드 실행과 함께 예외가 발생하게 된다.

  • 이때 Excetpion은 두가지 종류로 구분할 수 있다.

    • RuntimeException : 실행시 발생하는 예외, 발생할 가능성이 있는 경우에 사용하는 형태이다.

    • Exception : 컴파일 발생시 사용하는 예외, 따라서 이는 컴파일 과정에서 예외를 발견할 수 있다.

      • 따라서 이미 예측이 되어서 막아버리기 때문에 Checked Exception, RuntimeException, Unchecked Exception이라고도 부른다.

Exception

  • FoolException 의 예시를 Runtime이 아닌 Exception을 상속받게 되면, 예외 처리가 컴파일러가 강제하므로 컴파일 자체가 안된다. 따라서 이를 수정해줘야 한다.

    • 즉, 에러가 발생했을 때의 대처가 없이는 Exception 을 상속받은 걸로, 예외처리를 만들게 되면, 정상적인 컴파일이 안될 수 있다는 것이다.

class FoolException extends Exception {
}

public class Sample {
    public void sayNick(String nick) {
        try {
            if("fool".equals(nick)) {
                throw new FoolException();
            }
            System.out.println("당신의 별명은 "+nick+" 입니다.");
        }catch(FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("fool");
        sample.sayNick("genious");
    }
}

예외 던지기(throws)

  • 예제를 수정하여 sayNick 메서드에서 했는데 이렇게 하지 않고, sayNick을 호출한 지점에서 FoolException을 처리하도록 만들수도 있다. 즉, 특정 메소드가 호출된 순간에 그 내부에서 수행되는게 아니라, 해당 메소드를 호출한 지점에서 이를 대처하도록 만드는 것이다.

```java
public class Sample {
    public void sayNick(String nick) throws FoolException {
        try {
            if("fool".equals(nick)) {
                throw new FoolException();
            }
            System.out.println("당신의 별명은 "+nick+" 입니다.");
        /**}catch(FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }*/
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("fool");
        sample.sayNick("genious");
    }
}
  • 이렇게 throws라는 구문을 이용하여 위로 보낼 수 가 있다. (이를 예외를 뒤로 미룬다 라고도 표현한다.) 그러나 이경우 main 메소드에서 컴파일 에러가 발생한다. 왜냐면 throws 구문 때문에 예외처리의 대상이 main메소드로 바뀌었기 때문이다.

  • 따라서 이를 해결하기 위해서는 try catch 구문이 main 메소드로 나가야 한다.

class FoolException extends Exception {
}

public class Sample {
    public void sayNick(String nick) throws FoolException {
        if("fool".equals(nick)) {
            throw new FoolException();
        }
        System.out.println("당신의 별명은 "+nick+" 입니다.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        try {
            sample.sayNick("fool");
            sample.sayNick("genious");
        } catch (FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }
}
  • 그러나 이러한 방식에서는 큰 차이가 있다.

    • 만약 sayNick이라는 메소드 내부에서 실행되게 된다면, 아마도 sample.sayNick("genious") 호출에 대해서는 정상적으로 수행될 것이다.

    • 하지만 위에 있는 예시의 경우 sayNick이 겹쳐져서 있고, fool 이 매개변수로 넘어간 경우이므로 아래의 "genious"는 수행되지 않고 catch 문으로 넘어가 실행되게 된다.

  • 결론적으로 이러한 이유로 프로그래밍 시 Exception은 매우 중요하며, 위치가 특히나 중요하다. 트랜잭션 처리와도 매우 밀접한 관계가 있다.

트랜잭션(Transaction)

  • 트랜잭션이란 하나의 작업 단위를 말한다. 어떤 작업을 진행하다가 예외가 발생한다고 할 때, 신경써야 하는 것을 말한다.

  • 어떤 값들의 상호 전환에서, 정상적으로 처리되어야 하지만, 예외처리가 정상적으로 되지 않아 처리가 엉망으로 되어 있는 경우가 트랜잭션이 정상적이지 않은 것으로 볼 수 있다.

💡 수도코드란?

수도코드(슈도코드, pseudocode)는 특정 프로그래밍 언어의 문법을 따라 씌여진 것이 아니라, 일반적인 언어로 코드를 흉내내어 알고리즘을 써놓은 코드를 말한다. 수도코드는 말그대로 흉내만 내는 코드이기 때문에, 실제적인 프로그래밍 언어로 작성된 코드처럼 컴퓨터에서 실행할 수 없으며, 특정 언어로 프로그램을 작성하기 전에 알고리즘의 모델을 대략적으로 모델링하는 데에 쓰인다.

상품발송() {
    try {
        포장();
        영수증발행();
        발송();
    } catch(예외) {
        모두취소();  // 하나라도 실패하면 모두 취소한다.
    }
}

포장() throws 예외 {
   ...
}

영수증발행() throws 예외 {
   ...
}

발송() throws 예외 {
   ...
}
상품발송() {
    포장();
    영수증발행();
    발송();
}

포장(){
    try {
       ...
    } catch(예외) {
       포장취소(); // 각각의 조건을 따로하다보니 정상적인 처리가 안될 수 있다. 
    }
}

영수증발행() {
    try {
       ...
    } catch(예외) {
       영수증발행취소();
    }
}

발송() {
    try {
       ...
    } catch(예외) {
       발송취소();
    }
}

07-05 쓰레드(Thread)

  • 쓰레드를 이용하면 한 프로세스 내에서 두가지 또는 그 이상의 일을 동시에 할 수 있다.

Thread

  • 가장 간단한 쓰레드 예시는 다음과 같다.

Sample.java

public class Sample extends Thread {
    public void run() {  // Thread 를 상속하면 run 메서드를 구현해야 한다.
        System.out.println("thread run.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.start();  // start()로 쓰레드를 실행한다.
    }
}
  • 더 많은 쓰레드를 돌리는 예시는 다음과 같다.

public class Sample extends Thread {
    int seq;

    public Sample(int seq) {
        this.seq = seq;
    }

    public void run() {
        System.out.println(this.seq + " thread start.");  // 쓰레드 시작
        try {
            Thread.sleep(1000);  // 1초 대기한다.
        } catch (Exception e) {
        }
        System.out.println(this.seq + " thread end.");  // 쓰레드 종료 
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {  // 총 10개의 쓰레드를 생성하여 실행한다.
            Thread t = new Sample(i);
            t.start();
        }
        System.out.println("main end.");  // main 메소드 종료
    }
}
0 thread start.
4 thread start.
6 thread start.
2 thread start.
main end.
3 thread start.
7 thread start.
8 thread start.
1 thread start.
9 thread start.
5 thread start.
0 thread end.
4 thread end.
2 thread end.
6 thread end.
7 thread end.
3 thread end.
8 thread end.
9 thread end.
1 thread end.
5 thread end.

Join

  • 쓰레드가 모두 수행되고 종료되기 전에 main 메소드가 종료되지 않도록 만들었다.

import java.util.ArrayList;

public class Sample extends Thread {
    int seq;
    public Sample(int seq) {
        this.seq = seq;
    }

    public void run() {
        System.out.println(this.seq+" thread start.");
        try {
            Thread.sleep(1000);
        }catch(Exception e) {
        }
        System.out.println(this.seq+" thread end.");
    }

    public static void main(String[] args) {
        ArrayList<Thread> threads = new ArrayList<>();
        for(int i=0; i<10; i++) {
            Thread t = new Sample(i);
            t.start();
            threads.add(t);
        }

        for(int i=0; i<threads.size(); i++) {
            Thread t = threads.get(i);
            try {
                t.join(); // t 쓰레드가 종료할 때까지 기다린다.
            }catch(Exception e) {
            }
        }
        System.out.println("main end.");
    }
}
0 thread start.
5 thread start.
2 thread start.
6 thread start.
9 thread start.
1 thread start.
7 thread start.
3 thread start.
8 thread start.
4 thread start.
0 thread end.
5 thread end.
2 thread end.
9 thread end.
6 thread end.
1 thread end.
7 thread end.
4 thread end.
8 thread end.
3 thread end.
main end.

Runnable

  • 보통 쓰레드 객체를 만들 때는 위의 방식으로 만든다. 하지만 Runnable 인터페이스를 구현하도록 하는 방법으로 보통 사용한다. 왜냐면, Thread클래스를 상속해버려야 하고, 이러면 다른 클래스 상속이 불가능하기 때문이다.

  • Runnable 인터페이스를 만드는 방식으로 수정하면 다음처럼 구현이 가능하다.

import java.util.ArrayList;

public class Sample implements Runnable {
    int seq;
    public Sample(int seq) {
        this.seq = seq;
    }

    public void run() {
        System.out.println(this.seq+" thread start.");
        try {
            Thread.sleep(1000);
        }catch(Exception e) {
        }
        System.out.println(this.seq+" thread end.");
    }

    public static void main(String[] args) {
        ArrayList<Thread> threads = new ArrayList<>();
        for(int i=0; i<10; i++) {
            Thread t = new Thread(new Sample(i));
            t.start();
            threads.add(t);
        }

        for(int i=0; i<threads.size(); i++) {
            Thread t = threads.get(i);
            try {
                t.join();
            }catch(Exception e) {
            }
        }
        System.out.println("main end.");
    }
}
  • 참고로 Runnable 인터페이스를 사용하면 run 메소드를 반드시 구현해야 한다. (인터페이스 필수조건)

  • 더불어 쓰레드를 생선하는 부분이 다음처럼 변경된다.

Thread t = new Sample(i); // before

Thread t = new Thread(new Sample(i)); // after

07-06 함수형 프로그래밍

  • 함수형 프로그래밍을 지원하기 위해, 람다와 스트림이 도입되었다. 람다와 스트림을 사용하여 작성한 코드를 일반 스타일의 자바 코드로 바꾸는 것은 불가능하지 않고, 없어도 된다. 하지만 그럼에도 이를 사용함은 코드 양을 줄이기에 적절한다.

람다(Lambda)

  • 익명함수(Anonymous functions)

일반적인 코드

interface Caclcuator {
	int sum(int a, int b); // 인터페이스 구현 
}

class MyCalculator implements Calculator { // 이를 활용한 My Calculator 생성
	public int sum(int a, int b) {
		return a + b;
	}
}

public class Sample {
	public static void main(String[] args) {
		MyCalculator mc = new MyCalculator();
		int result = mc.sum(3, 4);
		System.out.println(result); 
	}
}

람다를 적용한 코드

interface Calculator {
	int sum(int a, int b);
}

public class Sample {
	public static void main(String[] args){
		Calculator mc = (int a, int b) -> a + b; // 람다 방식의 
		int result = mc.sum(3, 4);
		System.out.println(result);
	}
}

인터페이스 사용시 주의사항

  • 람다를 사용시 인터페이스의 메서드가 1개 이상이면 람다 함수를 사용할 수 없다.

interface Calculator {
    int sum(int a, int b);
    int mul(int a, int b);  // mul 메서드를 추가하면 컴파일 에러가 발생한다.
}
  • 따라서 여기엔 @FunctionalInterface 어노테이션을 사용하는 것이 좋다. 해당 어노테이션을 넣으면 2개 이상의 메서드를 가진 인터페이스를 작성하는 것이 불가능해진다.

@FunctionalInterface
interface Calculator {
	int sum(int a, int b);
	int mul(int a, int b); // @FunctionalInterface 는 두번째 메서드를 허용하지 않는다. 
} 

람다 축약

  • 기본적인 형태에서 더 축약하면 다음처럼 수정이 가능하다.

(a, b) -> a + b // 자료타입을 생략하는 것이 가능하다. 

Integer.sum

  • 위의 예시는 Integer.sum(int a, int b)와 동일하기 때문에 더 축약이 가능하다.

@FunctionalInterface
interface Calculator {
	int sum(int a, int b)
}

public class Sample {
	public static void main(String[] args) {
		Calculator mc = Integer::sum;
		int result = mc.sum(3, 4);
		System.out.println(result);
	}
}

람다 함수 인터페이스

  • 이번엔 인터페이스 생성조차 더 축약이 가능하다. 자바가 제공하는 BiFunction 인터페이스를 사용하면 더 축약이 가능하다.

import java.util.function.BiFunction;

public class Sample {
	public static void main(String[] args) {
		BitFunction<Integer, Integer, Integer> mc = (a, b) -> a + b;
		int result = mc.apply(3, 4);
		System.out.println(reuslt);
	}
}

💡 자바와 람다함수를 제공하는 인터페이스

상당히 많은 인터페이스 라이브러리 패키지가 있으나, 이해 목적으로 BiFunction, BinaryOperator 에 대해서만 알아보았다.

스트림(Stream)

  • 데이터가 물결처럼 흘러가면서 필터링 과정으로 변경되어 반환되는 구조라, 이러한 이름으로 불린다.

  • 문제

    • 다음 정수 배열이 있다.

      int[] data = {5, 6, 4, 2, 3, 1, 1, 2, 2, 4, 8}
	-  이 배열에서 짝수만 찾아 중복을 제거한 후 역순으로 정렬하는 프로그램을 작성하시오.
	  ```java
	  int[] result = {8, 6, 4, 2};
	```

```java
import java.util.*;

public class Sample {
    public static void main(String[] args) {
        int[] data = {5, 6, 4, 2, 3, 1, 1, 2, 2, 4, 8};

        // 짝수만 포함하는 ArrayList 생성
        ArrayList<Integer> dataList = new ArrayList<>();
        for(int i=0; i<data.length; i++) {
            if(data[i] % 2 == 0) {
                dataList.add(data[i]);
            }
        }

        // Set을 사용하여 중복을 제거
        HashSet<Integer> dataSet = new HashSet<>(dataList);

        // Set을 다시 List로 변경
        ArrayList<Integer> distinctList = new ArrayList<>(dataSet);

        // 역순으로 정렬
        distinctList.sort(Comparator.reverseOrder());

        // Integer 리스트를 정수 배열로 변환
        int[] result = new int[distinctList.size()];
        for(int i=0; i< distinctList.size(); i++) {
            result[i] = distinctList.get(i);
        }
    }
}
```

- 위의 일반적인 코드를 수정하면 다음처럼 정리 된다.

```java
import java.util.Arrays;
import java.util.Comparator;

public class Sample {
    public static void main(String[] args) {
        int[] data = {5, 6, 4, 2, 3, 1, 1, 2, 2, 4, 8};
        int[] result = Arrays.stream(data)  // IntStream을 생성한다.
                .boxed()  // IntStream을 Stream<Integer>로 변경한다.
                .filter((a) -> a % 2 == 0)  //  짝수만 걸러낸다.
                .distinct()  // 중복을 제거한다.
                .sorted(Comparator.reverseOrder())  // 역순으로 정렬한다.
                .mapToInt(Integer::intValue)  // Stream<Integer>를 IntStream으로 변경한다.
                .toArray()  // int[] 배열로 반환한다.
                ;
    }
}
```

- 제공되는 스트림 방식은 일반적인 코드보다 확실히 간결하고 가독성이 좋다. 이를 위한 기능들을 더 찾으면서 사용하는것이 중요해 보인다. 

Last updated