본문 바로가기

Java

[Java] 디자인패턴 - 싱글톤 패턴, 템플릿 메소드 패턴, 팩토리 메소드 패턴

싱글톤 패턴

인스턴스가 오직 1개만 생성되는 패턴을 의미한다.

public class Singleton {

    private static Singleton instance = new Singleton();
    
    private Singleton() {
        // 생성자는 외부에서 호출 못하게 private 으로 지정해야 한다.
    }

    public static Singleton getInstance() {
        return instance;
    }

    public void say() {
        System.out.println("hi, there");
    }
}

 

싱글톤 패턴 사용하는 이유

메모리 측면

-> 한번의 new 연산자를 통해서 고정된 메모리 영역을 사용하기 때문에 추후 해당 객체에 접근할 때 메모리 낭비 방지

 

데이터 공유가 쉽다.

-> 싱글톤 인스턴스는 전역으로 사용되는 인스턴스이기 때문에, 다른 클래스의 인스턴스들이 접근하여 사용할 수 있다.

 

도메인 관점에서 인스턴스가 한개만 존재하는 것을 보증하고 싶을 때 싱글톤 패턴을 사용한다.

 

싱글톤 패턴 문제점

  • 여러 클래스에서 싱글톤 인스턴스의 데이터에 동시 접근하게 되면 동시성 문제가 발생할 수 있다.
  • 자원을 공유하고 있기 때문에 테스트가 어렵다.
  • 클라이언트가 구체 클래스에 의존하게 된다. new 키워드를 직접 사용하여 클래스 안에서 객체를 생성하기 때문에, SOLID 원칙 중 DIP 위반.
  • 이외에도 자식 클래스를 만들 수 없고, 내부 상태를 변경하기 어려운 유연성이 떨어지는 문제점이 있다.

팩토리 메소드 패턴

객체 생성을 하는 클래스를 따로 두는 패턴

실질적인 클래스의 구현은 하위 클래스에서 이루어지며, 상위 클래스는 하위 클래스의 구현 내용을 몰라도 사용 가능하다.

예) 인터페이스

장점

  • 객체간의 결합도가 낮아진다. -> 클래스 생성과 처리 로직을 분리하기 때문
  • 유지보수 용이 -> 기존의 클래스를 변경할 필요 없이 확장 가능하다.

단점

새로 생성할 객체가 늘어날 때마다, Factory 클래스에 추가해야 하기 때문에 서브 클래스 수가 많아진다.

 

 

예를 들어 현대 자동차에서 차를 만든다고 가정하자. 그럼 자동차 공장에 팩토리 메소드를 두고, 공장에서 승용차, 버스, 스쿠터 등을 생성하도록 둔다.

현대 자동차는 Factory 클래스의 MakeCar() 메소드로 모든 차를 만들 수 있다.

String으로 승용차, 버스, 스쿠터 중 무엇을 줄지만 결정하면 된다. 뿐만 아니라 Car 인터페이스의 Drive()메소드를 각각 SmallCar, Bus, Scooter 클래스가 구현하므로 차종에 맞게 오버라이딩할 수 있다.

 

팩토리 메소드 패턴 사용 예시

// 객체를 생성할 Factory 클래스
public class Factory {
    // 객체를 생성할 팩토리 메서드
    public Car MakeCar(String s) {
        if (s == "승용차") {return new SmallCar();}
        if (s == "버스") {return new Bus();}
        if (s == "스쿠터") {return new Scooter();}

        return null;
    }
}
// Bus 클래스
public class Bus implements Car {
    // 버스에 맞는 추상메서드 Drive() 구현
    @Override
    public void Drive() {
        System.out.println("버스 - 1종 운전 주행을 시작합니다.");
    }
}
public class SmallCar implements Car{
    // SmallCar에 맞는 추상메서드 Drive() 구현
    @Override
    public void Drive() {
        System.out.println("승용차 - 2종 운전 주행을 시작합니다.");
    }
}
public class Scooter implements Car{
    // Scooter에 맞는 추상메서드 Drive() 구현
    @Override
    public void Drive() {
        System.out.println("스쿠터 - 스쿠터 주행을 시작합니다.");
    }
}
public class Hyundai {
    public static void main(String[] args) {
        Factory factory = new Factory();

        // 팩토리 메서드 MakeCar()로 버스, 승용차, 스쿠터 객체 생성
        Car bus = factory.MakeCar("버스");
        Car smallcar = factory.MakeCar("승용차");
        Car scooter = factory.MakeCar("스쿠터");

        // 차종 별로 운전해보기
        bus.Drive();
        smallcar.Drive();
        scooter.Drive();
    }
}

실행 결과

버스 - 1종 운전 주행을 시작합니다.

승용차 - 2종 운전 주행을 시작합니다.

스쿠터 - 스쿠터 주행을 시작합니다.

 

팩토리 메소드 패턴을 사용하면서 MakeCar()에서만 객체를 생성하니 객체 관리가 편하다. 만약 새로운 클래스 Truck을 추가하고 싶다면, 팩토리 클래스에 Truck을 생성한 뒤 Truck 구현 클래스만 추가하면 된다. 기존의 클래스를 변경할 필요 없이 확장이 가능하다는 장점이 있다. 

 

 


템플릿 메소드 패턴

상속 시 상위 클래스의 메소드를 3종류로 나누는 패턴

추상 클래스가 템플릿을 제공하고, 이를 상속 받는 하위 클래스가 구체적인 로직을 작성한다.

중복된 로직은 추상 클래스에서 정의하고 달라지는 비즈니스 로직만 자식 클래스에서 오버라이딩한다.

여기서 중복된 로직은 "변하지 않는 부분"이고, 달라지는 로직은 "변하는 부분"이라고 할 수 있다. 

 

1. 공통된 역할을 수행하는 메소드인 템플릿 메소드

2. 반드시 구현해야 하는 추상 메소드

3. 그대로 사용해도 되고, 오버라이딩해서 사용해도 되는 훅 메소드

예) 상속

 

장점

  • 하위 클래스가 전체 로직을 변경하지 않으면서 부분적인 수정이 가능하다. 
  • 코드 중복을 최소화할 수있다. -> 디테일한 코드는 하위 클래스에서만 오버라이딩해서 사용하면 되기 때문, 재사용성 증가
  • 자식 클래스의 역할을 줄여 핵심 로직 관리가 용이하다.

단점

  • 추상 메소드가 많아지면서 클래스 관리가 복잡해진다.
  • 클래스간의 관계와 코드가 꼬여버릴 수 있다.

템플릿 메소드 패턴 사용 예제

abstract class Teacher{
	
    public void start_class() {
        inside();
        attendance();
        teach();
        outside();
    }
	
    // 공통 메서드
    public void inside() {
        System.out.println("선생님이 강의실로 들어옵니다.");
    }
    
    public void attendance() {
        System.out.println("선생님이 출석을 부릅니다.");
    }
    
    public void outside() {
        System.out.println("선생님이 강의실을 나갑니다.");
    }
    
    // 추상 메서드
    abstract void teach();
}
 
// 국어 선생님
class Korean_Teacher extends Teacher{
    
    @Override
    public void teach() {
        System.out.println("선생님이 국어를 수업합니다.");
    }
}
 
//수학 선생님
class Math_Teacher extends Teacher{

    @Override
    public void teach() {
        System.out.println("선생님이 수학을 수업합니다.");
    }
}

//영어 선생님
class English_Teacher extends Teacher{

    @Override
    public void teach() {
        System.out.println("선생님이 영어를 수업합니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Korean_Teacher kr = new Korean_Teacher(); //국어
        Math_Teacher mt = new Math_Teacher(); //수학
        English_Teacher en = new English_Teacher(); //영어
        
        kr.start_class();
        System.out.println("----------------------------");
        mt.start_class();
        System.out.println("----------------------------");
        en.start_class();
    }
}

실행결과

선생님이 강의실로 들어옵니다.

선생님이 출석을 부릅니다.

선생님이 국어를 수업합니다.

선생님이 강의실을 나갑니다.

-------------------------------------------------

선생님이 강의실로 들어옵니다.

선생님이 출석을 부릅니다.

선생님이 수학을 수업합니다.

선생님이 강의실을 나갑니다.

-------------------------------------------------

선생님이 강의실로 들어옵니다.

선생님이 출석을 부릅니다.

선생님이 영어를 수업합니다.

선생님이 강의실을 나갑니다.

 

 

코드 설명

국어 교사, 수학 교사, 영어 교사 모두 강의실에 들어와서 출석을 부르고 수업을 하고 강의실을 나가는 루틴이 모두 같다.

다만 어떤 과목을 수업하냐만 다르다.

이러한 예제에 템플릿 메소드 패턴을 적용하면, 먼저 추상 클래스(선생님)에 함수의 기본 틀을 정의하고, 공통 알고리즘을 구성한 뒤 하위 클래스(국어 선생님, 수학 선생님, 영어 선생님)에 구현해야 할 기능을 추상 메소드를 상속받아 정의한다.

 

'Java' 카테고리의 다른 글

[Java] 상속  (0) 2023.09.01
[Java] 열거 타입(Enum)  (0) 2023.09.01
[Java] 추상클래스와 인터페이스의 공통점/차이점  (0) 2023.09.01
[Java] 추상 클래스  (0) 2023.09.01
[Java] static의 의미와 사용법  (0) 2023.09.01