티스토리 뷰

1.  싱글톤 패턴

1) 싱글톤 패턴

  • 하나의 인스턴스만 생성해서 사용하는 디자인패턴
  • 인스턴스가 필요할 때, 또 생성해서 사용하는 것이 아니라 기존에 생성된 인스턴스를 활용하는 디자인 패턴
  • 기존의 인스턴스를 재사용하기 때문에 인스턴스 생성 비용을 줄일 수 있다.
  • 객체가 리소스를 많이 갖고 있어 무거운 클래스일 때, 보통 싱글톤 패턴을 사용한다.

2) 싱글톤 패턴을 사용하는 이유

① 데이터 공유

  • 싱글톤 패턴은 전역으로 사용되기 때문에 다른 클래스에서 객체를 공유할 수 있다.
  • 하나의 프로그램 안에서 한 객체를 공유해야 할  때 해당 객체를 싱글톤으로 구현해서 모든 사용자가 해당 객체를 공유해 사용할 수 있다.
  • 하지만 동시성 문제가 발생할 수 있어 이 점은 유의하여 설계하여야 한다.

② 메모리 절약

  • 객체를 생성할 때 메모리가 할당된다.
  • 싱글톤 패턴은 객체를 생성하기 위해서 인스턴스를 생성하는데 인스턴스 한 개만 고정적으로 사용한다.
  • 인스턴스를 한 개만 사용하기 때문에 추후에 객체를 생성할 때 메모리 낭비를 방지할 수 있다.

③ 속도

  • 이미 만들어진 인스턴스를 사용하기 때문에 속도적인 이점이 있다.

 

3) 싱글톤 패턴은 언제 사용할까?

① 공통된 개체를 여러개 생성해서 사용해야 하는 상황

  • 데이터베이스 연결모듈
    • 인스턴스 생성할 때 코스트가 많이 들기 때문에 싱글톤으로 인스턴스를 생성하면 효율적임
    • 객체를 한번 생성하고나면 그 객체를 다시 활용하는 것이 효율적이다.
    • ex) 디스크 연결, 네트워크 통신, DBCP 커넥션풀, 스레드풀, 캐시, 로그 기록 객체 
  • 안드로이드 앱
    • 각 액티비티들이나, 클래스마다 주요 클래스들을 하나하나 전달하는게 번거롭기 때문에 싱글톤 클래스를 만들어 어디서든 접근하도록 설계한다.

② 인스턴스가 절대적으로 한 개만 존재하는 것을 보증하고 싶을 때

 

4) 싱글톤 패턴 구현 in JAVA

Eager Initialization

  • 싱글톤 패턴을 구현하는 가장 간단한 방법
  • 싱글톤 패턴 생성 여부를 확인하고 싱글톤이 없으면 새로 만들고 있다면 만들어진 인스턴스를 반환한다.
  • static 멤버는 당장 객체를 사용하지 않더라도 메모리에 적재하기 때문에 만일 리소스가 큰 객체일 경우, 공간  자원 낭비가 발생함
  • 예외처리를 하지 않는다.
class Singleton {
	//static을 통해 class가 로드될때 객체를 생성 
	private static Singleton singleton = new Singleton(); 
	
	private Singleton() {} //생성자에 접근 x
	
	public static Singleton getInstance() {
		return singleton;
	}
}

② Thread safe initialization

  • 인스턴스를 반환하기 전까지 격리 공간에 놓기 위해 synchronized 키워드로 잠금을 할 수 있습니다.
  • 최초로 접근한 스레드가 해당 메서드 호출시에 다른 스레드가 접근하지 못하도록 잠금(lock)을 걸어준다.
  • getInstance() 메서드를 호출할 때마다 lock이 걸리기 때문에 동기화 작업에 overhead가 발생해 성능저하된다.
// 원자성 결여 코드 - 쓰레드 세이프하지 않다.
class Singleton {
    // 싱글톤 클래스 객체를 담을 인스턴스 변수
    private static Singleton instance;

    // 생성자를 private로 선언 (외부에서 new 사용 X)
    private Singleton() {}
	
    // 외부에서 정적 메서드를 호출하면 그제서야 초기화 진행 (lazy)
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); // 오직 1개의 객체만 생성
        }
        return instance;
    }
}
 
// synchronized 키워드 사용 
public class Singleton {
    private static Singleton instance;
    
    private Singleton() {}
    
    public static synchronized Singleton getInstance() {
        if (instance == null) {
        	instance = new Singleton();
        }
        return instance;
    }
}

[참고] synchronized 키워드

  • 멀티 쓰레드 환경에서 두개 이상의 쓰레드가 하나의 변수에 동시에 접근을 할 때 Race condition(경쟁상태)이 발생하지 않도록 한다.
  • 쓰레드가 해당 메서드를 실행하는 동안 다른 쓰레드가 접근하지 못하도록 잠금(lock)을 건다.
  • thread-1이 메서드에 진입하는 순간 나머지 thread-2 ~ 4의 접근을 제한하고, thread-1이 완료가 되면 다음 스레드를 접근시킨다.

③ Static Block Initialization

  • Eager Initialization 방법과 비슷하지만 Static Block을 사용하여 Exception 처리를 해주는 방법
  • static block을 통해서 Exception Handling에 대한 옵션을 제공한다.
  • static블록을 사용하기 때문에 사용하지 않는 공간을 차지하는 문제는 해결되지 않는다 -> 공간낭비
// static 블록 사용
class Singleton {
	private static Singleton singleton; 
	
	private Singleton() {} //생성자에 접근 x
	
	//static block을 통해 클래스가 처음 로딩 될때 객체를 생성
	static {
		try {
			singleton = new Singleton();
		} catch (Exception e) {
			throw new RuntimeException("Exception occured in creating singleton instance");
		}
	}
	public static Singleton getInstance() {
		return singleton;
	}
}

// static 멤버 사용
public class Singleton {
    private final static Singleton instance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
    	return instance;
    }
}

 


④ 정적 멤버와 Lazy Holder(중첩 클래스)

  • 멀티쓰레드 환경에서 안전하고 Lazy Loading(나중에 객체 생성) 도 가능한 완벽한 싱글톤 기법 -> 권장하는 기법
  • singleInstanceHolder(내부클래스)를 하나 더 만들어, Singleton클래스가 최초에 로딩되더라도 함께 초기화가 되지 않고. getInstance()가 호출될 때 singleInstanceHolder 클래스가 로딩되어 인스턴스를 생성하게 한다.
  • static 메소드에서는 static 멤버만을 호출할 수 있다. + 메모리 누수문제 해결 ->  내부 클래스를 static으로 설정
  • Reflection API, 직렬화/역직렬화를 통해 클라이언트가 임의로 싱글톤을 파괴할 수 있다
class Singleton {

    private static class singleInstanceHolder {
    	private static final Singleton INSTANCE = new Singleton();
    }
    
    // static 내부 클래스를 이용
    // Holder로 만들어, 클래스가 메모리에 로드되지 않고 getInstance 메서드가 호출되어야 로드됨
    public static Singleton getInstance() {
    	return singleInstanceHolder.INSTANCE;
    }
}

Double-Checked Locking (DCL)

  • 매번 synchronized 동기화를 실행하는 것이 문제라면, 최초 초기화할때만 적용하고 이미 만들어진 인스턴스를 반환할때는 사용하지 않도록 하는 기법
  • 이때 인스턴스 필드에 volatile 키워드를 붙여주어야 I/O 불일치 문제를 해결 할 수 있다.
  • volatile 키워드를 이용하기위해선 JVM 1.5이상이어야 되고, JVM에 대한 심층적인 이해가 필요하여, JVM에 따라서 여전히 쓰레드 세이프 하지 않는 경우가 발생하기 때문에 사용하기를 지양하는 편이다.
class Singleton {
    private static volatile Singleton instance; // volatile 키워드 적용

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
        	// 메서드에 동기화 거는게 아닌, Singleton 클래스 자체를 동기화 걸어버림
            synchronized (Singleton.class) { 
                if(instance == null) { 
                    instance = new Singleton(); // 최초 초기화만 동기화 작업이 일어나서 리소스 낭비를 최소화
                }
            }
        }
        return instance; // 최초 초기화가 되면 앞으로 생성된 인스턴스만 반환
    }
}

[참고] volatile 키워드

  • Java에서는 쓰레드를 여러개 사용할경우, 성능을 위해서 각각의 쓰레드들은 변수를 메인 메모리(RAM)으로부터 가져오는 것이 아니라 캐시(Cache) 메모리에서 가져오게 된다.
  • 문제는 비동기로 변수값을 캐시에 저장하다가, 각 쓰레드마다 할당되어있는 캐시 메모리의 변수값이 일치하지 않을수 있다는 점이다.
  • 그래서 volatile 키워드를 통해 이 변수는 캐시에서 읽지 말고 메인 메모리에서 읽어오도록 지정해주는 것이다.

 enum

  • 멤버를 만들때 private로 만들고 한번만 초기화 하기 때문에 thread safe함. -> 권장되는 기법
  • enum 내에서 상수 뿐만 아니라, 변수나 메서드를 선언해 사용이 가능. ->싱글톤 클래스처럼 응용이 가능
  • Bill Pugh Solution 기법과 달리, 클라이언트에서 Reflection을 통한 공격에도 안전하다.
  • 싱글톤 클래스를 멀티톤(일반적인 클래스)로 마이그레이션 해야할때 처음부터 코드를 다시 짜야 한다.
  • 클래스 상속이 필요할때, enum 외의 클래스 상속은 불가능하다.
enum SingletonEnum {
    INSTANCE;

    private final Client dbClient;
	
    SingletonEnum() {
        dbClient = Database.getClient();
    }

    public static SingletonEnum getInstance() {
        return INSTANCE;
    }

    public Client getClient() {
        return dbClient;
    }
}

public class Main {
    public static void main(String[] args) {
        SingletonEnum singleton = SingletonEnum.getInstance();
        singleton.getClient();
    }
}

 

2) 팩토리 패턴

  • 객체 생성을 공장(Factory) 클래스로 캡슐화 처리하여 대신 생성하게 하는 생성 디자인 패턴
  • 상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정한다.
  • 상위 클래스에서는 객체 생성방식에 대해 알 필요가 없어져 유연성을 갖게 되며 객체 생성 로직은 하위클래스에서만 관리 되기 때문에 유지보수성이 증가됩니다

 

예제 코드

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: " + type);
    	}
	}
}

public class Main {
    public static void main(String[] args) {
        Coffee coffee = " ";
        CoffeeFactory.createCoffee(CoffeeType.LATTE);
        System.out.println(coffee.getName()); // latte
    }
}

2. DI와 DIP

1) Dependency Injection, 의존성 주입

  • 메인 모듈(main mudule)이 ‘직접’ 다른 하위 모듈에 대한 의존성을 주기보다는 중간에 의존성 주입자(dependency injector)가 이 부분을 가로채 메인 모듈이 ‘간접’적으로 의존성을 주입하는 방식
  • 메인 모듈과 하위모듈간의 의존성을 조금 더 느슨하게 만들 수 있으며 모듈을 쉽게 교체 가능한 구조로 만든다.

[참고] '의존한다' 의 의미

  • A가 B에 의존한다. => B가 변하면 A에 영향을 미치는 관계
import java.util.*;
    class B {
        public void go() {
        	System.out.println("B의 go()함수");
        }
    }
    class A {
        public void go() {
        	new B().go();
        }
    }
    public class main{
        public static void main(String args[]) {
        	new A().go();
        }
    }
// B의 go()함수

2) 자바에서 DI 적용

① DI 적용 X

 

import java.util.*;
class BackendDeveloper {
    public void writeJava() {
    	System.out.println("자바가 좋아 인터네셔널~");
    }
}
class FrontEndDeveloper {
    public void writeJavascript() {
    	System.out.println("자바스크립트가 좋아 인터네셔널~");
    }
}
public class Project {
    private final BackendDeveloper backendDeveloper;
    private final FrontEndDeveloper frontEndDeveloper;
    
    public Project(BackendDeveloper backendDeveloper, FrontEndDeveloperfrontEndDeveloper) {
        this.backendDeveloper = backendDeveloper;
        this.frontEndDeveloper = frontEndDeveloper;
    }
    public void implement() {
        backendDeveloper.writeJava();
        frontEndDeveloper.writeJavascript();
    }
    public static void main(String args[]) {
        Project a = new Project(new BackendDeveloper(), new
        FrontEndDeveloper());
        a.implement();
    }
}

② DI 적용

  • 의존적인 화살표가 “역전"되어 있다 -> 의존관계역전원칙(Dependency Inversion Principle)이 적용된다.
interface Developer {
	void develop();
}
class BackendDeveloper implements Developer {
    @Override
    public void develop() {
    	writeJava();
	}
    public void writeJava() {
        System.out.println("자바가 좋아~ 새삥새삥");
    }
}
class FrontendDeveloper implements Developer {
    @Override
    public void develop() {
    	writeJavascript();
    }
    public void writeJavascript() {
    	System.out.println("자바스크립트가 좋아~ 새삥새삥");
    }
}
public class Project {
    private final List<Developer> developers;
    
    public Project(List<Developer> developers) {
    	this.developers = developers;
	}
	public void implement() {
    	developers.forEach(Developer::develop);
	}
	public static void main(String args[]) {
        List<Developer> dev = new ArrayList<>();
        dev.add(new BackendDeveloper());
        dev.add(new FrontendDeveloper());
        Project a = new Project(dev);
        a.implement();
    }
}

3) 의존 관계 역전 원칙(DIP, Dependency Inversion Principle)

  • 의존성 주입을 할 때는 의존관계역전원칙(DIP, Dependency Inversion Principle)이 적용된다.
  • DIP가 적용될 때는 2가지 규칙을 지키는 상태이다.
    • 상위 모듈은 하위 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.
    • 추상화는 세부사항에 의존해서는 안 된다. 세부 사항은 추상화에 따라 달라져야 한다.

4) 의존성 주입 장단점

① 장점

  • 외부에서 모듈을 생성하여 dev.add(new BackendDeveloper()) 이런식으로 집어넣는 구조가 되기 때문에 모듈들을 쉽게 교체할 수 있는 구조가 된다
  • 단위 테스팅과 마이그레이션이 쉽다.
  • 애플리케이션 의존성 방향이 좀 더 일관되어 코드를 추론하기가 쉽다..

② 단점

  • 결국에는 모듈이 더 생기게 되므로 복잡도가 증가한다.
  • 종속성 주입자체가 컴파일을 할 때가 아닌 런타임 때 일어나기 때문에 컴파일을 할 때 종속성 주입에 관한 에러를 잡기가 어려워진다.

4. MVC, MVP, MVVM패턴

1) MVC 패턴

  • 모델(Model), 뷰(View), 컨트롤러(Controller)로 이루어진 디자인 패턴

① 구조 

  • 모델(model) : 어플리케이션에서 사용되는 데이터와 그 데이터를 처리하는 부분.
    • 뷰에서 데이터를 생성하거나 수정할 때 컨트롤러를 통해 모델이 생성 또는 업데이트 된다.
    • ex) 사용자가 박스 안에 글을 적는다 -> 모델 : 박스의 크기정보, 글자내용, 글자의 위치, 글자의 포맷 정보 
  • 뷰(view) : 사용자에서 보여지는 UI 부분.
    • inputbox, checkbox, textarea 등 사용자 인터페이스 요소를 나타내며 모델을 기반으로 사용자가 볼 수 있는 화면
    • 모델이 가지고 있는 정보를 따로 저장하지 않아야 하며 변경이 일어나면 컨트롤러에 이를 전달해야 한다.
  • 컨트롤러(controller) : 사용자의 입력(Action)을 받고 처리하는 부분.
    • 하나 이상의 모델과 하나 이상의 뷰를 잇는 다리 역할 + 이벤트 등 메인 로직을 담당
    • 모델과 뷰의 생명주기를 관리한다.
    • 모델이나 뷰의 변경 통지를 받으면 이를 해석하여 각각의 구성 요소에 해당 내용에 대해 알려준다.

② 동작 과정

  • 사용자의 Action들은 Controller에 들어오게 됩니다.
  • Controller는 사용자의 Action를 확인하고, Model을 업데이트합니다.
  • Controller는 Model을 나타내줄 View를 선택합니다.
  • View는 Model을 이용하여 화면을 나타냅니다.

③ MVC패턴의 장점

  • 애플리케이션의 구성 요소를 세 가지 역할로 구분하여 개발 프로세스에서 각각의 구성 요소에만 집중해서 개발할 수 있다.
  • 재사용성과 확장성이 용이하다

④ MVC패턴의 단점

  • 애플리케이션이 복잡해질수록 모델과 뷰의 관계가 복잡해진다.

2) MVP 패턴

  • MVC패턴과 동일하지만 컨트롤러가 프레젠터로 교체된다.
  •  V와 P는 1:1 관계이므로 MVC보다 더 강한 결합을 지닌 디자인 패턴
  • Model과 view는 서로 알 필요가 없다. -> 프레젠터로 데이터를 전달받기 때문에

① 구조 

  • Model : 어플리케이션에서 사용되는 데이터와 그 데이터를 처리하는 부분
    • view에 의존적이지 않다.
  • View : 사용자에서 보여지는 UI 부분.
    • 사용자 인터페이스 요소를 나타내며 모델을 기반으로 사용자가 볼 수 있는 화면
  • Presenter : View에서 요청한 정보로 Model을 가공하여 View에 전달해 주는 부분
    • View와 Model사이를 연결해준다.

② 동작 과정

  1. 사용자의 Action들은 View를 통해 들어오게 됩니다.
  2. View는 데이터를 Presenter에 요청합니다.
  3. Presenter는 Model에게 데이터를 요청합니다.
  4. Model은 Presenter에서 요청받은 데이터를 응답합니다.
  5. Presenter는 View에게 데이터를 응답합니다.
  6. View는 Presenter가 응답한 데이터를 이용하여 화면을 나타냅니다.

③ MVP패턴의 장점

  • View와 Model의 의존성이 없다는 것입니다. -> Presenter를 통해서만 데이터를 전달 받기 때문에

④ MVP패턴의 단점

  • View와 Presenter 사이의 의존성이 높다.
    • 어플리케이션이 복잡해 질수록 View와 Presenter 사이의 의존성이 강해진다.

3) MVVM 패턴

[출처]&nbsp;https://brunch.co.kr/@oemilk/113

  • MVC 패턴에서 파생된 Model과 view간의 의존성 뿐만 아니라 controller와 view간의 의존성도 고려하여 각 구성요소가 독립적으로 작성되고 테스트 될 수 있도록 설계된 아키텍처 패턴
  • MVC의 C가 VM(뷰모델, view model)로 바뀐 패턴.
    • VM은 뷰를 추상화한 계층
    • VM : V = 1 : N 이라는 관계
  • VM은 커맨드와 데이터바인딩을 가집니다.
    • 커맨드 : 여러 요소에 대한 처리를 하나의 액션으로 처리할 수 있는 기법
    • 데이터바인딩 : 화면에 보이는 데이터와 브라우저 상의 메모리 데이터를 일치시키는 방법.
  • 대표적인 프레임워크로는 뷰(Vue.js)

① 구조

  • Model : 어플리케이션에서 사용되는 데이터와 그 데이터를 처리하는 부분.
    • view에 의존적이지 않다.
  • View : 사용자에서 보여지는 UI 부분.
    • 사용자 인터페이스 요소를 나타내며 모델을 기반으로 사용자가 볼 수 있는 화면
  • View Model : View를 표현하기 위해 만든 View를 위한 Model
    • View를 나타내 주기 위한 Model이자 View를 나타내기 위한 데이터 처리를 하는 부분.
    • view에 종속되지 않는다.

② 동작 과정

  1. 사용자의 Action들은 View를 통해 들어오게 됩니다.
  2. View에 Action이 들어오면, Command 패턴으로 View Model에 Action을 전달합니다.
  3. View Model은 Model에게 데이터를 요청합니다.
  4. Model은 View Model에게 요청받은 데이터를 응답합니다.
  5. View Model은 응답 받은 데이터를 가공하여 저장합니다.
  6. View는 View Model과 Data Binding하여 화면을 나타냅니다.

③ MVVM패턴의 장점

  • view와 Model 사이의 의존성이 없습니다.
  • Command 패턴과 Data Binding을 사용하여 View와 View Model 사이의 의존성도 없다.
    • 각각의 부분은 독립적이기 때문에 모듈화 하여 개발할 수 있습니다.

④ MVVM패턴의 단점

  • View Model의 설계가 쉽지 않다

4) Spring의 MVC패턴 적용사례

  • Spring : 자바기반으로 애플리케이션 개발을 할 때 많은 기능들을 제공하는 프레임워크

spring 아키텍쳐

① 디스패처 서블릿의 요청 처리과정

 

  • 1. 클라이언트가 요청을 했을 때 가장 먼저 디스패처 서블릿이 이를 받는다.
    • 디스패쳐 서블릿 : 프론트 컨트롤러 역할을 해준다. 
      • url, form data 등의 데이터를 기반으로 어떤 컨트롤러에게 이를 처리하게 결정하는 역할
    • 클래스이름, url, xml의 설정 등을 참고 할 수 있지만 보통 @requestmapping를 참고한다.
@RequestMapping(value = "/ex/foos", method = POST) 
@ResponseBody public String postFoos() {
	return "Post some Foos";
}
  • 2. 하나 이상의 handler mapping을 참고해서 적절한 컨트롤러를 설정한 후 해당 컨트롤러로 요청을 보냅니다.
  • 3. 해당 컨트롤러는 데이터베이스에 접근하여 데이터를 가져오는 등 비즈니스로직을 수행한다.
  • 4. 사용자에게 전달해야할 정보인 모델을 생성합니다.
  • 5. 뷰를 구현하기 위한 view resolver를 참고한다.
  • 6. 해당 정보를 기반으로 뷰를 렌더링한다.
  • 7. 응답 데이터를 보낸다.

참고자료

 

💠 싱글톤(Singleton) 패턴 - 꼼꼼하게 알아보자

Singleton Pattern 싱글톤 패턴은 디자인 패턴들 중에서 가장 개념적으로 간단한 패턴이다. 하지만 간단한 만큼 이 패턴에 대해 코드만 던져주고 끝내버리는 경우가 있어, 어디에 쓰이는지 어떠한 문

inpa.tistory.com

 

안드로이드 [Kotlin] - 아키텍처 패턴 with MVC, MVP, MVVM (feat 코드 예제)

MVP/MVVM/Clean Architecture 등 아키텍처 설계 혹은 적용 경험이 있으신 분 안드로이드 채용 공고를 보다 보면 어렵지 않게 볼 수 있는 글들이다. 오늘은 안드로이드 아키텍처패턴으로 많이 언급되는 MV

jminie.tistory.com

 

 

[디자인패턴] MVC, MVP, MVVM 비교

웹 개발자로 일을 하면서 가장 먼저 접한 디자인패턴이 바로 MVC 패턴이었습니다. 그만큼 유명하고 많이 쓰이는 디자인패턴인 MVC 패턴과 MVC 패턴에서 파생되어져 나온 MVP 패턴과 MVVM 패턴을 이야

beomy.tistory.com

 

 

💠 팩토리 메서드(Factory Method) 패턴 - 완벽 마스터하기

Factory Method Pattern 팩토리 메소드 패턴은 객체 생성을 공장(Factory) 클래스로 캡슐화 처리하여 대신 생성하게 하는 생성 디자인 패턴이다. 즉, 클라이언트에서 직접 new 연산자를 통해 제품 객체를

inpa.tistory.com

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/05   »
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
글 보관함