사용자가 편집하기를 원하는 모든 데이터를 가지고 있어야 함 - 화면 안에 글자가 표현된다면 박스의 위치, 크기, 글자 포맷 정보 등을 가지고 있어야 함
뷰나 컨트롤러에 대해서 어떤 정보도 알지 말아야 함 - 데이터 변경이 일어났을 때 모델에서 직접 UI를 수정할 수 있도록 뷰를 참조하는 내부 속성값을 가지면 안됨
변경이 일어나면, 변경 통지에 대한 처리방법을 구현해야만 함 - 모델의 정보가 변경된다면 이벤트를 발생시켜 누군가에게 전달해야 함 - 누군가가 모델을 변경하도록 요청하는 이벤트를 보냈을 때 이를 수신할 수 있는 처리 방법을 구현해야 함 - 모델은 재사용가능해야 하며 다른 인터페이스에서도 변하지 않아야 함
뷰
화면에 무엇인가를 보여주기 위한 역할
사용자에게 보여주는 화면(UI)에 해당하며, 텍스트, 체크박스 항목과 같은 사용자 인터페이스 요소를 나타냄
지연 초기화를 위한 코드 작성이 필요하는데, 이를 모든 클래스마다 직접 넣을 경우 엄청 많은 코드 중복이 발생함
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 객체를 만들어내고 기능을 위임한다. 이제 프로그램 코드를 수정하자.
다음의 코드는 처음 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;
}
객체 지향 설계 원칙(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 키워드를 사용하면 필드에 값을 쓸 때 위와같은 재배열이 일어나지 않도록 컴파일러에게 지시하는 것이 가능하다.
내부에 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는 아래 작업들이 처음 일어나기 직전에 초기화됨
T가 클래스이고 T의 인스턴스가 생성될 때
T에 선언된 정적 메서드가 호출될 때
T에 선언된 정적 필드가 할당될 때
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();
}