Difference between r1.4 and the current
@@ -11,7 +11,7 @@
[[TableOfContents]]
=CallBack=
= CallBack =
CallBack 함수: 자기가 직접 호출 하는 것이 아닌 다른 것에 의해 호출되는 함수를 콜백 함수라고 부릅니다. Callback이라는 개념 자체는 다른 코드를 매개변수로 받아서 실행하는 코드를 말하는데 검색하면 아마 OS의 이벤트 핸들러랑 C언어의 qsort함수가 자주 언급이 될 거에요…
(C언어의 함수포인터가 콜백함수의 예시라서 그렇습니다)
@@ -28,7 +28,8 @@
=Java 네트워크=
= Java 네트워크 =
일단 가장 먼저 기본적인 네트워크를 만들 수 있는 클래스들은 java.net이라는 패키지 속에 들어있어요. 네트워크를 하고 싶으면 얘부터 import하는게 기본일 거에요. 여기에는 네트워크에서 가장 중요한 통신, 그러니까 “내가 쟤한테 어떤 정보를 전달할거야”를 자동으로 해주는 클래스들이 들어있어요. 그 전에… 네트워크니까… 조금 지루한 얘기부터 먼저 갑시다. 흘려 들어도 상관은 없다고 생각해요...(글만 두두둑 적힌 이 문서 자체가 이미 지겹지만…)
@@ -53,7 +54,7 @@
그리고 소켓 또한 스트림처럼 다 쓰면 닫아줘야 하는데, 먼저 소켓에서 뽑아낸 스트림들을 모두 닫고( ex)bufferedReader.close();) 소켓을 닫아야 합니다. socket.close();
=Java Thread=
= Java Thread =
두 달만에 쓰는 쓰레드 이야기…
플젝 팀원들이 회의 때든지 어느 때든 왠지 모르겠다 싶은 거나 진짜 몰랐던 것들을 공부하자는 차원에서 적어보는 문서입니다. 아무런 생각이 없어도 일단 찾아가면서 적어보고 자바든 뭐든 일단 배워봅시다.
1. CallBack ¶
CallBack 함수: 자기가 직접 호출 하는 것이 아닌 다른 것에 의해 호출되는 함수를 콜백 함수라고 부릅니다. Callback이라는 개념 자체는 다른 코드를 매개변수로 받아서 실행하는 코드를 말하는데 검색하면 아마 OS의 이벤트 핸들러랑 C언어의 qsort함수가 자주 언급이 될 거에요…
(C언어의 함수포인터가 콜백함수의 예시라서 그렇습니다)
qsort를 예로 들면, 만약 일반적으로 내가 만든 sort함수를 부른다고(call)한다고 하면 배열을 받아서 정렬해서 다시 내놓는 sort함수를 짜서 쓰겠죠. 그러면 결국 나는 sort보고 “자 이런 배열을 던져줄테니까 알아서 정렬해서 다시 나한테 내놔. 난 기다리고 있을께”라고 하는 게 보통 함수를 부르는 거에요. 내가 일을 하다가 함수를 불러서 함수가 혼자서 일을 하고 그 결과를 받아서 내가 다시 일을 하는 과정이 보통 함수를 call하는 과정이에요.
callback함수를 활용한다면, qsort를 예로 들 때, qsort에 이제 정렬할 배열과 어떻게 정렬해야 될지 적힌 callback함수를 포인터를 통해 매개변수로 넣어줍니다.(qsort니까 보통 어느 쪽이 큰지 비교하는 함수를 넣어놓겠죠?) 이렇게 해서 qsort를 호출하면, “자 여기 이 쪽으로 전화하면 내가 어떻게 비교할 지는 알려줄테니까 넌 내가 비교하는 방법을 알려주면 그거 대로 정렬해서 나한테 돌려줘”라고 하면서 qsort를 호출할 거에요.
qsort의 경우는 자기가 정렬하는 도중에 콜백함수(비교함수)를 사용하기 때문에 자기가 반환되기 전에 콜백함수를 사용하는 경우고, 이벤트 핸들러의 경우는 JAVA를 예로 들면, 이벤트가 발생했다는 결과를 이벤트 관리하는 곳에서 반환을 하면, 그 때 미리 적어뒀던 “새 창을 띄워라” 같은 메소드가 동작하는 것처럼 자기가 반환된 후에 콜백함수를 호출하는 경우도 있어요.
이런 방법을 활용하는 이유는 여러가지가 있어요. qsort같은 어떤 자료형이든 처리할 수 있는 범용 라이브러리 함수의 경우, 이렇게 하면 굳이 int, float, String 등등에 대해서 따로 따로 만들 필요가 없어요. 또 위의 이벤트의 경우에는 GUI는 GUI대로 일하고, 이벤트는 이벤트대로 일하다가 이벤트가 GUI를 바꿀 때만 GUI가 이벤트를 위해서 일해주고 다시 각자 자기 일하게 짤 수 있죠.(이게 없었다면 아마 GUI가 보여주는 일하다가, 이벤트 들어왔는지 보다가, 다시 보여주는 일을 하는 것을 혼자서 해야 했을 것이고, GUI 바꾸다가 이벤트 놓치는 일도 일어났을 거에요.)
출저: KLDP, Wikipedia 등등...
(C언어의 함수포인터가 콜백함수의 예시라서 그렇습니다)
callback함수를 활용한다면, qsort를 예로 들 때, qsort에 이제 정렬할 배열과 어떻게 정렬해야 될지 적힌 callback함수를 포인터를 통해 매개변수로 넣어줍니다.(qsort니까 보통 어느 쪽이 큰지 비교하는 함수를 넣어놓겠죠?) 이렇게 해서 qsort를 호출하면, “자 여기 이 쪽으로 전화하면 내가 어떻게 비교할 지는 알려줄테니까 넌 내가 비교하는 방법을 알려주면 그거 대로 정렬해서 나한테 돌려줘”라고 하면서 qsort를 호출할 거에요.
qsort의 경우는 자기가 정렬하는 도중에 콜백함수(비교함수)를 사용하기 때문에 자기가 반환되기 전에 콜백함수를 사용하는 경우고, 이벤트 핸들러의 경우는 JAVA를 예로 들면, 이벤트가 발생했다는 결과를 이벤트 관리하는 곳에서 반환을 하면, 그 때 미리 적어뒀던 “새 창을 띄워라” 같은 메소드가 동작하는 것처럼 자기가 반환된 후에 콜백함수를 호출하는 경우도 있어요.
이런 방법을 활용하는 이유는 여러가지가 있어요. qsort같은 어떤 자료형이든 처리할 수 있는 범용 라이브러리 함수의 경우, 이렇게 하면 굳이 int, float, String 등등에 대해서 따로 따로 만들 필요가 없어요. 또 위의 이벤트의 경우에는 GUI는 GUI대로 일하고, 이벤트는 이벤트대로 일하다가 이벤트가 GUI를 바꿀 때만 GUI가 이벤트를 위해서 일해주고 다시 각자 자기 일하게 짤 수 있죠.(이게 없었다면 아마 GUI가 보여주는 일하다가, 이벤트 들어왔는지 보다가, 다시 보여주는 일을 하는 것을 혼자서 해야 했을 것이고, GUI 바꾸다가 이벤트 놓치는 일도 일어났을 거에요.)
2. Java 네트워크 ¶
Java는 (예전에는 그랬다는데 지금은 모르지만) 주요 특징 중 하나로 꼽힐 정도로 네트워크에 대한 기능이 정말 강력해요. 컴퓨터 간에 직접 연결하는 것은 기본, 우리가 보통 아는 주소 창에 치는 저 주소들(URL이라고 부르죠.)로도 여러가지 네트워크 프로그래밍을 할 수 있고, 데이터베이스를 활용할 때, 여러 개의 컴퓨터들의 연결도 묶어서 관리할 수도 있게 만들 수 있을 정도로 여러가지 기능이 많아요. 그 중에서 이번 프로젝트 1.0버전에서 반드시 써야 될 기본적인 네트워크 구현에 대해 언급을 할께요.
일단 가장 먼저 기본적인 네트워크를 만들 수 있는 클래스들은 java.net이라는 패키지 속에 들어있어요. 네트워크를 하고 싶으면 얘부터 import하는게 기본일 거에요. 여기에는 네트워크에서 가장 중요한 통신, 그러니까 “내가 쟤한테 어떤 정보를 전달할거야”를 자동으로 해주는 클래스들이 들어있어요. 그 전에… 네트워크니까… 조금 지루한 얘기부터 먼저 갑시다. 흘려 들어도 상관은 없다고 생각해요...(글만 두두둑 적힌 이 문서 자체가 이미 지겹지만…)
네트워크를 얘기할 때는 이게 사람이 택배로 물건 보내는 것을 컴퓨터에 적용한 거라 택배 보낼 때처럼 각각의 한 가지 일들이 계층처럼 쌓여서 구분되어 있어요. 지금은 이 계층을 설명할 때는 OSI 7계층이라는 개념을 사용하고, 실제로 우리가 통신을 할 때는, TCP/IP 4계층(=인터넷 프로토콜 스위트)을 주로 사용하는데, OSI 7계층의 하나하나를 설명하기는 그렇고 일단 얘가 통신을 하는데 필요한 기능들이 뭐가 있는가를 7개로 나누고 각각의 기능들을 쓸 때, 쓸 수 있는 규칙(프로토콜) 들을 분류해 놓은 녀석이라는 것만 알아두세요. 그리고 TCP/IP는 이 OSI보다는 사람들이 쓰기 쉬운 방향으로 변한 녀석이에요.
TCP/IP에는 직접 선이나 무선으로 연결하는 것을 관리하는 링크 계층과, 데이터가 목적지를 향해서 가는 방법을 관리하는 인터넷 계층과 데이터를 주고받는 것을 관리하는 전송 계층과 주고받은 데이터를 특정 프로그램과 연결하는 응용 계층이 있어요. 우리는 응용계층의 프로그램을 짜고 있고, 인터넷 연결은 선이나 와이파이로 하고, 데이터가 날아가는 방향은 보통 네트워크 장비(공유기 등)이 하는 일이에요. 따라서 Java는 전송 계층에 관한 일을 미리 구현해서 java.net에 넣어놨고, 우리는 그 클래스들을 쓰면 됩니다.
전송 계층을 위해 사용할 클래스는 Socket계열 또는 URL계열의 클래스와 Datagram계열의 클래스로 나뉩니다. 앞의 클래스는 데이터의 순서와 데이터가 확실하게 전송되었다고 확인할 수 있는 TCP라는 프로토콜을 사용할 때, 뒤의 클래스는 데이터를 확인하지 않고 전송해서 조금이라도 더 속도를 빨리 하겠다는 UDP라는 프로토콜을 사용할 때 씁니다. 우리는 채팅이나 게임 정보가 제대로 도착해야하므로, TCP를 사용하는 클래스를 쓸 겁니다. 그리고 인터넷 주소로 플레이어끼리 연결할 수 없기 때문에, URL계열의 클래스가 아닌 Socket계열의 클래스를 쓸 겁니다.
Socket은 프로그램의 네트워크 입구인 소켓을 구현해주는 클래스에요. 소켓을 구현할 때는 포트 번호라는 것을 써야 하는데, 이것은 예로 들어, 컴퓨터가 빌딩이고 프로그램이 한 방 일때, 포트번호는 방 번호입니다. 택배가 올 때는 보통 주소를 통해서 오죠. 이처럼 컴퓨터로 데이터가 올 때는 IP주소로 찾아오는데 이 다음에는 어느 프로그램이 이 데이터를 쓸 지 모르면 안되겠죠. 그럴 때 2바이트 길이의 포트번호를 사용해서 데이터를 쓸 프로그램을 찾아갑니다. 이 포트번호는 보통 앞의 약 1000번대까지는 컴퓨터가 네트워크에 기본적으로 사용하는 번호에요. 따라서 자기가 임의로 만든 프로그램들은 원래 쓰고 있는 포트 번호들과 충돌하지 않도록 10000~60000(포트 번호는 65535번 까지)번 사이 중 하나를 쓰는 게 좋습니다.
이 소켓을 구현할 때는 Java에서는 다음과 같이 합니다.
먼저, 서버에서 ServerSocket serverSocket = new ServerSocket(portNum);을 해서 클라이언트의 연결 요청을 받을 소켓을 만듭니다.네트워크를 얘기할 때는 이게 사람이 택배로 물건 보내는 것을 컴퓨터에 적용한 거라 택배 보낼 때처럼 각각의 한 가지 일들이 계층처럼 쌓여서 구분되어 있어요. 지금은 이 계층을 설명할 때는 OSI 7계층이라는 개념을 사용하고, 실제로 우리가 통신을 할 때는, TCP/IP 4계층(=인터넷 프로토콜 스위트)을 주로 사용하는데, OSI 7계층의 하나하나를 설명하기는 그렇고 일단 얘가 통신을 하는데 필요한 기능들이 뭐가 있는가를 7개로 나누고 각각의 기능들을 쓸 때, 쓸 수 있는 규칙(프로토콜) 들을 분류해 놓은 녀석이라는 것만 알아두세요. 그리고 TCP/IP는 이 OSI보다는 사람들이 쓰기 쉬운 방향으로 변한 녀석이에요.
TCP/IP에는 직접 선이나 무선으로 연결하는 것을 관리하는 링크 계층과, 데이터가 목적지를 향해서 가는 방법을 관리하는 인터넷 계층과 데이터를 주고받는 것을 관리하는 전송 계층과 주고받은 데이터를 특정 프로그램과 연결하는 응용 계층이 있어요. 우리는 응용계층의 프로그램을 짜고 있고, 인터넷 연결은 선이나 와이파이로 하고, 데이터가 날아가는 방향은 보통 네트워크 장비(공유기 등)이 하는 일이에요. 따라서 Java는 전송 계층에 관한 일을 미리 구현해서 java.net에 넣어놨고, 우리는 그 클래스들을 쓰면 됩니다.
전송 계층을 위해 사용할 클래스는 Socket계열 또는 URL계열의 클래스와 Datagram계열의 클래스로 나뉩니다. 앞의 클래스는 데이터의 순서와 데이터가 확실하게 전송되었다고 확인할 수 있는 TCP라는 프로토콜을 사용할 때, 뒤의 클래스는 데이터를 확인하지 않고 전송해서 조금이라도 더 속도를 빨리 하겠다는 UDP라는 프로토콜을 사용할 때 씁니다. 우리는 채팅이나 게임 정보가 제대로 도착해야하므로, TCP를 사용하는 클래스를 쓸 겁니다. 그리고 인터넷 주소로 플레이어끼리 연결할 수 없기 때문에, URL계열의 클래스가 아닌 Socket계열의 클래스를 쓸 겁니다.
Socket은 프로그램의 네트워크 입구인 소켓을 구현해주는 클래스에요. 소켓을 구현할 때는 포트 번호라는 것을 써야 하는데, 이것은 예로 들어, 컴퓨터가 빌딩이고 프로그램이 한 방 일때, 포트번호는 방 번호입니다. 택배가 올 때는 보통 주소를 통해서 오죠. 이처럼 컴퓨터로 데이터가 올 때는 IP주소로 찾아오는데 이 다음에는 어느 프로그램이 이 데이터를 쓸 지 모르면 안되겠죠. 그럴 때 2바이트 길이의 포트번호를 사용해서 데이터를 쓸 프로그램을 찾아갑니다. 이 포트번호는 보통 앞의 약 1000번대까지는 컴퓨터가 네트워크에 기본적으로 사용하는 번호에요. 따라서 자기가 임의로 만든 프로그램들은 원래 쓰고 있는 포트 번호들과 충돌하지 않도록 10000~60000(포트 번호는 65535번 까지)번 사이 중 하나를 쓰는 게 좋습니다.
이 소켓을 구현할 때는 Java에서는 다음과 같이 합니다.
그 다음 Socket socketToClient = serverSocket.accept();를 하면, 이제 클라이언트로 부터 위의 포트 번호로 연결을 할 때 까지, 기다리게 됩니다.
클라이언트 에서는 Socket socketToServer = new Socket(serverIP, portNum);을 해서 서버와 연결을 시도합니다.(반드시 서버가 accept()를 실행하는 중에 해야함.)
서버의 serverSocket이 클라이언트와 연결을 성공하면 클라이언트와 이어주는 소켓을 socketToClient에 만들어줍니다.
이렇게 구현하면 서버와 클라이언트가 서로 연결이 됩니다.
만약 서로 데이터를 주고 받고 싶다면, Socket클래스에 있는 getOutputStream()과 getInputStream() 메소드를 이용해서 파일 입출력과 같이 Reader Writer를 만들어서 주고 받으면 됩니다.
그리고 소켓 또한 스트림처럼 다 쓰면 닫아줘야 하는데, 먼저 소켓에서 뽑아낸 스트림들을 모두 닫고( ex)bufferedReader.close();) 소켓을 닫아야 합니다. socket.close();만약 서로 데이터를 주고 받고 싶다면, Socket클래스에 있는 getOutputStream()과 getInputStream() 메소드를 이용해서 파일 입출력과 같이 Reader Writer를 만들어서 주고 받으면 됩니다.
3. Java Thread ¶
두 달만에 쓰는 쓰레드 이야기…
참고 자료는 주로 자바 튜토리얼을 썼습니다. 해석이 안되는 부분은 Power Java라는 책을 읽었고요.
정확히 말하면 Concurrency: 동시성에 관련된 이야기 인데, 개요부터 시작하자면, 여러분이 보통 사용하는 프로그램은, 예로 워드를 들면, 키보드와 마우스 입력을 받으면서, 맞춤법 체크도 하고, 정해진 시간마다 자동으로 저장도 해주는 등의 여러가지 일을 동시에 처리 하죠? 하지만 이 때까지 여러분이 짠 코드는 대부분 한 번에 한 가지 일밖에 못했습니다.
하지만 위의 워드와 같은 concurrent software를 만들기 위해 자바에서는 이와 관련된 API를 지원합니다. 앞으로 볼 Thread 클래스도 여기에 속하고, 나중에 더 배운다면 java.util.concurrent 패키지도 사용하겠죠? 하지만 여기선 Thread까지만 언급할 겁니다.(제가 여기까지만 배우고 던졌기 때문에…)는 아니고 concurrent 패키지는 멀티코어 프로그램을 지원하기 때문에 어렵더라고요… 구현할 것도 많아 보여서 스킵했습니다.
그럼 Thread 클래스를 쓰기 앞서 프로세스와 쓰레드에 대해 개념을 잡고 갑시다. 일단 컴퓨터 내에는 많은 쓰레드와 프로세스가 돌아가고 있습니다. 만약 여러분이 코어가 하나인, 그러니까 뇌가 하나 달린 CPU를 쓰더라도 이렇게 할 수 있습니다. 왜냐하면 이런 것들은 단위 시간을 프로세스만큼 쪼개서 이 프로세스 했다 저 프로세스 했다 하는 것, 즉 타임 슬라이싱을 사용해서 실행되고 있기 때문입니다. 여러분이 인지를 못하는 것은 CPU가 하나 하나 일을 하는 게 분신술처럼 빨라서 여러 개를 동시에 하는 것처럼 보이는 겁니다.
(이렇게 일하고 있는 거에요…)
아무튼 여기서 프로세스란 자기만의 메모리 영역을 가지고 있는 완전한 실행 환경입니다. 그리고 쓰레드는 프로세스 안에서 작동하는 작은 프로세스입니다. 프로세스는 쓰레드 하나는 반드시 있어야 하고, 이는 보통 우리가 잘 아는 main() 메소드의 내용이 됩니다. 쓰레드는 반드시 프로세스 안에서 프로세스의 자원을 서로 공유하면서 작동해야 합니다.
그럼 쓰레드를 구현해봅시다. 쓰레드 구현은 두 가지 방법으로 할 수 있습니다.
Runnable 인터페이스 구현
public class HelloRunnable implements Runnable {
Runnable 인터페이스 구현
public class HelloRunnable implements Runnable {
public void run() {
public static void main(String args[]) {
}System.out.println("Hello from a thread!");
}public static void main(String args[]) {
(new Thread(new HelloRunnable())).start();
}위와 같이 구현하면 Thread 객체를 하나 더 생성해서 위의 Runnable을 실행시켜줘야 된다는 단점이 있지만, 단일 상속밖에 안되는 Java에서 (매우 소중한) extends 칸을 쓰레드에게 넘겨주지 않아도 된다는 장점이 있습니다.
public void run() {
public static void main(String args[]) {
}System.out.println("Hello from a thread!");
}public static void main(String args[]) {
(new HelloThread()).start();
}아까와는 달리 Thread를 직접 상속 받아서 사용하는데, 이러면 자기.start()하면 실행된다는 직관적인 소스 코드를 작성할 수 있지만, 인터페이스로도 구현되는 쓰레드가 extends 옆을 꿰차고 있는 건 프로그램의 확장성에서 보면 안 좋은 코드입니다.
어쨋든 쓰레드가 할 일, 쓰레드의 main()은 public void run()의 몸체입니다. 여기에 실행할 내용을 적고, 쓰레드 객체를 생성해서 쓰레드.start()를 하면 run()이 실행됩니다.
(주의: run에 내용을 적고 start메소드로 실행해야 쓰레드가 동시에 작동합니다. run메소드를 실행하면 호출한 쓰레드가 호출된 쓰레드가 끝날 때까지 멈춰있습니다.)
(주의: run에 내용을 적고 start메소드로 실행해야 쓰레드가 동시에 작동합니다. run메소드를 실행하면 호출한 쓰레드가 호출된 쓰레드가 끝날 때까지 멈춰있습니다.)
참고로 튜토리얼에서는 Runnable로 쓰레드를 구현하는 것을 권장합니다. 확장하기 쉽다는 장점이 있기 때문입니다.
(쓰다가 피곤해진 상황… 뭐 API에 찾아보면 정확히 나오겠지...)
쓰는 메소드 중 튜토리얼에 나오는 거 몇 개
sleep(int ms): 현재 쓰레드를 밀리세컨드 동안 재워버림. 하지만 인터럽트를 받으면 InterruptedException이 발생함
interrupt(): 해당 쓰레드에 인터럽트를 발생시킴. isInterrupted()나 interrupted()라는 메소드로 현재 쓰레드에 인터럽트가 들어왔는지 확인할 수도 있지만, 복잡한 프로그램의 경우 InterruptedException을 try-catch해서 확인한다.
join(): 해당 쓰레드가 종료될 때까지, 현재 쓰레드를 재워버림.
(주의: sleep과 join은 시간 지정이 가능하지만, 이 시간은 프로세스 타임이기 때문에 실제 시간과 맞지 않을 수도 있다.)
이제 그럼 동기화에 대해서 알아봅시다.(초코 우유 먹고 와서 머리가 돌아감)
쓰는 메소드 중 튜토리얼에 나오는 거 몇 개
sleep(int ms): 현재 쓰레드를 밀리세컨드 동안 재워버림. 하지만 인터럽트를 받으면 InterruptedException이 발생함
interrupt(): 해당 쓰레드에 인터럽트를 발생시킴. isInterrupted()나 interrupted()라는 메소드로 현재 쓰레드에 인터럽트가 들어왔는지 확인할 수도 있지만, 복잡한 프로그램의 경우 InterruptedException을 try-catch해서 확인한다.
join(): 해당 쓰레드가 종료될 때까지, 현재 쓰레드를 재워버림.
(주의: sleep과 join은 시간 지정이 가능하지만, 이 시간은 프로세스 타임이기 때문에 실제 시간과 맞지 않을 수도 있다.)
이제 그럼 동기화에 대해서 알아봅시다.(초코 우유 먹고 와서 머리가 돌아감)
여러 개의 쓰레드는 같은 프로세스 공간을 공유할 수 있다고 했습니다. 이 때, 둘 이상의 쓰레드가 같은 객체의 값을 건드리면 어떻게 될까요? 하나는 0에다가 1을 더하고, 다른 하나는 0에다가 1을 뺄 때, 과연 이 객체의 값은 0이 될까요? 동기화를 하지 않았다면, 1이 될 수도 있고 -1이 될 수도 있습니다. 위의 예는 쓰레드가 가지는 전형적인 문제인데 이와 같은 문제들에 대해 조금 더 자세히 살펴보도록 하죠. 그리고 해결 방안인 동기화에 대해 알아봅시다.
쓰레드를 다룰 때는 대표적인 두 개의 문제를 조심해야 합니다. 하나는 쓰레드 간섭이고 다른 하나는 메모리 일관성 오류입니다.
쓰레드 간섭: 위에서 예를 들었던 것이 쓰레드 간섭입니다. 쓰레드 간섭은 두 개 이상의 쓰레드가 같은 값에 대해 변경했을 때, 한 쪽에서 변경한 값이 다른 한 쪽이 덮어 씌워서 원하는 값이 나오지 않을 때를 말합니다.
쓰레드 간섭: 위에서 예를 들었던 것이 쓰레드 간섭입니다. 쓰레드 간섭은 두 개 이상의 쓰레드가 같은 값에 대해 변경했을 때, 한 쪽에서 변경한 값이 다른 한 쪽이 덮어 씌워서 원하는 값이 나오지 않을 때를 말합니다.
위의 경우를 과정을 자세히 따라가면 먼저 A가 0을 받아서 1을 증가시키고, 다시 저장하는 과정과, B가 0을 받아서 1을 감소시키고, 다시 저장하는 과정이 있다고 합시다. 이를 타임 슬라이싱, 다시 말해 A가 일했다가 B가 일했다가를 반복한다면, 나올 수 있는 과정이 A가 0을 받고, B가 0을 받은 후, A가 1을 증가시키고, B가 1을 감소시켜서, A가 1을 저장했는데 B가 -1을 저장해서 원하던 0이 아닌 -1이 나오는 쓰레드 간섭이 발생합니다.
메모리 일관성 오류: 만약 여러분이 0인 값을 1로 바꾼 후 이 값을 받아내는 과정을 1로 바꾸는 쓰레드와 값을 받아내는 쓰레드로 확인하려고 한다면, 앞의 쓰레드가 먼저 일을 하고 뒤의 쓰레드가 일을 해야 정답이 나오겠죠? 하지만 타임 슬라이싱은 앞의 쓰레드가 먼저 할 수도 있고 뒤의 쓰레드가 먼저 할 수도 있는 예측 불가능 한 것이기 때문에 여러분은 메모리의 값을 0으로 볼 수도 있고, 1로 볼 수도 있습니다. 이러한 것을 메모리 일관성 오류라고 합니다.
사실 2번의 경우는 순서를 정해서 즉, Happen-Before Relationship을 정해서 해결할 수 있습니다. 앞에서 배웠던 join으로 기다리게 하든지, 뒤의 쓰레드를 앞의 쓰레드가 끝나면 start()를 하든지를 통해서도 가능하지만, 방법이 제한적입니다. 따라서 우리는 동기화를 사용할 줄 알아야 합니다.
사실 2번의 경우는 순서를 정해서 즉, Happen-Before Relationship을 정해서 해결할 수 있습니다. 앞에서 배웠던 join으로 기다리게 하든지, 뒤의 쓰레드를 앞의 쓰레드가 끝나면 start()를 하든지를 통해서도 가능하지만, 방법이 제한적입니다. 따라서 우리는 동기화를 사용할 줄 알아야 합니다.
동기화란 현재 쓰레드가 어떤 객체에 대해 작업을 하고 있을 때, 다른 쓰레드가 이 객체에 대해 일을 하지 못하게 하는 것을 말합니다. 아까처럼 1을 증가시키는 중에는 1을 감소시키는 행위를 막아버리는 것을 동기화라고 합니다. 방법은 synchronized라는 키워드를 사용해서 동기화를 할 수 있습니다.
이 키워드는 메소드 또는 메소드 내용 중 일부에 사용할 수 있습니다.
public class SynchronizedCounter {
이 것은 메소드 자체에 사용하는 예로, 이렇게 하면, 이 클래스의 synchronized 메소드를 사용하는 순간, 다른 쓰레드가 이 객체에 대한 작업을 시도하면, 현재 쓰레드가 작업을 끝낼 때까지 기다리게 됩니다. 현재 쓰레드는 이 때, lock(=monitor)이란 synchronized 객체를 사용할 권리를 갖고 있습니다. 만약 현재 쓰레드가 작업이 끝나면, 이 lock을 반환하고, 기다리고 있던 다른 쓰레드가 이 lock을 가지고 작업을 하게 됩니다.
public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
public synchronized void decrement() {
public synchronized int value() {
}public synchronized void increment() {
c++;
}public synchronized void decrement() {
c--;
}public synchronized int value() {
return c;
}이 것은 메소드 자체에 사용하는 예로, 이렇게 하면, 이 클래스의 synchronized 메소드를 사용하는 순간, 다른 쓰레드가 이 객체에 대한 작업을 시도하면, 현재 쓰레드가 작업을 끝낼 때까지 기다리게 됩니다. 현재 쓰레드는 이 때, lock(=monitor)이란 synchronized 객체를 사용할 권리를 갖고 있습니다. 만약 현재 쓰레드가 작업이 끝나면, 이 lock을 반환하고, 기다리고 있던 다른 쓰레드가 이 lock을 가지고 작업을 하게 됩니다.
public void addName(String name) {
다음은 synchronized를 메소드의 특정 부분만 사용하는 예입니다. 위와 같이 synchronized(lock을 줄 객체){}라고 적어야 합니다. 여기서는 this, 객체 자신이 addName()의 lastName = name;을 먼저 사용하는 쓰레드에게 lock을 넘겨주게 됩니다. 이렇게도 쓸 수 있는 이유는 메소드의 일부분만 객체에 접근하는데, 메소드 전체를 동기화해버리면, 메소드가 시간이 오래 걸리는 메소드인 경우, 다른 쓰레드는 “잠깐만 쓸 꺼면서 오래도 쥐고 있네” 거리면서 기다리다 홧병날겁니다. 그럼 프로그램이 느려지죠. 이 때문에 부분적인 동기화 구문이 사용되는 겁니다.
synchronized(this) {
nameList.add(name);
}lastName = name;
nameCount++;
}nameCount++;
nameList.add(name);
다음은 synchronized를 메소드의 특정 부분만 사용하는 예입니다. 위와 같이 synchronized(lock을 줄 객체){}라고 적어야 합니다. 여기서는 this, 객체 자신이 addName()의 lastName = name;을 먼저 사용하는 쓰레드에게 lock을 넘겨주게 됩니다. 이렇게도 쓸 수 있는 이유는 메소드의 일부분만 객체에 접근하는데, 메소드 전체를 동기화해버리면, 메소드가 시간이 오래 걸리는 메소드인 경우, 다른 쓰레드는 “잠깐만 쓸 꺼면서 오래도 쥐고 있네” 거리면서 기다리다 홧병날겁니다. 그럼 프로그램이 느려지죠. 이 때문에 부분적인 동기화 구문이 사용되는 겁니다.
동기화는 쓰레드의 문제를 막아주지만 정작 자기 자신도 남용하면 문제가 생깁니다. 대표적으로 Deadlock, Livelock, Starvation이 있습니다.
Deadlock: 쓰레드끼리 서로 동기화 메소드가 끝나지 않아서 쓰레드들이 멈춘 경우를 말합니다. 예로 설명하죠. 두 사람이 있습니다. 서로 인사를 하려는데 너무 예절교육을 잘 받아서 반대쪽 사람이 고개를 들지 않으면 자신도 고개를 들지 않습니다. 이제 둘이 만나서 인사를 한다면, 둘이 고개는 숙였는데 이제 상대보고 고개를 들라고 해야하는데, 서로보고 상대방이 고개를 들어야지 내가 들겠다는 소리를 하면서 고개를 계속 숙이고 있는 겁니다. 이를 프로그램으로 설명하면, 둘이 고개를 숙였으니 서로 자신의 객체를 사용중입니다. 둘 다 자기 객체의 lock을 가지고 있죠? 이제 상대방의 고개를 들게 하려고 상대방의 메소드를 사용하면, lock을 이미 상대방이 가지고 있기 때문에, lock을 받을 때까지 기다립니다. 하지만 상대방 또한 자신의 고개 드는 메소드 때문에 lock을 받을 때까지 기다리므로 두 쓰레드가 멈추게 됩니다.
public class Deadlock {
static class Friend {
public static void main(String[] args) {
}private final String name;
public Friend(String name) {
public String getName() {
public synchronized void bow(Friend bower) {
public synchronized void bowBack(Friend bower) {
}public Friend(String name) {
this.name = name;
}public String getName() {
return this.name;
}public synchronized void bow(Friend bower) {
System.out.format("%s: %s"
}+ " has bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);this.name, bower.getName());
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s"
}+ " has bowed back to me!%n",
this.name, bower.getName());
this.name, bower.getName());
public static void main(String[] args) {
final Friend alphonse =
new Thread(new Runnable() {
}new Friend("Alphonse");
final Friend gaston =new Friend("Gaston");
new Thread(new Runnable() {public void run() { alphonse.bow(gaston); }
}).start();new Thread(new Runnable() {
public void run() { gaston.bow(alphonse); }
}).start();Livelock: Deadlock과 상당히 유사하지만 다른 경우입니다. Livelock은 오히려 상대 쓰레드의 응답에 반응하는 쓰레드끼리 서로의 응답을 영원히 기다리고 있는 상태입니다. Deadlock과 다른 점은 Livelock은 말 그대로 응답을 계속 기다리기 때문에 죽은 쓰레드가 아니라는 점입니다. 위의 문제를 살짝 바꿔서 상대방보고 고개를 들라고 시키는 것이 아닌 상대방이 고개를 들기를 기다렸다가 고개를 드는 코드로 바꾼다면 livelock인 것입니다.
Starvation: 이름 그대로 한 쓰레드가 특정 객체를 오랫동안 붙잡고 있어서 다른 쓰레드들이 그 객체를 쓰지 못해 배고파 죽은 경우입니다. 실제론 너무 독점상태로 있어서 다른 쓰레드들이 일을 못하고 대기상태라 프로그램이 제대로 진행되고 있지 못하는 경우를 뜻합니다.
위의 대표적인 동기화의 세 문제 때문에 동기화를 쓸 때, 조심을 해야 합니다. 위의 세 문제들이 위험한 이유는 컴파일 에러가 아닌 런타임 에러기 때문에, 즉, 에러 메세지도 없고, 빨간 줄도 안 뜨는 문제라서 그렇습니다.
그럼 잠깐 동기화 문제 때문에 머리가 아팠으니 이를 피하는 예외를 몇 개 알고 마지막으로 lock을 임의로 건네주는 방법을 알고 쓰레드 이야기를 끝내도록 할게요.
동기화는 쓰레드들이 한 객체를 써서 생기는 문제는 막는 것인데, 이 문제는 사실 객체를 쓰는 과정이 보통 정도 길이라도 타임 슬라이싱이 가능하기 때문에, 그 사이로 다른 쓰레드의 일을 집어 넣을 수 있어서 생기는 문제입니다. 그리고, 결국 객체를 바꾸려고 하기 때문에 생기는 문제지요. 따라서, 이 둘이 안 되는 경우는 동기화를 쓰지 않아도 됩니다.
객체에 접근하는 과정이 너무 짧은 경우: int나 byte같은 작은 자료를 읽고 쓰는 행위는 컴퓨터에게도 정말 짧게 걸립니다. 이런 경우에는 쓰레드 간섭이 발생할 수 없기 때문에 굳이 동기화를 하지 않아도 됩니다. 단, 이 경우, 메모리 일관성 오류를 제거하기 위해선 volatile 변수를 사용해야 합니다. volatile이 선언된 변수는 무조건 값을 쓰는 행위를 읽는 행위 전에 하는 것을 원칙으로 하기 때문에, 자동으로 happen-before relationship이 정해져 있고, 그렇기 때문에 읽을 때, 변경된 값을 볼 수 있습니다.
객체 자체가 수정이 불가능한 경우: final 객체의 경우는 수정이 불가능하기 때문에 굳이 잘못된 값을 읽을 것이라는 걱정이 생길 일이 없습니다.
객체 자체가 수정이 불가능한 경우: final 객체의 경우는 수정이 불가능하기 때문에 굳이 잘못된 값을 읽을 것이라는 걱정이 생길 일이 없습니다.
이제 마지막으로 객체의 lock을 임의로 기다리고 전달할 수 있는 메소드 두 개를 알아보고 마치겠습니다. Object 클래스의 wait()와 notifyAll()입니다.
wait(): 현재 lock을 가지고 있는 쓰레드가 실행 중 이 메소드를 호출하면, lock을 반환하고 자신이 lock 대기열로 갑니다.(반드시 lock을 소지한 쓰레드가 이 메소드를 호출해야 함)
notifyAll(): 자신의 lock을 반환하면서 기다리고 있던 쓰레드 중 대기순위 첫번째를 깨운다. 일어난 쓰레드는 자신이 wait()했던 시점부터 lock을 가지고 실행한다.
notifyAll(): 자신의 lock을 반환하면서 기다리고 있던 쓰레드 중 대기순위 첫번째를 깨운다. 일어난 쓰레드는 자신이 wait()했던 시점부터 lock을 가지고 실행한다.
이 두 메소드를 쓰는 경우는 보통 Producer-Consumer 방법에서 사용합니다. 한 객체를 택배상자처럼 한 쓰레드가 객체에 값을 넣는 생산자, 다른 쓰레드는 객체에 값이 들어있으면 그 값을 뜯어서 사용하는 소비자처럼 행동하는 방법이 Producer-Consumer = 생산자-소비자 방법입니다. 이 때, 소비자는 생산자가 값을 넣을 때까지 무한루프를 돌리는 것은 프로그램 성능에 있어서 매우 좋지 않은 방법입니다. 이 대신, 소비자는 값을 꺼낼 때, 먼저 객체 안에 아무것도 없으면 wait()를 한 다음 생산자가 값을 넣고, notifyAll()을 할 때까지 기다리도록 하면, 기다리는 동안, 프로그램은 이 쓰레드에 대한 일을 하지 않아도 되고, 쓰레드끼리는 정상적인 Happen-Before Relationship을 만들 수 있습니다.
이제 쓰레드에 대한 내용이 끝났습니다. 사실 영어 읽기 싫어서 제 머리 속에 있던 예전 지식을 많이 끌어다 써서 정확한 설명인지는 모릅니다. 그러니 그냥 구글신을 믿으세요…
인테그레이션 테스트 우왕