'Programing/Java'에 해당되는 글 35건

  1. [Java] 인터페이스
  2. [Java] 인터페이스와 다형성 1
  3. [Java] 다형성
  4. [Java] 쓰레드의 동기화
  5. [Java] 쓰레드의 실행제어
  6. [Java] 데몬쓰레드
  7. [Java] 쓰레드 그룹
  8. [Java] 쓰레드의 우선순위
  9. [Java] 쓰레드 기본
  10. [Java] 직렬화

[Java] 인터페이스


인터페이스


1. 인터페이스란?

인터페이스는 일종의 추상클래스이다. 인터페이스는 추상클래스처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다.

오직 추상메서드와 상수만을 멤버로 가질 수 있으며, 그 외의 다른 어떠한 요소도 허용하지 않는다.

추상클래스를 부분적으로만 완성된 '미완성 설계도'라고 한다면, 인터페이스는 구현된 것은 아무 것도 없고 밑그림만 그려져 있는 '기본 설계도'라 할 수 있다. 인터페이스도 추상클래스처럼 완성되지 않은 불완전한 것이기 때문에 그 자체만으로 사용되기 보다는 다른 클래스를 작성하는데 도움 줄 목적으로 작성된다.


cp.) 인터페이스란 무엇인가

사전적 의미는 어떤 객체와 객체의 중간에 놓이는 것(객체와 객체의 중간에 놓이는 통신 채널)


1) 인터페이스는 뭐 할 때 쓰는 건가?

둘이서 서로 다른 클래스를 만들 때 서로 ‘이렇게 만들자, 이렇게 만들어 드릴게요’라면서 약속할 때 쓰이는 것이 인터페이스이다. 즉 클래스를 만들기 전에 앞으로 이런 기능을 만들겠다. 이런 기능이 있었으면 좋겠다고 먼저 합의해놓은 것이 인터페이스이다.


2) 인터페이스는 어떻게 쓰는 것인가?

먼저 필요한 약속을 먼저 잡고, 한쪽에서 약속대로 호출하고, 한쪽에서는 약속대로 구현대로 사용한다. 인터페이스를 구현하는 입장에서는 약속한 대로 기능(메소드)을 만들어주고, 사용하는 입장에서는 실제 객체가 어떻든 간에 약속을 이행할 수 있는 객체이기만 한다면 마음대로 약속된 기능을 호출하는 방식이다.


3) 인터페이스는 새로운 기능인가?

프로그램을 설계하고 조금 더 유연한 프로그램을 만드는 기법이다. 인터페이스는 상속과 더불어서 다형성이라는 객체지향 프로그래밍의 특징을 구현하는 방식이다.


2. 인터페이스를 사용하는 상황

(1) 당신과 친구가 서로 약속을 한다면: 객체의 스펙

A가 사용할 객체를 만들어준다면 아마도 B는 A가 프로그래밍이 다 완료될 때까지 기다리고 있어야만 한다. 처음부터 A가 어떤 메소드를 만들 것인지를 먼저 정하는 것이다. 어떤 메소드에는 어떤 파라미터를 받게 할 것이고, 어떤 메소드에는 어떤 리턴값을 이용할 것이다 등 이런 논의를 먼저하게 되면 B는 적어도 A가 만든 클래스와 객체가 어떤 메소드를 가졌는지 알게 되고, 바로 당장은 아니더라도 프로그램을 개발하는데 도움이 될 것이다.

우리가 해야 하는 일은 정확하게 구분하고, 상대방에 해야 할 일을 명시해주는 작업을 해야 한다면 인터페이스를 이용해야 한다고 생각하면 된다. 이러한 의미에서 인터페이스는 일종의 객체의 계약서(스펙)라는 의미가 있다.


(2) 오늘 점심 뭐 먹지?: 스펙을 만족하는 객체

여러 객체 중에서 여러분이 원하는 기능을 가진 객체의 스펙을 만족하게 한다는 객체를 만들어주고자 하는 것이 인터페이스이다. 즉 어떤 객체가 여러분이 선택한 기준의 기능들을 다 구현하고 있다고 생각하면 된다.

따라서 인터페이스를 구현한다는 의미는 어떤 객체가 어떤 기준을 만족하게 한다는 의미로 해석할 수 있다.

(입사 기준에 만족하는 사람은 여러 명이 있을 수 있다. 실행활에서 어떤 기준을 만족하듯이 객체들이 어떤 기준을 만족하게 하는 장치가 바로 인터페이스의 용도 중의 하나이다.)


(3) 꿩 대신 닭: 현재 객체의 대체물

프로그래밍에서는 어떤 기준에 만족한 객체를 이용해서 프로그래밍을 만들다가 새로운 버전이나 다른 객체로 변경하는 일도 자주 일어난다. 이럴 때 기존의 메소드를 전면 수정하게 되면 결국은 모든 코드의 내용을 수정해야 하는 일이 발생한다.

인퍼페이스를 이용한다는 것은 인터페이스를 구현한 객체들을 하나의 부속품처럼 마음대로 변경해서 사용하는 것이 가능하다는 것이다.

자동차의 순정부품이 있긴 하지만, 때로는 다른 부품을 결합하기도 하는 것처럼, 인터페이스는 시스템이 확장되면서 발생하는 잦은 변경의 문제를 해결하는 데 사용한다.

--> 인터페이스를 코드에 적용하게 되면 실제 객체의 클래스를 몰라도 프로그램을 코딩할 수 있다. 따라서 더 유연한 프로그램을 설계할 수 있다.


(4) 호텔 떡볶이와 길거리 떡볶이: 전혀 다른 객체의 같은 기능

실제로 시내의 모 호텔에 갔더니 떡볶이를 파는 상황을 그려보자.

떡볶이는 길거리 음식이라고 생각했는데 버젓이 호텔의 메뉴판에 있는 것이다. 호텔 요리사와 노점에서 장사하는 사람, 이 두사람은 서로 다른 객체이지만 같은 기능을 할 수 있는 존재들이다. 즉 필요하다면 위에서 말한 하나의 부속품처럼 두 객체를 시스템에서 마음대로 사용할 수 있어야만 한다.

인터페이스는 하나의 기능에 대한 약속이기 때문에 중요한 점은 어떤 객체이든 간에 그 약속을 지키기만 한다면 필요한 곳에서 사용할 수 있게 한다는 것을 의미한다.


3. 문법으로 알아보는 인터페이스

(1) 인터페이스는 실제 객체를 의미하지 않는다.

인터페이스는 그 자체가 객체를 의미하지 않는다. 인터페이스라는 것은 결국은 어떤 객체가 할 수 있는 기능 자체를 의미하고, 그 기능을 하나의 스펙으로 모은 것에 불과하다. 따라서 인터페이스가 실제 기능을 가지는 것이 아니다. 즉 실제로 구현된 코드를 가지지 않았다는 것이다. 인터페이스는 실제로 구현된 코드 대신에 오로지 추상 메소드와 상수만을 가지고 있게 된다.


(2) 인터페이스의 상수는 private으로 만들 수 없다.

인터페이스는 실제 객체는 아니지만 서로 간의 약속으로 사용된다. 정해진 약속을 한 쪽에서 일방적으로 수정하게 되면 문제가 발생할 수 있다. 인터페이스는 객체와 객체의 중간에 놓이기 때문에 인터페이스에 선언하는 변수는 자동으로 'public static final'이 된다. 즉 완벽한 상수로 정의된다. 반면에 private는 객체를 만들 때 외부 클래스에서 접근할 수 없게 하려고 사용하기 때문에 외부로 공개되기 위해서 사용하는 인터페이스에는 맞지 않는다. 그래서 실제로 인터페이스에 private으로 시작하는 변수는 선언할 수 없다.


(3) 인터페이스에는 추상 메소드만 존재한다.

인터페이스는 실제 객체로 만들어지지 않는다. 즉 우리가 원하는 어떤 기능들을 모아서 하나의 인터페이스로 선언하는 것이 가장 일반적으로 사용되는 용도이기 때문에 인터페이스는 ‘기능의 묶음’아고 해석하는 것이 편리하다. 상속을 영어로 해석할 때 ‘is a relation'이라고 한다면, 인터페이스는 ’has a relation'으로 해석된다. 즉 어떤 객체가 어떤 인터페이스를 구현했다는 것은 인터페이스에서 정의한 기능들을 그 객체가 모두 구현해두었다는 것을 의미한다. 따라서 실제 객체는 모든 메소드를 구현했겠지만, 인터페이스 자체에는 메소드의 선언만 들어 있게 된다.


(4) 인터페이스는 객체의 타입으로만 사용된다.

인터페이스는 실제 객체로 사용되지는 않지만, 객체의 타입으로는 사용된다. 이 말은 상속에서와 같이 변수 선언 시에 객체의 타입으로 인터페이스를 사용할 수 있다는 것을 의미한다.

--> 인터페이스 a = new 인터페이스 구현 객체( );

이 경우에 컴파일러는 실제로 변수를 사용할 때 변수의 타입만을 보기 때문에 a라는 변수를 이용해서 객체의 메소드를 호출하는 작업을 실행하면 컴파일러는 인터페이스에 호출하려는 메소드가 있는지만을 따지게 된다.

상속에서 타입이 부모 클래스일 때 컴파일러가 부모 클래스에 선언된 메소드만 따지는 것과 같은 방식이라고 생각하자.

인터페이스 역시 객체의 타입으로 선언될 수 있기 때문에 컴파일러는 변수의 타입에 메소드가 존재하는지만 따지게 되고, 실제로 호출되는 것은 실행되고 있는 객체의 메소드가 호출된다.

cf.) implements은 어떤 기준을 구현한다고 보자


4. 인터페이스의 작성

인터페이스를 작성하는 것은 클래스를 작성하는 것과 같다. 다만 키워드로 class대신 interface를 사용한다는 것만 다르다. 그리고 interface에도 클래스와 같이 접근제어자로 public 또는 default를 사용할 수 있다.

interface 인터페이스 이름{ // 인터페이스의 선언

public static final 타입 상수이름 = 값; // 상수 선언부

public abstract 메서드 이름(매개변수 목록); // 추상 메소드 선언부

}


일반적인 클래스의 멤버들과 달리 인터페이스의 멤버들은 다음과 같은 제약사항을 가지고 있다.

- 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.

- 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.

cf.) 인터페이스에 정의된 모든 멤버에 예외없이 적용되는 사항이기 때문에 제어자를 생략할 수 있는 것이며, 편의상 생략하는 경우가 많다. 생략된 제어자는 컴파일 시에 컴파일러가 자동적으로 추가해준다.


interface PlayingCard{

public static final int SPADE =4;

final int DIAMOND = 3; // public static final int DIAMOND = 3;

static int HEART =2; // public static int HEART =2;

int CLOVER =1; // public static int CLOVER =1;

public abstract String getCardNumber();

String getCardKind(); // public abstract String getCardKind();

}


5. 인터페이스의 상속

인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와는 달리 다중상속, 즉 여러 개의 인터페이스로부터 상속을 받는 것이 가능하다.

cf.) 인터페이스는 클래스와는 달리 Object클래스와 같은 최고 조상은 없다.

interface Movable{

void move(int x, int y);// 지정된 위치(x ,y)로 이동하는 기능의 메서드

}

inferface Attackable{

void attack(Unit u);// 지정된 대상(u)을 공격하는 기능의 메서드

}

interface Fightable extends Movable, Attackable{   }

클래스의 상속과 마찬가지로 자손 인터페이스는 조상 인터페이스에 정의된 멤버를 모두 상속받는다. 그래서 Fightable 자체에는 정의된 멤버가 하나도 없지만 조상 인터페이스로 부터 상속받은 두 개의 추상메서드 move(int x, int y)와 attack(Unit u)을 멤버로 갖게된다.


6. 인터페이스의 구현

인터페이스도 추상클래스처럼 그 자체로는 인스턴스를 생성할 수 없으며, 추상클래스가 상속을 통해 추상메서드를 완성하는 것처럼, 인터페이스도 자신에 정의된 추상메서드의 몸통을 만들어주는 클래스를 작성해야하는데, 그 방법은 추상클래스가 자신을 상속받는 클래스를 정의하는 것과 다르지 않다. 다만 클래스는 확장한다는 의미의 키워드 'extends'를 사용하지만 인터페이스는 구현한다는 의미의 키워드 implements'를 사용할 뿐이다.

cf.) 만일 구현하는 인터페이스의 메서드 중 일부만 구현한다면, 추상클래스로 선언되어야 한다.


class FighterTest {

public static void main(String[] args) {

Fighter f = new Fighter();


if (f instanceof Unit) {

System.out.println("f는 Unit클래스의 자손입니다.");

}


if (f instanceof Fightable) {

System.out.println("f는 Fightable인터페이스를 구현했습니다.");

}


if (f instanceof Movable) {

System.out.println("f는 Movable인터페이스를 구현했습니다.");

}


if (f instanceof Attackable) {

System.out.println("f는 Attackable인터페이스를 구현했습니다.");

}


if (f instanceof Object) {

System.out.println("f는 Object클래스의 자손입니다.");

}

}

}


class Fighter extends Unit implements Fightable {

public void move(int x, int y) { /* 내용 생략 */ }

public void attack(Unit u) { /* 내용 생략 */ }

}


class Unit {

int currentHP; // 유닛의 체력

int x; // 유닛의 위치(x좌표)

int y; // 유닛의 위치(y좌표)

}


interface Fightable extends Movable, Attackable { }

interface Movable { void move(int x, int y); }

interface Attackable { void attack(Unit u); }

실행결과)

f는 Unit클래스의 자손입니다.

f는 Fightable인터페이스를 구현했습니다.

f는 Movable인터페이스를 구현했습니다.

f는 Attackable인터페이스를 구현했습니다.

f는 Object클래스의 자손입니다.

인터페이스는 상속 대신 구현이라는 용어를 사용하지만, 인터페이스로부터 상속받은 추상메서드를 구현하는 것이기 때문에 인터페이스도 조금은 다른 의미의 조상이라고 할 수  있다.여기서 주의 깊게 봐두어야 할 것은 movable인터페이스에 정의된 void moveful(int x, int y)를 Fighter클래스에서 구현할 때 접근 제어자를 public으로 했다는 것이다.


7. 인터페이스 관련 주제


(1) 다중 구현은 다중 타입으로 선언할 수 있다.

클래스의 선언에 implements 인터페이스 구문이 나온다는 얘기는 해당 클래스가 인터페이스의 메소드들을 실제로 구현하고 있다는 선언이다.

클래스가 여러 개의 인터페이스를 구현한다는 것은 또 다른 의미로는 하나의 객체를 여러 가지의 타입으로 선언하는 것이 가능하다는 것을 의미한다.

즉 인터페이스도 타입으로 사용된다. ‘MyMp3 mp3 = new TonyMp3( );'에서처럼 어떤 클래스가 인터페이스를 구현하게 되면 변수 선언 시에 인터페이스를 타입으로 사용할 수 있다는 얘기이다. 클래스가 여러 개의 인터페이스를 구현하게 되면 결과적으로 변수의 타입으로도 다양하게 쓰일 수 있다는 것을 의미하게 된다.

ex.) Phone3G phone = new Phone3G( );

      VisualCall v1 = new VisualCall( );

      VoiceCall v2= new VoiceCall( );

--> 변수의 타입으로 보면 실제로 만들어지는 객체는 마찬가지로 Phone3G 이지만 VoiceCall 타입으로도, VisualCall 타입으로도 사용되는 것을 보실 수 있다.


(2) 상속과 인터페이스 차이(다중상속 관련)

상속은 구체적으로 구현된 메소드를 물려주기 때문에 다중 상속을 하게 되면 문제가 생긴다. 하지만, 인터페이스는 스펙(추상 메소드)만을 물려주기 때문에 여러 개의 인터페이스의 같은 메소드를 물려받아도 구현은 실제 구현 클래스 한 곳에서만 한다.

상속과 달리 인터페이스는 하나의 기준이면서 ‘~를 할 수 있는 기능을 가진 객체’의 의미로 해석될 수 있다. 인터페이스 자체가 하나의 클래스가 아니고, 그저 객체를 판단하는 기준이기 때문에 하나의 객체가 여러 가지 기능을 가질 때에는 클래스의 선언 뒤에 여러 개의 인터페이스를 ‘,’를 이용해서 사용할 수 있다.


(3) 인터페이스가 타입으로 쓰일 수 있기에 가능한 일들

인터페이스는 하나의 타입으로 선언되기 때문에 변수의 선언으로 가능한 모든 작업이 고스란히 가능해진다. 우리가 일반적으로 변수를 쓰는 경우는 다음과 같다.

1) 객체를 나중에 다시 사용하기 위해서 변수로 선언하는 경우

cf.) 인터페이스를 변수로 선언하는 경우

인터페이스로 변수를 선언하게 되면 사용하는 입장에서는 보이는 메소드는 인터페이스의 메소드들만 보이게 된다.

인터페이스 변수를 선언하게 되면 뒤에 오는 모든 객체는 간단히 인터페이스만 구현한 객체이면되기 때문에 실제로 new 뒤에 올 수 있는 클래스에 대한 제한이 없게 된다. 따라서 좀 더 시스템이 유연해지는 계기를 마련하게 된다.


2) 메소드의 파라미터나 메소드의 리턴 타입으로 사용하는 경우

메소드의 파라미터나 메소드의 리턴 타입으로 사용되는 경우에는 추상 클래스나 상속을 이용하는 것과 같아진다. 역시 이런 경우에는 상속과는 달리 전혀 무관하지만, 인터페이스에 선언된 기능을 가진 객체이면 되기 때문에 좀 더 확장성 있는 구조가 된다.


3) 배열이나 자료구조를 선언하기 위해서 사용하는 경우

배열이나 자료구조에서 인터페이스 타입으로 선언되는 경우에는 상속의 단점을 보완하는 방식의 설계가 가능해진다. 

--> 인터페이스를 이용하면 이런 작업들이 모두 가능해진다.

- 인터페이스는 전혀 다른 객체를 하나의 타입으로 인식할 수 있게 한다.


cr.)

1) 인터페이스가 실제 객체를 의미하는 것이 아니라 원하는 객체의 스펙을 의미하는 것이다.

2) 어떤 객체가 인터페이스를 구현함에 따라 여러 가지 타입으로 선언될 수 있다는 점이다.

--> 인터페이스 자체가 의미하는 것은 어떤 기능만을 의미하기 때문에 우리가 만든 여러 가지의 클래스에 원하는 인터페이스를 붙여주면 전혀 다른 객체이지만 같은 타입으로 인식할 수 있게 된다.


cp.) 개발하다 예상치 못한 새로운 기능이 추가되는 경우가 있다. 이렇게 전혀 다른 객체들의 공통적인 문제를 해결하는 데 있어서 인터페이스가 도움을 줄 수 있다.

-->  인터페이스는 하나의 타입으로 이전에 전혀 관계가 없는 클래스를 하나의 타입으로 볼 수 있게 한다. 따라서 전혀 엉뚱한 데이터를 가진 객체들을 하나의 자료구조와 같은 타입으로 묶어줄 수 있다.

1) 공통적인 기능만을 인터페이스로 정의해버린다.

가장 먼저 해야 할 일은 공통적인 기능을 인터페이스로 정의하는 것이다. 예를 들어 4가지의 클래스가 결국 매달 지급해야 하는 돈을 계산해주는 기능이 필요하다고 판단된다. 따라서 Ipayable

이라는 인터페이스를 정의하도록 한다.(4가지 클래스가 공통적인 기능을 갖는 인터페이스)

2) 공통 기능을 필요한 클래스가 정의하도록 클래스의 선언을 수정한다.

이제 필요한 클래스가 공통 기능을 할 수 있도록 수정해주어야 한다. 즉 전혀 관계없는 객체들을 공통된 기능을 구현한 객체로 볼 수 있게 한다는 것이다.

3) 인터페이스 타입으로 여러 종류의 객체를 처리할 수 있다.

4가지의 클래스가 동일한 인터페이스를 구현 했다면 전혀 관계가 없는 4가지 클래스의 객체들을 하나의 타입으로 볼 수 있도록 하는 중간 적인 역할이 필요할 때 사용된다.

ex.) IPayable[] arr = new Ipayable( );

    arr[0] = new RegularWorker( );

    arr[1] = new ContractWorker( );

    arr[2] = new PartTimer( );

    arr[3] = new RentalPay( );


(4) 인터페이스는 일종의 접근 제한 역할도 할 수 있다.

예를 들어 실제 MP3가 가진 기능은 playMovie( ), viewImage( ) 기능이지만, 외부에 인터페이스만 호출하게 되면 호출하는 쪽에서는 이 객체를 어떤 타입으로 보는지에 따라서 사용할 수 있는 메소드가 제한되나. 즉, PlayMovie m = new MP3(); 로 보면 실제로 MP3 객체가 가진 기능은 이미지를 보는 기능 viewImage( ) 와 동영상을 보는 기능인 viewImage( )이지만 변수의 선언에 의해서 사용할 수 있는 메소드는 playMovie( ) 만 보이게 된다. 이런 이유 때문에 클래스에 여러 가지 메소드를 만들어 둔 다음 인터페이스로 분리하는 작업을 진행하는 경우가 가끔 있다.

-->  A,B,C라는 인터페이스를 구현한 클래스를 반환할 때 A타입으로 변환하게 되면 외부에서는 A

인터페이스의 메소드만 보이게 된다. 따라서 별도의 접근 제한을 이용하지 않고도 접근 제한과 마찬가지 효과를 보게 하는 방법이다.


- 인터페이스는 다중 상속 문법도 된다.

인터페이스는 다중 구현을 통해서 하나의 객체가 여러 가지의 인터페이스를 구현할 수 있도록 하고 있다.

-->  인터페이스의 다중 상속은 여러 개의 스펙을 모아서 하나의 스펙을 만들어내는 일종의 조합방식이다.

-->  인터페이스의 다중 상속은 ‘스펙+스펙=새로운 스펙’으로 이해하자

만약 각각 한 가지씩의 기능을 가진 인터페이스를 하나의 제품의 스펙으로 규정하길 원할 때 다중상속을 사용한다.


ex.) interface PerfectPhone extends Camera, Mp3, DMB, Widget 

-->  이렇듯 새로운 규격을 만들어 주었다면 이제 PerfectPhone 하나만 물려받으면 끝난다.( 굳이 4가지의 인터페이스를 다 extends할 필요 없다.)


cf.) 인터페이스는 하나의 타입이나 규격일 뿐이지 그 자체가 하나의 객체가 되는 것이 아니다. 따라서 인터페이스의 상속은 클래스의 상속처럼 부모의 속성과 동작을 물려받는 것이 아니다. 인터페이스의 상속은 규격이나 스펙 자체 혹은 기능 자체의 선언을 물려받은 것이다. 규격이나 스펙을 물려받아서 새로운 스펙을 만든다면 기존 여러 개의 스펙을 조합해서 하나로 묶거나 기존의 스펙을 고스란히 물려받은 후에 다시 추가적인 기능을 가지게 하는 것이다.(extends는 상속이 아니다.)


cf.) 인터페이스에서 가장 중요한 사실은 인터페이스를 통해서 객체와 객체 간의 메시지를 주고받는다는 것이다.


(5) 인터페이스는 하나의 클래스에 여러 개를 붙여줄 수 있다.

인터페이스는 객테와 관련된 것이 아니라 객체가 할 수 있는 기능에 대한 정의이기 때문에 상속과는 달리 하나의 객체가 여러 가지의 기능의 스펙을 구현하는 것도 이상한 일이 아니다.

즉 java에서는 단일 상속만을 지원 했던 것과는 달리 하나의 객체는 여러 가지 종류의 기능 스펙인 인터페이스를 구현 할 수 있다.

ex.) 복합기가 프린트도 되고, 팩스도 되고, 복사도 되는 모습(하나의 객체가 여러 개의 기능 스펙을 구현)

복사기 a = new A기계( );

팩스 a = new A기계( );

프린트 a = new A기계( );

-->  하나의 객체를 여러 타입(모습)으로 보는 것이 다형성의 핵심이다.

-->  A 기계(객체)가 여러 가지의 인터페이스를 구현하는 형태이다.

-->  인터페이스 위주로 프로그램을 설계한다는 것은 만들어야 하는 기능에 대한 기준을 미리 잡는다는 것과 유사어 정도라고 생각된다.


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

[Java] 익명클래스  (0) 2014.12.23
[Java] public static void main(String [] args)  (0) 2014.12.23
[Java] 인터페이스와 다형성  (1) 2014.12.21
[Java] 다형성  (0) 2014.12.21
[Java] 쓰레드의 동기화  (0) 2014.12.21

[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

[Java] 쓰레드 기본


쓰레드 기본


1. 프로세스와 쓰레드

프로세스란 간단히 말해서 '실행 중인 프로그램'이다. 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.


프로그램 ---------------실행---------------> 프로세스


프로세스는 프로그램을 수행하는 데 필요한 데이터메모리 등의 자원 그리고 쓰레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다.

그래서 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 가진 프로세스를 멀티쓰레드 프로세스라고 한다.

(쓰레드를 프로세스라는 작업공간(공장)에서 작업을 처리하는 일꾼으로 생각하자)


싱글쓰레드 프로세스 = 자원 + thread

멀티쓰레드 프로세스 = 자원 + thread + thread+..........


하나의 프로세스가 가질 수 있는 쓰레드의 개수는 제한되어 있지 않으나 쓰레드가 작업을 수행하는데 개별적인 메모리공간(호출 스택)을 필요로 하기 때문에 프로세스의 메모리 한계(호출 스택의 크기)

에 따라 생성할 수 있는 쓰레드의 수가 결정된다. 실제로는 프로세스의 메모리 한계에 다다를 정도로 많은 쓰레드를 생성하는 일은 없을 것이니 이에 관해서는 걱정하지 않아도 된다.

현재 우리가 사용하고 있는 윈도우나 유닉스를 포함한 대부분의 OS는 멀티태스킹(다중 작업)을 지원하기 때문에 여러 개의 프로세스가 동시에 실행될 수 있다.

멀티캐스팅과 마찬가지로 멀티쓰레딩은 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것이 가능하다. 실제로는 한 개의 CPU가 한 번에 단 한가지 작업만 수행할 수 있기 때문에 아주 짧은 시간동안

여러 작업을 번갈아 가면 수행함으로써 동시에 여러 작업이 수행되는 것처럼 보이게 하는 것이다. 그래서 프로세스의 성능이 쓰레드의 개수에 비례하지 않으며, 하나의 쓰레드를 가진 프로세스 보다 두 개의 쓰레드를 가진 프로세스가 오히려 더 낮은 성능을 보일 수 있다.

도스와 같이 한번에 한가지 작업만 할 수 있는 OS와 윈도우와 같은 멀티태스킹이 가능한 OS의 차이는 이미 경험으로 잘 알 수 있을 것이다. 싱글쓰레드 프로그램과 멀티쓰레드 프로그램의 차이도 이와 같다고 생각하면 된다.


멀티쓰레딩의 장점

1 CPU의 사용률을  향상시킨다.

2 자원을 보다 효율적으로 사용할 수 있다.

3 사용자에 대한 응답성이 향상된다.

4 작업이 분리되어 코드가 간결해진다.


메신저의 경우 채팅하면서 파일을 다운로드 받거나 음성대화를 나눌 수 있는 것이 가능한 이유가 바로 멀티쓰레드로 작성되어 있기 때문이다. 만일 싱글쓰레드로 작성되어 있다면 파일을 다운로드 받는 동안에는 채팅을 할 수 없을 것이다. 여러 사용자에게 서비스를 해주는 서버 프로그램의 경우 멀티쓰레드로 작성하는 것은 필수적이어서 하나의 서버프로세스가 여러 개의 쓰레드를 생성해서 쓰레드와 사용자의 요청이 일대일로 처리되도록 멀티쓰레드로 프로그래밍해야 한다. 만일 싱글쓰레드로 서버프로그램을 작성한다면 사용자의 요청 마다 새로운 프로세스를 생성해야하는데 프로세스를 생성하는 것은 쓰레드를 생성하는 것에 비해 훨씬 더 많은 시간과 메모리공간을 필요하기 때문에 많은 수의 사용자 요청을 서비스하기 어렵다.

--> 쓰레드를 가벼운 프로세스 즉, 경량 프로세스하고 부르기도 한다.


그러나 멀티쓰레딩에 장점만 있는 것이 아니어서 멀티쓰레드 프로세스는 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에 발생할 수 있는 동기화, 교착상태(deadlock)와 같은 문제들을 고려해서 신중히 프로그래밍해야한다. 


2. 쓰레드의 구현과 실행

쓰레드를 구현하는 방법은 Thread클래스를 상속받는 방법 Runnable 인터페이스를 구현하는 방법, 모두 2가지가 있다.

그러나 Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에, Runnable 인터페이스를 구현하는 방법이 일반적이다.

1) Thread 클래스를 상속

class MyThread extends thread{

public void run(){/*작업 내용*/}// Thread 클래스의 run()을 오버라이딩

}


2) Runnable 인터페이스를 구현

class MyThread implements Runnable{

public void run(){/*작업 내용*/}// Runnable인터페이스의 추상메서드 run()을 구현

}

Runnable 인터페이스는 run()메서드만 정의되어 간단한 인터페이스이다. Runnable 인터페이스를 구현하기 위해서 해야 할 일은 추상 메서드인 run()의 몸통을 만들어 주는 것일 뿐이다.


class ThreadEx1 {

public static void main(String args[]) {

A t1 = new A();


Runnable r = new B();

Thread t2 = new  Thread(r); // 생성자 Thread(Runnable target)

// Runnable 인터페이스를 구현한 경우, Runnable 인터페이스를 구현한 클래스의 인스턴스를 생성한 다음, 이 인스턴스를 가지고 Thread 클래스의 인스턴스를 생성할 때 생성자의 매개변수로 제공해야 한다.

// 이 때 사용되는 Thread 클래스의 생성자는 Thread(Runnable target)로 호출시에 Runnable인터페이스를 구현한 클래스의 인스턴스를 넘겨줘야 한다.

t1.start();// 쓰레드를 생성한 다음에는 start()를 호출해야한 비로소 작업을 시작하게 된다.

t2.start();

//한 번 사용한 쓰레드는 다시 재사용할 수 없다. 즉 하나의 쓰레드에 대해 start()가 한 번만 호출될  수 있다는 뜻이다.

//그래서 쓰레드의 작업이 한 번 더 수행되기를 원한다면 오른쪽의 코드와 같이 새로운 쓰레드를 생성한 다음에 start()를 호출해야 한다.

}

}


class A extends Thread {

public void run() {

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

System.out.println(getName()); // 조상인 Thread의 getName()을 호출, 즉 쓰레드의 이름을 반환한다.

}

}

}


class B implements Runnable {

public void run() {

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

// Thread.currentThread() - 현재 실행중인 Thread를 반환한다.

     System.out.println(Thread.currentThread().getName());

    // Thread 클래스를 상속받으면, Thread 클래스의 메서드를 직접 호출할 수 있지만, Runnable을 구현하면 Thread클래스의 static 메서드인 currentThread()를 호출하여 쓰레드에 대한 참조를 얻어 

    // 와야만 호출이 가능하다.

}

}

}

실행결과)

Thread-0

Thread-0

Thread-0

Thread-0

Thread-0

Thread-1

Thread-1

Thread-1

Thread-1

Thread-1

(실행결과는 그때그때 달라요!~)

cf.) 인스턴스 변수로 Runnable타입의 변수 r을 선언해 놓고 생성자를 통해서 Runnable 인터페이스를 구현한 인스턴스를 참조하도록 되어 있는 것을 확인할 수 있다. 그리고 run()을 호출하면 참조변수 r을 통해서 Runnable 인터페이스를 구현한 인스턴스의 run()이 호출된다. 이렇게 함으로써 상속을 통해 run()을 오버라이딩하지 않고도 외부로부터 제공받을 수 있다.


3. start()와 run()

쓰레드를 실행시킬 때 run()이 아닌 start()를 호출한다는 것에 대해서 다소 의문을 들었을 것이다.

run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 속한 메서드 하나를 호출하는 것이다.

반면에 start()는 새로운 쓰레드가 작업을 실행하는데  필요한 호출스택을 생성한 다음에 run()을 호출해서, 생성된 호출스택에 run()이 첫 번째로 저장되게 한다.

모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 하기 때문에, 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고 쓰레드가 종료되면 작업에 사용된 호출스택은 소멸된다.


(1) start()와 run() - 쓰레드의 호출 구조

main()에서 start()호출 -> start()에서 호출스택 생성 -> 호출스택에서 run()호출하여 쓰레드 작업수행 ->

호출스택이 2개이므로 스케쥴러가 정한 순서에 의해서 번갈아가며 작업 수행. 


1 main 메서드에서 쓰레드의 start메서드를 호출한다.

2 start 메서드는 쓰레드가 작업을 수행하는데 사용될 새로운 호출스택을 생성한다.

3 생성된 호출스택에 run 메서드를 호출해서 쓰레드가 작업을 수행하도록 한다.

4 이제는 호출스택이 2개이기 때문에 스케줄러가 정한 순서에 의해 번갈아 가면서 실행된다.


호출스택에서는 가장 위에 있는 메서드가 현재 실행중인 메서드이고 나머지 메서드들은 대기상태에 있다.

그러나 쓰레드가 둘 이상일 때는 호출스택의 최상위에 있는 메서드 일지라도 대기상태에 있을 수 있다는 것을 알 수 있다.

스케줄러는 시작되었지만 아직 종료되지 않은 쓰레드들의 우선순위를 고려하여 실행 순서와 실행 시간을 결정하고 각 쓰레드들은 작성된 스케줄에 따라 자신의 순서가 되면 지정된 시간동안 작업을 수행한다.

cf.) start()가 호출된 쓰레드는 바로 실행되는 것이 아니라는 것에 주의하자. 일단 대기상태로 있다가 스케줄러가 정한 순서에 의해서 실행된다.


이때 주어진 시간동안 작업을 마치지 못한 쓰레드는 다시 자신의 차례가 돌아올 때 까지 대기상태에 있게 되며, 작업을 마친 쓰레드, 즉 run()의 수행이 종료된 쓰레드는 호출스택이 모두 비워지면서 이 쓰레드가 사용하던 호출스택은 사라진다.

자바프로그램을 실행하면 호출스택이 생성되고 main 메서드가 처음으로 호출되고 main 메서드가 종료되면 호출스택이 비워지고 프로그램이 종료되는 것과 정확히 일치한다.

쓰레드는 일꾼이다. 프로그램이 실행되기 위해서는 작업을 수행하는 일꾼이 최소한 하나는 필요하다. 그래서 프로그램을 실행하면 기본적으로 하나의 쓰레드(일꾼)를 생성하고, 그 쓰레드가 main 메서드를 호출해서 작업이 수행되도록 하는 것(main 메서드의 작업을 수행하는 것도 쓰레드이다.)이다.

지금까지는 main 메서드가 수행을 마치면 프로그램이 종료되었으나, main 메서드가 수행을 마쳤다하더라도 다른 쓰레드가 아직 작업을 마치지 않은 상태라면 프로그램이 종료되지 않는다.(실행중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.)


class ThreadEx2 {

public static void main(String args[]) throws Exception {

MyThreadEx2_1 t1 = new MyThreadEx2_1();

t1.start();

}

}


class MyThreadEx2_1 extends Thread {

public void run() {

throwException();

}


public void throwException() {

try {

throw new Exception();

} catch(Exception e) {

e.printStackTrace();

}

}

}

실행결과)

java.lang.Exception

at MyThreadEx2_1.throwException(ThreadEx2.java:15)

at MyThreadEx2_1.run(ThreadEx2.java:10)

새로 생성한 쓰레드에서 고의로 예외를 발생시키고 printStackTrace()를 이용해서 예외가 발생한 당시의 호출스택을 출력하는 예제이다.

호출스택의 첫번째 메서드가 main 메서드가 아니라 run 메서드인 것을 확인하자.

그리고 한 스레드에서 예외가 발생해서 종료되어도 다른 쓰레드의 실행에는 영향을 미치지 않는다.


class ThreadEx3 {

public static void main(String args[]) throws Exception {

MyThreadEx3_1 t1 = new MyThreadEx3_1();

t1.run();

}

}


class MyThreadEx3_1 extends Thread {

public void run() {

throwException();

}


public void throwException() {

try {

throw new Exception();

} catch(Exception e) {

e.printStackTrace();

}

}

}

실행결과)

java.lang.Exception

at MyThreadEx3_1.throwException(ThreadEx3.java:15)

at MyThreadEx3_1.run(ThreadEx3.java:10)

at ThreadEx3.main(ThreadEx3.java:4)

이 예제 역시 고의적으로 예외를 발생시켜서  호출스택의 내용을 확인할 수 있도록 했다. 이전 예제와는 달리 main 메서드가 호출스택에 포함되어 있음을 확인하자.


4. 핵심

1 Thread를 상속받는 클래스는 독립적인 흐름을 갖는 하나의 쓰레드로서의 역할을 수행한다. 

2 run()는 main() 쓰레드와 별개의 독립적인 실행 흐름을 갖는다. 또는 별개의 움직이는 실행 위치이다.

3 run()만하면 단순히 메소드 호출에 불과하다.--> 독립적인 실행을 하는 쓰레드가 아니다.

4 Runnable 인터페이스에는 start()라는 메서드가 없다. 그래서 Runnable을 매개변수로 갖는 Thread 생성자를 사용한다.  객체를 Thread 생성자의 매개변수로 담아 버리면 Thread 클래스와 같이 사용할 수 있다.

5 main () 쓰레드를 실행하면 main 메서드만 실행하는 클래스가 되나, 쓰레드이나, 만약 main() 하고 마치 동시에 실행되는 듯한 클래스를 만들고 싶다면 멀티쓰레드의 개념이 성립된다.

6 멀티쓰레드를 사용하면 여러개의 프로세스가 동시에 실행하는 듯한 모습을 보인다.

7 쓰레드라는 것은 프로세스이기 때문에 준비하는데 약간의 시간이 필요하다. t1.run()의 경우 단순히 메서드 호출하므로 시간이라는 것이 필요한것이 아니다.


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

[Java] 쓰레드 그룹  (0) 2014.12.21
[Java] 쓰레드의 우선순위  (0) 2014.12.21
[Java] 직렬화  (0) 2014.12.17
[Java] File 클래스  (0) 2014.12.16
[Java] 문자 기반 스트림  (0) 2014.12.16

[Java] 직렬화


직렬화


컴퓨터에 저장했다가 다음에 다시 꺼내 쓸 수는 없을지 또는 네트웍을 통해 컴퓨터 간에 서로 객체를 주고 받을 수는 없을까라고 고민해 본 적이 있는가? 과연 이러한 일들이 가능할까?

가능하다. 이러한 것을 직렬화가 처리해준다.


1. 직렬화란


직렬화(스트림으로)란 객체를 데이터 스트림으로 만드는 것을 뜻한다. 즉, 객체에 저장된 데이터를 스트림에 쓰기 위해 연속적인 데이터로 변환하는 것을 말한다.

반대로 스트림으로부터 데이터를 읽어서 객체를 만드는 것을 역직렬화(객체로)라고 한다.

객체 스트림은 프로그램 메모리상에 존재하는 객체를 직접 입출력해 줄 수 있는 스트림으로 현재 상태를 보존하기 위한 영속성을 지원할 수 있다.

자바에서 객체 안에 저장되어 있는 내용을 파일로 저장하거나 네트워크를 통하여 다른 곳으로 전송하려면 객체를 바이트 형태로 일일이 분해해야 한다. 

이를 위하여 객체를 직접 입출력 할 수 있도록 해주는 객체 스트림이다.


직렬화(Serialization)

 - 객체를 데이터스트림(스트림에 쓰기(write)위한 연속적인(serial) 데이터)으로 만드는 것.

 - 예) 객체를 컴퓨터에 저장했다가 꺼내 쓰기. 네트워크를 통한 컴퓨터 간의 객체 전송.  


역직렬화(Deserialization)

 - 스트림으로부터 데이터를 읽어서 객체를 만드는 것. 



사실 객체를 저장하거나 전송하려면 당연히 직렬화를 거칠수 밖에 없다.

객체를 저장한다는 것이 무엇을 의미하는지 상기시켜야 한다.

객체는 클래스에 정의된 인스턴스변수의 집합이다. 객체에는 클래스변수나 메서드가 포함되지 않는다. 객체는 오직 인스턴스변수들로만 구성되어 있다.

인스턴스변수는 인스턴스마다 다른 값을 가질 수 있어야하기 때문에 별도의 메모리공간이 필요하지만 메서드는 변하는 것이 아니라서 메모리를 낭비해 가면서 인스턴스마다 같은 내용의 코드를 포함시킬 이유가 없다.



위의 그림은 6장에 나오는 Tv클래스의 객체가 생성되었을 때 사용한 그림인데, 왼쪽 그림은 이해를 돕기 위해 인스턴스에 메서드를 포함시켜서 그렸지만, 실제로는 오른쪽 그림과 같이 인스턴스에 메서드가 포함되지 않는것이 더 정확한 그림이다. 그래서 객체를 저장한다는 것은 바로 객체의 모든 인스턴스변수의 값을 저장한다는 것과 같은 의미이다. 어떤 객체를 저장하고자 한다면, 현재 객체의 모든 인스턴스변수의 값을 저장하기만 하면된다. 그리고 저장했던 객체를 다시 생성하려면, 객체를 생성한 후에 저장했던 값을 읽어서 생성한 객체의 인스턴스변수에 저장하면 되는 것이다.

클래스에 정의된 인스턴스변수가 단순히 기본형일 때는 인스턴스변수의 값을 저장하는 일이 간단하지만. 인스턴스변수의 타입이 참조형일 때는 그리 간단하지 않다. 예를 들어 인스턴스변수의 타입이 배열이라면 배열에 저장된 값들도 모두 저장되어야 할 것이다. 그러나 우리는 객체를 어떻게 직렬화해야 하는지 전혀 고민하지 않아도 된다. 다만 객체를 직렬화/역직렬화할 수 있는 ObjectInputStream과 ObjectOutputStream을 사용하는 방법만 알면 된다.

--> 두 객체가 동일한지 판단하는 기준이 두 객체의 인스턴스변수 값들이 같고 다름이라는 것을 기억하자.


2. ObjectInputStream(직렬화) / ObjectOutputStream(역직렬화)

직렬화(스트림에 객체를 출력)에는 ObjectInputStream을 사용하고 역직렬화(스트림으로부터 객체를 입력)에는 ObjectOutputStream을 사용한다.

ObjectInputStream과 ObjectOutputStream은 각각 InputStream / OutputStream을 직접 상속받지만 기반스트림을 필요로하는 보조스트림이다. 그래서 객체를 생성할 때 입출력(직렬화/역직렬화)할 스트림을 지정해주어야 한다.


만일 파일에 객체를 저장(직렬화)하고 싶다면 다음과 같이 해야 한다.


※ UserInfo 객체를 직렬화하여 저장

FileOutputStream fos = new FileOutputStream("objectfile.ser");

ObjectOutputStream out = new ObjectOutputStream(fos);

out.writeObject(new UserInfo());

--> objectfile.ser이라는 파일에 UserInfo객체를 직렬화하여 저장한다. 출력할 스트림(FileOutputStream)을 생성해서 이를 기반 스트림으로 하는 ObjectOutputStream을 생성한다.

ObjectOutputStream의 writeObject(Object obj)를 사용해서 객체를 출력하면, 객체가 파일에 직렬화되어 저장된다. 


※ UserInfo 객체를 역직렬화

FileInputStream fis = new FileInputStream("objectfile.ser");

ObjectInputStream in = new ObjectInputStream(fis);

UserInfo Info = (UserInfo)in.readObject();

--> 직렬화할 때와는 달리 입력스트림을 사용하고 writeObject(Object obj)대신 readObject()를 사용하여 저장된 데이터를 읽기만 하면 객체로 역직렬화된다.

다만, readObject()의 반환타입이 Object이기 때문에 객체  원래의 타입으로 형변환 해주어야 한다.


(1) 객체 전송의 단계

객체를 분해하여 전송하기 위해서는 직렬화(Serialization) 되어야 한다.

객체를 전송하기 위해서는 3가지 단계를 거친다.

1) 직렬화된 객체를 바이트 단위로 분해한다. (marshalling)

2) 직렬화 되어 분해된 데이터를 순서에 따라 전송한다.

3) 전송 받은 데이터를 원래대로 복구한다. (unmarshalling)


(2) 마샬링 (marshalling)

마샬링(marshalling)은 데이터를 바이트의 덩어리로 만들어 스트림에 보낼 수 있는 형태로 바꾸는 변환 작업을 뜻한다.

자바에서 마샬링을 적용할 수 있는 데이터는 원시 자료형(boolean, char, byte, short, int, long, float, double)와 객체 중에서 Serializable 인터페이스를 구현한 클래스로 만들어진 객체이다. 객체는 원시 자료형과 달리 일정한 크기를 가지지 않고 객체 내부의 멤버 변수가 다르기 때문에 크기가 천차만별로 달라진다. 이런 문제점을 처리할 수 있는게 ObjectOutputStream 클래스이다.


(3) 직렬화 (Serializable)

마샬링으로 바이트로 분해된 객체는 스트림을 통해서 나갈 수 있는 준비가 되었다. 앞에서 언급한대로 객체를 마샬링하기 위해서는 Serializable 인터페이스를 구현한 클래스로 만들어진 객체에 한해서만 마샬링이 진행될 수 있다. Serializable 인터페이스는 아무런 메소드가 없고 단순히 자바 버추얼 머신에게 정보를 전달하는 의미만을 가진다.


* 직렬화가 가능한 객체의 조건

1) 기본형 타입(boolean, char, byte, short, int, long, float, double)은 직렬화가 가능

2) Serializable 인터페이스를 구현한 객체여야 한다. (Vector 클래스는 Serializable 인터페이스구현)

3) 해당 객체의 멤버들 중에 Serializable 인터페이스가 구현되지 않은게 존재하면 안된다.

4) transient 가 사용된 멤버는 전송되지 않는다. (보안 변수 : null 전송)


(4) 언마샬링 (unmarshalling)

언마샬링은 객체 스트림을 통해서 전달된 바이트 덩어리를 원래의 객체로 복구하는 작업이다. 이 작업을 제대로 수행하기 위해서는 반드시 어떤 객체 형태로 복구할지 형 변환을 정확하게 해주어야 한다.

Vector v = (Vector)ois.readObject(); 

// OutputInputStream의 객체를 읽어서 Vector 형으로 형변환 한다.

이때 ObjectInputStream을 사용하여 데이터를 복구한다.



import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;


public class ObjectStream {

public static void main(String[] args){

// ObjectOutputStream 을 이용한 객체 파일 저장

FileOutputStream fos = null;

ObjectOutputStream oos = null;

// UserClass 에 이름과 나이를 입력하여 객체를 3개 생성한다.

UserClass us1 = new UserClass("하이언", 30);

UserClass us2 = new UserClass("스티브", 33);

UserClass us3 = new UserClass("제이슨", 27);

try{

// object.dat 파일의 객체 아웃풋스트림을 생성한다.

fos = new FileOutputStream("object.dat");

oos = new ObjectOutputStream(fos);

// 해당 파일에 3개의 객체를 순차적으로 쓴다

oos.writeObject(us1);

oos.writeObject(us2);

oos.writeObject(us3);

// object.dat 파일에 3개의 객체 쓰기 완료.

System.out.println("객체를 저장했습니다.");

}catch(Exception e){

e.printStackTrace();

}finally{

// 스트림을 닫아준다.

if(fos != null) try{fos.close();}catch(IOException e){}

if(oos != null) try{oos.close();}catch(IOException e){}

}

// 파일로 부터 객체 데이터 읽어온다.

FileInputStream fis = null;

ObjectInputStream ois = null;

try{

// object.dat 파일로 부터 객체를 읽어오는 스트림을 생성한다.

fis = new FileInputStream("object.dat");

ois = new ObjectInputStream(fis);

// ObjectInputStream으로 부터 객체 하나씩 읽어서 출력한다.

// (UserClass) 로 형변환을 작성해야 한다.

// System.out.println 으로 객체의 구현된 toString() 함수를 호출한다.

System.out.println( (UserClass)ois.readObject());

System.out.println( (UserClass)ois.readObject());

System.out.println( (UserClass)ois.readObject());



}catch(Exception e){

e.printStackTrace();

}finally{

// 스트림을 닫아준다.

if(fis != null) try{fis.close();}catch(IOException e){}

if(ois != null) try{ois.close();}catch(IOException e){}

}

}


}

실행결과)

- UserClass 객체를 생성하여 ObjectOutputStream을 통해 object.dat 에 순차적으로 객체를 쓴다.

- UserClass 객체를 Stream에 쓰기위해서는 Serializable 인터페이스를 사용해야 직렬화 할 수 있다.

- Serializable 구현하지 않으면 NotSerializableException이 발생한다.

- ObjectInputStream을 통해 object.dat에 저장되어 있는 객체를 읽어온다.

- ObjectInputStream에서 readObject()로 읽을때는 정확한 형변환을 해주어야 정확하게 언마샬링을 할 수 있다.


3. Serializable 과 transient


(1) Serializable

데이터를 파일에 쓰거나, 네트워크를 타고 다른 곳에 전송할 때는 데이터를 바이트 단위로 분해하여 순차적으로 보내야 한다. 이것을 직렬화(Serialization)라고 한다.

기본 자료형(boolean, char, byte, short, int ,long, float, double)은 정해진 바이트의 변수이기 때문에 바이트 단위로 분해하여 전송한 후 다시 조립하는데 문제가 없다.

하지만 객체의 크기는 가변적이며, 객체를 구성하는 자료형들의 종류와 수에 따라 객체의 크기는 다양하게 바뀔 수 있다. 이런 객체를 직렬화 하기 위해서 Serializable 인터페이스를 구현하게 된다.


* 직렬화가 가능한 객체의 조건

① 기본형 타입(boolean, char, byte, short, int, long, float, double)은 직렬화가 가능

② Serializable 인터페이스를 구현한 객체여야 한다. (Vector 클래스는 Serializable 인터페이스구현)

③ 해당 객체의 멤버들 중에 Serializable 인터페이스가 구현되지 않은게 존재하면 안된다.

④ transient 가 사용된 멤버는 전송되지 않는다. (보안 변수 : null 전송)

객체 직렬화는 객체에 implements Serializable 만 선언해 주면 된다.


(2) transient

하지만, 객체의 데이터 중 일부의 데이터는(패스워드와 같은 보안) 여러가지 이유로 전송을 하고 싶지 않을 수 있다. 이러한 변수는 직렬화에서 제외해야 되며, 이를 위해서 변수에 transient를 선언한다.

또한, 직렬화 조건 중 객체의 멤버들 중에 Serializable 인터페이스 구현되지 않은 객체가 있으면, 직렬화 할 수 없다.(NonSerializableException) 직렬화 해야 되는 객체 안의 객체 중 Serializable 인터페이스가 구현되지 않으면서 전송하지 않아도 되는 객체 앞에는 transient 를 선언해준다. 그러면 직렬화 대상에서 제외되므로 해당 객체는 직렬화가 가능해진다.


public class UserInfo implements Serializable{

    String name;

    String password;

    int age;

     

    Object ob = new Object();   

    // 모든 클래스의 최고조상인 Object는 Serializable을

    // 구현하지 않았기 때문에 직렬화가 불가능하다.

     

    Object obj = new String("abc"); // String은 직렬화될 수 있다.

     

    // 직렬화 제외

    transient String weight;    

    transient Object obe = new Object();

}


cp.) 직렬화하고자 하는 객체의 클래스에 직렬화가 안 되는 객체에 대한 참조를 포함하고 있다면, 제어자 transient를 붙여서 직렬화 대상에서 제외되도록 할 수 있다.

또는 password와 같이 보안상 직렬화되면 안되는 값에 대해서 transient를 사용할 수 있다.

다르게 표현하면 transien가 붙은 인스턴스변수의 값은 그 타입의 기본값으로 직렬화된다고 볼 수 있다.

즉, UserInfo객체를 역직렬화하면 참조변수인 obj와 password의 값은 null이 된다.


※ Serializable 과 transient 사용 예제

UserClass.java

import java.io.Serializable;


// 직렬화 한다.

public class UserClass implements Serializable{

private static final long serialVersionUID = 4220461820168818967L;

String name;

// age 비 전송

transient int age;

// NonSerializable 클래스

NonSerializableClass nonSerializable;

public UserClass() {

}

public UserClass(String name, int age){

this.name = name;

this.age = age;

this.nonSerializable = new NonSerializableClass(false);

}


public String getName() {

return name;

}


public void setName(String name) {

this.name = name;

}


public int getAge() {

return age;

}


public void setAge(int age) {

this.age = age;

}


public NonSerializableClass getNonSerializable() {

return nonSerializable;

}


public void setNonSerializable(NonSerializableClass nonSerializable) {

this.nonSerializable = nonSerializable;

}


@Override

public String toString() {

return "UserClass [name=" + name + ", age=" + age

+ ", nonSerializable=" + nonSerializable + "]";

}

}


public class NonSerializableClass {

boolean serializable;

public NonSerializableClass(){

this.serializable = false;

}

public NonSerializableClass(boolean serializable){

this.serializable = serializable;

}

}


import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;


public class ObjectStream {

public static void main(String[] args){

// ObjectOutputStream 을 이용한 객체 파일 저장

FileOutputStream fos = null;

ObjectOutputStream oos = null;

// UserClass 에 이름과 나이를 입력하여 객체를 3개 생성한다.

UserClass us1 = new UserClass("하이언", 30);

UserClass us2 = new UserClass("스티브", 33);

UserClass us3 = new UserClass("제이슨", 27);

try{

// object.dat 파일의 객체 아웃풋스트림을 생성한다.

fos = new FileOutputStream("object.dat");

oos = new ObjectOutputStream(fos);

// 해당 파일에 3개의 객체를 순차적으로 쓴다

oos.writeObject(us1);

oos.writeObject(us2);

oos.writeObject(us3);

// object.dat 파일에 3개의 객체 쓰기 완료.

System.out.println("객체를 저장했습니다.");

}catch(Exception e){

e.printStackTrace();

}finally{

// 스트림을 닫아준다.

if(fos != null) try{fos.close();}catch(IOException e){}

if(oos != null) try{oos.close();}catch(IOException e){}

}

// 파일로 부터 객체 데이터 읽어온다.

FileInputStream fis = null;

ObjectInputStream ois = null;

try{

// object.dat 파일로 부터 객체를 읽어오는 스트림을 생성한다.

fis = new FileInputStream("object.dat");

ois = new ObjectInputStream(fis);

// ObjectInputStream으로 부터 객체 하나씩 읽어서 출력한다.

// (UserClass) 로 형변환을 작성해야 한다.

// System.out.println 으로 객체의 구현된 toString() 함수를 호출한다.

System.out.println( (UserClass)ois.readObject());

System.out.println( (UserClass)ois.readObject());

System.out.println( (UserClass)ois.readObject());



}catch(Exception e){

e.printStackTrace();

}finally{

// 스트림을 닫아준다.

if(fis != null) try{fis.close();}catch(IOException e){}

if(ois != null) try{ois.close();}catch(IOException e){}

}

}

}


UserClass.java 의 변수를 보면 transient int age; 로 age 변수는 직렬화에서 제외했다.

- NonSerializableClass 객체는 Serializable 인터페이스를 구현하지 않은 클래스이다.

- 따라서 UserClass.java 로 직렬화를 시도하면, 위와 같이 NonSerializableClass Exception이 발생한다.

- 위의 문제를 해결하기 위해서는 NonSerializableClass.java 에 Serializable 인터페이스를 구현하여 직렬화를 할 수 있게 하는 방법과

- NonSerializableClass 를 전송하지 않아도 되면, 또는 않아야 한다면 transient 를 앞에 붙여주는 것이다.

- 그러면 NonSerializableClass 객체는 직렬화 대상에서 제외되면서 UserClass 가 정상적으로 직렬화되어 처리될 것이다.


※ NonSerializableClass 객체 선언 앞에 transient 선언 결과

import java.io.Serializable;


// 직렬화 한다.

public class UserClass implements Serializable{

private static final long serialVersionUID = 4220461820168818967L;

String name;

// age 비 전송

transient int age;

// NonSerializable 클래스

transient NonSerializableClass nonSerializable;

public UserClass() {

}

public UserClass(String name, int age){

this.name = name;

this.age = age;

this.nonSerializable = new NonSerializableClass(false);

}


public String getName() {

return name;

}


public void setName(String name) {

this.name = name;

}


public int getAge() {

return age;

}


public void setAge(int age) {

this.age = age;

}

public NonSerializableClass getNonSerializable() {

return nonSerializable;

}


public void setNonSerializable(NonSerializableClass nonSerializable) {

this.nonSerializable = nonSerializable;

}


@Override

public String toString() {

return "UserClass [name=" + name + ", age=" + age

+ ", nonSerializable=" + nonSerializable + "]";

}

}


- 객체가 정상적으로 직렬화되어 전송되고, 가져와 출력되는 것을 볼 수 있다.

- 당연히 transient가 붙은 age 변수와 nonSerializable 은 직렬화 되지 않기에 데이터가 없다.


4. 직렬화가능한 클래스의 버전관리
직렬화된 객체를 역직렬화할 때 서로 같은 클래스를 사용해야 하는데, 클래스의 이름이 같더라도 클래스의 내용이 변경된 경우 역직렬화가 실패하며 에러가 발생한다.
static 변수나 상수 또는 trasient가 붙은 인스턴스변수의 경우 직렬화에 영향을 미치지 않는다.

 java.io.InvalidClassException: UserInfo; local class incompatible: stream classdesc
 serialVersionUID = 6953673583338942489, local class serialVersion UID = -6256164443556992367 ...
 
해결책) serialVersionUID를 정의한다.
class MyData implements java.io.Serializable{
    static final long serialVersionUID = 3518731767529258119L;
    // 이렇게 추가해주면 클래스의 내용이 바뀌어도 클래스의 버전이 고정된다.
     
    int value;
}
 
serialVersionUID 얻기 (아무 정수를 써도 상관없지만 중복될 가능성때문에 사용하는 편이 좋다.)

cp.) erialVersionUID 이란? Warning 해결하기


객체를 파일에 쓰거나 전송하기 위해서는 직렬화를 해야 하는데 그러기 위해 객체 클래스에 Serializable 인터페이스를 implements 하게 된다.
하지만 Serializable 인터페이스를 implements 하게 되면 노란색 Warning이 발생한다.
The serializable class *** does not declare a static final serialVersionUID field of type long
저렇게 Warning이 발생하지만 동작하는데는 문제가 없다.
그래도 계속 저렇게 Warning이 떠있는데 왜 생기는 것이며 serialVersionUID 는 무엇이길래 없다고 그러는 건가?
serialVersionUID 는 직렬화에 사용되는 고유 아이디인데, 선언하지 않으면 JVM에서 디폴트로 자동 생성된다.
따라서 선언하지 않아도 동작하는데 문제는 없지만, 불안하기 때문에 JAVA에서는 명시적으로 serialVersionUID를 선언할 것을 적극 권장하고 있다.

* JVM에 의한 디폴트 serialVersionUID 계산은 클래스의 세부 사항을 매우 민감하게 반영하기 때문에 컴파일러 구현체에 따라서 달라질 수 있어 deserialization 과정에서 예상하지 못한 InvalidClassException을 유발할 수 있다.

serialVersionUID는 private static final 로 선언하면 된다.

그럼 serialVersionUID는 어떻게 생성하면 될까?
이클립스에서는 serialVersionUID를 자동으로 선언해주는 플러그인 있다.
위의 파일을 다운받고 압축을 풀어서 eclipse\plugin 폴더에 넣어 놓고 이클립스를 재시작 한다.

http://hyeonstorage.tistory.com/attachment/cfile26.uf@25748E385325AEA31EC4FD.zip


serialVersionUID 를 생성하고자 하는 (Serializable을 implements 한) 클래스에 마우스 오른쪽 버튼을 누르면 
아래 그림과 같이 Add SerialVersionUID 가 있다.


Add SerialVersionUID를 클릭하면 serialVersionUID가 생성된다.

앞에 private를 붙여서 private static final long 형태가 되도록 하자.


이제 노란 Warning이 없어지는 것을 볼 수 있다.
Warning을 없애는 방법은 SerialVersionUID 선언 외에 다른 방법이 있다.
클래스 위에 @SuppressWarnings("serial") 이라고 어노테이션 처리를 해주면 없어진다.
하지만 SerialVersionUID를 선언해주는 것이 권장되는 방법이다.


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

[Java] 쓰레드의 우선순위  (0) 2014.12.21
[Java] 쓰레드 기본  (0) 2014.12.21
[Java] File 클래스  (0) 2014.12.16
[Java] 문자 기반 스트림  (0) 2014.12.16
[Java] 바이트 기반의 스트림  (0) 2014.12.16