티스토리 뷰
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사이를 연결해준다.
② 동작 과정
- 사용자의 Action들은 View를 통해 들어오게 됩니다.
- View는 데이터를 Presenter에 요청합니다.
- Presenter는 Model에게 데이터를 요청합니다.
- Model은 Presenter에서 요청받은 데이터를 응답합니다.
- Presenter는 View에게 데이터를 응답합니다.
- View는 Presenter가 응답한 데이터를 이용하여 화면을 나타냅니다.
③ MVP패턴의 장점
- View와 Model의 의존성이 없다는 것입니다. -> Presenter를 통해서만 데이터를 전달 받기 때문에
④ MVP패턴의 단점
- View와 Presenter 사이의 의존성이 높다.
- 어플리케이션이 복잡해 질수록 View와 Presenter 사이의 의존성이 강해진다.
3) MVVM 패턴
- 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에 종속되지 않는다.
② 동작 과정
- 사용자의 Action들은 View를 통해 들어오게 됩니다.
- View에 Action이 들어오면, Command 패턴으로 View Model에 Action을 전달합니다.
- View Model은 Model에게 데이터를 요청합니다.
- Model은 View Model에게 요청받은 데이터를 응답합니다.
- View Model은 응답 받은 데이터를 가공하여 저장합니다.
- View는 View Model과 Data Binding하여 화면을 나타냅니다.
③ MVVM패턴의 장점
- view와 Model 사이의 의존성이 없습니다.
- Command 패턴과 Data Binding을 사용하여 View와 View Model 사이의 의존성도 없다.
- 각각의 부분은 독립적이기 때문에 모듈화 하여 개발할 수 있습니다.
④ MVVM패턴의 단점
- View Model의 설계가 쉽지 않다
4) Spring의 MVC패턴 적용사례
- 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