일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- spring
- rpa
- Array
- Scanner
- string
- 배열
- JDBC
- 문자열
- Thymeleaf
- Oracle
- 조건문
- Database
- 상속
- db
- Eclipse
- 이클립스
- React
- Board
- Java
- git
- View
- API
- mysql
- html
- jsp
- MVC
- SpringBoot
- Uipath
- Controller
- jquery
- Today
- Total
유정잉
10일차 Java [ java.base 모듈, 멀티 스레드 ] 본문
[ API 도큐먼트 ]
자바 표준 모듈에서 제공하는 라이브러리는 방대하기 때문에 쉽게 찾아서 사용할 수 있도록 도와주는 API ( Application Programming Interface ) 도큐먼트가 있다. 라이브러리가 클래스와 인터페이스의 집합이라면, API 도큐먼트는 이를 사용하기 위한 방법을 기술한 것이다.
Overview (Java Platform SE 8 ) (oracle.com)
Java Platform SE 8
docs.oracle.com
NESTED : 중첩 클래스 / 중첩 인터페이스 목록으로 이동하는 링크
FIELD : 필드 목록으로 이동하는 링크
CONSTR : 생성자 목록으로 이동하는 링크
METHOD : 메소드 목록으로 이동하는 링크
[ java.base 모듈 ]
java.base는 모든 모듈이 의존하는 기본 모듈.
아래는 java.base 모듈에 포함된 주요 패키지와 용도를 설명한 표이다.
패키지 | 용도 |
java.lang | 자바 언어의 기본 클래스를 제공 |
java.util | 자료 구조와 관련된 컬렉션 클래스를 제공 |
java.io | 입출력 스트림 클래스를 제공 |
아래는 java.lang 패키지에 포함된 주요 클래스와 용도를 설명한 표이다.
클래스 | 용도 |
Object | 자바 클래스의 최상위 클래스로 사용 |
System | - 프로세스를 종료시킬 때 사용 - 진행 시간을 읽을 때 사용 - 시스템 속성(프로피터)을 읽을 때 사용 - 모니터(콘솔)로 출력하기 위해 사용 - 키보드로부터 데이터를 입력받을 때 사용 |
문자열 관련 | String - 문자열을 저장하고 조작할 때 사용 StringBuilder - 효율적인 문자열 조작 기능이 필요할 때 사용 java.util.StringTokenizer - 구분자로 연결된 문자열을 분리할 때 사용 |
포장 관련 ( Wrapper ) Byte, Short, Character, Integer, Float, Double, Boolean |
- 기본 타입의 값을 포장할 때 사용 - 문자열을 기본 타입으로 변환할 때 사용 |
Math | 수학 계산이 필요할 때 사용 |
Class | 클래스의 메타 정보 ( 이름, 구성멤버 ) 등을 조사할 때 사용 |
[ Object 클래스 ] ☆★
클래스를 선언할 때 extends 키워드로 다른 클래스를 상속하지 않으면 암시적으로 java.lang.Object 클래스를 상속하게 된다. 따라서 자바의 모든 클래스는 Object의 자식이거나 자손 클래스 이다
Object <- System, String, Number
메소드 | 용도 |
boolean equals(Object obj) | 객체의 번지를 비교하고 결과를 리턴 |
int hashCode() | 객체의 해시코드를 리턴 |
String toString() | 객체 문자 정보를 리턴 |
※ 객체 동등 비교 : Object의 equals() 메소드는 객체의 번지를 비교하고 boolean 값을 리턴한다. equals() 메소드의 매개변수 타입이 Object이므로 자동 타입 변환에 의해 모든 객체가 매개값으로 대입될 수 있다. equals() 메소드는 비교 연산자인 ==과 동일한 결과를 리턴한다. 두 객체가 동일한 객체라면 true를 리턴하고, 그렇지 않으면 false를 리턴한다.
Object obj1 = new Object();
Object obj2 = obj1;
boolean result = obj1.equals(obj2); // 결과가 동일
boolean result = (obj1==obj2); // 결과가 동일
※ 객체 해시코드 : 객체 해시코드란 객체를 식별하는 정수를 말한다. Object의 hashCode() 메소드는 객체의 메모리번지를 이용해서 해시코드를 생성하기 때문에 객체마다 다른 정수값을 리턴한다. hashCode() 메소드의 용도는 equals() 메소드와 비슷한데, 두 객체가 동등한지 비교할 때 주로 사용한다. equals() 메소드와 마찬가지로 hashCode() 메소드 역시 객체의 데이터를 기준으로 재정의해서 새로운 정수값을 리턴하도록 하는 것이 일반적이다. 객체가 다르다 할지라도 내부 데이터가 동일하다면 같은 정수값을 리턴하기 위해서이다. HashSet은 동등 객체를 중복 저장하지 않는 특징이 있다. <> : angle blanket
public int hashCode()
※ 객체 문자 정보 ☆★ : Object의 toString() 메소드는 객체의 문자 정보를 리턴한다. 객체의 문자 정보란 객체를 문자열로 표현한 값을 말한다. 기본적으로 Object의 toString() 메소드는 '클래스명@16진수해시코드'로 구성된 문자열을 리턴한다.
※ 레코드 선언 : 데이터 전달을 위한 DTO ( Data Transfer Object ) ( 이름 알아두기 ! Spring 작업시 많이 나옴 ) 를 작성할 때 반복적으로 사용되는 코드를 줄이기 위해 도입되었다.
※ 롬복 사용하기 : 롬복은 레코드와 마찬가지로 DTO 클래스를 작성할 때, Getter, Setter, hashCode(), equals(), toString() 메소드를 자동생성하기 때문에 작성할 코드의 양을 줄여준다.
[ System 클래스 ]
정적 멤버 | 용도 | |
메소드 | exist(int status) | 프로세스 종료 |
currentTimeMillis() | 현재 시간을 밀리초 단위의 long 값으로 리턴 |
※ 콘솔 출력 : out 필드를 이용하면 콘솔에 원하는 문자열을 출력할 수 있다.
※ 키보드 입력 : 자바는 키보드로부터 입력된 키를 읽기 위해 System 클래스에서 in 필드를 제공한다. 다음과 같이 read()메소드를 호출하면 입력된 키의 코드값을 얻을 수 있다.
int keyCode = System.in.read();
read() 메소드는 호출과 동시에 키 코드를 읽는 것이 아니라, Enter 키를 누르기 전 까지는 대기 상태이다가 Enter 키를 누르면 입력했던 키들을 하나씩 읽기 시작한다.
※ 프로세스 종료 : 프로세스를 강제 종료하고 싶다면 System.exit() 메소드를 사용한다. exit() 메소드는 int 매개값이 필요한데, 이 값을 종료 상태값이라고 한다. 종료 상태값으로 어떤 값을 주더라도 프로세스는 종료되는데 정상 종료일 경우 0, 비정상 종료 1 또는 -1로 주는 것이 관례이다.
※ 진행 시간 읽기 : System 클래스의 currentTimeMillis() 메소드와 nanoTime() 메소드는 1970년 1월 1일 0시부터 시작해서 현재까지 진행된 시간을 리턴한다.
※ 시스템 프로퍼티 읽기 : 시스템 프로퍼티 ( System Property ) 란 자바 프로그램이 시작될 때 자동 설정되는 시스템의 속성을 말한다. 예를 들어 운영체제 종류 및 사용자 정보, 자바 버전 등의 기본 사양 정보가 해당한다.
[ 문자열 클래스 ]
클래스 | 설명 |
String | 문자열을 저장하고 조작할 때 사용 |
StringBuilder | 효율적인 문자열 조작 기능이 필요할 때 사용 |
StringTokenizer | 구분자로 연결된 문자열을 분리할 때 사용 |
※ String 클래스 : String 클래스는 문자열을 저장하고 조작할 때 사용한다. 문자열 리터럴은 자동으로 String 객체로 생성되지만, String 클래스의 다양한 생성자를 이용해서 직접 객체를 생성할 수도 있다. 프로그램을 개발하다 보면 byte 배열을 문자열로 변환하는 경우가 종종 있다. 예를 들어 네트워크 통신으로 얻은 byte 배열을 원래 문자열로 변환하는 경우이다. 이때는 String 생성자 중에서 다음 두가지를 사용해 String 객체로 생성할 수 있다.
// 기본 문자셋으로 byte 배열을 디코딩해서 String 객체로 생성
String str = new String(byte[] bytes);
// 특정 문자셋으로 byte 배열을 디코딩해서 String 객체로 생성
String str = new String(byte[] bytes, String charsetName);
다음 예제는 문자열을 byte 배열로 변환시키고 다시 문자열로 복원하는 방법을 보여준다. p.520
public class ByteToStringExample {
public static void main(String[] args) {
String data = "자바";
//String -> byte 배열 ( 기본 UTF-8 인코딩 )
byte[] arr1 = data.getBytes(); // byte[] arr1 = data.getBytes("UFT-8");
System.out.println("arr1: "+Arrays.toString(arr1));
//byte 배열 -> String ( 기본 UFT-8 디코딩 )
String str1 = new String(arr1); //String str1 = new String(arr1,"UFT-8");
System.out.println("str1: "+str1);
//String -> byte 배열( EUC-KR 인코딩 )
byte[] arr2 = data.getBytes("EUC-KR");
System.out.println("arr2: "+Arrays.toString(arr2));
//byte 배열 -> String ( 기본 UTF-8 디코딩 )
String str2 = new String(arr2, "EUC-KR");
System.out.println("str2: "+str2);
}
}
=> 출력결과 : arr1: [-20, -98, -112, -21, -80, -108] / str1: 자바 / arr2: [-64, -38, -71, -39] / str2: 자바
한글 1자를 UTF-8로 인코딩 하면 3바이트가 되고, EUC-KR로 인코딩 하면 2바이트가 된다. 따라서 인코딩할 때 사용한 문자셋으로 디코딩 해야만 한글이 올바르게 복원될 수 있다. ( UTF-8와 EUC-KR 차이점 알기 )
※ StringBuilder 클래스 : String은 내부 문자열을 수정할 수 없다. 다음 코드를 보면 다른 문자열을 결합해서 내부 문자열을 변경하는 것처럼 보이지만 사실 'ABCDEF' 라는 새로운 String 객체를 생성하는 것이다. 그리고 data 변수는 새로 생성된 String 객체를 참조하게 된다. 잦은 문자열 변경 작업을 해야 한다면 String 보다는 StringBuilder를 사용하는 것이 좋다. StringBuilder는 내부 버퍼 (데이터를 저장하는 메모리)에 문자열을 저장해두고 그 안에서 추가, 수정, 삭제 작업을 하도록 설계되어 있다. 따라서 String처럼 새로운 객체를 만들지 않고도 문자열을 조작할 수 있다.
String data = "ABC";
data += "DEF";
※ StringTokenizer 클래스 : 문자열이 구분자 ( delimiter ) 로 연결되어 있을 경우, 구분자를 기준으로 문자열을 분리하려면 String의 split() 메소드를 이용하거나 java.util 패키지의 StringTokenizer 클래스를 이용할 수 있다. split은 정규 표현식으로 구분하고, StringTokenizer는 문자로 구분한다는 차이점이 있다. ( 잘 쓰이지 않음 )
리턴 타입 | 메소드 ( 매개변수 ) | 설명 |
int | countTokens() | 분리할 수 있는 문자열의 총 수 |
boolean | hasMoreTokens() | 남아 있는 문자열이 있는지 여부 |
String | nextToken() | 문자열을 하나씩 가져옴 |
[ 포장 클래스 ]
자바는 기본 타입 ( byte, char, short, int, long, float, double, boolean )의 값을 갖는 객체를 생성할 수 있다. 이런 객체를 포장 ( wrapper ) 객체라고 한다. 값을 포장하고 있다고 해서 붙여진 이름이다. 포장 객체를 생성하기 위한 클래스는 java.lang 패키지에 포함되어 있는데, char 타입과 int 타입이 각각 Character와 Integer인 것만 제외하고는 기본 타입의 첫 문자를 대문자로 바꾼 이름을 가지고 있다.
※ 박싱과 언박싱 : 기본 타입의 값을 포장 객체로 만든는 과정을 박싱 ( boxing )이라고 하고, 반대로 포장 객체를 기본 타입의 값으로 얻어내는 과정을 언박싱 ( unboxing ) 이라고 한다. 박싱은 포장 클래스의 변수에 기본 타입 값이 대입될 때 발생한다. 반대로 언박싱은 기본 타입 변수에 포장 객체가 대입될 때 발생한다.
Integer obj = 100; // => 박싱
int value = obj; // => 언박싱
언박싱은 다음과 같이 연산 과정에서도 발생한다. obj는 50과 연산되기 전에 언박싱 된다.
int Value = obj + 50; // => 언박싱 후 연산
※ 문자열을 기본 타입 값으로 변환 : 포장 클래스는 문자열을 기본 타입 값으로 변활할 때도 사용된다. 대부분의 포장 클래스에는 'parse+기본타입' 명으로 되어있는 정적 ( static ) 메소드가 있다. 이 메소드는 문자열을 해당 기본 타입 값으로 변환한다.
※ 포장 값 비교 : 포장 객체는 내부 값을 비교하기 위해 ==와 != 연산자를 사용할 수 없다. 이 연산은 내부의 값을 비교하는 것이 아니라 포장 객체의 번지를 비교하기 때문이다. 예를 들어 다음 두 Integer 객체는 300이라는 동일한 값을 갖고 있지만 == 연산의 결과는 false다.
Integer obj1 = 300;
Integer obj2 = 300;
System.out.println(obj1==obj2);
예외도 있다. 포장 객체의 효율적인 사용을 위해 다음 범위의 값을 갖는 포장 객체는 공유된다. 이 범위의 값을 갖는 포장 객체는 ==와 != 연산자로 비교할 수 있지만, 내부 값을 비교하는 것이 아니라 객체 번지를 비교한다는 것을 알아야한다.
타입 | 값의 범위 |
boolean | true, false |
char | \u0000 ~ \u007f |
byte, short, int | -128 ~ 127 |
포장 객체에 정확히 어떤 값이 저장될 지 모르는 상황이라면 ==과 !=은 사용하지 않는 것이 좋다. 대신 equals() 메소드로 내부 값을 비교할 수 있다. 포장 클래스의 equals() 메소드는 내부의 값을 비교하도록 재정의되어 있다.
※ Integer.valueOf : valueOf에 int값 String값 모두 들어갈 수 있음.
parse는 반환형이 기본자료형 / valueOf는 반환형이 객체
int i3 = Integer.valueOf(123); // 123객체를 기본타입으로 바꿔주는언박싱
inti 4 = Integer.valueOf("10")+Integer.valueOf("20"); //문자열을 기본타입으로 바꿔주는 언박싱
System.out.println(i4); => 30 출력됨
// 문자열을 기본타입으로 변환 !!!
int i2=Integer.parseInt("123");
boolean b2=Boolean.parseBoolean("true");
double d2=Double.parseDouble("3.14");
//parse~~ 반환형이 기본자료형
int i3=Integer.valueOf(123); // 123객체를 기본타입으로 바꿔주는언박싱
int i4=Integer.valueOf("10")+Integer.valueOf("20"); //문자열을 기본타입으로
System.out.println(i4);
// valueOf는 반환형이 객체
//정수형 객체 123을 문자열로 변경 String에 저장
String a=Integer.toString(123);
System.out.println(a);
Integer i11=100; // auto boxing
int n1=i11+100; // auot unboxing
System.out.println(n1);
Integer i = new Integer(10); //정수 10을 i객체로 만듦. boxing
Integer j = 10; // auto boxing 자동 박싱
int a=j+10; // 자동 박싱으로 10은 객체가 되지만 객체 + 10이 됨 왜냐면
// 객체가 연산 되기 전에 자동언박싱 되어서 !
System.out.println(a);
int i1 = i.intValue(); // unboxing
Character c = new Character('a');
char c1=c.charValue();
Boolean b=new Boolean(true);
boolean b1=b.booleanValue();
[ 수학 클래스 ]
Math 클래스는 수학 계산에 사용할 수 있는 메소드를 제공한다. Math 클래스가 제공하는 메소드는 모두 정적 ( static ) 이므로 Math 클래스로 바로 사용이 가능하다. 다음은 math 클래스가 제공하는 주요 메소드이다.
구분 | 코드 | 리턴값 |
절대값 | int v1= Math.abs(-5); double v2=Math.abs(-3.14); |
v1=5 v2=3.14 |
올림값 | double v3=Math.ceil(5.3) double v4=Maath.ceil(-5.3) |
v3=6.0 v4=-5.0 |
버림값 | double v5=Math.floor(5.3); double v6-Mth.floor(-5.3); |
v5=5.0 v6=-6.0 |
최대값 | int v7=Math.max(5,9); double v8=Math.max(5.3,2.5); |
v7=9 v8=5.3 |
최소값 | int v9=Math.min(5,9); double v10=Math.min(5.3,2.5); |
v9=5 v10=2.5 |
랜덤값 | double v11=Math.random(); | 0.0<=v11<1.0 |
반올림값 | long v14=Math.round(5.3); long v15=Mah.round(5.7); |
v14=5 ( round는 소수점 리턴 x ) v15=6 |
random() 메소드는 0.0과 1.0 사이의 double 타입 난수를 리턴한다. 이 값을 이용해서 start부터 시작하는 n개의 정수 (start<=...<(start+n)) 중 하나의 정수를 얻기 위한 공식을 만들면 다음과 같다.
int num = (int) (Math.random() * n ) + start;
난수를 얻는 또 다른 방법으로는 java.util.Random 클래스를 이용할 수 있다. 이 클래스를 이용하면 boolean, int, double 난수를 얻을 수 있다. 다음은 Random 객체를 생성하기 위한 생성자이다.
객체 생성 | 설명 |
Random() | 현재 시간을 이용해서 종자값을 자동 설정한다. |
Random(long seed) | 주어진 종자값을 사용한다. |
종자값 ( seed ) 이란 난수를 만드는 알고리즘에 사용되는 값으로, 종자값이 같으면 같은 난수를 얻는다. 다음은 Random 클래스가 제공하는 메소드이다.
리턴값 | 메소드(매개변수) | 설명 |
boolean | nextBoolean() | boolean 타입의 난수를 리턴 |
double | nextDouble() | double 타입의 난수를 리턴(0.0<=!<1.0) |
int | nextInt() | int 타입의 난수를 리턴 (-232승<=~<=232승-1); |
int | nextInt(int n) | int 타입의 난수를 리턴(0<=~ |
[ 날짜와 시간 클래스 ] ( 그냥 알고만 있기 )
자바는 컴퓨터 날짜 및 시각을 일을 수 있도록 java.util 패키지에서 Data와 Calendar 클래스를 제공하고 있다. 또한 날짜와 시간을 조작할 수 있도록 java.time 패키지에서 LocalDateTime 등의 클래스를 제공한다.
클래스 | 설명 |
Date | 날짜 정보를 전달하기 위해 사용 |
Calendar | 다양한 시간대별로 날짜와 시간을 얻을 때 사용 |
LocalDateTime | 날짜와 시간을 조작할 때 사용 |
※ Date 클래스 : Date는 날짜를 표현하는 클래스로 객체 간에 날짜 정보를 주고 받을 때 사용된다. Date 클래스에는 여러 개의 생성자가 선언되어 있지만 대부분 Deprecated ( 더 이상 사용되지 않음 ) 되어 Date() 생성자만 주로 사용된다. Date() 생성자는 컴퓨터의 현재 날짜를 읽어 Date 객체로 만든다.
Date now = new Date();
현재 날짜를 문자열로 얻고 싶다면 toString() 메소드를 사용할 수 있지만 영문으로 출력되기 때문에 우리가 원하는 형식이 아니다. 원하는 문자열로 얻고 싶다면 SimpleDateFormat 클래스와 함께 사용해야 한다.
import java.text.*;
import java.util.*;
public class DateExample {
public static void main(String[] args) {
Date now = new Date();
String strNow1 = now.toString();
System.out.println(strNow1);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
String strNow2 = sdf.format(now);
System.out.println(strNow2);
}
}
// 달력 만들 때 사용
※ Calendar 클래스 : Calendar 클래스는 달력을 표현하는 추상 클래스이다. 날짜와 시간을 계산하는 방법이 지역과 문화에 따라 다르기 때문에 특정역법 ( 날짜와 시간을 매기는 방법 )에 따르는 달력은 자식 클래스에서 구현하도록 되어 있다. 특별한 역법을 사용하는 경우가 아니라면 직접 하위 클래스를 만들필요는 없고, Calendar 클래스의 정적 메소드인getInstance() 메소드를 이용하면 컴퓨터에 설정되어있는 시간대 ( TimeZone )를 기준 Calendar 하위객체를 얻을 수 있다.
Calendar now = Calendar.getInstance();
int year = now.get(Calendar.YEAR); // 년도를 리턴
int month = now.get(Calendar.MONTH); // 월을 리턴
int day = now.get(Calendar.DAY_OF_MONTH; // 일을 리턴
int week = now.get(Calendar.DAY_OF_WEEK); // 요일을 리턴
int amPm = now.get(Calendar.AM_PM); // 오전/오후를 리턴
int hour = now.get(Calendar.HOUR); // 시를 리턴
int minute = now.get(Calendar.MINUTE); // 분을 리턴
int second = now.get(Calendar.SECOND); // 초를 리턴
※ 날짜와 시간 조작 : Date와 Calendar는 날짜와 시간 정보를 얻기에는 충분하지만, 날짜와 시간을 조작할 수는 없다. 이떄 java.time 패키지는 LocalDateTime 클래스가 제공하는 다음 메소드를 이용하면 매우 쉽게 날짜와 시간을 조작할 수 있다.
[ 형식 클래스 ]
Format 형식 클래스는 숫자 또는 날짜를 원하는 형태의 문자열로 변환해주는 기능을 제공한다. Format 클래스는 java.text 패키지에 포함되어 있는데, 주요 Format 클래스는 다음과 같다.
Format 클래스 | 설명 |
DecimalFormat | 숫자를 형식화된 문자열로 반환 |
SimpleDateFormat | 날짜를 형식화된 문자열로 반환 |
[ 정규 표현식 클래스 ]
문자열이 정해져 있는 형식으로 구성되어 있는지 검증해야 하는 경우가 있다. 예를들어 이메일이나 전화번호를 사용자가 제대로 입력했는지 검증할 때이다. 자바는 정규 표현식 ( Regular Experssion ) 을 이용해서 문자열이 올바르게 구성되어 있는지 검증한다.
[ 리플렉션 ]
자바는 클래스와 인터페이스의 메타 정보를 Class 객체로 관리한다. 여기서 메타 정보란 패키지 정보, 타입 정보, 멤버 ( 생성자, 필드, 메소드 ) 정보 등을 말한다. 이러한 메타 정보를 프로그램에서 읽고 수정하는 행위를 리플렉션 ( reflection ) 이라고 한다. 프로그램에서 Class 객체를 얻으려면 다음 3가지 방법 중 하나를 이용하면 된다.
1) Class clazz = 클래스이름.class; => 클래스로부터 얻는 방법
2) Class clazz = Class.forName("패키지...클래스이름"); => 클래스로부터 얻는 방법 ( 제일 많이 사용 )
3) Class clazz = 객체참조변수.getClass(); => 객체로부터 얻는 방법
※ 패키지와 타입 정보 얻기 : String getName() 패키지를 포함한 전체 타입 이름
[ 어노테이션 ]
코드에서 @으로 작성되는 요소를 어노테이션 ( Annotation ) 이라고 한다. 어노테이션은 클래스 또는 인터페이스를 컴파일 하거나 실행할 때 어떻게 처리해야 할 것인지를 알려주는 설정 정보이다. 어노테이션은 다음 세가지 용도로 사용된다.
1. 컴파일 시 사용하는 정보 전달
2. 빌드 툴이 코드를 자동으로 생성할 때 사용하는 정보 전달
3. 실행 시 특정 기능을 처리할 때 사용하는 정보 전달
컴파일 시 사용하는 정보 전달의 대표적인 예는 @Override 어노테이션이다. @Override는 컴파일러가 메소드 재정의 검사를 하도록 설정한다. 웹 개발에 많이 사용되는 SpringFramework 또는 Spring Boot는 다양한 종류의 어노테이션을 사용해서 웹 애플리케이션을 설정하는 데 사용된다.
[ 멀티 스레드 개념 ]
운영체제는 실행 중인 프로그램을 프로세스 ( process )로 관리한다. 멀티 태스킹 ( multi tasking )은 두 가지 이상의 작업을 동시에 처리하는 것을 말하는데, 이때 운영체제는 멀티 프로세스를 생성해서 처리한다. 하지만 멀티 태스킹이 꼭 멀티 프로세를 뜻하지는 않는다. 하나의 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진 프로그램들도 있다. 예를 들어 메신저는 채팅 작업을 하면서 동시에 파일 전송 작업을 수행하기도 한다. 하나의 프로세스가 두 가지 이상의 작업을 처리할 수 있는 이유는 멀티 스레드 ( multi thread )가 있기 때문이다. 스레드 ( thread )는 코드의 실행 흐름을 말하는데, 프로세스 내에 스레드가 두 개라면 두개의 코드 실행 흐름이 생긴다는 의미이다. 멀티 프로세스가 프로그램 단위의 멀티 태스킹이라면 멀티 스레드는 프로그램 내부에서의 멀티 태스킹이라고 볼 수 있다. 다음 그림은 멀티 프로세스와 멀티 스레드의 차이점을 보여준다.
멀티 프로세스들은 서로 독립적이므로 하나의 프로세스에서 오류가 발생해도 다른 프로세스에게 영향을 미치지 않는다. 하지만 멀티 스레드는 프로세스 내부에서 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스가 종료되므로 다른 스레드에게 영향을 미친다. 예를 들어 워드와 엑셀을 동시에 사용하는 도중에 워드에 오류가 생겨 먹통이 되더라도 엑셀은 여전히 사용 가능하다. 그러나 멀티 스레드로 동작하는 메신저의 경우, 파일을 전송하는 스레드에서 예외가 발생하면 메신저 프로세스 자체가 종료되기 때문에 채팅 스레드도 같이 종료된다. 그렇기 때문에 멀티 스레드를 사용할 경우에는 예외 처리에 만전을 기해야 한다. 멀티 스레드는 데이터를 분할해서 처리하는 곳에서 사용하기도 하고, 안드로이드 앱에서 네트워크 통신을 하기 위해 사용하기도 한다. 또한 다수의 클라이언트 요청을 처리하는 서버를 개발할 때에도 사용된다. 프로그램 개발에 있어서 멀티 스레드는 꼭 필요한 기능이기 때문에 반드시 이해하고 활용할 수 있도록 한다.
[ 메인 스레드 ]
모든 자바 프로그램은 메인 스레드 ( main thread )가 main() 메소드를 실행하면서 시작된다. 메인 스레드는 main() 메소드의 첫 코드부터 순차적으로 실행하고, main() 메소드의 마지막 코드를 실행하거나 return 문을 만나면 실행을 종료한다.
public static void main(String[] args) { => 코드의 실행 흐름 => 메인 스레드
String data = null;
if(...) {
}
while(...) {
}
System.out.println("...");
}
메인 스레드는 필요에 따라 추가 작업 스레드들을 만들어서 실행시킬 수 있다. 아래 그림에서 오른쪽의 멀티 스레드를 보면 메인 스레드가 작업 스레드 1을 생성하고 실행시킨 다음, 곧이어 작업 스레드2를 생성하고 실행시키는 것을 볼 수 있다.
싱글 스레드에서는 메인 스레드가 종료되면 프로세스도 종료된다. 하지만 멀티 스레드에서는 실행 중인 스레드가 하나라도 있다면 프로새스는 종료되지 않는다. 메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않는다.
[ 작업 스레드 생성과 실행 ]
멀티 스레드로 실행하는 프로그램을 개발하려면 먼저 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해야 한다.
자바 프로그램은 메인 스레드가 반드시 존재하기 때문에 메인 작업 이외에 추가적인 작업 수 만큼 스레드를 생성하면 된다. 자바는 작업 스레드도 객체로 관리하므로 클래스가 필요하다. Thread 클래스로 직접 객체를 생성해도 되지만, 하위 클래스를 만들어 생성할 수도 있다.
※ Thread 클래스로 직접 생성 : java.lang 패키지에 있는 Thread 클래스로 부터 작업 스레드 객체를 직접 생성하려면 다음과 같이 Runnable 구현 객체를 매개값으로 갖는 생성자를 호출하면 된다.
Thread thread = new Thread(Runnable target);
Runnable은 스레드가 작업을 실행할 때 사용하는 인터페이스이다. Runnable에는 run() 메소드가 정의되어 있는데, 구현 클래스는 run()을 재정의해서 스레드가 실행할 코드를 가지고 있어야한다.
※ Thread 자식 클래스로 생성 : 작업 스레드 객체를 생성하는 또 다른 Thread의 자식 객체로 만드는 것이다. Thread 클래스를 상속한 다음 run() 메소드를 재정의해서 스레드가 실행할 코드를 작성하고 객체를 생성하면 된다.
public class WorkerThread extends Thread {
@Override
public void run() {
// 스레드가 실행할 코드
}
}
// 스레드 객체 생성
Thread thread = new WorkerThread();
[ 스레드 이름 ]
스레드는 자신의 이름을 가지고 있다. 메인 스레드는 'main' 이라는 이름을 가지고 있고, 작업 스레드는 자동적으로 'Thread-n'이라는 이름을 가진다. 작업 스레드의 이름을 Thread-n 대신 다른 이름으로 설정하고 싶다면 Thread 클래스의 setName() 메소드를 사용하면 된다.
thread.setName("스레드 이름");
스레드 이름은 디버깅할 때 어떤 스레드가 작업을 하는지 조사할 목적으로 주로 사용된다. 현재 코드를 어떤 스레드가 실행하고 있는지 확인하려면 정적 메소드인 currentThread()로 스레드 객체의 참조를 얻은 다음 getName() 메소드로 이름을 출력해보면 된다.
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
[ 스레드 상태 ]
스레드 객체를 생성 ( NEW )하고, start() 메소드를 호출하면 곧바로 스레드가 실행되는 것이 아니라 실행 대기 상태 ( RUNNABLE )가 된다. 실행 대기 상태란 실행을 기다리고 있는 상태를 말한다. 실행 대기하는 스레드는 CPU 스케쥴링에 따라 CPU를 점유하고 run() 메소드를 실행한다. 이때를 실행 ( RUNNING ) 상태라고 한다. 실행 스레드는 run() 메소드를 모두 실행하기 전에 스케쥴링에 의해 다시 실행 대기 상태로 돌아갈 수 있다. 그리고 다른 스레드가 실행 상태가 된다. 이렇게 스레드는 실행 대기 상태와 실행 상태를 번갈아 가면서 자신의 run() 메소드를 조금씩 실행한다. 실행 상태에서 run() 메소드가 종료되면 더 이상 실행할 코드가 없기 때문에 스레드의 실행은 멈추게 된다. 이 상태를 종료 상태 ( TERMINATED )라고 한다.
실행 상태에서 일시 정지 상태로 가기도 하는데, 일시 정지 상태는 스레드가 실행할 수 없는 상태를 말한다. 스레드가 다시 실행 상태로 가기 위해서는 일시 정지 상태에서 실행 대기 상태로 가야만 한다. 다음은 일시 정지로 가기 위한 메소드와 벗어나기 위한 메소드들을 보여준다.
구분 | 메소드 | 설명 |
일시 정지로 보냄 | sleep(long millis) | 주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. |
join( ) | join() 메소드를 호출할 스레드는 일시 정지 상태가 된다. 실행 대기 상태가 되려면, join() 메소드를 가진 스레드가 종료되어야 한다. | |
wait( ) | 동기화 블록 내에서 스레드를 일시 정지 상태로 만든다. | |
일시 정지에서 벗어남 | interrupt( ) | 일시 정지 상태일 경우, InterruptedException을 발생시켜 실행 대기 상태 또는 종료 상태로 만든다. |
notify( ) notifyAll( ) |
wait() 메소드로 인해 일시 정지 상태인 스레드를 실행 대기 상태로 만든다. | |
실행 대기로 보냄 | yield( ) | 실행 상태에서 다른 스레드에게 실행을 양보하고 실행 대기 상태가 된다. 멀티스레드일 경우 많이 사용 된다. |
위 표에서 wait() notify() notifyAll()은 Object 클래스의 메소드이고 그 외는 Thread 클래스의 메소드이다.
※ 주어진 시간동안 일시 정지 : 실행 중인 스레드를 일정 시간 멈추게 하고 싶다면 Thread 클래스의 정적 메소드인 sleep()을 이용하면 된다. 매개값에는 얼마 동안 일시 정지 상태로 있을 것인지 밀리세컨드(1/1000) 단위로 시간을 주면 된다. 일시 정지 상태에서는 InterruptedException이 발생할 수 있기 때문에 sleep()은 예외 처리가 필요한 메소드이다.
try {
Thread.sleep(1000); // 1초 동안 일시 정지 상태를 만드는 방법
} catch(InterruptedException e) {
// interrupt() 메소드가 호출되면 실행
}
※ 다른 스레드의 종료를 기다림 : 스레드는 다른 스레드와 독립적으로 실행하지만 다른 스레드가 종료될 때까지 기다렸다가 실행을 해야 하는 경우도 있다. 예를 들어 계산 스레드의 작업이 종료된 후 그 결과값을 받아 처리하는 경우이다. 이를 위해 스레드는 join() 메소드를 제공한다. ThreadA가 ThreadB의 join() 메소드를 호출하면 ThreadA는 ThreadB가 종료할 때 까지 일시 정지 상태가 된다. ThreadB의 run() 메소드가 종료되고 나서야 비로소 ThreadA는 일시 정지에서 풀려 다음 코드를 실행한다.
※ 다른 스레드에게 실행 양보 : 스레드가 처리하는 작업은 반복적인 실행을 위해 for문이나 while문을 포함하는 경우가 많은데, 가끔 반복문이 무의미한 반복을 하는 경우가 있다. 다음 코드를 보자. work의 값이 false라면 while문은 어떠한 실행문도 실행하지 않고 무의미한 반복을 한다.
public void run() {
while(true) {
if(work) {
System.out.println("ThreadA 작업 내용");
}
}
}
이때는 다른 스레드에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 프로그램 성능에 도움이 된다. 이런 기능을 위해 Thread는 yield()메소드를 제공한다. yield()를 호출한 스레드는 실행 대기 상태로 돌아가고, 다른 스레드가 실행 상태가 된다.
다음 무의미한 반복을 하지 않고 다른 스레드에게 실행을 양보하도록 이전 코드를 수정한 것이다.
public void run() {
while(true) {
if(work) {
System.out.println("ThreadA 작업 내용");
} else {
Thread.yield(); }
}
}
[ 스레드 동기화 ]
멀티 스레드는 하나의 객체를 공유해서 작업할 수도 있다. 이 경우, 다른 스레드에 의해 객체 내부 데이터가 쉽게 변경될 수 있기 때문에 의도했던 것과는 다른 결과가 나올 수 있다. 스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸면 된다. 이를 위해 자바는 동기화 ( synchronized ) 메소드와 블록을 제공한다.
※ 동기화 메소드 및 블록 선언 : 동기화 메소드를 선언하는 방법은 다음과 같이 synchronized 키워드를 붙이면 된다. synchronized 키워드는 인스턴스와 정적 메소드 어디든 붙일 수 있다.
public synchronized void method() {
// 단 하나의 스레드만 실행하는 영역
}
스레드가 동기화 메소드를 실행하는 즉시 객체는 잠금이 일어나고, 메소드 실행이 끝나면 잠금이 풀린다. 메소드 전체가 아닌 일부 영역을 실행할 때만 객체 잠금을 걸고 싶다면 다음과 같이 동기화 블록을 만들면 된다.
public void method() {
// 여러 스레드가 실행할 수 있는 영역
synchronized( 공유객체 ) {
//단 하나의 스레드만 실행하는 영역
}
// 여러 스레드가 실행할 수 있는 영역
}
※ wait()과 notify()를 이용한 스레드 제어 ( 잘 사용하지 않음 ) : 경우에 따라서 두 개의 스레드를 교대로 번갈아 가며 실행할 때도 있다. 정확한 교대 작업이 필요할 경우, 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고 자신은 일시 정지 상태로 만들면 된다. 이 방법의 핵심은 공유 객체에 있다. 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 정해 놓는다. 한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업을 하지 않도록 wait() 메소드를 호출하여 일시 정지 상태로 만든다.
notify()는 wait()에 의해 일시 정지 된 스레드 중 한개를 실행 대기 상태로 만들고, notifyAll()은 wait()에 의해 일시 정지된 모든 스레드를 실행 대기 상태로 만든다. 주의할 점은 이 두 메소드는 동기화 메소드 또는 동기화 블록 내에서만 사용할 수 있다는 것이다.
class Cook { // 요리쓰레드
String food;
boolean send = false;
void set(String f) {
food = f; // 초기화 코드
send = true;
// synchronized void pr()
synchronized (this) {
notifyAll(); // 잠자고 있는 쓰레드 다 깨운다!
}
}
String get() {
if (send == false) { // 음식 도착안했으면
try {
synchronized (this) {
wait(); // 손님이 기다림
}
} catch (Exception e) {
e.printStackTrace();
}
}
return food;
}
}
class Chef extends Thread { // 요리사
Cook c;
Chef(Cook c) {
this.c = c;
}
public void run() {
c.set("음식");
}
}
class Custumer extends Thread {
Cook c;
Custumer(Cook c) {
this.c = c;
}
public void run() {
System.out.println(c.get());
}
}
public class Test {
public static void main(String[] args) {
Cook co = new Cook();
Custumer c1 = new Custumer(co);
Custumer c2 = new Custumer(co);
Chef c3 = new Chef(co);
try {
c1.start();
c2.start();
Thread.sleep(1000);
c3.start();
c1.join();
c2.join();
c3.join();
} catch (Exception e) {
}
}
}
[ 스레드 안전 종료 ]
스레드는 자신의 run() 메소드가 모두 실행되면 자동적으로 종료 되지만, 경우에 따라서는 실행 중인 스레드를 즉시 종료한 필요가 있다. 예를 들어 동영상을 끝까지 보지 않고 사용자가 멈춤을 요구하는 경우이다. 스레드를 강제 종료하기 위해 Thread는 stop() 메소드를 제공하고 있으나 이 메소드는 deprecated ( 더 이상 사용하지 않음 ) 되었다. 그 이유는 스레드를 갑자기 종료하게 되면 사용 중이던 리소스들이 불안전한 상태로 남겨지기 때문이다. 여기에서 리소스란 파일, 네트워크 연결 등을 말한다. 스레드를 안전하게 종료하는 방법은 사용하던 리소스들을 정리하고 run() 메소드를 빨리 종료하는 것이다. 주로 조건 이용 방법과 interrupt() 메소드 이용 방법을 사용한다.
※ 조건 이용 : 스레드가 while 문으로 반복 실행할 경우, 조건을 이용해서 run() 메소드의 종료를 유도할 수 있다. 다음 코드는 stop 필드 조건에 따라서 run() 메소드의 종료를 유도한다.
public class XXXThread extends Thread {
private boolean stop; // stop이 필드 선언
public void run() {
while ( !stop) { // stop이 true가 되면 while문을 빠져나감
// 스레드가 반복 실행하는 코드;
}
// 스레드가 사용한 리소스 정리 // 리소스 정리
} // 스레드 종료
}
※ interrupt() 메소드 이용 : interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다. 이것을 이용하면 예외 처리를 통해 run() 메소드를 정상 종료시킬 수 있다.
[ 데몬 스레드 ] x
데몬 ( daemon ) 스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다. 주 스레드가 종료되면 데몬 스레드도 따라서 자동으로 종료된다. 데몬 스레드를 적용한 예로는 워드프로세서의 자동 저장, 미디어 플레이어의 동영상 및 음악 재생, 가비지 컬렉터 등이 있는데, 여기에서 주 스레드(워드프로세스, 미디어플레이어, JVM)가 종료되면 데몬 스레드도 같이 종료된다. 스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true)를 호출하면 된다.
[ 스레드풀 ]
병렬 작업 처리가 많아지면 스레드의 개수가 폭증하여 CPU가 바빠지고 메모리 사용량이 늘어난다. 이에 따라 애플리케이션의 성능 또한 급격히 저하된다. 이렇게 병렬 작업 증가로 인한 스레드의 폭증을 막으려면 스레드풀 ( ThreadPool )을 사용하는 것이 좋다. 스레드풀 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐 ( Queue )에 들어오는 작업들을 스레드가 하나씩 맡아 처리하는 방식이다. 작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리한다. 이렇게 하면 작업량이 증가해도 스레드의 개수가 늘어나지 않아 애플리케이션의 성능이 급격히 저하되지 않는다.
※ 스레드풀 생성 : 자바는 스레드풀을 생성하고 사용할 수 있도록 java.util.concurrent 패키지에서 ExecutorService 인터페이스와 Executor 클래스를 제공하고 있다. Executors의 다음 두 정적 메소드를 이용하면 간단하게 스레드풀인 ExecutorService 구현 객체를 만들 수 있다.
메소드명(매개변수) | 초기 수 | 코어 수 | 최대 수 |
newCachedThreadPool() | 0 | 0 | Integer.MAX_VALUE |
newFixedThreadPool(int n Threads) | 0 | 생성된 수 | nThreads |
초기 수는 스레드풀이 생성될 때 기본적으로 생성되는 스레드 수를 말하고, 코어수는 스레드가 증가된 후 사용되지 않는 스레드를 제거할 때 최소한 풀에서 유지하는 스레드 수를 말한다. 그리고 최대 수는 증가하는 스레드의 한도 수이다.
[ 복습 연습 ]
'네이버 클라우드 부트캠프 > 복습 정리' 카테고리의 다른 글
12일차 Java [ 컬렉션 자료구조 ] (1) | 2024.03.07 |
---|---|
11일차 Java [ 제네릭 ] (1) | 2024.03.06 |
9일차 Java [ 예외 처리 ] (0) | 2024.03.04 |
8일차 Java [ 인터페이스 ] ☆★ (0) | 2024.02.29 |
7일차 Java [ 상속, 객체배열 ] ☆★ (4) | 2024.02.28 |