디자인패턴(1) - 어댑터패턴, 싱글톤패턴

백엔드류·2024년 5월 3일

Design Pattern

목록 보기
1/2

디자인 패턴이란 일종의 설계 기법이며, 설계 방법이다.
목적 : SW 재사용성, 호환성, 유지 보수성을 보장
특징 : 아이디어이며, 특정한 구현이 아니다 / 프로젝트에 항상 적용해야 하는것은 아니지만, 추후 재사용, 호환, 유지 보수시 발생하는 문제 해결을 예방하기 위해 패턴을 만들어 둔 것.



원칙- SOLID(객체 지향 설계 원칙)

  1. SRP - 단일 책임 원칙 : 한 클래스는 하나의 책임만 가져야 함.
  2. OCP - 개방/폐쇄 원칙 : 확장에는 열려있고, 수정에는 닫혀있어야 함.
  3. LSP - 리스코프 치환 원칙 : 하위 타입은 항상 상위 타입을 대체 할 수 있어야 함.
  4. ISP - 인터페이스 분리 원칙 : 인터페이스 내에 메소드는 최소한 일수록 좋다.( 하나의 일반적인 인터페이스 보다 여러 개의 구체적인 인터페이스가 낫다.) + SRP와 같은 문제에 대한 두 가지 다른 해결책임.
  5. DIP - 의존관계 역전 원칙 : 구체적인 클래스보다 상위 클래스, 인터페이스, 추상클래스와 같이 변하지 않을 가능성이 높은 클래스와 관계를 맺어라. DIP 원칙을 따르는 가장 인기 있는 방법은 의존성 주입(DI)임.


분류(⭐)

  1. 생성 패턴 : 객체의 생성 방식 결정
    EX) DBConnection을 관리하는 Instance를 하나만 만들 수 있도록 제한하여, 불필요한 연결을 막음
  2. 구조 패턴 : 객체간의 관계를 조직
    EX) 2개의 인터페이스가 서로 호환이 되지 않을 때, 둘을 연결해주기 위해서 새로운 클래스를 만들어서 연결시킬 수 있도록 함.
  3. 행위 패턴 : 객체의 행위를 조직, 관리, 연합
    EX) 하위 클래스에서 구현해야 하는 함수 및 알고리즘들을 미리 선언하여, 상속시 이를 필수로 구현하도록 함.


어댑터 패턴

클래스를 바로 사용할 수 없는 경우가 있다.주로 다른 곳에서 개발했다거나, 수정할 수 없을때. 이에 중간에서 변환 역할을 해주는 클래스가 필요하다 -> 어댑터 패턴
사용방법 : 상속
호환되지 않은 인터페이스를 사용하는 클라이언트 그대로 활용 가능
향후 인터페이스가 바뀌더라도, 변경 내역은 어댑터에 캡슐화 되므로 클라이언트 바뀔 필요X



  • 어댑터는 필요로 하는 인터페이스로 바꿔주는 역할을 한다.
  • 예를 들어 업체에서 제공한 클래스가 기존 시스템에 맞지 않는다면? --> 기존 시스템을 수정할 것이 아니라, 어댑터를 활용해 유연하게 해결하자!


코드로 어댑터 패턴 이해하기

오리와 칠면조 인터페이스 생성
만약 오리 객체가 부족해서 칠면조 객체를 대신 사용해야 한다면?
두 객체는 인터페이스가 다르므로, 바로 칠면조 객체를 사용하는 것은 불가능함
따라서 칠면조 어댑터를 생성해서 활용해야 함

  • Duck.java
package AdapterPattern;

public interface Duck {
	public void quack();
	public void fly();
}
  • Turkey.java
package AdapterPattern;

public interface Turkey {
	public void gobble();
	public void fly();
}
  • MallardDuck.java
package AdapterPattern;

public class MallardDuck implements Duck {

    @Override
    public void quack() {
        System.out.println("Quack");
    }

    @Override
    public void fly() {
        System.out.println("I'm flying");
    }
}
  • WildTurkey.java
package AdapterPattern;

public class WildTurkey implements Turkey {

	@Override
	public void gobble() {
		System.out.println("Gobble gobble");
	}

	@Override
	public void fly() {
		System.out.println("I'm flying a short distance");
	}
}
  • TurkeyAdapter.java
package AdapterPattern;

public class TurkeyAdapter implements Duck {

	Turkey turkey;

	public TurkeyAdapter(Turkey turkey) {
		this.turkey = turkey;
	}

	@Override
	public void quack() {
		turkey.gobble();
	}

	@Override
	public void fly() {
		turkey.fly();
	}

}
  • DuckTest.java
package AdapterPattern;

public class DuckTest {

	public static void main(String[] args) {

		MallardDuck duck = new MallardDuck();
		WildTurkey turkey = new WildTurkey();
		Duck turkeyAdapter = new TurkeyAdapter(turkey);

		System.out.println("The turkey says...");
		turkey.gobble();
		turkey.fly();

		System.out.println("The Duck says...");
		testDuck(duck);

		System.out.println("The TurkeyAdapter says...");
		testDuck(turkeyAdapter);

	}

	public static void testDuck(Duck duck) {

		duck.quack();
		duck.fly();

	}
}
/* 출력결과
The turkey says...
Gobble gobble
I'm flying a short distance
The Duck says...
Quack
I'm flying
The TurkeyAdapter says...
Gobble gobble
I'm flying a short distance
*/


싱글톤 패턴

애플리케이션이 시작될 때, 어떤 클래스가 최초 한 번만 메모리를 할당(static)하고 해당 메모리에 인스턴스를 만들어 사용하는 패턴 즉, 싱글톤 패턴은 '하나'의 인스턴스만 생성하여 사용하는 디자인 패턴이다.(인스턴스가 필요할 때, 똑같은 인스턴스를 만들지 않고 기존의 인스턴스를 활용하는 것)

  • 생성자가 여러번 호출되어도, 실제로 생성되는 객체는 하나이며 최초로 생성된 이후에 호출된 생성자는 이미 생성한 객체를 반환시키도록 만드는 것이다.

  • Java에서는 생성자를 private으로 선언해 다른 곳에서 생성하지 못하도록 만들고, getInstance() 메소드를 통해 받아서 사용하도록 구현한다.

  • 왜 쓸까? - 객체를 생성할 때마다 메모리 영역을 할당받아야 한다. 하지만 한번의 new를 통해 객체를 생성한다면 메모리 낭비를 방지할 수 있다.
    또한 싱글톤으로 구현한 인스턴스는 '전역'이므로, 다른 클래스의 인스턴스들이 데이터를 공유하는 것이 가능한 장점이 있다.

  • 그럼 많이 사용하는 경우는 언제? - 주로 공통된 객체를 여러개 생성해서 사용해야하는 상황 : DB에서 커넥션 풀, 스레드 풀, 캐시, 로그 기록 객체 등등

기본 싱글톤 패턴

public class Singleton { 
	
	private static Singleton instance; // 단 하나의 인스턴스만 사용

	private Singleton() {} // private 생성자. 외부에서 인스턴스 생성못함.

	public static Singleton getInstance() { // 단하나의 인스턴스만 사용

		if (instance == null){ //여러 스레드에서 이 곳을 동시에 실행하면 문제 발생
			instance = new Singleton(); 
		} 
	return instance; 
	} 
}
  • 단점도 있을까? - SOLID 원칙 중의 OCP(개방/폐쇄 원칙) 에서 만약 싱글톤 인스턴스가 혼자 너무 많은 일을 하거나, 많은 데이터를 공유시키면 다른 클래스들 간의 결합도가 높아지는데, 이때 OCP에 위반된다.
    결합도가 높아지면, 유지보수가 힘들고 테스트도 원활하게 진행할 수 없는 문제점이 발생한다.
    또한, 멀티 스레드 환경에서 동기화 처리를 하지 않았을 때, 인스턴스가 2개가 생성되는 문제도 발생할 수 있다.


멀티스레드 환경에서 안전한 싱글톤 만드는 법


  1. Lazy Initialization(게으른 초기화)
public class ThreadSafe_Lazy_Initialization{
 
    private static ThreadSafe_Lazy_Initialization instance;
 
    private ThreadSafe_Lazy_Initialization(){} //private으로 생성자를 만들어 외부에서의 생성을 막음
     
    public static synchronized ThreadSafe_Lazy_Initialization getInstance(){
        if(instance == null){
            instance = new ThreadSafe_Lazy_Initialization();
        }
        return instance;
    }
 
}
  • 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦춘다. 값이 전혀 쓰이지 않으면 초기화도 결코 일어나지 않는다. 대부분의 경우에는 초기화 지연보다는 일반적으로 클래스를 생성하면서 초기화 하는것이 좋다. 초기화 비용이 크고, 내부적으로 필드 사용 빈도가 적다면 초기화 지연이 적절하다.
  • 기본 싱글톤 패턴의 동기화 문제는 해결했지만, 성능상에 문제가 있다.
    • 여러 client의 요청이 있을 때 getInstance() 메서드에 static synchronized를 적용했기에 싱글톤 클래스 자체를 락을 걸었다. 즉, 클래스를 사용할 때 단 하나의 스레드만 사용하게 되는 것이다.


  1. Lazy Initialization + Double-checked Locking
public class ThreadSafe_Lazy_Initialization{
    private volatile static ThreadSafe_Lazy_Initialization instance;

    private ThreadSafe_Lazy_Initialization(){}

    public static ThreadSafe_Lazy_Initialization getInstance(){
    	if(instance == null) { //첫번째 검사는 락을 사용안함. 이미 초기화되어있다면 바로 리턴하여 동기화 비용을 없애준다.
        	synchronized (ThreadSafe_Lazy_Initialization.class){ //위와 동일한 static synchronized
                if(instance == null){ //두번째 검사는 락을 사용한다. 초기화되어있지 않기 때문에
                    instance = new ThreadSafe_Lazy_Initialization();
                }
            }
        }
        return instance;
    }
}
  • double checked 방식은 필드가 초기화된 후로는 동기화하지 않으므로 해당 필드는 반드시 volatile로 선언해서 가시성을 확보해야 한다. 특히 멀티 코어 환경에서 동작한다면 스레드 별 cpu cache와 메인 메모리 간에 동기가 이뤄지지 않을 수 있다.
  • 예를 들어 첫번째 스레드가 instance를 생성하고 synchronized를 벗어남.
    • 두번째 스레드가 synchronized 블록에 들어와서 null 체크를 하는 시점에서, 첫번째 스레드에서 생성한 instance가 cpu cache가 있는 working memory에만 존재하고 main memory에는 존재하지 않을 경우(메인 메모리에 반영이 안된 경우)
    • 또는, main memory에 존재하지만 두번째 스레드의 working memory에 존재하지 않을 경우
    • 즉, 메모리 간 동기화(스레드가 메인메모리와 cpu cache간에 데이터를 일관되게 유지하는 것)가 완벽히 이루어지지 않은 상태라면 두번째 스레드는 인스턴스를 또 생성하게 된다.
    • 메인 메모리는 모든 스레드가 공유하는 공간이며, cpu cache는 각 스레드가 자주 액세스하는 데이터를 저장하는 임시 저장소이다.
    • 여기서 volatile이란? cpu메모리 영역에 캐싱된 값이 아니라 항상 최신의 값을 가지도록 메인 메모리 영역에서 값을 참조하는 키워드!
   public class Singleton {
    private volatile static DoubleCheckedSingleton instance;
    }
  • 하지만 이 방법도 완벽하지는 않다.


참고 : https://dev-coco.tistory.com/109

https://gyoogle.dev/blog/design-pattern/Singleton%20Pattern.html

profile
공부한 내용을 정리한 블로그입니다 & 백엔드 개발자

0개의 댓글