디자인 패턴은 프로그램 설계와 구현에서 발생하는 문제들을 해결하기 위한 일련의 해결책 또는 규약이다.
이러한 디자인 패턴은 라이브러리나 프레임워크를 만들 때의 기초적인 원리가 된다.
→ 라이브러리: 특정 기능을 모듈화하여 제공하며 개발자에게 파일 구조나 규칙에 대해 크게 제한을 두지 않아 개발자에게 자유도가 높다. (예: Axios)
→ 프레임워크: 특정 기능을 모듈화하여 제공 및 기본적인 구조와 규칙이 정해진 뼈대가 있으며, 이로써 일관성을 유지하며 개발자에게 가이드라인을 제공한다. (예: Vue, Spring)
장점
- 코드 재사용성: 디자인 패턴을 사용하면 유사한 문제에 대한 해결책을 반복해서 작성하지 않고 기존의 패턴을 재사용할 수 있다.
- 가독성 향상: 디자인 패턴은 코드를 더 추상화하고 의도를 명확하게 표현할 수 있도록 도와준다.
- 유지 보수성 향상: 디자인 패턴을 따르면 코드의 일관성을 유지하고 변경 사항을 더 쉽게 적용할 수 있다.
종류
- 생성패턴: 객체 생성 방법이 들어간 디자인패턴
→ 싱글톤, 팩토리- 구조패턴: 객체 또는 클래스 등으로 큰 구조를 만들 때, 유연하고 효율적으로 만드는 방법
→ 프록시, 어댑터- 행동 패턴: 객체나 클래스 간의 알고리즘, 책임 할당(프로세스)에 관한 디자인패턴
→ 이터레이터, 옵저버, 전략
하나의 클래스에 오직 하나의 인스턴스만 존재하도록 하는 디자인 패턴으로, 인스턴스 생성을 효율적으로 할 수 있다.
→ 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
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));
}
}
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
*/
→ 멀티스레딩 환경에서의 스레드 동기화와 경쟁 조건 때문에 예상했던 결과값과 다르게 나온다.
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);
}
}
}
//결과값 정상적
public class Singleton {
private final static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
→ 클래스 로딩 시점에서 인스턴스가 미리 생성되어 있기 때문에 싱글톤 인스턴스가 필요하지 않은 경우에도 항상 생성되어 있다.
이로 인해 불필요한 자원 낭비가 발생할 수 있다.
public class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
class Singleton {
private static class SingleInstanceHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingleInstanceHolder.INSTANCE;
}
}
- JVM은 클래스 로딩과 초기화를 스레드 안전하게 관리한다.
여러 스레드가 동시에 클래스를 로드하려고 시도해도 실제로 한 번만 클래스가 초기화되고, 그 이후의 시도에서는 클래스 초기화는 무시된다.
이것은 싱글톤 패턴에서 내부 클래스를 활용하는 경우에 동작하는 원리이다.
→ 최초 getInstance 메서드가 호출될 때 내부 클래스가 초기화 되면서, 싱글톤 인스턴스가 생성된다.
또한, JVM은 해당 초기화 과정을 스레드 안전하게 처리하므로 멀티스레드 환경에서도 동작이 안전하게 보장될 수 있다.
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로 수정함으로써 해결할 수 있다.
public enum SingletonEnum {
INSTANCE;
public void oortCloud() {
// 싱글톤 인스턴스의 동작을 수행하는 코드
}
}