소프트웨어 시스템에서 컴포넌트의 구조와 컴포넌트들의 관계, 연결 유형 및 상호작용을 의미합니다.
아키텍처를 적용하면 소프트웨어 시스템을 이해하기 쉽게 만들어 줍니다. 시스템이 구조화되어 일부를 독립적으로 작업할 수 있게 되고, 확장과 재사용을 용이하게 해줍니다.
패키지 다이어그램은 패키지간의 관계를 나타낸 다이어그램으로 패키지와 관계로 이루어져 있습니다.
import는 클래스를 통해 다른 클래스에 접근할 수 있을 때 import라고 할 수 있고, 다른 클래스에 접근할 수 없을 때 access라고 할 수 있습니다.
개발될 시스템의 소프트웨어 및 하드웨어 컴포넌트의 물리적인 관계를 나타낸 다이어그램입니다. 배치 다이어그램에서 배치는 Batch가 아닙니다.
배치 다이어그램은 직육면체로 표기되는 노드와 노드들 간의 통신 경로인 링크로 이루어져 있습니다.
배치 다이어그램에서는 모델링 요소에 대한 추가적인 의미를 부여하기 위해 스테레오 타입을 많이 사용합니다.
웹 서비스에서 가장 기본적인 형태로 서버는 강력한 컴퓨팅 파워를 기반으로 모든 처리를 감당하며 클라이언트가 요청하는 기능이나 자원을 제공합니다.
서버는 클라이언트에 비해 상대적으로 보안이 좋고 데이터가 집중화 되어 관리에 용이합니다. 단점으로는 서버가 모든 것을 책임지기 때문에 많은 요청에 의해 병목현상이나 심지어는 리소스 공급이 중단될 수 있습니다.
소프트웨어의 기능을 상호 작용하는 여러 계층으로 분할하는 방법으로 각 계층은 인터페이스를 통해 연결되며 제한적인 커뮤니케이션을 한다는 특징을 가집니다.
가장 대표적인 계층형 아키텍처는 통신 시스템으로 각 층계에서는 다음 층계와만 소통하기 때문에 추상화 및 캡슐화 수준이 높고 재사용성이 유리하다는 장점을 가지고 있습니다.
이벤트 기반 아키텍처는 이벤트를 생성하는 생산자(producer)와 이벤트 수신을 대기하는 소비자(consumer)로 구성되어 있습니다. 동영상 스트리밍의 경우에는 이벤트 기반 아키텍처의 대표적인 예라고 할 수 있습니다. 스트리밍 시슽메은 외부로부터 영상 데이터를 받아 동영상 버퍼를 채우는 생산자와 이를 이용해 동영상을 재생하는 소비자가 존재합니다.
이벤트 기반 아키텍처는 명확한 역할의 구분이 존재하기 때문에 캡슐화와 응집에 강점을 가집니다. 또한 소비자를 쉽게 추가할 수 있다는 장점이 있습니다.
하지만 이벤트가 많아질수록 복잡하고 정교한 제어가 필요하며 특히 허용되지 않는 이벤트에 대해 적절한 오류 메시지와 제어가 필요합니다.
모델 뷰 컨트롤(MVC: Model-View-Control)은 백엔드 개발에서 주로 사용되며 사용자 인터페이스로부터 비즈니스 로직과 데이터를 분리하도록 하는 아키텍처입니다.
MVC 아키텍처를 이용하면 컴포넌트의 결합이 비교적 약해 다른 부분에 영향을 주지 않고 수정이 가능해집니다. 또한 데이터와 비즈니스 로직이 분리되어 있기 때문에 하나의 모델을 위해 다수의 뷰(사용자 화면)을 제공할 수 있습니다.
하지만 분리된 컴포넌트들은 프로그래밍의 복잡도가 상승하게 되고 데이터 접근을 위해 단계를 거쳐야 한다는 비효율성이 있습니다.
파이프에 여러 개의 필터가 존재하고 각 필터를 거칠 때마다 단계적으로 데이터의 변환이 이루어지며 이동하게 됩니다.
이를 통해 단순성과 재사용성, 병렬성을 증가시킬 수 있습니다. 예를 들어 그래픽 렌더링 작업의 경우 파이프 라인이 존재하며 적절한 필터를 추가함으로써 단순성과 재사용성을 증가시킬 수 있으며, 여러 개의 파이프를 이용해 병렬성을 증가시킬 수 있습니다.
말 그대로 하나의 중심 데이터가 존재하고 이를 여러 객체들이 접근할 수 있는 것을 의미합니다. 이를 통해 접근자들 각각이 가지는 결합도가 낮아 수정 및 확장에 유리한 점을 가집니다.
하지만 중앙 집중형의 단점인 단일 지점 장애(single point of failure)가 발생할 수 있습니다.
소위 P2P로도 불리는 스타일로 컴포넌트들이 동등하고 각각이 서버이자 클라이언트 관계로 얽혀있는 것을 의미합니다.
모두가 서버 및 클라이언트 역할을 하기 때문에 규모 확장성과 신뢰성이 높습니다. 또한 컴포넌트의 고장이 존재하더라도 전체 시스템이 다운되지 않습니다. 하지만 흩어진 노드들의 경우 한번에 제어하기가 어려우며 보안에 취약할 수 있습니다.
디자인 패턴은 좋은 설계가 이루어지도록 도와주는 노하우라고 할 수 있습니다. 개발하는 과정에서 비슷한 역할의 클래스를 자주 사용하게 되는데, 이를 패턴으로 정리하여 목록화한 것을 디자인 패턴이라고 합니다.
아키텍처와 디자인
디자인은 클래스를 설계하는 방식을 의미한다면 아키텍처는 시스템 수준의 설계라고 할 수 있습니다.
디자인 패턴을 사용하면 개발 협력관계에 있는 사람들끼리의 의사소통을 향상시킬 수 있습니다. 고정된 패턴을 사용함으로써 시스템에 대한 이해를 높일 수 있고 재사용과 확장이 쉬워지기 때문입니다.
위 사이트에는 자세한 예시와 함께 디자인 패턴들을 알기 쉽게 설명하고 있습니다. 개인적으로 위 사이트에 보시는 걸 추천합니다.
기본 패턴이란 가장 흔히 사용되는 기본적인 패턴을 의미합니다. 개발 중에 무의식적으로 사용하고 있지만 그것들도 패턴이 존재하는 것이었습니다.
추상화 과정에서 공유하는 정보를 담은 클래스와 개별 정보를 가진 클래스가 존재할 수 있습니다. 예를 들어 도서관에는 책이 존재하지만 같은 책이 여러 권 존재할 수 있습니다.
이때 책은 개념 클래스가 되고 실제로 도서관에서 책을 구분하는 기준(바코드 번호 등)을 담은 클래스가 실체 클래스가 됩니다.
//개념 클래스
class Book{
private String name;
private String author;
private String ISBN;
private String publicationDae;
//Book 1개에 LibraryBook이 여러개 존재할 수 있음
private LibraryBook[] libraryBooks;
}
//실체 클래스
class LibraryBook{
private String barCode;
private String purchaseDate;
}
이는 중복된 정보를 저장하지 않기 위해 사용하는 것입니다. 그런데 상속을 사용하게 될 경우에는 중복된 정보를 계속해서 저장해야 하기 때문에 적합한 해결방법이 될 수 없습니다.
하나의 객체가 여러 가지 역할을 가지게 될 때 사용됩니다. 예를 들어 사람은 회사에서는 직원으로써 일할 수도 있고, 가정에서는 아버지로써의 역할을 할 수 있습니다. 또한 밤에는 배트맨이 될 수도 있겠죠.
class Person{
private String name;
private int age;
}
class Emplyoee{
private Person person;
private String role;
private int pay;
}
class Father{
private Person person;
private String home;
}
class Batman{
private Person person;
private int power;
}
자신의 역할을 다른 클래스의 메서드를 이용해 처리하는 것을 의미합니다. 예를 들어 스택 클래스는 링크드 리스트로 구현할 수 있는데, 링크드 리스트에 존재하는 모든 메서드를 사용하지 않아도 됩니다.
따라서 스택 클래스에서는 링크드 리스트 클래스를 이용해 작업을 처리하지만 결과적으로 사용자에게 보여지는 것은 push나 pop, isEmpty 정도일 것입니다.
class LinkedList<T>{...}
class Stack<T>{
private LinkedList<T> list;
public void push(T item){
list.addLast(item);
}
public void pop(){
list.removeLast();
}
public T top(){
return list.atLast();
}
...
}
슈퍼 클래스와 서브클래스가 재귀적인 관계를 가질 때 계층 구조 패턴이라고 할 수 있습니다. 아래는 대표적인 계층 구조 패턴의 예시입니다.
계층 구조 패턴의 일반적인 형태 | 출처 : https://course.ccs.neu.edu/cs5010f18/lecture8.html
리눅스에서는 파일과 디렉토리 모두 파일로써 처리됩니다. 파일에는 데이터가 디렉토리에는 파일들의 목록이 존재하는 형식입니다. 이를 계층 구조 패턴 형식에 적용할 수 있습니다.
생성 패턴은 기존 코드의 유연성과 재사용성을 증가시킬 수 있도록하는 다양한 방법들입니다.
팩토리 패턴(Factory pattern)이란 객체 생성을 위한 인터페이스를 정의하고 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 인터페이스에서 이루어지도록하는 것을 의미합니다.
여기에서 인터페이스는 자바 언어에서 사용하는 인터페이스와 다른 개념입니다. 팩토리 패턴에서 인터페이스란 인스턴스를 생성해주는 중간자 역할이라고 보는 것이 맞습니다.
크로스 플랫폼 다이얼로그(대화 상자) 예시. -출처: https://refactoring.guru/ko/design-patterns/factory-method
같은 버튼이라고 해도 플랫폼에 따라 다르게 보일 수 있습니다. 하지만 클릭한다는 본질적인 기능을 똑같기 때문에 인터페이스를 구현함으로써 각 클래스들을 나타낼 수 있습니다.
작동하는 플랫폼을 추가하고 싶다면 Dialog를 상속한 새로운 플랫폼 다이얼로그를 만들어서 사용하면 됩니다. 이를 통해 유연성을 높일 수 있습니다.
추상 팩토리는 기본 팩토리 패턴과 유사하지만 구체적인 클래스가 아닌 연관된 클래스를 생성하는 인터페이스를 이용한다는 점에서 차이가 있습니다. 즉 추상 클래스들만을 이용해 팩토리 패턴을 구현한다는 의미입니다.
추상 팩토리 패턴 구조 - 출처: https://refactoring.guru/ko/design-patterns/abstract-factory
위 그림을 보면 팩토리들은 모두 추상 클래스 혹은 인터페이스로 선언되어 있고, 이들을 생성하도록 구성되어 있습니다. 이를 통해 생성된 객체들은 호환성이 보장되기 때문에 확장에 유리해집니다.
어떤 객체를 복제한다는 것은 쉬워 보이지만 실제로는 간단한 일이 아닐 수 있습니다. 예를 들어 보겠습니다.
interface Something{...}
class SomeOne implements SomeThing{...}
class Main{
public static void main(String[] args){
Something exam1 = new SomeOne();
SomeOne exam2 = new SomeOne();
}
}
위 코드에서 exam1은 Something 인터페이스 객체로써 존재합니다. 이때 exam1에서 SomeOne에 존재하는 멤버변수에 접근할 수 없기 때문에 완벽한 프로토타입 생성이 불가능해집니다.
또한 value가 복사되는 것이 아니라 reference가 복제된다면 기존 값이 변경될 경우 프로토타입으로 생성된 객체 또한 문제가 발생하게 됩니다.
이를 해결하기 위해서는 프로토타입을 생성할 수 있는 메서드를 계속 구현해 나가서 전달하거나 생성자를 이용해 해결할 수 있습니다.
클래스 계층구조에 속한 객체 집합의 복제 | 출처 :https://refactoring.guru/ko/design-patterns/prototype
싱글턴 패턴이란 클래스의 인스턴스가 단 하나만 존재하도록 강제하는 패턴입니다. 데이터베이스는 싱글턴 패턴을 적용할 수 있는 예시라고 할 수 있습니다. 데이터베이스에서 데이터를 호출할 때 데이터의 수정이 이루어진다면 참으로 난감할 수 없지 않겠습니까.
싱글턴 패턴은 아래와 같이 생성자를 숨기고 내부에 instance를 제공하는 것으로 구현할 수 있습니다.
class Singleton{
private static Singleton instance;
private Singleton(){...}
public static Singleton getInstance(){
if(instance==null)
instance = new Singleton();
return instance;
}
...
}
일반 패턴의 계층 패턴과 유사합니다. 보통은 재귀적으로 수행되는 요청이 필요할 때 사용됩니다. 예를 들어 웹 페이지는 각각의 컴포지트들로 이루어지는데 이를 렌더링 할때에는 컴포지트들에 재귀적으로 호출이 일어나게 됩니다.
interface Graphic{
void draw();
}
class WebPage implements Graphic{
public void draw();
}
class Button implements Graphic{
public void draw(){
createButton();
}
}
class SubmitButton extends Button{
public void draw(){
super.draw();
createSubmitText();
}
}
데코레이터 패턴은 기존 코드를 훼손하지 않고 추가적인 행동을 부여하기 위해 사용합니다. 이를 위해 기존 코드를 감싸는 래퍼(Wrapper)를 통해 객체들을 감싸고 새로운 행동을 부여합니다.
이렇게 래퍼로 감싸진 객체들은 공통적인 기능을 수행할 수 있으므로 하나의 묶음으로써 기능할 수 있게 됩니다. 아래 예시처럼 send라는 기능을 수행하는 데코레이터를 구현하여 서로 다른 객체들이 같은 동작을 할 수 있게끔 만들어줍니다.
다양한 알림 메서드들이 데코레이터가 됩니다. | 출처 : https://refactoring.guru/ko/design-patterns/decorator
어댑터라는 말처럼 호환되지 않는 객체들을 위한 인터페이스를 만들어주는 것을 의미합니다. 예를 들어 문자열로만 값을 전달하는 클래스에 대해 어댑터를 생성하여 문자열을 정수나 실수형으로 전달하거나 새로운 형식으로 전달하도록 할 수 있습니다.
class Buffer{
private String buf;
public String read(){...}
}
interface BufferAdapter{
int readInteger();
float readFloat();
String readString();
}
class Adapter implements BufferAdapter{
private Buffer buffer;
public int readInteger(){...}
public float readFloat(){...}
public String readString(){...}
}
퍼사트 패턴은 클래스 패키지에 인터페이스를 제공하기 위해 사용됩니다. 어떤 기능을 수행하기 위해 다양하고 복잡한 인터페이스들을 조합해야 한다면 이러한 기능들을 하나의 메서드로 만들어 인터페이스로 제공합니다.
이를 통해 사용자는 간단한 인터페이스를 통해서 패키지에 접근하게 되므로 유지보수가 쉬워집니다. 그리고 패키지를 관리하는 개발자는 기능의 수정이 발생했을 경우 해당 인터페이스를 수정하여 주는 것으로 해결할 수 있게 됩니다.
어댑터와 퍼사트 패턴은 중간 인터페이스를 제공하여 기능을 수행하게 해준다는 공통점이 있지만 퍼사트 패턴은 복잡한 인터페이스의 간소화에 의미를 두는 반면 어댑터 패턴은 두 클래스 사이의 호환을 위해서 사용된다는 차이점 있습니다.
프록시라는 말처럼 어떤 것을 대신한다는 의미입니다. 실제 객체에 수행하면 되는 것을 왜 프록시 패턴을 사용해야 하냐면 특정 기능의 경우에는 수행 전이나 후에 처리가 필요할 수 있기 때문입니다.
프록시 패턴 구조 | 출처 : https://refactoring.guru/ko/design-patterns/proxy
프록시에서도 실제 서비스에 수행되는 모든 작업들을 수행할 수 있지만 이것은 실제 서비스에 반영되지 않습니다. 문서 작업을 할 때 저장을 하지 않으면 바뀐 데이터들은 원본에 반영되지 않습니다. 프록시된 서비스에만 반영된 것들을 새롭게 저장하거나 원본에 반영하도록 할 수 있습니다.
옵서버는 쉽게 말해 유튜브의 구독 서비스라고 생각할 수 있습니다. (엄밀하게는 구독 서비스가 옵서버 패턴을 따르는 거지만) 서비스를 제공하는 어떤 객체는 기능 수행을 기다리는 여러 개의 옵저버들을 지니고 있습니다. 그래서 기능을 수행하면 지니고 있는 옵저버들에게 모두 전파하도록 하는 심플한 개념입니다.
옵서버 패턴의 예시 | 출처 : https://refactoring.guru/ko/design-patterns/observer
컴포넌트가 다른 컴포넌트를 참조하게 될 때 구조가 커지면 커질수록 의존성이 커지게 됩니다. 따라서 컴포넌트에서 다른 컴포넌트를 참조하고 싶다면 중재자 클래스를 만들어 해당 클래스와만 통신하게 하는 것으로 결합을 낮출 수 있습니다.
비행기와 관제탑의 역할 | 출처 : https://refactoring.guru/ko/design-patterns/mediator
상태 패턴은 객체의 모든 가능한 상태들에 대해 클래스를 만들고 상태별 행동들을 구현하는 것을 의미합니다. 특히 상태에 따라 다른 작업이 이루어져야하는 프로그램의 개발에 사용될 수 있습니다. 예를 들어 음악플레이어의 경우 음악이 재생중일 때, 음악이 멈춰 있을 때와 같이 상태를 나눌 수 있으며 해당 상태에서 메서드들은 서로 다른 행위가 일어날 수 있습니다.