336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
출처 : http://dreamgoer.net/200

이 글은 REST에 대해 정리하는 두편의 글중 두번째 글입니다. 따라서 1부("REST 알아보기 - 1부, 연동의 역사", 클릭)를 읽지 않으신 분은 먼저 1부부터 보시고 본 글을 읽어주시길 바랍니다. 1부 글은 왠만하면 반복하지 않을테니까 말입니다. 


[1부를 아주 간단히 요약해버리자면]

REST도 네트웍에 물려있는 서비스 자원들간의 연동이라는 이슈에서 나온 것으로써, 1부에서 네트웍에서 연동의 역사를 한번 나름대로 짚어보았습니다. RPC - CORBA - RMI - SOAP - REST에 이르기까지, 그 기술들이 진화하며 해결한 부분들을 파악했습니다. 

REST는 "Representaional State Transfer"의 약자로써, 웹프로토콜(HTTP)을 활용하여, Resource 중심으로 연동 인터페이스를 정의하고 사용하는 방법을 제안한 것입니다. 한마디로 웹(HTTP) 프로토콜을 최대한 그대로 사용하여, Open API를 만드는 스타일가이드라고 할 수 있습니다. 

그러나, 이런 개념적인 얘기로는 개발자가 아니신 분들이라면 잘 이해가 가지 않을 것입니다. 그래서 오늘은 좀 더 명확하게 이해할 수 있도록 REST가 좋은 이유를 구체적으로 설명하도록 하겠습니다. 
 


[REST는 뭐가 그리 좋은가?]

1. 웹친화적이다.
이미 웹 친화적이라는 말씀은 서두에서 드렸으므로, 더이상 설명할 필요는 없을 것 같습니다. 요새는 방화벽을 넘어서 연동해야 하는 경우가 허다합니다. 그럴때 SOAP이나 REST를 제외한 다른 연동프로토콜들을 대부분 방화벽에서 문제를 야기합니다. 
(출처: http://olivecentre.org.uk)


2. 웹서버로 해결이 가능하다
REST는 HTTP 동사를 그대로 사용합니다. 앞서서 얘기했듯이 HTTP request, response 전부 재활용합니다. 심지어 Error 상태 code까지도 말입니다. 게다가 결과를 주고받는 것도 웹브라우저에서 지원되는 XML 또는 JSON입니다. (JSON은 자바스크립트에서 XML보다 데이타를 쉽게 전달하는 방법이지요) 따라서 대부분의 서버프로그램을 제외하면 REST를 운용할 수 있는환경이 웹서버하나로 해결되는 셈입니다. (참고로 요새 공유기, IP전화기, 각종 셋탑 등 모든 IP장비는 웹을 통해 설정을 할 수 있게 되어 있습니다. 그 얘기는 그런 장비들에 웹서버들이 모두 들어간다는 것입니다.)
(출처: http://www.symbian-freeware.com)

3. 캐슁(Caching)도 된다
REST 방식을 제대로 적용한다면, 즉 ReSTful하게 잘 만든다면, 웹에서 캐쉬기능도 그대로 활용할 수 있습니다. 아시다시피, 웹에서는 동일한 내용을 계속조회할때 중간노드에서 프락시를 사용하여 트래픽을 줄이기도 합니다. (IE 브라우저 옵션에 보시면 HTTP-Proxy 설정부분이 있는데 바로 그 부분을 말합니다) 그런데, 웹서비스도 알고보면 업데이트보다는 조회가 훨씬 많다는 것이고, 그 조회의 대답은 앞단에서 Proxy가 캐쉬한 값을 전달해서 처리할 수도 있다는 얘기입니다. 잘 사용하면 서버 부하, Response time을 확 줄일 수 있겠죠. 
(출처: http://knowledgehub.zeus.com/media/caching_01.png)


4. 개발자에게 상당히 직관적이다. 
이것도 앞에서 말씀드렸던 내용인데, HTTP 동사를 그대로 사용하고, 예외코드도 그대로 사용하므로, 개발자 관점에서는 상당히 익숙하고 직관적으로 이해할 수 있습니다. 즉 REST로 OpenAPI를 제공하면, 매개변수와 URI에 집중하면 되고, 함수를 실제 호출하고 사용하는 방식에 있어서, 많은 부분 RESTful하다는 얘기로 설명이 된다는 부분입니다. 
(출처: http://4.bp.blogspot.com)


5. 컨텐츠 협상(Negotiation)이 가능하다. 
아니 프로그램에서 웬 협상하실 겁니다. 그런데, Open API는 클라이언트 서버처럼 Tight couple된 관계가 아니라서, 어떤 놈이 HTTP-request를 날릴지 모르는 것이고, 그 놈이 서버에서 제공해주는 HTTP-response를 다 알아 먹을 수 있는지도 알 수 없다는 점입니다. 즉, Response가 기본 한글인데, 함수를 호출한 놈은 영문만 받아들 일 수 있다면, HTTP에서는 컨텐츠 협상을 통해, 이 요구에 대해서는 좀 다르게 응답을 할 수도 있는 것이지요. (아마 HTTP request를 할때, 본문에서 Accept-Language header를 활용하여 이런 협상을 하게되겠죠)
(출처: http://www.isoc.org/inet96)

일반적으로 서버 프로그램하면서 이런 것까지 다 고려해서, 확장성을 확보하기는 사실 어렵습니다. 기껏 한다고 해봐야, if절이나 예외처리(Try catch) 몇개로 해결할 뿐이죠. 그런데 HTTP는 언어나 미디어타입(MIME)까지 기본적으로 협상할 수 있게 왁꾸가 되어 있는 것입니다. 제게는 이런 Feature가 참 아름다운데, 그렇지 않으신지요 ^^; 
(출처: http://www.bbc.co.uk)


6. 웹 기술은 다 써먹을 수 있다. 
데이타를 주고받는데 가장 확장성있는 방법은 누구나 인정하듯 XML입니다. 그래서 웹에서 XML은 엄청 널리 쓰이고 있습니다. REST에서는 결과값을 전달하는 데 있어서 복잡한 내용도, 구조적인 결과도 XML로 처리할 수 있는 것입니다. 
(출처: http://esto.nasa.gov)

아울러 웹은 HTTP-response로 전달될 결과값이, 그리 복잡한 구조도 아니고, 아주 간편한 처리를 할 수 있도록 할 경우 JSON을 쓰기도 합니다. (JSON은 JavaScript에서 손쉽게 구조적인 값을 주고받는 방식이지요) REST에서도 이 방식도 그대로 사용할 수 있습니다. (JSON을 쓸경우, HTTP request의 헤더중에 "Accept: application/json"이 들어가게 되겠지요)

결국 REST에서는 널리 사용되는 웹기술(HTTP + URI + XHTML + XML + JSON ...)을 그대로 받아들여 OpenAPI를 아주 실용적인 관점에서 접근하게 해주는 셈입니다. 


[끝으로 한마디]

제가 설명한 것외에도 HTTP 프로토콜을 사용함으로 얻는 이점은 더 많이 있을 것입니다. HTTP를 아는 사람이라면 새로 배울 필요가 없다, 개발툴, 검증/테스트가 쉽다 등등 말입니다. 그래서 REST는 OpenAPI로 각광받는 것이며, 아울러 계속해서 발전 진화하고 있습니다. WOA(Web oriented architecture)나 WOT(Web of things)같은 게 그발전, 활용도의 극대화 예라고도 할 수 있겠습니다. 

아무쪼록 웹하시는 분들, 매쉬업 아이템을 기획하시는 분들은 REST의 기본개념은 알아두시기 바랍니다. 


'ⓦeb ⓢtory > web service' 카테고리의 다른 글

Serenity  (0) 2018.07.24
[스크랩] REST 알아보기 - 1부, 연동의 역사  (0) 2011.08.30
블로그 이미지

뚱땡이 우주인

,
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
원본 : http://dreamgoer.net/199

2000년대 후반부터 웹에서 OPEN API하면 REST가 항상 언급됩니다. 그리고 제가 WOT(Web of things) 소개하면서 쓴 글("왜 WOT인가? - 1부", 클릭)에도, REST가 핵심이라고 했습니다. 그래서 오늘은 웹기반 Open API의 표준 연동방식처럼 회자되는 REST라는 놈에 대해 정리를 해보고자 합니다. 

우선 REST는 무엇일까요? REST(Representaional State Transfer의 약자)는 웹프로토콜(HTTP)을 활용하여, Resource 중심으로 연동 인터페이스 구조를 제안한 것입니다. 그런데 리소스 중심이라는 얘기나, HTTP라는 얘기 모두 감이 잘 오지 않을 것입니다. 일단은 웹서비스를 Open API로 연동하는 데 있어서, 어떻게 HTTP를 사용하면 좋다는 스타일 가이드라고만 해두고 REST가 나오기까지의 히스토리를 좀 짚어보겠습니다. 왜냐하면 웹세상 이전으로 돌아가서, 고전적인 시스템(클라이언트-서버)간 연동 문제가 어떻게 해결되어 왔는지 그 실타래를 따라가다보면 REST가 어떻게 Open API의 강자가 되었는지를 저절로 이해하게 되기 때문입니다. 



[프로그램간 연동 - 고민의 시작]

개발의 기본은 무엇인가요? 라이브러리화(쉽게 말해 함수화하는 것)입니다. 그렇게 함으로써 소스코드를 쉽게 재활용할 수 있게 하고, 특히 라이브러리를 공유함으로 나혼자가 아니라, 다른 사람들도 이어서 개발을 할 수 있는 기반이 됩니다. 그런데, 이렇게 하는 것도 하나의 컴퓨터에서나 가능할 뿐, 네트웍을 넘어갈 순 없었습니다. 즉 A컴퓨터에서 B의 컴퓨터에 있는 라이브러리를 호출할 수는 없다는 것이었습니다. 

그래서 B의 컴에 네트웍을 통해 호출받을 수 있도록 좀 작업을 하기 시작합니다. 그게 소위 RPC 데몬이라는 놈인데, Remote Procedure Call의 약자로 주로 유닉스에서 사용되었었습니다. 하지만 이놈은 보안에도 문제가 좀 있었고, 네트웍 자원 활용이나 함수를 동적으로 사용하는데 있어서도 제약이 있었습니다. (프로그래밍 안해보신 분들은 아래그림보고 그냥 이런게 있구나 하고 넘어가시면 되겠습니다)

(출처: http://www2.cs.uregina.ca)

그래서, 이 문제를 해결하는 김에, 아예 프로그램언어, OS와도 무관하게 해결해보자는 시도가 있게됩니다. 바로 그게 CORBA였습니다. CORBA는 A컴에 있는 C++ 프로그램이, B컴에 있는 자바 서버의 함수도 호출할 수 있게 한다는 뭐 그런 취지였습니다. 원리는 대강 이렇습니다. IIOP라는 프로토콜을 통해서, 프로그램 언어에 상관없이 인터페이스를 정의(IDL)하고, 그걸 기반으로 각 언어에서 CORBA 플랫폼을 통해 개발하면 C++로 짜놓은 함수도, 자바에서 호출할 수 있게 되는 것이었습니다. 

(출처: http://img.zdnet.com)

그런데 CORBA는 무겁고 비쌌습니다. (참고로 많이는 아니지만, Iona의 Orbix와 볼랜드의 Visibroker라는 대표적인 Corba 플랫폼을 다 만져볼 기회가 있었습니다.) 클라이언트는 무료였지만, 서버쪽 CORBA 플랫폼은 2000년 초반가격으로도 1억이 넘었습니다. 게다가 연동오버헤드도 무거운 편이어서, 개발해보면 서버쪽 HW도 어느정도 빠방해야 했지요. 결국 프로그램간 연동을 쉽고 경제적으로 하기 위해 나온 놈이 비싸고 쉽지 않았던 겁니다. 

그래서 Java 진영에서는 RMI (RPC의 자바버전)를 소개합니다. 제가 NMS 개발하면서 사용해보았는데, 쉽고 아주 편했습니다. 하지만 이 놈도 사내 네트웍에서는 연동이 쉬웠지만 방화벽에서는 늘 상 걸리곤 했습니다. 방화벽은 기본적으로 HTTP만 열어주지요. 





[HTTP - 웹서비스 화두를 열다]

그래서 HTTP로 무엇인가 연동하는 방안을 고민했었고, 그게 바로 XML과 HTTP를 결합한 SOAP기반의 웹서비스였습니다. 일단 SOAP은 HTTP를 활용할 수 있므로, 방화벽문제는 어느정도 해결했습니다. 그런데 문제는 여전히 좀 어려웠다는 겁니다. SOAP기반 웹서비스는 근본적으로 서비스지향 구조(Service-Oriented Architecture)로써, SOA라고 불리는데요. 서비스를 자유자재로 정의할 수 있다는 게 매력인 동시에 그만큼 복잡해질 수도 있다는 게 문제였습니다. 

즉 서비스를 정의하는 것 자체도, WSDL이란 놈을 만들어 함수명부터 시작해서 매개변수, 결과값까지 세부적인 사항 하나하나를 정의합니다. 그리고 이를 UDDI(인터넷에서 DNS같은 거라고 이해하시면 됩니다)라는 놈에 등록해서 외부에서 발견할 수 있게 하는 구조였습니다.

(출처: http://nuwanbando.com)

그러다가, 서비스지향으로 바라보기 보다는 리소스기반(Resource-Oriented Architecture: ROA)으로 정의하면 어떻겠느냐는 아이디어가 나옵니다. 잘 아시듯, 웹이라는 놈 자체가 리소스기반입니다. 웹에서는 모든 문서, 객체(URI를 통해 가져올 페이지를 표시하지요)를 대상으로 해서, HTTP Get하는 게 기본 조작입니다. 그래서 자원관점으로 보면, 웹에서 사용하는 HTTP Primitive (GET, PUT, POST, DELETE)를 그대로 쓸 수 있지 않겠냐는 생각을 하게 된거죠. 바로 그게 REST인겁니다. 

결국 REST는 SOAP기반의 웹서비스인 SOA(Service-Oriented Architecture)와 대비되는 자원기반의 ROA(Resource-oriented Architecture) 연동방식인 것입니다. 참고로 ROA는 더 확장해서 WOA 즉 Web Oriented Architecture로까지 발전됩니다. 아래 그림은 ROA의 대표격인 REST를 포함해서, 이를 더 넓게 그린 WOA를 보여줍니다. 


참고로 저는 SOAP시대까지만 프로그램에 손을 대었는지라, REST로 직접 개발을 해본 경험은 없습니다. 대신 REST 책은 한권 읽어보았고, 그걸 기반으로 아는체 하고 있는 겁니다. 따라서 REST 개발자분들께서 댓글을 달아주시면 저와 읽는 분들 모두 다 행복해지겠지요.



[ROA - 생각해보면 그럴싸하다]

SOA에서는 WSDL에 호출할 함수명을 마음대로 정의할 수 있습니다. 즉 id로 이름을 가져오는 웹서비스를 만든다고 할때, getName, retrieveName, findName 모두 사용할 수 있습니다. 그런데 과연 이런 자유도가 중요할까요? 알고보면 이름은 거기서 다 거기인 셈입니다. 

REST는 모든 것을 리소스기반으로 보기때문에, 위의 함수(getName, retrieveName, findName)를 만드는데 사용하는 함수명은 HTTP의 Get임이 자명합니다. 그리고 특정 ID로 이름을 입력하려면 HTTP POST, 갱신하려면 HTTP PUT, 삭제하려면 HTTP Delete를 사용하면 될 것입니다. 바로 이 부분에서만 해도, REST는 개발상 굉장히 직관적인 셈입니다. 

리소스 관점으로 보는 대표적인 예가 DB입니다. DB안에 있는 것은 모두 리소스입니다. 그러면 DB를 조작하는 데 사용하는 SQL의 Primitive는 무엇이 있습니까? 아래 그림 가운데 있는 insert, select, update, delete입니다. 


위에 보시는 것처럼, 각각은 알고보면 HTTP의 기본동사(Primitive)와도 1:1 대응이 됩니다. 그리고 프로그램할때, 특정 변수에 대해 생각해볼 기본조작 함수인 CRUD(Create, Read, Update, Delete)도 알고보면 HTTP 기본동사(Primitive)에 대응이 되는 셈입니다. 즉 REST는 나름 우리가 표준적으로 사용하는 프로그래밍 패러다임과 쉽게 매핑이 된다는 얘기입니다. 

※ SDK를 보신 분들이라면, 메쏘드 이름을 떠올려보세요. 객체지향 프로그램을 하는 분들은 다 공감하실 것이, 객체를 생성하거나, 삭제하는 것을 제외하면, 대부분 함수는 Getter와 Setter로 구성된다는 겁니다. 즉 GetXXX, SetXXX식의 함수가 되버리죠. 물론 Go, run, doIt 같은 함수들도 돌리지만, 그게 알고보면 Create, Delete, GetXXX, SetXXX로 구성될 수 있는 것들입니다. 



[그러면 매개변수와 결과값은 어떻게 전달해주나요?]

아마 누군가가 벌써 질문하고 있을 겁니다. 기존프로그램에서는 함수라는 게 알고보면 함수명만 있는 게 아니고 매개변수고 있고, 결과값도 있는데 그게 어떻게 처리되냐고 말입니다. 일반적인 함수의 형식은 이렇지요. 
  • NameType GetName(String id);

위에서 말한 것처럼, GetName에 해당하는 부분은 HTTP의 GET인 셈입니다. 그리고 Name이라는 부분은 URI를 통해서 이뤄지는데, 아마도 http://abc.com/employee/name?id="ooo" 같은 식으로 HTTP-Request의 GET에서 충분히 표현이 될 것입니다. 그리고 그 결과값(위에서는 NameType)은 형식만 정의하면(보통 XML이나 JSON으로 정의합니다) HTTP response로도 정리가 될 것이구요. 

특히 HTTP는 RESPONSE W3C에서 표준 코드로 인터넷에서 발생할 수 있는 상황을 모두 고려하여 정리되어 있습니다. 즉 결과값에서 200OK뿐만 아니라, 다음과 같은 상태 코드들이 나올 수 있고, 그에 대한 대응도 대부분의 브라우저는 다 알고 있습니다. 다시말해 웹개발을 해본 사람들은 누구나 쉽게 이해할 수 있으며, 이에 맞게 대응할 수 있다는 얘기입니다. (아래는 Status code 일부만 표시해본 것입니다)
  • 301 Moved permanently
  • 302 Found
  • 303 See Other
  • 403 Forbidden
  • 404 Not Found
상기의 HTTP-response 코드는 단순해보이지만, Loosely coupled 된 웹을 지켜주는 오랜 경험의 산물입니다. 실상 이 코드에 따라 IE나 파폭, 크롬, 사파리같은 웹브라우저는 알지도 못하는 아프리카의 한 웹사이트에서 오는 가끔 에러를 발생시키는 검증안된 웹 response에도 걸맞게 대응할 수 있는 것입니다. 

REST에서는 힘하나 안들이고, 상기의 HTTP 프로토콜에서 제공해주는 메커니즘을 그대로 활용하고 있는 셈입니다.

※ 물론 연동방식을 결정하실때 REST로 하자고만 하면 다 해결되는 것은 아닙니다. REST는 RESTful approach, 즉 하나의 방식이라는 표현을 사용하고 있는데 그 이유는 REST가 HTTP Primitive + URI를 사용하여, HTTP-request를 날리고, HTTP-response를 통해 응답결과를 주고받으라는 방식을 지정했을 뿐, 실제 결과 데이타의 표현형식은 개발자가 알아서 정해야 하기 때문입니다.(보통 XML이나 JSON으로 정합니다) 이 점에서 REST는 나름 꽤 열려있지만, 그만큼 REST 이름만 빌릴 뿐 RESTful하지 않은 OpenAPI가 마구 나올 수 있다는 얘기가 됩니다.



[정리하자면...]

REST는 네트웍상에서 원격에 있는 서버의 서비스(API)를 사용할 수 있게, 그것도 방화벽이라는 제약을 넘어서 가능하게 하자는 취지에서 나왔습니다. 이런 시도는 서비스지향의 SOAP기반의 웹서비스도 있었으나 REST는 좀더 실용적인 접근을 했고, 그 실용성, 이해하기 쉬움, HTTP의 Verb, request, response를 그대로 사용한다는 특징덕에 현재는 가장 각광받는 Open API 방식이 되었습니다. 

2부에서는 상기 이유외에도, 아니 상기 이유를 좀 더 구체적으로 명기하여 왜 REST가 OpenAPI의 대명사가 되었는지를 설명하도록 하겠습니다. 

'ⓦeb ⓢtory > web service' 카테고리의 다른 글

Serenity  (0) 2018.07.24
[스크랩] REST 알아보기 - 2부, 웹에서 뭐가 그리도 좋을까?  (0) 2011.08.30
블로그 이미지

뚱땡이 우주인

,
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
출처 : http://javafreak.tistory.com/232

자바 언어가 1.x 에서 2.x 대를 넘나들 시절에 thread 를 다룰때 뻔질나게 자주 쓰였던 thread 메소드가 resume, suspend , stop 인데 아쉽게도 deprecated (앞으로 쓰지 말라는 뜻) 되어서 별 수 없이 쓰레드의 상태를 관리하는 방식으로 구현을 해야 한다.

구현은 아래와 같은 간단한 코드에서 시작한다.
public class ThreadHandle implements Runnable {
    @Override    public void run() {
        // TODO Auto-generated method stub
    }
}
Runnable을 구현한 것을 볼 수 있는데, 꼭 저렇게 할 필요는 없으나 Runnable을 implement 해주지 않으면 별도의 구현체를 클래스로 정의해야하기 때문에 쓰레드를 이용해서 단위 작업을 수행하는 ThreadHandle과 같은 클래스가 자체적으로 Runnable을 구현하면 편리하다.

이제 저 상태에서 start, resume, suspend, stop 메소드를 정의해준다.
public class ThreadHandle implements Runnable {
    public void start(){
        ;
    }
    public void resume() {
        ;
    }
    public void suspend() {
        ;
    }
    public void stop() {
        ;
    }
    public void join() {
        ;
    }
    @Override
    public void run() {
        // TODO Auto-generated method stub
    }
}
ThreadHandle을 사용하는 쪽에서는 아래와같이 초기화하는데 쓰레드를 사용하는 것과 별 차이가 없어보이게 된다.
    public void test_thread_handle() throws Exception {
        ThreadHandle threadHandle = new ThreadHandle();
        threadHandle.start();
    }
그리고 start() 메소드가 제대로 동작하도록 하려면 아래와 같이 ThreadHandle이 thread를 생성하고 실행하게 만들어준다.
    private Thread thisThread ;
    private String threadName ;
    public ThreadHandle(){}
    public ThreadHandle(String threadName){
        this.threadName = threadName;
    }
    public void start(){
        thisThread = new Thread(this);
        if ( threadName != null) thisThread.setName(threadName);
        thisThread.start();
    }
ThreadHandle.start() 안에서 쓰레드 인스턴스를 생성하고 start()를 호출함으로써 새로운 쓰레드가 실행된다. threadHandle.start()를 호출이 <<main>> 쓰레드에서 실행되었다면 아래와 같이 새로운 쓰레드가 분기하게 된다.

두 개의 쓰레드가 함께 실행된다.

"t-0"라고 이름을 붙인 새로운 쓰레드는 run() 메소드를 시작으로 작업을 수행하는데 쓰레드에 일시정지, 재시작, 종료 기능을 추가하기 위해서 run() 메소드 안에서 반복문을 집어넣는다.
public class ThreadHandle implements Runnable {
    ....
    @Override
    public void run() {
        // TODO Auto-generated method stub
        while ( true ){
            processTask();
        }
    }
}
반복문에 별다른 종료 조건을 주지 않았기 때문에 위와같은 상태에서는 정신없이 작업이 계속될 것이다. while 안에 적절한 종료 조건을 넣어서 적절한 시점에 쓰레드가 종료하게 해야 한다.

종료 조건은 두가지로 나눌 수 있다.

1. run() 메소드의 반복문 안에서 더이상 작업을 계속할 필요가 없어서 끝내는 경우
2. 외부에서 ThreadHandler.stop() 메소드를 호출했을 때.

1번은 쓰레드가 스스로 종료 조건을 판단하는 것이고 2번은 외부에서 강제로 종료시키는 경우에 해당한다.

1번의 전형적인 예로 소켓 연결을 통해서 데이터를 주고받은 뒤 연결을 끊는 경우를 들 수 있다.
    public void run() {
        while ( true ) {
            int nRead = in.read(buf, 0, buf.length);
            if ( nRead < 0 ) break;
            ....
        }
    }
더이상 읽을 데이터가 없다면 while 문을 끝내고 run() 메소드가 종료하면서 쓰레드도 생을 마감하게 된다.

2번처럼 외부에서 강제로 종료시키는 경우(정확하게 말하면 종료하라고 신호만 보낸다. 자바 스레드에서는 한 스레드가 다른 스레드를 강제로 종료시킬 방법이 없다.)가 바로 여기서 구현할 내용인데 다음과 같이 thread의 상태를 나타내는 static 변수를 정의하고 이를 참조하는 stateCode 프로퍼티를 도입해서 상태를 관리한다.
public class ThreadHandle implements Runnable {
    final private static int STATE_INIT = 0x1;
    final private static int STATE_STARTED = 0x1 << 1;
    final private static int STATE_SUSPENDED = 0x1 << 2;
    final private static int STATE_STOPPED = 0x1 << 3;

    private int stateCode = STATE_INIT;
    public void start(){
        if ( stateCode != STATE_INIT) // INIT 상태가 아니면 이미 실행되었다.
            throw new IllegalStateException("already started");
       
        thisThread = new Thread(this);
        if ( threadName != null) thisThread.setName(threadName);
        thisThread.start();
        stateCode = STATE_STARTED;
    }
    ....
    public void stop() {
        if ( stateCode == STATE_STOPPED) // 이미 멈췄다면 또 호출할 필요가 없음.
            throw new IllegalStateException("already stopped");
        thisThread.interrupt();
        stateCode = STATE_STOPPED ;
    }
    ....
}
STATE_INIT은 쓰레드 인스턴스가 생성되었지만 실행되지는 않은 상태를 의미하고, STATE_STARTED 는 Thread의 start() 메소드가 호출되어서 실행된 상태를 의미한다. 마찬가지로 STATE_STOPPED 는 쓰레드가 끝났거나 끝나는 중임을(while 루프를 빠져나와서 run() 메소드가 끝나가는 단계) 나타낸다.

각 상태는 다음 상태의 조건이 되는데, start() 메소드에서 현재 상태를 확인해서 STATE_INIT이 아니라면 이미 시작되었다는 뜻이므로 예외를 던지게 한다. 마찬가지로 stop() 메소드에서도 상태가 STATE_STOPPED 라면 이미 종료가 되었거나 종료 중이므로 예외를 던진다.

위 구현은 멀티쓰레드 환경에서는 오작동을 할 가능성이 있는데, 하나의 ThreadHandle 인스턴스를 여러개의 스레드들이 공유하는 상황이라면 스레드들이 동시에 start(), stop() 메소드를 호출할 때 stateCode 프로퍼티의 일관성이 깨질 수가 있다.

스레드 A와 스레드 B가 동시에 start()를 호출할 때 A가 현재 stateCode가 STATE_INIT임을 보고 if 조건절을 무사히 통과해서 thisThread.start();를 호출할 것이다. 그리고 스레드 A가 stateCode를 STATE_STARTED로 갱신하기 전에 스레드 B가 if 조건절에 진입하면 stateCode가 STATE_INIT 이기 때문에 무사히 통과해서 또다시 스레드 인스턴스를 생성하고 실행시키게 된다.

스레드 A가 stateCode를 갱신한 후에 스레드 B가 if 조건절에 진입하더라도 stateCode가 volatile이 아니기 때문에 스레드 B는 스레드 A가 갱신한 최신의 stateCode 값을 보지 못할 수도 있다.

따라서 ThreadHandle을 멀티쓰레드에서 안전하게 하려면 아래와 같이 critical section을 잘 막아줘야 한다.
    public void start(){
        synchronized ( this ){
            if ( stateCode != STATE_INIT)
                throw new IllegalStateException("already started");
           
            thisThread = new Thread(this);
            if ( threadName != null) thisThread.setName(threadName);
            thisThread.start();
            stateCode = STATE_STARTED;
        }
    }
    ....
    public void stop() {
        synchronized ( this ){
            if ( stateCode == STATE_STOPPED)
                throw new IllegalStateException("already stopped");
            this.stateCode = STATE_STOPPED;
            thisThread.interrupt();
        }
    }
suspend(), resume() 도 위와같이 구현하면 오직 하나의 스레드만이 threadHandle의 상태를 변경할 수 있고 상태를 변경하는동안 다른 스레드들이 진입해서 일관성이 깨지는 것을 막을 수 있다.

stop() 메소드가 호출된 후 상태가 변경되었으므로(stateCode가 변경되었으므로) run() 메소드의 while 문 안에서 이 변경을 확인해서 반복문을 빠져나오는 코드를 삽입한다.
    @Override
    public void run() {
        while ( true ){           
            if ( stateCode == STATE_STOPPED)
                break//종료하라는 신호이므로 루프를 끝낸다.
            processComplexJob();
        }
    }
위에서는 processComplexJob(); 메소드를 실행하기 전에 상태를 확인했는데, 수행할 작업을 끝내는데 꽤 많은 시간이 걸린다면 그 작업을 시작하기 전에 확인해주면 응답성을 높일 수가 있다.

하지만 if 조건절을 판단한 직후에(종료조건이 아님을 확인한 직후에) 다른 스레드가 stop() 을 호출해서 스레드의 상태를 STATE_STOPPED로 바꾸주었다면 간발의 차이로 오랜 시간이 걸리는 작업은 실행될 수 밖에 없다.

이럴 경우 processComplexJob() 메소드 안에서 별도로 stateCode를 다시 한 번 확인하지 않는 한, 일단 시작된 작업이 끝나고 나서 while문을 한 번 반복한 후 다시 if 조건절에 도착해서 STAE_STOPPED 값을 보고 루프를 빠져나올 것이다.

suspend()를 구현한 코드는 아래와 같은데 stateCode 값을 변경하기 전에 현재의 상태를 미리 확인하는 과정을 거친다.

    public void suspend() {
        synchronized ( this ){
            if ( stateCode == STATE_SUSPENDED) return;
            if ( stateCode == STATE_INIT )
                throw new IllegalStateException("not started yet");
            if ( stateCode == STATE_STOPPED)
                throw new IllegalStateException("already stopped");
            stateCode = STATE_SUSPENDED;
        }   
    }

현재의 상태가 STATE_STARTED 가 아니라면 일시 정지가 별 의미가 없으므로 위처럼 모든 상태를 체크해서 적절히 예외를 던지거나 더이상 진행하지 말아야 하는데, 모든 상태를 체크하다보니 실제 상태를 변경하는 코드보다 조건을 검증하는 코드(if 구문들)가 더 많아진다.

여기서는 STARTED, SUSPENDED, STOPPED 의 조건만을 판단하기 때문에 코딩량을 감당할 수 있으나 구현 조건에 따라서 정지중, 정지, 종료중, 종료, 재시작중, 재시작처럼 쓰레드가 거치는 상태가 많다면 state pattern을 도입하는 것을 고려해볼만 하다.

일시 정지 기능을 구현했으니 stateCode의 변경을 보고 스레드를 잠시 정지하는 기능을 run 메소드의 while 반복문 안에 삽입해준다.

    public void run() {
        while ( true ){
            // 상태 코드가 일시 정지라면 while문에서 계속 대기하도록 한다.
            while ( stateCode == STATE_SUSPENDED){
                try {
                    System.out.println("[handle] suspending...");
                    Thread.sleep(24 * 60 * 60 * 1000);
                } catch (InterruptedException e) {
                    if ( stateCode != STATE_SUSPENDED){
                        System.out.println("[handle] resuming...");
                        break;
                    }
                }
            }
            if ( stateCode == STATE_STOPPED){
                System.out.println("[handle] stopping...");
                break;
            }
            processComplexJob();
        }

여기서는 아주 오랜시간동안 쓰레드를 재우는 식으로 구현했는데 저렇게 오랫동안 스레드를 정지시켰을때에는 반드시 누군가가 thisThread.interrupt(); 를 호출해주어야 한다. 따라서 suspend()에서 잠재운 스레드를 다시 실행시키는 resume() 메소드에서는 반드시 thisThread.interrupt(); 를 호출해 준다.

    public void resume() {
        synchronized ( this ){
            if ( stateCode == STATE_STARTED || stateCode == STATE_INIT) return;
            if ( stateCode == STATE_STOPPED)
                throw new IllegalStateException("already stopped");
            stateCode = STATE_STARTED;
            thisThread.interrupt(); // 꼭 해줘야 한다.
        }
    }

stop() 메소드 구현에서도 interrupte() 를 호출하는데, stop() 메소드는 이미 멈춘 상태가 아니라면 언제든 호출될 수 있기 때문에 resume()과 마찬가지로 상태를 변경한 후에 스레드를 깨워주어야 한다.

마지막으로 stateCode 를 volatile 로 바꿔주어서 run() 메소드를 실행중인 스레드가 공유 변수의 복사본만 바라보느라 외부 스레드가 갱신한 최신의 stateCode 의 값을 놓치는 일이 없게 해준다.
public class ThreadHandle implements Runnable {
    ....
    private volatile int stateCode = STATE_INIT;
이렇게 대강 start, resume, suspend, stop 기능을 구현했는데, 이런 메소드가 호출될때마다 ThreadHandle 내에서 관리하는 스레드는 아래와 같이 상태 전이가 반복된다.
스레드의 상태 전이를 관리할 때 위와같이 state diagram을 그려놓으면 구현할 때 도움이 많이 된다.  현재 상태에 따라서 호출할 수 있는 메소드의 종류가 나뉘는 것을 한눈에 알 수 있기 때문에 상태 관리 및 전이를 코드로 옮기기에 용이하다.

어떤 코드들을 보면 스레드에 걸리는 인터럽트 신호를 상태 전이의 조건으로 사용하는 경우도 있다.

위에서는 stop()이 호출된 후 run() 메소드의 while 문 안에서 STATE_STOPPED 값을 확인하고 break; 하도록 했는데 아래처럼 while문의 조건절에서 인터럽트 신호 여부를 종료 조건으로 판단할 수도 있다.
    while ( ! thisThread.isInterrupted() ){
        .....
    }
이럴 경우 스레드 내에서 함부로 인터럽트 신호를 먹어버리지 않도록 유의해야한다. ( 관련글 : [Java] 자바 Thread의 interrupt 이해하기 ) 실행 상태가 아닌 BLOCK 상태에서 인터럽트를 걸면 스레드가 깨어나면서 인터럽트 신호가 해제되므로 catch 절에서 다시 한 번 인터럽트를 걸어서 인터럽스 신호를 복구시켜야 한다.

    try {
        Thread.sleep( sleepTime );
    catch (InteruptedException e){
        thisThread.interrupt(); // 다시 한 번 신호를 걸어준다.
    }

인터럽트 신호를 상태 전이의 힌트로 사용하는 방식도 나쁘진 않지만 스레드가 java nio 를 이용해서 스트립 입출력을 다룬다면 스레드에 인터럽트를 걸 때 신중해야 한다.

NIO 이전의 입출력 메소드들은 읽기, 쓰기 도중에 BLOCK 상태에 있으면 인터럽트에도 반응하지 않았지만 NIO 에서는 읽고 쓰는 도중에 BLOCK 상태에 있을때 인터럽트를 걸면 ClosedByInterruptException 예외가 던져지면서 스트림이 닫혀버린다. ( BLOCK 상태가 아니더라도 현재 스레드에 인터럽트가 걸려있는걸 확인하면 NIO 관련 스트림 클래스들은 스트림을 닫아버린다.)

위의 구현에는 해당되지 않으나 단지 스레드를 깨울 목적으로 인터럽트를 걸었는데 NIO 의 read() 에서 여전히 살아있는 인터럽트 신호를 보고 스트림을 닫아버리는 일이 발생한다. 이럴 경우 NIO의 read, write 실행 직전에 반드시 인터럽트 신호를 해제해줘야하고 read, write 실행 중에 인터럽트가 걸리지 않게 해줘야 하는데 이렇게되면 "입출력중" 이라는 별도의 상태를 정의해야 할 수도 있다. ( 복잡도 증가 )
블로그 이미지

뚱땡이 우주인

,