프린터를 관리하는 프로그램이 있다고 가정하자.
public class Printer {
public Printer() {}
public void print(Resource r) {
...
}
}
Printer 클래스를 사용해 프린터를 이용하려면 new Printer()로 생성하면 된다. 하지만 new Printer()를 하면 무한대로 프린터를 할당할 수 있기 때문에 Printer 생성을 막고자하면 private로 선언하면 된다.
public class Printer {
private Printer() {}
public void print(Resource r) {
...
}
}
이렇게 되면 외부에서 Printer를 만들 수 없어서 인스턴스를 제공하는 함수를 생성해야 한다.
public class Printer {
private static Printer printer = null;
private Printer() { }
public static Printer getPrinter() {
if (printer == null) {
printer = new Printer();
return printer;
}
}
public void print(Resource r) {
...
}
}
getPrinter 코드는 printer가 이미 생성되어 있는지를 판단하여 없다면 새롭게 만들고 반환해주는 함수이다.
이를 이용해 print 함수를 호출해보자
public class User {
private String name;
public User(String name) {
this.name = name;
}
public void print() {
Printer printer = Printer.getPrinter();
printer.print(this.name + " " + printer.toString());
}
}
Printer class
//생략
public class Main {
public static void main(String[] args) {
User[] user = new User[5];
for (int i = 0; i < 5; i++) {
user[i] = new User((i + 1) + "-user"); //User 인스턴스 생성
user[i].print();
}
}
}
// Result
1-user Printer@54abe48c
2-user Printer@54abe48c
3-user Printer@54abe48c
4-user Printer@54abe48c
5-user Printer@54abe48c
순서대로 User가 print를 하기 때문에 같은 프린터 인스턴스를 사용하는 걸 확인할 수 있다.
하지만
1. Printer 인스턴스가 아직 생성되지 않았을 때 스레드1이 getPrinter 메서드의 if문을 실행하고 있을 때 printer 변수는 null 이다.
2. 스레드2가 이 때 getPrinter를 호출하면 printer가 null이기 때문에 새롭게 printer를 만들게 된다.
3. 결과적으론 스레드1과 스레드2가 각각의 printer 인스턴스를 갖게된다.
public class UserThread extends Thread {
public UserThread(String name) {
super(name);
}
public void run() {
Printer printer = Printer.getPrinter();
printer.print(Thread.currentThread().getName() + " " + printer.toString());
}
}
public class Printer {
...
public static Printer getPrinter() {
if (printer == null) {
try {
Thread.sleep(1); //스레드 속도 차이를 위함
} catch (InterruptedException e) { }
printer = new Printer();
}
return printer;
}
...
}
public class Client {
public static void main(String[] args) {
UserThread[] user = new UserThread[5];
for (int i = 0; i < 5; i++) {
user[i] = new UserThread((i+1) + "-thread");
user[i].start();
}
}
}
// Result
3-thread Printer@54abe48c
5-thread Printer@37dcb6a1
1-thread Printer@54abe48c
4-thread Printer@5aab5135
2-thread Printer@7d513daa
실행결과를 통해 각 스레드마다 Printer 인스턴스를 만들게 됩니다. 이 자체로는 문제가 없지만 count하거나 데이터를 관리하는 로직이 들어가게 되면 문제가 발생합니다.
이걸 간단하게 해결하는 방법으로는 2가지가 있습니다.
1. 정적 변수에 인스턴스를 만들어 바로 초기화하는 방법
public class Printer {
private static Printer printer = new Printer();
private Printer() { }
public static Printer getPrinter() {
return printer;
}
}
2. 인스턴스를 만드는 메서드에 동기화하는 방법
public class Printer {
private static Printer printer = null;
private int counter = 0;
private Printer() { }
public synchronized static Printer getPrinter() {
if (printer == null) {
printer = new Printer();
}
return printer;
}
public void print(String str) { //str: 유저가 호출하는 print 인스턴스
counter++;
System.out.println(str+ " " + counter);
}
}
//Result
Printer@5ffdfb42 2
Printer@5ffdfb42 5
Printer@5ffdfb42 3
Printer@5ffdfb42 4
Printer@5ffdfb42 2
이 코드의 경우 인스턴스는 정상적으로 제공되지만 counter 변수가 제대로 돌아가지 않습니다. 그렇다면 print함수 역시 synchronized 처리해줍니다.
public class Printer {
private static Printer printer = null;
private int counter = 0;
private Printer() { }
public synchronized static Printer getPrinter() {
if (printer == null) {
printer = new Printer();
}
return printer;
}
public void print(String str) { //str: 유저가 호출하는 print 인스턴스
synchronized(this) {
counter++;
System.out.println(str+ " " + counter);
}
}
}
//Result
Printer@5ffdfb42 1
Printer@5ffdfb42 2
Printer@5ffdfb42 3
Printer@5ffdfb42 4
Printer@5ffdfb42 5
이렇게 처리하면 Counter 역시 잘 체크되는 것을 확인할 수 있다.
싱글톤 패턴 (Singleton Pattern)이란 인스턴스가 오직 하나만 생성되는 것을 보장하고 어디에서든 이 인스턴스에 접근할 수 있도록 하는 디자인 패턴이다.
정적 클래스를 이용하는 방법이 싱글톤 패턴을 이용하는 방법과 가장 차이가 나는 점은 객체를 전혀 생성하지 않고 메서드를 사용한다는 점이다
public class Printer {
private static int counter = 0;
public synchronized static void print(String str) {
counter++;
System.out.println(str + " " + counter);
}
}
public class UserThread extends Thread {
public UserThread(String name) {
super(name);
}
public void run() {
Printer.print(Thread.currentThread().getName() + " " + printer.toString());
}
}
public class Client {
public static void main(String[] args) {
UserThread[] user = new UserThread[5];
for (int i = 0; i < 5; i++) {
user[i] = new UserThread((i+1) + "-thread");
user[i].start();
}
}
}
// Result
3-thread 1
5-thread 2
1-thread 3
4-thread 4
2-thread 5
이렇듯 printer 객체를 생성하지 않아도 counter 변수에 아무 문제 없이 접근할 수 있다. 더욱이 정적 메서드를 사용하므로 일반적으로 실행할 때 바인딩되는 인스턴스 메서드를 사용하는 것보다 성능면에서 우수하다고 할 수 있다.
그러나 정적 클래스를 사용할 수 없는 경우가 있는데 대표적으로 인터페이스를 구현해야하는 경우다.
정적 메서드는 인터페이스에서 사용할 수 없다.
public interface Printer {
public static void print(String str); //Interface에서 허용 X
}
public class RealPrinter315 implements Printer {
public synchronized static void print(String str) {
...
}
}
따라서 위와 같은 코드는 사용할 수 없다.
인터페이스를 사용하는 주된 이유는 대체 구현이 필요한 경우다. 이는 특히 모의 객체를 사용해 단위 테스트를 수행할 때 매우 중요하다.
public class UsePrinter {
public void doSomething() {
String str;
...
str = ...;
RealPrinter315.print(str);
}
}
public class RealPrinter315 {
public synchronized static void print(String str) {
... // 실제 프린터 하드웨어를 조작하는 코드
}
}
가령 실제로 출력해야 하는 프린터가 아직 준비가 되어 있지 않거나 준비가 되었더라도 테스트할 때 결과가 올바른지를 확인하려고 매번 프린터 출력물을 검사하는 것은 매우 번거로운 일이다. 또한 프린터에 따라 테스트 실행 시간에 병목 현상이 나타날 수도 있다.
이 경우 UsePrinter 클래스의 단위 클래스를 실행할 때는 실제 프린터를 테스트용 가짜 프린터 객체로 대체하는 것이 좋다.
이렇게 설계를 변경하면 UsePrinter 클래스는 필요에 따라 실제의 프린터 하드웨어를 구동하는 RealPrinter315나 FakePrinter 클래스를 사용할 수 있게 된다.
public class UsePrinter {
public void doSomething(Printer printer) {
String str;
...
str = ...;
printer.print(str);
...
}
}
public interface Printer {
public void print(String str);
}
public class RealPrinter315 implements Printer {
private static Printer printer = null;
private RealPrinter315() { }
public synchronized static Printer getPrinter() {
if (printer == null) {
printer = new RealPrinter315();
}
return printer;
}
public void print(String str) {
... //실제 프린터 하드웨어를 조작하는 코드
}
}
public class FakePrinter implements Printer { //테스트용 가짜 프린터
private String str;
public void print(String str) {
this.str = str;
}
public String get() {
return str;
}
}
이때 doSomething 메서드로 인터페이스의 단위 테스트를 하는 상황을 가정해 인자를 준다고 하자.
import junit.framework.TestCase;
public class UsePrinterTest extends TestCase {
public void testdoSomething() {
FakePrinter fake = new FakePrinter();
UsePrinter u = new UsePrinter();
u.doSomething(fake);
assertEquals("this is a test", fake.get());
}
}
FakePrinter 클래스는 실저 출력을 실행하지 않고 doSomething 메서드를 실행할 때 프린터로 올바른 값이 전달되었는지 확인해야 한다. 따라서 전달된 문자열을 str 문자열 변수에 저장하고 나중에 테스트 케이스에서 get 메서드를 사용해 확인하게 한다.
이 방법외에도 정적 setter 메서드를 사용해 테스트용 대역 클래스를 만들 수 있따. 이렇게 하려면 싱글턴 클래스에 정적 setter 메서드를 추가하면 된다. 이 메서드는 인자로 싱글턴 클래스 인스턴스 객체를 참조하도록 하여 실제의 싱글톤 객체를 대신하도록 적절한 변수에 설정한다.
public class UsePrinter {
public void doSomething() {
String str;
...
str = ...;
PrinterFactory.getPrinterFactory().getPrinter().print(str);
...
}
public void print(str);
}
public class PrinterFactory {
private static PrinterFactory printerFactory = null;
protected Printer() { } // 접근 제한자를 protected로 변경
public synchronized static PrinterFactory getPrinterFactory() {
if (printerFactory == null) {
printerFactory = new PrinterFactory();
}
return printerFactory;
}
public static void setPrinterFactory(PrinterFactory p) { // 정적 setter 메서드
printerFactory = p;
}
public Printer getPrinter() {
return new Printer();
}
}
public class FakePrinterFactory extends PrinterFactory {
public Printer getPrinter() {
return new FakePrinter();
}
}
import junit.framework.TestCase;
public class DoSomeThingTest extends TestCase {
public void testdoSomething() {
FakePrinterFactory fake = new FakePrinterFactory();
UsePrinter u = new UsePrinter();
PrinterFactory.setPrinter(fake); // 가짜 Printer 객체 주입
u.doSomething(); // 가짜 프린터 사용 작업 실행
...
}
}
'CS' 카테고리의 다른 글
커맨드 패턴 (Command Pattern) (0) | 2022.07.27 |
---|---|
스테이트 패턴 (0) | 2022.07.22 |
스트래티지 패턴 (Strategy Pattern) (0) | 2022.07.06 |
객체지향 원리 (0) | 2022.06.24 |
객체지향 모델링 (0) | 2022.06.20 |
댓글