디자인 패턴(1)

이태곤·2023년 10월 18일
0

CS

목록 보기
6/23

1. 디자인 패턴

  • 디자인 패턴은 프로그램 설계와 구현에서 발생하는 문제들을 해결하기 위한 일련의 해결책 또는 규약이다.
    이러한 디자인 패턴은 라이브러리나 프레임워크를 만들 때의 기초적인 원리가 된다.
    → 라이브러리: 특정 기능을 모듈화하여 제공하며 개발자에게 파일 구조나 규칙에 대해 크게 제한을 두지 않아 개발자에게 자유도가 높다. (예: Axios)
    → 프레임워크: 특정 기능을 모듈화하여 제공 및 기본적인 구조와 규칙이 정해진 뼈대가 있으며, 이로써 일관성을 유지하며 개발자에게 가이드라인을 제공한다. (예: Vue, Spring)

  • 장점

    1. 코드 재사용성: 디자인 패턴을 사용하면 유사한 문제에 대한 해결책을 반복해서 작성하지 않고 기존의 패턴을 재사용할 수 있다.
    2. 가독성 향상: 디자인 패턴은 코드를 더 추상화하고 의도를 명확하게 표현할 수 있도록 도와준다.
    3. 유지 보수성 향상: 디자인 패턴을 따르면 코드의 일관성을 유지하고 변경 사항을 더 쉽게 적용할 수 있다.
  • 종류

    1. 생성패턴: 객체 생성 방법이 들어간 디자인패턴
      → 싱글톤, 팩토리
    2. 구조패턴: 객체 또는 클래스 등으로 큰 구조를 만들 때, 유연하고 효율적으로 만드는 방법
      → 프록시, 어댑터
    3. 행동 패턴: 객체나 클래스 간의 알고리즘, 책임 할당(프로세스)에 관한 디자인패턴
      → 이터레이터, 옵저버, 전략

2. 싱글톤 패턴

  • 하나의 클래스에 오직 하나의 인스턴스만 존재하도록 하는 디자인 패턴으로, 인스턴스 생성을 효율적으로 할 수 있다.
    → I/O 바운드 작업(네트워크 연결, DB 연결 등)과 같이 리소스 소모가 큰 작업에서 인스턴스를 효율적으로 관리하여 비용을 절감할 수 있다.

  • 싱글톤 패턴을 적용하지 않은 코드

class Rectangle {
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
}

const a = new Rectangle(1, 2);
const b = new Rectangle(1, 2);

console.log(a === b); // false
  • 싱글톤 패턴을 적용한 코드
class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    this.data = "This is a singleton instance";
    Singleton.instance = this;
  }
}

const singletonA = new Singleton();
const singletonB = new Singleton();

console.log(singletonA === singletonB); // true
console.log(singletonA.data); // This is a singleton instance
console.log(singletonB.data); // This is a singleton instance
  • 싱글톤 패턴의 단점: 의존성이 높아지고 TDD를 할 때 불편하다.
    → 단위 테스트는 서로 독립적이며 순서에 관계없이 실행될 수 있어야 한다.
    하지만, 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 공유하며 구현하는 패턴이므로 독립성을 보장받기 힘들다.
public class ArrayTest {
    private int[] a = {1, 2, 3};

    @Test	//성공
    public void testIndexOf() {
        assertEquals(-1, indexOf(4));
        a[0] = 4;
    }

    @Test	//실패
    public void testIndexOfAfterChange() {
        assertEquals(-1, indexOf(4));
    }
}

3. 싱글톤 패턴 구현 방법

  1. Lazy Initialization: 인스턴스가 필요한 경우에만 생성하고, 이미 생성된 인스턴스가 있다면 해당 인스턴스를 반환하는 지연 생성 방법이다.
public class Singleton {
    private static Singleton instance;  
    private Singleton() { }  

    public static Singleton getInstance() {
        if (instance == null) {  // 인스턴스가 아직 생성되지 않았다면
            instance = new Singleton();  // 인스턴스를 생성
        }
        return instance;  // 생성된 인스턴스 반환
    }
}

→ 원자성을 보장하지 않기 때문에 멀티스레드 환경에서 인스턴스가 2개 이상 생성될 수 있다.

public class SyncTest {
    private static String song = "TestSong1";

    public static void main(String[] args) {
        SyncTest a = new SyncTest();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                a.say("TestSong2");
            }
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                a.say("TestSong3");
            }
        }).start();
    }

    public void say(String newSong) {
        song = newSong;
        try {
            long sleep = (long) (Math.random() * 100);
            Thread.sleep(sleep);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (!song.equals(newSong)) {
            System.out.println(newSong + " | " + song);
        }
    }
}
/*
TestSong2 | TestSong1
TestSong3 | TestSong1
TestSong2 | TestSong1
TestSong2 | TestSong3
TestSong3 | TestSong2
TestSong3 | TestSong2
TestSong2 | TestSong3
*/

→ 멀티스레딩 환경에서의 스레드 동기화와 경쟁 조건 때문에 예상했던 결과값과 다르게 나온다.

  1. Lazy Initialization(Synchronized): 인스턴스를 생성하여 반환하기 전까지 격리 공간에 놓기 위해 synchronized 키워드를 통해 lock을 걸어준다.
    → 최초로 접근한 스레드가 해당 메서드 호출시에 다른 스레드가 접근하지 못하게 된다.
public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

→ getInstance() 메서드를 호출할 때마다 lock이 걸려있다면 성능저하가 발생할 수 있다.
→ 인스턴스가 생성됬음에도 불구하고 getInstacne()가 호출될 수 있으므로 성능저하가 될 수 있다.

public class SyncTest {
    private static String song = "TestSong1";

    public static void main(String[] args) {
        SyncTest a = new SyncTest();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                a.say("TestSong2");
            }
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                a.say("TestSong3");
            }
        }).start();
    }

    public synchronized void say(String newSong) {
        song = newSong;
        try {
            long sleep = (long) (Math.random() * 100);
            Thread.sleep(sleep);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (!song.equals(newSong)) {
            System.out.println(newSong + " | " + song);
        }
    }
}
//결과값 정상적
  1. 정적 멤버: 런타임이 아닌, JVM이 모든 클래스들을 로드할 때, 싱글톤 인스턴스를 미리 생성하는 방법이다.
    → 클래스 로딩과 동시에 싱글톤 인스턴스 생성한다.
    따라서, 각각의 모듈들이 싱글톤 인스턴스를 요청할 때 만들어진 인스턴스를 반환하면 된다.
public class Singleton {
    private final static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}

→ 클래스 로딩 시점에서 인스턴스가 미리 생성되어 있기 때문에 싱글톤 인스턴스가 필요하지 않은 경우에도 항상 생성되어 있다.
이로 인해 불필요한 자원 낭비가 발생할 수 있다.

  1. 정적 블록: 정적 블록은 클래스가 로딩될 때 실행되며, 인스턴스를 생성하는 부분을 정적 블록 내에서 수행한다.
    → 정적 멤버 방법과 동일
public class Singleton {
    private static Singleton instance = null;

    static {
        instance = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}
  1. Lazy Holder(중첩클래스): singleInstanceHolder라는 내부클래스를 하나 더 만들어서 Singleton클래스가 최초 로딩될 때 초기화가 하지 않고 getInstance()가 호출될 때 singleInstanceHolder 클래스를 통해 로딩되어 인스턴스를 생성하게 된다.
class Singleton {
    private static class SingleInstanceHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingleInstanceHolder.INSTANCE;
    }
}
  • JVM은 클래스 로딩과 초기화를 스레드 안전하게 관리한다.
    여러 스레드가 동시에 클래스를 로드하려고 시도해도 실제로 한 번만 클래스가 초기화되고, 그 이후의 시도에서는 클래스 초기화는 무시된다.
    이것은 싱글톤 패턴에서 내부 클래스를 활용하는 경우에 동작하는 원리이다.
    → 최초 getInstance 메서드가 호출될 때 내부 클래스가 초기화 되면서, 싱글톤 인스턴스가 생성된다.
    또한, JVM은 해당 초기화 과정을 스레드 안전하게 처리하므로 멀티스레드 환경에서도 동작이 안전하게 보장될 수 있다.
  1. DCL(Double Checked Locking, 이중 확인 잠금): 인스턴스가 이미 생성되었는지 확인한 후, 필요한 경우에만 잠금을 걸어 인스턴스를 생성하는 방법이다.
    잠금을 걸고 실제로 객체를 생성하기 전에도 한번 총 2번 체크함으로써 인스터가 존재하지 않을 경우에만 새롭게 생성할 수 있다.
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: 자바에서는 스레드 2개 이상 생성되면 변수를 메인 메모리(RAM)으로부터 가져오는 것이 아니라 캐시메모리에서 각각의 캐시메모리를 기반으로 가져오게 된다.
    volatile 키워드를 사용하게 되면 Main Memory를 기반으로 저장하고 읽어오기 때문에 이 문제를 해결할 수 있다.
public class Test {
    boolean flag = true;
    
    public void test() {
        new Thread(() -> {
            int cnt = 0;
            while (flag) {
                cnt++;
            }
            System.out.println("Thread1 finished\n");
        }).start();
        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ignored) {
            }
            System.out.println("flag to false");
            flag = false;
        }).start();
    }

    public static void main(String[] args) {
        new Test().test();
    }
}

→ Infinite loop 발생
→ boolean flag = true를 volatile boolean flag = true로 수정함으로써 해결할 수 있다.

  1. enum: enum의 인스턴스는 기본적으로 스레드세이프(thread safe)가 보장된다.
public enum SingletonEnum {
    INSTANCE;

    public void oortCloud() {
        // 싱글톤 인스턴스의 동작을 수행하는 코드
    }
}

0개의 댓글