디자인패턴?
프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용하여 해결할 수 있도록 하나의 ‘규약’ 형태로 만들어 놓은 것을 의미한다.
디자인패턴은 라이브러리나 프레임워크를 만드는데 기초적인 원리가 되며 지금도 많은 라이브러리, 프레임워크가 어떠한 디자인패턴을 기반으로 만들어지고 있다.
라이브러리와 프레임워크의 차이?
- 라이브러리 : 공통으로 사용될 수 있는 특정한 기능들을 모듈화한 것이며 폴더명, 파일명 등에 대한 규칙이 없고 프레임워크에 비해 자유롭다.
- 프레임워크 : 공통으로 사용될 수 있는 특정한 기능들을 모듈화한 것이며 폴더명, 파일명 등에 대한 규칙이 있고 라이브러리에 비해 엄격하다.
라이브러리는 자동차, 프레임워크는 비행기에 비유 가능.
자동차는 내가 몰면서 커스텀할 수 있는 것이 많다. 경로 등등
하지만 비행기는 경로같은 것을 내가 바꿀 수 없다. 단 제공되는 부분도 많다.
Vue.js 프레임워크에 axios 라이브러리를 넣는 것은 쉬우나
React.js 라이브러리에 Vue.js 프레임워크를 넣는 것은 어렵다.
결론적으로 똑같이 공통으로 사용될 수 있는 기능들을 모듈화 해놓은 것이지만, 라이브러리가 비교적 자유롭다. 이유는 폴더명, 파일명 등에 대한 규칙이 프레임워크에는 존재하기 때문이다.
디자인 패턴의 의의
디자인 패턴들을 미리 알아둔다면 이것을 기반으로 여러 문제들을 해결하는데 있어 영감을 받을 수도 있고 팀원들과 협업할 때 어떤 문제가 나타나면 “그 부분은 ~~패턴” 으로 해보는게 어떤가요? 라고 했을 때 빠른 의사소통이 가능하다.
디자인 패턴의 종류
생성패턴, 구조패턴, 행동패턴 3가지로 나누어진다
- 생성패턴 : 객체 생성 방법이 들어간 디자인 패턴 ex) 싱글톤, 팩토리, 빌더 등
- 구조패턴 : 객체, 클래스 등으로 큰 구조를 만들 때 유연하고 효율적으로 만드는 방법이 들어간 패턴 ex) 프록시, 어댑터, 데코레이터 등
- 행동패턴 : 객체나 클래스간의 알고리즘, 책임 할당에 관한 디자인패턴 ex) 이터레이터, 옵저버, 전략 등
싱글톤 패턴
하나의 클래스에 하나의 인스턴스만 가지는 패턴이다.
하나의 클래스 기반으로 여러 개의 개별적인 인스턴스를 만들 수 있지만 그렇게 하지 않고 하나의 클래스를 기반으로 단 하나의 인스턴스를 만들어 이를 기반으로 로직을 만드는데 쓰이며 보통 DB 연결 모듈에 많이 사용된다.
장점
하나의 인스턴스를 기반으로 해당 인스턴스를 다른 모듈들이 공유하여 사용하기 때문에 인스턴스를 생성할 때 드는 비용이 줄어든다. 그렇게 때문에 ‘인스턴스 생성에 많은 비용’이 드는 I/O 바운드 작업에 많이 쓰인다.
I/O 바운드?
디스크 연결, 네트워크 통신, 데이터베이스 연결
ex) 서버와 DB는 다른 인스턴스이기 때문에 서버에서 DB에 어떤 것을 요청하려면 계속 DB를 호출해 연결해야 하는데 이것을 싱글톤으로 해놓으면 생성 비용이 줄어든다.
단점
의존성이 높아지며 TDD를 할 때 걸림돌이 된다.
TDD를 할 때 단위 테스트를 주로 하는데, 단위 테스트는 테스트가 서로 독립적이어야 한다. 테스트를 어떤 순서로든 실행할 수 있어야 한다.
하지만 싱글톤 패턴은 테스트마다 ‘독립적인’ 인스턴스를 만들기가 어렵다.
싱글톤 패턴을 구현하는 7가지 방법
- 단순한 메서드 호출
1 2 3 4 5 6 7 8 9 10 11 12 13 14
public class Singlton{ private static Singleton instance; private Singleton(){ } public static Singleton getInstance(){ //메서드를 호출했을 때 인스턴스가 생성되는 코드 if(instance == null){ instance = new Singleton(); } return instance; } }
문제점
자바는 멀티쓰레드 언어이다.
쓰레드1이 있고 쓰레드2가 있다 치자. 쓰레드 1에서 getInstance() 호출, 쓰레드 2에서도 호출했다 가정.
순서대로 함수가 호출될 수 있지만 최악의 경우 쓰레드 1에서 if문까지만 돌고 바로 쓰레드2의 if문으로 간다면 if문 둘 다 instance == null 이기 때문에 인스턴스가 두 개 생겨버릴 수 있다.
- Synchronized
인스턴스를 반환하기 전까지 격리 공간에 놓기 위해 synchronized 키워드로 잠금을 할 수 있다. 최초로 접근한 스레드가 해당 메서드 호출시에 다른 스레드가 접근하지 못하도록 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singlton{
private static Singleton instance;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
문제점
여러 모듈들이 각각 getInstance라는 메서드를 통해 필요할 때 가져올텐데 그럼 매번 호출되는 것이고 synchronized에 의해 lock이 되는 상황이 오므로 성능저하가 올 수 있다.
- 정적멤버
정적(static) 멤버나 블록은 런타임이 아니라 최초에 JVM이 클래스 로딩 시 모든 클래스들을 로드할 때 미리 인스턴스를 생성하는데 이를 이용한 방법이다.
클래스 로딩과 동시에 싱글톤 인스턴스를 만든다. 그렇게 때문에 모듈들이 싱글톤 인스턴스를 요청할 때 그냥 만들어진 인스턴스를 반환하면 된다.
1
2
3
4
5
6
7
8
9
10
public class Singlton{
private final static Singleton INSTANCE = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
문제점
불필요한 자원낭비가 일어날 수 있다. 인스턴스가 필요없는 경우에도 생성하게 되어 메모리가 낭비된다.
- 정적블럭 (위와 비슷)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singlton{
private static Singleton instance = null;
static{
instance = new Singleton();
}
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
- Lazy Holder(중첩 클래스)
singleInstanceHolder라는 내부 클래스를 하나 더 만듦으로써, Singleton 클래스가 최초에 로딩되더라도 함께 초기화가 되지 않고, getInstance()가 호출될 때 singleInstanceHolder 클래스가 로딩되어 인스턴스를 생성하게 된다.
1
2
3
4
5
6
7
8
class Singleton{
private static class singleInstanceHolder{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return singleInstanceHolder.INSTANCE;
}
}
- 이중 확인 잠금 (DCL)
Double Checked Locking의 약자로 인스턴스 생성 여부를 싱글톤 패턴 잠금 전에 한번, 객체를 생성하기 전에 한 번 이렇게 2번 체크하면 인스턴스가 존재하지 않을 때만 잠금을 걸 수 있기 때문에 앞서 생겼던 문제를 해결할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton{
private volatile Singleton instance;
private Singleton() {}
public Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
** volatile?
메모리 구조는 보통 메인 메모리 위에 CPU 캐시 메모리라고 불리는 L3, L2, L1 캐시가 있다.
자바에서는 스레드 2개가 열리면 변수를 메인 메모리(RAM)으로부터 가져오는 것이 아니라 캐시메모리에서 각각의 캐시 메모리를 기반으로 가져오게 된다.
즉 volatile을 붙여야 각각의 스레드에서 변수를 공유할 수 있는 것이다.
- enum
enum의 인스턴스는 기본적으로 스레드 세이프한 점이 보장되기 때문에 이를 통해 생성가능하다.
1
2
3
4
5
public enum SingletonEnum{
INSTANCE;
public void ~(){
}
}
5번과 7번이 가장 추천되는 방법이다.
5번은 가장 많이 쓰인다고 알려져있고, 7번은 이펙티브 자바를 쓴 조슈아 블로크가 추천한 방법이다.
팩토리 패턴
상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴이다.
상위 클래스에서는 객체 생성 방식에 대해 알 필요가 없어져 유연성을 갖게 되며 객체 생성 로직은 하위클래스에서만 관리되기 때문에 유지보수성이 증가된다.
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
32
33
34
35
36
37
38
39
40
41
42
43
44
enum CoffeeType{
LATTE,
ESPRESSO
}
abstract class Coffee{
protected String name;
public String getName(){
return name;
}
}
class Latte extends Coffee{
public Latte(){
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 CoffeeType: " + tpye);
}
}
}
public class Main{
public static void main(String[] args){
Coffee coffee = CoffeeFactory.createCoffee(CoffeeType.LATTE);
System.out.println(coffee.getName());
}
}
커피팩토리에서는 커피의 객체생성에만 관심이 있고
커피클래스에서는 라떼와 에스프레소의 틀만 만들어 준다.
세부사항은 라떼와 에스프레소에서 결정한다.
최종적으로 팩토리에서는 객체 생성에만 관심이 있고 하위 클래스에서 객체 생성에 관한 구체적인 내용을 작성한다.
수정이 필요할 때 커피팩토리 건들필요없이 라떼클래스와 에스프레소 클래스만 고치면 된다. -> 유지보수성 및 유연성이 증가한다.
이터레이터 패턴
이터레이터패턴은 이터레이터를 사용하여 컨테이너의 요소들에 접근하는 디자인 패턴이다. 각기 다른 자료구조들을 똑같은 인터페이스로 순회를 쉽게 할 수 있다는 장점이 있다.