-
[CS] 디자인 패턴과 프로그래밍 패러다임CS 2024. 10. 22. 17:37
디자인 패턴
- 디자인 패턴이란 프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용하여 해결할 수 있도록 하나의 ‘규약’ 형태로 만들어 놓은 것이다.
싱글톤 패턴
- 싱글톤 패턴은 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴이다.
- 보통 데이터베이스 연결 모듈에 많이 사용한다.
- 예시 코드
- class Singleton { private static class singleInstanceHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return singleInstanceHolder.INSTANCE; } } public class HelloWorld { public static void main(String[] args) { Singleton a = Singleton.getInstance(); Singleton b = Singleton.getInstance(); System.out.println(a.hashCode()); System.out.println(b.hashCode()); if (a == b) { System.out.println(true); } } }
- 장점 하나의 인스턴스를 만들어 놓고 해당 인스턴스를 다른 모듈들이 공유하며 사용하기 때문에 인스턴스를 생성할 때 드는 비용이 줄어든다.
- 단점
- 단위 테스트에 걸림돌이 된다.
- 단위 테스트는 테스트가 서로 독립적이어야 하며 테스트를 어떤 순서로든 실행할 수 있어야 한다. 하지만 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이므로 각 테스트마다 독립적인 인스턴스를 만들기가 어렵다.
- 의존성이 강해진다. (= 모듈 간의 결합을 강하게 만들 수 있다.)
- 단위 테스트에 걸림돌이 된다.
의존성 주입
- DI (Dependency Injection)
- 의존성 주입으로 모듈 간의 결합을 느슨하게 만들어 싱글톤 패턴의 단점 해결 가능.
A가 B에 의존성이 있다 == B의 변경 사항에 대해 A 또한 변해야 한다.
A가 B에 의존성이 있다 == B의 변경 사항에 대해 A 또한 변해야 한다.
- 메인 모듈이 직접 다른 하위 모듈에 대한 의존성을 주입 → 의존성 주입자(dependency injector)가 간접적으로 의존성을 주입하는 방식
- 메인 모듈(상위 모듈)은 하위 모듈에 대한 의존성이 떨어지게 됨 (= 디커플링 된다)
장점
- 모듈들을 쉽게 교체할 수 있는 구조가 되어 테스팅하기 쉽고 마이그레이션 하기도 수월함.
- 구현할 때 추상화 레이어를 넣고 이를 기반으로 구현체를 넣어 주기 때문에 애플리케이션 의존성 방향이 일관되고, 애플리케이션을 쉽게 추론할 수 있으며, 모듈 간의 관계들이 조금 더 명확해짐.
단점
- 모듈들이 더욱더 분리되므로 클래스 수가 늘어나 복잡성이 증가될 수 있으며, 약간의 런타임 패널티가 생기기도 한다.
원칙
- 상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 한다.
- 상위 모듈과 하위 모듈 둘 다 추상화에 의존해야 하며, 이때 추상화는 세부 사항에 의존하지 말아야 한다.
팩토리 패턴
- 팩토리 패턴은 객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴이자 상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴이다.
- 상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 가지며 상위 클래스에서는 인스턴스 생성 방식에 대해 전혀 알 필요가 없기 때문에 더 많은 유연성을 갖게 된다.
- 객체 생성 로직이 따로 떼어져 있기 때문에 코드를 리팩터링하더라도 한 곳만 고칠 수 있게 되어 유지 보수성이 증가된다.
- 예시 코드
- enum CoffeeType { LATTE, ESPRESO } abstract class Coffee { protected String name; public String getName() { return name; } } class Lattee extends Coffee { public Lattee() { name = "latte"; } } class Espresso extends Coffee { public Espresso() { name = "Espresso"; } } class CoffeeFactory { public static Coffee createCoffee(CoffeeType type) { switch (type) { case LATTE: return new Latte(); case ESPRESSO: return new Espresso(); default: throw new IllegalArgumentException("Invalid coffee type: " + type); } } } public class Main { public static void main(String[] args) { Coffee coffee = CoffeeFactory.createCoffee(CoffeeType.LATTE); System.out.println(coffee.getName()); //latte } }
전략 패턴
- 전략 패턴은 객체의 행위를 바꾸고 싶은 경우 ‘직접’ 수정하지 않고 전략이라고 부르는 ‘캡슐화한 알고리즘’을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴이다.
- 컨텍스트: 상황, 맥락, 문맥을 의미하며 개발자가 어떠한 작업을 완료하는 데 필요한 모든 관련 정보
- 정책 패턴이라고도 함
옵저버 패턴
- 주체가 어떤 객체의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴이다.
- 주체: 객체의 상태 변화를 보고 있는 관찰자
- 옵저버: 이 객체의 상태 변화에 따라 전달되는 메서드 등을 기반으로 ‘추가 변화 사항’이 생기는 객체들
- 주로 이벤트 기반 시스템에 사용한다. MVC 패턴에도 사용된다.
- 예시 주체라고 볼 수 있는 모델에서 변경 사항이 생겨 update() 메서드로 옵저버인 뷰에 알려주고 이를 기반으로 컨트롤러 등이 동작하는 것이다.
- 예시 코드
enum CoffeeType { LATTE, ESPRESO } abstract class Coffee { protected String name; public String getName() { return name; } } class Lattee extends Coffee { public Lattee() { name = "latte"; } } class Espresso extends Coffee { public Espresso() { name = "Espresso"; } } class CoffeeFactory { public static Coffee createCoffee(CoffeeType type) { switch (type) { case LATTE: return new Latte(); case ESPRESSO: return new Espresso(); default: throw new IllegalArgumentException("Invalid coffee type: " + type); } } } public class Main { public static void main(String[] args) { Coffee coffee = CoffeeFactory.createCoffee(CoffeeType.LATTE); System.out.println(coffee.getName()); //latte } }
자바의 상속 (extends)
- 상속은 자식 클래스가 부모 클래스의 메서드 등을 상속받아 사용하며 자식 클래스에서 추가 및 확장을 할 수 있는 것
- 재사용성, 중복성의 최소화
자바의 구현 (implements)
- 구현은 부모 인터페이스를 자식 클래스에서 재정의하여 구현하는 것을 의미한다.
- 상속과는 달리 반드시 부모 클래스의 메서드를 재정의하여 구현해야 한다.
→ 상속은 일반 클래스, abstract 클래스를 기반으로 구현하며, 구현은 인터페이스를 기반으로 구현한다.
프록시 패턴과 프록시 서버
- 프록시 패턴은 대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채 해당 접근을 필터링하거나 수정하는 등의 역할을 하는 계층이 있는 디자인 패턴이다.
- 이를 통해 객체의 속성, 변환 등을 보완하며 보안, 데이터 검증, 캐싱, 로깅에 사용한다.
프록시 서버
- 서버와 클라이언트 사이에서 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 컴퓨터 시스템이나 응용 프로그램을 가리킨다.
- nginx
- 비동기 이벤트 기반의 구조와 다수의 연결을 효과적으로 처리 가능한 웹 서버이다.
- 서버를 구축할 때 앞단에 nginx를 두면, 익명 사용자가 직접적으로 서버에 접근하는 것을 차단하고, 간접적으로 한 단계를 더 거치게 만들어 보안을 강화할 수 있다.
- nginx를 프록시 서버로 둬서 실제 포트를 숨길 수 있다.
- CloudFlare
- 전 세계적으로 분산된 서버가 있고 이를 통해 어떠한 시스템의 콘텐츠 전달을 빠르게 할 수 있는 CDN 서비스이다.
- CDN(Content Delivery Network): 각 사용자가 인터넷에 접속하는 곳과 가까운 곳에서 콘텐츠를 캐싱 또는 배포하는 서버 네트워크를 말한다. 이를 통해 사용자가 웹 서버로부터 콘텐츠를 다운로드하는 시간을 줄일 수 있다.
- DDOS 공격 방어
- 의심스러운 트래픽(특히 사람이 아닌 시스템을 통해 오는 트래픽)을 자동으로 차단해서 DDOS 공격으로부터 보호한다.
- DDOS: 짧은 시간 동안 네트워크에 많은 요청을 보내 네트워크를 마비시켜 웹 사이트의 가용성을 방해하는 사이버 공격 유형
- 의심스러운 트래픽(특히 사람이 아닌 시스템을 통해 오는 트래픽)을 자동으로 차단해서 DDOS 공격으로부터 보호한다.
- HTTPS 구축
- HTTPS 구축할 때 인증서 기반으로 구축할 수도 있지만, CloudFlare를 사용하면 별도의 인증서 설치 없이 좀 더 쉽게 HTTPS를 구축할 수 있다.
- 전 세계적으로 분산된 서버가 있고 이를 통해 어떠한 시스템의 콘텐츠 전달을 빠르게 할 수 있는 CDN 서비스이다.
- CORS와 프런트엔드의 프록시 서버
- Cross-Origin-Resource-Sharing: 서버가 웹 브라우저에서 리소스를 로드할 때 다른 오리진을 통해 로드하지 못하게 하는 HTTP 헤더 기반 메커니즘
- 예시
- 프런트엔드에서는 127.0.0.1:3000으로 테스팅을 하는데 백엔드 서버는 127.0.0.1:12010이라면 포트 번호가 다르기 때문에 CORS 에러가 나타난다. 이때 프록시 서버를 둬서 프런트엔드 서버에서 요청되는 오리진을 127.0.0.1:12010으로 바꾼다.
[프록시 서버에서의 캐싱]
- 캐시 안에 정보를 담아두고, 캐시 안에 있는 정보를 요구하는 요청에 대해 다시 멀리 있는 원격 서버에 요청하지 않고 캐시 안에 있는 데이터를 활용하는 것을 말한다.
- 불필요하게 외부와 연결하지 않기 때문에 트래픽을 줄일 수 있다는 장점이 있다.
이터레이터 패턴
- 이터레이터를 사용하여 컬렉션의 요소들에 접근하는 디자인 패턴이다.
- 순회할 수 있는 여러 가지 자료형의 구조와는 상관없이 이터레이터라는 하나의 인터페이스로 순회가 가능하다.
- 자바스크립트 치중..
MVC 패턴
- 애플리케이션의 구성 요소를 세 가지 역할로 구분하여 개발 프로세스에서 각각의 구성 요소에만 집중해서 개발할 수 있다.
- 장점 재사용성과 확장성이 용이하다.
- 단점 애플리케이션이 복잡해질수록 모델과 뷰의 관계가 복잡해진다.
모델
- 애플리케이션의 데이터인 데이터베이스, 상수, 변수 등을 뜻한다.
- 뷰에서 데이터를 생성하거나 수정하면 컨트롤러를 통해 모델을 생성하거나 갱신한다.
뷰
- 사용자 인터페이스 요소를 나타낸다.
- 모델을 기반으로 사용자가 볼 수 있는 화면
- 모델이 가지고 있는 정보를 따로 저장하지 않아야 한다.
- 변경이 일어나면 컨트롤러에 이를 전달해야 한다.
컨트롤러
- 하나 이상의 모델과 하나 이상의 뷰를 잇는 다리 역할을 한다.
- 메인 로직을 담당한다.
- 모델과 뷰의 생명주기를 관리한다.
- 모델이나 뷰의 변경 통지를 받으면 이를 해석하여 각각의 구성 요소에 해당 내용에 대해 알려준다.
MVP 패턴
- MVC 패턴의 컨트롤러가 프레젠터로 교체된 패턴이다.
- 뷰와 프레젠터는 일대일 관계이기 때문에 MVC 패턴보다 더 강한 결합을 가진다.
MVVM 패턴
- MVC 패턴의 컨트롤러가 뷰모델(view model)로 바뀐 패턴이다.
- 뷰모델은 뷰를 더 추상화한 계층이다.
- MVC 패턴과 다르게 커맨드와 데이터 바인딩을 가지는 것이 특징이다.
- 뷰와 뷰모델 사이의 양방향 데이터 바인딩을 지원하며 UI를 별도의 수정 없이 재사용할 수 있고 단위 테스팅하기 쉽다는 장점이 있다.
- 커맨드: 여러 가지 요소에 대한 처리를 하나의 액션으로 처리할 수 있게 하는 기법이다.
- 데이터 바인딩: 화면에 보이는 데이터와 웹 브라우저의 메모리 데이터를 일치시키는 기법으로, 뷰모델을 변경하면 뷰가 변경된다.
프로그래밍 패러다임
- 프로그래머에게 프로그래밍의 관점을 갖게 해주는 역할을 하는 개발 방법론
- jdk 1.8 이전의 자바는 객체지향 프로그래밍을 지원했다. 하지만, 1.8 이후로 함수형 프로그래밍 패러다임을 지원하기 위해서 람다식, 생성자 레퍼런스, 메서드 레퍼런스를 도입했고 선언형 프로그래밍을 위해 스트림(stream) 같은 표준 API 등도 추가했다.
- 프로그래밍 패러다임은 크게 선언형, 명령형으로 나눈다.
- 선언형은 함수형이라는 하위 집합을 갖는다.
- 명령형은 객체지향, 절차지향으로 나눈다.
선언형과 함수형 프로그래밍
- ‘무엇을’ 풀어내는가에 집중하는 패러다임이다.
- 선언형 패러다임의 일종이다.
함수형 프로그래밍
- 작은 ‘순수 함수’들을 블록처럼 쌓아 로직을 구현하고 ‘고차 함수’를 통해 재사용성을 높인 프로그래밍 패러다임 → 자바스크립트에 적합
순수함수
- 출력이 입력에만 의존하는 것
고차 함수
- 함수가 함수를 값처럼 매개변수로 받아 로직을 생성할 수 있는 것
일급 객체
- 고차 함수를 쓰기 위해서는 해당 언어가 일급 객체라는 특징을 가져야 한다.
- 변수나 메서드에 함수를 할당할 수 있다.
- 함수 안에 함수를 매개변수로 담을 수 있다.
- 함수가 함수를 반환할 수 있다.
객체지향 프로그래밍
OOP(Object-Oriented Programming)
- 객체들의 집합으로 프로그램의 상호 작용을 표현하며, 데이터를 객체로 취급하여 객체 내부에 선언된 메서드를 활용하는 방식
- 설계에 많은 시간이 소요되며 처리 속도가 다른 프로그래밍 패러다임에 비해 상대적으로 느리다.
객체지향 프로그래밍의 특징
- 추상화
- 복잡한 시스템으로부터 핵심적인 개념 또는 기능을 간추려내는 것
- 캡슐화
- 객체의 속성과 메서드를 하나로 묶고 일부를 외부에 감추어 은닉하는 것
- 상속성
- 상위 클래스의 특성을 하위 클래스가 이어받아서 재사용하거나 추가, 확장하는 것
- 코드의 재사용 측면, 계층적인 관계 생성, 유지보수성 측면에서 중요
- 다형성
- 하나의 메서드나 클래스가 다양한 방법으로 동작하는 것
- 오버로딩
- 같은 이름을 가진 메서드를 여러 개 두는 것
- 메서드의 타입, 매개변수의 유형, 개수 등으로 여러 개를 둘 수 있으며 컴파일 중에 발생하는 ‘정적’ 다형성
- 오버라이딩
- 상위 클래스로부터 상속받은 메서드를 하위 클래스가 재정의하는 것
- 런타임 중에 발생하는 ‘동적’ 다형성
설계 원칙
객체지향 프로그래밍을 설계할 때 지켜야 할 SOLID 원칙
단일 책임 원칙 (SRP; Single Responsibility Principle)
- 모든 클래스는 각각 하나의 책임만 가져야 한다.
개방-폐쇄 원칙 (OCP; Open Closed Principle)
- 유지 보수 사항이 생긴다면 코드를 쉽게 확장할 수 있도록 하고 수정할 때는 닫혀 있어야 한다.
- 즉, 기존 코드는 잘 변경하지 않으면서도 확장은 쉽게 할 수 있어야 한다는 것.
리스코프 치환 원칙 (LSP; Liskov Substitution Principle)
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
- → 부모 객체에 자식 객체를 넣어도 시스템이 문제없이 돌아가게 만드는 것
인터페이스 분리 원칙 (ISP; Interface Segregation Principle)
- 하나의 일반적인 인터페이스보다 구체적인 여러 개의 인터페이스를 만들어야 한다.
의존 역전 원칙 (DIP; Dependency Inversion Principle)
- 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 원칙
- 상위 계층은 하위 계층의 변화에 대한 구현으로부터 독립해야 한다.
절차형 프로그래밍
- 로직이 수행되어야 할 연속적인 계산 과정으로 이루어져 있다.
- 일이 진행되는 방식으로 그저 코드를 구현하기만 하면 되기 때문에 코드의 가독성이 좋으며 실행 속도가 빠르다. → 계산이 많은 작업 등에 쓰인다.
- 하지만, 모듈화하기가 어렵고 유지 보수성이 떨어진다.
패러다임의 혼합
- 어떤 패러다임이 가장 좋은가에 대한 답은 없다.
- 비즈니스 로직이나 서비스의 특징을 고려해서 패러다임을 정하는 것이 좋다.
- ex) 한 프로젝트 내에서도 머신 러닝 파이프라인은 절차지향형 & 거래 관련 로직은 함수형 프로그래밍 적용