싱글톤 패턴?

  • 인스턴스가 프로그램 내에서 오직 하나만 생성되는 것을 보장하고, 프로그램 어디서든 이 인스턴스에 접근할 수 있도록 하는 패턴.

간단히 말하면, 인스턴스가 사용될 때, 똑같은 인스턴스를 여러게 만드는 것이 아니라, 기존에 생성했던 동일한 인스턴스를 사용하도록 하는 패턴이다.


1. 의존성 주입(Dependency Injection)?

1) Dependency(의존성/의존관계)란 무엇인가?

ㅁ "A가 B에 의존한다."라는 표현에 대해 생각해보자.

이 말은 즉, "B에 변화가 생긴다면, A에 영향을 미친다" 라는 뜻이지 않을까?
예를 들어, "초밥의 레시피에 따라서 그 초밥이 정해진다" 라고 생각해 보자.

class Sushi {
	private SushiRecipe sushiRecipe;
    public Sushi() {
    	sushiRecipe = new SushiRecipe();
    }
}

이처럼 레시피(SushiRecipe)에 초밥(Sushi)이 의존한다.


ㅁ 그렇다면 위의 코드를 인터페이스로 추상화해서 더 다양한 레시피로 초밥을 만들어 보도록 하자.

class Sushi {
	private SushiRecipe sushiRecipe;
    public Sushi() {
    	sushiRecipe = new SushiRecipe(); //기존의 스시
        //sushiRecipe = new EggRecipe(); // Egg스시
        //sushiRecipe = new MeatRecipe(); //Meat스시
    }
}
interface moreSuchiRecipe{
	newSushi();
    // + 다양한 메서드 추가
}
class SushiRecipe implements moreSushiRecipe {
	public Sushi newSushi() {
    	return new Sushi();
    }
    // + 다양한 레시피를 따른 다양한 초밥
}

이처럼 의존관계를 인터페이스로 추상화하면, 더 다양한 의존 관계를 맺을 수 있으며, 실제 구현 클래스와의 관계가 느슨해지고, 결합도가 낮아진다.


ㅁ 그렇다면 의존성 주입은 무엇일까?

위의 코드를 보면, Sushi 내부적으로 의존관계인 SushiRecipe가 어떤 값을 가질지 직접 정하고있다. 하지만 SushiRecipe를 만든는 Owner가 정하는 상황을 생각해 보자. 다시 말하면, Sushi가 의존하고 있는 SushiRecipe를 Owner가 결정하고 주입하는 것이다.

이처럼 그 의존관계를 외부에서 결정하고 주입하는 것을 의존성 주입이라고 한다.


ㅁ 의존성 주입 구현 방법

의존성 주입은 의존관계를 외부에서 결정하는 것이기 때문에, 클래스 변수를 결정하는 방법들이 곧 이를 구현하는 방법이다. 런타임 시점의 의존관계를 외부에서 주입하여 구현이 완성된다.

  • 생성자를 이용
class Sushi{
	private SushiRecipe sushiRecipe;
    //Sushi생성자 
    public Sushi(SushiRecipe sushiRecipe) {
    	this.sushiRecipe = sushiRecipe;
    }
}
class Owner {
	private Sushi sushi;
    sushi = new Sushi(new SushiRecipe());
    //EggSushi를 만드는 메서드
    public void EggSushi() {
    	//생성자를 통해 EggRecipe를 가져와 EggSushi를 만든다
    	sushi = new Sushi(new EggRecipe());
    }
    //MeatSushi를 만드는 메서드
    public void MeatSushi() {
    	//생성자를 통해 MeatRecipe를 가져와 MeatSushi를 만든다
    	sushi = new Sushi(new MeatRecipe());
    }
    // + 더 다양한 Recipe의 Sushi를 만드는 메서드 추가 가능
}
  • 메서드를 이용(대표적으로 Setter메서드)
class Sushi{
	private SushiRecipe sushiRecipe;
    sushiRecipe = new SushiRecipe();
    //SushiRecipe를 지정하는 메서드 생성
    public void setSushiRecipe(SushiRecipe sushiRecipe) {
    	this.sushiRecipe = sushiRecipe;
    }
}
class Owner {
	private SushiRecipe sushi;
    sushi = new Sushi();
    //EggSushi를 만드는 메서드
    public void eggSushi() {
    	//Sushi클래스의 setSushiRecipe메서드 호출 - EggSushiRecipe객체 호출
        sushi.setSushiRecipe(new EggSushiRecipe());
    }
    // + 더 다양한 Recipe의 Sushi를 만드는 메서드를 setSushiRecipe메서드 호출로 추가 가능
}

ㅁ 의존성 주입의 장점

1) 의존성이 줄어든다. => 앞서 말했듯 의존한다는 것은 의존 대상의 변화에 취약하다는 것이다. 하지만 이처럼 구현한다면, 주입받는 대상이 변하더라도 그 구현 자체를 수정할 일이 없거나 줄어들게 된다.

2) 재사용성이 높은 코드가 된다. => 위 코드에서 Sushi내부에서만 사용되었던 SushiRecipe를 별도로 구분하여 구현하면, 다른 클래스에서 재사용이 가능하다.

3) 테스트를 하기 편하다. => SushiRecipe의 테스트를 Sushi와 분리해 테스트가 가능하다.

4) 가독성이 높아진다. => SushiRecipe의 기능들을 별도로 분리하게 되어 자연스레 가독성이 높아지게 된다.


2. 멀티 프로세스와 멀티 쓰레드

멀티 프로세스(Multi Process)

정의

  • 프로세스란 운영체제로부터 자원을 달당받는 작업의 단위를 말한다.
  • 두개 이상 다수의 프로세서(CPU)가 협력적으로 하나 이상의 작업을 동시에 처리하는 것이다.(병렬처리)
  • 각 프로세스 간 메모리 구분이 필요하거나, 독립된 주소 공간을 가져야 할 경우 사용한다.

장점

  • 독립된 구조로 안정성이 높다.
  • 프로세스 중 하나에 문제가 생겨도 다른 프로세스에 영향을 주지 않아, 작업속도가 느려지는 손해는 생기지만, 정지되는 문제는 발생하지 않는다.
  • 여러 개의 프로세스가 처리되어야 할 때, 동일한 데이터를 사용하고, 이러한 데이터를 하나의 디스크에 두고 모든 프로세서(CPU)가 이를 공유하면 비용적으로 저렴하다.

단점

  • 독립된 메모리 영역이기 때문에, 작업량이 많을수록 오버헤드(어떤 처리를 하기 위해 들어가는 간접적 처리시간/메모리)가 발생하여, 성능저하 위험이 있다.
  • 컨텍스트 스위칭(Context Switching) 과정에서 캐시 메모리 초기화 등 부거운 작업이 진행되고 시간이 소모되는 등 오버헤드가 발생한다.

Context Switching : 현재 진행되고 있는 Task(Process, Thread)의 상태를 저장하고 다음 진행할 Task의 상태 값을 읽어 적용하는 과정을 말한다.

멀티 쓰레드(Multi Thread)

정의

장점

  • 시스템 자원 소모가 감소한다. => 프로세스를 생성하여 자원을 할당하는 시스템 콜이 줄어 자원을 효율적으로 관리할 수 있다.
  • 시스텔 처리율이 향성한다. => 스레드 간 데이터를 주고 받는 것이 간단해지고, 시스템 자원 소모가 줄어들며, 스레드 사이 작업량이 작아 컨텍스트 스위칭이 빠르다.(캐시 메모리를 비울 필요가 없다.)
  • 간단한 통신 방법으로 프로그램 응답시간이 단축된다. => 프로세스 내 스택영역을 제외한 메모리 영역을 공유하기 때문에, 통신비용이 적으며, 힙 영역을 공유하므로 데이터를 주고 받을 수 있다.

힙 영역 : 사용자가 직접 관리할 수 있으며, 해야만 하는 역역이다. 이는 자용자에 의해 메모리 공간이 동적으로 할당되고 해제된다. 또한 메모리의 낮은 주소에서 높은 주소 방향으로 할당된다.

쓰레드를 사용하면 자원의 효율성이 증가하기도 하지만, 쓰레드 간의 자원 공유는 전역 변수를 사용하므로, 동기화 문제가 발생할 수 있다 => 프로그래머의 주의가 필수적이다.


ㅁ 멀티 프로세스 VS 멀티 쓰레드

  • 멀티 쓰레드는 멀티 프로세스보다 적은 메모리 공간을 차지하고, 컨텍스트 스위칭이 빠른 장점이 있지만, 동기화 문제와 하나의 쓰레드 장애로 전체 쓰레드가 종료될 위험이 있다.
  • 멀티 프로세스하나의 프로세스가 죽더라도 다른 프로세스에 영향을 주지 않아 안정성이 높지만, 멀티 쓰레드보다 많은 메모리 공간과, CPU 시간을 차지하는 단점이 있다.

결론은, 두 방법은 동시에 여러 작업을 수행한다는 공통점이 있지만, 각각의 장/단점이 있으므로 적용한느 시스템에 따라 적합한 동작 방식을 선택하고 적용해야 한다.


3. 싱글턴 패턴 구현 방법

싱글턴 패턴은 개념도 간단하며, 구현도 간단하다.

class SingleTon{
    private static SingleTon singleTon = null;
    //외부에서 직접 생성하지 못하도록 private로 생성자 선언
    private SingleTon(){}
    //오직 1개의 객체만 생성
    public static SingleTon getInstance() {
        if (singleTon == null) {
            singleTon = new SingleTon();
        }
        return singleTon;
    }
}
  //singleTon 변수에 객체가 할당되지 않으면(즉, null값이라면) 새로운 객체를 생성한다.
  //singleTon 변수에 객체가 이미 있으면, 그대로 반환한다.

이렇게 정의한다면, SingleTon객체를 외부에서 생성할 수 없다. 그러므로 미리 생성된 객체를 반환할 수 있도록 getInstance()메서드를 정의한다.

또한, 생성자를 private로 선언했기에, 객체를 생성할 수 없으므로, getInstance()메서드가 클래스에 정의되도록 static제어자를 사용한다.


4. 싱글턴 패턴 확인

  //메인 클래스 구현
  public class Main {
  	public static void main(String args[]) {
  		//객체가 정말 1개만 생성되어 있는지 확인하기 위한 반복문
  		for (int i = 0; i < 10; i++) {
        	SingleTon singleTon = SingleTon.getInstance();
            System.out.println(i + "번째 객체 : " + singleTon.toString);
        }
  	}
  }
//결과값
0번째 객체 : SingleTon@1b6d3586
1번째 객체 : SingleTon@1b6d3586
2번째 객체 : SingleTon@1b6d3586
3번째 객체 : SingleTon@1b6d3586
4번째 객체 : SingleTon@1b6d3586
5번째 객체 : SingleTon@1b6d3586
6번째 객체 : SingleTon@1b6d3586
7번째 객체 : SingleTon@1b6d3586
8번째 객체 : SingleTon@1b6d3586
9번째 객체 : SingleTon@1b6d3586

이처럼 확인해보면, 모두 같은 객체임을 확인할 수 있다.


5. 싱글턴 패턴의 장/단점

장점

  1. 고정된 메모리 영역을 얻으면서, 한 번의 new로 인스턴스를 사용한다 => 메모리 낭비를 방지할 수 있다.
  2. 싱글톤으로 만들어진 클래스의 인스턴스는 전역(static)이므로 다른 클래스의 인스턴스들이 데이터를 공유하기 편하다.
  3. 인스턴스가 절대적으로 하나만 존재하는 것을 보장할 수 있다.

단점

  1. 다른 클래스의 인스턴스들 간의 결합도가 높아진다. => OOP의 SOLID의 원칙중 O에 위배될 가능성이 있다.(개방-폐쇄의 원칙)
  2. 수정이 어려워지고, 유지보수 비용이 높아질 수 있다.
profile
개발을 꿈꾸는 초짜

0개의 댓글