JAVA 언어로 배우는 디자인 패턴 입문, 헤드퍼스트 디자인 패턴을 통해 학습 중이며 두 책의 예제 위주로 정리.

 

생성패턴(Creational Pattern)

1. 싱글톤(Singleton)

2. 빌더(Builder)

3. 팩토리 메소드(Factory Method)

4. 추상 팩토리(Abstract Factory)

5. 프로토타입(Prototype)

 

구조패턴(Structural Pattern)

1. 어댑터(Adapter)

2. 브릿지(Bridge)

3. 컴포지트(Composite)

4. 데코레이터(Decorator)

5. 퍼사드(Facade)

6. 플라이웨이트(flyweight)

7. 프록시(Proxy)

 

행동(행위) 패턴(Behavioral Pattern)

1. 책임 연쇄(Chain of Responsibility)

2. 커맨드(Command)

3. 인터프리터(Interpreter)

4. 반복자(Iterator)

5. 중재자(Mediator)

6. 메멘토(Memento)

7. 옵저버(Observer)

8. 상태(State)

9. 전략(Strategy)

10. 템플릿 메소드(Template Method)

11. 방문자(Visitor)


템플릿 메소드 패턴은 행동(행위) 패턴에 속하는 디자인 패턴으로 상위 클래스에서 처리의 뼈대를 결정하고 하위 클래스에서 구체적인 내용을 결정하는 패턴이다.

알고리즘의 구조를 변경하지 않고 알고리즘의 특정 단계들을 다시 정의할 수 있게 해준다.

 

예제

문자 또는 문자열을 출력하는 프로그램.

Character 타입의 경우 << >> 안에 해당 문자가 5번 반복이 되며, String 타입의 경우 +---+ +---+ 사이에 | 문자열 | 형태로 개행이 되며 5번 반복이 된다.

 

구조로는 추상클래스인 AbstractDisplay 에는 open, print, close라는 세개의 추상 메소드, open, 반복문으로 5번 수행하는 print, close 순서로 호출하고 있는 display 메소드가 존재한다.

구현체인 CharDisplay와 StringDisplay는 각각 출력할 문자 혹은 문자열 필드를 갖고 있으며, AbstractDisplay를 상속받아 추상메소드를 구현하고 있다.

이 구조에서 Template Method는 display가 된다.

 

//예제 출처 - JAVA 언어로 배우는 디자인 패턴 입문

public abstract class AbstractDisplay {
    public abstract void open();
    
    public abstract void print();
    
    public abstract void close();
    
    public final void display() {
        open();
        for(int i = 0; i < 5; i++)
            print();
        
        close();
    }
}
public class CharDisplay extends AbstractDisplay {
    
    private char ch;
    
    public CharDisplay(char ch) {
        this.ch = ch;
    }
    
    @Override
    public void open() {
        System.out.println("<<");
    }
    
    @Override
    public void print() {
         System.out.print(ch);
    }
    
    @Override
    public void close() {
        System.out.println(">>");
    }
}
public class StringDisplay extends AbstractDisplay {
    private String string;
    
    private int width;
    
    public StringDisplay(String string) {
        this.string = string;
        this.width = string.length();
    }
    
    @Overrdie
    public void open() {
        printLine();
    }
    
    @Overrdie
    public void print() {
        System.out.println("|" + string + "|");
    }
    
    @Override
    public void close() {
        printLine();
    }
    
    private void printLine() {
        System.out.print("+");
        for(int i = 0; i < width; i++)
            System.out.print("-");
        System.out.println("+");
    }
}
public class Main {
    public static void main(String[] args) {
        AbstractDisplay charH = new CharDisplay('H');
        
        AbstractDisplay stringHello = new StringDisplay("Hello World");
        
        charH.display();
        stringHello.display();
    }
}

 

실행결과

<<HHHHH>>

+----------+

|Hello World|

|Hello World|

|Hello World|

|Hello World|

|Hello World|

+----------+

 

예제 작성 전에 Template Method는 display() 메소드라고 했다.

display() 메소드는 동일하게 open() -> print() * 5 -> close() 를 처리하고 있다.

즉, 처리과정에 대한 뼈대가 된다.

그럼 상위 클래스인 AbstractDisplay에서 뼈대를 결정한 것이라고 볼 수 있다.

그리고 하위 클래스인 CharDisplay와 StringDisplay에서는 AbstractDisplay를 상속받아 메소드를 구현하고 있다.

각 클래스에서는 open(), print(), close()를 어떻게 처리할지 구체적인 구현을 담당하고 있다.

처음 템플릿 메소드의 설명에 빗대어 본다면,

상위 클래스(AbstractDisplay)에서 처리의 뼈대(display())를 결정하고 하위 클래스(CharDisplay, StringDisplay)에서 그 구체적인 내용(open(), print(), close())을 결정하는 패턴이다.

 

코드를 보며 재정리.

 

AbstractDisplay에는 구현해야 할 추상메소드(open(), print(), close())와 템플릿 메소드(display())가 존재한다.

템플릿 메소드는 추상 메소드들을 호출하게 된다.

실제 사용한다고 하면 여러 알고리즘 + 추상 메소드의 구조가 될 것이다.

즉, 템플릿 메소드는 해당 처리에 대한 정형화된 틀이 된다.

그리고 이 틀을 만들어 두게 됨으로써 다른 타입을 받거나 다른 결과를 내야 하는 경우 중복되는 처리 과정을 거치는 여러 메소드들을 생성하지 않고 재사용할 수 있다.

 

즉, CharDisplay와 StringDisplay의 공통점으로는 open() -> print() * 5 -> close()로 처리되고 있는데, 

이걸 각 클래스의 display() 메소드로 처리한다고 한다면 동일한 코드가 반복되거나 혹은 전혀 분리되어 있지 않은 코드가 생길것이다.

public class CharDisplay {
    private char ch;
    
    public CharDisplay(char ch) {
        this.ch = ch;
    }
    
    public void display() {
        System.out.print("<<");
        
        for(int i = 0; i < 5; i++)
            System.out.print(ch);
        
        System.out.println(">>");
    }
}



public class StringDisplay {
    private String string;
    
    private int width;
    
    public StringDisplay(String string) {
        this.string = string;
        this.width = string.length();
    }
    
    private void open() {
        printLine();
    }
    
    private void print() {
        for(int i = 0; i < 5; i++)
            System.out.println("|" + string + "|");
    }
    
    private void close() {
        printLine();
    }
    
    public void display() {
        open();
        print();
        close();
    }
    
    private void printLine() {
        System.out.print("+");
        
        for(int i = 0; i < width; i++)
            System.out.print("-");
            
        System.out.println("+");
    }
    
    
}

 

이러한 코드 구조라면 틀만 봤을때는 같은 처리과정을 갖고 있는데, 각 클래스에서 구현하게 됨으로써 중복이 발생한다.

그리고 만약 이 문자 또는 문자열을 10번 출력하도록 수정해야 한다면 모든 클래스를 체크하면서 5번을 10으로 수정해줘야 한다.

또한 재사용성이라고는 전혀 찾아볼 수도 없다.

그래서 이 처리 과정들을 추상메소드로 구체적인 것은 하위 클래스에서 맡겨놓고 처리하는 틀은 템플릿 메소드로 처리하게 되면 코드도 간결해져 코드가 보다 간결해 질 수 있고, 틀을 수정해야 하더라도 각 하위 클래스에서 수정할 것은 없고 템플릿 메소드에서만 수정이 발생하게 된다.

 

이렇게 상위 클래스 타입의 변수에 생성한 인스턴스 중 어느것을 대입하더라도 제대로 동작할 수 있도록 하는 원칙을 리스코프 치환법칙(LSP, Liskov Substitution Principle)이라고 한다.

 

템플릿 메소드 패턴을 하위 클래스 관점에서 본다면

  • 상위 클래스에서 정의 된 메소드를 하위 클래스에서 이용할 수 있다.
  • 하위 클래스에서 약간의 메소드를 기술하는 것만으로 새로운 기능을 추가할 수 있다.
  • 하위 클래스에서 메소드를 오버라이드 하게 되면 동작을 변경할 수 있다.

상위 클래스 관점에서 본다면

  • 하위 클래스에서 그 메소드를 구현하기를 기대한다.
  • 하위 클래스에 메소드 구현을 요청한다.

즉, 하위 클래스에서는 상위 클래스에서 선언한 추상 메소드를 구현할 책임이 있다고 할 수 있는데 이것을 subclass responsibility(하위 클래스의 책임)이라고 한다.

 

JAVA 언어로 배우는 디자인 패턴 입문, 헤드퍼스트 디자인 패턴을 통해 학습 중이며 두 책의 예제 위주로 정리.

 

생성패턴(Creational Pattern)

1. 싱글톤(Singleton)

2. 빌더(Builder)

3. 팩토리 메소드(Factory Method)

4. 추상 팩토리(Abstract Factory)

5. 프로토타입(Prototype)

 

구조패턴(Structural Pattern)

1. 어댑터(Adapter)

2. 브릿지(Bridge)

3. 컴포지트(Composite)

4. 데코레이터(Decorator)

5. 퍼사드(Facade)

6. 플라이웨이트(flyweight)

7. 프록시(Proxy)

 

행동(행위) 패턴(Behavioral Pattern)

1. 책임 연쇄(Chain of Responsibility)

2. 커맨드(Command)

3. 인터프리터(Interpreter)

4. 반복자(Iterator)

5. 중재자(Mediator)

6. 메멘토(Memento)

7. 옵저버(Observer)

8. 상태(State)

9. 전략(Strategy)

10. 템플릿 메소드(Template Method)

11. 방문자(Visitor)


전략 패턴(Strategy Pattern)은 행위패턴(Behavioral Pattern)에 속하는 디자인 패턴이다.

전략패턴은 스위치를 전환하듯 알고리즘(전략)을 바꿔서 같은 문제를 다른 방법으로 해결하기 쉽게 만들어주는 패턴이다.

특정한 계열의 알고리즘들을 정의하고 각 알고리즘을 캡슐화하여 이 알고리즘들을 해당 계열 안에서 상호 교체가 가능하게 만든다.

 

예제

자바 언어로 배우는 디자인패턴 입문 책에서의 예제를 먼저 본다.

예제 프로그램은 가위바위보를 하는 프로그램이다.

이 프로그램에서는 두가지의 전략을 갖는다.

1. 이기면 다음에도 같은 손을 내는 다소 어리석은 방법(WinningStrategy)

2. 직전 손에서 다음 손을 확률적으로 계산하는 방법(ProbStrategy)

 

//예제 출처 - JAVA 언어로 배우는 디자인패턴 입문

public enum Hand {
    
    ROCK("바위", 0)
    , SCISSORS("가위", 1)
    , PAPER("보", 2);
    
    private String name;   //가위바위보 손 이름
    private int handValue; //가위바위보 손의 값
    
    private static Hand[] hands = {
        ROCK, SCISSORS, PAPER
    };
    
    // Constructor
    private Hand(String name, int handValue) {
        this.name = name;
        this.handValue = handValue;
    }
    
    //손의 값으로 enum 상수를 가져온다
    public static Hand getHand(int handValue) {
        return hands[handValue];
    }
    
    //this가 h 보다 강하다면 true
    public boolean isStrongerThan(Hand h) {
        return fight(h) == 1;
    }
    
    //this가 h보다 약하다면 true
    public boolean isWeakerThan(Hand h) {
        return fight(h) == -1;
    }
    
    //무승부는 0, this가 이기면 1, h가 이기면 -1
    private int fight(Hand h) {
        if(this == h)
            return 0;
        else if((this.handValue + 1) % 3 == h.handValue)
            return 1;
        else
            return -1;
    }
    
    @Override
    public String toString() {
        return name;
    }
}
public interface Strategy {
    public abstract Hand nextHand();
    
    public abstract void study(boolean win);
}
/**
* 이기면 다음에도 같은 손을 내는 전략
*/

public class WinningStrategy implements Strategy{
    private Random random;
    
    private boolean won = false;
    
    private Hand prevHand;
    
    public WinningStrategy(int seed) {
        random = new Random(seed);
    }
    
    @Override
    public Hand nextHand() {
        if(!won)
            prevHand = Hand.getHand(random.nextInt(3));
        
        return prevHand;
    }
    
    @Override
    public void study(boolean win) {
        won = win;
    }
}
/**
* 확률을 계산해 다음 손을 결정하는 전력
* 
* history[직전에 낸 손][이번에 낸 손]
* 배열의 value는 과거의 승리 수를 의미
*/
public class ProbStrategy implements Strategy{
    
    private Random random;
    
    private int prevHandValue = 0;
    
    private int currentHandValue = 0;
    
    private int[][] history = {
        { 1, 1, 1 },
        { 1, 1, 1 },
        { 1, 1, 1 }
    };
    
    public ProbStrategy(int seed) {
        random = new Random(seed);
    }
    
    @Override
    public Hand nextHand() {
        int bet = random.nextInt(getSum(currentHandValue));
        int handValue = 0;
        
        if(bet < history[currentHandValue][0])
            handValue = 0;
        else if(bet < history[currentHandValue][0] + history[currentHandValue][1])
            handValue = 1;
        else
            handValue = 2;
        
        prevHandValue = currentHandValue;
        currentHandValue = handValue;
        
        return Hand.getHand(handValue);
    }
    
    private int getSum(int handValue) {
        int sum = 0;
        
        for(int i = 0; i < 3; i++)
            sum += history[handValue][i];
        
        return sum;
    }
    
    @Override
    public void study(boolean win) {
        if(win)
            history[prevHandValue][currentHandValue]++;
        else{
            history[prevHandValue][(currentHandValue + 1) % 3]++;
            history[prevHandValue][(currentHandValue + 2) % 3]++;
        }
    }
}
public class Player {
    private String name;
    
    private Strategy strategy;
    
    private int winCount;
    
    private int loseCount;
    
    private int gameCount;
    
    //이름과 전략을 받아 플레이어를 생성
    public Player(String name, Strategy strategy) {
        this.name = name;
        this.strategy = strategy;
    }
    
    //전략에 따라 다음 손을 결정
    public Hand nextHand() {
        return strategy.nextHand();
    }
    
    //승리
    public void win() {
        strategy.study(true);
        winCount++;
        gameCount++;
    }
    
    //패배
    public void lose() {
        strategy.study(false);
        loseCount++;
        gameCount++;
    }
    
    //무승부
    public void even() {
        gameCount++;
    }
    
    @Override
    public String toString() {
        return "["
                + name + " : "
                + gameCount + " games, "
                + winCount + " win, "
                + loseCount + " lose]";
    }
}
public class Main{
    public static void main(String[] args) {
        int seed1 = 314;
        int seed2 = 15;
        
        Player player1 = new Player("Kim", new WinningStrategy(seed1));
        Player player2 = new Player("Lee", new ProbStrategy(seed2));
        
        for(int i = 0; i < 10000; i++) {
            Hand nextHand1 = player1.nextHand();
            Hand nextHand2 = player2.nextHand();
            
            if(nextHand1.isStrongerThan(nextHand2)) {
                System.out.println("Winner : " + player1);
                player1.win();
                player2.lose();
            }else if(nextHand2.isStrongerThan(nextHand1)) {
                System.out.println("Winner : " + player2);
                player1.lose();
                player2.win();
            }else{
                System.out.println("Even...");
                player1.even();
                player2.even();
            }
        }
        
        System.out.println("Total result : ");
        System.out.println(player1);
        System.out.println(player2);
    }
}

 

구조

Hand 가위바위보에서 손을 나타내는 클래스
Strategy 가위바위보의 '전략'을 나타내는 인터페이스
WinningStrategy 이기면 다음에도 같은 손을 내는 전략의 클래스.
Strategy의 구현체
ProbStrategy 직전 손에서 다음 손을 확률적으로 계산하는 전략의 클래스.
Strategy의 구현체
Player 가위바위보를 하는 플레이어 클래스

 

메인메소드 실행 시 10,000번의 게임을 진행하고 가장 마지막에 A와 B의 승리, 패배 횟수가 출력된다.

Hand는 enum 클래스로 가위, 바위, 보 라는 세개의 enum 상수를 갖고 있으며, 필드로 name, handValue를 갖고 있다.

또한 isStrongerThan() 메소드를 통해 A와 B의 결과를 반환 할 수 있다.

각 전략에 대한 인터페이스인 Strategy가 존재한다.

그리고 전략에 대한 구체화를 표현한 WinningStrategy, ProbStrategy가 있다.

각 플레이어는 이름과 전략을 갖고 생성된다.

그래서 게임이 진행될때마다 각 플레이어는 자신의 전략에 따라 다음에 낼 손을 결정하게 된다.

 

Strategy 패턴의 구조는 아래와 같다.

1. Strategy - 전략을 이용하기 위한 인터페이스. 예제에서 인터페이스로 처리했지만 추상 클래스로 처리하는 것도 가능하다.

2. ConcreteStrategy - Strategy의 구현체. 여러개의 ConcreteStrategy로 다양한 전략이 가능하다.

3. Context - Strategy를 이용하는 주체. ConcreteStrategy의 인스턴스를 갖고 있다가 필요에 따라 이용한다. 예제에서의 Player 클래스.

 

마무리 정리는 조금 뒤로 미루고 헤드퍼스트 디자인 패턴 책에서의 예제도 정리한다.

예제 프로그램은 오리 시뮬레이터이다.

객체로는 일반 오리와 모형오리 두가지가 존재.

 

코드 먼저 정리한다.

이 예제에서는 패키지가 나눠져 있으니 패키지 구조도 확인.

//예제 출처 - 헤드퍼스트 디자인 패턴

package strategyPattern.headfirst.fly;

public interface FlyBehavioral {
    public void fly();
}
package strategyPattern.headfist.fly;

public class FlyNoWay implements FlyBehavioral {
    
    @Override
    public void fly() {
        System.out.println("날지 못해요");
    }
}
package strategyPattern.headfist.fly;

public class FlyRocketPowered implements FlyBehavioral {
    
    @Override
    public void fly() {
        System.out.println("로켓 추진!!!!!!!!!!!");
    }
}
package strategyPattern.headfist.fly;

public class FlyWithWings implements FlyBehavioral {
    
    @Override
    public void fly() {
        System.out.println("날고 있어요");
    }
}
package strategyPattern.headfist.quack;

public interface QuackBehavioral {
    
    public void quack();
}
package strategyPattern.headfist.quack;

public class MuteQuack implements QuackBehavioral {
    
    @Override
    public void quack() {
        System.out.println("---");
    }
}
package strategyPattern.headfist.quack;

public class Quack implements QuackBehavioral {
    
    @Override
    public void quack() {
        System.out.println("꽥!");
    }
}
package strategyPattern.headfist.quack;

public class Squeak implements QuackBehavioral {
    
    @Override
    public void quack() {
        System.out.println("삑!");
    }
}
package strategyPattern.headfirst;

public abstract class Duck {
    
    FlyBehavioral flyBehavioral;
    
    QuackBehavioral quackBehavioral;
    
    public Duck() {
    
    }
    
    public void swim() {
        System.out.println("모든 오리는 물에 뜹니다.");
    }
    
    public abstract void display();
    
    public void perfomFly() {
        flyBehavioral.fly();
    }
    
    public void performQuack() {
        quackBehavioral.quack();
    }
    
    public void setFlyBehavioral(FlyBehavioral fb) {
        flyBehavioral = fb;
    }
    
    public void setQuackBehavioral(QuackBehavioral qb) {
        quackBehavioral = qb;
    }
}
package strategyPattern.headfirst;

public class MallardDuck extends Duck {
    
    @Override
    public void display() {
        System.out.println("물오리!");
    }
    
    public MallardDuck() {
        quackBehavioral = new Quack();
        flyBehavioral = new FlyWithWings();
    }
}
package strategyPattern.headfirst;

public class ModelDuck extends Duck {
    
    @Override
    public void display() {
        System.out.println("모형오리!");
    }
    
    public ModelDuck() {
        flyBehavioral = new FlyNoWay();
        quackBehavioral = new MuteQuack();
    }
}
package strategyPattern.headfirst;

public class DuckSimulator {
    public static void main(String[] args) {
        // 물오리
        Duck mallardDuck = new MallardDuck();
        mallardDuck.display();
        mallardDuck.performFly();
        mallardDuck.performQuack();
        
        System.out.println("------------");
        
        Duck modelDuck = new ModelDuck();
        modelDuck.display();
        modelDuck.performFly();
        modelDuck.setFlyBehavioral(new FlyRocketPowered());
        modelDuck.performFly();
        modelDuck.performQuack();
    }
}

메인 메소드를 실행해보면

물오리!

날고 있어요

꽥!

----------

모형오리!

날지 못해요

로켓 추진!!!!!!!

---

 

이렇게 출력된다.

그럼 책에서의 예시를 정리해보자.

다양한 종류의 오리의 모습을 보여주고, 헤엄치고 울 수 있는 시뮬레이터를 만들었다.

 

그래서 Duck 이라는 클래스에 quack(), swim(), display() 메소드를 만들었다.

모든 오리는 꽥꽥 소리를 내고 헤엄을 칠 수 있기 때문에 display() 메소드만 추상 메소드로 만들었다.

그래서 각 오리 객체는 Duck 클래스를 상속받고, display() 메소드를 각각 구현하고 있다.

 

그런데 시뮬레이터를 서비스하다가 날아가는 것도 표현되었으면 좋겠다는 사용자 의견에 따라 날아 가는 기능을 추가하게 되었다.

그래서 Duck 클래스에 fly() 메소드를 추가하게 되었다.

시간이 조금 지나서 좀 더 다양한 객체를 추가하려고 러버덕을 추가하기로 했다.

러버덕 객체 역시 다른 오리 객체들과 마찬가지로 Duck을 상속받는 형태로 작성했다.

그랬더니??

날 수 없는 러버덕이 날아다녔다.

또한 러버덕은 꽥꽥 소리가 아닌 삑삑 이라는 소리가 나야 하는데 똑같이 꽥꽥 소리를 내며 울고 있었다.

 

그래서 러버덕 객체에서는 quack() 메소드와 fly() 메소드를 모두 오버라이드해 처리하는 방법을 생각했지만, 그 후 객체를 계속 추가하게 된다면 이것 역시 비효율적이라는 생각을 하게 된다.

 

그래서 분리를 생각하게 되었고, 공통적으로 모든 객체가 수행하는 기능인 swim()과 display() 메소드는 Duck 클래스에 남겨두고, fly()와 quack()은 각각의 인터페이스로 분리한 뒤 필요로 하는 클래스에서 인터페이스들을 확장해 구현하도록 처리한다.

 

여기까지가 책에서의 예시이다.

그럼 위와 같이 처리했을 때의 문제점으로는 무엇이 있을까?

바로 코드의 중복이다.

디자인패턴의 장점으로 유지보수성과 재사용성이 있다.

인터페이스로 분리하는것까지는 좋았지만, 인터페이스를 확장해 구현하는 모든 객체에서 동일한 기능을 하는 객체가 단 하나도 없다면 이러한 분리도 괜찮을 수 있을지도 모르겠다.

하지만, 가장 먼저 시뮬레이터에 생성한 객체인 실제 오리들의 날아가는 행동이 '날고 있어요' 라는 기능으로 동일하다면?

모든 해당 객체들에서는 인터페이스 구현으로 인해 '날고 있어요' 라는 기능의 코드가 작성되어 있을 것이다.

그럼 코드의 중복이 발생한다는 것이다. 중복이 발생한다는 것은 동일한 코드를 사용하고 있음에도 재사용을 하지 않고 새로운 코드를 사용한다고 할 수 있다.

또한, 이 '날고 있어요' 라는 기능을 '날고 있네요??' 라고 수정해야 하는 상황이 왔다면??

그리고 여기에 해당하는 객체들이 몇십 몇백개라면???

모든 객체들의 구현 코드를 수정해야 하는 상황이 발생하게 된다.

 

이 문제의 해결책이 예제이다.

분리까지는 좋은 방법이니 swim, display는 Duck에 그대로 두고 Quack과 Fly를 인터페이스로 만들어준다.

단, 이 인터페이스들을 각 객체에서 구현하는 것이 아닌 클래스 집합으로 문제를 해결한다.

예제 구조를 보면 FlyBehavioral 인터페이스를 구현하는 클래스는 FlyNoWay(날지 못해요), FlyRocketPowered(로켓 추진!!!!), FlyWithWings(날고 있어요) 이렇게 존재한다.

그리고 각 오리 객체에서는 이를 구현하지 않고 있으며, Duck 클래스에서는 각 인터페이스변수를 갖고 있게 된다.

각 객체는 Duck 클래스를 상속받고 있기 때문에 생성자를 통해 각 인터페이스 변수에 행동 클래스 인스턴스를 대입하게 된다.

그 코드가 각 객체 생성자에 있는 flyBehavioral = new FlyWithWings(); 코드이다.

그럼 객체가 생성되고 행동 처리를 하는 메소드를 호출하게 된다면 행동 클래스 인스턴스가 대입되어 있는 인터페이스 변수를 통해 기능을 수행하게 되는 것이다.

 

이렇게 수행했을때를 한번 살펴보면,

일단, 오리 객체들이 직접 fly()를 구현하고 있지 않기 때문에 코드의 중복이 발생하지 않는다.

동일하게 날아가는 모습을 표현하는 오리 객체가 200개가 존재한다고 했을 때, 

이 객체들은 생성자를 통해 flyBehavioral에 행동 클래스 인스턴스를 담아주기만 하면 될 뿐이지 직접 '날고 있어요' 라는 기능의 코드를 작성할 필요가 없게 된다.

그럼 FlyWithWings()의 구현체는 재사용이 처리되고 있는 코드라고 볼 수 있고, 그로인해 중복이 제거되며 재사용성이 높아졌다고 볼 수 있게 된다.

 

다음은 유지보수 관점에서다.

각 객체에서 구현하는 경우 '날고 있어요'를 '날고 있네요??' 라고 수정하기 위해서는 200개의 객체를 모두 수정해야 했다.

하지만 이렇게 구현을 해두고 나면 FlyWithWing() 의 구현체만 수정한다면 문제가 해결된다.

그럼 200개의 메소드를 수정할 필요없이 단 한개의 메소드 수정으로 문제가 해결되는 것이다.

 

또한, 클래스 집합에 대한 디자인은 최대한 유연하게 만드는 것이 좋다.

메인 메소드를 보면 모형오리는 날지 못하는 것이 생성자를 통한 기본값이지만, setter를 통해 FlyRocketPowered 클래스 인스턴스를 담도록 처리한다.

그리고 모형오리를 날도록 해보면 로켓을 달아놓은 모형오리가 된다.

이렇게 유연하게 만들면 다양한 기능을 사용할 수 있도록 처리할 수 있다.

 

위 정리에서의 중점은 나는 행동과 우는 행동을 Duck(슈퍼 클래스)나 그 서브클래스에서 정의한 메소드를 사용해 구현하는 것이 아닌 다른 클래스에 '위임'한다는 것이다.

 

 

정리

두 책에서의 예제는 완전 다른 스타일의 예제였다.

JAVA 언어로 배우는 디자인패턴 입문 책의 예제는 전략 패턴 그 자체에 집중한 예제였다.

가위바위보를 수행하는 프로그램을 만들것이지만, 승리했을 때 이전에 냈던 손을 그대로 낸다는 전략(WinningStrategy)와 직전에 낸 손에서 다음 손을 확률적으로 계산해 낸다는 전략(ProbStrategy)가 존재하며 플레이어는 생성될 때 어떤 전략을 사용할 것인지를 결정하게 되고, 이 전략은 Strategy 라는 인터페이스를 통해 같은 메소드의 구현체로 전략을 구현한다.

그리고 플레이어는 생성될 때 전략을 받아 Strategy 인터페이스 변수에 전략을 담아두고 게임에 임하는 내내 해당 전략을 구사한다.

 

이렇게 구현한다면 각 전략을 플레이어 객체에서 구현하느라 여러 플레이어 객체를 생성해 구현할 필요가 없고 전략 인터페이스를 구현하는 집합체로 구현 클래스를 통한 전략 설계를 하면 된다.

 

헤드퍼스트 디자인 패턴에서는 전략 패턴은 캡슐화가 중요하다! 라는 느낌이었다.

아무래도 좀 더 다양한 기능에 대한 예제라 그렇지 않나 싶다.

시뮬레이터라는 프로그램에서의 기능을 전략이라고 볼 수 있다.

직관적으로 이게 전략이다! 라는 느낌은 없었지만 이렇게 캡슐화를 해야 코드의 재사용성과 유지보수성을 높일 수 있다 라는 것에 대해 이해하기는 더 좋았다.

 

두 책에서의 공통적인 중점.

전략패턴은 알고리즘을 캡슐화 해 각각의 알고리즘 수정에 다른 클래스를 수정해야 하는 경우가 발생하지 않도록 해야 한다.

구현체인 ConcreteStrategy를 수정하거나 추가로 생성하는 형태로 처리할 수 있으며, 위임이라는 약한 결합으로 알고리즘 수정이 용이하다.

 

 

 

 

 

JAVA 언어로 배우는 디자인 패턴 입문, 헤드퍼스트 디자인 패턴을 통해 학습 중이며 두 책의 예제 위주로 정리.

 

생성패턴(Creational Pattern)

1. 싱글톤(Singleton)

2. 빌더(Builder)

3. 팩토리 메소드(Factory Method)

4. 추상 팩토리(Abstract Factory)

5. 프로토타입(Prototype)

 

구조패턴(Structural Pattern)

1. 어댑터(Adapter)

2. 브릿지(Bridge)

3. 컴포지트(Composite)

4. 데코레이터(Decorator)

5. 퍼사드(Facade)

6. 플라이웨이트(flyweight)

7. 프록시(Proxy)

 

행동(행위) 패턴(Behavioral Pattern)

1. 책임 연쇄(Chain of Responsibility)

2. 커맨드(Command)

3. 인터프리터(Interpreter)

4. 반복자(Iterator)

5. 중재자(Mediator)

6. 메멘토(Memento)

7. 옵저버(Observer)

8. 상태(State)

9. 전략(Strategy)

10. 템플릿 메소드(Template Method)

11. 방문자(Visitor)


어댑터 패턴(Adapter Pattern)은 구조패턴(Structural Pattern)에 속하는 패턴이다.

현실에서 어댑터라는 것은 보통 전류 변환을 해주는 용도로 사용이 된다.

제공되는 것과 필요한 것 사이에서 그 사이를 채워주는 역할을 하는 것이 어댑터이다.

책에서는 이렇게 예시를 들고 있다.

노트북에 필요한 전류는 12볼트이지만 콘센트에서는 100볼트로 들어온다면, 그것을 변환해줘야 하는 것이 필요하다.

100볼트로 들어온 전류를 12볼트로 변환해주는 역할을 어댑터가 담당한다.

 

프로그램에서도 이미 제공된 코드를 그대로 사용할 수 없을 때, 필요한 형태로 변환 후 이용하는 경우가 발생했을 때 어댑터 패턴을 사용한다.

어댑터 패턴은 Wrapper 패턴이라고 불리기도 한다.

감싼다는 의미인데 무엇인가를 포장해서 다른 용도로 사용할 수 있도록 변환해주는것이 Wrapper이자 어댑터이다.

 

어댑터 패턴에는 두가지 종류가 있다.

1. 클래스에 의한 어댑터(상속을 사용한 패턴)

2. 인스턴스에 의한 어댑터(위임을 사용한 패턴)

 

예제

 

예제 프로그램은 주어진 문자열을 ( ) 안에 넣어 출력하거나 * * 안에 넣어 출력하도록 하는 프로그램이다.

 

1. 클래스에 의한 어댑터

메인 클래스를 제외하고 2개의 클래스와 1개의 인터페이스로 작성한다.

//예제 출처 - JAVA 언어로 배우는 디자인 패턴 입문

public class Banner {
    
    private String string;
    
    public Banner(String string) {
        this.string = string;
    }
    
    public void showWithParen() {
        System.out.println("(" + string + ")");
    }
    
    public void showWithAster() {
        System.out.println("*" + string + "*");
    }
}
public interface Print {
    
    public abstract void printWeak();
    
    public abstract void printStrong();
}
public class PrintBanner extends Banner implements Print {
    
    public PrintBanner(String string) {
        super(string);
    }
    
    @Override
    public void printWeak() {
        showWithParen();
    }
    
    @Override
    public void printStrong() {
        showWithAster();
    }
}
public class AdapterMain {
    
    public static void main(String[] args) {
        Print p = new PrintBanner("Hello");
        
        p.printWeak();
        p.printStrong();
    }
}

처리 과정을 먼저 보면 PrintBanner 생성자의 super를 통해 Banner 의 생성자에 접근하게 된다.

그리고 생성된 인스턴스는 Print를 구현하고 있는 구현체이기 때문에 Print 타입의 p 변수에 담을 수 있게 된다.

그리고 여기서 printWeak과 printStrong을 호출하면 PrintBanner 클래스에서는 Banner를 상속받고 있기 때문에

Banner의 showWithParen()과 showWithAster()를 바로 호출해 사용할 수 있게 된다.

그래서 (Hello)와 *Hello*가 출력되게 된다.

 

여기서 Banner 클래스는 문자열을 괄호로 묶는 메소드와 * * 로 묶는 메소드가 준비되어 있다.

이 Banner 클래스를 위 전류 예시 중 콘센트에서 제공되는 100볼트라고 가정한다.

 

Print 인터페이스에서는 printWeak과 printStrong이 선언되어 있다.

이 인터페이스를 12볼트가 필요한 노트북이라고 가정한다.

 

마지막으로 PrintBanner 클래스는 Banner 클래스를 상속받으며 Print 인터페이스를 구현하고 있다.

PrintBanner에서는 부모클래스인 Banner의 showWithParen()으로 printWeak을 구현하고 있고, showWithAster()로 printStrong을 구현하고 있다.

이 PrintBanner 클래스가 어댑터의 역할을 한다.

 

정리해보자면 아래와 같다.

- 제공되는 것(100볼트) -  Banner(showWithParen, showWithAster)

- 전류 변환장치(어댑터) -  PrintBanner

- 필요한 것(12볼트)       -  Print 인터페이스(printWeak, printStrong)

 

그럼 메인메소드 코드를 이렇게 볼 수 있다.

PrintBanner 인스턴스를 Print 타입으로 생성해준다.

Print에서 사용하고자 한 printWeak, printStrong을 호출하게 되면

어댑터인 PrintBanner는 제공되는 Banner의 showWithParen, showWithAster를 Print()가 사용할 수 있게 해준다.

 

그럼 메인 메소드에서는 Print 인터페이스를 통해 요청을 처리하고 있게 되는 것이기 때문에 Print 인터페이스에만 관심이 있을 뿐, Banner클래스와 PrintBanner 클래스가 어떻게 구현되어있는지는 관심이 없게 된다.

즉, Banner와 PrintBanner에서 어떻게 처리하는지는 알 수 없고, Print 인터페이스에 printWeak(), printStrong()이 선언되어 있다는것만 알고 있는 것이다.

마치 노트북에서 12볼트로 동작을 하지만 어댑터에 들어오는 전류가 100볼트이던 200볼트이던 상관이 없다는 것이다.

이렇게 구현하게 되면 수정이 필요하게 되더라도 메인메소드의 수정이 발생하지 않고 Banner 클래스나 PrintBanner 클래스에서의 수정만 발생하게 된다.

 

 

2. 인스턴스에 의한 어댑터

클래스에 의한 어댑터와 다른 점으로는 상속이 아닌 위임을 사용한다는 점이다.

자바에서 위임은 어떤 메소드의 실제 처리를 다른 인스턴스의 메소드에 맡기는 것을 말한다.

예제는 Banner 클래스는 수정이 없고, Print 인터페이스를 추상클래스로, 그리고 PrintBanner 클래스에서 Banner 클래스를 상속받지 않도록 수정한다.

public abstract class Print2 {
    public abstract void printWeak();
    
    public abstract void printStrong();
}
public class PrintBanner2 extends Print2 {
    
    private Banner banner;
    
    public PrintBanner2(String string) {
        this.banner = new Banner(string);
    }
    
    @Override
    public void printWeak() {
        banner.showWithParen();
    }
    
    @Override
    public void printStrong() {
        banner.showWithAster();
    }
}
public class AdapterMain {
    public static void main(String[] args) {
        Print2 p2 = new PrintBanner2("Hi");
        
        p2.printWeak();
        p2.printStrong();
    }
}

1번과 다른점을 보자면 Print 인터페이스가 추상클래스인 Print2로 바뀌었다는 점.

PrintBanner2에서는 Banner를 필드로 갖고 생성자에서는 상위 클래스가 아니기 때문에 new 키워드를 통한 인스턴스를 생성해 필드에 담아주게 된다.

그리고 구현메소드 역시 상속에서 벗어났기 때문에 그냥 사용할 수 없어 banner 필드를 통해 호출한다.

 

자바에서는 다중상속이 불가능하기 때문에 Print2 클래스를 상속받은 구현체라면 Banner 클래스를 또 상속받을 수 없다.

그래서 Banner 필드를 갖게 되는 것이고 이 필드를 통한 호출로 인해 위임이 발생하게 된다.

즉, PrintBanner2에서 printWeak() 메소드가 호출되었을 때 자신이 처리하지 않고, 다른 인스턴스인 banner의 showWithParen()으로 위임하게 되는 것이다.

 

 

처음에는 이게 너무 이해가 어려웠다.

PrintBanner 클래스가 어댑터라는데 이게 왜 어댑터인지, 중간에서 뭘 변환해준다는 것인지 이해하기가 어려웠다.

그래서 헤드퍼스트 디자인 패턴에서의 예제를 봤는데 이게 좀 더 이해가 쉬웠다.

 

예제 프로그램은 아래와 같다.

Duck, Terkey 라는 객체가 존재하고, 

Duck은 꽥! 이라는 소리를 내고 날아간다.

Terkey는 골골 이라는 소리를 내고 5번을 짧게 날아간다.

 

위 예제에서와는 다르게 다른 처리과정이 존재하는 예제라고 볼 수 있다.

구조는 인터페이스로 Duck, Terkey를 갖게 되고, 클래스로 MallardDuck, WildTurkey, TurkeyAdapter가 존재한다.

그리고 메인 클래스로 DuckMain 클래스가 있다.

//예제 출처 - 헤드퍼스트 디자인 패턴

public interface Duck {
    public void quack();
    
    public void fly();
}
public class MallardDuck implements Duck {
    
    @Override
    public void quack() {
        System.out.println("꽥!");
    }
    
    @Override
    public void fly() {
        System.out.println("날고 있어요!");
    }
}
public interface Turkey {
    public void gobble();
    
    public void fly();
}
public class WildTurkey implements Turkey {
    
    @Override
    public void gobble() {
        System.out.println("골골");
    }
    
    @Overrdie
    public void fly() {
        System.out.println("짧게 나는중!");
    }
}
public class TurkeyAdapter implements Duck {
    private Turkey turkey;
    
    public TurkeyAdapter(Turkey turkey) {
        this.turkey = turkey;
    }
    
    @Override
    public void quack() {
        turkey.gobble();
    }
    
    @Override
    public void fly() {
        for(int i = 0; i < 5; i++)
            turkey.fly();
    }
}
public class DuckMain {
    public static void main(String[] args) {
        Turkey turkey = new WildTurkey();
        
        System.out.println("칠면조가");
        turkey.gobble();
        turkey.fly();
        
        Duck duck = new MallardDuck();
        
        System.out.println();
        System.out.println("오리가");
        testDuci(duck);
        
        Duck turkeyAdapter = new TurkeyAdapter(turkey);
        
        System.out.println();
        System.out.println("칠면조 어댑터가");
        testDuck(turkeyAdapter);
    }
    
    static void testDuck(Duck duck) {
        duck.quack();
        duck.fly();
    }
}

실행해보면

칠면조가 한번 울고 한번 날고, 오리가 한번 울고 한번 날고, 칠면조 어댑터가 한번 울고 5번을 나는것을 볼 수 있다.

 

Duck은 quack()과 fly()메소드를, Turkey는 gobble(), fly() 메소드를 갖고 있고

각각 MallardDuck과 WildTurkey라는 구현체를 갖고 있다.

각 구현체에서는 울음소리와 날아가는 것을 한번씩 출력하도록 되어있다.

 

그리고 칠면조 어댑터가 존재한다.

이 어댑터의 역할은 칠면조를 Duck과 같이 사용하도록 하는 것이다.

메인 클래스의 testDuck 메소드에서는 Duck 타입의 매개변수를 받아 quack()과 fly()를 호출하고 있다.

이 testDuck 메소드는 들어오는 매개변수가 오리인지 칠면조인지 전혀 구분하지 못하지만 다른 결과를 출력하게 된다.

그럼 이 결과로 gobble()과 fly()를 갖고 있는 Turkey 인터페이스가 어댑터를 통해 Duck과 같은 quack(), fly()로 변환되어 동작하고 있다고 볼 수 있고, testDuck 메소드는 타깃 인터페이스인 Duck를 통해 어댑터에게 요청을 보낼 뿐 어댑터에 연결된 반대쪽 객체가 무엇인지에 대해서는 알 수 없다.

 

정리.

어댑터 패턴은 새로운 객체를 추가하여 연결해주는 목적이라기 보다 호환이 되지 않는 서로 다른 인터페이스(오리와 칠면조)를 어댑터를 통해 연결하여 사용할 수 있도록 하는 패턴이다.

JAVA 언어로 배우는 디자인 패턴 입문, 헤드퍼스트 디자인 패턴을 통해 학습 중이며 두 책의 예제 위주로 정리.

 

생성패턴(Creational Pattern)

1. 싱글톤(Singleton)

2. 빌더(Builder)

3. 팩토리 메소드(Factory Method)

4. 추상 팩토리(Abstract Factory)

5. 프로토타입(Prototype)

 

구조패턴(Structural Pattern)

1. 어댑터(Adapter)

2. 브릿지(Bridge)

3. 컴포지트(Composite)

4. 데코레이터(Decorator)

5. 퍼사드(Facade)

6. 플라이웨이트(flyweight)

7. 프록시(Proxy)

 

행동(행위) 패턴(Behavioral Pattern)

1. 책임 연쇄(Chain of Responsibility)

2. 커맨드(Command)

3. 인터프리터(Interpreter)

4. 반복자(Iterator)

5. 중재자(Mediator)

6. 메멘토(Memento)

7. 옵저버(Observer)

8. 상태(State)

9. 전략(Strategy)

10. 템플릿 메소드(Template Method)

11. 방문자(Visitor)


 

반복자(Iterator)는 행위패턴(Behavioral Pattern)에 속하는 디자인 패턴이다.

자바에서 배열의 모든 요소를 표현하기 위해서는 반복문을 사용해 처리하는 경우가 많다.

for문을 통해 사용하게 되는 경우가 많고 이 경우 i 라는 인덱스 변수를 사용해 i를 증가시켜가며 검색하게 되는데

이 인덱스 변수인 i 의 기능을 추상화 하여 일반화 한 것을 디자인 패턴에서는 Iterator 패턴이라고 한다.

무엇인가 많이 모여있을 때 이를 순서대로 가리키며 전체를 검색하고 처리를 반복하는 것이다.

Iterate의 의미는 '반복하다'라는 의미이기 때문에 Iterator를 반복자라고 부른다.

 

예제

예제 프로그램으로는 책장(BookShelf) 안에 책(Book)을 넣고 책 이름을 차례대로 표시하는 프로그램이다.

 

//예제 출처 - JAVA 언어로 배우는 디자인 패턴 입문

public class Book {
    private String name;
    
    public Book(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}
import java.util.Iterator;

public BookShelf implements Iterable<Book> {
    private Book[] books;
    
    private int last = 0;
    
    public BookShelf(int maxSize) {
        this.books = new Book[maxSize];
    }
    
    public Book getBookAt(int index) {
        return books[index];
    }
    
    public void appendBook(Book book) {
        this.books[last] = book;
        last++;
    }
    
    public int getLength() {
        return last;
    }
    
    @Override
    public Iterator<Book> iterator() {
        return new BookShelfIterator(this);
    }
}
import java.util.Iterator;
import java.util.NoSuchElementException;

public class BookShelfIterator implements Iterator<Book> {
    private BookShelf bookShelf;
    
    private int index;
    
    public BookShelfIterator(BookShelf bookShelf) {
        this.bookShelf = bookShelf;
        this.index = 0;
    }
    
    @Override
    public boolean hasNext() {
        if(index < bookShelf.getLength())
            return true;
        else
            return false;
    }
    
    @Override
    public Book next() {
        if(!hasNext())
            throw new NoSuchElementException();
        
        Book book = bookShelf.getBookAt(index);
        
        index++;
        
        return book;
    }
}
import java.util.Iterator

public class BookMain {
    
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(4);
        
        bookShelf.appendBook(new Book("Around the world in 80 Days"));
        bookShelf.appendBook(new Book("Bible"));
        bookShelf.appendBook(new Book("Cinderella"));
        bookShelf.appendBook(new Book("Daddy-Long-Legs"));
        
        //명시적으로 Iterator를 사용하는 방법
        //Iterator 인스턴스를 생성
        Iterator<Book> it = bookShelf.iterator();
        
        //hasNext()가 false가 될때까지.
        //즉, 다음 객체가 존재하지 않을때까지
        //book에 next()로 값을 받아오고 책 이름을 출력한다.
        while(it.hasNext()) {
            Book book = it.next();
            System.out.println(book.getName());
        }
        
        System.out.println();
        
        //확장 for문을 사용하는 경우
        for(Book book : bookShelf)
            System.out.println(book.getName());
    }
}

 

 

 

구조

BookShelf

- 책장을 나타내는 클래스. Iterable<book> 인터페이스를 구현한다.

- books라는 Book 타입 배열이 존재하며, 크기는 BookShelf 인스턴스 생성 시 생성자 인수를 통해 결정된다.

- last는 appendBook() 메소드를 통해 books에 데이터가 추가될때마다 하나씩 증가하게 되며 배열의 인덱스를 담당한다.

- 그 외 메소드로는 배열의 가장 끝에 있는 데이터를 반환하는 getBookAt()과 인덱스를 반환하는 getLength()가 있다.

- 마지막으로 Iterable<Book> 인터페이스를 구현한 iterator() 메소드가 존재한다.

- 이 메소드는 BookShelf 클래스에 대응하는 Iterator로서, BookShelfIterator 클래스의 인스턴스를 생성하여 반환한다.

- 책장에 꽂혀있는 책을 반복해서 처리하고자 할 때 이 iterator() 메소드를 호출.

 

BookShelfIterator

- BookShelf 클래스의 검색을 실행하는 클래스이다.

- 생성자에서는 BookShelf 인스턴스를 인수로 받아 bookShelf 필드에 담아주고 index 필드는 0으로 초기화 한다.

- Iterator<Book> 인터페이스를 구현한다.

- hasNext() 메소든느 다음 책이 있는지 확인하고 다음 책이 있다면 true, 없다면 false를 리턴하도록 한다.

- next() 메소드는 다음 책이 존재하지 않는다면 NoSuchElementException을 발생시키고 존재한다면 해당 객체를 반환한다.

- 반환 전 index 필드 값을 하나 미리 증가시켜 다음 next() 를 준비하도록 한다.

 

Iterable<E>

- 처리를 반복할 대상을 나타내는 것으로 java.lang에 선언되어 있다.

- 이 인터페이스의 구현체는 배열같은 집합체가 된다. 예제에서는 Book을 모은 인터페이스를 사용하기 때문에 Iterable<Book>으로 선언한다.

- Iterable 인터페이스에는 iterator 메소드가 선언되어있는데 집합체에 대응하는 Iterator<E>를 만들도록 하기 위함이다.

- 집합체에 포함된 요소를 하나하나 처리해나가고자 할 때는 iterator 메소드를 사용해 Iterator<E> 인터페이스를 구현한 클래스의 인스턴스를 하나 만든다.

 

Iterator<E>

- 하나하나의 요소 처리를 반복하기 위한 것으로 루프 변수(i) 와 같은 역할을 한다.

- 선언되는 메소드로는 hasNext()와 next()가 있다.

- hasNext()는 다음 요소가 존재하는지 여부에 대한 것이고 next()는 다음 요소를 가져오는 것이다.

- next()의 경우는 보이지 않는 곳에서 다음에 다시 호출되었을 때 다음 요소를 반환할 수 있도록 사전에 미리 준비를 하게 된다.

 

 

예제 프로그램에서의 역할들

Iterator(반복자) 역 - Iterator<Book>

- 요소를 순서대로 검색하는 인터페이스를 결정한다.

- 다음 요소의 존재여부를 파악하는 hasNext()와 다음 요소를 가져오는 next()를 결정한다.

 

ConcreteIterator(구체적인 반복자) 역 - BookShelfIterator

- Iterator 인터페이스를 구현하는 구현체 역할이다.

- 검색에 필요한 정보를 가지고 잇어야 하기 때문에 BookShelf 클래스의 인스턴스를 bookShelfIterator 클래스의 bookShelf 필드에서 기억하고, 검색중인 위치를 index 필드에서 기억한다.

 

Aggregate(집합체) 역 - Iterable<Book>

- Iterator를 만들어내는 인터페이스를 결정한다.

- 이 인터페이스는 내가 가진 요소를 차례대로 검색해 주는 것을 만들어내는 메소드이다.

 

ConcreteAggregate(구체적인 집합체) 역 - BookShelf

- Aggregate가 결정한 Iterable 인터페이스를 실제로 구현하는 구현체 역할이다.

- 구체적인 Iterator 역할인 ConcreateIterator의 인스턴스를 만들어 낸다.

 

 

for 문 대신 Iterator를 사용하는 이유

Iterator도 for문처럼 반복을 통해 다음 값을 가져오게 되는데 굳이 Iterator를 써야 하는 이유가 무엇일까.

이유는 구현과 분리해서 사용할 수 있기 때문이다.

Iterator의 hasNext()와 next()는 Iterator의 메소드일 뿐이지 실질적인 BookShelf의 구현에 사용되는 것은 아니다.

또한 Iterator 인터페이스는 BookShelf의 메소드에 접근해야할 이유도 없다.

 

기존 예제는 배열을 사용하고 있지만 만약 리스트로 수정하고자 한다면?

Iterator를 사용하는 경우에는 BookShelf에서 Book[] 을 List<Book>으로 수정하고 appendBook에서 this.books.add로 수정만 해주면 된다.

import java.util.Iterator;

public BookShelf implements Iterable<Book> {
    //private Book[] books;
    private List<Book> books;
    
    private int last = 0;
    
    public BookShelf(int maxSize) {
        this.books = new Book[maxSize];
    }
    
    public Book getBookAt(int index) {
        return books[index];
    }
    
    public void appendBook(Book book) {
        //this.books[last] = book;
        this.books.add(book);
        last++;
    }
    
    public int getLength() {
        return last;
    }
    
    @Override
    public Iterator<Book> iterator() {
        return new BookShelfIterator(this);
    }
}

이렇게 BookShelf 하나만 수정하면 메인클래스 포함 다른 클래스에서는 수정이 필요없어진다.

반면, Iterator를 사용하지 않는다면 메인 메소드에서는 books[i].getName() 형태로 출력하게 되었을 것이고,

그럼 이렇게 처리되고 있는 모든 것들에 대해 books.get(i) 형태로 수정해야 했을 것이다.

Iterator를 사용하게 되면서 코드 재사용성이 높아졌다고 볼 수 있다.

또한, 자바에서는 사용하지 않는 인스턴스에 대해 GarbegeCollection을 통해 자동으로 삭제되기 때문에 iterate에 대응하는 deleteIterator 메소드는 불필요하다.

 

그리고 메인메소드에서 추가로 하단에 확장 for문을 사용한 것을 볼 수 있다.

확장 for문의 구조를 보면 for(Book book : bookShelf) 로 처리하여 간단하게 Iterator를 통한 처리와 마찬가지로 book.getName()으로 출력할 수 있는것을 볼 수 있다.

이 코드로 알 수 있는 점으로는 확장 for문은 Iterator와 동일한 처리를 하고 있는 것을 알 수 있다.

물론 bookShelf에 Book 배열이 들어가있는 것이기 때문에 동일하게 Iterator가 구현이 되어있어야 이렇게 사용할 수 있게 된다.

단지 hasNext와 next를 굳이 명시하지 않아도 된다는 편의성이 제공되는 것이다.

디자인패턴은 개발하면서 공통적으로 발생하는 문제들을 어떻게 해결 할 것인지에 대한 해결 방안으로 다양한 해결책 중에서 많은 사람들이 인정한 모범사례이다.

바로 전환될 수 있는 완성된 디자인은 아니며, 다른 상황에 맞게 사용될 수 있는 문제들을 해결하는데에 쓰이는 서술이나 템플릿이다.

디자인패턴은 객체지향 4대 특성(캡슐화, 상속, 추상화, 다형성)과 설계원칙(SOLID)을 기반으로 구현되어 있다.

 

디자인패턴은 재사용성, 가독성, 유지보수성, 확장성, 안정성과 신뢰성이라는 장점이 있다.

재사용성은 반복적인 문제에 대한 일반적인 해결책을 제공하는 것으로 이를 재사용하여 유사한 상황에서 코드를 더 쉽게 작성할 수 있다.

가독성은 명확하게 작성하여 개발자가 코드를 이해하고 유지보수하기 쉽게 만드는 것이다.

유지보수성은 코드를 쉽게 모듈화 할 수 있으며, 변경이 필요한 경우 해당 모듈만 수정하는 것으로 유지보수가 가능하도록 하는 것이다.

확장성은 새로운 기능을 추가하거나 변경할 때 기존코드를 변경하지 않고도 새로운 기능을 통합할 수 있는 것이다.

안정성과 신뢰성은 수많은 사람들이 인정한 모범사례로 검증된 솔루션이기 때문에 안정적이고 신뢰성있는 패턴이라고 할 수 있다.

 

 

디자인 패턴의 종류

디자인 패턴으로 가장 유명한 GoF 디자인 패턴에서는  크게 생성패턴(Creational Pattern), 구조패턴(Structural Pattern), 행동(행위)패턴(Behavioral Pattern)으로 분류되며 각각 5개, 7개, 11개의 패턴으로 총 23개의 패턴이 존재한다. 

 

생성패턴(Creational Pattern)

1. 싱글톤(Singleton)

2. 빌더(Builder)

3. 팩토리 메소드(Factory Method)

4. 추상 팩토리(Abstract Factory)

5. 프로토타입(Prototype)

 

구조패턴(Structural Pattern)

1. 어댑터(Adapter)

2. 브릿지(Bridge)

3. 컴포지트(Composite)

4. 데코레이터(Decorator)

5. 퍼사드(Facade)

6. 플라이웨이트(flyweight)

7. 프록시(Proxy)

 

행동(행위) 패턴(Behavioral Pattern)

1. 책임 연쇄(Chain of Responsibility)

2. 커맨드(Command)

3. 인터프리터(Interpreter)

4. 반복자(Iterator)

5. 중재자(Mediator)

6. 메멘토(Memento)

7. 옵저버(Observer)

8. 상태(State)

9. 전략(Strategy)

10. 템플릿 메소드(Template Method)

11. 방문자(Visitor)

 

 

계속해서 디자인패턴을 학습할 계획이며, 포스팅 할 때마다 위 패턴 목록에 링크를 추가할 계획이다.

 

Reference

 

[OOP] 디자인 패턴(Design Pattern)이란? - 장점 및 종류

디자인 패턴(Design Pattern)이란? 디자인 패턴은 개발하면서 발생하는 반복적인 문제들을 어떻게 해결할 것인지에 대한 해결 방안으로 실제 현업에서 비즈니스 요구 사항을 프로그래밍으로 처리하

ittrue.tistory.com

 

소프트웨어 디자인 패턴 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 소프트웨어 디자인 패턴(software design pattern)은 소프트웨어 공학의 소프트웨어 디자인에서 특정 문맥에서 공통적으로 발생하는 문제에 대해 재사용 가능한 해결

ko.wikipedia.org

 

Servlet&JSP에 대해 공부만 해봤지 프로젝트를 진행해본적이 없어서 이번에 한번 해봤다.

설정이나 Annotation 사용에 있어서 조금 고민이 필요했던 부분이 있었을 뿐 크게 문제가 된 부분은 없었지만 파일 업로드 요청을 서버가 처리하는데 있어서는 방법이 달라 이래저래 알아보고 정리한다.

 

개발환경

  • Java8
  • MySQL8.0
  • Servlet 4.0
  • commons-io-2.11.0
  • commons-fileupload-1.5
  • jQuery(Ajax)

 

기능은 게시판으로 제목, 내용, 이미지 파일을 작성하고 출력한다.

요청은 FormData에 담아 Ajax로 요청하도록 처리했다.

 

스프링에서 파일 저장은 @RequestParam Annotation과 List<Multipart>로 간단하게 받을 수 있었지만 Servlet에서는 doPost를 통해 받도록 했기 때문에 어떻게 해야할지 고민이었다.

알아보기 전에 먼저 ajax로 보낸 데이터를 request.getParameter()를 통해 꺼낼 수 있는지 테스트해봤지만 꺼낼 수 없었다.

그래서 이래저래 알아보니 방법이 여러가지가 있었으나 대체로 단일 파일만 처리하는 예제가 많았다.

다중 파일 업로드를 처리하는 방법을 알면 단일 파일 업로드는 쉽게 처리할 수 있기 때문에 조건에 맞는 예제나 팁을 찾았다.

 

예제 참고한 포스팅

https://blog.miyam.net/144

 

Ajax 와 Servlet 을 이용한 파일 업로드

자료를 찾아보면 Spring을 이용한 파일 업로드가 대다수 이며 오렐리에서 제공하는 라이브러리가 주를 이룬다. Spring은 사용하기 싫고 오렐리에서 제공하는 라이브러리는 몇가지 버그가 있는 듯

blog.miyam.net

 

기존 Spring 기반의 프로젝트에서도 commons-io와 commons-fileupload 라이브러리를 통해 처리해서 이걸 그대로 써야 하나 고민하고 있었는데 마침 이 분이 해당 라이브러리를 사용해 처리하는 방법을 남겨두셨었다.

 

//Ajax 요청


$('#imageInsert').on('click', function() {
    var form = $('#uploadForm')[0];
    var formData = new FormData(form);
    
    for(var index = 0; index < Object.keys(files).length; index++) {
        formData.append('files', files[index]);
    }
    
    $.ajax({
        type: 'POST',
        enctype: 'multipart/form-data',
        processData: false,
        contentType: false,
        cache: false,
        url: '/imageBoard/imageInsert',
        data: formData,
        success: function(data) {
            if(data == 0 || data == -1) {
                alert('오류가 발생했습니다.');
            }else {
                location.href='/imageBoard/imageDetail?imageNo=' + data;
            }
        }
    });
});
//Servlet

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/imageBoard/imageInsert")
public class ImageBoardInsertServlet extends HttpServlet {
    private static final long serialVersionUID = 1;
    
    private ImageBoardService imageBoardService = new ImageBoardServiceImpl();
    
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
                                                 throws ServletException, IOException {
    
        request.setCharacterEncoding("UTF-8");
        
        long result = imageBoardService.insert(request);
        PrintWriter out = response.getWriter();
        out.print(result);
    }
    
}
//imageBoardService

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

public class ImageBoardServiceImpl implements ImageBoardService {
    private ImageBoardDao imageBoardDAO = new ImageBoardDaoImpl();
    
    @Override
    public long insert(HttpServletRequest request) {
        ImageBoardInsertDTO dto = new ImageboardInsertDTO();
        List<ImageDataDTO> imageDTOList = new ArrayList<ImageDataDTO>();
        String title = null;
        String content = null;
        HttpSession session = request.getSession();
        String uid = (String) session.getAttribute("id");
        
        try{
            // 1.DiskFileItemFactory
            DiskFileItemFactory = diskFactory = new DiskFileFactory();
            disFactory.setSizeThreshold(4096);
            diskFactory.setRepository(new File(FileProperties.TEMP_PATH));
            
            // 2.ServletFileUpload
            ServletFileUpload upload = new ServletFileUpload(disFactory);
            upload.setSizeMax(FileProperties.FILE_SIZE);
            
            List<FileItem> items = upload.parseRequest(request);
            Iterator<FileItem> iter = items.iterator();
            
            
            while(iter.hasNext()) {
                FileItem item = (FileItem) iter.next();
                
                //3. isFormField() == true
                if(item.isFormField()) {
                    String fieldName = item.getFieldName();
                    
                    if(fieldName.equals("imagetitle"))
                        title = item.getString("UTF-8");
                    else if(fieldName.equals("imageContent"))
                        content = item.getString("UTF-8");
                        
                //4. isFormField() == false
                }else {
                    if(item.getSize() > 0)
                        saveFile(item, imageDTOList, step);
                }
            }
        }catch(Exception e) {
            return -1;
        }
        
        dto = new ImageBoardInsertDTO.ImageBoardInsertDTOBuilder()
                    .imageTitle(title)
                    .imageContent(content)
                    .userId(uid)
                    .build();
        
        return imageBoardDAO.insert(dto, imageDTOList);
    }
    
    public void saveFile(FileItem item, List<ImageDataDTO> imageDTOList) {
        try{
            //5. file name
            String name = item.getFieldName();
            String fileName = item.getName();
            
            StringBuffer sb = new StringBuffer();
            String saveName = sb.append(new SimpleDateFormat("yyyyMMddHHmmss")
                                          .format(System.currentTimeMillis()))
                                .append(UUID.randomUUID())
                                .append(fileName.substring(fileName.lastIndexOf(".")))
                                .toString();
            
            //6. path
            Path filePath = Paths.get(FileProperties.FILE_PATH + "/" + saveName);
            File uploadFile = filePath.toFile();
            item.write(uploadFile);
            
            imageDTOList.add(new ImageDataDTO.ImageDataDTOBuilder()
                                    .imageName(saveName)
                                    .oldName(fileName)
                                    .build();
        }catch(Exception e) {
            e.printStackTrace();
        }
    }
}

1. DiskFileItemFactory

DiskFileItemFactory는 FileItemFactory의 구현체이다.

FileItem Object를 생성하는 클래스이며, 작은 항목은 메모리에 큰 항목은 디스크의 임시 파일로 보관하는 인스턴스를 생성한다.

아무 설정도 하지 않는다면 메모리 버퍼의 기본 임계값은 10KB이다.

그래서 setSizeThreshold(4096)로 4MB의 크기로 설정해줬다. 이때, 매개변수의 단위는 byte단위이다.

데이터의 크기가 이 임계값을 초과하는 경우에는 일시영역에 데이터가 보존된다.

 

setRepository는 임시보관되는 일시영역을 지정한다.

그래서 파일의 저장 경로로 설정해둔 FILE_PATH가 아닌 TEMP_PATH라는 디렉터리를 새로 생성하고 사용하도록 했다.

 

DiskFileItemFactory Document

https://commons.apache.org/proper/commons-fileupload/apidocs/org/apache/commons/fileupload/disk/DiskFileItemFactory.html

 

DiskFileItemFactory (Apache Commons FileUpload 1.5 API)

The default FileItemFactory implementation. This implementation creates FileItem instances which keep their content either in memory, for smaller items, or in a temporary file on disk, for larger items. The size threshold, above which content will be store

commons.apache.org

 

2. ServletFileUpload

Servlet 프로그램을 경유하고 파일 데이터를 취득하는 클래스이다.

ServletFileUpload의 생성은 이전에 생성한 DiskFileItemFactory 인스턴스를 매개변수로 생성한다.

그리고 setSizeMax로 업로드 할 수 있는 최대 용량을 설정해준다.

이때, 이 용량 단위 역시 byte 단위로 설정하게 된다.

무한대로 설정하는 경우에는 -1로 설정해 줄 수 있다.

 

다음으로 FileItem 타입 리스트를 .parseRequest(request)로 생성하게 되는데,

parseRequest의 경우 FileItem의 목록을 얻을 수 있게 해준다.

 

FileItem은 'multipart/form-data'로 들어온 POST 요청 내에서 수신 된 파일 또는 양식 항목을 나타낼 수 있다.

그럼 parseRequest로 FileItem 타입의 리스트를 생성한다는 것은 POST 요청으로 들어온 'multipart/form-data'의 리스트를 생성한다고 볼 수 있다.

그래서 ServletFileUpload 문서에서 역시 FileItem 목록을 얻으려면 해당 메소드를 사용하라고 명시하고 있다.

 

이렇게 만들어준 리스트는 Iterator.next()로 접근해 풀어내게 된다.

 

3. item.isFormField() == true

FileItem에서 isFormFiled()의 경우 파일데이터라면 false를 파일 데이터가 아니라면 true를 반환하도록 되어있다.

현재 이 프로젝트에서 넘어오는 FileItem들로는 텍스트 형태의 제목, 내용이 있고 파일 형태의 이미지 파일로 구성되어있다.

그럼 제목과 내용은 파일 데이터가 아니기 때문에 true를 반환받게 되고 파일 데이터인 이미지 파일의 경우 false를 반환받아 구분할 수 있게 된다.

 

.getFieldName()은 필드명이다. formData의 구조를 보면 imageTitle : "testImageTitle"  이런 형태의 K : V로 구성되어 있다.

여기서 getFieldName()이라는 것은 키값인 필드명을 의미한다.

그리고 getString()은 값을 의미한다. 그래서 testImageTitle이라는 값을 받아올 수 있게 된다.

 

그로인해 이 코드에서는 FieldName이 imageTitle이라면 title 변수에 값을 담아주고, imageContent라면 content 변수에 내용을 담아 추후 DTO에 담을 수 있게 처리가 가능한 것이다.

 

4. item.isFormField() == false

위에서 얘기했듯이 파일 데이터인 경우 false를 반환받는다.

item.getSize()는 이 FileItem의 크기를 체크하는 것이다.

만약 크기가 0이라면 제대로 된 파일이 아니라고 볼 수 있기 때문에 검증하는 정도로 생각할 수 있다.

이 프로젝트에서는 게시글 수정 기능 역시 존재하고 수정 기능 처리에서도 파일 저장 처리를 수행해야 하기 때문에 saveFile() 이라는 메소드에 파일 저장 처리를 분리했다.

 

5. filename

파일 데이터에서 역시 getFieldName은 formData의 키값에 해당하는 필드명을 가져온다.

Ajax 코드를 보면 이미지 파일의 경우 'files'라는 필드명을 사용했기 때문에 files를 가져오게 된다.

 

getName()의 경우 파일명을 가져온다.

기존의 파일명 자체를 가져오기 때문에 여기서는 확장자명을 분리하는것과 oldName이라는 기존 파일명을 저장하는 컬럼에 담기 위해 사용했다.

 

6. Path

Path 인터페이스의 경우 아직 제대로 이해하지는 못했다.

document를 확인해보고 여러 포스팅을 봤을 때는 파일 경로에 대한 처리를 담당하는 인터페이스이다.

 

Paths는 Path 객체로 변환하여 URI를 반환하는데 사용한다.

Paths.get(url) 로 저장 경로를 Path 객체로 변환하는 것이다.

 

Path 인터페이스에 대해서는 전부 이해하지 못했지만 이 코드 처리과정을 보자면

Paths.get(url)로 Path 객체를 만들어준다.

그리고 toFile()로 해당 경로를 나타내는 객체인 Path 객체를 File 타입으로 변환하게 된다.

 

그 후, FileItem.write(File)을 통해 파일을 저장하게 된다.

 

이 Path를 사용하는 방법은 상단에 참고한 포스팅에 있는 코드를 가져다 사용한건데 처음본거고 이해도 좀 어려웠다.

 

다시 좀 정리해보자면

Path filePath = Paths.get(FILE_PATH + "/" + saveName);

여기서 파일 저장 경로를 Path 객체로 변환해 filePath라는 객체를 만들어준다.

 

File uploadFile = filePath.toFile();

그 후, Path객체에서 해당 경로의 객체를 File로 반환하게 되고

 

item.write(uploadFile)

File 객체를 저장해준다.

라고 볼 수 있다.

 

이 과정을 좀 다르게 풀어보기도 했었다.

String saveName = ........;

File uploadFile = new File(FileProperties.FILE_PATH + "/" + saveName);
item.write(uploadFile);

이렇게 처리하더라도 동일하게 처리할 수 있다.

 

Path가 다르게 어떻게 또 쓰임이 있을지는 아직 모르겠지만 적어도 이 처리 과정내에서는 파일 저장 경로 설정 및 파일 객체로의 반환을 처리하는 용도로 사용되고 있다고 볼 수 있지 않나 싶다.

 

 

 

마지막으로 한가지 짚고 넘어가야 할 점!

수정 처리에서는 삭제해야 하는 이미지 파일이 존재할 수 있다.

새로 등록하는 파일의 경우 기존 등록 처리와 동일한 구조로 오겠지만 삭제해야 하는 이미지 파일의 경우 이미지명만 List화 해서 받도록 구현했다.

즉, List<String> 타입으로 넘어오게 되는데, 진짜 아무생각없이 그냥 테스트를 먼저 해봤다.

결과는 삭제해야하는 파일의 리스트도 결국에는 파일 데이터가 아니기 때문에 isFormField에서 true를 반환하게 된다.

isFormField()는 파일 데이터인지 아닌지만 구분한다는 것을 잘 생각해야 한다.

 

 

Reference

참고 포스팅

https://kumbange.tistory.com/180

 

파일업로드 (FileItem, DiskFileItemFactory)

파일업로드 (FileItem, DiskFileItemFactory) Servlet에서 파일 업로드를 행하는 프로그램을 작성한다. Jakarta Commons에서는 파일업로드를 생하는 프로그램을 간단히 작성하는 라이브러리 FileUpload을 제공하

kumbange.tistory.com

 

FileItem Document

https://commons.apache.org/proper/commons-fileupload/apidocs/org/apache/commons/fileupload/FileItem.html

 

FileItem (Apache Commons FileUpload 1.5 API)

A convenience method to write an uploaded item to disk. The client code is not concerned with whether or not the item is stored in memory, or on disk in a temporary location. They just want to write the uploaded item to a file. This method is not guarantee

commons.apache.org

ServletFileUpload Document

https://commons.apache.org/proper/commons-fileupload/apidocs/org/apache/commons/fileupload/servlet/ServletFileUpload.html

 

ServletFileUpload (Apache Commons FileUpload 1.5 API)

High level API for processing file uploads. This class handles multiple files per single HTML widget, sent using multipart/mixed encoding type, as specified by RFC 1867. Use parseRequest(HttpServletRequest) to acquire a list of FileItems associated with a

commons.apache.org

Path Document

https://docs.oracle.com/javase/8/docs/api/java/nio/file/Path.html#getFileName--

 

Path (Java Platform SE 8 )

Returns the parent path, or null if this path does not have a parent. The parent of this path object consists of this path's root component, if any, and each element in the path except for the farthest from the root in the directory hierarchy. This method

docs.oracle.com

Paths Document

https://docs.oracle.com/javase/8/docs/api/java/nio/file/Paths.html

 

Paths (Java Platform SE 8 )

Converts the given URI to a Path object. This method iterates over the installed providers to locate the provider that is identified by the URI scheme of the given URI. URI schemes are compared without regard to case. If the provider is found then its getP

docs.oracle.com

 

'JAVA' 카테고리의 다른 글

멀티쓰레드(Multi Thread)  (0) 2021.02.14
쓰레드(Thread)  (0) 2021.02.13
입출력스트림(IOStream)  (0) 2021.02.12
스트림(Stream)  (0) 2021.02.11
람다식(Lambda)  (0) 2021.02.10

멀티쓰레드 프로그래밍(Multi Thread Programming)이란?

  동시에 여러개의 Thread가 수행되는 프로그래밍이다.

  Thread는 각각의 작업공간(context)를 갖는다.

  공유자원이 있는 경우에는 race condition이 발생하게 된다.

  Critical Section에 대한 동기화(Synchronization)의 구현이 필요하다.

 

임계영역(Critical Section)이란?

  두개 이상의 Thread가 동시에 접근하게 되는 리소스다.

  Critical section에 동시에 Thread가 접근하게 되면 실행결과를 보장할 수 없게 된다.

  그래서 Thread간의 순서를 맞추는 동기화(Synchronization)이 필요하다.

 

동기화(Synchronization)이란?

  임계영역(Critical Section)에 여러 Thread가 접근하는 경우 한 Thread가 수행하는 동안 공유자원을 lock하려

  다른 Thread의 접근을 막는다.

  그렇기때문에 동기화를 잘못 구현하게 되면 교착상태(Deadlock)에 빠질 수 있다.

 

 

교착상태(Deadlock)이란?

  t1과 t2라는 Thread가 존재한다고 한다고 가정한다.

  t1은 t2가 끝날때까지 대기하도록 하는 코드가 중간에 있고 t2도 t1이 끝날때까지 대기하도록 하는 코드가 있다고 하면

  이 두 Thread는 처리하지 못하고 계속해서 대기하게 된다.

  t1은 t2가 끝나지 않았으니 대기하게 되고 t2역시 t1이 끝나지 않았으니 기다리게 되며 교착상태에 빠지게 된다.

  이렇게 서로 무한정으로 대기하고 있는 상태를 교착상태(Deadlock)이라고 한다.

  이것을 방지하기 위해 synchronized 메서드에서는 다른 synchronized 메서드를 부르지 않는것이 좋다.

 

예제코드

 

class Bank {
  private int money = 10000;
  
  public void saveMoney(int save) {
    int m = this.getMoney();
    
    try {
      Thread.sleep(3000);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
    
    setMoney(m + save);
  }
  
  public void minusMoney(int minus) {
    int m = this.getMoney();
    
    try {
      Thread.sleep(200);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
    
    setMoney(m - minus);
  }
  
  public int getMoney() {
    return money;
  }
  
  public void setMoney(int money) {
    this.money = money;
  }
}


class Park extends Thread {
  public void run() {
    System.out.println("start save");
    SyncTest.myBank.saveMoney(3000);
    System.out.println("save Money " + SyneTest.myBank.getMoney());
  }
}


class Parkwife extends Thread {
  public void run() {
    System.out.println("start minus");
    SyncTest.myBank.minusMoney(1000);
    System.out.println("minus money " + SyneTest.myBank.getMoney());
  }
}


public class SyncTest {
  
  public static Bank myBank = new Bank();
  
  public static void main(String[] args) throws InterruptedException {
    Park p = new Park();
    p.start();
    
    Thread.sleep(200);
    
    Parkwife pw = new Parkwife();
    pw.start();
  }
}

  Thread에서 가장 많이 보이는 예제인 금액 인출 예제다.

  통장에는 현재 10,000원이 있고 Park은 3,000원을 입금하고 Parkwife는 1,000원을 출금하려 한다.

  그럼 예상되는 결과값은 Park이 먼저 실행되니까 3,000원을 입금해서 save money 13,000이 출력된 후

  Parkwife가 1,000원을 출금해서 minus money 12,000이 출력될 것이라고 생각할 수 있다.

  하지만 결과값은

  start save

  start minus

  minus money 9000

  save money 13000

  이렇게 출력된다.

  출력 순서로만 본다면 save가 시작 된 후에 save에 대한 결과가 나오기 전에 minus가 수행되었다.

  그리고 minus가 먼저 수행되고 save가 수행되었는데 그래도 결과값은 10,000원에 3,000원을 더한 13,000원이

  출력된다.

  

  코드를 보면 save는 sleep으로 3초를 대기하도록 되어있고 minus는 0.2초를 대기하도록 되어있다.

  그럼 save는 일단 수행하자마자 getMoney()로 10,000원을 가져온다. 그리고 3초간의 대기상태에 빠지게 된다.

  그 사이 minus가 수행되며 똑같이 getMoney()로 10,000원을 가져온다. 그리고 minus는 0.2초간의 대기상태에

  빠지게 되고 대기시간이 짧다보니 save의 대기시간이 채 끝나기도 전에 1,000원을 빼주며 처리를 마무리하고

  wife의 run메서드의 마지막 출력문을 출력하게 되어 minus money 9000을 출력하게 된다.

  그 후에 대기하고 있던 save는 3초의 대기시간이 지난 후에 처리하게 되는데 이때 처리하는 m의 값은

  wife가 인출하고 난 뒤 잔액인  9,000원이 아닌 10,000의 값을 그대로 갖고 있게 되고 그 값에서 3,000을 save하게

  된다. 그래서 save money 13000 을 출력하게 되는것이다.

 

  현실에서의 상황으로 생각해보자면 Park이 은행에 가서 은행 직원이랑 얘기하느라 시간을 좀 더 쓰게 되었고

  Parkwife는 바로 인출해갔다면 인출후의 잔액이 바로 동기화가 되어 Park이 입금하는 금액과 더해져야 한다.

  즉, 이 예제에서는 동기화처리가 전혀 되지 않은 상태이기 때문에 두 Thread가 수행되는 동안 각자의 기준에서만

  결과값을 출력했다는 것이다.

  이렇게 되면 원하는 결과값을 출력할 수 없고 이게 처리 중간과정이었다면 뒤의 후폭풍이 심할 수 있기 때문에

  동기화가 꼭 필요하고 중요한 부분이다.

 

 

  위 예제는 아래와 같이 두가지 방식으로 수정할 수 있다.

// synchronized 메서드 방식

class Bank {
  private int money = 10000;
  
  public synchronized void saveMoney(int save) {
    int m = this.getMoney();
    
    try {
      Thread.sleep(3000);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
    
    setMoney(m + save);
  }
  
  public synchronized void minusMoney(int minus) {
    int m = this.getMoney();
    
    try {
      Thread.sleep(200);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
    
    setMoney(m - minus);
  }
  
  public int getMoney() {
    return money;
  }
  
  public void setMoney(int money) {
    this.money = money;
  }
}


class Park extends Thread {
  public void run() {
    System.out.println("start save");
    SyncTest.myBank.saveMoney(3000);
    System.out.println("save money " + SyncTest.myBank.getMoney());
  }
}


class Parkwife extends Thread {
  public void run() {
    System.out.println("start minus");
    SyncTest.myBank.minusMoney(1000);
    System.out.println("minus money " + SyncTest.myBank.getMoney());
  }
}


public class SyncTest {
  
  public static Bank myBank = new Bank();
  
  public static void main(String[] args) {
    Park p = new Park();
    p.start();
    
    Thread.sleep(200);
    
    Parkwife pw = new Parkwife();
    pw.start();
  }
}

  이렇게 saveMoney와 minusMoney에 synchronized를 붙여준다.

  그럼 이 두 메서드가 속해있는 Bank에 lock이 걸리게 된다.

  하나를 수행하게 되면 다른 Thread의 접근을 막아주는 것이다.

  Park이 수행중이라면 Parkwife는 접근할 수 없게 되는 것이다.

 

  그럼 결과값으로

  start save
  start minus
  save money 13000
  minus money 12000

  이렇게 출력된다.

  처음과 같이 save가 수행되고 처리되기 전에 minus가 실행되었지만 synchronized에 의해 접근할 수 없어서

  save가 끝날때까지 대기하게 되고 save money 13000을 출력해서 처리가 마무리 된 다음에야 수행되어서

  minus money 12000을 출력하는 것이다.

 

//synchronized 수행문(block) 방식

class Bank {
  
  private int money = 10000;
  
  public void saveMoney(int save) {
    
    synchronized(this) {
      int m = this.getMoney();
      
      try {
        Thread.sleep(3000);
      }catch (InterruptedException e) {
        e.printStackTrace();
      }
      
      setMoney(m + save);
    }
  }
  
  public void minusMoney(int minus) {
    
    synchronized(this) {
      int m = this.getMoney();
      
      try {
        Thread.sleep(200);
      }catch (InterruptedException e) {
        e.printStackTrace();
      }
      
      setMoney(m - minus);
    }
  }
  
  public int getMoney() {
    return money;
  }
  
  public void setMoney(int money) {
    this.money = money;
  }
}


class Park extends Thread {
  public void run() {
    System.out.println("start save");
    SyncTest.myBank.saveMoney(3000);
    System.out.println("save money " + SyncTest.myBank.getMoney());
  }
}


class Parkwife extends Thread {
  public void run() {
    System.out.println("start minus");
    SyncTest.myBank.minusMoney(1000);
    System.out.println("minus money " + SyncTest.myBank.getMoney());
  }
}


public class SyncTest {
  
  public static Bank myBank = new Bank();
  
  public static void main(String[] args) throws InterruptedException {
    Park p = new Park();
    p.start();
    
    Thread.sleep(200);
    
    Parkwife pw = new Parkwife();
    pw.start();
  }
}

  이렇게 block방식을 사용할 수 있다.

  block은 메서드 구현부에 작성해 주면 된다.

  이렇게 둘다 block으로 구현해도 되고 saveMoey는 block방식 minusMoney는 메서드 방식으로 구현해도 된다.

  하지만 saveMoney에는 구현했지만 minusMoney에서는 구현하지 않았다면 제대로 동기화되지 않는다.

  block 부분에서 this에 lock을 걸고자 하는 객체를 써주면 되는데 여기서는 Bank에 lock을 걸어야 하므로

  this로 쓴것이다.

public void saveMoney(int save) {
  synchronized(this) {
    int m = this.getMoney();
    
    try {
      Thread.sleep(3000);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
    
    setMoney(m + save);
  }
}

public synchronized void minusMoney(int minus) {
  
  int m = this.getMoney();
  
  try {
    Thread.sleep(200);
  }catch (InterruptedException e) {
    e.printStackTrace();
  }
  
  setMoney(m - minus);
}

  이렇게 작성할 수 있다.

 

  그리고 run메서드에서 lock을 걸수도 있는데

class Park extends Thread {
  public synchronized void run() {
  }
}

  이렇게 run에다가 걸면 리소스에 lock을 걸겠다는 것이기 때문에 소용이 없다.

  만약 run메서드에서 lock을 걸고 싶다면

class Park extends Thread {
  public void run() {
    synchronized(SyncTest.myBank) {
      //구현부
    }
  }
}

  이런 형태로 구현하면 된다.

  이렇게 run메서드에 lock을 걸었을때의 차이점으로는 지금까지의 예제 출력문을 보면 start save   start minus

  이렇게 시작했었다. 즉, save가 시작되고 minus도 실행되었지만 대기하게 되었다는 건데 run메서드에 lock을 걸게되면

  save가 끝나고 난 뒤에 minus가 수행되게 된다.

  그래서 start save    save money 13000   start minus    minus money 12000   이렇게 출력된다.

  run에 lock이 걸려있으니 start minus 조차 출력하지 못하고 대기하게 되기 때문이다.

//run에서 lock

class Bank {
  
  private int money = 10000;
  
  public void saveMoney(int save) {
    
      int m = this.getMoney();
      
      try {
        Thread.sleep(3000);
      }catch (InterruptedException e) {
        e.printStackTrace();
      }
      
      setMoney(m + save);
  }
  
  public void minusMoney(int minus) {
    
      int m = this.getMoney();
      
      try {
        Thread.sleep(200);
      }catch (InterruptedException e) {
        e.printStackTrace();
      }
      
      setMoney(m - minus);
  }
  
  public int getMoney() {
    return money;
  }
  
  public void setMoney(int money) {
    this.money = money;
  }
}


class Park extends Thread {
  public void run() {
    synchronized(SyncTest.myBank) {
      System.out.println("start save");
      SyncTest.myBank.saveMoney(3000);
      System.out.println("save money " + SyncTest.myBank.getMoney());
    }
  }
}


class Parkwife extends Thread {
  public void run() {
    synchronized(SyncTest.myBank) {
      System.out.println("start minus");
      SyncTest.myBank.minusMoney(1000);
      System.out.println("minus money " + SyncTest.myBank.getMoney());
    }
  }
}


public class SyncTest {
  
  public static Bank myBank = new Bank();
  
  public static void main(String[] args) throws InterruptedException {
    Park p = new Park();
    p.start();
    
    Thread.sleep(200);
    
    Parkwife pw = new Parkwife();
    pw.start();
  }
}

 

 

wait()

  리소스가 더이상 유효하지 않은 경우 리소스가 사용가능할 때까지 Thread를 non-runnable상태로 전환한다.

  wait 상태가 된 Thread는 notify가 호출될때까지 기다린다.

 

notify() / notifyAll()

  notify()는 wait상태인 Thread중 한 Thread를 runnable한 상태로 깨운다.

  notifyAll()은 wait상태인 모든 Thread가 runnable한 상태가 되도록한다.

  notify()보다는 notifyAll()을 사용하기를 권장한다고 한다.

  특정 Thread가 통지를 받도록 제어하는 것은 어려우므로 모두 깨운 후 스케쥴러에 CPU를 점유하도록 하는 것이

  좀 더 공평하기 때문이다.

 

예제코드

 

import java.util.ArrayList;

class Library {
  
  public ArrayList<String> books = new ArrayList<String>();
  
  public Library() {
    books.add("Java 1");
    books.add("Java 2");
    books.add("Java 3");
    books.add("Java 4");
    books.add("Java 5");
    books.add("Java 6");
  }
  
  public synchronized String rentBook() {
    Thread t = Thread.currentThread();
    
    String title = books.remove(0);
    System.out.println(t.getName() + " : " + title + " rent");
    return title;
  }
  
  public synchronized void returnBook(String title) {
    Thread t = Thread.currentThread();
    
    books.add(title);
    System.out.println(t.getName() + " : " + title + " return");
  }
}


class Student extends Thread {
  
  public void run() {
    
    try {
      String title = LibraryMain.library.rentBook();
      sleep(5000);
      LibraryMain.library.returnBook(title);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}


public class LibraryMain {

  public static Library library = new Library();
  
  public static void main(String[] args) {
    Student std1 = new Student();
    Student std2 = new Student();
    Student std3 = new Student();
    
    std1.start();
    std2.start();
    std3.start();
  }
}

  결과값은

  Thread-0 : Java 1 rent

  Thread-2 : Java 2 rent

  Thread-1 : Java 3 rent

  Thread-0 : Java 1 return

  Thread-1 : Java 3 return

  Thread-2 : Java 2 return

  이렇게 출력된다.

  순서는 실행할때마다 조금씩 달라지기도 한다.

  이 예제코드에서 shared resource가 되는것은 library다.

  동시에 빌릴일은 별로 없겠지만 synchronized는 rent와 return에 걸어주는것이 좋다.

 

  그럼 이제 여기서 학생이 6명이고 책이 3권밖에 없다고 하면 이 코드를 그대로 사용했을 때

  IndexOutOfBoundException 이 발생한다.

  학생 6명이 책을 빌려달라고 했으나 앞에 있는 학생 3명이 책을 다 빌려갔고 남은 3명은 빌릴 책이 존재하지 않기

  때문에 이런 오류가 발생한다.

  이런 경우에는 리소스가 가능하지 않으면 빌리지 못하도록 하면 되는데 이럴 때 wait()과 notify()를 사용한다.

 

/*
  책은 3권이지만 학생이 6명이라면
  notify()를 사용한 예제
*/

import java.util.ArrayList;

class Library {
  
  public ArrayList<String> books = new ArrayList<String>();
  
  public Library() {
    books.add("Java 1");
    books.add("Java 2");
    books.add("Java 3");
  }
  
  public synchronized String rentBook() throws InterruptedException {
    
    Thread t = Thread.currentThread();
    
    if(books.size() == 0) {
      System.out.println(t.getName() + " waiting start");
      wait();
      System.out.println(t.getName() + " waiting end");
    }
    
    String title = books.remove(0);
    System.out.println(t.getName() + " : " + title + " rent");
    return title;
  }
  
  public synchronized void returnBook(String title) {
    
    Thread t = Thread.currentThread();
    
    books.add(title);
    notify();
    
    System.out.println(t.getName() + " : " + title + " return");
  }
}


class Student extends Thread {
  
  public void run() {
    
    try {
      String title = LibraryMain.library.rentBook();
      
      if(title == null) return;
      
      sleep(5000);
      LibraryMain.library.returnBook(title);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}


public class LibraryMain {
  
  public static Library library = new Library();
  
  public static void main(String[] args) {
    Student std1 = new Student();
    Student std2 = new Student();
    Student std3 = new Student();
    Student std4 = new Student();
    Student std5 = new Student();
    Student std6 = new Student();
    
    std1.start();
    std2.start();
    std3.start();
    std4.start();
    std5.start();
    std6.start();
  }
}

  결과값은 

  Thread-0 : Java 1 rent

  Thread-4 : Java 2 rent

  Thread-5 : Java 3 rent

  Thread-3 waiting start

  Thread-2 waiting start

  Thread-1 waiting start

  Thread-4 : Java 2 return

  Thread-3 waiting end

  Thread-3 : Java 2 rent

  Thread-5 : Java 3 return

  Thread-0 : Java 1 return

  Thread-1 waiting end

  Thread-1 : Java 3 rent

  Thread-2 waiting end

  Thread-2 : Java 1 rent

  Thread-2 : Java 1 return

  Thread-1 : Java 3 return

  Thread-3 : Java 2 return

  이렇게 출력된다.

  이것도 물론 순서는 차이가 있을 수 있다.

 

  만약 책이 없을 때 그냥 아예 빌리지 못하게 할거였으면 rent와 run에서 books.size가 0일때 null을 리턴하도록

  했으면 뒤에 3명의 학생은 아예 처리가 안되도록 할 수도 있다.

  하지만 기다렸다가 빌리겠다고 한다면 대기하도록 해야 하기 때문에 이렇게 처리한다.

  rent에서 books.size가 0이면 책의 재고가 없는 것이므로 wait()으로 대기하도록 한다.

  그리고 다른 Thread가 반납하게 되면 notify()로 대기상태인 Thread를 깨워줌으로써 책을 빌릴 수 있도록

  해준다.

 

 

  notifyAll은 다음과 같이 쓴다.

/*
  책은 3권이지만 학생은 6명이다.
  notifyAll()사용
*/

import java.util.ArrayList;

class Library {
  
  public ArrayList<String> books = new ArrayList<String>();
  
  public Library() {
    books.add("Java 1");
    books.add("Java 2");
    books.add("Java 3");
  }
  
  public synchronized String rentBook() throws InterruptedException {
    Thread t = Thread.currentThread();
    
    while(books.size() == 0) {
      System.out.println(t.getName() + " waiting start");
      wait();
      System.out.println(t.getName() + " waiting end");
    }
    
    String title = books.remove(0);
    System.out.println(t.getName() + " : " + title + " rent");
    return title;
  }
  
  public synchronized String returnBook(String title) {
    Thread t = Thread.currentThread();
    
    books.add(title);
    notifyAll();
    System.out.println(t.getName() + " : " + title + " return");
  }
}


class Student extends Thread {
  
  public void run() {
    
    try {
      String title = LibraryMain.library.rentBook();
      
      if(title == null) return;
      
      sleep(5000);
      LibraryMain.library.returnBook(title);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}


public class LibraryMain {
  
  public static Library library = new Library();
  
  public static void main(String[] args) {
    Student std1 = new Student();
    Student std2 = new Student();
    Student std3 = new Student();
    Student std4 = new Student();
    Student std5 = new Student();
    Student std6 = new Student();
    
    std1.start();
    std2.start();
    std3.start();
    std4.start();
    std5.start();
    std6.start();
  }
}

  결과값은

  Thread-0 : Java 1 rent
  Thread-5 : Java 2 rent
  Thread-3 : Java 3 rent
  Thread-4 waiting start
  Thread-2 waiting start
  Thread-1 waiting start
  Thread-5 : Java 2 return
  Thread-0 : Java 1 return
  Thread-1 waiting end
  Thread-1 : Java 2 rent
  Thread-2 waiting end
  Thread-2 : Java 1 rent
  Thread-4 waiting end
  Thread-4 waiting start
  Thread-3 : Java 3 return
  Thread-4 waiting end
  Thread-4 : Java 3 rent
  Thread-1 : Java 2 return
  Thread-2 : Java 1 return
  Thread-4 : Java 3 return

  이렇게 출력된다.

  역시 순서는 바뀔 수 있다.

 

  notify()를 사용한 경우는 하나씩 깨어나기 때문에 잘 되면 다행이지만 공정하지 못할 수 있다.

  notifyAll()을 사용하면 대기하고 있던 모든 Thread가 깨어난다.

  그럼 반환된 책은 하나인데 모든 스레드가 깨어나면 처음과 같이 IndexOutOfBoundException이 발생할 수 있다.

  그래서 rentBook에서 wait을 걸어주는 부분을 if문이 아닌 while문으로 작성해준다.

  그래서 깨어난 Thread가 못빌리는 상태라면 다시 wait으로 들어갈 수 있도록 해줘야 한다.

  

  결과값을 보면 Thread4, 2, 1이 대기상태로 들어간다.

  그 후에 Thread5와 0이 책을 반납했다. 이때 반납처리가 되면서 대기중이던 4, 2, 1은 모두 깨어난 상태가 되고

  그 중에서 Thread1과 2는 wait이 풀리자마자 도서를 빌렸다.

  하지만 제일 마지막에 깨어난 Thread4는 깨어나서 waiting end를 찍고 책을 빌리려 했지만 books.size가 0이 

  되었으므로 while문을 벗어나지 못하고 다시 wait에 들어가게 된다.

 

  

 

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 자바 객체지향프로그래밍

'JAVA' 카테고리의 다른 글

Servlet&JSP에서 파일 업로드 처리  (0) 2023.10.20
쓰레드(Thread)  (0) 2021.02.13
입출력스트림(IOStream)  (0) 2021.02.12
스트림(Stream)  (0) 2021.02.11
람다식(Lambda)  (0) 2021.02.10

쓰레드(Thread)란?

  실제 프로그램이 수행되는 작업의 최소단위이다.

  하나의 프로세스는 하나 이상의 Thread를 갖게 된다.  

 

  실행중인 프로그램을 프로세스(Process)라고 한다.

  OS로부터 메모리를 할당받고 프로그램이 메모리에 올라간 상태를 Process라고 한다.

  쓰레드는 웹 서버 자체가 멀티쓰레드를 서포트 하기 때문에 자주 사용할만한 일은 별로 없다고 한다.

 

 

  그림처럼 여러개의 Thread가 있고 여러개가 돌아가는 것을 멀티쓰레드(Multi Thread)라고 한다.

  Thread는 CPU를 점유해서 돌아가는데 CPU를 점유할 수 있는 것으로는 스케쥴러가 있다.

  스케쥴러가 Thread에 CPU를 할당해서 Thread가 수행되도록 한다.

 

Thread구현

  Runnable인터페이스 구현

   

    자바는 다중 상속이 허용되지 않으므로 이미 다른 클래스를 상속한 경우 Thread를 만들려면 

    Runnable Interface를 implements 하도록 한다.

  

예제코드

 

class MyThread extends Thread {
  public void run() {
    /*
      아무것도 구현하라고 하지 않지만
      Thread가 스타트되면 Thread는 run메서드가 수행되기 때문에
      run을 구현해야 한다.
    */
    
    int i;
    System.out.println("extends Thread");
    for(i = 0; i <= 200; i++) {
      System.out.print(i + "\t");
      
      try{
        sleep(100);
      }catch (InterruptedException e) {
        e.printStackTrace();
      }
      /*
        sleep은 InterruptedException이 발생해서
        깨어날 수 있는 가능성이 있기 때문에 처리해줘야 한다.
        100이라는 것은 0.1초씩 잠들었다 깨어나면서 수행하도록 한다는 것.
        sleep은 Thead메서드의 static클래스다.
        여기서 sleep을 사용할 수 있다는 것은 이 클래스가
        Thread를 상속받았다는 것을 의미한다.
      */
    }
  }
}


class MyThread2 implements Runnable {
  
  @Override
  public void run() {
    int i;
    System.out.println("impl Runnable");
    for(i = 0; i <= 200; i++) {
      System.out.print(i + "\t");
      
      try{
        Thread.sleep(10);
      }catch (InterruptedException e) {
        e.printStackTrace();
      }
      /*
        Thread를 상속받지 않고 Runnable을 implements해도
        run을 구현해야 한다.
        단, 여기서는 sleep을 바로 사용할 수 없고
        Thread.sleep() 형태로 사용해야 한다.
        아니면 import해서 sleep으로 사용할 수 있다.
        import static java.lang.Thread.sleep;
      */
    }
  }
}


public class ThreadTest {
  
  public static void main(String[] args) {
    
    System.out.println("start");
    
    //Thread 상속클래스 사용
    MyThread th1 = new MyThread();
    MyThread th2 = new MyThread();
    
    th1.start();
    th2.start();
    
    //Runnable impl 클래스 사용
    MyThread2 runner1 = new MyThread2();
    Thread th3 = new Thread(runner1);
    th3.start();
    
    MyThread2 runner2 = new MyThread2();
    Thread th4 = new Thread(runner2);
    th4.start();
    
    Thread t = Thread.currentThread();
    System.out.println(t);
    
    System.out.println("end");
  }
}
/*
  Runnable을 implements해서 사용한다면
  Thread 객체를 만들고 Thread 인스턴스에
  Runnable 객체를 넣어서 돌려야 한다.
  상속 클래스 사용하는것과 결과는 같다.
*/

  위 예제에서 상속클래스를 사용하는것이나 Runnable 클래스를 사용하는것 둘중 하나는 주석처리하고 실행하는것이

  결과확인이 편하다.

  어떤 것으로 실행을 하던 start와 end가 출력 된 후 0부터 200까지 횡으로 출력되는데 0 0 1 1 2 2 3 3 4 4 

  이런 형태로 출력되는 것을 확인할 수 있다.

  th1와 th2를 실행했으니 그런것이고 중간에 숫자 순서가 섞이는 경우도 있지만 둘씩 붙어서 200까지 출력하게 된다.

  Runnable를 사용한 코드도 마찬가지 형태로 출력된다.

 

  main안에서 돌아가는 쓰레드는 총 세개다.

  메인쓰레드 그리고 메인안에서 두개의 쓰레드가 생성된다.

  그래서 제일 먼저 end를 찍으며 종료되는것이 메인쓰레드다.

  메인쓰레드가 하는 일은 start를 찍고 쓰레드를 두개 만들고 end를 찍은 뒤 끝난다.

  그리고나서 th1, th2가 수행된다. 그래서 start와 end가 먼저 출력 된 뒤에 run에 구현한 0부터 200까지의 반복문이

  출력되는 것이다.

 

  중간에 Thread.currentThread()가 있는데 우선순위를 확인할 수 있다.

  출력은 Thread[main, 5, main]으로 출력되는데 우선순위가 5번째라는 것이다.

  [쓰레드 이름, 우선순위, 어느그룹에 속해있는지] 이러한 내용이 출력된다.

 

  Thread우선순위는

  Thread.MIN_PRIORITY(=1) ~ Thread.MAX_PRIORITY(=10) 이렇게 있다.

  기본적으로 디폴트 우선순위는 Thread.NORM_PRIORITY(=5)로 5를 갖게 된다.

  setPriority(int newPriority)로 우선순위를 지정할 수 있고

  int getPriority()로 가져올 수 있다.

  우선순위가 높은 Thread는 CPU를 배분받을 확률이 높다.

Thread t = Thread.currentThread();

int a = 2;

t.setPriority(a);
t.setPriority(3);

t.getpriority();

Thread.currentThread().setPriority(a);
Thread.currentThread().setPriority(3);

Thread.currentThread().getPriority();

  이렇게 사용할 수 있다.

 

join()메서드

  다른 Thread의 결과를 보고 진행해야 하는 일이 있는 경우 join()메서드를 활용한다.

  join()메서드를 호출한 Thread가 non-runnable상태가 된다.

  예제코드

 

public class JoinTest extends Thread {
  
  int start;
  int end;
  int total;
  
  public JoinTest(int start, int end) {
    this.start = start;
    this.end = end;
  }
  
  public void run() {
    int i;
    for(i = start; i <= end; i++) {
      total += i;
    }
  }
  
  public static void main(String[] args) {
    
    JoinTest jt1 = new JoinTest(1, 50);
    JoinTest jt2 = new JoinTest(51, 100);
    
    jt1.start();
    jt2.start();
    
    int total = jt1.total + jt2.total;
    
    System.out.println("jt1.total = " + jt1.total);
    System.out.println("jt2.total = " + jt2.total);
    
    System.out.println("total = " + total);
  }
}

    결과값은

    jt1.total = 0

    jt2.total = 3775

    total = 0

    이렇게 출력된다.

 

    강의에서는 jt1.total = 1275 total = 1275로 출력되었지만 계속 둘다 0으로 출력되었다.

    jt1과 total은 계속 같은 값으로 출력이 되는데 jt2가 수행이 되기 전에 total이 먼저 출력되기 때문이다.

    또 어떠한 경우는 jt1이 1275로 출력되지만 total은 0으로 출력되는 경우도 있는데

    그만큼 수행 순서가 변동이 계속 생긴다는 의미다.

    하지만 원하는 결과값을 출력하기 위해서는 jt1을 수행하고 jt2를 수행한다음 결과값을 더한 total을 처리하도록

    해야하는데 이럴때는 아래와 같이 join()메서드를 이용해 처리한다.

 

public class JoinTest extends Thread {
  
  int start;
  int end;
  int total;
  
  public JoinTest(int start, int end) {
    this.start = start;
    this.end = end;
  }
  
  public void run() {
    int i;
    for(i = start; i <= end; i++) {
      total += i;
    }
  }
  
  public static void main(String[] args) {
    
    JoinTest jt1 = new JoinTest(1, 50);
    JoinTest jt2 = new JoinTest(51, 100);
    
    jt1.start();
    jt2.start();
    
    try {
      jt1.join();
      jt2.join();
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
    
    int total = jt1.total + jt2.total;
    
    System.out.println("jt1.total = " + jt1.total);
    System.out.println("jt2.total = " + jt2.total);
    
    System.out.println("total = " + total);
    
  }
}

    jt1.total = 1275

    jt2.total = 3775

    total = 5050

    이렇게 출력된다.

    total은 join이 끝나야 수행되기 때문에 연산이 전부 진행된 다음 total의 연산이 수행된다.

    위에 있는 ThreadTest에서의 결과값을 보면 start와 end가 출력 된 다음 쓰레드가 수행되었었다.

    join은 그렇게 main쓰레드가 먼저 수행된 다음 내부 쓰레드를 수행하도록 하지 않고

    내부 쓰레드가 모두 수행될때까지 대기하는 역할을 한다.

 

interrupt()메서드

  다른 Thread에 예외를 발생시키는 interrupt를 보낸다.

  Thread가 join, sleep, wait 메서드에 의해 블록킹 되었다면 interrupt에 의해 다시 runnable상태가 될 수 있다.

  즉, join, sleep, wait은 try catch로 많이 처리하는데 interrupt를 만나면 InterruptedException이 되어

  catch에 있는 코드를 처리하게 된다는 것이다.

 

  예제코드

 

public class InterruptTest extends Thread {
  
  public void run() {
    int i;
    
    for(i = 0; i < 100; i++) {
      System.out.println(i);
    }
    
    try {
      sleep(5000);
    }catch (InterruptedException e) {
      System.out.println(e);
      System.out.println("Wake!!");
    }
  }
  
  public static void main(String[] args) {
    
    IntteruptTest test = new InterruptTest();
    
    test.start();
    test.interrupt();
    
    System.out.println("end");
  }
}

    end 출력 이후 0부터 99까지 출력 한 뒤 java.lang.InterruptedException : sleep interrupted     Wake!!

    이렇게 출력된다.

    test.interrupt() 로 인해 sleep상태에서 5초간 기다리는 것이 아닌 Exception으로 빠지게 된다.

    어떤 Thread가 sleep이나 join, wait 상태에서 interrupt() 메서드를 호출하면 Exception이 발생하게 되면서

    처리하게 된다고 했기 때문에 InterruptedException이 발생했다고 출력되고 Wake!!가 출력된 것이다.

    여기서는 그냥 Wake!!라는 출력문을 작성했지만 처리하는 코드를 적어두면 되는것이다.

 

 

Thread 종료하기

  데몬 등 무한 반복하는 Thread가 종료될 수 있도록 run메서드 내의 while문을 활용해서 종료하도록 한다.

  이때, Thread.stop()은 사용하지 않는다.

import java.io.IOException;

public class TerminateThread extends Thread {
  
  private boolean flag = false;
  int i;
  
  public TerminateThread(String name) {
    super(name);
    //Thread Constructor 중에 Thread이름을 받을 수 있는 Constructor가 있다.
  }
  
  public void run() {
    while(!flag) {
      
      try {
        sleep(100);
      }catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    
    System.out.println(getName() + " end");
  }
  
  public void setFlag(boolean flag) {
    this.flag = flag;
  }
  
  public static void main(String[] args) throws IOException {
    TerminateThread threadA = new TherminateTread("A");
    TerminateThread threadB = new TherminateTread("B");
    
    threadA.start();
    threadB.start();
    
    int in;
    
    while(true) {
      in = System.in.read();
      if(in == 'A') {
        threadA.setFlag(true);
      }else if(in == 'B') {
        threadB.setFlag(true);
      }else if(in == 'M') {
        threadA.setFlag(true);
        threadB.setFlag(true);
        break;
      }
    }
    System.out.println("main end");
  }
}

    실행 후 A를 입력하면 A end  B를 입력하면 B end  M을 입력하면 main end가 출력된다.

    실행하자마자 M을 먼저 입력한다면 A end   B end   main end 가 출력된다.

    그리고 A를 입력해서 A end 를 본 후 다시 A를 입력하면 아무것도 출력되지 않는다. B도 마찬가지.

    

    이 예제에서 run이 데몬이라고 볼 수 있다. 그럼 main에서 A를 입력했을 때 Flag를 true로 set해주었고

    그럼 run에서 !flag로 반복문을 수행하도록 했기 때문에 true일때 멈추고 빠져나오게 된다.

    그렇게 빠져나오게 되면서 end를 찍어주게 되는 것이다.

 

    IDE에서 실행하고 보면 M을 입력한 뒤에는 실행이 종료되는것을 확인할 수 있다.

    그럼 결국 다 잘 꺼졌다는 것.

    

 

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 자바 객체지향프로그래밍

'JAVA' 카테고리의 다른 글

Servlet&JSP에서 파일 업로드 처리  (0) 2023.10.20
멀티쓰레드(Multi Thread)  (0) 2021.02.14
입출력스트림(IOStream)  (0) 2021.02.12
스트림(Stream)  (0) 2021.02.11
람다식(Lambda)  (0) 2021.02.10

+ Recent posts