[Spring] AOP 개요


AOP 개요


Aspect Oriented Programming, 줄여서 AOP는 문제를 바라보는 관점을 기준으로 프로그래밍하는 기법을 말한다. AOP는 문제를 해결하기 위한 핵심 관심 사항과 전제에 적용되는 공통 관심 사항을 기준으로 프로그래밍 함으로써 공통 모듈을 여러 코드에 쉽게 적용할 수 있도록 도와 준다.


AOP를 구현하는 다양한 방법이 존재하지만, 기본적인 개념은 공통 관심 사항을 구현한 코드를 핵심 로직을 구현한 코드 안에 삽입한다는 것이다.



AOP 기법에서는 핵심 로직을 구현한 코드에서 공통 기능을 직접적으로 호출하지 않는다. 핵심 로직을 구현한 코드를 컴파일 하거나, 컴파일 된 클래스를 로딩하거나, 또는 로딩한 클래스의 객체를 생성할 때 AOP가 적용되어 핵심 로직 구현 코드 안에 공통 기능이 삽입된다.


AOP 프로그래밍에서는 AOP 라이브러리 공통 기능을 알맞게 삽입해주기 때문에 개발자는 게시글 쓰기나 목록 읽기와 같은 핵심 로직을 구현할 때 트랜잭션 적용이나 보안 검사와 같은 공통 기능을 처리하기 위한 코드를 핵심 로직 코드에 삽입할 필요가 없다. 핵심 로직을 구현한 코드에 공통 기능 관련 코드가 포함되어 있지 않기 때문에 적용해야 할 공통 기능이 변경되더라도 핵심 로직을 구현한 코드를 변경할 필요가 없다. 단지, 공통 기능 코드를 변경한 뒤 핵심 로직 구현 코드에 적용만 하면 된다.


1. AOP 용어


(1) Advice: 언제 공통 관심 기능을 핵심 로직에 적용할 지를 정의하고 있다. 예를 들어, '메서드를 호출하기 전'(언제)에 '트랜잭션을 시작한다.'(공통기능)기능을 적용한다는 것을 정의하고 있다.

조인 포인트에 삽입되어져 동작할 수 있는 코드를 '어드바이스'라 한다. 관점으로서 분리되고 실행시 모듈에 위빙된 구체적인 처리를 AOP에서는 Advice라고 한다. Advice를 어디에서 위빙하는지는 뒤에 나오는 PointCut이라는 단위로 정의한다. 또한 Advice가 위빙되는 인스턴스를 '대상객체'라고 한다.


cp.) 스프링의 Advice 타입

- Around Advice: Joinpoint 앞과 뒤에서 실행되는 Adcvice

- Before Advice: Joinpoint 앞에서 실행되는 Advice

- After Returning Advice: Jointpoint 메서드 호출이 정상적으로 종료된 뒤에 실행되는 Advice

- After Throwing Advice: 예외가 던져질 때 실행되는 Advice

- Introduction:  클래스에 인터페이스와 구현을 추가하는 특수한 Advice


(2) JoinPoint: Advice를 적용 가능한 지점을 의미한다. 메서드 호출, 필드 값 변경 등이 Joinpoint에 해당한다.

클래스의 인스턴스 생성 시점', '메소드 호출 시점', '예외 발생 시점'과 같이 어플리케이션을 실행할 때 특정 작업이 시작되는 시점을 '조인포인트'라고 한다. 실행시의 처리 플로우에서 Advice를 위빙하는 포인트를 JointPoint라고 한다. 구체적으로는 메서드 호출이나 예외발생이라는 포인트를 Joinpoint라고 한다.


(3) Pointcut: Joinpoint의 부분 집합으로서 실제로 Advice가 적용되는 Jointpoint를 나타낸다. 스프링에서는 정규 표현식이나 AspectJ 문법을 이용하여 Pointcut을 정의할 수 있다. 여러 개의 조인포인트를 하나로 결합한 것을 포인트 컷이라 한다.

하나 또는 복수의 Jointpoint를 하나로 묶은 것을 Pointcut 이라고 한다. Advice의 위빙 정의는 Pointcut을 대상으로 설정한다. 하나의 Pointcut에는 복수 Advice를 연결할 수 있다. 반대로 하나의 Advice를 복수 Pointcut에 연결하는 것도 가능하다.


(4) Weaving: Advice를 핵심 로직 코드에 적용하는 것을 weaving 이라고 한다. 즉 공통 코드를 핵심 로직 코드에 삽입하는 것이 weaving이다.

어드바이스를 핵심 로직 코드에 삽입하는 것을  위빙이라고 한다.


(5) Aspect: 여러 객체에 공통으로 적용되는 공통 관심 사항을 Aspect라고 한다. 트랜잭션이나 보안 등이 Aspect의 좋은 예이다.

여러 객체에 공통으로 적용되는 공통 관점 사항을 에스펙트라 한다.


(6) Target

핵심 로직을 구현하는 클래스를 말한다.


(7) advisor

어드바이스와 포인트컷을 하나로 묶어 취급한 것을 '어드바이저'라 부른다.

advisor와 Pointcut을 하나로 묶어 다루는 것을 Advisor라고 한다. Advisor는 스프링 AOP에만 있는 것인데, 관점 지향에서 관점을 나타내는 개념이라고 할 수 있다.


2. 세 가지 Weaving 방식

advice를 Weaving하는 방식에는 다음과 같이 세 가지 방식이 존재한다.


(1) 컴파일시에 Weaving 하기

컴파일 시에 코드를 삽입하는 방법은 AspectJ에서 사용하는 방식이다. 컴파일 방식에서는 핵심 로직을 구현한 자바 소스 코드를 컴파일할 때에 알맞은 위치에 공통 코드에 삽입한다.

따라서 AOP가 적용된 클래스 파일이 생성된다. 컴파일 방식을 제공하는 AOP 도구는 공통 코드를 알맞은 위치에 삽입할 수 있도록 도와주는 컴파일러나 IDE를 함께 제공한다.


(2) 클래스 로딩 시에 Weaving 하기

클래스를 로딩할 때에 AOP를 적용할 수도 있다. AOP 라이브러리는 JVM이 클래스를 로딩할 때 클래스 정보를 변경할 수 있는 에이전트를 제공한다. 이 에이전트는 로딩한 클래스의 바이너리 정보를 변경하여 알맞은 위치에 공통 코드를 삽입한 새로운 클래스 바이너리 코드를 사용하도록 한다. 즉 원본 클래스 파일은 변경하지 않고 클래스를 로딩할 때에 JVM이 변경된 바이트 코드를 사용하도록 함으로써 AOP를 적용한다.


(3) 런타임 시에 Weaving 하기

런타임 시에 AOP를 적용할 때에는 소스 코드나 클래스 정보 자체를 변경하지 않는다. 대신 프록시에 이용하여 AOP를 적용한다.

프록시 기반의 AOP는 핵심 로직을 구현한 객체에 직접 접근하는 것이 아니라 중간에 프록시를 생성하여 프록시를 통해서 핵심 로직을 구현한 객체에 접근하게 된다.

이때, 프록시의 핵심 로직을 실행하기 전 또는 후에 공통 기능을 적용하는 방식으로 AOP를 적용하게 된다. 프록시 기반에서는 메서드가 호출될 때에만 Advice를 적용할 수 있기 때문에 필드 값 변경과 같은 Jointpoint에 대해서는 적용할 수 없는 한계가 있다.


4. 프록시를 이용한 AOP 구현

스프링은 프록시를 이용하여 AOP를 구현하고 있다. 스프링은 Aspect의 적용 대상이 되는 객체에 대한 프록시를 만들어 제공하며, 대상 객체를 사용하는 코드는 대상 객체에 직접 접근하기 보다는 프록시를 통해서 간접적으로 접근하게 된다. 이 과정에서 프록시는 공통 기능을 실행한 뒤 대상 객체의 실제 메서드를 호출하거나 또는 대상 객체의 실제 메서드가 호출된 뒤에 공통 기능을 실행하게 된다.

스프링에서 어떤 대상 객체에 대해 AOP를 적용할 지의 여부는 설정 파일을 통해서 지정할 수 있으며, 스프링은 설정 정보를 이용하여 런타임에 대상 객체에 대한 프록시 객체를 생성하게 된다.

프록시 객체를 생성하는 방식은 대상 객체가 인터페이스를 구현하고 있으냐 없느냐 여부에 따라 달라진다.

대상 객체가 인터페이스를 구현하고 있다면, 스프링은 자바 리플렉션 API가 제공하는 java.lang.reflect.Proxy를 이용하여 프록시 객체를 생성한다. 이때 생성된 프록시 객체는 대상 객체와 동일한 인터페이스를 구현하게 되며, 클라이언트는 인터페이스를 통해서 필요한 메서드를 호출하게 된다. 하지만, 인터페이스를 기반으로 프록시 객체를 생성하기 때문에 인터페이스에 정의되어 있지 않은 메서드에 대해서는 AOP가 적용되지 않는 점에 유의해야 한다.

대상 객체가 인터페이스를 구현하고 있지 않다면, 스프링은 CGLIB를 이용하여 클래스에 대한 프록시 객체를 생성한다. CGLIB는 대상 클래스를 상속받아 프록시를 구현한다.

따라서, 대상 클래스가 final인 경우 프록시를 생성할 수 없으며, final인 메서드에 대해서는 AOP를 적용할 수 없게 된다.


[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

[Spring] Locale 처리


Locale 처리


스프링 제공하는 <spring:message> 커스텀 태그는 웹 요청과 관련된 언어 정보를 이용해서 알맞은 언어의 메시지를 출력한다.

웹 브라우저의 언어 설정을 한국어(ko_kr)로 했을 때와 영어(en_us)로 했을 때 <spring:message> 커스텀 태그가 언어에 따라 알맞은 메시지를 출력해 주는 결과 화면을 보여주고 있다.


실제로, 스프링 MVC는 LocaleResolver를 이용해서 웹 요청과 관련된 Locale을 추출하고, 이 Locale 객체를 이용해서 알맞은 언어의 메시지를 선택하게 된다.

본 절에서는 스프링이 제공하는 LocaleResolver를 사용해서 Locale을 변경하는 방법에 대해서 살펴보도록 하겠다.


1. LocaleResolver 인터페이스

org,springframework.web.serlvet.LocaleResolver 인터페이스는 다음과 같이 정의 되어 있다.


package org.springframework.web.servlet;

import java.util.Locale;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;


public interface LocaleResolver{

Locale resolveLocale(HttpServletRequest request);

void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale);

}

resolveLocale() 메서드는 요청과 관련된 Locale을 리턴한다. DispatcherServlet은 등록되어 있는 LocaleResolver의 resolveLocale() 메서드를 호출해서 웹 요청을 처리할 때 사용할 Locale을 구한다.

setLocale() 메서드는 Locale을 변경할 때 사용된다. 예를 들어, 쿠키나, 쿠기나 HttpSession에 Locale 정보를 저장할 때에 이 메서드가 사용된다.


2. LocaleResolver의 종류

스프링이 기본적으로 제공하는 LocaleResolver 구현 클래스는 다음과 같다.


※ 스프링이 제공하는 LocaleResolver 구현 클래스

 클래스

 설 명

 AcceptHeaderLocaleResolver

 웹 브라우저가 전송한 Accept-Language 헤더로부터 Locale 선택한다. setLocale() 메서드를 지원  하지 않는다.

 CookieLocaleResolver

 쿠키를 이용해서 Locale 정보를 구한다. setLocale() 메서드는 쿠키에 Locale 정보를 저장한다.

 SessionLocaleResolver

 세션으로부터 Locale 정보를 구한다. setLocale() 메서드는 세션에 Locale 정보를 저장한다.

 FixedLocaleResolver

 웹 요청에 상관없이 특정한 Locale로 설정한다. setLocale() 메서드를 지원하지 않는다.


(1) AcceptHeaderLocaleResolver

LocaleResolver를 별도로 설정하지 않을 경우 AcceptHeaderLocaleResolver를 기본 LocalResolver로 사용한다.

AcceptHeaderLocaleResolver는 AcceptLanguage 헤더로부터 Locale 정보를 추출한다.

헤더로부터 Locale 정보를 추출하기 때문에, setLocale() 메서드를 이용해서 Locale 설정을 변경할 수 없다.


(2) CookieLocaleResolver

CookieLocaleResolver는 쿠키를 이용해서 Locale 정보를 저장한다. setLocale() 메서드를 호출하면 Locale 정보를 담은 쿠키를 생성하고, resolveLocale() 메서드는 쿠기로부터 Locale 정보를 가져와 Locale 정보를

담은 쿠키가 존재하지 않을 경우, defaultLocale 프로퍼티의 값을 Locale로 사용한다. defaultLocale 프로퍼티의 값이 null인 경우에는 Accept-Language 헤더로부터 Locale 정보를 추출한다.

CookieLocaleResolver는 쿠티와 관련해서 별도 설정을 필요로 하지 않지만. 생성할 쿠키 이름, 도메인, 경로 등의 설정을 직접하고 싶다면 프로퍼티에 알맞게 설정해주면 된다.


※ CookieLocaleResolver의 쿠키 설정 관련 프로퍼티

프로퍼티 

설 명  

 cookieName

 사용할 쿠키 이름

 cookieDomain

 쿠키 도메인

 cookiePath

 쿠키 경로, 기본값은 "/"이다.

 cookieMaxAge

 쿠키 유효 시간

 cookieSecure

 보안 쿠키 여부, 기본값은 false 이다.


(3) SessionLocaleResolver

SessionLocaleResolver HttpSessio에 Locale 정보를 저장한다. setLocale() 메서드를 호출하면 Locale 정보를 세션에 저장하고, resolveLocale() 메서드는 세션으로부터 Locale을 가져와 웹 요청의 Locale을 설정한다.

만약 Locale 정보가 세션에 존재하지 않으면, defaultLocale 프로퍼티의 값을 Locale로 사용한다. defaultLocale 프로퍼티의 값이 null인 경우에는 Accept-Language 헤더로부터 Locale 정보를 추출한다.


(4) FixedLocaleResolver

FixedLocaleResolver는 웹 요청에 상관없이 defaultLocale 프로퍼티로 설정한 값을 웹 요청을 위한 Locale로 사용한다. FixedLocaleResolver는 setLocale() 메서드를 지원하지 않는다.

setLocale() 메서드를 호출할 경우 UnsupportedOperationException 예외를 발생시킨다.


3. LocaleResolver를 이용한 Locale 변경

LocaleResolver를 빈으로 등록했다면, 컨트롤러에서 LocaleResolver를 이용해서 Locale을 변경할 수 있게 된다. 예를 들어 다음과 같이 LocaleResolver를 설정했다고 하자.


<bean class="madvirus.spring.chap07.controller.LocaleChangeController">

<property name="localeResolver" ref="localeResolver" />

</bean>

<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver" />


이 경우, 컨트롤러 클래스는 다음과 같이 LocaleResolver의 setLocale() 메서드를 호출해서 클라이언트의 웹 요청을 위한 Locale을 변경할 수 있다.

import org.springframework.web.servlet.LocaleResolver;


@Controller

public class LocaleChangeController {


private LocaleResolver localeResolver;


@RequestMapping("/changeLanguage")

public String change(@RequestParam("lang") String language, HttpServletRequest request, HttpServletResponse response) {

Locale locale = new Locale(language);

localeResolver.setLocale(request, response, locale);

return "redirect:/index.jsp";

}


public void setLocaleResolver(LocaleResolver localeResolver) {

this.localeResolver = localeResolver;

}

}


LocaleResolver를 이용해서 Locale을 변경하면, 이후 요청에 대해서는 지정한 Locale을 이용해서 메시지 등을 로딩하게 된다.

ResolverContextUtils 클래스는 웹 요청과 관련된 LocaleResolver를 구할 수 있는 메서드를 제공하고 있으므로, 위 코드를 다음과 같이 변경할 수도 있다.

@Controller

public class LocaleChangeController2 {


@RequestMapping("/changeLanguage2")

public String change(@RequestParam("lang") String language, HttpServletRequest request, HttpServletResponse response) {

Locale locale = new Locale(language);

LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);

localeResolver.setLocale(request, response, locale);

return "redirect:/index.jsp";

}

}


4. LocaleChangeInterceptor를 이용한 Locale 변경

Locale을 변경하기 위해 별도의 컨트롤러 클래스를 개발한다는 것은 다소 성가신 일이다.

이 경우, 스프링이 제공하는 LocaleChangeInterceptor 클래스를 사용하면 웹 요청 파라미터를 이용해서 손쉽게 Locale을 변경할 수 있다.


LocaleChangeInteceptor 클래스는 handlerInterceptor로서 다음과 같이 HandlerMapping의 intercaptors 프로퍼티에 등록만 하면 설정이 완료된다. 아래 코드는 설정 예이다.

<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"

p:paramName="language" />


<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">

<property name="interceptors">

<list>

<ref bean="localeChangeInterceptor" />

</list>

</property>

</bean>

paramName 프로퍼티는 Locale을 설정할 때 사용할 파라미터 이름을 명시한다. 예를 들어, 위 코드에서는 paramName 프로퍼티의 값으로 language를 설정했는데,

이 경우 language 요청 파라미터를 사용해서 Locale을 변경할 수 있다.


http://localhost:8080/chap07/jsp/login/login.do?language=en

LocaleChangeInterceptor는 paramName 프로퍼티로 설정한 요청 파라미터가 존재할 경우, 파라미터의 값을 이용해서 Locale을 생성한 뒤 LocaleResolver를 이용해서 Locale을 변경한다.

이후, 요청에서는 변경된 Locale이 적용된다.