[JavaScript] arguments(유사 배열 객체)

arguments(유사 배열 객체)


argument라는 객체는 함수 안에서 함수의 여러가지 정보를 담고있는 특히 인자의 정보를 담고 있는 객체이다.(사용법이 배열과 유사)

함수에는 arguments라는 변수에 담긴 숨겨진 유사 배열이 있다. 이 배열에는 함수를 호출할 때 입력한 인자가 담겨있다. 

--> 몇 개의 인자가 있는지 알 수 있다, sum()으로 들어온 특정자리수의 값을 알수 있다.


function sum(){// 매개변수가 없다.

    var i, _sum = 0;    

    for(i = 0; i < arguments.length; i++){

    // argments는 약속되어 있는 특수한 변수명(argument라는 배열이 담겨있다.) 역할은 사용자가 전달한 인자가 argments 객체에 있다. 그러면 이 객체를 통해서 사용자가 전달한 인자에 

    // 접근할 수 있는 기능을 제공한다. sum()이 전달한 인자 개수를 알 수 있다.--> 사용자가 전달한 인자 수 만큼 for문의 루프를 돌린다.

        document.write(i+' : '+arguments[i]+'<br />');

        _sum += arguments[i];

    }   

    return _sum;

}

document.write('result : ' + sum(1,2,3,4));// 함수 정의 문장에 매개변수 선언 되어 있지 않지만 인자 수 상관 없다.

실행 결과)

0 : 1

1 : 2

2 : 3

3 : 4

result : 10

함수 sum은 인자로 전달된 값을 모두 더해서 리턴하는 함수다. 

그런데 1행처럼 함수 sum은 인자에 대한 정의하가 없다. 하지만 마지막 라인에서는 4개의 인자를 함수 sum으로 전달하고 있다. 

함수의 정의부분에서 인자에 대한 구현이 없음에도 인자를 전달 할 수 있는 것은 왜 그럴까? 그것은 arguments라는 특수한 배열이 있기 때문이다.


arguments는 함수안에서 사용할 수 있도록 그 이름이나 특성이 약속되어 있는 일종의 배열이다. 

arguments[0]은 함수로 전달된 첫번째 인자를 알아낼 수 있다.arguments.length를 이용해서 함수로 전달된 인자의 개수를 알아낼 수도 있다. 

이러한 특성에 반복문을 결합하면 함수로 전달된 인자의 값을 순차적으로 가져올 수 있다. 그 값을 더해서 리턴하면 인자로 전달된 값에 대한 총합을 구하는 함수를 만들 수 있다.


arguments는 사실 배열은 아니다. 실제로는 arguments 객체의 인스턴스다.


1. 매개변수의 수

매개변수와 관련된 두가지 수가 있다. 하나는 함수.length, 다른 하나는 arguments.length이다. 

함수.length는 함수로 전달된 실제 인자의 수를 의미하고, arguments.length는 함수에 정의된 인자의 수를 의미한다. 


function zero(){

    console.log(

        'zero.length', zero.length,// 0

        'arguments', arguments.length// 0

    );

}

function one(arg1){

    console.log(

        'one.length', one.length, // 1 --> 함수 정의한 매개변수(인수) 개수

        'arguments', arguments.length // 2 --> 함수 호출시 인자 개수

    );

}

function two(arg1, arg2){

    console.log(

        'two.length', two.length,

        'arguments', arguments.length

    );

}

zero(); // zero.length 0 arguments 0 

one('val1', 'val2');  // one.length 1 arguments 2 

two('val1');  // two.length 2 arguments 1

실행결과)

zero.length 0 arguments 0

VM123:9 one.length 1 arguments 2

VM123:15 two.length 2 arguments 1



'WebFont-end > JavaScript' 카테고리의 다른 글

[JavaScript] 전역객체 window  (0) 2014.12.21
[JavaScript] Object Model  (0) 2014.12.21
[JavaScript] 클로저  (1) 2014.12.21
[JavaScript] 유효범위  (0) 2014.12.21
[JavaScript] Object  (0) 2014.12.07

[JavaScript] 클로저


클로저


클로저(closure)는 내부함수가 외부함수의 맥락(context)에 접근할 수 있는 것을 가르킨다. 클로저는 자바스크립트를 이용한 고난이도의 테크닉을 구사하는데 필수적인 개념으로 활용된다.  


1. 내부함수

자바스크립트는 함수 안에서 또 다른 함수를 선언할 수 있다. 아래의 예제를 보자. 결과는 경고창에 coding everybody가 출력될 것이다.

function outter(){// 외부 함수

    function inner(){ //내부 함수

        var title = 'coding everybody'; 

        alert(title);

    }

    inner();

}

outter();

실행결과)

coding everybody

위의 예제에서 함수 outter의 내부에는 함수 inner가 정의 되어 있다. 함수 inner를 내부 함수라고 한다.


내부함수는 외부함수의 지역변수에 접근할 수 있다.  이것이 바로 클로저이다. 아래의 예제를 보자.

function outter(){

    var title = 'coding everybody';

    function inner(){        

        alert(title);

    }

    inner();

}

outter();

실행결과)

coding everybody

위의 예제는 내부함수 inner에서 title을 호출(4행)했을 때 외부함수인 outter의 지역변수에 접근할 수 있음을 보여준다.


2. 클로저


클로저(closure)는 내부함수와 밀접한 관계를 가지고 있는 주제다. 내부함수는 외부함수의 지역변수에 접근 할 수 있는데 외부함수의 실행이 끝나서 외부함수가 소멸된 이후(return)에도 내부함수가 외부함수의 변수에 접근 할 수 있다. 

이러한 메커니즘을 클로저라고 한다. 아래 예제는 이전의 예제를 조금 변형한 것이다.

function outter(){

    var title = 'coding everybody';  

    return function(){ // return했다면 그 함수는 생을 마감했다는 것이다.       

        alert(title);// title은 외부함수에 존재하는 값이다.

    }

}

inner = outter();//outter라는 함수가 생을 마감했음에도 불구하고

inner();

실행결과)

coding everybody

예제의 실행순서를 주의깊게 살펴보자. 7행에서 함수 outter를 호출하고 있다. 

그 결과가 변수 inner에 담긴다. 그 결과는 이름이 없는 함수다. 실행이 8행으로 넘어오면 outter 함수는 실행이 끝났기 때문에 이 함수의 지역변수는 소멸되는 것이 자연스럽다. 

하지만 8행에서 함수 inner를 실행했을 때 coding everybody가 출력된 것은 외부함수의 지역변수 title이 소멸되지 않았다는 것을 의미한다. (title은 외부함수의 지역변수이다.)

클로저란 내부함수가 외부함수의 지역변수에 접근 할 수 있고, 외부함수는 외부함수의 지역변수를 사용하는 내부함수가 소멸될 때까지 소멸되지 않는 특성을 의미한다.


조금 더 복잡한 아래 예제를 살펴보자. 아래 예제는 클로저를 이용해서 영화의 제목을 저장하고 있는 객체를 정의하고 있다. 

function factory_movie(title){ // 매개변수는 함수 안에서 지역변수로 사용되기 때문에 지역변수이다.

    return {

        get_title : function (){// 여기 메소드들은 factory_movie의 내부함수라고 보면된다.

// 변수 가져오기는 get_title을 통해서만

            return title;// factory_movie의 인자인 title이다.

        },

        set_title : function(_title){

// 변수 수정하는 것은 set_title을 통해서만 --> 보다 변수를 안전하게 사용할 수 있다.

            title = _title

        }

    }

}

ghost = factory_movie('Ghost in the shell'); 

matrix = factory_movie('Matrix');

 

alert(ghost.get_title()); 

alert(matrix.get_title());

// 각각 get_title()이라는 메소드가 접근하는 외부함수의 지역변수에 담겨있는 값이 서로 다르다.

ghost.set_title('공각기동대');

 

alert(ghost.get_title());

alert(matrix.get_title());

실행결과)

Ghost in the shell -> Matrix -> 공각기동대 -> Matrix 


위의 예제를 통해서 알 수 있는 것들을 정리해보면 아래와 같다.

1) 클로저는 객체의 메소드에서도 사용할 수 있다. 위의 예제는 함수의 리턴값으로 객체를 반환하고 있다. 

이 객체는 메소드 get_title과 set_title을 가지고 있다. 이 메소드들은 외부함수인 factory_movie의 인자값으로 전달된 지역변수 title을 사용하고 있다.


2) 동일한 외부함수 안에서 만들어진 내부함수나 메소드는 외부함수의 지역변수를 공유한다. 17행에서 실행된 set_title은 외부함수 factory_movie의 지역변수 title의 값을 '공각기동대'로 변경했다. 19행에서 ghost.get_title();의 값이 '공각기동대'인 것은 set_movie와 get_movie 함수가 title의 값을 공유하고 있다는 의미다.


3) 그런데 똑같은 외부함수 factory_movie를 공유하고 있는 ghost와 matrix의 get_title의 결과는 서로 각각 다르다. 그것은 외부함수가 실행될 때마다 새로운 지역변수를 포함하는 클로저가 생성되기 때문에 ghost와 matrix는 서로 완전히 독립된 객체가 된다.


4) factory_movie의 지역변수 title은 2행에서 정의된 객체의 메소드에서만 접근 할 수 있는 값이다. 이 말은 title의 값을 읽고 수정 할 수 있는 것은 factory_movie 메소드를 통해서 만들어진 객체 뿐이라는 의미다. JavaScript는 기본적으로 Private한 속성을 지원하지 않는데, 클로저의 이러한 특성을 이용해서 Private한 속성을 사용할 수 있게된다.


cf.)

Private 속성은 객체의 외부에서는 접근 할 수 없는 외부에 감춰진 속성이나 메소드를 의미한다. 이를 통해서 객체의 내부에서만 사용해야 하는 값이 노출됨으로서 생길 수 있는 오류를 줄일 수 있다. 

자바와 같은 언어에서는 이러한 특성을 언어 문법 차원에서 지원하고 있다. 아래의 예제는 클로저와 관련해서 자주 언급되는 예제다. 

var arr = [];

for(var i = 0; i < 5; i++){

    arr[i] = function(id){

        return function(){

            return id;

        }

    }

}

for(var index in arr) {

    console.log(arr[index]());

}

실행결과)

1

2

3

4

5

5

5

5

5

5


위의 코드는 아래와 같이 변경해야 한다.

var arr = [];

for(var i = 0; i < 5; i++){

    arr[i] = function(id) {

        return function(){

            return id;

        }

    }(i);

}

for(var index in arr) {

    console.log(arr[index]());

}

실행결과)

0

1

2

3

4


※ 클로저 참고

https://developer.mozilla.org/ko/docs/JavaScript/Guide/Closures

http://ejohn.org/apps/learn/#48

http://blog.javarouka.me/2012/01/javascripts-closure.html

'WebFont-end > JavaScript' 카테고리의 다른 글

[JavaScript] Object Model  (0) 2014.12.21
[JavaScript] arguments(유사 배열 객체)  (0) 2014.12.21
[JavaScript] 유효범위  (0) 2014.12.21
[JavaScript] Object  (0) 2014.12.07
[JavaScript] prototype  (0) 2014.12.07

[JavaScript] 유효범위


유효범위


유효범위(Scope)는 변수의 수명을 의미한다. 

var vscope = 'global';// 전역 변수

function fscope(){

    alert(vscope);

}

fscope();

실행결과)

global


함수 밖에서 변수를 선언하면 그 변수는 전역변수가 된다. 전역변수는 에플리케이션 전역에서 접근이 가능한 변수다. 다시 말해서 어떤 함수 안에서도 그 변수에 접근 할 수 있다. 그렇기 때문에 함수 fscope 내에서 vscope를 호출 했을 때 함수 밖에서 선언된 vscope의 값 global이 반환된 것이다. 

var vscope = 'global';

function fscope(){

    var vscope = 'local';// 로컬 변수

    alert('함수안 '+vscope);//local

}

fscope();

alert('함수밖 '+vscope);//grobal

실행결과)

함수 안: local 

함수 밖: global


즉 함수 안에서 변수 vscope을 조회(4행) 했을 때 함수 내에서 선언한 지역변수 vscope(3행)의 값인 local이 사용되었다. 하지만 함수 밖에서 vscope를 호출(7행) 했을 때는 전역변수 vscope(1행)의 값인 global이 사용된 것이다. 즉 지역변수의 유효범위는 함수 안이고, 전역변수의 유효범위는 에플리케이션 전역인데, 같은 이름의 지역변수와 전역변수가 동시에 정의되어 있다면 지역변수가 우선한다는 것을 알 수 있다. 아래 예제를 보자. 결과는 모두 local이다.


var vscope = 'global';

function fscope(){

    vscope = 'local'; // 전역 변수

    alert('함수안='+vscope); //local

}

fscope();

alert('함수밖='+vscope); //grobal

실행결과)

함수안=local

함수밖=local

// local(전역변수인 vscope를 변경한 것이므로)


var vscope = 'global';

function fscope(){

    var vscope = 'local';

    vscope = 'local';// 지역변수가 변경

    alert('함수안='+vscope);

}

fscope();

alert('함수밖='+vscope);

실행결과)

함수안=local

함수밖=global


함수밖에서도 vscope의 값이 local인 이유는 무엇일까? 그것은 함수 fscope의 지역변수를 선언할 때 var를 사용하지 않았기 때문이다. var를 사용하지 않은 지역변수는 전역변수가 된다. 따라서 3행은 전역변수의 값을 local로 변경하게 된 것이다. var을 쓰는 것과 쓰지 않는 것의 차이를 이해해야 한다.


전역변수는 사용하지 않는 것이 좋다. 여러가지 이유로 그 값이 변경될 수 있기 때문이다. 함수 안에서 전역변수를 사용하고 있는데, 누군가에 의해서 전역변수의 값이 달라졌다면 어떻게 될까? 함수의 동작도 달라지게 된다. 이것은 버그의 원인이 된다. 또한 함수를 다른 에플리케이션에 이식하는데도 어려움을 초래한다. 함수의 핵심은 로직의 재활용이라는 점을 상기하자. 변수를 선언할 때는 꼭 var을 붙이는 것을 습관화해야 한다. 전역변수를 사용해야 하는 경우라면 그것을 사용하는 이유를 명확히 알고 있을 때 사용하도록 하자.


1. 유효범위의 효용

아래 두개의 예제는 변수 i를 지역변수로 사용했을 때와 전역변수로 사용했을 때의 차이점을 보여준다. 전역변수는 각기 다른 로직에서 사용하는 같은 이름의 변수값을 변경시켜서 의도하지 않은 문제를 발생시킨다.

(1) 지역변수의 사용

function a (){

    var i = 0;// i는 지역변수

}

for(var i = 0; i < 5; i++){

    a();

    document.write(i);

}

실행결과)

01234


(2) 전역변수의 사용

본 예제는 무한반복을 발생시킨다. for문 안의 i값이 전역변수 

function a (){

    i = 0;// i는 전역변수

}

for(i = 0; i < 5; i++){

    a();

    document.write(i);

}


불가피하게 전역변수를 사용해야 하는 경우는 하나의 객체를 전역변수로 만들고 객체의 속성으로 변수를 관리하는 방법을 사용한다.

var MYAPP = {} // MAYAPP이라는 전역변수에다 다 집어 넣음

MYAPP.calculator = { // 객체안의 속성의 값으로 다시 객체를 선언

    'left' : null,

    'right' : null

}

MYAPP.coordinate = {

    'left' : null,

    'right' : null

}

 

MYAPP.calculator.left = 10;

MYAPP.calculator.right = 20;

function sum(){

    return MYAPP.calculator.left + MYAPP.calculator.right;

}

document.write(sum());

실행결과)

30 


전역변수를 사용하고 싶지 않다면 아래와 같이 익명함수를 호출함으로서 이러한 목적을 달성할 수 있다.

(function(){

    var MYAPP = {}

    MYAPP.calculator = {

        'left' : null,

        'right' : null

    }

    MYAPP.coordinate = {

        'left' : null,

        'right' : null

    }

    MYAPP.calculator.left = 10;

    MYAPP.calculator.right = 20;

    function sum(){

        return MYAPP.calculator.left + MYAPP.calculator.right;

    }

    document.write(sum());

}())

실행결과)

30 

함수를 정의한 후에 바로 호출할 때 괄호 사용(익명함수 사용이유) -->의미: MYAPP이라는 변수는 함수안에서 사용되는 변수로서 함수의 지역변수가 된다.

이러한 방식은 JQuery에서 많이 사용하는 방식


function myappfn(){

    var MYAPP = {}

    MYAPP.calculator = {

        'left' : null,

        'right' : null

    }

    MYAPP.coordinate = {

        'left' : null,

        'right' : null

    }

    MYAPP.calculator.left = 10;

    MYAPP.calculator.right = 20;

    function sum(){

        return MYAPP.calculator.left + MYAPP.calculator.right;

    }

    document.write(sum());

}

myappfn(); //MYAPP은 전역변수이다.

실행결과)

30 

위와 같은 방법은 자바스크립트에서 로직을 모듈화하는 일반적인 방법이다. 


2. 유효범위의 대상 (함수)

자바스크립트는 함수에 대한 유효범위만을 제공한다. 많은 언어들이 블록(대체로 {,})에 대한 유효범위를 제공하는 것과 다른 점이다. 

for(var i = 0; i < 1; i++){

    var name = 'coding everybody';

}

alert(name);//유효 범위 밖에서도 가능

실행결과)

coding everybody


cf.) 자바에서는 아래의 코드는 허용되지 않는다. name은 지역변수로 for 문 안에서 선언 되었는데 이를 for문 밖에서 호출하고 있기 때문이다.

for(int i = 0; i < 10; i++){

    String name = "egoing";

}

System.out.println(name); //console.log()와 같다.

자바스크립트의 지역변수는 함수에서만 유효하다.


3. 정적 유효범위

자바스크립트는 함수가 선언된 시점에서의 유효범위를 갖는다. 이러한 유효범위의 방식을 정적 유효범위(static scoping), 혹은 렉시컬(lexical scoping)이라고 한다. 

var i = 5;

 

function a(){

    var i = 10;

    b();

}


function b(){

    document.write(i);//지역변수가 없으면 전역변수를 찾게된다.

}

a();

실행 결과)

5

동적 유효범위: 사용되는 대상에 따라서 그 대상이 가지고 있는 변수에 접근할 수 있다.

정적 유효범위: 사용될 때가 아니고 정의(그 시점)될 때의 전역변수가 사용되게 된다. b는 누구에게 사용될 지 모른다. 누가 사용하든 똑같은 범위이다.


'WebFont-end > JavaScript' 카테고리의 다른 글

[JavaScript] arguments(유사 배열 객체)  (0) 2014.12.21
[JavaScript] 클로저  (1) 2014.12.21
[JavaScript] Object  (0) 2014.12.07
[JavaScript] prototype  (0) 2014.12.07
[JavaScript] 상속  (0) 2014.12.07

[Java] 인터페이스와 다형성


인터페이스와 다형성


1. 인터페이스와 다형성


인터페이스는 이를 구현한 클래스의 조상이라 할 수 있으므로, 해당 인터페이스의 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있다. 인터페이스 타입으로 형변환도 가능하다.
(특정한 인터페이스를 구현하고 있는 클래스가 있을 때 이 클래스의 데이터 타입으로 인터페이스를 지정 할 수 있다.)
Fightable t = (fightable) new Fighter();
또는
Fighetable f = new Figher();
인터페이스 Fightable을 클래스 Fighter가 구현했을 때, Fighter 인스턴스를 Fightable타입의 참조변수로 참조하는 것이 가능하다.
Fightable 타입의 참조변수로 인터페이스 Fightable에 정의된 멤버들만 호출이 가능하다.

void attack(Fightable f){
...
}
인터페이스 타입의 매개변수가 갖는 의미는 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야 한다는 것이다.
그래서 attack메서드를 호출할 때는 매개변수로 Fightable 인터페이스를 구현한 클래스의 인스턴스를 넘겨주어야 한다.

class Fightable extends implements Fightable{
public void move(int x, int y){
///내용 생략
}
public void attack(Fightable f){
//내용 생략
}
}
위와 같이 Fightable 인터페이스를 구현한 Fightable클래스가 있을 때, attack메서드의 매개변수로 Fighter인스턴스를 넘겨 줄 수 있다. 즉, attack(new Fighter())와 같이 할 수 있다는 것이다.
그리고 다음과 같이 메서드의 리턴타입으로 인터페이스의 타입을 지정하는 것 역시 가능하다.

Fightable method(){
//...
return new Fighter();
}
리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.
위의 코드에서는 method()의 리턴타입이 Fightable인터페이스이기 때문에 메서드의 return문에서 Fightable인터페이스를 구현한 Fightable클래스의 인스턴스를 반환한다.

(1) 예제
interface I{ }
class C implements I{ }
public class PolymorphismDemo2 {
    public static void main(String[] args) {
        I obj = new C();
    }
}
위의 코드를 통해서 알 수 있는 것은 클래스 C의 데이터 타입으로 인터페이스 I가 될 수 있다는 점이다. 이것은 다중 상속이 지원되는 인터페이스의 특징과 결합해서 상속과는 다른 양상의 효과를 만들어낸다. 아래 코드를 보자.

interface I2{
    public String A();
}
interface I3{
    public String B();
}
class D implements I2, I3{
    public String A(){
        return "A";
    }
    public String B(){
        return "B";
    }
}
public class PolymorphismDemo3 {
    public static void main(String[] args) {
        D obj = new D();
        I2 objI2 = new D();
        I3 objI3 = new D();
         
        obj.A();
        obj.B();
         
        objI2.A();
        //objI2.B();//I2 인터페이스에 B 메소드가 없다.
         
        //objI3.A();
        objI3.B();
    }
}
주석처리된 메소드 호출은 오류가 발생하는 것들이다. objI2.b()에서 오류가 발생하는 이유는 objI2의 데이터 타입이 인터페이스 I이기 때문이다. 인터페이스 I는 메소드 A만을 정의하고 있고 I를 데이터 타입으로 하는 인스턴스는 마치 메소드 A만을 가지고 있는 것처럼 동작하기 때문이다.
이것은 인터페이스의 매우 중요한 특징 중의 하나를 보여준다. 인스턴스 objI2의 데이터 타입을 I2로 한다는 것은 인스턴스를 외부에서 제어할 수 있는 조작 장치를 인스턴스 I2의 맴버로 제한한다는 의미가 된다. 인스턴스 I2와 I3로 인해서 하나의 클래스가 다양한 형태를 띄게 되는 것이다.
cf.) 하나의 인터페이스를 구현하는 여러 개의 클래스가 있다면 각각의 클래스를 인스턴스화 했을 때 데이터 타입으로 각가의 클래스가 공통적으로 구현하고 있는 인터페이스를 데이터 타입으로 해서 같은 데이터 타입으로 가지고 있지만 실제 클래스가 무엇이냐에 따라 다르게 동작할 수 있다.

ex.) MyMp3 mp3 = new TonyMp3( );
‘MyMp3 기준을 만족하는 제품 TonyMp3 객체‘이런 해석으로 보면 성립되는 구문이다. 이것이 바로 인터페이스를 변수 타입으로 선언한다는 의미이다.
반드시 기억해야할 사실은 컴파일러가 따지는 것은 단순히 변수의 타입이라는 것이다. 
즉 MyMp3 mp3 = new TonyMp3( );에서 컴파일러는 뒤의가 MyMp3의 기 객체 기능이 구현된 객체이기만 하면 된다. 때문에 mp3.XXX를 호출할 때 컴파일러가 따지는 것은 인터페이스인 
MyMp3에 호출하려는 XXX가 존재하는지만이 중요할 뿐이다.
→ 인터페이스는 어떤 객체를 직접 알고서 호출하는 방식이 아니라. 어떤 객체가 어떤 기능적인 스펙을 만족한다는 것을 더 중요하게 생각한다.
따라서 인터페이스를 이용해서 어떤 호출을 할 때 중요한 것은 실제 객체가 무엇인가가 중요한 것이 아니라 인터페이스에 해당하는 메소드가 존재하는지가 더 중요하다.

2. 다형성을 실행활에 비유
필자가 이해를 돕기 위해서 비유를 시도해보겠다. 누차 강조 하지만 비유는 비유일 뿐이다. 비유는 여러분의 머리속을 더욱 복잡하게 할 수 있다.
사람은 다면적인 존재다. Steve라는 사람이 있다. 이 사람은 집에서는 아버지이고 직업적으로는 프로그래머이고 또 종교단체 내에서는 신도(believer)가 될 수 있다. 하나의 사람이지만 그가 어디에 있는가? 누구와 관계하는가에 따라서 아버지이면서 프로그래머이고 또 신도인 것이다.
Rachel는 집에서는 엄마고 직장에서는 프로그래머다.
Steve와 Rachel이 같은 직장(Workspace)에 다니고 있다고 한다면 직장 입장에서는 두사람이 프로그래머라는 점이 중요할 뿐 이들의 가족관계나 종교성향에는 관심이 없다. 직장 입장에서 두사람은 프로그래머이고 프로그래머는 코딩을 통해서 무엇인가를 창조하는 사람들이다. 따라서 이들에게 업무를 요청할 때는 코딩을 요청하면 된다. 하지만 두 사람의 실력이나 성향에 따라서 코딩의 결과물은 달라질 것이다. 이러한 관계를 굳이 코드로 만들면 아래와 같다.

interface father{}
interface mother{}
interface programmer{
    public void coding();
}
interface believer{}
class Steve implements father, programmer, believer{
    public void coding(){
        System.out.println("fast");
    }
}
class Rachel implements mother, programmer{
    public void coding(){
        System.out.println("elegance");
    }
}
public class Workspace{
    public static void main(String[] args[]){
        programmer employee1 = new Steve();
        programmer employee2 = new Rachel();
         
        employee1.coding();
        employee2.coding();
    }
}
위의 코드를 보면 알겠지만 Steve와 Rachel의 사용자인 직장에서는 Steve와 Rachel의 인터페이스인 programmer를 통해서 두사람과 관계하게 된다. 두 사람이 어떤 종교나 가족관계를 가졌건 인터페이스 programmer을 가지고 있다면 고용할 수 있다. 회사에서는 코딩을 할 수 있는 사람이 필요하고 어떤 사람이 programmer라는 인터페이스를 구현하고 있다면 그 사람은 반드시 coding이라는 메소드를 구현하고 있을 것이기 때문이다. 또 두 사람에게 업무를 요청 할 때는 programmer라는 인터페이스의 메소드인 coding을 통해서 요청하면 된다. 하지만 두 사람의 성향이나 능력에 따라서 그 업무를 수행한 결과는 다른데 Steve는 빠르게 코딩하고 Rachel은 우아하게 코딩하고 있다.

3. 다형성 관련 예제(상속)

class AAA {
protected int x = 100;
}
class BBB extends AAA {
int y = 300;
}
class CCC extends AAA {
int z = 400;
}
public class Exam_12 {
public static void main(String[] ar) {
BBB bb = new BBB();
System.out.println("bb.x = " + bb.x);
System.out.println("bb.y = " + bb.y);
CCC cc = new CCC();
System.out.println("cc.x = " + cc.x);
System.out.println("cc.y = " + cc.z);
AAA ab = new BBB();
System.out.println("ab.x = " + ab.x);
//System.out.println("ab.y = " + ab.y);
AAA ac = new CCC();
System.out.println("ac.x = " + ac.x);
//System.out.println("ac.z = " + ac.z);
}
}
//다형성을 쓰는 이유: 한 가지 타입으로 관리하여 묶어서 사용하기 위함(ex. 한 가지 타입으로 배열로 묶어 사용할 수 있음)
//다형성을 안쓰면 매번 클래스를 생성해야 함. 대신 부모타입으로 객체생성하면 하위클래스의 멤버는 사용할 수 없다.

class AA {
public String toString() {
return "AAAA";
}
}

class BB {
public String toString() {
return "BBBB";
}
}

public class Exam_13 {
public static void main(String[] ar) {
// 두 객체를 Object 타입으로 담아서 배열로 관리하기
Object[] obj = new Object[2];
obj[0] = new AA();
obj[1] = new BB();

for (int i = 0; i < obj.length; ++i) {
System.out.println("obj[" + i + "] = " + obj[i]);
}
/*
* Object a = new AA(); Object b = new BB(); 
* System.out.println("a = " + a);//a.toString() 
* System.out.println("b = " + b);//b.toString()
*/
}
}

class CCCC {
int x = 100;
int y = 200;
}

class DDDD extends CCCC {
int y = 300;
int z = 400;
}

public class Exam_14 {
public static void main(String[] ar) {
// CCCC.x, DDDD.y, DDDD.z 사용가능
DDDD dd = new DDDD();
System.out.println("dd.x = " + dd.x);
System.out.println("dd.y = " + dd.y);
System.out.println("dd.z = " + dd.z);
// CCCC.x, CCCC.y 사용가능 / DDDD.z는 사용불가
CCCC cd = new DDDD();
System.out.println("cd.x = " + cd.x);
System.out.println("cd.y = " + cd.y);// 부모의 멤버 필드 사용
}
}

class FF {
public void aaa() {
System.out.println("FF=AAA");
}

public void bbb() {
System.out.println("FF=BBB");
}
}

class HH extends FF{
public void bbb() {
System.out.println("HHCCC");
}

public void ccc() {
System.out.println("HHDDD");
}
}

public class Exam_15 {
public static void main(String[] ar) {
HH hh = new HH();
hh.aaa();
hh.bbb();
hh.ccc();
FF fh = new HH();
fh.aaa();
// fh.bbb()는 HH.bbb()를 사용한다.--> fh.bbb
fh.bbb();
// ap.ccc();
}
}


'Programing > Java' 카테고리의 다른 글

[Java] public static void main(String [] args)  (0) 2014.12.23
[Java] 인터페이스  (0) 2014.12.23
[Java] 다형성  (0) 2014.12.21
[Java] 쓰레드의 동기화  (0) 2014.12.21
[Java] 쓰레드의 실행제어  (0) 2014.12.21

[Java] 다형성


다형성 


1. 다형성이란?

다형성은 상속과 깊은 관계가 있다.

객체지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미하며 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록함으로써 다형성을 프로그램적으로 구현하였다.


인터페이스와 상속은 둘 다 다형성이라는 객체지향 프로그래밍의 특징을 구현하는 방식이다.

1 다형성: 하나의 객체를 여러 개의 타입으로, 하나의 타입으로 여러 종류의 객체를 여러 가지 모습으로 해석될 수 있는 성격이라고 생각하면 된다.

vo.) 다형성(Ploymorphism): 'poly'는 다양한, 많은/‘morp'는 형태


2 다형성은 하나의 객체를 여러 가지 타입으로 선언할 수 있다는 뜻이다.

다형성은 개발자들에세는 간단히 말해서 하나의 사물(객체)을 다양한 타입으로 선언하고 사용할 수 있다는 의미로 해석해주면 된다. 일반적으로 어떤 객체가 하나의 분류에만 포함되는 것은 아니다. 대한민국의 국민인 동시에, 남자인 동시에, 서울에 사는 사람 등과 같이 이처럼 다형성은 어떤 사물을 여러 가지 시선으로 바라보는 모습을 생각하면 쉽게 이해할 수 있다.


3 Java에서 다형성은 상속과 인터페이스를 통해 이루어진다.

다형성의 의미는 하나의 객체를 다양한 시선(타입)으로 바라볼 수 있게 한다는 의미이다.

중요한 것은 다양한 타입으로 본다는 사실 자체가 아니라 다양한 타입으로 객체를 바라보게 되면 호출할 수 있는 메소드 역시 타입에 따라 달라진다는 것이다. 상속의 오버라이딩을 설명하면서 오버라이딩을 하게 되면 컴파일러는 실제 객체의 메소드를 바라보는 것이 아니라. 변수 선언 타입의 메소드를 본다.

Mouse m = new WheelMouse( );

실제 객체가 WheelMouse이지만 컴파일러는 Mouse 타입의 메소드가 정상적으로 호출되고 있는지에만 관심을 두게 된다.


4 인터페이스가 상속보다 다형성에 더욱 유연함을 제공한다.

인터페이스는 클래스의 선언 뒤에서 여러 개의 인터페이스를 구현할 수 있게 할 수 있다. 이런 이유 때문에 하나의 객체를 여러 개의 타입으로 바라보는 다형성에는 상속보다 인터페이스가 더 큰 유연함을 제공한다고 할 수 있다.

cf.) 인터페이스가 여러 개 올 수 있다는 의미는 다시 말해 ‘여러가지 타입으로 변수를 선언할 수 있다’라는 것이다.(인터페이스를 상속과 결부시키지 말고 다형성의 측면에서 이해해야만 한다. 인터페이스는 다중 구현이라는 말이 더 정확하다.)

인터페이스는 그 목적상 기능을 의미하게 할 수 있다. 즉 어떤 객체가 어떤 기능을 할 수 있는가로 설계할 경우에 기능에 초점을 두고 인터페이스로 설계할 수 있다는 얘기이다. 따라서 이렇게 되면 어떤 객체는 여러 가지 기능을 가지게 된다.

결론적으로 인터페이스를 이용하면 하나의 객체가 여러 개의 기능을 가지는 형태로 보이게 만들어줄 수 있다.

마치 상속에서 부모 타입으로 변수를 선언하고 자식 타입으로 객체를 생성하는 코드와 유사하긴 하지만 인터페이스는 더 다양한 형태로 객체를 정의해줄 수 있다. 이것은 마치 부모 클래스의 기능을 물려받는 모습처럼 선언되기는 하지만 상속보다는 더 많은 종류를 보여줄 수 있게 된다. 이런 모습 때문에 일반적으로 다중 상속의 기능을 활용하기 위해서 인터페이스를 사용한다는 설명되는 경우가 많다.


※ 이슈: 다형성을 반영한 참조변수

1) 메서드를 호출한 실제 객체 타입

2) 멤버변수의 실제 객체 타입


이를 좀 더 구체적으로 말하자면, 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다는 것이다.

class Tv{

boolean power;

int channel;

void power(){ 

power = power;

}

void channelUp(){

++channel;

}

void channelDown(){

--channel;

}

}

class CationTv extends Tv{

String text;//캡션을 보여 주기 위한 문자열

void captionTv(){

...

}

}


지금까지 생성된 인스턴스를 다루기 위해서, 인스턴스의 타입과 일치하는 타입의 참조변수만을 사용했다. 즉, Tv인스턴스를 다루기 위해서는 Tv타입의 참조변수를 사용하고, CaptionTv인스턴스를 다루기 위해서는 CaptionTv타입의 참조변수를 사용했다.

Tv t =new Tv();

CationTv e = new CaptionTv();

이처럼 인스턴스의 타입과 참조변수의 타입이 일치하는 것이 보통이지만, Tv와 CaptionTv클래스가 서로 상속관계에 있을 경우, 다음과 같이 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조하도록 하는 것도 가능하다.


Tv t =new  CationTv();


※ 인스턴스를 같은 타입의 참조변수로 참조하는 것과 조상타입의 참조변수로 참조하는 것의 차이점

CationTv c = new CaptionTv();


Tv t =new CaptionTv();

위의 코드에서 CaptionTv 인스턴스 2개를 생성하고, 참조변수 c, t가 생성된 인스턴스를 하나씩 참조하도록 하였다. 이 경우 실제 인스턴스가 CationTv타입이라 할지라도, 참조변수 t로는 CaptionTv인스턴스의 모든 멤버를 사용할 수 없다. Tv 타입의 참조변수로는 CaptionTv 인스턴스 중에서 Tv클래스의 멤버들(상속받은 멤버포함)만 사용할 수 있다. 따라서, 생성된 CaptionTv 인스턴스의 멤버 중에서 Tv클래스에 정의 되지 않은 멤버, text와 caption()은 참조변수 t로 사용이 불가능하다. 즉, t.text 또는 t.caption()와 같이 할 수 없다는 것이다.

둘 다 같은 타입의 인스턴스지만, 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.


cf.) Caption c = new Tv(); // 컴파일 에러

실제 인스턴스인 Tv의 멤버 개수보다 참조변수 c가 사용할 수 있는 멤버 개수가 더 많기 때문이다.

--> 자손타입의 참조변수로 조상타입의 인스턴스를 참조하는 것은 존재하지 않는 멤버를 사용하고자 할 가능성이 있으므로 허용하지 않는다. 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 하는 것이다.

참조변수의 타입이 참조변수가 참고하고 있는 인스턴스에서 사용할 수 있는 멤버의 개수를 결정한다는 사실을 이해하는 것은 매우 중요하다.


2. 참조변수의 형변환 (기본형변수의 형변환과 구별)


기본형 변수와 같이 참조변수도 형변환이 가능하다. 단, 서로 상속관계에 있는 클래스 사이에서만 가능하기 때문에 자손 타입의 참조변수를 조상타입의 참조변수로, 조상타입의 참조변수를 자손타입 참조변수로의 형변환만 가능하다.

(Child-->Person, Person -->Child)

cf.) 바로 위 조상이나 자손이 아닌 간접적인 상속관계, 예를 들면 조상의 조상에 있는 경우에도 형변환이 가능하다. 따라서 모든 참조변수는 모든 클래스의 조상인 Object클래스 타입으로 형변환이 가능하다.


기본형 변수의 형변환에서 작은 자료형에서 큰 자료형의 형변환은 생략이 가능하듯이, 참조형 변수의 형변환에서는 자손타입의 참조변수를 조상타입으로 형변환하는 경우에는 형변환을 생략할 수 있다.

(작은 것 --> 큰 것은 형변환 생략가능--> 멤버의 개수가 실제 인스턴스가 갖고 있는 멤버의 개수보다 적을 것이 분명하므로 문제가 되지 않는다. 그래서 형변환을 생략할 수 있도록 한 것이다.

하지만, 큰 것에서 작은 것으로 형변환 할 경우 참조변수가 다룰 수 있는 멤버의 개수를 늘이는 것이므로, 실제 인스턴스의 멤버 개수보다 참조변수가 사용할 수 있는 멤버의 개수가 더 많아지므로 문제가 발생할 가능성이 있다. 그래서 자손타입으로의 형변환은 생략할 수 없으며, 형변환을 수행하기 전에 instanceof 연산자를 사용해서 참조변수가 참조하고 있는 실제 인스턴스의 타입을 확인하는 것이 안전하다.


참조 변수의 형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다.

단지 참조변수의 형변환을 통해서, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것뿐이다.)

cf.) Tv t =new Caption(); 도 원래는 Tv t = (Tv)new Caption();의 형태이다.


class CastingTest1 {

public static void main(String args[]) {

Car car = null;

FireEngine fe = new FireEngine();

FireEngine fe2 = null;


fe.water();

car = fe; // car =(Car)fe;에서 형변환이 생략된 형태다.

    // car.water(); // 컴파일 에러!!! Car타입의 참조변수로는 water()를 호출할 수 없다.

fe2 = (FireEngine)car; //자손타입 ← 조상타입

fe2.water();

}

}


class Car {

String color;

int door;


void drive() {// 운전하는 기능

System.out.println("drive, Brrrr~");

}


void stop() {// 멈추는 기능

System.out.println("stop!!!");

}

}


class FireEngine extends Car {// 소방차

void water() {// 물을 뿌리는 기능

System.out.println("water!!!");

}

}

실행결과)

water!!!

water!!!


class CastingTest2 {

public static void main(String args[]) {

Car car = new Car();

Car car2 = null;

FireEngine fe = null;

  

car.drive();

fe = (FireEngine)car;// 8번째 줄. 실행 시 에러가 발생한다.(조상타입의 인스턴스를 자손타입의 참조변수로 참조하는 것을 허용되지 않는다.--> Child c = new Person();)

fe.drive();

car2 = fe;

car2.drive();

}

}

실행결과)

drive, Brrrr~

java.lang.ClassCastException: Car

at CastingTest2.main(CastingTest2.java:8)

cf.) 캐스트 연산자를 사용하면 서로 상속관계에 있는 클래스 타입의 참조변수간의 형변환은 양방향으로 자유롭게 수행될 수 있다. 그러나 참조변수가 참조하고 있는 인스턴스의 자손타입으로 형변환을 하는 것은 허용되지 않는다.


3. 참조변수와 인스턴스의 연결

조상타입의 참조변수와 자손타입의 참조변수의 차이점이 사용할 수 있는 멤버의 개수에 있다고 배웠다. 
조상클래스에 선언된 멤버변수와 같은 이름의 인스턴스변수를 자손 클래스에 중복으로 정의했을 때, 조상타입의 참조변수로 자손 인스턴스를 참조하는 경우와 자손타입의 참조변수로 자손 인스턴스를 참조하는 경우는 서로 다른 결과를 얻는다.
메서드의 경우 조상 클래스의 메서드를 자손의 클래스에서 오버라이딩한 경우에도 참조변수의 타입에 관계없이 항상 실제 인스턴스의 메서드(오버라이딩된 메서드)가 호출되지만, 멤버변수의 경우 참조변수의 타입에 따라 달라진다.
메소드: 실제 인스턴스에 따라
멤버변수: 참조변수의 타입에 따라
cf.) static 메서드는 static변수처럼 참조변수의 타입에 영향을 받는다. 참조변수의 타입에 영향을 받지 않는 것은 인스턴스메서드 뿐이다.

멤버변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우, 조상타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 멤버변수가 사용되고, 자손 타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버변수가 사용된다.
하지만 중복 정의가 되지 않은 경우, 조상타입의 참조변수를 사용했을 때와 자손타입의 참조변수를 사용했을 때의 차이는 없다. 중복된 경우는 참조변수의 타입에 따라 달라지지만, 중복되지 않은 경우 하나뿐이므로 선택의 여지가 없기  때문이다.

class BindingTest{
public static void main(String[] args) {
Parent p = new Child();
Child c = new Child();

System.out.println("p.x = " + p.x);
p.method();

System.out.println("c.x = " + c.x);
c.method();
}
}

class Parent {
int x = 100;

void method() {
System.out.println("Parent Method");
}
}

class Child extends Parent {
int x = 200;

void method() {
System.out.println("Child Method");
}
}
실행결과)
p.x=100
Child Method
c.x=200
Child Method
타입은 다르지만, 참조변수 p,c 모두 Child인스턴스를 참조하고 있다. 그리고, Parent클래스와 Child클래스는 서로 같은 멤버들을  정의하고 있다.
이 때 조상타입의 참조변수 p로 Child 인스턴스의 멤버들을 사용하는 것과 자손타입의 참조변수 c로 Child인스턴의 멤버들을 사용하는 것의 차이를 알 수 있다.
메서드인 method()의 경우 참조변수의 타입에 관계없이 항상 실제 인스턴스의 타입인 Child클래스에 정의된 메서드가 호출되지만, 인스턴스변수인 x는 참조변수의 타입에 따라서 달라진다.

class BindingTest2 {
public static void main(String[] args) {
Parent p = new Child();
Child c = new Child();

System.out.println("p.x = " + p.x);
p.method();

System.out.println("c.x = " + c.x);
c.method();
}
}

class Parent {
int x = 100;

void method() {
System.out.println("Parent Method");
}
}
class Child extends Parent { }
실행결과)
p.x=100
Child Method
c.x=200
Child Method
Child 클래스에는 아무런 멤버도 정의되어 있지 않고 단순히 조상으로부터 멤버들을 상속받는다. 그렇기 때문에 참조변수의 타입에 관계없이 조상의 멤버들을 사용하게 된다. 
이처럼 자손 클래스에서 조상 클래스의 멤버를 중복으로 정의하지 않았을 때는 참조변수의 타입에 따른 변화가 없다.
어느 클래스의 멤버가 호출되어야 할지, 즉 조상의 멤버가 호출되어야할 지, 자손의 멤버가 호출되어야할 지에 대해 선택의 여지가 없기 때문이다. 
참조변수의 타입에 따라 결과가 달라지는 경우는 조상 클래스의 멤버변수와 같은 이름의 멤버변수를 자손 클래스에 중복해서 정의한 경우뿐이다.

class BindingTest3{
public static void main(String[] args) {
Parent p = new Child();
Child c = new Child();

System.out.println("p.x = " + p.x);
p.method();
System.out.println();
System.out.println("c.x = " + c.x);
c.method();
}
}

class Parent {
int x = 100;

void method() {
System.out.println("Parent Method");
}
}

class Child extends Parent {
int x = 200;

void method() {
System.out.println("x=" + x); // this.x와 같다.
System.out.println("super.x=" + super.x);
System.out.println("this.x=" + this.x);
}
}
실행결과)
p.x=100
x=200
super.x=100
this.x=200

c.x=200
x=200
super.x=100
this.x=200
자손클래스 Child에 선언된 인스턴스변수 x와 조상 클래스 Parent로부터 상속받은 인스턴스변수 x를 구분하는데 참조변수 super와 this가 사용된다. 
자손인 Child클래스에서의 super.x는 조상 클래스인 Parent에 선언된 인스턴스변수 x를 뜻하며, this.x 또는 x는 Child클래스의 인스턴스변수 x를 뜻한다. 그래서 위 결과에서 x와 this.x의 값이 같다.
전에 배운 것과 같이 멤버변수들은 주로 private으로 접근을 제한하고, 외부에서는 메서드를 통해서만 멤버변수에 접근할 수 있도록 하지, 이번 예제에서처럼 다른 외부 클래스에서 참조변수를 통해 직접적으로 인스턴스변수에 접근할 수 있게 하지 않는다. 인스턴스변수에 직접 접근하면, 참조변수의 타입에 따라 사용되는 인스턴스변수가 달라질 수 있으므로 주의해야 한다.

4. 매개변수의 다형성
참조변수의 다형적인 특징은 메서드의 매개변수에도 적용된다. 아래와 같이 Product, Tv, Computer, Audio, Buyer클래스가 정의되어 있다고 가정하자.
cp.) 메서드 매개변수에 객체와 객체 타입 선언 한다는 것의 의미: 매개변수로 선언된 객체를 사용하겠다는 의미

class Product{
int price;
int bonusPoint;
}

class Tv extends Product{}
class Computer extends Product{}
class audio extends Product{}

class Buyer{
int money=1000;
int bonusPoint=0;
}
Product 클래스는 Tv와 Computer클래스의 조상이며, Buyer클래스는 제품을 구현하는 사람을 클래스로 표현한 것이다.
Buyer클래스에 물건을 구입하는 기능의 메서드를 추가해보자. 구입할 대상이 필요하므로 매개변수로 구입할 제품을  넘겨받아야 한다. Tv를 살 수 있도록 매개변수를 Tv타입으로 하였다.

void buy(Product p){
money=money-p.price;
bonusPoint=bonusPoint+p.bonusPoint;
}
매개변수가 Product타입의 참조변수라는 것은,  메서드의 매개변수로 Product클래스의 자손타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있다는 뜻이다.
--> 앞으로 다른 제품 클래스를 추가할 때 Product클래스를 상속받기만 하면, buy(Product  p)메서드의 매개변수로 받아들여질 수 있다. 따라서, 메서드의 매개변수에 다형성을 적용하면 하나의 메서드로 간단히 처리할 수 있다.

class Product 
{
int price;// 제품의 가격
int bonusPoint;// 제품구매 시 제공하는 보너스점수

Product(int price) {
this.price = price;
bonusPoint =(int)(price/10.0);// 보너스점수는 제품가격의 10%
}
}

class Tv extends Product {
Tv() {
// 조상클래스의 생성자 Product(int price)를 호출한다.
super(100);// Tv의 가격을 100만원으로 한다.
}

public String toString() {// Object클래스의 toString()을 오버라이딩한다.
return "Tv";
}
}

class Computer extends Product {
Computer() {
super(200);
}

public String toString() {
return "Computer";
}
}

class Buyer {// 고객, 물건을 사는 사람
int money = 1000;// 소유금액
int bonusPoint = 0;// 보너스점수

void buy(Product p) {
if(money < p.price) {
System.out.println("잔액이 부족하여 물건을 살수 없습니다.");
return;
}

money -= p.price;// 가진 돈에서 구입한 제품의 가격을 뺀다.
bonusPoint += p.bonusPoint;// 제품의 보너스 점수를 추가한다.
System.out.println(p + "을/를 구입하셨습니다.");
}
}

class PolyArgumentTest {
public static void main(String args[]) {
Buyer b = new Buyer();
Tv tv = new Tv();
Computer com = new Computer();

b.buy(tv);
b.buy(com);

System.out.println("현재 남은 돈은 " + b.money + "만원입니다.");
System.out.println("현재 보너스점수는 " + b.bonusPoint + "점입니다.");
}
}
실행결과)
Tv을/를 구입하셨습니다.
Computer을/를 구입하셨습니다.
현재 남은 돈은 700만원입니다.
현재 보너스점수는 30점입니다.
cf.)
Print(Object o)는 매개변수로 Object타입의 변수가 선언되어 있는데 Object 클래스는 모든 클래스의 조상이므로 이 메서드의 매개변수로 어떤 타입의 인스턴스도 가능하므로, 매개변수에 toString()을 호출하여 문자열을 얻어서 출력한다.


'Programing > Java' 카테고리의 다른 글

[Java] 인터페이스  (0) 2014.12.23
[Java] 인터페이스와 다형성  (1) 2014.12.21
[Java] 쓰레드의 동기화  (0) 2014.12.21
[Java] 쓰레드의 실행제어  (0) 2014.12.21
[Java] 데몬쓰레드  (0) 2014.12.21

[Java] 쓰레드의 동기화


쓰레드의 동기화


싱글쓰레드 프로세스의 경우 프로세스 내에 단 하나의 쓰레드만 작업하기 때문에 프로세스의 자원을 가지고 작업하는 데 별문제가 없지만, 멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업을 하기 때문에 서로의 작업에 영향을 주게 된다. 만일 쓰레드A가 작업하던 도중에 다른 쓰레드B에게 제어권이 넘어갔을 때, 쓰레드A가 작업하던 공유 데이터를 쓰레드B가 임의로 변경하였다면, 다시 쓰레드A가 제어권을 받아서 나머지 작업을 마쳤을 때 원래 의도했던 것과는 다른 결과를 얻을 수 있다.

이는 마치 한 방의 여러 사람이 방안의 컴퓨터를 함께 나눠 쓰는 상황과 같아서 한 사람이 컴퓨터로 문서작업 도중에 잠시 자리를 비웠을 때 다른 사람이 컴퓨터를 만져서 앞 사람이 작업하던 문서가 지원진다던가 하는 일이 생길 수 있다. 이럴 때는 문서작업이 끝날 때까지는 컴퓨터에 비밀번호를 걸어서 다른 사람이 사용할 수 없도록 해야 한다.

이처럼 멀티쓰레드 프로그래밍에서 동기화는 중요한 요소이다. 얼마만큼 동기화를 잘 처리하는가에 따라서 프로그램의 성능에 많은 영향을 미치게 된다.


1. synchronized를 이용한 동기화

자바에서는 키워드 synchronized를 통해 해당 작업과 관련된 공유 데이터에 lock을 걸어서 먼저 작업 중이던 쓰레드가 작업을 완전히 마칠 때까지는 다른 쓰레드에게 제어권이 넘어가더라도 데이터가 변경되지 않도록 보호함으로써 쓰레드의 동기화를 가능하게 한다.


(1) 특정 객체에 lock을 걸고자 할 때

synchronized(객체의 참조변수){

//.....

}

synchronized블록의 경우 지정된 객체는  synchronized블럭의 시작부터 lock이 걸렸다가 블록이 끝나면 lock이 풀린다. 이 블록을 수행하는 동안은 지정된 객체에 lock이 걸려서 다른 쓰레드가 이 객체에 접근할 수 없게된다.


(2) 메서드에 lock을 걸고자 할 때

public synchronized void calcSum(){

//.....

}

synchronized 메서드의 경우에도 한 쓰레드가 synchronized 메서드를 호출해서 수행하고 있으면, 이 메서드가 종료될 때까지  다른 쓰레드가 이 메서드를 호출하여 수행할 수 없게 된다.


2. 예제

class ThreadEx24 {

public static void main(String args[]) {

Runnable r = new A();

Thread t1 = new Thread(r);

Thread t2 = new Thread(r);


t1.start();

t2.start();

}

}


class Account {

int balance = 1000;


public void withdraw(int money){

if(balance >= money) {

try { Thread.sleep(1000);} catch(Exception e) {}

balance -= money;

}

} // withdraw

}


class A implements Runnable {

Account acc = new Account();


public void run() {

while(acc.balance > 0) {

// 100, 200, 300중의 한 값을 임으로 선택해서 출금(withdraw)

int money = (int)(Math.random() * 3 + 1) * 100;

acc.withdraw(money);

System.out.println("balance:"+acc.balance);

}

} // run()

}

실행결과)

balance:700

balance:500

balance:200

balance:200

balance:0

balance:-100

실행결과를 보면 잔고(balance)가 음수인 것을 알 수 있다. 그 이유는 한 쓰레드가 if문의 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들어서 출금을 먼저 했기 때문이다.

에를 들어 한 쓰레드가 id문의 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들어서 출금을 먼저 했기 때문이다. 예를 들어 한 쓰레드가 if문의 조건식을 계산했을 때는 잔고(balane)가 200이고 출금하려는

금액(money)이 100이라서 조건식(balance >= money)이 true가 되어 출금(balance -= money)을 수행하려는 순간 다른 쓰레드에게 제어권이 넘어가서 다른 쓰레드가 200을 출금하여 잔고가 0이 되었다.

다시 이전의 쓰레드로 제어권이 넘어오면 if문 다음부터 수행하게 되므로 확인하는 if문과 출금하는 문장은 하나로 동기화블록으로 묶어져야 한다.

예제에서는 상황을 보여주기 위해 일부러 Thread.sleep(1000)을 사용해서 if문을 통과하자마자 다른 쓰레드에게 제어권이 넘기도록 하였지만, 굳이 이렇게 하지 않더라도 쓰레드의 작업이 다른 쓰레드에 의해서 영향을 받는 일이 발생할 수 있기 때문에 동기화가 반드시 필요하다.


class ThreadEx24 {

public static void main(String args[]) {

Runnable r = new A();

Thread t1 = new Thread(r);

Thread t2 = new Thread(r);


t1.start();

t2.start();

}

}


class Account {

int balance = 1000;


public synchronized void withdraw(int money){ //synchronized 키워드를 붙이기만 하면 간단히 동기화가 된다.

if(balance >= money) {

try { Thread.sleep(1000);} catch(Exception e) {}

balance -= money;

}

} // withdraw

}


class A implements Runnable {

Account acc = new Account();


public void run() {

while(acc.balance > 0) {

// 100, 200, 300중의 한 값을 임으로 선택해서 출금(withdraw)

int money = (int)(Math.random() * 3 + 1) * 100;

acc.withdraw(money);

System.out.println("balance:"+acc.balance);

}

} // run()

}

실행결과)

balance:800

balance:700

balance:500

balance:300

balance:100

balance:100

balance:0

balance:0

한 쓰레드에 의해서 먼저 withdraw()가 호출되면, 종료될 때까지 다른 쓰레드가 withdraw()를 호출하더라도 대기상태에 머물게 된다. 즉, withdraw()는 한 순간에 단 하나의 쓰레드만 사용할 수 있다는 것이다.


cf.) 만일 withdraw()가 수행되는 동안 객체에 lock을 걸고자 한다면 다음과 같이 할 수도 있다.

public void withdraw(int money){

synchronized (this){

if(balance >= money) {

try { Thread.sleep(1000);} catch(Exception e) {}

balance -= money;

}

    }

  }// withdraw()


아래와 같이 어떠한 하나의 데이터(int x) 를 처리함에 있어 쓰레드로 작성되어 있다면 그 데이터를 관리하는 set, get 메서드는 함께 동기화를 걸어주는 것이 기존적인 사항이다.

class K extends Thread {

private int x = 100;

public synchronized void setX(int x) {

this.x += x;

}

public synchronized int getX() { 

return x;

}

public void run() {

synchronized (this) { // 지역 동기화

setX(200);//300 + 200

System.out.println("x = " + getX()); //500

}

}

}

public class Exam_04 {

public static void main(String[] ar) {

K kp = new K();

kp.start();

}

}

실행결과)

x = 300

여러개의 쓰레드가 동시에 실행되면서 문제가 발생한다. 여러개 쓰레드 수행 도중에 데이터 꼬임 발생한다. 하나의 쓰레드에 대해 만약에 여러 개가 동작해서 문제의 소지가 발생할 경우를 대비하여 동기화를 시켜준다.

누군가 run()이라는 메서드를 처리하는 동안에는 다른 사람은 run()이라는 메서드를 실행할 수 없다.


cf.) 동기화 메서드(메서드 전체)

public synchronized void run() {

// 지역 동기화(특정 범위에 하나의 메서드가 들어있을 경우)

// 지역을 지정하여, setX()나 getX() 메서드 두 가지(동기화 필요한 메서드만)에 대해 동기화를 걸 경우 사용하는 것이다.

synchronized (this) { 

setX(200);

System.out.println("x = " + getX()); 

}

}



'Programing > Java' 카테고리의 다른 글

[Java] 인터페이스와 다형성  (1) 2014.12.21
[Java] 다형성  (0) 2014.12.21
[Java] 쓰레드의 실행제어  (0) 2014.12.21
[Java] 데몬쓰레드  (0) 2014.12.21
[Java] 쓰레드 그룹  (0) 2014.12.21

[Java] 쓰레드의 실행제어


쓰레드의 실행제어


쓰레드 프로그래밍이 어렵다고 하는 이유는 동기화와 스케줄링 때문이다. 앞서 우선순위를 통해 쓰레드간의 스케줄링을 하는 방법을 제시하였지만, 사실 이것만으로는 부족하다.

효율적인 멀티쓰레드 프로그램을 만들기 위해서는 보다 정교한 스케줄링을 통해 프로세스에게 주어진 자원과 시간을 여러 쓰레드가 낭비없이 잘 사용하도록 프로그래밍 해야 한다.


1. 쓰레드의 스케줄링과 관련된 메서드


2. 쓰레드의 상태


3. 쓰레드의 생성부터 소멸까지의 모든 과정

1 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행대기열에 저장되어 자신의 차례가 될 때까지 기다려야 한다. 실행대기열은 큐(queue)와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행된다.


2 실행대기상태에 있다가 자신의 차례가 되면 실행상태가 된다.


3 주어진 실행시간이 다되거나 yeild()를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 된다.


4 실행 중에 suspecd(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다. I/O block은 입출력작업에서 발생하는 지연상태를 말한다. 사용자의 입력을 기다리는 경우를 예로 들 수 있는데, 이런 경    우 일시정지 상태에 있다가 사용자가 입력을 마치면 다시 실행대기상태가 된다.


5 지정된 일시정지시간이 다되거나(time-out), notify(), resume(), interrupt()가 호출되면 일시정지 상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 된다.


6 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸된다.

cf.) 1부터 6까지 번호를 붙였으나 번호의 순서대로 쓰레드가 수행되는 것은 아니다.


4. 예제

class ThreadEx13 {

static long startTime = 0;


public static void main(String args[]) {


A th1 = new A();

B th2 = new B();


th1.start();

th2.start();

startTime = System.currentTimeMillis();


try {

th1.join(); // th1의 작업이 끝날 때까지 기다린다.

th2.join(); // th2의 작업이 끝날 때까지 기다린다.

} catch(InterruptedException e) {}


System.out.print("소요시간:" + (System.currentTimeMillis() - ThreadEx13.startTime));

} // main

}


class A extends Thread {

public void run() {

for(int i=0; i < 300; i++) {

System.out.print("-");

}

} // run()

}


class A extends Thread {

public void run() {

for(int i=0; i < 300; i++) {

System.out.print("|");

}

} // run()

}

실행결과)

------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||소요시간:13

join()을 사용하지 않으면 main쓰레드는 바로 종료되지만, join()을 사용해서 쓰레드 th1과 th2의 작업을 마칠 때 까지 main 쓰레드가 기다리도록 했다.

만약 join(long millis)이나 join(long millis, int nanos)를 사용하면 지정된 시간만큼만 main 쓰레드가 기다리도록 할 수 있다. 이처럼 한 쓰레드의 작업의 중간에 다른 쓰레드의 작업이 필요할 때 join()을 사용한다.


class ThreadEx14 {

public static void main(String args[]) {

A th1 = new A();

  B th2 = new B();


th1.start();

try {

th1.join(); 

  // join(): 두 개 이상의 쓰레드가 동작할 시 하나의 쓰레드에 대해서 지속을 거는 것. 두 개의 쓰레드가 진행하고 있는데 한 쓰레드에 대해서 join 걸면                                           // 그 쓰레드가 끝날때 까지 기다려준다.

  // cf.) sleep()은 전체 쓰레드에 대해 지연을 건다. 하지만 join()은 특정 쓰레드에 대해 지연을 건다.

} catch(InterruptedException e) {}

th2.start();

}

}


class A extends Thread {

public void run() {

for(int i=0; i < 300; i++) {

System.out.print("-");

}

}

}


class B extends Thread {

public void run() {

for(int i=0; i < 300; i++) {

System.out.print("|");

}

}

}

실행결과)

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||

join()이 사용된 부분을 주석처리하고 실행한 결과이다.

두 쓰레드가 번갈아 가며 실행되지 않고, 순차적으로 실행해야할 때 join()을 사용해서 해결하는 방법을 보여주는 예제이다.


cf.) 바로 위 예제에 join() 부분을 주석처리한 경우

/*try {th1.join();} catch(InterruptedException e) {}*/

실행결과)

--|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||------------------------------------------------------------------------------------|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

두 쓰레드가 순차적으로 실행되지 않고 번갈아 가며 실행되는 모습이다.


 class ThreadEx15{

public static void main(String args[]) {

A th1 = new A();

B th2 = new B();


th1.start();

th2.start();


try {

th1.sleep(5000);

  // sleep(): 작업 흐름 대기시간 설정한다. 5초동안 대기시간 갖은 후에 다음 문자의 실행흐름을 이어 나간다.

} catch(InterruptedException e) {}


System.out.print("<<main 종료>>");

} // main

}


class A extends Thread {

public void run() {

for(int i=0; i < 300; i++) {

System.out.print("-");

}

System.out.print("<<th1 종료>>");

} // run()

}


class B extends Thread {

public void run() {

for(int i=0; i < 300; i++) {

System.out.print("|");

}

System.out.print("<<th2 종료>>");

} // run()

}

실행결과)

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------||||||||||||||||||||||||||||-------------------------------------<<th1 종료>>||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||<<th2 종료>><<main 종료>>

결과를 보면 쓰레드 th1의 작업이 가장 먼저 종료되었고, 그 다음이 th2, main의 순인 것을 알 수 있다.

th1과 th2에 대해 start()를 호출하자마자 th1.sleep(5000)을 호출하여 쓰레드 th1이 5초동안 작업을 멈추고 일시정지상태에 있도록 하였으니까 쓰레드 th1이 가장 늦게 종료되어야 하는데 결과에서는 제일 먼저 종료되었다. 그 이유는 sleep()이 항상 현재 실행 중인 쓰레드에 대해 작동하기 때문에 th1.sleep(5000)과 같이 호출하였어도 실제로 영향을 받는 것은 main 메서드를 실행하는 main 쓰레드이다.

그래서 sleep()은 static으로 선언되었으며 참조변수를 이용해서 호출하기 보다는 Thread.sleep(5000);과 같이 해야 한다. yield() 또한 이와 같은 이유에서 static으로 선언되어 있어서 항상 현재

실행 중인 쓰레드에 대해 동작하며 Thread.yield()와 같이 호출해야 한다.


cf.) 위의 예제 중 th1.sleep(5000) 대신 Thread.sleep(5000)

try {Thread.sleep(5000);} catch(InterruptedException e) {}

실행결과)

||||||||||||||||||||||--------------------------|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||----------------|||||||||||||||||||||||--------------------------------------------------------------------------------------------------------------------------|||||||||||||||||||||||||||||||||||||||||||||||||||||||---------------------|||||||||||||||-------------------------------------------------------------------------------<<th2 종료>>------------------------------------<<th1 종료>><<main 종료>>


 class ThreadEx19 {

public static void main(String args[]) {

MyThreadEx19 th1 = new MyThreadEx19("*");

MyThreadEx19 th2 = new MyThreadEx19("**");

MyThreadEx19 th3 = new MyThreadEx19("***");

th1.start();

th2.start();

th3.start();


try {

Thread.sleep(2000);

th1.suspend();

Thread.sleep(2000);

th2.suspend();

Thread.sleep(3000);

th1.resume();

Thread.sleep(3000);

th1.stop();

th2.stop();

Thread.sleep(2000);

th3.stop();

} catch (InterruptedException e) {}

}

}


class MyThreadEx19 implements Runnable {

boolean suspended = false;

boolean stopped = false;


Thread th;


MyThreadEx19(String name) {

th = new Thread(this, name); // Thread(Runnable r, String name)

}


public void run() {

String name = Thread.currentThread().getName();


while(!stopped) {

if(!suspended) {

System.out.println(name);

try {

Thread.sleep(1000);

} catch(InterruptedException e) {

System.out.println(name + " - interrupted");

}

} else {

Thread.yield();

}

}

System.out.println(name + " - stopped");

}


public void suspend() {

suspended = true;

th.interrupt();

// interrupt(): 특정 객체를 멈추고자 할때 사용, 쓰레드의 권한 중지

System.out.println("interrupt() in suspend()");

}


public void resume() {

suspended = false;

}


public void stop() {

stopped = true;

th.interrupt();

System.out.println("interrupt() in stop()");

}


public void start() {

th.start();

}

}

실행결과)

*

**

***

*

**

***

*

interrupt() in suspend()

* - interrupted

***

**

**

***

** - interrupted

interrupt() in suspend()

***

***

***

*

***

*

***

*

***

*

interrupt() in stop()

interrupt() in stop()

* - interrupted

* - stopped

** - stopped

***

***

interrupt() in stop()

*** - interrupted

*** - stopped

- yield(): yield()를 호출해서 남은 실행시간을 while문에서 낭비하지 않고, 다른 쓰레드에게 양보하게 된다.

- interrupt(): InterruptedException을 발생시켜서 sleep(), join(), wait()에 의해 일시정지 상태인 쓰레드를 실행대기상태로 만든다. interrupt()가 호출되었을 때, sleep(), join(), wait()에 의한 일시정지상태가 아니라면 아무런 일도 일어나지 않는다. 만일 stop()이 호출되었을 때 Thread.sleep(1000)에 의해 쓰레드가 일시정지상태에 머물러 있는 상황이라면 쓰레드가 정지될 때까지 최대 약 1초의 시간지연이 생길 것이다. 그래서 잠자고 있는 쓰레드를 깨울 interrupt()가 필요하다. 같은 상황에서 interrupt()가 호출되면, Thread.sleep(1000)에서 InterruptedException이 발생하고 즉시 일시정지상태에서 벗어나 catch 블럭 내의 문장이 실행된다.

이처럼 interrupt()를 사용하면 쓰레드의 응답성을 높일 수 있다.


'Programing > Java' 카테고리의 다른 글

[Java] 다형성  (0) 2014.12.21
[Java] 쓰레드의 동기화  (0) 2014.12.21
[Java] 데몬쓰레드  (0) 2014.12.21
[Java] 쓰레드 그룹  (0) 2014.12.21
[Java] 쓰레드의 우선순위  (0) 2014.12.21

[Java] 데몬쓰레드


데몬 쓰레드


데몬 쓰레드는 다른 일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다.

레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료되는데, 그 이유는 데몬 쓰레드는 일반 쓰레드의 보조 역할을 수행하므로 일반 쓰레드가 모두 종료되고 나면 데몬 쓰레드의 존재의 의미가 없기 때문이다. 이 점을 제외하고는 데몬 쓰레드와 일반 쓰레드는 다르지 않다. 데몬 쓰레드의 예로는 가비지 컬렉션, 워드 프로세서의 자동저장, 화면 자동갱신 등이 있다.

데몬 쓰레드는 무한 루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다. 

데몬 쓰레드는 일반 쓰레드의 작성 방법과 실행 방법이 같으며, 다만 쓰레드를 생성한 다음 실행하기 전에 setDaemon(true)를 호출하기만 하면 된다. 

쓰레드 실행 전에 반드시 데몬쓰레드로 설정해야 한다.


boolean isDaemon()  // 쓰레드가 데몬 쓰레드인지 확인한다. 데몬 쓰레드이면 true 반환한다.

void setDaemon(boolean on)  // 쓰레드를 데몬 쓰레드 또는 사용자 쓰레드로 변경한다.(매개변수 on의 값을 true로 지정하면 데몬 쓰레드가 된다.)


예) 이 쓰레드를 데몬 쓰레드로 설정하지 않았다면 계속 무한 루프를 돌 것이다.

import java.util.*;

 

class example implements Runnable{

    static boolean autoSave = false;

     

    public static void main(String[] args) {

        Thread t = new Thread(new example());

        t.setDaemon(true); // 이 부분이 없으면 종료되지 않는.

        // 디폴트: 독립==> setDaemon(true): 데몬쓰레드로 (메인쓰레드 종료시 종속쓰레드는 작업 다 못끝내도 메인 쓰레드와 함께 종료된다.)

        t.start();

         

        for(int i=1; i<=20; i++)

        {

            try{

                Thread.sleep(1000);

            }catch(InterruptedException e){}

            System.out.println(i);

             

            if(i==5)

                autoSave = true;

        }

        System.out.println("프로그램을 종료합니다.");

    }

 

    @Override

    public void run() {

        while(true)

        {

            try{

                Thread.sleep(3 * 1000);

            }catch(InterruptedException e){}

             

            if(autoSave)

                autoSave();

        }

    }

 

    private void autoSave() {

        System.out.println("작업파일이 자동저장되었습니다.");

    }

}

실행 결과)

1

2

3

4

5

작업파일이 자동저장되었습니다.

6

7

8

작업파일이 자동저장되었습니다.

9

10

11

작업파일이 자동저장되었습니다.

12

13

14

작업파일이 자동저장되었습니다.

15

16

17

작업파일이 자동저장되었습니다.

18

19

20

프로그램을 종료합니다.

3초마다 변수 autoSave의 값을 확인해서 그 값이 true이면, autoSave()를 호출하는 일을 무한히 반복하도록 쓰레드를 작성하였다.

만일 이 쓰레드를 데몬 쓰레드로 설정하지 않았다면, 이 프로그램은 강제종료하지 않는 한 영원히 종료되지 않을 것이다.

setDaemon 메서드는 반드시 start()를 호출하기 전에 실행되어야 한다. 그렇지 않으면 IllegalThreadStateException이 발생한다.


'Programing > Java' 카테고리의 다른 글

[Java] 쓰레드의 동기화  (0) 2014.12.21
[Java] 쓰레드의 실행제어  (0) 2014.12.21
[Java] 쓰레드 그룹  (0) 2014.12.21
[Java] 쓰레드의 우선순위  (0) 2014.12.21
[Java] 쓰레드 기본  (0) 2014.12.21

[Java] 쓰레드 그룹


쓰레드 그룹


쓰레드 그룹은 서로 관련된 쓰레드를 그룹으로 다루기 위한 것으로, 폴더를 생성해서 관련된 파일들을 함께 넣어서 관리하는 것처럼 쓰레드 그룹을 생성해서 쓰레드를 그룹으로 묶어서 관리할 수 있다. 폴더 안에 폴더를 생성할 수 있듯이 쓰레드 그룹에 다른 쓰레드 그룹을 포함 시킬 수 있다. 

사실 쓰레드 그룹은 보안상의 이유로 도입된 개념으로, 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경할 수 있지만 다른 쓰레드 그룹의 쓰레드를 변경할 수는 없다.


1. ThreadGroup의 생성자와 메서드


2. 쓰레드를 쓰레드 그룹에 포함시키는 방법

Thread(ThreadGroup group, String name)

Thread(ThreadGroup group, Runnable target)

Thread(ThreadGroup group, Runnable target, String name)

Thread(ThreadGroup group, Runnable target, String name, long stackSize)


모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 하기 때문에, 위와 같이 쓰레드 그룹을 지정하는 생성자를 사용하지 않은 쓰레드는 기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 된다.

자바 어플리케이션이 실행되면, JVM은 main과 system이라는 쓰레드 그룹을 만들고 JVM운영에 필요한 쓰레드들을 생성해서 이 쓰레드 그룹에 포함시킨다. 예를 들어, main 메서드를 수행하는 main이라는 이름의 쓰레드는 main 쓰레드 그룹에 속하고, 가비지컬렉션을 수행하는 Finalizer쓰레드는 system쓰레드 그룹에 속한다.

우리가 생성하는 모든 쓰레드 그룹은 main 쓰레드 그룹의 하위 쓰레드 그룹이 되며, 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 자동적으로 main 쓰레드 그룹에 속하게 된다.


3. 그 외에 Thread의 쓰레드 그룹과 관련된 메서드

static int activeCount()

// 쓰레드 자신이 속한 그룹에 작업이 완료되지 않은 쓰레드 수를 반환한다.

ThreadGroup getThreadGroup()

// 쓰레드 자신이 속한 쓰레드 그룹을 반환한다.


 class ThreadEx10 {

public static void main(String args[]) {

ThreadGroup main = Thread.currentThread().getThreadGroup();

ThreadGroup grp1 = new ThreadGroup("Group1");

ThreadGroup grp2 = new ThreadGroup("Group2");


// ThreadGroup(ThreadGroup parent, String name) 

ThreadGroup subGrp1 = new ThreadGroup(grp1,"SubGroup1"); 


grp1.setMaxPriority(3); // 쓰레드 그룹 grp1의 최대우선순위를 3으로 변경.

// Thread(ThreadGroup tg, String name)

Thread th1 = new Thread(grp1, "th1"); 

// Thread(ThreadGroup tg, String name)

Thread th2 = new Thread(subGrp1, "th2");

// Thread(ThreadGroup tg, String name)

Thread th3 = new Thread(grp2, "th3");   


th1.start();

th2.start();

th3.start();


System.out.println(">>List of ThreadGroup : "+ main.getName() 

  +", Active ThreadGroup: " + main.activeGroupCount()

  +", Active Thread: " + main.activeCount());

main.list();

}

}

실행 결과)

>>List of ThreadGroup : main, Active ThreadGroup : 3, Active Thread : 4

java.lang.ThreadGroup[name=main,maxpri=10]

    Thread[main,5,main]

    java.lang.ThreadGroup[name=Group1,maxpri=3]

        Thread[th1,3,Group1]

        java.lang.ThreadGroup[name=SubGroup1,maxpri=3]

            Thread[th2,3,SubGroup1]

    java.lang.ThreadGroup[name=Group2,maxpri=10]


쓰레드 그룹과 쓰레드를 생성하고 main.list()를 호출해서 main 쓰레드 그룹의 정보를 출력하는 예제이다.

결과를 보면 쓰레드 그룹에 포함된 하위 쓰레드 그룹이나, 쓰레드는 들여쓰기를 이용해서 구별되도록 하였음을 알 수 있다.

새로 생성한 모든 쓰레드 그룹은 main 쓰레드 그룹의 하위 쓰레드 그룹으로 포함되어 있다는 것과 setMaxPriority()는 쓰레드가 쓰레드 그룹에 추가되기 이전에 호출되어야 하며, 쓰레드 그룹 grp1의 최대우선순위를 3으로 했기 때문에, 후에 여기에 속하게 된 쓰레드 그룹과 쓰레드가 영향을 받았음을 확인하자.


'Programing > Java' 카테고리의 다른 글

[Java] 쓰레드의 실행제어  (0) 2014.12.21
[Java] 데몬쓰레드  (0) 2014.12.21
[Java] 쓰레드의 우선순위  (0) 2014.12.21
[Java] 쓰레드 기본  (0) 2014.12.21
[Java] 직렬화  (0) 2014.12.17

[Java] 쓰레드의 우선순위


쓰레드의 우선순위


쓰레드는 우선순위라는 속성(멤버변수)을 가지고 있는데, 이 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다. 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다.


예를 들어 파일 전송기능이 있는 메신저의 경우, 파일다운로드를 처리하는 쓰레드보다 채팅 내용을 전송하는 쓰레드의 우선순위가 더 높아야 사용자가 채팅을 하는데 불편함이 없을 것이다. 대신 파일다운로드 작업에 걸리는 시간은 더 길어질 것이다.

이처럼 시각적인 부분이나. 사용자에게 빠르게 반응해야하는 작업을 하는 쓰레드의 우선 순위는 다른 작업을 수행하는 쓰레드에 비해 높아야 한다.

만약 A,B 두 쓰레드에게 거의 같은 양의 실행시간이 주어지지만, 우선순위가 다르다면 우선순위가 높은 A에게 상대적으로 B보다 더 많은 양의 실행시간이 주어지고 결과적으로 더 빨리 작업이 완료될 수 있다.


※ 쓰레드의 우선순위와 관련된 메서드와 필드

void setPriority(int newPriority): 쓰레드의 우선순위를 지정한 값으로 변경한다.

int getPriority(): 쓰레드의 우선순위를 반환한다.


public static final int MAX_PRIORITY = 10 // 최대 우선 순위

public static final int MIN_PRIORITY = 1 // 최소 우선 순위

public static final int NORM_PRIORITY = 5 //보통 우선 순위

쓰레드가 가질 수 있는 우선순위의 범위는 1~10이며 숫자가 높을 수록 우선순위가 높다.

그러나 우선수위의 높고 낮음은 절대적인 것이 아니라 상대적인 것임에 주의하자.

한가지 더 알아둘 것은 쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속 받는다는 것이다. main 메서드를 수행하는 쓰레드는 우선순위가 5이므로 main 메서드 내에서 생성하는 쓰레드의 우선순위는 자동적으로 5가 된다.


class ThreadPriority {

public static void main(String args[]) {

A th1 = new A();

B th2 = new B();


th1.setPriority(4); // defalut 우선순위 5

th2.setPriority(7);


System.out.println("Priority of th1(-) : " + th1.getPriority() );

System.out.println("Priority of th2(|) : " + th2.getPriority() );

th1.start();

th2.start();

}

}


class A extends Thread {

public void run() {

for(int i=0; i < 300; i++) {

System.out.print("-");

for(int x=0; x < 10000000; x++);

}

}

}


class B extends Thread {

public void run() {

for(int i=0; i < 300; i++) {

System.out.print("|");

for(int x=0; x < 10000000; x++);

}

}

}

실행결과)

Priority of th1(-) : 4

Priority of th2(|) : 7

-||-||-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||

th1과 th2 모두 main 메서드에서 생성하였기 때문에 main 메서드를 실행하는 쓰레드의 우선순위인 5를 상속받았다. 그 다음에는 th2.setPriority(7)로 th2의 우선순위를 7로 변경한 다음에 start()를 호출해서 쓰레드를 실행시켰다. 이처럼 쓰레드를 실행하기 전에만 우선 순위를 변경할 수 있다는 것을 기억하자.

--> 우선순위가 높은 th2의 실행시간이 th1에 비해 상당히 늘어난다.


'Programing > Java' 카테고리의 다른 글

[Java] 데몬쓰레드  (0) 2014.12.21
[Java] 쓰레드 그룹  (0) 2014.12.21
[Java] 쓰레드 기본  (0) 2014.12.21
[Java] 직렬화  (0) 2014.12.17
[Java] File 클래스  (0) 2014.12.16