여러 블로그를 봐도 차이점이 잘 이해되지 않던 중 한 포스팅을 발견했고.. 너무나도 명확히 설명해주셔서 더 정리할게 없지만 제가 보고 싶은 내용만 추려봤습니다. 다이어그램에서는 1:1 / 1:n 관계, 인풋의 위치, 화살표의 방향 등에서 차이점을 확인할 수 있습니다. 


MVC 패턴

특징

  • Input이 컨트롤러로 들어옴
  • 하나의 컨트롤러는 여러 개의 뷰를 참조할 수 있음
  • View와 Model 사이의 의존성이 높음

 

동작

  1. 모든 입력(Input)들은 Controller로 전달됨
  2. Controller는 입력에 해당하는 Model을 업데이트
  3. 업데이트 결과에 따라 View를 선택
  4. Controller는 View를 선택할 뿐, 직접 업데이트를 하지 않음

 


MVP 패턴

특징

  • Model + View + Presenter
  • Input이 View로 들어옴
  • Presenter와 View는 1:1 관계임
  • 모델과 뷰가 분리되어 있고 오직 Presenter를 통해서 상태나 변화를 알려줄 수 있음
  • MVC와는 달리 UI(뷰)와 로직(모델)을 분리하고, 서로 간에 상호작용을 하는 다른 객체(Presenter)를 둠으로써 서로의 의존성을 최소화함
  • 장점: Presenter를 통해서만 데이터를 전달받기 때문에 View와 Model의 의존성이 없음
  • 단점: View가 Presenter를 통해 데이터를 주고받기 때문에 매우 의존적임

 

동작

  1. 모든 입력(Input)들은 View로 전달됨
  2. Presenter는 입력에 해당하는 Model을 업데이트
  3. Model 업데이트 결과를 기반으로 View를 업데이트
  4. Presenter는 해당 View를 참조하고 있음
  5. Presenter는 View와 Model 인스턴스를 가지고, Model과 View 사이의 매개체 역할을 함

 


MVVM 패턴

특징

  • Model + View + View Model
  • Input이 View로 들어옴
  • ViewModel과 View는 1:n 관계지만 MVC에서 화살표 방향이 바뀜
  • VM은 뷰를 표현하기 위해 만든 모델로, 뷰를 나타내기 위한 데이터 처리를 하는 부분
  • Command 패턴과 Data Binding 패턴을 이용하여 V-VM 간 의존성을 없앨 수 있음
  • 장점: V-M, V-VM 사이의 의존성이 없음 → 독립적으로 모듈화 개발 가능
  • 단점: ViewModel의 설계가 쉽지 않음

 

동작

  1. 모든 입력(Input)들은 View로 전달됨
  2. ViewModel은 입력에 해당하는 Presentation Logic을 처리하여 View에 데이터를 전달
  3. ViewModel은 View를 참조하지 않기 때문에 독립적임. 따라서 View는 자신이 이용할 ViewModel을 선택해 바인딩하여 업데이트를 받게 됨
  4. Model이 변경되면 해당하는 ViewModel을 이용하는 View가 자동으로 업데이트됨
  5. ViewModel은 View를 나타내 주기 위한 Model이자, View의 Presentation Logic을 처리

 


References

개요

정의 및 특징

  • Model + View + Controller 의 약자
  • 3가지 형태로 역할을 나누어 개발하는 방법론
  • 비즈니스 처리 로직과 사용자 인터페이스 요소를 분리시켜 서로 영향 없이 개발하도록 함
  • 사용자가 입력을 담당하는 View를 통해 요청을 보내면 해당 요청을 Controller가 받고, Controller는 Model을 통해 데이터를 가져오고, 해당 데이터를 바탕으로 출력을 담당하는 View를 제어해서 사용자에게 전달
  • MVC에 기반을 둔 디자인 패턴으로 MVVM, MVP, MVW 등이 있음

 

모델

  • 데이터와 애플리케이션이 무엇을 할 것인지를 정의하는 부분
  • 내부 비즈니스 로직을 처리하기 위한 역할
  • 컨트롤러가 호출하면 DB와 연동하여 사용자의 입출력 데이터를 다루는, 데이터와 연관된 비즈니스 로직을 처리함(데이터 추출, 저장, 삭제, 업데이트)
  • 여러 개의 데이터 변경 작업을 하나의 작업으로 묶은 트랜잭션을 다루는 일도 수행함
더보기
  1. 사용자가 편집하기를 원하는 모든 데이터를 가지고 있어야 함
    - 화면 안에 글자가 표현된다면 박스의 위치, 크기, 글자 포맷 정보 등을 가지고 있어야 함
  2. 뷰나 컨트롤러에 대해서 어떤 정보도 알지 말아야 함
    - 데이터 변경이 일어났을 때 모델에서 직접 UI를 수정할 수 있도록 뷰를 참조하는 내부 속성값을 가지면 안됨
  3. 변경이 일어나면, 변경 통지에 대한 처리방법을 구현해야만 함
    - 모델의 정보가 변경된다면 이벤트를 발생시켜 누군가에게 전달해야 함
    - 누군가가 모델을 변경하도록 요청하는 이벤트를 보냈을 때 이를 수신할 수 있는 처리 방법을 구현해야 함
    - 모델은 재사용가능해야 하며 다른 인터페이스에서도 변하지 않아야 함

 

  • 화면에 무엇인가를 보여주기 위한 역할
  • 사용자에게 보여주는 화면(UI)에 해당하며, 텍스트, 체크박스 항목과 같은 사용자 인터페이스 요소를 나타냄
  • 사용자와 상호작용을 하며 컨트롤러로부터 받은 모델의 결과값을 출력하는 화면을 만듦
더보기
  1. 모델이 가지고 있는 정보를 따로 저장해서는 안됨
    - 화면에 글자를 표시하기 위해 모델이 갖고 있던 정보를 전달받는데, 이 정보를 유지하기 위해 임의의 뷰 내부에 저장하면 안됨
    - 단순히 명령에 따라 정보를 표시하기만 하고, 그 정보들은 저장하지 않아야 함
  2. 모델이나 컨트롤러와 같은 다른 구성 요소들을 몰라야 함
    - 자기 자신을 제외하고는 다른 요소는 참조하거나 어떻게 동작하는지 알아서는 안됨
    - 그냥 데이터를 받으면 화면에 표시해주는 역할만 수행함
  3. 변경이 일어나면 변경 통지에 대한 처리 방법을 구현해야만 함
    - 화면에서 사용자가 내용을 변경하게 되면 이를 모델에게 전달해야 하며, 이 작업을 하기 위해 변경 통지를 구현함
    - 재사용이 가능하게끔 설계를 해야함

 

컨트롤러

  • 모델이 데이터를 어떻게 처리할지를 알려주는 역할
  • 비즈니스 로직을 처리하는 모델과 뷰 사이에 위치하며 둘이 서로 직접적으로 연결되지 않게 함
  • 클라이언트의 요청에 대해 모델과 뷰를 결정하여 전달하는 일종의 조정자 역할
  • 사용자가 데이터를 클릭하고 수정하는 것에 대한 “이벤트”들을 처리(+데이터 전처리)
  • 사용자의 요청 내용을 받아 Model과 View에 업데이트 요청을 보냄
  • 자바의 Spring에서는 DispatcherServlet으로 컨트롤러를 이해해야 함
더보기
  1. 모델이나 뷰에 대해서 알고있어야 함
    - 모델이나 뷰는 서로의 존재를 모르고 변경을 외부로 알리고/수신하는 방법만 갖고 있지만, 컨트롤러가 이를 중재하기 위해서는 모델과 그에 관련된 뷰에 대해서 알고 있어야 함
  2. 모델이나 뷰의 변경을 모니터링해야 함
    - 모델이나 뷰의 변경 통지를 받으면 이를 해석해서 각각의 구성 요소에게 통지해야 함
    - 애플레키에션의 메인 로직을 컨트롤러가 담당하게 됨

 

장점

  • 사용자 인터페이스로부터 비즈니스 로직을 분리하여 애플리케이션의 시각적 요소나 그 이면에서 실행되는 비즈니스 로직을 서로 영향 없이 쉽게 고칠 수 있음
  • 각 구성 요소들을 독립시켜 협업할 때 맡은 부분의 개발에만 집중할 수 있어 효율성을 높여줌
  • 서로 분리되어 각자의 역할에 집중할 수 있도록 개발하여 유지보수성, 애플리케이션의 확장성, 유연성이 증가하고 중복 코드도 줄어듦

 

단점(한계)

Massive ViewController (대규모 MVC 어플리케이션)

  • 모델과 뷰는 서로의 정보를 갖고 있지 않는 독립적인 상태라고 하지만, 모델과 뷰는 컨트롤러를 통해서 소통하기에 의존성이 완전히 분리될 수는 없음
  • 대규모 프로그램의 경우 화면에 복잡한 데이터의 구성이 필요하다면 컨트롤러에 다수의 모델과 뷰가 복잡하게 연결되는 상황이 생길 수 있음
  • 컨트롤러가 불필요하게 커지는 현상이 발생(Massive ViewController)
  • 컨트롤러는 뷰와 라이프 사이클과 강하게 연결되어 있어 분리할 수도 없고, 코드 분석/수정과 테스트가 모두 힘들어짐
  • 복잡하게 엮여있는 모델과 뷰는 여러 Side-Effect를 불러와 프로그램 운영을 힘들게 함

 


MVC 모델의 종류와 방식

MVC 구동 원리

MVC패턴은 Spring프레임워크와 JSP를 사용하는 웹 애플리케이션 개발에 많이 사용되고 있다. 이 때 웹 애플리케이션에서의 MVC의 구동 원리는 다음과 같다.

  1. 웹 브라우저가 웹 서버에 웹 애플리케이션 실행을 요청(MVC 구조가 WAS라고 보면 됨)
  2. 웹 서버는 들어온 요청을 처리할 수 있는 서블릿을 찾아서 요청을 전달(Matching)
  3. 서블릿은 모델 자바 객체의 메서드를 호출
  4. 데이터를 가공하여 값 객체를 생성하거나, JDBC를 사용하여 데이터베이스와의 인터랙션을 통해 값 객체를 생성
  5. 업무 수행을 마친 결과값을 컨트롤러에게 반환
  6. 컨트롤러는 모델로부터 받은 결과값을 View에게 전달
  7. JSP는 전달받은 값을 참조하여 출력할 결과 화면을 만들고 컨트롤러에게 전달
  8. 뷰로부터 받은 화면을 웹 서버에게 전달
  9. 웹 브라우저는 웹 서버로부터 요청한 결과값을 응답받으면 그 값을 화면에 출력

 

모델1

  • JSP에서 출력과 로직을 전부 처리 (컨트롤러와 뷰 영역을 같이 구현하는 방식)
  • 사용자의 요청을 JSP가 전부 처리
  • 빠르고 쉽게 개발할 수 있으나, JSP 파일이 너무 비대해지며 컨트롤러와 뷰가 혼재하므로 향후 유지보수에 어려움

⇒ 백엔드와 프론트엔드의 역할 분담이 모호해져 협업이 쉽지 않기 때문에 실제 서비스에서는 거의 사용하지 않음

 

모델2

  • JSP에서 출력만 처리
  • 사용자의 요청을 서블릿이 받고, 서블릿은 해당 요청으로 뷰로 보여줄지 모델로 보낼지 판단하여 전송
  • HTML 소스와 자바 소스를 분리해 놓았기 때문에 모델1 방식보다 확장시키기 쉽고 유지보수도 쉬움

 


References

개요

프록시란?

  • ‘대리’라는 의미로 무엇인가를 대신 처리함
  • 보안 상의 이유로 서버를 외부에 노출시키지 않기 위해 서버와 클라이언트단 중간에 위치한 서버를 프록시 서버라 함

 

정의

  • 실제 기능을 수행하는 객체(Real Object)를 바로 접근해서 제어하지 않고, 가상의 객체(Proxy Object)를 통해 우회하여 실제 기능에 접근함
  • 대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 디자인 패턴
  • 클라이언트 쪽에서는 실제 객체를 통해 메서드를 호출하고 반환 값을 받는지, 대리자 객체를 통해 받는지 전혀 모름

 

클래스 다이어그램

  • 클라이언트가 요청을 보냄(RealSubject의 request()메서드 호출)
  • Proxy가 대신 RealSubject의 request()메서드를 호출
  • 그 반환 값을 클라이언트에게 전달
  • Real Object와 Proxy Object는 동일한 인터페이스를 구현
  • Proxy Object는 메소드 수행 시 Real Object의 메소드에 위임함

 

특징

  • 대리자는 실제 서비스와 같은 이름의 메서드를 구현함(이때 인터페이스를 사용)
  • 대리자는 실제 서비스에 대한 참조 변수를 가짐
  • 대리자는 실제 서비스의 메서드 호출 전후에도 별도의 로직 수행 가능
  • 흐름 제어만 할 뿐 결과값을 조작하거나 변경시키면 안됨
  • 일반적으로 프록시는 서비스 객체들의 전체 수명 주기를 관리함
더보기

⇒ 동기적인 처리를 최대한 비동기적으로 처리하기 위함

1. 프록시 객체를 사용하지 않는 경우

  • 많은 양의 리소스를 필요로 하는 상황에서 DB 쿼리가 많이 느려질 수 있음
  • 지연 초기화를 위한 코드 작성이 필요하는데, 이를 모든 클래스마다 직접 넣을 경우 엄청 많은 코드 중복이 발생함

2. 프록시 객체를 사용하는 경우

  • 프록시 객체가 요청을 먼저 받은 뒤 흐름을 제어하여 DB에 쿼리를 날릴 수 있음
  • 서비스를 유연하게 제공 가능

 

장점

  • 참조를 통한 메모리 공간 확보
    • 해당 객체가 메모리에 존재하지 않아도 기본적인 정보를 참조하거나 설정할 수 있음
    • 사이즈가 큰 객체가 로딩되기 전에도 프록시를 통해서 참조할 수 있음
    • 객체의 기능이 필요한 시점까지 객체의 생성을 미룰 수 있음
    • 클라이언트들의 활성 상태 여부를 확인하여, 비어있을 경우 해당 서비스 객체를 닫고 그에 해당하는 시스템 자원을 확보할 수 있음
  • 흐름 제어
    • 클라이언트의 모든 요청을 받음 → 서비스를 유연하게 제공
    • 실제 메소드가 호출되기 이전에 전처리 등을 구현 객체의 변경 없이 추가할 수 있음
  • 캐시 사용 가능
    • 프록시가 내부 캐시를 통해 데이터가 캐시에 존재하지 않는 경우에만 주체 클래스에서 작업이 실행되도록 할 수 있음
  • 인터페이스를 두기 때문에 개발 코드에서 구현체에 영향을 받지 않아 코드 변경이 최소화
  • 실제 객체의 메소드를 숨기고 인터페이스를 통해 노출시킬 수 있어 방패(보안) 역할
  • 사용자 입장에서는 프록시 객체나 실제 객체나 사용법은 유사하므로 사용성이 좋음

 

활용

  • 기존의 작업 수행하며 부가적인 작업(초기화 지연, 로깅, 액세스 제어, 캐싱 등)을 수행하기 좋음
  • 비용이 많이 드는 연산(DB 쿼리, 대용량 텍스트 파일 등)을 실제로 필요한 시점에 수행할 수 있음
  • 페이지를 로드할 때 이미지는 텍스트보다 용량이 커서 로딩 시간이 오래 걸릴 수 있기 때문에, 프록시의 흐름 제어를 통해 현재까지 완료된 내용들을 우선적으로 보여주도록 함

 

단점

  • 객체를 생성할 때 한 단계를 거치게 되므로 빈번한 객체 생성은 성능을 저하시킬 수 있음
  • 객체 생성을 위해 스레드가 생성, 동기화가 구현되어야 하는 경우 성능을 저하시킬 수 있음
  • 로직이 난해해져 가독성이 떨어짐

 


프록시 패턴의 종류

가상 프록시와 보호 프록시의 사용 예시는 아래 예제 코드의 시나리오를 참고하면 도움이 됩니다.
해당 예제는 이 블로그를 참고했습니다. 

 

가상 프록시(Virtual Proxy)

  • 생성하기 힘든 자원에 대한 접근을 제어
  • 꼭 필요로 하는 시점까지 객체의 생성을 연기하고, 해당 객체가 생성된 것처럼 동작하도록 만들고 싶을 때 사용
  • 프록시 클래스에서 작은 단위의 작업을 처리하고 리소스가 많이 요구되는 작업들이 필요할 경우만 주체 클래스를 사용하도록 구현
  • 무거운 서비스 객체가 항상 가동되며 시스템 자원을 낭비하는 초기화 지연 경우에 사용

 

보호 프록시(Protection Proxy)

  • 접근 권한이 필요한 자원에 대한 접근을 제어
  • 특정 클라이언트에게만 서비스 객체를 사용할 수 있도록 하는 경우에 사용
  • 프록시 클래스에서 클라이언트가 주체 클래스에 대한 접근을 허용할지 말지 결정하도록 함
  • 예를 들어 운영 체제의 중요한 부분에 악의적인 응용 프로그램들이 접근하지 못하도록 할 수 있음

 

원격 프록시(Remote Proxy)

  • 원격 객체에 대한 접근을 제어
  • 서비스 객체가 원격 서버에 존재하는 경우, 네트워크를 통해 클라이언트의 요청을 전달하여, 네트워크와의 작업의 모든 복잡한 세부 사항을 처리함
  • 서로 다른 주소 공간에 있는 객체에 대해 마치 같은 주소 공간에 있는 것처럼 동작하게 하는 패턴
  • Ex: Google Docs

 

기타

  • 로깅 프록시(요청들의 로깅)
    • 서비스 객체에 대한 요청들의 기록을 유지하고 싶은 경우
    • 프록시는 각 요청을 서비스에 전달하기 전에 로깅할 수 있음
  • 캐싱 프록시(요청 결과들의 캐싱)
    • 요청 결과들을 캐싱하고, 이 캐시들의 수명 주기를 관리할 때(특히 결과들이 상당히 큰 경우)에 사용
    • 항상 같은 결과를 생성하는 반복 요청들에 대해 캐싱을 구현
    • 요청들의 매개변수들을 캐시 키로 사용할 수 있음

 


예제 코드

일반

public interface Subject {
    void request();
}
public class RealSubject implements Subject {
 
    @Override
    public void request() {
        System.out.println("Hello World!");
    }
}
public class Proxy implements Subject {
 
    private Subject subject;
 
    @Override
    public void request() {
        // 객체의 초기화를 지연시켜 실제로 사용될 때 생성함으로써 메모리 절약 가능
        if (this.subject == null) {
            this.subject = new RealSubject();
        }
 
        subject.request(); // 프록시가 RealSubject()의 메소드를 호출
    }
}
public class Main {
    public static void main(String[] args) {
        // RealSubject 클래스의 메소드를 호출하는것이 아닌 Proxy 클래스의 메소드를 호출
        Subject subject = new Proxy();
        subject.request(); // 내부적으로 RealSubject의 메소드를 호출
    }
}

 

가상 프록시

콘솔 프로그램으로 20개씩 난독화된 전자 서류의 본문을 복호화해서 보여주는 프로그램 작성한다고 해보자. 아래처럼 텍스트 파일을 읽는 인터페이스가 있다. 메서드가 하나밖에 없는 아주 간단한 인터페이스로, 앞으로 이 인터페이스를 구현하는 클래스는 반드시 fetch() 메서드를 구현해야 한다.

 

interface TextFile {
    String fetch();
}

실제 업무를 수행하기에 앞서 협업 조직에 SecretTextFile 이라는 클래스를 인수 받았다. 이 클래스는 난독화되어 있는 텍스트 파일을 복호화해서 평문으로 바꿔주는 클래스로, 협업 조직에서 라이브러리로 제공한 클래스라고 가정하자. 즉, 나는 이 클래스를 수정할 권한이 없다.

 

class SecretTextFile implements TextFile {
	private String plainText;

	public SecretTextFile(String fileName) {
		// 특별한 복호화 기법을 이용해 데이터를 복원해서 내용을 반환
		this.plainText = SecretFileHolder.decodeByFileName(fileName);
	}

	@Override
	public String fetch() {
		return plainText;
	}
}

이 클래스로 콘솔 프로그램을 구성했으나, 실행 후 첫 결과가 나오기까지 6초라는 시간이 걸렸다. 이유를 확인해보니 SecretTextFile 클래스에서 사용 중인 SecretFileHolder.decodeByFileName() 메서드의 수행 속도가 0.3초였던 것이다. 그리고 목록에 20개의 파일 내용을 노출해야 하는 상태였기 때문에 문제가 된 것이다. 화면을 구성할 때 이 파일들을 전부 객체로 만들다 보니 6초 정도의 로딩 시간을 갖게 된 것이다. 이제 프록시 클래스를 구현하자.

 

class ProxyTextFile implements TextFile {
    private String fileName;
    private TextFile textFile;

    public ProxyTextFile(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public String fetch() {
        if (textFile == null) {
            textFile = new SecretTextFile(fileName);
        }
				// 프록시 객체를 사용하는 경우를 확인하기 위해 [proxy] 문구를 넣음
        return "[proxy] " + textFile.fetch();
    }
}

프록시 패턴을 적용해 필요할 때만 파일 복호화를 하도록 수정하기로 했다. ProxyTextFile 클래스는 객체를 생성할 때에는 별다른 동작을 하지 않지만, 실제로 데이터를 가져와야 할 때는 실제 객체인 SecretTextFile 객체를 만들어내고 기능을 위임한다. 이제 프로그램 코드를 수정하자.

 

void main() {
	List<TextFile> textFileList = new ArrayList<>();

	textFileList.addAll(TextFileProvider.getSecretTextFile(0, 3));
	textFileList.addAll(TextFileProvider.getProxyTextFile(3, 20));

	textFileList.stream().map(TextFile::fetch).forEach(System.out::println);
}

다음의 코드는 처음 3개의 파일만 실제 객체를 사용하고 나머지는 프록시 객체를 사용해 프로그램에서 첫 결과가 나오는 것을 1초 내로 만들도록 한다. textFileList를 사용하는 입장에서는 별다른 조치없이 그대로 사용하면 된다. 콘솔에서 textFileList를 순회하면서 노출한다고 하면 처음 세개는 이미 로딩이 되어 있는 상태이므로 바로 노출하고 그다음 아이템부터는 차근차근 노출할 것이다.

따라서 ProxyTextFile 같은 프록시 클래스를 만들고 기존 SecretTextFile 클래스 대신 사용한것만으로도 초기 객체 생성 시간이 대폭 감소했다. 정말 필요한 시점에만 텍스트 복호화를 하게 된 것이다. 이렇게 초기 비용이 많이 드는 연산이 포함된 객체의 경우 가상 프록시를 사용했을 때 효과를 볼 수 있다.

 

보호 프록시

인사팀에서 인사정보에 대한 데이터 접근을 직책 단위로 세분화 하려고 한다. 기존에는 오로지 인사팀에서만 사용했던 부분이었으나 최근 인사정보를 직책별로 공개해줘야 하는 경우가 생겼다. 따라서 직책에 따라서 조직원의 인사정보 접근을 제어하는 업무를 수행해야 한다. 기존에 인사팀만 사용하던 코드는 아래와 같다.

 

// 직책 등급(차례대로 조직원, 조직장, 부사장)
enum GRADE {
    Staff, Manager, VicePresident
}

// 구성원
interface Employee {
    String getName(); // 구성원의 이름
    GRADE getGrade(); // 구성원의 직책
    String getInformation(Employee viewer); // 구성원의 인사정보(매개변수는 조회자)
}

// 일반 구성원
class NormalEmployee implements Employee {
    private String name;
    private GRADE grade;

    public NormalEmployee(String name, GRADE grade) {
        this.name = name;
        this.grade = grade;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public GRADE getGrade() {
        return grade;
    }

    // 기본적으로 자신의 인사정보는 누구나 열람할 수 있도록 되어있습니다.
    @Override
    public String getInformation(Employee viewer) {
        return "Display " + getGrade().name() + " '" + getName() + "' personnel information.";
    }
}

NormalEmployee 클래스는 단순히 Employee 인터페이스를 구현하기만 한 것으로, getInformation() 메서드는 조직원이 누구든 자신의 인사정보를 열람할 수 있도록 작성이 되어있다. 지금 이 상태로만 놔둔다면 누구든지 Employee 객체에서 getInformation() 메서드를 호출하면 누가 조회하든 정보를 보여줄 것이다. 이제부터 보호 프록시 클래스를 구성해보자. 이 클래스는 조회자의 직책을 확인하고 예외를 던지거나 인사 정보를 노출할 수 있도록 실제 객체에게 위임한다. 보호 프록시에서 메서드 호출시 조회자에게 권한이 없으면 NotAuthorizedException 예외를 던지도록 한다.

 

// 인사정보가 보호된 구성원(인사 정보 열람 권한 없으면 예외 발생)
class ProtectedEmployee implements Employee {
    private Employee employee;

    public ProtectedEmployee(Employee employee) {
        this.employee = employee;
    }

    @Override
    public String getInformation(Employee viewer) {
        // 본인 인사정보 조회
        if (this.employee.getGrade() == viewer.getGrade() && this.employee.getName().equals(viewer.getName())) {
            return this.employee.getInformation(viewer);
        }

        switch (viewer.getGrade()) {
            case VicePresident:
            	// 부사장은 조직장, 조직원들을 볼 수 있다.
                if (this.employee.getGrade() == GRADE.Manager || this.employee.getGrade() == GRADE.Staff) {
                    return this.employee.getInformation(viewer);
                }
            case Manager:
                if (this.employee.getGrade() == GRADE.Staff) { // 조직장은 조직원들을 볼 수 있다.
                    return this.employee.getInformation(viewer);
                }
            case Staff:
            default:
                throw new NotAuthorizedException(); // 조직원들은 다른 사람의 인사정보를 볼 수 없다.
        }
    }

    @Override
    public String getName() {
        return employee.getName();
    }

    @Override
    public GRADE getGrade() {
        return employee.getGrade();
    }
}

class NotAuthorizedException extends RuntimeException {
    private static final long serialVersionUID = -1714144282967712658L;
}

References

해당 포스팅에 사용된 예제는 이 블로그 를 참고했습니다 

개요

등장 배경

  • 객체 지향 디자인 패턴의 기본 원칙(OCP; Open Closed Principle)
    • “확장에 있어서는 열려 있어야 하며, 수정에 있어서는 닫혀있어야 한다”
    • 코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경할 수 있어야 함
    수정이 일어날 가능성이 큰 부분과 그렇지 않은 부분을 분리하는 것이 좋음
  • 객체는 속성과 함수가 변경, 추가될 가능성이 높음 → 객체의 생성을 담당하는 코드는 변경될 가능성이 높음
  • 객체의 생성을 담당하는 클래스를 한 곳에서 관리하여 클라이언트 코드와의 결합도를 줄이기 위해 사용
    • 결합도(의존도)란 한 클래스의 변경 사항이 다른 클래스에 얼마나 영향을 주는가를 의미
    • 클라이언트 코드란 분리시킨 객체 생성 코드를 호출하는 쪽을 말함

 

정의 및 특징

  • 객체를 생성하는 작업을 (팩토리) 클래스에 따로 모아두는 것을 의미
  • 상속 관계에서 부모 클래스가 중요한 뼈대를 결정하고, 자식 클래스가 객체 생성에 관한 구체적인 내용을 결정함
  • 상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 가짐
  • 크게 2가지 종류를 가짐
    • 팩토리 메소드 패턴: 객체를 생성하기 위한 인터페이스를 정의할 때, 어떤 클래스의 인스턴스를 만들지 서브 클래스에서 결정
    • 추상 팩토리 패턴: 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 인터페이스를 이용하여 생성할 수 있음
      (ex: 핸드폰=기종+OS+…)
    • 추상 팩토리 패턴에 팩토리 메소드 패턴이 포함될 수 있음

 

장점

  • 서로 간의 결합도를 낮추고 확장을 쉽게 함 → 유지보수성 향상
  • 상위 클래스에서는 인스턴스 생성 방식에 대해 전혀 알 필요가 없기 때문에 더 많은 유연성을 가짐
  • 사용자가 사용할 때는 정의된 인터페이스에 정의된 추상 메소드만 사용하면 됨
  • 단일 책임 원칙을 따름. 프로그램의 코드에서 생성자 코드를 분리함으로써 코드를 더욱 간결하게 만들 수 있음
  • 개방 폐쇄 원칙을 따름. 기존 client의 코드를 파괴하지 않고 새로운 타입을 추가할 수 있음

 

활용

  • 어떤 클래스가 자신이 생성해야 하는 객체의 클래스를 예측할 수 없는 경우
  • 생성할 객체를 기술하는 책임을 자신의 서브 클래스가 지정했으면 할 경우
  • java.util 패키지에 있는 Calendar, ResourceBundle, NumberFormat 등의 클래스에 정의된 getInstance() 메소드에서 팩토리 패턴을 사용
  • Boolean, Integer, Long 등 Wrapper 클래스에 정의된 valueOf() 메소드에서 팩토리 패턴을 사용

 

단점

  • 패턴을 구현할 많은 서브 클래스를 도입함으로써 코드가 복잡해질 수 있음

 

종합

  • 팩토리 패턴에 사용되는 슈퍼 클래스는 인터페이스나 추상 클래스, 혹은 그냥 평범한 자바 클래스여도 상관 없음
  • 팩토리 클래스를 Singleton으로 구현해도 되고, 서브 클래스를 리턴하는 static 메소드로 구현해도 됨
  • 팩토리 메소드는 입력된 파라미터에 따라 다른 서브 클래스의 인스턴스를 생성하고 리턴함

팩토리 메소드 패턴

정의 및 특징

  • 객체를 생성하기 위한 인터페이스는 미리 정의하되, 어떤 클래스의 인스턴스를 만들지는 서브 클래스에서 결정하도록 함
  • 즉, 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것
  • 여러 개의 서브 클래스를 가진 슈퍼 클래스가 있을 때, 인풋에 따라 하나의 자식 클래스의 인스턴스를 리턴해주는 방식

 

예제 코드

1. 프로토콜 생성

protocol Shape {
    func draw()
}

2. 클래스 생성

class Rectangle : Shape {
    func draw() {
        print("Inside Rectangle::draw() method")
    }
}
class Square : Shape {
    func draw() {
        print("Inside Square::draw() method")
    }
}
class Circle : Shape {
    func draw() {
        print("Inside Circle::draw() method")
    }
}

3. 팩토리 생성

class ShapeFactory {
    public func getShape(shapeType : String) -> Shape? {
        if shapeType == nil {
            return nil
        }
        if shapeType == "CIRCLE" {
            return Circle()
        }
        else if shapeType == "RECTANGLE" {
            return Rectangle()
        }
        else if shapeType == "SQUARE"{
            return Square()
        }
        return nil
    }
}

4. 데모

let shapeFactory = ShapeFactory()

let shape1 = shapeFactory.getShape(shapeType: "CIRCLE")
shape1?.draw()

let shape2 = shapeFactory.getShape(shapeType: "RECTANGLE")
shape2?.draw()

let shape3 = shapeFactory.getShape(shapeType: "SQUARE")
shape3?.draw()

5. 출력 결과

Inside Circle::draw() method
Inside Rectangle::draw() method
Inside Square::draw() method

추상 팩토리 패턴

정의 및 특징

  • 팩토리 메소드 패턴과 유사하나, 팩토리를 만드는 상위 팩토리(super-factory) 클래스가 존재
  • 연관된 서브 클래스를 그룹화할 수 있고 그 그룹을 자유롭게 교체할 수 있는 패턴
  • 인터페이스를 이용하여 구체적인 클래스에 의존하지 않고 서로 연관되거나 의존적인 객체들의 조합을 생성함

 

예제 코드

1. 클래스 생성

class RoundedRectangle : Shape {
    func draw() {
        print("Inside RoundedRectangle::draw() method")
    }
}
class RoundedSquare : Shape {
    func draw() {
        print("Inside RoundedSquare::draw() method")
    }
}

2. 슈퍼 팩토리 생성

protocol AbstractFactory {
    func getShape(shapeType : String) -> Shape?
}

3. 하위 팩토리 생성

class ShapeFactory : AbstractFactory {
    public func getShape(shapeType : String) -> Shape? {
        if shapeType == "RECTANGLE" {
            return Rectangle()
        } else if shapeType == "SQUARE"{
            return Square()
        }
        return nil
    }
}
class RoundedShapeFactory : AbstractFactory {
    public func getShape(shapeType : String) -> Shape? {
        if shapeType == "RECTANGLE" {
            return RoundedRectangle()
        } else if shapeType == "SQUARE"{
            return RoundedSquare()
        }
        return nil
    }
}

4. 팩토리를 생성하는 Producer 생성

class FactoryProducer {
    static func getFactory(rounded : Bool) -> AbstractFactory {
        if rounded {
            return RoundedShapeFactory()
        }else {
            return ShapeFactory()
        }
    }
}

5. 데모

let shapeFactory1 = FactoryProducer.getFactory(rounded: false)

let shape1 = shapeFactory1.getShape(shapeType: "RECTANGLE")
shape1?.draw()

let shape2 = shapeFactory1.getShape(shapeType: "SQUARE")
shape2?.draw()


let shapeFactory2 = FactoryProducer.getFactory(rounded: true)

let shape3 = shapeFactory2.getShape(shapeType: "RECTANGLE")
shape3?.draw()

let shape4 = shapeFactory2.getShape(shapeType: "SQUARE")
shape4?.draw()

6. 출력 결과

Inside Rectangle::draw() method
Inside Square::draw() method
Inside RoundedRectangle::draw() method
Inside RoundedSquare::draw() method

 


References

개요

정의

  • 애플리케이션이 시작될 때, 최초 한 번만 메모리를 할당하고(static) 그 메모리에 인스턴스를 만들어 사용하는 디자인 패턴
  • 즉, 객체의 인스턴스가 오직 ‘하나’만 생성됨
  • 인스턴스가 필요할 때 똑같은 것을 새로 만들지 않고 기존의 인스턴스를 활용하는 것
  • 생성자가 여러 번 호출되어도 실제로 생성되는 객체는 하나임
    • 자바에서는 생성자를 private로 선언해 다른 곳에서 생성하지 못하도록 방지

 

장점

  • 메모리 낭비 방지
    • 객체는 생성할 때마다 새로운 메모리 영역을 할당받아야 함
    • 한 번의 new를 통해 객체를 생성하기 때문에 고정된 메모리 영역을 사용
  • 이미 생성된 인스턴스를 활용하여 객체 로딩 시간이 줄어들며 속도 성능 향상
  • 전역으로 사용되기 때문에 다른 인스턴스들이 접근 가능하여 클래스 간 데이터 공유가 용이
  • 도메인 관점에서 인스턴스가 절대적으로 한 개만 존재하는 것을 보증하고 싶은 경우에 사용

 

활용

  • 주로 공통된 객체를 여러 개 생성해서 사용해야 하는 상황에 많이 사용됨
  • DB의 커넥션 풀, 스레드풀, 캐시, 로그 기록 객체 등
  • 안드로이드 앱에서도 각 Activity들이나 클래스마다 주요 클래스들이 하나하나 전달하는게 번거롭기 때문에 싱글톤 클래스를 만들어 어디서든 접근하도록 설계함

 

단점

  • 테스트가 어려움
    • 정적 필드는 한번 할당되면 프로그램이 종료되기 전까지 계속 살아있게 됨
    • 각 테스트는 독립적(다른 테스트에 영향을 미치지 않아야) 하는데, 한번 싱글톤 객체가 만들어지면 자원이 공유됨
      → 인스턴스의 상태를 매번 초기화해야 함
    • 일반적으로 인터페이스가 아닌 클래스를 통해 구현되는 싱글톤은 mock으로 대체할 수 없으므로 단위 테스트가 까다로워짐
    • 이를 해결하기 위해 주로 의존관계 주입(DI)를 사용하며, 이 경우 보통 DI 프레임워크가 싱글톤 객체의 생성을 제어함
  • 개방-폐쇄 원칙(OCP)” 위배 가능성
    • 객체 지향 설계 원칙(SOLID) 중 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계되어야 한다는 원칙
    • 싱글톤 인스턴스를 여러곳에서 많이 공유할 경우 다른 클래스들 간의 의존도가 높아지면서 객체 간의 독립성을 지향하는 원칙에 어긋남 → 유지보수가 어려워짐
  • 멀티 스레드 동시성 문제
    • 동기화가 제대로 이루어지지 않으면 경쟁 상태(race condition)가 될 수 있음
    • 인스턴스가 2개가 생성될 수도 있는 것

⇒ 유연성이 많이 떨어지는 패턴이라고 할 수 있음

 

종합

  • 싱글톤 패턴은 안티 패턴으로 불릴 만큼 단독으로 사용한다면 객체 지향에 위반되는 사례가 많음
  • 애플리케이션의 한 곳에서만 사용될 모듈을 싱글톤으로 구현할 경우, 해당 인스턴스가 제대로 gc되지 않는다면 끝까지 메모리를 차지하고 있기 때문에 적절한 경우에 사용할 필요가 있음(남용x)
  • 스프링 컨테이너같은 프레임워크의 의존성 주입(DI)를 통해 모듈 간의 결합을 느슨하게 만들 수 있음

 


예제

아래 방법들은 모두 Lazy Initialization에 해당한다. 모두 처음부터 싱글톤 변수에 초기화를 시키지 않고, getInstance() 메소드를 통해 필요로 할 때(게으르게) 인스턴스를 생성하고 있다.

일반적인 방식

  • 단일 스레드에서는 전혀 문제가 없는 방식
public class Car {
	// 메모리에 static으로 최초에 한번만 생성
	private static Car instance;
	
	private Car() {}

	// 외부에서 멤버로 선언된 instance를 가져올 수 있는 메서드
	public static Car getInstance(){
		if(instance == null){
			instance = new Car();
		}
		return instance;
	}
}

/************************************/

public class Test {
	// 인스턴스 사용 요청
	Car car = Car.getInstance();
}

 

동시성 문제 해결 방식

1. 지연 초기화(Lazy Initialization) + synchronized

  • synchronized 동기화를 활용해 스레드를 안전하게 만듦
  • 하지만 synchronized는 큰 성능저하를 발생시키므로 권장하지 않는 방법
public class Car {
	private static Car instance;
	
	private Car(){}

	public static synchronized Car getInstance(){
		if(instance == null){
			instance = new Car();
		}
		return instance;
	}
}

 

2. 더블 체크 락킹(Double-checked Locking)

  • 조건문으로 인스턴스의 존재 여부를 확인한 이후 synchronized를 통해 동기화를 시켜 인스턴스를 생성
  • 스레드를 안전하게 만들면서, 처음 생성 이후에는 synchronized를 실행하지 않기 때문에 1번 방법보다 성능이 좋음
  • 프로그램 구조가 그만큼 복잡해지고 비용 문제가 발생
public class Car {
	private static volatile Car instance;

	private Car(){}

	public static Car getInstance(){
		if(instance == null) {
			synchronized (Car.class){
				if(instance == null){
					instance = new Car();
				}
			}
		}

		return instance;
	}
}
  • volatile을 사용하는 이유
    • 컴파일러가 최적화라는 명목으로 연산의 순서를 변경(reordering)할 수 있기 때문
    • A 스레드에서 4번을 실행하여 객체를 위한 메모리 공간을 할당하고 참조를 instance에 저장한 후 싱글톤 객체의 생성자에서 내부 상태 초기화가 이루어지고 있다고 해보자. 이때B 스레드에서 1번을 실행할 경우 null이 아님을 확인하고 초기화 중인 객체 참조를 그대로 반환할 수도 있다. 따라서 외부에서 올바르게 초기화되지 않은 객체의 상태를 관찰할 수 있다는 것이다.
    • "객체의 초기화가 완전히 끝난 다음에 그 객체에 대한 참조가 instance에 저장되는 것이 아니라는 점"에 유의해야 한다. 실제로는 동기화 블록 내부에서 컴파일러로 인해 재배열이 일어나므로 다르게 동작할 수도 있다(단일 스레드에서는 개발자 입장에서 결과가 동일하다). volatile 키워드를 사용하면 필드에 값을 쓸 때 위와같은 재배열이 일어나지 않도록 컴파일러에게 지시하는 것이 가능하다. 
if (instance == null) { // (1)
	synchronized (Singleton.class) { // (2)
		if (instance == null) { // (3)
			instance = new Singleton(); // (4)
		} // (5)
	} // (6)
} // (7)
return instance; // (8)

 

3. 요청 시 초기화 홀더(Initialization-on-demand holder)

  • 내부에 Holder 클래스를 두어 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용
  • JVM의 클래스 초기화 과정에서 보장되는 원자적 특성을 이용
  • final을 사용해서 다시 값이 할당되지 않도록 만듦
  • 실제로 가장 많이 사용되는 일반적인 방식이라고 함
public class Car {
	private Car() {}
 
	private static class Holder {
		public static final Car INSTANCE = new Car();
	}
 
	public static Car getInstance() {
		// Class가 로딩되며 초기화가 진행. 이 시점에서 thread-safe 보장
		return Holder.INSTANCE;
	}
}
  • Holder.INSTANCE 부분에서 thread-safe가 되는 이유 
    • 클래스나 인터페이스 타입 T는 아래 작업들이 처음 일어나기 직전에 초기화됨
      1. T가 클래스이고 T의 인스턴스가 생성될 때
      2. T에 선언된 정적 메서드가 호출될 때
      3. T에 선언된 정적 필드가 할당될 때
      4. T에 선언된 정적 필드가 사용될 때(상수 변수가 아닌 필드)
    • Holder 클래스에 선언된 정적 필드인 INSTANCE가 사용될 때 Holder 클래스의 초기화가 일어남
    • 위 예시에서는 런타임에 getInstance()를 호출하여 Holder.INSTANCE 를 사용하기 전에, 클래스로더를 통해 Holder 클래스의 초기화가 일어나게 됨
    • 그와 동시에 Holder 클래스의 초기화 단계에서 정적 필드 INSTANCE의 초기화는 단 한 번만 일어남

 

4. 열거형(Enum)

  • 열거형에 열거형 상수를 통해 정의된 인스턴스 이외의 인스턴스는 존재하지 않음
    (열거형을 명시적으로 인스턴스화하려고 시도하면 컴파일 에러가 발생)
  • 직렬화(Serialization)가 가능
  • Reflection API를 통해 인스턴스를 만드려는 시도를 무력화 가능
  • 열거형은 다른 클래스를 상속받을 수 없으므로, 클래스 상속이 필요하다면 사용 불가능
public enum Singleton {
	INSTANCE;
 
	public static Singleton getInstance() {
		return INSTANCE;
	}
}

final class Singleton extends Enum<Singleton> {
	public static final INSTANCE = new Singleton();
}

 


References

+ Recent posts