일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 42seoul
- 오라클
- 리눅스
- 프로그래밍언어론
- Spring
- 다이어리
- AI
- 스프링부트 웹 소켓
- 소켓
- 스프링부트
- springboot
- IOS
- libasm
- 네트워크
- 밥먹는 철학자
- CD
- JPA
- swift
- DBMS
- Dining philosopher problem
- 인공지능
- Xcode
- CI
- 데이터베이스
- 스프링
- 아이패드다이어리
- sql
- javascript
- MySQL
- jenkins
- Today
- Total
Hi yoahn 개발블로그
디자인패턴 중간 정리 본문
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)
- books
- Book
책을 나타내는 클래스 - BookShelfIterator 클래스 - Iterator 구현
- bookShelf
BookShelf 객체를 가짐 - index
반환할 객체 인덱스 정보 - hasNext()
index < length 이면 true - next()
현재 객체를 반환하고 index + 1
- bookShelf
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의 구현에 의존하지 않는 것
- BookShelf가 Book을 배열이 아닌 Vector에 저장하는것으로 변경하더라도, Main 클래스의 부분을 변경하지 않아도 된다.
- 집합체의 각 원소를 끄집어내는 방법이, 집합체 구현과 무관하다.
- 디자인패턴은 클래스의 재사용성을 높인다.
- 가능한 추상클래스 & 인터페이스를 자주 사용하여 결합도를 낮춘다.
- 구체적인 클래스만으로 프로그래밍하면 클래스간의 결합도가 강해져 재사용성이 낮아짐
- 코드 일부 수정으로 수정해야 될 다른 부분들을 최소화하는 것이 중요
- Aggregate와 Iterator의 대응 관계
- Aggregate 클래스와 Iterator 클래스는 밀접하게 관련이 있듯이, BookShelf와 BookShelfIterator 도 밀접한 관계가 있다.
- BookShelfIterator 가 BookShelf 구현을 알고 있어야 한다.
- ex) BookShelf 의 getBookAt() 메소드의 이름을 getBookFrom()으로 바꾸면, BookShelfIterator 의 next() 내부도 수정해야 한다.
- Aggregate-Iterator 쌍을 이루듯 BookShelf - BookShelfIterator도 쌍을 이루는 밀접한 관계이다.
- Aggregate 클래스와 Iterator 클래스는 밀접하게 관련이 있듯이, BookShelf와 BookShelfIterator 도 밀접한 관계가 있다.
- Iterator는 여러종류를 만들 수 있다.
ex) 역방향으로 원소를 순회하는 이터레이터
연습문제
- Vector, ArrayList 는 max size 를 지정하지 않으면 10개로 설정된다.
- max size를 넘으면 자동으로 늘어남 => 동적 배열
- 배열: 정적 메모리 할당
- ArrayList = 비동기 , Vector = 동기 (한 스레드만 접근 가능)
2. Adapter 패턴
- 이미 제공되어있는 것을 그대로 사용할 수 없는 경우
- '이미 제공되어 있는 것'과 '필요한 것' 사이의 간격을 메우는 것
(서로 다른 두개의 인터페이스 사이를 연결) - 두가지 종류
- 상속을 이용
클래스에 의한 패턴 - 위임을 이용
인스턴스에 의한 패턴
- 상속을 이용
2.1 상속을 이용한 Adapter 패턴
- Banner 클래스
- showWithParen()
문자열 앞뒤에 괄호를 쳐서 표시하는 메소드 - showWithAster()
: 문자열 앞뒤에 '*'를 붙여서 표시하는 메소드 - 이 두 메소드를 '이미 제공되어 있는 것'으로 가정
- showWithParen()
- Print 인터페이스
- printWeak()
: 문자열을 약하게 표시 (괄호 붙임) - printStrong()
: 문자열을 강하게 표시 (* 붙임) - 이 두 메소드를 '필요한 것'이라고 가정
- printWeak()
- 목표
- Banner 클래스라는 기존의 클래스를 이용해서 Print 인터페이스를 구현하는 클래스를 만든다.
- 새로 만든 인터페이스를 구현할 때, 이미 구현된 클래스를 상속받아 인터페이스를 구현한다.
- Main 클래스
- Print p = new PrintBanner()
- Print 인터페이스를 사용함으로써 실제 일을 하는 Banner 클래스의 메소드는 Main클래스에서는 볼 수 없음
- Main클래스를 수정하지 않고도, PrintBanner 의 구현을 수정할 수 있다.
- 확장성을 위해서 추상화를 사용하여 유지보수성을 높인다.
- Print p = new PrintBanner()
2.2 위임을 이용한 Adapter 패턴
- 위임
: 내가 할 일을 누군가에게 맡긴다.- 예제에서 -> PrintBanner 가 할 일을 Banner 클래스의 인스턴스에게 맡긴다.
(메소드의 실제 처리를 다른 객체의 메소드를 호출해서 처리)
- 예제에서 -> PrintBanner 가 할 일을 Banner 클래스의 인스턴스에게 맡긴다.
- 예제 1과 달리 Print 가 클래스이면,
- PrintBanner 가 Print 와 Banner 의 하위 클래스로 정의할 수 없다. (다중 상속 불가능)
- 위임 사용 (멤버변수 추가) Banner banner;
- Print 클래스
추상 클래스로 정의 - PrintBanner
- banner 필드가 Banner 클래스의 인스턴스
- printWeak()
- banner.showWithParen() 을 호출
- PrintBanner 자신이 일을 처리하지 않고, banner에게 위임
- printStrong()
- banner.showWithAster() 을 호출
-> Banner에게 일을 위임하는 것
- banner.showWithAster() 을 호출
Adapter 패턴에 등장하는 역할
- Target
: 필요한 메소드를 제공하는 역할 (Print)
-> 꼭 이 클래스의 메소드를 써야하는 경우 - Client
: Target 역할의 메소드를 이용하는 역할 (Main) - Adaptee
: 이미 준비되어 있는 메소드를 제공하는 역할 (Banner) - Adapter
: Target 역할을 실제로 충족시키는 역할 (PrintBanner)
- 어떤 경우에 사용할까
- 이미 존재하는 클래스를 부품으로 재사용
- 기존 클래스가 충분히 테스트 되어있을 때 더욱 좋다
- 비록 소스가 없더라도, 기존 클래스 수정 없이 원하는 인터페이스에 기존의 클래스를 맞출 수 있다.
- 특히 기존 클래스의 소스 코드를 몰라도, 메소드의 프로토타입만 알면 어댑터 패턴을 적용할 수 있다.
3. Template Method
- 템플릿이란?
- 문자 모양을 따라 구멍이 뚫려있는 얇은 플라스틱 판
- 필기도구의 종류에 따라 실제로 쓰여지는 문자의 인스턴스가 결정된다.
- 상위 클래스에, 템플릿 역할을 하는 메소드가 정의됨
- 그 메소드에서는 추상 메소드들을 사용
- 상위 클래스에서는 정의되지 않은 메소드가 호출되는 것은 보이지만, 어떻게 처리되는지는 알 수 없음
(구현되지 않은 메소드를 호출함) - 추상 클래스에서 템플릿 메소드를 구현할 때 추상 메소드를 호출
-> 로직을 공통화 할 수 있다.
- 하위 클래스가 추상 메소드를 구현한다 == 오버라이딩
- 하위 클래스에서 어떤 구현을 하더라도, 처리의 큰 흐름은 상위 클래스가 결정한대로 이루어진다.
- 상위 클래스에서 처리의 뼈대를 결정하고, 하위 클래스에서 구체적인 내용을 결정하는 디자인 패턴
- AbstractDisplay
- abstract class
- 추상 메소드 외에 final 메소드를 정의할 수 있다.
- final 메소드는 하위클래스에서 오버라이딩 할 수 없음을 의미
- 추상 클래스는 인스턴스를 만들 수 없다.
- 추상 메소드를 템플릿 메소드에서 호출하지만, 구현은 하위 클래스에서 담당한다.
- interface 를 사용해도 된다.
- 인터페이스에는 default 메소드를 사용하여 구현 메소드를 포함시킬 수 있으므로 final 메소드와 비슷한 역할을 한다.
- default 메소드는 오버라이딩이 가능하다.
(추상클래스 final 메소드는 오버라이딩 불가능)
- abstract class
- protected 메소드
: 상속관계 및 동일 패키지에 있는 클래스에서만 호출 가능
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 - 문자열 길이가 영어랑 한글이 달라서 적용
- string = "안녕하세요"
- string.getBytes().length
- 다형성
- 상위 클래스로 하위클래스 타입을 가리킴
- 상위클래스 타입 변수로 오버라이딩 된 함수를 호출 == 다형성
연습문제
- java.io.InputStream 클래스의 abstract int read(); 메소드가 추상 메소드
int read(byte[] b, int off, int len) 메소드가 read() 메소드를 호출하여 사이즈만큼 읽어들임
-> template method - final func()
오버라이딩이 불가능한 메소드 - protected
- 자식 클래스와 같은 패키지에 있는 클래스에서 호출이 가능함
- 상속 관계에 있는 하위 클래스에서 호출 가능
- 다른 패키지에서는 호출 불가 - 추상클래스 대신 인터페이스를 사용할 수 있다
인터페이스에 구현 메소드를 추가할 수 있게 되었기 때문에
4. Factory Method 패턴
하위 클래스에서 인스턴스 만들기
- Template Method를 이용한 패턴
- 인스턴스를 생성하는 공장을 Template 메소드 패턴으로 구성
- 인스턴스를 만드는 방법은 상위 클래스에서 결정
- 인스턴스를 실제로 생성하는 일은 하위 클래스에서 결정
- 구체적인 제품 생성 -> 공장을 통해서 함
4.1 예제 프로그램 - 신분증 만드는 공장
- framework 패키지
- Product - 추상 클래스
- 추상메소드 use()
- 생성된 제품이 가지고 있어야 할 인터페이스를 결정하는 추상클래스
(구체적인 역할은 하위 클래스인 ConcreteProduct 역할이 결정)
- Factory - 추상 클래스
- create() 구현
createProduct(), registerProduct() 추상 메소드 호출하여 템플릿 메소드 형태 - Product 클래스 타입의 인스턴스를 생성하는 추상 클래스 (Creator)
(실제 제품을 생성하는 ConcreteCreator의 역할에 대해서는 모름)
- create() 구현
- Product - 추상 클래스
- idcard 패키지
- IDCard (제품)
- use() 구현
- ConcreteProduct 역할
- 생성자를 default 로 접근 => 외부 패키지에서는 인스턴스 생성 불가
- IDCardFactory (공장)
- createProduct(), registerProduct() 구현
- ConcreteCreator 역할
- IDCard (제품)
- IDCard 객체를 Main 클래스에서 직접 생산할 수도 있다.
- 그러나 Factory Method 패턴을 이용해서
IDCard 객체가 필요하면, IDCardFactory 를 통해서 IDCard 제품을 생산
4.2 Factory method 사용 이유
- 코드 분리
- 같은 프레임워크를 이용해서 다른 공장과 다른 제품을 추가로 정의할 수 있다
- TV + TV 공장
Main에서 TVFactory 객체를 생성한 후에 TVFactory 객체의 create() 메소드를 호출하기만 하면 된다.
- 각 공장이 어떤 제품을 어떻게 생산하는지 클라이언트는 모른다.
- 생산하는 제품이(IDCard->IDCard2) 바뀌어도, IDCardFactory 객체를 생성하고, create()만 호출하면 된다.
- 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 | 동작 테스트용 클래스 |
- Product 인터페이스
- java.lang.Cloneable 인터페이스를 상속
-> 이를 구현한 클래스는, clone() 메소드를 사용하여 자기 자신을 복제할 수 있다. - use()
- '사용'이 실제로 무엇을 의미하는지는 하위 클래스가 결정함
- 인스턴스를 복사해서 새로운 인스턴스를 만들기 위한 메소드를 결정 (use())
- java.lang.Cloneable 인터페이스를 상속
- 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 를 구체적인 클래스와 상관없이 수정할 수 있다.
- MessageBox
- createClone()
- 자기 자신을 복제하는 메소드 clone() 호출
- clone() 인스턴스가 가지고 있는 필드 값이 그대로 복사된 복제 인스턴스를 반환
- java.lang.Cloneable 인터페이스를 구현한 클래스만이 이 clone() 메소드를 가진다.
- 이 인터페이스를 구현하지 않는 경우에는, CloneNotSupportedException이 발생한다
- clone() 메소드는, 자신의 클래스 & 하위 클래스에서만 호출 가능
- 다른 클래스의 요청으로 복제하는 경우, createClone() 처럼 다른 메소드로 clone() 메소드를 감싸야 한다.
- createClone()
- 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
- 두 장소를 연결하는 역할
- "기능의 클래스 계층" 과 "구현의 클래스 계층" 사이에 다리를 놓는다.
- 클래스 계층의 두가지 역할
- 기능의 클래스 계층
- 구현의 클래스 계층
- 두 그룹의 클래스를 Bridge 로 연결한다.
- 새로운 '기능'을 추가하고 싶을 때
- Something 클래스에 새로운 기능 추가 => 새로운 클래스 추가 SomethingGood
- 하위 클래스를 새롭게 만든다
- 소규모의 클래스 계층 = '기능의 클래스 계층'
- 새로운 기능을 추가하고 싶을 때, 클래스 계층 안에서 만들려고 하는 클래스와 유사한 클래스를 상속받아 하위 클래스를 만들어 기능을 추가한다.
- 새로운 '구현'을 추가하고 싶을 때
- 다른 하위 클래스를 추가하여 구현하면 된다.
- 추상 클래스는 일련의 메소드들을 추상 메소드로 선언하고, 인터페이스(API)를 규정한다.
- 하위클래스 쪽에서 그 추상메소드를 실제로 구현한다.
- 상위클래스는 추상메소드로, 인터페이스를 규정하는 역할
- 상위 클래스와 하위 클래스의 역할 분담에 의해 부품으로서의 가치가 높은 클래스를 만들 수 있다. (다형성)
- 상위 클래스를 구현한 하위 클래스들끼리의 API는 통일되므로
- '구현의 클래스 계층'
구현한 클래스별로 API는 같지만 구현 내용이 다르다.
Bridge 패턴의 목적
- 클래스 계층의 분리
- 클래스 계층구조 하나에 '기능' / '구현' 이 혼재되어 있으면 새로운 하위 클래스를 만들 때 어려움이 있다.
- '기능의 클래스 계층'과 '구현의 클래스 계층'을 분리하고, 이들 사이에 다리를 놓아서 기능 추가나 새로운 구현 추가를 쉽게 할 수 있도록
9.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()
- Display 클래스에 기능을 추가함
- Display 클래스
- 구현의 클래스 계층
- DisplayImpl 클래스
- '구현의 클래스 계층'의 최상위에 위치
- 추상 클래스이며, rawOpen(), rawPrint(), rawClose() 메소드를 가짐
- StringDisplayImpl 클래스
- 문자열을 표시하는 클래스
- rawOpen(), rawPrint(), rawClose() 메소드를 구현
- printLine()을 이용해서 문자열을 표시
- DisplayImpl 클래스
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만 수정하여 구현 교체 가능
- 예: open()을 실행할 때, impl.rawOpen() 을 호출하여 '떠넘기기'
- 상속은 소스코드를 고치지 않는 한 바꿀 수 없는 매우 견고한 연결임
출력을 어디에 어떻게(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 알고리즘 사용
- Strategy를 이용하면, 알고리즘을 변경하기 쉽다.
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 구현 방법
- Entry 클래스에서 구현하고, 에러로 처리한다.
: 오버라이딩하지 않는 클래스에서는 에러로 처리됨 - Entry 클래스에서 구현하고, 아무것도 실행하지 않는다.
: File에서 에러나지 않음 - Entry 클래스에서 추상 메소드로 선언
: File 클래스는 add()를 사용하지 않는데도 짜야함 - Directory 클래스에만 넣는다.
: 이 방법은 Entry 형의 변수에 add 할 때, Directory 형으로 일일이 형변환해야하는 번거로움
- Entry 클래스에서 구현하고, 에러로 처리한다.
- 재귀적 구조
- 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 |