Hi yoahn 개발블로그

디자인패턴 중간 정리 본문

sswu

디자인패턴 중간 정리

hi._.0seon 2022. 4. 3. 22:19
반응형

1. Iterator 패턴

  • for 문 루프 변수 i 의 역할을 추상화해서 일반화시킨 것
  • 무엇인가 많이 모여있는 것 중에서 하나씩 끄집어내어 열거하면서 전체를 처리하는 일을 할 때 이 패턴을 적용

예제 프로그램 - 책꽂이

  • 책꽂이에 책을 넣은 후, 순서대로 하나씩 다시 끄집어 내서 책 이름을 표시하는 프로그램

책꽂이 프로그램

  • Aggregate 인터페이스
    -> Iterator 객체를 생성하는 추상 메소드 가짐
  • Iterator 인터페이스
    • hasNext() : 다음 메소드가 있는지 체크하는 추상 메소드
    • next(): 다음 원소를 꺼내는 추상 메소드
  • BookShelf 클래스  - Aggregate 구현
    • books
      Book 클래스의 배열
    • last
      Book 객체들이 담긴 배열 사이즈
    • getBookAt(int index)
    • appendBook(Book book)
    • getLength()
    • iterator()
      : 책꽂이의 책 하나하나를 끄집어내는 BookShelfIterator를 생성
      -> 자기 자신 정보를 인자로 넘김 (this)
  • Book
    책을 나타내는 클래스
  • BookShelfIterator 클래스  - Iterator 구현
    • bookShelf
      BookShelf 객체를 가짐
    • index
      반환할 객체 인덱스 정보
    • hasNext()
      index < length 이면 true
    • next()
      현재 객체를 반환하고 index + 1
더보기
public interface Aggregate {
    public abstract Iterator iterator();
}

public interface Iterator {
    public abstract boolean hasNext();
    public abstract Object next();
}
public class BookShelf implements Aggregate {
	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;
    }
    public Iterator iterator() {
        return new BookShelfIterator(this);
    }
}
public class BookShelfIterator implements Iterator {
    private BookShelf bookShelf;
    private int index;
    public BookShelfIterator(BookShelf bookShelf) {
        this.bookShelf = bookShelf;
        this.index = 0;
    }
    public boolean hasNext() {
        if (index < bookShelf.getLength()) {
            return true;
        } else {
            return false;
        }
    }
    public Object next() {
        Book book = bookShelf.getBookAt(index);
        index++;
        return book;
    }
}
public class Book {
    private String name;
    public Book(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

 

public class Main {
    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 it = bookShelf.iterator();
        while (it.hasNext()) {
            Book book = (Book)it.next();
            System.out.println(book.getName());
        }
    }
}

BookShelf 객체 생성 후 bookshelf에서 iterator 생성하여 순회

1.1 역할

1) Iterator의 역할

  • 원소를 하나씩 끄집어낼 때 사용할 공통된 메소드를 선언한 인터페이스
  • hasNext(), next()

2) ConcreteIterator의 역할

  • Iterator 를 구현한 클래스
  • BookShelfIterator
  • 검색하기 위한 정보인 BookShelf 를 bookshelf 필드에 가지고 있어야 함

3) Aggregate(집합체)의 역할

  • Iterator를 만들어내는 인터페이스를 제공
  • iterator(): 내가 가지고 있는 각 원소들을 차례로 검색해줄 반복자를 만들어내는 메소드

4) ConcreteAggregate의 역할

  • Aggregate 인터페이스를 구현하는 클래스
  • ConcreteIterator 객체를 생성한다 (BookShelf)

1.2 Iterator를 사용하는 이유

  • 구현과 분리
    -> 추상클래스 & 인터페이스를 사용하여 프로그래밍 (재사용성, 유지보수성)
  • 집합체가 원소를 어떻게 유지하고 있든지 상관없이, 집합체의 원소를 차례로 끄집어내고자 하면, Iterator의 hasNext()와 next()메소드를 사용
    • BookShelf가 Book을 배열이 아닌 Vector에 저장하는것으로 변경하더라도, Main 클래스의 부분을 변경하지 않아도 된다.
      -> BookShelf의 구현에 의존하지 않음
    • Main에서 BookShelfIterator가 아닌 Iterator 타입을 사용한 이유도, BookShelfIterator의 구현에 의존하지 않는 것
  • 집합체의 각 원소를 끄집어내는 방법이, 집합체 구현과 무관하다.
  • 디자인패턴은 클래스의 재사용성을 높인다.
  • 가능한 추상클래스 & 인터페이스를 자주 사용하여 결합도를 낮춘다.
    • 구체적인 클래스만으로 프로그래밍하면 클래스간의 결합도가 강해져 재사용성이 낮아짐
    • 코드 일부 수정으로 수정해야 될 다른 부분들을 최소화하는 것이 중요
  • Aggregate와 Iterator의 대응 관계
    • Aggregate 클래스와 Iterator 클래스는 밀접하게 관련이 있듯이, BookShelf와 BookShelfIterator 도 밀접한 관계가 있다.
      • BookShelfIterator 가 BookShelf 구현을 알고 있어야 한다.
      • ex) BookShelf 의 getBookAt() 메소드의 이름을 getBookFrom()으로 바꾸면, BookShelfIterator 의 next() 내부도 수정해야 한다.
    • Aggregate-Iterator 쌍을 이루듯 BookShelf - BookShelfIterator도 쌍을 이루는 밀접한 관계이다.
  • Iterator는 여러종류를 만들 수 있다.
    ex) 역방향으로 원소를 순회하는 이터레이터

연습문제

  • Vector, ArrayList 는 max size 를 지정하지 않으면 10개로 설정된다.
  • max size를 넘으면 자동으로 늘어남 => 동적 배열
  • 배열: 정적 메모리 할당
  • ArrayList = 비동기 , Vector = 동기 (한 스레드만 접근 가능)

2. Adapter 패턴

  • 이미 제공되어있는 것을 그대로 사용할 수 없는 경우
  • '이미 제공되어 있는 것'과 '필요한 것' 사이의 간격을 메우는 것
    (서로 다른 두개의 인터페이스 사이를 연결)
  • 두가지 종류
    • 상속을 이용
      클래스에 의한 패턴

    • 위임을 이용
      인스턴스에 의한 패턴

2.1 상속을 이용한 Adapter 패턴

상속을 이용한 Adapter 패턴 클래스 다이어그램

  • Banner 클래스
    • showWithParen()
      문자열 앞뒤에 괄호를 쳐서 표시하는 메소드
    • showWithAster()
      : 문자열 앞뒤에 '*'를 붙여서 표시하는 메소드
    • 이 두 메소드를 '이미 제공되어 있는 것'으로 가정
  • Print 인터페이스
    • printWeak()
      : 문자열을 약하게 표시 (괄호 붙임)
    • printStrong()
      : 문자열을 강하게 표시 (* 붙임)
    • 이 두 메소드를 '필요한 것'이라고 가정
  • 목표
    • Banner 클래스라는 기존의 클래스를 이용해서 Print 인터페이스를 구현하는 클래스를 만든다.
    • 새로 만든 인터페이스를 구현할 때, 이미 구현된 클래스를 상속받아 인터페이스를 구현한다.

상속을 이용한 예제 프로그램

  • Main 클래스
    • Print p = new PrintBanner()
      • Print 인터페이스를 사용함으로써 실제 일을 하는 Banner 클래스의 메소드는 Main클래스에서는 볼 수 없음
      • Main클래스를 수정하지 않고도, PrintBanner 의 구현을 수정할 수 있다.
      • 확장성을 위해서 추상화를 사용하여 유지보수성을 높인다.

2.2 위임을 이용한 Adapter 패턴

  • 위임
    : 내가 할 일을 누군가에게 맡긴다.
    • 예제에서 -> PrintBanner 가 할 일을 Banner 클래스의 인스턴스에게 맡긴다.
      (메소드의 실제 처리를 다른 객체의 메소드를 호출해서 처리)
  • 예제 1과 달리 Print 가 클래스이면,
    • PrintBanner 가 Print 와 Banner 의 하위 클래스로 정의할 수 없다. (다중 상속 불가능)
    • 위임 사용 (멤버변수 추가) Banner banner;
  • Print 클래스
    추상 클래스로 정의
  • PrintBanner
    • banner 필드가 Banner 클래스의 인스턴스
    • printWeak()
      • banner.showWithParen() 을 호출
      • PrintBanner 자신이 일을 처리하지 않고, banner에게 위임
    • printStrong()
      • banner.showWithAster() 을 호출
        -> Banner에게 일을 위임하는 것

위임을 이용한 예제 프로그램

Adapter 패턴에 등장하는 역할

  • Target
    : 필요한 메소드를 제공하는 역할 (Print)
    -> 꼭 이 클래스의 메소드를 써야하는 경우
  • Client
    : Target 역할의 메소드를 이용하는 역할 (Main)
  • Adaptee
    : 이미 준비되어 있는 메소드를 제공하는 역할 (Banner)
  • Adapter
    : Target 역할을 실제로 충족시키는 역할 (PrintBanner)

상속을 이용한 패턴
위임을 이용한 패턴

  • 어떤 경우에 사용할까
    • 이미 존재하는 클래스를 부품으로 재사용
    • 기존 클래스가 충분히 테스트 되어있을 때 더욱 좋다
  • 비록 소스가 없더라도, 기존 클래스 수정 없이 원하는 인터페이스에 기존의 클래스를 맞출 수 있다.
  • 특히 기존 클래스의 소스 코드를 몰라도, 메소드의 프로토타입만 알면 어댑터 패턴을 적용할 수 있다.

3. Template Method

  • 템플릿이란?
    • 문자 모양을 따라 구멍이 뚫려있는 얇은 플라스틱 판
    • 필기도구의 종류에 따라 실제로 쓰여지는 문자의 인스턴스가 결정된다.
  • 상위 클래스에, 템플릿 역할을 하는 메소드가 정의됨
    • 그 메소드에서는 추상 메소드들을 사용
    • 상위 클래스에서는 정의되지 않은 메소드가 호출되는 것은 보이지만, 어떻게 처리되는지는 알 수 없음
      (구현되지 않은 메소드를 호출함)
    • 추상 클래스에서 템플릿 메소드를 구현할 때 추상 메소드를 호출
      -> 로직을 공통화 할 수 있다.
  • 하위 클래스가 추상 메소드를 구현한다 == 오버라이딩
    • 하위 클래스에서 어떤 구현을 하더라도, 처리의 큰 흐름은 상위 클래스가 결정한대로 이루어진다.
  • 상위 클래스에서 처리의 뼈대를 결정하고, 하위 클래스에서 구체적인 내용을 결정하는 디자인 패턴

open, print, close == 오버라이딩, 다형성
AbstractDisplay code

  • AbstractDisplay
    • abstract class
      • 추상 메소드 외에 final 메소드를 정의할 수 있다.
      • final 메소드는 하위클래스에서 오버라이딩 할 수 없음을 의미
      • 추상 클래스는 인스턴스를 만들 수 없다.
      • 추상 메소드를 템플릿 메소드에서 호출하지만, 구현은 하위 클래스에서 담당한다.
    • interface 를 사용해도 된다.
      • 인터페이스에는 default 메소드를 사용하여 구현 메소드를 포함시킬 수 있으므로 final 메소드와 비슷한 역할을 한다.
      • default 메소드는 오버라이딩이 가능하다.
        (추상클래스 final 메소드는 오버라이딩 불가능)
  • protected 메소드
    : 상속관계 및 동일 패키지에 있는 클래스에서만 호출 가능

CharDisplay

public class StringDisplay extends AbstractDisplay {    
    private String string;                     
    private int width;                             
    public StringDisplay(String string) {           
        this.string = string;                       
        this.width = string.getBytes().length;
    }
    public void open() {    
        printLine();        
                            
    }
    public void print() {                              
        System.out.println("|" + string + "|");     
    }
    public void close() {                 
        printLine();                                    
    }
    private void printLine() {                 
        System.out.print("+");               
        for (int i = 0; i < width; i++) {  
            System.out.print("-");    
        }
        System.out.println("+");           
    }
}
  • StringDisplay
    • string.getBytes().length
      : 문자열의 바이트 갯수를 얻는다.
      • string = "안녕하세요"
        -> string.length() == 5
        -> string.getBytes().length == 10
      • 문자열 길이가 영어랑 한글이 달라서 적용
  • 다형성
    1. 상위 클래스로 하위클래스 타입을 가리킴
    2. 상위클래스 타입 변수로 오버라이딩 된 함수를 호출 == 다형성

연습문제

  1. java.io.InputStream 클래스의 abstract int read(); 메소드가 추상 메소드
    int read(byte[] b, int off, int len) 메소드가 read() 메소드를 호출하여 사이즈만큼 읽어들임
    -> template method
  2. final func()
    오버라이딩이 불가능한 메소드
  3. protected
    - 자식 클래스와 같은 패키지에 있는 클래스에서 호출이 가능함
    - 상속 관계에 있는 하위 클래스에서 호출 가능
    - 다른 패키지에서는 호출 불가
  4. 추상클래스 대신 인터페이스를 사용할 수 있다
    인터페이스에 구현 메소드를 추가할 수 있게 되었기 때문에

4. Factory Method 패턴

하위 클래스에서 인스턴스 만들기

  • Template Method를 이용한 패턴
    • 인스턴스를 생성하는 공장을 Template 메소드 패턴으로 구성
    • 인스턴스를 만드는 방법은 상위 클래스에서 결정
    • 인스턴스를 실제로 생성하는 일은 하위 클래스에서 결정
  • 구체적인 제품 생성 -> 공장을 통해서 함

4.1 예제 프로그램 - 신분증 만드는 공장

클래스 다이어그램

  • framework 패키지
    • Product - 추상 클래스
      • 추상메소드 use()
      • 생성된 제품이 가지고 있어야 할 인터페이스를 결정하는 추상클래스
        (구체적인 역할은 하위 클래스인 ConcreteProduct 역할이 결정)
    • Factory - 추상 클래스
      • create() 구현
        createProduct(), registerProduct() 추상 메소드 호출하여 템플릿 메소드 형태
      • Product 클래스 타입의 인스턴스를 생성하는 추상 클래스 (Creator)
        (실제 제품을 생성하는 ConcreteCreator의 역할에 대해서는 모름)
  • idcard 패키지
    • IDCard (제품)
      • use() 구현
      • ConcreteProduct 역할
      • 생성자를 default 로 접근 => 외부 패키지에서는 인스턴스 생성 불가
    • IDCardFactory (공장)
      • createProduct(), registerProduct() 구현
      • ConcreteCreator 역할
  • IDCard 객체를 Main 클래스에서 직접 생산할 수도 있다.
  • 그러나 Factory Method 패턴을 이용해서
    IDCard 객체가 필요하면, IDCardFactory 를 통해서 IDCard 제품을 생산

4.2 Factory method 사용 이유

  1. 코드 분리
    • 같은 프레임워크를 이용해서 다른 공장과 다른 제품을 추가로 정의할 수 있다
    • TV + TV 공장
      Main에서 TVFactory 객체를 생성한 후에 TVFactory 객체의 create() 메소드를 호출하기만 하면 된다.
  2. 각 공장이 어떤 제품을 어떻게 생산하는지 클라이언트는 모른다.
    • 생산하는 제품이(IDCard->IDCard2) 바뀌어도, IDCardFactory 객체를 생성하고, create()만 호출하면 된다.
  3. Factory Method 패턴을 사용하지 않으면, new IDCard() -> new IDCard2()로 전부 바꿔야 한다.

4.3 인스턴스 생성 메소드 구현 방법

  • Factory의 createProduct() 구현 방법
    • 추상 메소드 구현 (template method)
    • 디폴트 구현을 준비한다
      • 하위클래스에서 구현하지 않은 경우 디폴트로 실행됨 
      • 추상클래스 사용 불가 (new Product() 로 생성하려면 Product가 추상클래스면 안됨)
    • 에러로 처리한다.
      • 디폴트 구현을 예외 발생문장으로 처리한다.
      • Product createProduct(String name) {
            throw new FactoryMethodRuntimeException();
        }

연습문제

  • IDCard 클래스의 생성자는 public 이 아님
    • 외부 패키지에서 IDCard 클래스의 new 를 사용한 객체 생성이 불가능함
  • synchronized 메소드
    : 멀티스레드에서 값 계산을 잘 할 수 있게
  • HashMap<String, String>, HashTable<String, String>
  • 추상 생성자는 만들 수 없음
    -> 생성자는 상속 안됨
    • 하위 클래스 객체가 생성될 때 상위클래스의 생성자를 호출하는 것

5. Singleton

  • 프로그램 실행 시, 하나의 클래스에 대한 인스턴스가 여러개 생성됨
  • 하나의 인스턴스만 생성되어야 하는 클래스가 있음
  • 반드시 1개의 인스턴스만 생성되도록 코드에 표현하고 싶을 때 사용하는 패턴

5.1 예제 프로그램

Singleton 인스턴스가 하나만 존재하는 클래스
Main 동작 테스트용 클래스

public class Singleton {
    private static Singleton singleton = new Singleton();
    private Singleton() {                                 
        System.out.println("인스턴스를 생성했습니다.");
    }
    public static Singleton getInstance() {
        return singleton;
    }
}
  • private static Singleton singleton = new Singleton();
    • 클래스용 멤버변수이므로 클래스가 로드될 때 (생성자 호출)초기화 됨
    • static 멤버 => Singleton 클래스를 로드할 때 한번만 실행됨
    • private 이므로 외부에서 접근할 수 없다.
  • private Singleton() 생성자
    • private 메소드 => 외부에서 객체 생성 불가
  • public static Singleton getInstance()
    • Singleton 클래스의 유일한 인스턴스를 얻을 때 사용하는 메소드
  • 프로그램에 객체가 하나이므로 주소를 비교하면 같은 인스턴스이다. (모든 Singleton 객체는 같은 객체 == 같은 주소)

5.2 사용하는 이유

  • 인스턴스가 하나만 존재한다는 것이 보증되면, 인스턴스 상호간에 영향을 주어 생각지 못한 버그가 발생할 가능성이 없어진다.
  • 유일한 하나의 인스턴스는 언제 생성되는가
    • 프로그램 실행 후, 처음으로 Singleton.getInstance()메소드가 호출되면, Singleton 클래스가 메모리에 로드되어 초기화되고, 이때 static 필드인 singleton 필드가 초기화된다.

연습문제

  • 5-1) 멤버변수를 계산하는 메소드의 경우, synchronized 메소드로 선언해야 올바로 작동된다.
  • 5-2) 인스턴스 개수가 3개로 한정된 클래스 => 멤버변수에 객체 3개를 미리 만들어둔다.
  • 5-3) synchronized 를 붙이지 않으면 스레드 환경에서 다수의 객체가 생길 수 있다. 그래서 싱글턴 패턴이 아니다.
public class Singleton {
    private static Singleton singleton = null;
    private Singleton() {
        System.out.println("인스턴스를 생성했습니다.");                         
    }
    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }                                        
}

6. Prototype

  • 일반적인 인스턴스 생성은 클래스의 생성자를 호출한다.
  • 인스턴스 생성 시에 반드시 클래스 이름을 지정해야 한다
    클래스 -> 인스턴스
  • 의도
    • 클래스로부터 인스턴스를 새로 만드는 것이 아니라, 현재 존재하는 인스턴스를 복사해서 새로운 인스턴스를 만들 필요가 있을 때, 이 작업을 편하게 하기 위해 사용한다.
    • 인스턴스 -> 인스턴스

6.1 Prototype 패턴

  • "원형이 되는 인스턴스를 근본으로 해서 똑같은 새로운 인스턴스를 만든다"
  • 클래스 안에, 자신을 복사하는 메소드를 두자
    • 복제: 모든 필드 값이 동일한 인스턴스를 생성함
  • Cloneable 구현한 객체만 clone() 메소드 사용 가능

6.2 예제 프로그램

framework Product 추상메소드 use 와 createClone() 이 선언되어있는 인터페이스
Manager createClone()을 사용해서 인스턴스를 복제하는 클래스
Anonymous MessageBox 문자열을 틀에 넣어 표시하는 클래스.
use와 createClone을 구현하고 있다
UnderLinePen 문자열에 밑줄을 그어 표시하는 클래스.
use와 createClone을 구현하고 있다.
Main 동작 테스트용 클래스

클래스 다이어그램
Manager, Product

  • Product 인터페이스
    • java.lang.Cloneable 인터페이스를 상속
      -> 이를 구현한 클래스는, clone() 메소드를 사용하여 자기 자신을 복제할 수 있다.
    • use()
      • '사용'이 실제로 무엇을 의미하는지는 하위 클래스가 결정함
    • 인스턴스를 복사해서 새로운 인스턴스를 만들기 위한 메소드를 결정 (use())
  • Manager 클래스
    • Product 인터페이스를 이용해서 인스턴스를 복제하는 일을 함
    • HashMap<String, Product> showcase
      • java.util.HashMap
      • Product의 이름과 인스턴스를 저장함
    • register()
      • 제품의 '이름'과 '인스턴스'를 showcase에 저장함
    • create()
      • 등록된 제품의 createClone() 을 호출하여 복사본을 만든 다음, 이것을 반환한다.
    • Client 역할
    • 인스턴스를 복사하는 메소드를 이용해 새로운 인스턴스를 만듦
      : Product 의 createClone() 메소드 사용
  • Product 인터페이스나 Manager 클래스의 소스에, 구체적인 제품인 MessageBox 나 UnderlinePen 클래스의 이름이 전혀 등장하지 않는다.
    • Manager 클래스는 구체적인 클래스 이름을 사용하지 않고, Product 인터페이스 이름만을 사용한다.
    • framework와 구체적인 클래스를 분리
    • Product와 Manager 를 구체적인 클래스와 상관없이 수정할 수 있다.

UnderlinePen
Prototype

  • MessageBox
    • createClone()
      • 자기 자신을 복제하는 메소드 clone() 호출
      • clone() 인스턴스가 가지고 있는 필드 값이 그대로 복사된 복제 인스턴스를 반환
      • java.lang.Cloneable 인터페이스를 구현한 클래스만이 이 clone() 메소드를 가진다.
      • 이 인터페이스를 구현하지 않는 경우에는, CloneNotSupportedException이 발생한다
    • clone() 메소드는, 자신의 클래스 & 하위 클래스에서만 호출 가능
      • 다른 클래스의 요청으로 복제하는 경우, createClone() 처럼 다른 메소드로 clone() 메소드를 감싸야 한다.
  • UnderlinePen
    • MessageBox 와 비슷한 동작
    • 문자열에 밑줄을 그어줌
    • ulchar 필드
      : 밑줄 그을 때 사용할 문자 가짐
  • Main
    • Manager 인스턴스 생성
    • UnderlinePen , MessageBox 인스턴스 이름을 붙여서 등록
    • Manage의 create() 메소드를 호출해서 원하는 '이름'의 제품을 얻어서, 그것의 use()를 실행

6.3 Prototype 사용 이유

  • 인스턴스 복제 시,
    • 객체 생성 후에 기존 객체의 모든 필드 값을 얻어와서 복사해야 한다.
    • 필드가 private이고 값을 얻어올 수 없으면 복제가 불가능하다.
      => Prototype 을 사용하면 모든 필드를 복제할 수 있다.

6.4 java.lang.Cloneable 인터페이스

  • 인스턴스를 복사하는 장치로 clone() 제공됨
    : 이 메소드는 자기 자신만 호출할 수 있다.
  • 복사 대상이 되는 클래스는, 반드시 java.lang.Cloneable 인터페이스를 구현해야 한다.
  • Cloneable 인터페이스를 구현한 클래스의 인스턴스는, clone()메소드를 호출하면 복사된다.
  • Cloneable 인터페이스를 구현하고 있지 않은 클래스의 인스턴스가 clone() 을 호출하면, CloneNotSupportedException 예외가 발생한다.
    • clone() 호출 시 try-catch 문을 사용하여 처리한다.
  • java.lang.Object 에 clone() 이 정의되어 있음
    => 모든 클래스에서 clone() 을 상속함
    • 그러나 Object가 Cloneable을 구현하고 있는 것은 아님
  • Cloneable 인터페이스에 clone 메소드가 선언되어있는것은 아님
    => clone()에 의해 복사될 수 있다는 표시로만 사용된다.
  • clone() 메소드는 얕은 복사를 한다.
    • 속성의 값만을 복사한다
    • 속성이 참조형인 경우, 기존과 같은 객체를 가리키게 된다.
    • C++ : 복사 생성자 사용
      JAVA: Cloneable 인터페이스를 구현, clone() 메소드를 사용하여 복사할 수 있다.
    • 깊은 복사를 하려면 clone() 을 오버라이드 해야한다.

Object 클래스는 java.lang.Cloneable 인터페이스를 구현하지 않고, clone() 메소드만을 가지고 있다.

일반 클래스에서 clone() 을 호출하면 Cloneable 인터페이스를 구현하지 않았다는 오류가 뜬다.

Comparable 인터페이스를 구현하여 compareTo() 메소드를 오버라이드하면 객체 비교 가능

9. Bridge 패턴

  • Bridge
    • 두 장소를 연결하는 역할
    • "기능의 클래스 계층" 과 "구현의 클래스 계층" 사이에 다리를 놓는다.
  • 클래스 계층의 두가지 역할
    1. 기능의 클래스 계층
    2. 구현의 클래스 계층
    3. 두 그룹의 클래스를 Bridge 로 연결한다.
  • 새로운 '기능'을 추가하고 싶을 때
    • Something 클래스에 새로운 기능 추가 => 새로운 클래스 추가 SomethingGood
    • 하위 클래스를 새롭게 만든다
    • 소규모의 클래스 계층 = '기능의 클래스 계층'
    • 새로운 기능을 추가하고 싶을 때, 클래스 계층 안에서 만들려고 하는 클래스와 유사한 클래스를 상속받아 하위 클래스를 만들어 기능을 추가한다.

새로운 기능 추가

  • 새로운 '구현'을 추가하고 싶을 때
    • 다른 하위 클래스를 추가하여 구현하면 된다. 
    • 추상 클래스는 일련의 메소드들을 추상 메소드로 선언하고, 인터페이스(API)를 규정한다.
    • 하위클래스 쪽에서 그 추상메소드를 실제로 구현한다.
    • 상위클래스는 추상메소드로, 인터페이스를 규정하는 역할
    • 상위 클래스와 하위 클래스의 역할 분담에 의해 부품으로서의 가치가 높은 클래스를 만들 수 있다. (다형성)
      • 상위 클래스를 구현한 하위 클래스들끼리의 API는 통일되므로
    • '구현의 클래스 계층'
      구현한 클래스별로 API는 같지만 구현 내용이 다르다.

구현의 클래스 계층

Bridge 패턴의 목적

  • 클래스 계층의 분리
    • 클래스 계층구조 하나에 '기능' / '구현' 이 혼재되어 있으면 새로운 하위 클래스를 만들 때 어려움이 있다.
    • '기능의 클래스 계층'과 '구현의 클래스 계층'을 분리하고, 이들 사이에 다리를 놓아서 기능 추가나 새로운 구현 추가를 쉽게 할 수 있도록

9.1 예제 프로그램

클래스 다이어그램

Display

코드

Display, CountDisplay, DisplayImpl, StringDisplayImpl
main

  1. 기능의 클래스 계층
    • Display 클래스
      • 추상적인 '무언가를 표시하기 위한 것'으로 '기능의 클래스 계층'의 최상위에 존재
      • impl 필드: Display 클래스의 '구현'을 나타내는 인스턴스
        • 두 클래스 계층의 다리 역할
      • 생성자
        • 구현을 나타내는 클래스(DisplayImpl)의 인스턴스를 인자로 넘겨 받는다
      • open, print, close 메소드: 모두 DisplayImpl의 API를 호출한다
        => Display 인터페이스인 open, print, close가 DisplayImpl의 rawOpen(), rawPrint(), rawClose()를 호출한다.
      •  display()
        • open, print, close를 차례로 호출한다
    • CountDisplay 클래스
      • Display 클래스에 기능을 추가함
        • Display 클래스에는 '표시한다' 기능밖에 없음
        • CountDisplay 클래스에는 '지정횟수만큼 표시한다'라는 기능이 추가
        • multiDisplay()
  2. 구현의 클래스 계층
    • DisplayImpl 클래스
      • '구현의 클래스 계층'의 최상위에 위치
      • 추상 클래스이며, rawOpen(), rawPrint(), rawClose() 메소드를 가짐
    • StringDisplayImpl 클래스
      • 문자열을 표시하는 클래스
      • rawOpen(), rawPrint(), rawClose() 메소드를 구현
        • printLine()을 이용해서 문자열을 표시

9.2 역할

Abstraction의 역할

  • '기능의 클래스 계층'의 최상위에 있는 클래스
  • Implementor 역할의 메소드를 사용해서 기본적인 기능만을 제공하는 클래스
  • Display

RefinedAbstraction의 역할

  • Abstraction역할에 기능을 추가
  • 클래스 상속, 메소드 추가 (기능을 추가한 역할)
  • CountDisplay

Implementor의 역할

  • '구현의 클래스 계층'의 최상위에 있는 추상 클래스
  • Abstraction 역할의 인터페이스를 구현하기 위한 메소드를 규정하는 역할
  • DisplayImpl 클래스가 해당됨

ConcreteImplementor의 역할

  • Implementor 역할의 인터페이스를 구체적으로 구현하는 역할
  • 예제에서, StringDisplayImpl 클래스가 해당됨

9.3 힌트

  • 분리해두면 확장이 편해진다.
    • 두개로 클래스 계층을 나누어두면, 각각의 클래스 계층을 독립적으로 확장할 수 있다.
      • 기능을 추가하고 싶으면, 기능의 클래스 계층에 추가한다.
        (구현 클래스 계층은 전혀 수정할 필요가 없다)
  • 분리해두면 확장이 편해진다.
    • 구현을 추가하고 싶으면 구현 클래스 계층을 확장한다.
      • 어떤 프로그램에 OS 의존 부분이 있어서, Window/Unix/Mac 으로 구분되는 경우 => 구현의 클래스 계층으로 표현
      • Display 생성 시, 생성자에게 WinImpl, MacImpl, UnixImpl 중 적당한 것으로 넘겨줌

  • 상속 == 견고한 연결, 위임 == 느슨한 연결
    • 상속은 소스코드를 고치지 않는 한 바꿀 수 없는 매우 견고한 연결임
      - 클래스간의 관계를 바꾸고 싶을 때는 상속을 사용해서는 안됨
    • Display 클래스 내에서 위임이 사용된다.
      • 예: open()을 실행할 때, impl.rawOpen() 을 호출하여 '떠넘기기'
        • 이때 impl 이 참조하고 있는 객체는, Display 생성시 클라이언트가 넘겨준 구현 클래스의 인스턴스(StringDisplayImpl)
        • StringDisplayImpl 이외의 다른 클래스의 객체를 넘겨주면, 구현이 교체되는 효과를 가져온다.
        • Main만 수정하여 구현 교체 가능

출력을 어디에 어떻게(file, head/foot..) 할건지 ==> DisplayImpl 의 구현 클래스로 작성

출력하는 방식(횟수, randomcount..)을 바꾸고 싶으면 ==> Display 클래스를 상속

10. Strategy

  • 전략: 알고리즘
  • Strategy 패턴
    • 알고리즘을 구현한 부분이 모두 교환 가능하도록 함
    • 알고리즘을 교체해서 동일한 문제를 다른 방법으로 해결하는 패턴

10.1 예제 프로그램

가위바위보 게임

  • WinningStrategy
    전략: 이기면 다음 번에도 같은 손을 내민다.
  • ProbStrategy
    전략: 바로 전에 내밀었던 손으로부터, 다음에 내밀 손을 확률적으로 계산

package Sample;

public class Hand {
    public static final int HANDVALUE_GUU = 0;  // 주먹
    public static final int HANDVALUE_CHO = 1;  // 가위
    public static final int HANDVALUE_PAA = 2;  // 보
    public static final Hand[] hand = {         // 가위바위보의 손을 표시하는 3개의 인스턴스
        new Hand(HANDVALUE_GUU),
        new Hand(HANDVALUE_CHO),
        new Hand(HANDVALUE_PAA),
    };
    private static final String[] name = {        // 가위바위보 손의 문자열 표현
        "주먹", "가위", "보",
    };
    private int handvalue;                     // 가위바위보 손의 값
    private Hand(int handvalue) {
        this.handvalue = handvalue;
    }
    public static Hand getHand(int handvalue) {  // 값을 가지고 인스턴스를 얻는다.
        return hand[handvalue];
    }
    public boolean isStrongerThan(Hand h) {     // this가 h를 이길 경우 = true
        return fight(h) == 1;
    }
    public boolean isWeakerThan(Hand h) {       // this가 h에게 질 경우 true
        return fight(h) == -1;
    }
    private int fight(Hand h) {                 // 무승부: 0, this 승: 1, h 승: -1
        if (this == h) {
            return 0;
        } else if ((this.handvalue + 1) % 3 == h.handvalue) {
            return 1;
        } else {
            return -1;
        }
    }
    public String toString() {                  // 문자열 표현
        return name[handvalue];
    }
}

Hand 클래스

  • 가위바위보의 손을 나타내는 클래스
  • hand 필드: 가위, 주먹, 보 손 세개의 손을 가지고 있는 배열
  • handvalue : 주먹은 0, 가위는 1, 보는 2
  • getHand()
    가위바위보를 나타내는 숫자로부터 해당 손을 반환함
  • isStrongerThan()
    현재 손이 입력 인자로 들어온 손을 이기면 true를 반환
  • isWeakerThan()
    현재 손이 입력 인자로 들어온 손에게 지면 true를 반환
  • fight()
    • 현재 손이 입력받은 손과 무승부: 0, 이기면:1, 지면: -1
    • 우열 판정 수식
      • (this.handvalue + 1) % 3 == h.handvalue
      • 현재 손이 주먹(0)이고 입력이 가위(1)
        or 현재 손이 가위(1) 입력이 보(2)
        or 현재 손이 보(2), 입력이 주먹(0)이면 현재 손이 이긴다. => 1을 반환
      • handvalue + 1 한 값을 3으로 나눈 나머지와 입력 값이 같으면 현재 손이 이긴다.
  • toString()
public interface Strategy {
    public abstract Hand nextHand();
    public abstract void study(boolean win);
}

Strategy 인터페이스

  • 가위바위보의 '전략'을 위한 추상 메소드를 모아놓은 곳
  • nextHand()
    • 다음에 내밀 손을 얻기 위해 호출하는 메소드
    • 이 메소드가 호출되면, Strategy 인터페이스를 구현한 클래스가 지혜를 모아 '다음 손'을 결정
  • study()
    • 다음 승부에 사용될 전략을 준비시키는 메소드
      • 직전에 낸 손으로 이겼는지 졌는지를 학습
      • 이긴 경우에는 Player가 study(true)를 호출하고, 진 경우에는 false로 호출
    • 자신의 내부 상태를 변화시켜 이후 nextHand() 에 사용
package Sample;
import java.util.Random;

public class WinningStrategy implements Strategy {
    private Random random;
    private boolean won = false;
    private Hand prevHand;
    public WinningStrategy(int seed) {
        random = new Random(seed);
    }
    public Hand nextHand() {
        if (!won) {
            prevHand = Hand.getHand(random.nextInt(3)); // 0 <= x < 3
        }
        return prevHand;
    }
    public void study(boolean win) {
        won = win;
    }
}

WinningStrategy 클래스

  • Strategy 인터페이스를 구현한 클래스
  • nextHand()에서의 전략
    • 직전의 승부에서 승리했으면, 동일한 손을 내민다
    • 직전 승부에서 패했으면, 난수를 사용해서 다음 손을 정한다
      java.util.Random 클래스 이용
      nextInt(3): 0~2 사이의 난수 정수 생성
  • won 필드
    지난번에 이겼으면 true, 지면 false 저장
  • prevHand 필드
    지난번 승부에서 내민 손 저장
package Sample;
import java.util.Random;

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);
    }
    public Hand nextHand() {
        int bet = random.nextInt(getSum(currentHandValue)); // bet는 [0, sum-1] 의 랜덤 정수값
        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 hv) {
        int sum = 0;
        for (int i = 0; i < 3; i++) {
            sum += history[hv][i];
        }
        return sum;
    }
    public void study(boolean win) {
        if (win) {
            history[prevHandValue][currentHandValue]++;
        } else {
            history[prevHandValue][(currentHandValue + 1) % 3]++;
            history[prevHandValue][(currentHandValue + 2) % 3]++;
        }
    }
}

ProbStrategy 클래스

  • 좀 더 머리를 쓰는 전략
  • history 필드: 과거의 승패를 유지하는 테이블
    • history[이전에 낸 손][이번에 낼 손]
    • history[0][0]: 주먹, 주먹 순으로 내밀어서 이긴 횟수
    • history[0][1]: 주먹, 가위 순으로 내밀어서 이긴 횟수
    • history[0][2]: 주먹, 보 순으로 내밀어서 이긴 횟수
  • prevHandValue: 지난번에 낸 손
  • currentHandValue: 이번에 냈던 손
  • nextHand(): 다음에 낼 손을 반환
    • handValue: 다음에 낼 손의 값을 저장
    • 전략
      • 이전에 주먹을 냈다면, history[0][0], history[0][1], history[0][2]로부터 다음 번에 낼 손의 확률을 계산하려고 한다.
      • history[0][0]=3, history[0][1]=5, history[0][2]=7 이면,
        • 세 숫자를 다 더한 값(3+5+7=15)로 난수를 얻음
        • 난수가 0~3 미만이면 주먹 (3/15 확률)
        • 난수가 3 ~ 8 미만이면 가위 (5/15 확률)
        • 난수가 8~15 미만이면 보 (7/15 확률)
      • 이전에 주먹을 내고 다음에 냈던 손 중, 가장 많이 이겼던 손을 다음에 내게 될 확률이 높음
  • study(): 전략을 위한 준비 작업을 하는 메소드
    • 이겼으면,
      history[직전에 냈던 손][이번에 냈던 손] + 1
    • 졌으면,
      history[직전에 냈던 손][이번에 안냈던 손] + 1
package Sample;

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++;
    }
    public String toString() {
        return "[" + name + ":" + gamecount + " games, " + wincount + " win, " + losecount + " lose" + "]";
    }
}

Player 클래스

  • 가위바위보를 하는 사람을 표현한 클래스
  • 생성 시, '이름'과 '전략'이 주어진다.
  • 생성 시의 전략에 따라 다음에 내밀 손이 결정된다: nextHand()
    • nextHand() 메소드 안에서 Strategy의 nextHand()를 호출한다.
    • Strategy 에게 위임한다.
  • 이김/짐/무승부 모두 다음 승부를 위해서 Strategy의 study() 호출
    win(), lose(), even()
  • 승패 횟수를 저장
    wincount, losecount, gamecount 필드
public class Main {
    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println("Usage: java Main randomseed1 randomseed2");
            System.out.println("Example: java Main 314 15");
            System.exit(0);
        }
        int seed1 = Integer.parseInt(args[0]);
        int seed2 = Integer.parseInt(args[1]);
        Player player1 = new Player("두리", new WinningStrategy(seed1));
        Player player2 = new Player("하나", 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.toString());
        System.out.println(player2.toString());
    }
}

수업에서 RandomStrategy가 더 많이 이김

Strategy - Strategy 패턴을 이용하기 위한 인터페이스 결정
- Strategy 인터페이스
ConcreteStrategy - Strategy 인터페이스를 실제로 구현
- 구체적 전략(알고리즘)을 나타냄
- WinningStrategy, ProbStrategy가 해당됨
Context의 역할 - Strategy를 이용하는 역할
- ConcreteStrategy 인스턴스를 가지고, 필요에 따라 이를 이용함
- Player

10.2 힌트

  • Strategy를 만드는 이유
    • Strategy를 이용하면, 알고리즘을 변경하기 쉽다.
      • 위임: 느슨한 연결(낮은 결합도)
    • 실행중에 교체도 가능하다.
      • ex) 메모리가 적을 땐 A , 메모리가 많을 땐 B 알고리즘 사용

10.3 예제

Comparable 인터페이스

  • 클래스 객체를 비교하려면 implements 해야함
  • 크기 비교가 가능한 클래스들이 구현하는 인터페이스
  • 크기 비교 시 compareTo(Comparable c) 메소드 이용
  • String 들은 크기 비교가 가능하므로 Comparable 인터페이스를 구현
  • Random vs ProbStrategy => Random 이 더 많이 이김
  • this == h 로 비교가 가능한 이유는 손 객체가 클래스용 멤버 객체로 3개(주먹, 가위, 보)만 존재하기 때문이다.
  • java에서 명시되지 않은 필드는 자동으로 초기화된다.
    Boolean = false
    int = 0
    ref = null
    char = '\u0000' (2byte unicode)
    c++ = (int)0 // 전역변수일때만 초기화됨

11. Composite

  • 컴퓨터의 파일 시스템
    • 디렉토리 안에 파일이나 또 다른 디렉토리가 존재
    • 디렉토리 & 파일 == '디렉토리 엔트리'
  • 재귀적인 구조
    그릇 안에 내용물을 넣을 수도 있고, 작은 그릇을 넣을 수도 있다.
    작은 그릇 안에는 더 작은 그릇 or 내용물을 넣을 수 있다.
  • Composite 패턴
    • 그릇과 내용물을 동일시해서 재귀적인 구조를 만들기 위한 패턴
    • composite: 혼합물, 복합물

클라이언트가 root에 대해 getSize()를 호출하면, root는 자신의 내용물에 대해 getSize()를 호출하는 방식으로 재귀적 호출

11.1 예제 프로그램

Entry 클래스

  • 추상클래스, 디렉토리 엔트리를 표현한다.
  • File / Directory 클래스를 하위 클래스로 가진다.
  • 이름과 size 가짐
  • add()
    • 디렉토리나 파일을 넣을 때 호출되는 메서드
    • 구현은 하위 클래스인 Directory가 제공한다.
    • Entry 클래스에서는 이 메소드가 호출되면 예외를 발생시킨다.
  • PrintList(), printList(String): 오버로드 됨
    • 호출될 때 인수의 모양에 따라 적절한 메소드가 실행됨
    • printList(String)은 protected로 하위 클래스에서만 접근 가능
  • toString(): 이름과 size를 문자열로 표현함 
    : Template Method 패턴
    -> getName(), getSize()를 호출

File 클래스

  • 파일을 표현하는 클래스
  • name 과 size 속성
  • 생성자 File(): 이름과 크기를 인자로 받아들임
  • getName(): 파일의 이름을 반환
  • getSize(): 파일 크기 반환
  • PrintList(String prefix)
    : prefix + "/" + this

Directory 클래스

  • name 필드 존재
  • size => 없음
    getSize() 에서 사이즈를 동적으로 계산
    • 현재 디렉토리에 포함된 모든 요소들의 getSize()를 호출해서 전부 더한다.
    • entry.getSize()
      entry에 들어있는 실제 객체가 어떤 것이던지, Entry가 부모 클래스이고 getSize()가 오버라이딩 되어있기 때문에 다형성을 이용하여 참조할 수 있다.
    • 디렉토리 타입인 경우, 디렉토리 안의 요소들의 getSize()를 호출한다.
  • printList()
    • getSize()와 마찬가지로, 디렉토리에 포함된 Entry의 printList를 재귀적으로 호출한다.
    • entry가 File의 인스턴스인지, Directory의 인스턴스인지 상관없다.
      • 그릇과 내용물이 동일시된다.

FileTreatmentException 클래스

  • File 에 add 메소드를 호출했을 때 발생하는 예외를 나타냄
  • RuntimeException을 상속받아서 정의됨
    • 실행 중에 발생하는 예외로, 예외처리를 하지 않아도 컴파일 에러가 발생하지 않는 예외
      : try-catch 를 안써도 됨
  • Entry 클래스에 add 메소드가 정의되어 있고, File 클래스에는 add 메소드가 없다.
    • File 클래스에 대해서 add() 메소드가 호출되면, Entry로부터 상속받은 add가 실행된다.
    • 오버라이딩 되지 않은 경우에만 예외가 던져진다.
public abstract class Entry {
    public abstract String getName();                               
    public abstract int getSize();                                 
    public Entry add(Entry entry) throws FileTreatmentException {   
        throw new FileTreatmentException();
    }
    public void printList() {                                       
        printList("");
    }
    protected abstract void printList(String prefix);              
    public String toString() {                                     
        return getName() + " (" + getSize() + ")";
    }
}
public class File extends Entry {
    private String name;
    private int size;
    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }
    public String getName() {
        return name;
    }
    public int getSize() {
        return size;
    }
    protected void printList(String prefix) {
        System.out.println(prefix + "/" + this);
    }
}
import java.util.Iterator;
import java.util.ArrayList;

public class Directory extends Entry {
    private String name;                            
    private ArrayList directory = new ArrayList();      
    public Directory(String name) {                  
        this.name = name;
    }
    public String getName() {                      
        return name;
    }
    public int getSize() {                            
        int size = 0;
        Iterator it = directory.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            size += entry.getSize();
        }
        return size;
    }
    public Entry add(Entry entry) {                  
        directory.add(entry);
        return this;
    }
    protected void printList(String prefix) {           
        System.out.println(prefix + "/" + this);
        Iterator it = directory.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            entry.printList(prefix + "/" + name);
        }
    }
}

Leaf 역할

  • 내용물. 이 안에는 다른 것을 넣을 수 없다.
  • File 클래스

Composite 역할

  • 그릇 역할. Leaf & Composite 을 넣을 수 있다.
  • 예제에서는 Directory 클래스가 해당됨

Component 역할

  • Leaf 역할 & Composite 역할을 동일시 하기 위한 역할
  • Leaf & Composite 의 상위 클래스로 구현됨
  • 예제에서는 Entry 클래스가 해당됨

Client 역할 => Main 클래스

11.2 힌트

  • 복수와 단수의 동일시
    • 여러개를 모아 하나인 것처럼 취급 가능
  • add 구현 방법
    1. Entry 클래스에서 구현하고, 에러로 처리한다.
      : 오버라이딩하지 않는 클래스에서는 에러로 처리됨
    2. Entry 클래스에서 구현하고, 아무것도 실행하지 않는다.
      : File에서 에러나지 않음
    3. Entry 클래스에서 추상 메소드로 선언
      : File 클래스는 add()를 사용하지 않는데도 짜야함
    4. Directory 클래스에만 넣는다.
      : 이 방법은 Entry 형의 변수에 add 할 때, Directory 형으로 일일이 형변환해야하는 번거로움
  • 재귀적 구조
    • Java GUI
    • Directory
    • HTML <ul> <li> <ol>

 

반응형

'sswu' 카테고리의 다른 글

오픈소스 소프트웨어 기말 정리  (0) 2022.05.20
독일어 정리  (0) 2022.04.15
오픈소스 소프트웨어 중간 정리  (0) 2022.03.20
[네트워크 분석 실습] 네분실 기말 정리  (0) 2021.11.19
파이썬 기말 정리  (0) 2021.11.15
Comments