디자인 패턴 총정리

2025. 4. 16. 02:55·개발
반응형

 

 

 

 


 

디자인 패턴이란?

 

디자인 패턴 : 소프트웨어 설계 과정에서 자주 발생하는 문제들에 대한 설계 방법론

 

디자인 패턴 개발 과정에서 반복되는 문제들을 대비한 모범 해결 사례라고 할 수 있다.

디자인 패턴은 개발자들에게 문제 상황을 해결하는 원리를 알려준다.

 

디자인 패턴을 통해 얻을 수 있는 이점은 다음과 같다.

  • 재사용성 : 반복적인 문제에 대한 일반적인 해결책을 제시해준다.
  • 가독성 : 코드가 일정한 구조로 명확하게 정리된다.
  • 유지보수성 : 코드를 쉽게 모듈화할 수 있게 해준다.
  • 확장성 : 기존 코드를 변경하지 않고 새로운 기능을 구현할 수 있다.
  • 안정성, 신뢰성 : 검증된 솔루션을 제공한다.
  • 업무 효율성 : 팀원과의 소통을 효율적으로 할 수 있게 해준다.

 

디자인 패턴의 종류는 다음과 같다.

  • 생성 패턴 : 기존 코드의 재활용과 유연성을 증가시키는 객체 생성 방법을 제공한다.
  • 구조 패턴 : 구조를 유연하고 효율적으로 유지하면서, 객체와 클래스를 더 큰 구조로 조합하는 방법을 제공한다.
  • 행동 패턴 : 객체 간의 의사소통 및 책임 분배를 효율적으로 처리하는 방법을 제공한다

 


생성 패턴

 

1. 팩토리 메서드 (Factory Method)

 

객체 생성 역할을 팩토리 클래스에 위임함으로써,
구현 클래스를 클라이언트가 결정하지 않도록 해라!

 

 

장점

  • 클라이언트 코드와 구체 클래스의 결합도를 낮출 수 있다.
  • 객체 생성 로직을 캡슐화할 수 있다.
  • 객체의 생성 기능을 분리할 수 있다. (SRP)
  • 새로운 객체 유형을 기존 코드를 수정하지 않고 쉽게 확장할 수 있다. (OCP)

 

단점

  • 클래스가 너무 많아져서 코드가 복잡해진다.

 

사용 시기

  • 객체 생성을 다른 클래스에 맡기고 싶은 경우
  • 인스턴스를 다양하게 변형하여 생성하고 싶은 경우

 


 

2. 추상 팩토리 (Abstract Factory)

 

관련있는 객체들을 생성하는 공통 인터페이스를 제공해라!

 

 

장점

  • 클라이언트 코드와 구체 클래스의 결합도를 낮출 수 있다.
  • 제품군을 함께 생성하여 일관성을 유지할 수 있다.
  • 객체의 생성 기능을 분리할 수 있다. (SRP)
  • 새로운 객체 유형을 기존 코드를 수정하지 않고 쉽게 확장할 수 있다. (OCP)

 

단점

  • 클래스가 너무 많아져서 코드가 복잡해진다.
  • 새로운 제품 종류가 추가되면 인터페이스와 구현체를 모두 변경해야 하는 상황이 생길 수 있다.

 

사용 시기

  • 여러 관련 객체들을 한 번에 생성해야 하는 경우
  • 제품군 간의 호환성을 보장해야 하는 경우

 


 

3. 싱글톤 (Singleton)

 

클래스의 인스턴스를 오직 하나만 생성해서 사용해라!

 

 

장점

  • 메모리 낭비를 방지할 수 있다.
  • 객체 상태의 일관성을 유지할 수 있다.

 

단점

  • 전역 상태를 가지기 때문에 테스트하기 어렵다.
  • 모듈 간의 의존성이 올라가서 확장성과 유연성이 떨어진다.
  • 멀티스레드 환경에서 동기화 문제가 발생할 수 있다.
  • SOLID 원칙을 위반하는 사례가 많다.
    • 싱글톤 객체 하나가 여러가지 책임을 지니는 경우 (SRP 위반)
    • 다른 클래스와의 결합도가 높아지는 경우 (OCP 위반)
    • 클라이언트가 싱글톤 구현 객체에 의존하게 되는 경우 (DIP 위반)

 

사용 시기

  • 애플리케이션 전체에서 공유해야 하는 자원(설정, 로깅, 캐시 등)을 관리해야 하는 경우

 


 

4. 빌더 (Builder)

 

객체를 단계별로 생성할 수 있게 해줘라!

 

 

장점

  • 객체 생성 과정을 단계별로 세분화할 수 있다.
  • 가독성이 향상된다.

 

단점

  • 빌더 클래스를 별도로 관리해야 한다.
  • 객체 생성 과정이 간단한 클래스의 경우, 오히려 불필요한 복잡성으로 작용할 수 있다.
  • 일반적인 생성자보다 약간이나마 성능이 떨어진다.

 

사용 시기

  • 생성 과정이 복잡한 객체의 경우
  • 객체 생성에 다양한 옵션이 필요한 경우

 


 

5. 프로토타입 (Prototype)

 

기존 객체를 복제하여 새로운 객체를 생성해라!

 

 

장점

  • 생성 시간 및 비용을 절감할 수 있다.
  • 복잡한 객체를 더 쉽게 생성할 수 있다.
  • 객체를 생성하는 과정을 숨길 수 있다.

 

단점

  • 복잡한 구조의 객체를 복사하는 경우, clone 메서드 구현이 어렵고 버그가 발생하기 쉽다.

 

사용 시기

  • 객체 생성 과정이 복잡한 경우
  • 객체 생성 비용이 높은 경우
  • 기존 객체를 복사하여 사용하고 싶은 경우

 


구조 패턴

 

1. 어댑터 (Adapter)

 

호환되지 않는 클래스를 사용하기 위해,
중간에 어댑터를 두어라!

 

 

장점

  • 프로그램의 기본 비즈니스 로직과 외부 인터페이스와의 호환성을 위한 로직을 분리할 수 있다. (SRP)
  • 기존 클래스를 수정하지 않고 새로운 인터페이스를 사용할 수 있도록 호환 작업을 수행할 수 있다. (OCP)

 

단점

  • 어댑터 클래스를 추가적으로 관리해야 한다.
  • 어댑터 계층이 많아지면 성능이 저하될 수 있다.

 

사용 시기

  • 새로운 인터페이스와 레거시 코드가 호환되지 않는 경우
  • 기존의 코드를 재사용하고자 하나, 해당 코드가 사용하는 라이브러리를 수정할 수 없는 경우
  • 이미 만들어진 클래스를 새로운 인터페이스에 맞게 개조해야 하는 경우
  • 소프트웨어의 구 버전과 신 버전을 공존시키고 싶은 경우
  • 서드파티 라이브러리와의 통합이 필요한 경우

 


 

2. 복합체 (Composite)

 

객체들을 트리 구조로 구성하여.
단일 객체와 복합 객체를 동일하게 다뤄라!

 

 

장점

  • 전체-부분의 관계를 가지는 객체들의 구조를 정의할 때 유용하다.
  • 단일 객체와 복합 객체를 동일하게 처리할 수 있다.
  • 기존 코드를 수정하지 않고 새로운 구성 요소를 도입할 수 있다. (OCP)

 

단점

  • 공통 인터페이스가 과도하게 일반화 될 수 있다.
  • 트리 구조가 커질 경우, 성능이 떨어지고 디버깅이 어려워질 수 있다.
  • 각 구성 요소의 일관된 인터페이스 설계가 어려울 수 있다.
  • 의도치 않은 객체 조작 가능성이 생긴다.

 

사용 시기

  • 전체-부분의 계층적 구조를 가지는 객체들을 일관된 인터페이스를 통해 처리하고 싶은 경우

 


 

3. 데코레이터 (Decorator)

 

객체에 연쇄적으로 새로운 기능을 추가할 수 있도록 해라!

 

 

장점

  • 객체의 기능을 동적으로 확장할 수 있다.
  • 상속보다 유연하게 기능을 추가할 수 있다.
  • 런타임에 객체로부터 책임을 추가/삭제할 수 있다.
  • 각 데코레이터 클래스마다 고유한 책임을 가진다. (SRP)
  • 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있다. (OCP)

 

단점

  • 데코레이터 스택에서 특정 데코레이터를 제거하기 어렵다.
  • 데코레이터 스택 내의 순서에 따라 동작이 달라진다.
  • 데코레이터가 중첩된 형식의 객체 생성 방식은 가독성이 좋지 않다.
  • 객체의 원래 인터페이스를 파악하기 어려워진다.

 

사용 시기

  • 객체의 기능을 여러 방식으로 조합하여 확장해야 하는 경우
  • 상황에 따라 객체에 다양한 기능을 동적으로 추가/삭제해야 하는 경우
  • 상속을 통해 객체의 동작을 확장하는 것이 비효율적이거나 불가능한 경우

 


 

4. 퍼사드 (Facade)

 

복잡한 외부 시스템에 대한 간단한 인터페이스를 제공해라!

 

 

장점

  • 서브 시스템의 복잡성을 감출 수 있다.
  • 클라이언트와 서브 시스템 간의 결합도를 낮출 수 있다.
  • 코드를 단순화시킴으로써 유지보수성을 높일 수 있다.
  • 클라이언트가 외부 시스템의 코드를 모르더라도 사용할 수 있게 해준다.

 

단점

  • 퍼사드 클래스의 권한이 너무 높아질 수 있다.
  • 잘못 설계된 퍼사드는 오히려 모듈 간의 결합도를 높일 수 있다.
  • 퍼사드 클래스의 외부 시스템에 대한 의존성이 높다.

 

사용 시기

  • 사용하려는 외부 시스템이 너무 복잡한 경우
  • 복잡한 시스템의 기능을 단순화하려는 경우
  • 사용자에게 명확한 인터페이스를 제공하려는 경우
  • 클라이언트 코드와 외부 라이브러리 간의 결합도가 너무 높은 경우

 


 

5. 프록시 (Proxy)

 

프록시를 통해 다른 객체의 사용 흐름을 제어해라!

 

 

장점

  • 접근 제어, 지연 로딩, 캐싱 등의 다양한 추가 기능을 구현할 수 있다.
  • 서비스 객체의 라이프사이클을 관리할 수 있다.
  • 대상 객체는 자신의 기능에만 집중하고, 부가 기능 구현은 프록시 객체에 위임하여 다중 책임을 회피한다. (SRP)
  • 기존 서비스 객체 코드의 변경 없이 새로운 기능을 추가할 수 있다. (OCP)

 

단점

  • 프록시 객체 추가로 성능이 떨어질 수 있다.
  • 클라이언트가 실제 객체와 프록시 객체를 구분하기 어려운 경우 버그가 생길 수 있다.

 

사용 시기

  • 객체에 대한 접근 제어가 필요한 경우
  • 원격 객체 혹은 무거운 객체에 대한 지연 로딩이 필요한 경우
  • 추가적인 기능을 추가하고 싶지만, 서비스 객체를 수정할 수 없는 경우

 


 

6. 브릿지 (Bridge)

 

추상화와 구현을 분리하여 두 계층을 독립적으로 확장해라!

 

 

장점

  • 추상화 계층과 구현부가 각각의 책임에 집중할 수 있다. (SRP)
  • 추상화 계층과 구현부를 각각 독립적으로 확장할 수 있다. (OCP)

 

단점

  • 결합도가 높은 클래스에 적용하면 코드가 복잡해질 수 있다.
  • 추상화와 구현부를 명확히 분리해야 하는 부담이 생긴다.

 

사용 시기

  • 추상화와 구현 사이의 결합도를 낮추고자 하는 경우
  • Monolithic 구조의 클래스를 나누고 정리해야 하는 경우
  • 여러 차원에서 클래스를 확장해야 하는 경우

 


 

7. 플라이웨이트 (Flyweight)

 

여러 객체들 간의 공통 부분을 공유함으로써,
메모리 사용을 최적화해라!

 

 

장점

  • 애플리케이션의 메모리 사용량을 줄일 수 있다.
  • 객체 생성 비용이 감소한다.
  • 프로그램 속도가 향상될 수 있다.

 

단점

  • 내부 상태와 외부 상태를 철저히 구분하지 않는다면 예기치 못한 버그가 발생할 수 있다.
  • 공유 객체가 많아질 경우, 동시성 문제가 발생할 수 있다.

 

사용 시기

  • 동일한 객체를 많이 생성해야 하는 경우
  • 메모리 자원이 제한적인 경우

 


행동 패턴

 

1. 책임 연쇄 (Chain of Responsibility)

 

클라이언트의 요청을 처리하는 객체들을 분리하여,
체인 형태로 연결하여 사용해라!

 

 

장점

  • 요청 처리 로직을 유연하게 변경할 수 있다.
  • 각각의 핸들러가 자신이 해야하는 일만 수행할 수 있다. (SRP)
  • 클라이언트 코드를 수정하지 않고, 핸들러를 체인에 동적으로 추가/변경/삭제할 수 있다. (OCP)

 

단점

  • 요청이 체인 전체를 순회할 경우, 성능이 저하될 수 있다.
  • 체인의 순서를 변경할 때, 예기치 못한 문제가 발생할 수 있다.

 

사용 시기

  • 요청에 대해 다양한 종류의 처리를 순차적으로 해야 하는 경우
  • 요청을 여러 객체에서 처리해야 하는 경우
  • 요청을 처리하는 객체를 동적으로 결정해야 하는 경우

 


 

2. 커맨드 (Command)

 

요청 자체를 캡슐화해라!

 

 

장점

  • 요청 자체를 데이터로 다룰 수 있다.
  • 간단한 커맨드들을 조합하여 복잡한 커맨드를 만들 수 있다.
  • 작업을 호출하는 클래스와 작업을 수행하는 클래스를 분리할 수 있다. (SRP)
  • 새로운 커맨드를 유연하게 추가할 수 있다. (OCP)

 

단점

  • 호출자와 수신자 간의 관계를 파악하기 어려워진다.
  • 단순한 요청에도 불필요한 클래스를 생성하게 될 수 있다.

 

사용 시기

  • 요청 정보에 대한 특별한 처리를 하고 싶은 경우
  • 작업들의 실행을 지연하고 싶은 경우
  • 실행 취소/재실행 기능을 구현해야 하는 경우

 


 

3. 반복자 (Iterator)

 

집합 객체 내의 요소들을 순차적으로 접근할 수 있는 방법을 제공해라!

 

 

장점

  • 순회 방식을 캡슐화 할 수 있다.
  • 여러 형태의 컬렉션에 대한 일관성 있는 순회 방식을 제공할 수 있다.
  • 클라이언트 코드를 단순화할 수 있다.

 

단점

  • 다양한 컬렉션 타입별로 별도의 반복자 클래스를 만들어야 한다.
  • 내부 구현에 따라 반복자가 추가적인 메모리를 사용할 수 있다.

 

사용 시기

  • 데이터 내부 구조와 관계 없는 순회 방법을 제공해야 하는 경우
  • 컬렉션을 순회하는 다양한 방식을 지원해야 하는 경우
  • 다양한 컬렉션에 대해 일관된 인터페이스를 제공해야 하는 경우
  • 데이터 집합을 표현하는 컬렉션 종류가 나중에 변경될 수 있는 경우
  • 컬렉션의 내부 구조를 클라이언트로부터 숨겨야 하는 경우

 


 

4. 중재자 (Mediator)

 

객체 간의 직접 통신을 제한하고,
중재자 객체를 통해서만 협력하게 해라!

 

 

장점

  • 객체 간의 의존성을 낮출 수 있다.
  • 객체가 서로 통신하기 위해, 서로를 참조하지 않아도 된다.
  • 시스템의 확장성이 좋아진다.
  • 다양한 객체 간의 통신을 한 곳에서 관리할 수 있다.

 

단점

  • 중앙 집중식으로 관리되므로 중재자 로직이 복잡해진다.
  • 중재자의 권한이 너무 커진다.
  • 새로운 객체 간의 상호작용이 추가되면, 중재자의 수정이 불가피하다.

 

사용 시기

  • 여러 객체들이 복잡하게 상호작용하는 경우
  • 객체들 간의 직접적인 참조를 줄이고 싶은 경우

 


 

5. 메멘토 (Memento)

 

객체의 이전 상태를 복원할 수 있는 방법을 제공해라!

 

 

장점

  • 객체의 캡슐화를 위반하지 않고 상태를 저장할 수 있다.
  • Caretaker가 Originator의 상태를 기록하게 함으로써, Originator의 코드를 단순화할 수 있다.
  • 상태 복원 로직을 분리하여 관리할 수 있다. (SRP)

 

단점

  • 객체의 상태를 저장하므로 메모리 사용량이 증가한다.
  • Caretaker가 Originator의 라이프사이클을 추적하지 않는다면, 더이상 쓸모없는 메멘토가 영구적으로 남을 수 있다.

 

사용 시기

  • 객체의 history를 유지해야 하는 경우
  • 객체 상태의 스냅샷을 생성하려 하는 경우
  • 객체의 이전 상태를 복원할 수 있어야 하는 경우
  • 실행 취소 기능을 구현하려고 하는 경우

 


 

6. 옵저버 (Observer)

 

어떤 객체의 상태가 변할 때,
의존하는 다른 객체들이 자동으로 반응하도록 할 수 있다!

 

 

장점

  • 대상 객체의 상태 변경을 주기적으로 확인하지 않아도 자동으로 감지할 수 있다.
  • 대상 객체와 옵저버 객체 간의 관계를 느슨하게 유지할 수 있다.
  • 대상 객체를 변경하지 않고 새로운 옵저버를 추가할 수 있다. (OCP)

 

단점

  • 옵저버 간의 알림 순서를 제어할 수 없다.
  • 옵저버 관리가 복잡할 수 있다.
  • 다수의 옵저버가 등록될 경우, 성능 저하 및 메모리 누수의 위험성이 존재한다.

 

사용 시기

  • 어떤 객체의 상태 변화에 따라 다른 객체들을 갱신해야 하는 경우
  • 이벤트 기반 시스템을 구현해야 하는 경우
  • MVC 아키텍처를 구현해야 하는 경우

 


 

7. 상태 (State)

 

객체의 내부 상태에 따른 객체의 행동을 정의해라!

 

 

장점

  • 복잡한 조건문을 사용하는 대신, 상태 객체로 행위를 분리할 수 있다.
  • 상태별 행동 변화를 모듈화하여 관리할 수 있다.
  • 상태 객체를 싱글톤으로 관리함으로써 일관성 없는 상태 주입을 방지할 수 있다.
  • 상태와 관련된 코드를 별도의 클래스로 구성할 수 있다. (SRP)
  • 기존 상태 클래스를 변경하지 않고, 새로운 상태를 도입할 수 있다. (OCP)

 

단점

  • 상태 클래스가 많고 상태가 변경되는 규칙이 다양하다면, 상태 변경 코드가 복잡해질 수 있다.
  • 객체에 적용할 상태가 별로 없거나, 상태가 잘 변경되지 않는다면 패턴을 적용하는 것이 오히려 비효율적일 수 있다.
  • 상태 변경 로직이 분산되어 있어, 전체 동작을 파악하기 어려울 수 있다.

 

사용 시기

  • 객체의 행동이 내부 상태에 따라 달라져야 하는 경우
  • 상태 변경이 빈번하고 복잡한 경우
  • 런타임에 객체의 상태를 유동적으로 변경해야 하는 경우
  • 상태 변경을 위한 대규모 조건 분기 코드 및 중복 코드가 존재하는 경우

 


 

8. 전략 (Strategy)

 

실시간으로 알고리즘을 선택할 수 있게 해줘라!

 

 

장점

  • 런타임에 객체 내부에서 사용하는 알고리즘을 교체할 수 있다.
  • 알고리즘의 구현 세부 정보를 숨길 수 있다.
  • 조건문 사용 없이 다양한 알고리즘을 적용할 수 있다.
  • 기존 코드를 변경하지 않고 새로운 전략을 도입할 수 있다. (OCP)

 

단점

  • 알고리즘 개수가 많지 않거나, 알고리즘이 잘 교체되지 않는다면 코드가 오히려 복잡해질 수 있다.
  • 클라이언트가 전략 간의 차이점을 잘 인지하고 있어야 한다.
  • 알고리즘 개수가 많아질수록 관리해야 할 객체의 개수가 늘어난다.
  • 인터페이스 설계에 신경을 쓰지 않으면 전략의 일관성이 떨어질 수 있다.

 

사용 시기

  • 여러 알고리즘을 경우에 따라 선택해야 하는 경우
  • 알고리즘의 동작이 런타임에 실시간으로 교체되어야 하는 경우
  • 동작을 실행하는 방식만 차이가 있는 유사한 클래스가 많은 경우
  • 비즈니스 로직과 알고리즘 로직을 분리해야 하는 경우
  • 알고리즘 교체를 위한 조건문 분기가 너무 많은 경우
  • 알고리즘이 노출되어서는 안되는 데이터에 접근하는 경우

 


 

9. 템플릿 메서드 (Template Method)

 

상위 클래스에서 기본 구조를 정의하고,
하위 클래스에서 세부 단계를 구현해라!

 

 

장점

  • 알고리즘의 구조를 고정하여 일관성을 확보할 수 있다.
  • 변하는 부분과 변하지 않는 부분을 분리할 수 있다.
  • 중복 코드를 제거할 수 있다.

 

단점

  • 구조가 고정되므로 유연성이 떨어진다.
  • 상위 클래스 변경 시 모든 하위 클래스의 수정이 불가피하다.
  • 하위 클래스를 통해, 상위 클래스의 기본 구현을 덮어 쓸 수 있다. (LSP 위반)

 

사용 시기

  • 여러 하위 클래스에서 동일한 알고리즘 구조를 유지해야 하는 경우

 


 

10. 방문자 (Visitor)

 

객체 구조와 객체마다 달라지는 동작을 분리해라!

 

 

장점

  • 복잡한 데이터 구조에 대한 연산을 분리할 수 있다.
  • 객체 구조를 유지할 수 있다.
  • 기존 클래스 변경 없이 새로운 작업을 추가할 수 있다. (OCP)

 

단점

  • 객체 종류가 추가되는 경우, 모든 방문자를 수정해야 한다.

 

사용 시기

  • 데이터 구조에 새로운 기능을 추가해야 하는 경우
  • 복잡한 객체 구조 내의 모든 요소에 대한 작업을 정의해야 하는 경우
  • 특정 행위가 일부 객체에서만 수행되는 경우

 


 

11. 인터프리터 (Interpreter)

 

특정 문법을 해석하여 적절한 명령을 실행하는
인터프리터를 제공해라!

 

 

장점

  • 명령 해석 과정을 객체화하여 관리할 수 있다.

 

단점

  • 문법이 복잡한 경우, 인터프리터 자체가 복잡해질 수 있다.
  • 해석하는 비용이 추가되어, 성능이 저하될 수 있다.
  • 문법 추가 시, 전체 구조가 수정될 수 있다.

 

사용 시기

  • 간단한 언어 및 스크립트를 해석하여 실행해야 하는 경우
  • 문법 규칙을 캡슐화하여 확장 가능한 인터프리터가 필요한 경우

 

 

 

반응형
저작자표시 비영리 변경금지 (새창열림)

'개발' 카테고리의 다른 글

스프링 배치(Spring Batch) 알아보기  (0) 2025.04.29
[Challenge.with] 엔티티를 어떻게 정의할까?  (0) 2025.04.26
스프링 총정리  (0) 2025.04.15
객체 지향 프로그래밍(OOP) 총정리  (0) 2025.04.14
[ 디자인 패턴: 행동 ] (10) 방문자 (Visitor)  (0) 2025.04.13
'개발' 카테고리의 다른 글
  • 스프링 배치(Spring Batch) 알아보기
  • [Challenge.with] 엔티티를 어떻게 정의할까?
  • 스프링 총정리
  • 객체 지향 프로그래밍(OOP) 총정리
sleepzzz214
sleepzzz214
아! 응애에요~!
  • sleepzzz214
    응애 개발자의 일지
    sleepzzz214
  • 전체
    오늘
    어제
    • ⭐ (52) N
      • 개발 (52) N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    싱글톤
    의존성 주입
    객체 지향 프로그래밍
    제어의 역전
    DB
    프로토타입
    데이터베이스
    스프링 빈
    java
    스프링
    Solid
    구조 패턴
    객체 지향 설계
    Kafka
    Spring
    김영한 스프링 강의
    자바
    DI
    상태
    디자인 패턴
    행동 패턴
    생성 패턴
    스프링부트
    의존 관계 주입
    @Autowired
    상태 패턴
    스프링 핵심 원리 - 기본편
    state
    싱글톤 패턴
    singleton
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
sleepzzz214
디자인 패턴 총정리
상단으로

티스토리툴바