느리더라도 꾸준히

[OOP]객체지향 프로그래밍(OOP) - (5) 싱글톤 패턴 본문

Java

[OOP]객체지향 프로그래밍(OOP) - (5) 싱글톤 패턴

테디규 2022. 11. 20. 19:10

목표

객체 지향적이지 않은 코드를 객체 지향으로 바꾸면서 객체 지향의 4가지 특징을 이해한다.

최종적으로 객체 지향적인 코드를 만들어보고, 이를 미리 구현 해 놓은 Springframework를 이해해보자.

(1) 상속

(2) 추상화 및 다형성을 통한 DI(Dependency Injection)

(3) 캡슐화

(4) SRP(Single-Relationship Principle)

(5) 싱글톤 패턴의 필요성

정리

Singleton을 사용하는 이유

  1. 최초 한번만 인스턴스를 생성하고 활용하므로 메모리와 속도 측면에서 이점이 있다.
  2. 다른 클래스간의 데이터 공유가 쉬워집니다. 단 여러 클래스에서 동시에 싱글톤 인스턴스에 접근하면 동시성 문제가 발생할 수 있다.

Singleton의 문제점

  1. 구현하는데 작성 해야 할 코드가 많다.
  2. 공유하는 자원이므로, 각 단위로 나누어 테스트 하기 어렵다.
  3. 의존관계상 구체 클래스에 의존하게 된다. 그러므로 DIP, OCP를 위반한다.
    • Spring Framework가 이 문제를 해결하며 객체를 singleton으로 제공할 수 있게 도와준다.

문제 상황

public class AppConfig {

    public CustomerRepository customerRepository(){
        return new CustomerRepository();
    }

    public PhoneInfo phoneInfo(){
        return new PhoneInfo(1000000, "iphone");
    }

    public Discount discount(){
        return new Discount(new HashMap<String, DiscountCondition>(){{
            put("학생",new StudentDiscountCondition(new RatePolicy(10)));
            put("직원",new EmployeeDiscountCondition(new AmountPolicy(10000)));
        }});
    }
        // 싱글톤
    public RemovedRepository removedRepository(){
        return new RemovedRepository(customerRepository());
    }
}

실제로는 CustomerRepository에서 회원들을 모두 관리할 수 있으므로 RemovedRepository 가 필요하지는 않지만, 싱글톤 패턴 연습을 위해 생성해보겠습니다.

RemovedRepository 는 CustomerRepository 에서 고객을 삭제 한 이후의 CustomerRepository 를 저장하는 클래스입니다.

CustomerRepository

public class CustomerRepository {
    private Customer[] customers = new Customer[]{
            new Customer(1, "김무개","일반"),
            new Student(2, "최학생", "학생", "한국고등학교"),
            new Employee(3, "이직원", "직원", "대한마트"),
            new Employee(4, "박직원", "직원", "대한마트") // 추가!!
    };

    public Customer[] findAll(){
        return customers;
    }
    // 고객 id가 들어오면 삭제하는 메서드를 추가.
    public void removeCustomer(int id){
       Customer[] newcustomers = new Customer[customers.length-1];
       System.arraycopy(customers,0,newcustomers,0,id-1);
       System.arraycopy(customers,id,newcustomers,id-1,customers.length - id);
       customers = newcustomers;
    }
}

removeCustomer() 를 추가해줍니다.

RemovedRepository

public class RemovedRepository {
    private CustomerRepository customerRepository;
    public RemovedRepository(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public CustomerRepository getCustomerRepository() {
        return customerRepository;
    }
}

삭제 이후의 DB를 담아와서 출력해주도록 RemovedRepository 를 작성합니다.

MartApp

public class MartApp {
    // 주입이 필요한 객체들-------------------
    CustomerRepository customerRepository;
    PhoneInfo phoneInfo;
    Discount discountClass;
    RemovedRepository removedRepository;
    //-------------------------------------

    public MartApp(CustomerRepository customerRepository, PhoneInfo phoneInfo, Discount discountClass, RemovedRepository removedRepository) {
        this.customerRepository = customerRepository;
        this.phoneInfo = phoneInfo;
        this.discountClass = discountClass;
        this.removedRepository = removedRepository;
    }

    public void start(){
        // 할인 로직 시작
        // -----------인스턴스변수의 초기화순서 : 기본값 -> 명시적초기화 -> 인스턴스 초기화 블럭 -> 생성자
        // 그래서 메서드 안에서 지역변수로 선언했다.
        Customer[] customers = customerRepository.findAll();
        int originalPrice = phoneInfo.getPrice();

        MartService martService = new MartService(customers, phoneInfo.getPrice(), discountClass);

        System.out.println("-".repeat(50));
        System.out.println("핸드폰의 원가는 " + phoneInfo.getPrice() + "원 입니다.");
        System.out.println("-".repeat(50));
        martService.service();

        // id == 4 를 가진 한 고객을 삭제한 후, 다시 할인 로직을 돌려보자.
        customerRepository.removeCustomer(4);
                //customers = customerRepository.findAll();
        customers = removedRepository.getCustomerRepository().findAll();
        martService = new MartService(customers, phoneInfo.getPrice(), discountClass);
        martService.service();
    }
}

사실 customerRepository.findAll(); 로 DB에서 빼오면 되지만, 싱글톤 패턴을 보기 위해 RemovedRepository 의 removedRepository 참조 변수에서 DB를 가져옵니다.

출력 결과

--------------------------------------------------
핸드폰의 원가는 1000000원 입니다.
--------------------------------------------------
할인 요금을 출력합니다.
고객이름 : 김무개 , 할인된요금 : 1000000
고객이름 : 박직원 , 할인된요금 : 990000
고객이름 : 이직원 , 할인된요금 : 990000
고객이름 : 최학생 , 할인된요금 : 900000
--------------------------------------------------
4번 id를 가진 고객을 지웠습니다.
--------------------------------------------------
할인 요금을 출력합니다.
고객이름 : 김무개 , 할인된요금 : 1000000
고객이름 : 박직원 , 할인된요금 : 990000
고객이름 : 이직원 , 할인된요금 : 990000
고객이름 : 최학생 , 할인된요금 : 900000

remove가 되지 않은 원본 Repository가 나옵니다. 왜 그런 것일까요?

다시 Appconfig를 봅시다.

public class AppConfig {

    public CustomerRepository customerRepository(){
        return new CustomerRepository();
    }

    public PhoneInfo phoneInfo(){
        return new PhoneInfo(1000000, "iphone");
    }

    public Discount discount(){
        return new Discount(new HashMap<String, DiscountCondition>(){{
            put("학생",new StudentDiscountCondition(new RatePolicy(10)));
            put("직원",new EmployeeDiscountCondition(new AmountPolicy(10000)));
        }});
    }
        // 싱글톤
    public RemovedRepository removedRepository(){
        return new RemovedRepository(customerRepository());
    }
}

RemovedRepository 는 생성자로 customerRepository()의 return 값인 new CustomerRepository() 를 받고 있습니다. 그러므로 삭제된 db 가 아니라 새로 생성된 원본 db가 나오는 것이죠. DI를 통한 주입에서 객체를 새로 생성하지 않고 단 한번만 생성하고 관리하고 싶을 때 우리는 싱글톤 패턴을 사용할 수 있습니다.

싱글톤 패턴을 적용하자.

싱글톤 패턴

객체의 인스턴스가 오직 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");
    }
}

우리 코드에 맞게 싱글톤을 적용 시키면 아래와 같습니다.

public class AppConfig {
        //private 으로 생성하여 한번만 인스턴스가 사용되도록한다.
    private CustomerRepository customerRepository = new CustomerRepository();
    public CustomerRepository customerRepository(){
        return customerRepository;
    }

    public PhoneInfo phoneInfo(){
        return new PhoneInfo(1000000, "iphone");
    }

    public Discount discount(){
        return new Discount(new HashMap<String, DiscountCondition>(){{
            put("학생",new StudentDiscountCondition(new RatePolicy(10)));
            put("직원",new EmployeeDiscountCondition(new AmountPolicy(10000)));
        }});
    }

    public RemovedRepository removedRepository(){
        return new RemovedRepository(customerRepository());
    }
}

CustomerRepository의 생성자private 하지 않으므로 완벽한 싱글톤 이라고 할 수는 없습니다만, Appconfig 속에서 단 한번만 인스턴스를 생성하고 메서드로는 호출만 해주는 방식으로 싱글톤 패턴을 구현하고 있습니다. 이제 RemovedRepository 는 고객이 제거된 db를 얻을 수 있습니다.

이때 AppConfig 는 CustomerRepository 객체에 의존하는 관계를 보여주고 있습니다.(DIP 위반)

출력결과

--------------------------------------------------
핸드폰의 원가는 1000000원 입니다.
--------------------------------------------------
할인 요금을 출력합니다.
고객이름 : 김무개 , 할인된요금 : 1000000
고객이름 : 박직원 , 할인된요금 : 990000
고객이름 : 이직원 , 할인된요금 : 990000
고객이름 : 최학생 , 할인된요금 : 900000
--------------------------------------------------
4번 id를 가진 고객을 지웠습니다.
--------------------------------------------------
할인 요금을 출력합니다.
고객이름 : 김무개 , 할인된요금 : 1000000
고객이름 : 이직원 , 할인된요금 : 990000
고객이름 : 최학생 , 할인된요금 : 900000

Process finished with exit code 0

싱글톤을 사용하는 이유

  1. 최초 한번만 인스턴스를 생성하고 활용하므로 메모리와 속도 측면에서 이점이 있다.
  2. 다른 클래스간의 데이터 공유가 쉬워집니다. 단 여러 클래스에서 동시에 싱글턴 인스턴스에 접근하면 동시성 문제가 발생할 수 있다.

싱글톤의 문제점

  1. 구현하는데 작성 해야 할 코드가 많다.
  2. 공유하는 자원이므로, 각 단위로 나누어 테스트 하기 어렵다.
  3. 의존관계상 구체 클래스에 의존하게 된다. 그러므로 DIP, OCP를 위반한다.
    • Spring Framework가 이 문제를 해결하며 객체를 singleton으로 제공할 수 있게 도와준다.

스프링 프레임워크와 싱글톤

@Configuration
public class AppConfig {
        @Bean
    public CustomerRepository customerRepository(){
        return new CustomerRepository();
    }
        @Bean
    public PhoneInfo phoneInfo(){
        return new PhoneInfo(1000000, "iphone");
    }
        @Bean
    public Discount discount(){
        return new Discount(new HashMap<String, DiscountCondition>(){{
            put("학생",new StudentDiscountCondition(new RatePolicy(10)));
            put("직원",new EmployeeDiscountCondition(new AmountPolicy(10000)));
        }});
    }
        @Bean
    public RemovedRepository removedRepository(){
        return new RemovedRepository(customerRepository());
    }
}

Spring Framework로 프로젝트를 만들면 위 코드처럼 똑같이 DI를 적용할 수 있습니다.

위 코드는 CustomerRepository 객체를 new로 생성하고 있지만, Spring Framework는 기본적으로 모든 객체들을 Bean이라는 개념으로 singleton으로 관리해줍니다. 그러므로 RemovedRepository의 생성자에는 고객이 제거된 CustomerRepository 객체가 들어갈 것 입니다.

출처

https://tecoble.techcourse.co.kr/post/2020-11-07-singleton/

https://hongchangsub.com/springcore5/

Comments