이번에 내가 토이 프로젝트를 진행하며 생각하게 된 싱글톤과 상속에 대한 부분을 다룰려고 한다.

🤔 내가 생각한 싱글톤

싱글톤이란

위키의 말을 빌려보자면 '생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다.' 이다. 하나의 객체를 호출하는 방법을 만들고 객체가 만들어지는 과정을 한번만 겪게 만드는 방식을 가장 많이 이용한다.

public class Singleton {
    static Singleton singletonObject; // 정적 참조 변수
    private Singleton(){}; // private 생성자
    
	// 객체 반환 정적 메서드
    public static Singleton getInstance() {
        if (singletonObject == null) {
            singletonObject = new Singleton();
        }
        return singletonObject;
    }
}

처음 생각한 싱글톤

아마 대부분의 사람들이 '싱글톤이 중요하다', '대부분 싱글톤으로 만들어진다'라는 말을 많이 들어봤을 것이다. 그런 말을 들어봤음에도 나는 이때까지 '싱글톤? 그냥 인스턴스 하나로 만들면 되는 거지' 라고 가볍게 생각해왔고 이번 토이 프로젝트를 진행하면서 내 생각이 짧았던 것임을 알게 되었다. 나는 하나의 객체로 이루어진건 그냥 무조건 싱글톤으로 만들면서 이번 프로젝트를 진행했습니다.

✍🏻 토이프로젝트에서 싱글톤 구현


'메인 메뉴에 비슷한 부분을 만들고 아래에서 재사용하면 좋을 것 같고 각자 다 하나씩이니까 싱글톤으로 만들어야지' 라고 생각했기에 나는 MainMenu클래스를 상속한 3개의 클래스를 만들었고 각각의 모든 클래스의 객체들을 싱글톤 패턴으로 구현하였다.

내가 경험한 문제점

생각대로 코딩을 진행하자 상속받은 하위의 클래스들의 생성자 부분들이 죄다 오류가 나기 시작했다. 이유는 내가 MainMenu클래스를 싱글톤으로 만들었고 그렇기에 생성자를 private으로 설정했기 때문이다. '왜 안돼지?'라는 생각보다는 '이게 말이되나?'라는 생각이 처음 들었다. 내가 생각한 논리에 결함이 없다고 생각했기 때문에 이런 문제를 받아들이지 못했던 것이다. 그래서 나는 상위 클래스의 생성자를 protected로 변경해버리고 넘어가 버렸다.

public class MainMenu {

    // private static MainMenu mainMenu;
    protected static MainMenu mainMenu;

    private MainMenu(){} 

    public static MainMenu getInstance(){
        if (mainMenu == null) {
            mainMenu = new MainMenu();
        }
        return mainMenu;
}
    
public class CustomerMenu extends MainMenu { 
    private static CustomerMenu customerMenu;

    private CustomerMenu(){} // 오류

    public static CustomerMenu getInstance(){
        if (CustomerMenu == null) {
            CustomerMenu = new CustomerMenu();
        }
        return CustomerMenu;
}

❗❗❗문제 인식

다시 돌아본 싱글톤

부끄럽게도 나는 이번 프로젝트를 완성한 이후에도 protected로 바꾼 방식이 문제가 없다고 생각했다. 그러다가 이번 스터디를 하는 가운데 팀장님이 이 부분에 대해 '다들 생성자 부분에서 오류가 발생한 일이 없었나요?'는 말을 해주셨고 이때 처음으로 문제가 있는지 없는지 확인하게 되었고 내가 어떤 부분을 잘 못했는 지 알게 되었다. '한번만 생성한다' 라는 것을 제대로 생각하지 않은 것이 문제가 되었던 것이다.

😮 한번만 생성한다

내가 만든 메뉴 클래스들을 메모리 상에서 나타내면 이렇게 될 것이다.

이렇게 메모리 상에 한번 그림을 나타내보니 내가 몰랐던 문제점들을 잘 알게 되었다. 나는 MainMenu의 클래스를 싱글톤으로 만들면서 어쨋든 내 눈에 보이는 'mainMenu라는 인스턴스는 하나니까 한번만 생성되겠지' 라는 짧게 생각했지만 실제는 달랐다. 상속된 객체를 생성할 때 '하위 인스턴스가 생성될 때 상위 인스턴스도 같이 생성된다'와 싱글톤의 '한번만 생성한다' 를 모두 고려하지 않은채 코딩을 했기 때문에 오류가 발생한 것이었다. 내가 MainMenu의 어떤 부분을 수정하게 된다면 모든 객체가 영향을 받을 것이고 이는 큰 문제를 만들 수 있는 구조를 만든 것이다.

✅ 내가 생각한 해결 방식

상속

✔ 해결 방안

가장 위에 Menu라는 상위 클래스를 만들고 여기에 공통되는 메서드를 정의한 다음 아래의 메뉴 클래스들에서 상속받아 사용하는 방식이다. 내가 처음 만든 방식과 가장 비슷하고 사실 다를게 별로 없는 방식이다. 그래서 '조금만 더 생각하면 되는 문제인데 왜 그랬을까?'라는 생각을 많이 한다.

⛔ 예상 문제

상속으로 해결하면 '모두 만든다' 라는 것이 문제가 될 것이라고 생각한다. 이유는 '하위 인스턴스가 생성될 때 상위 인스턴스도 같이 생성된다'인데 내가 만약 새롭게 Menu 클래스를 만든다면 하위 클래스들에서 겹치게 사용하는 것이 1개라도 존재한다면 상위클래스에서 만들고자 할 것이다.

이렇게 되면 실제로 MainMenu에서 필요한 메서드가 6개인데 실제 만들어지는 메서드는 8개나 만들어 지는 것이다. 사용하지 않는 메서드를 가지고 있는 클래스를 만드는 것은 객체 지향 원칙의 인터페이스 분리 법칙(ISP)에 위반한다고 알고 있기에 문제가 된다고 생각한다. 만약 알수 없는 이유로 사용자가 메인 메뉴에서 editInformation()을 호출하게 되면 프로그램에서 치명적인 오류가 발생할 것이다.

추상 클래스

✔ 해결 방안

추상 클래스는 좋지 않다고 강사님에게 배웠기에 사용하고 싶은 마음은 없다. 사실 방식 자체는 위의 상속과 다를게 없긴 하다. 왜냐하면 어차피 추상 클래스도 상속관계를 가지기 때문이다. 상위 클래스인 추상 클래스에서 선언한 메서드들을 다시 overriding해주는 것으로 해결이 가능하기는 하다.

⛔ 예상 문제

추상 클레스는 아래에서 모든 메서드를 구현해야 하기에 MainMenu가 Menu라는 추상 클래스를 상속받는다면

package me.smartstore.project.ok;

public class MainMenu extends Menu{
    @Override
    void showMain() { 
        System.out.print("\n\n1. 분류 기준 생성 및 관리\n" +
                "2. 고객 정보 생성 및 관리\n" +
                "3. 요약된 정보 보기\n" +
                "4. 종료\n\n");
    }

    @Override
    String editInformation() { // 필요없는 부분도 구현해 줘야함
        return null;
    }
}

필요없는 부분을 전부 null이나 {}의 형식으로 구현해 줘야하기 때문에 최악의 방식이라고 생각한다.

인터페이스

보통 기능을 나타내는 부분은 '할 수 있는'으로 생각해 인터페이스로 사용하는 것이 좋다고 생각하기에 가장 답안에 가깝다고 생각이 들었다. 그렇기에 가장 많이 고민하고 생각도 많이 한 부분이다. 여러가지 방식을 고려했는데 어떤 방식이 맞는 건지 잘 모르겠지만 나름대로의 답을 내봤다.

공통된 것은 묶어서 구현하기

✔ 해결 방안

처음에는 상속과 비슷한 방식의 인터페이스로 해결하고자 했다. 완전히 겹치는 부분은 하나로 관리하고 몇 개의 클래스에서 공유하는 기능은 따로 인터페이스를 만들자는 생각이었다. 내가 구현했을 때 겹치는 인터페이스는 하나로 관리해서 좋고 따로 만든 인터페이스들도 사용하는 메서들마다 implements를 해주면 그만이니 굉장히 잘 되었다고 생각했다.

⛔ 예상 문제

이 프로그램을 계속 유지 보수하면서 몸집을 불려가면 어떨까? 라는 생각을 하면서 이 방식의 문제점들이 보이기 시작했다. '만약에 Overlap 인터페이스의 일부분만 사용하는 메뉴 클래스를 만들면 어떻하지?' 그러면 'Overlap부분을 다시 쪼개야하나?' 아니면 '그냥 문제점을 감안하고 그냥 만들어야하나?' 거기다가 내가 앞으로 추가할 메뉴에 어떤 기능을 추가할 지는 모르는 것이기에 겹치는 부분이라는 것 자체가 예측이 불가능하다는 것을 알게 되었고 이 방식에 문제가 있다고 생각하게 되었다.

하나의 메서드에 하나씩 구현하기

✔ 해결 방안

각각의 메서드를 각각 인터페이스로 만들어서 하나씩 전부 implements하는 방식으로 구현하는 방식이다. 각각 하나의 인터페이스로 만들면서 '너무 많이 만드는 거 아닌가?'라는 생각을 하기는 했지만 필요한 부분을 각자 알아서 가져다 쓰고 고칠 때도 하나의 인터페이스만 수정하면 되니 유지 보수도 좋다고 생각이 들었다. 그리고 내가 메뉴를 추가로 만들더라도 겹치는 부분을 생각하지 말고 새로 만들어진 메뉴라는 객체가 사용할 기능들과 알맞는 인터페이스를 implements해버리면 되니 추가하는데도 문제가 없기에 가장 좋은 방법이라고 생각한다.

⛔ 예상 문제

위의 생각처럼 하나의 메서드마다 인터페이스를 만들어야하니 만들어지는 객체수가 상당히 많을 것이고 이게 어느 수준을 넘어가면 코드를 이해하는데 문제가 생길 것 같다. 내가 코딩했기에 나에게는 문제가 없고 사용자 또한 문제가 없지만 너무 많아진 객체들로 이루어진 코드를 다른 개발자가 본다면 이해하지 못할 수도 있다는 생각을 한다. 프로그램은 개발자들간의 협업을 통해 만들어지고 유지 보수가 되고 내가 없어진 이후에도 누군가가 이 프로그램을 계속해서 유지 보수할 것이기에 너무 잘게 쪼개는 일은 문제를 점점 키울 것이라고 생각한다.

하나의 기능단위로 나눠서 구현하기

✔ 해결 방안

이번에는 메서드들을 기능을 중심으로 하나로 묶어서 구현하고자 했다. 그러다 보니 이런 기능들이 객체와 비슷하다고 생각하게 되었고 가장 객체지향에 가까운 방식이라고 생각하게 되었다. 그래서 이런 방식이 최근에 공부하고 있는 단일 책임 원칙을 만족한다는 생각이 들기 시작해서 가장 좋은 방식이라고 생각하게 되었다. 이렇게 되면 각각의 기능 단위로 유지 보수하는 것에도 장점을 가질 것이고 다른 개발자와의 협업하는 과정에서 다른 개발자가 이 프로그램을 이해하는 것에도 문제를 느끼지 않게 될 것이라고 생각한다.

🙃 마무리

나는 분명 상속을 배웠고 많이 써왔고 싱글톤도 여러번 만들어 보았기에 상속과 싱글톤을 어느정도 안다고 생각하였다. 사실 '알고 있다'와 '능숙하게 사용한다'의 차이를 크게 느꼈던 것 같다. 내가 배운 것들을 함께 다양한 방법으로 사용하는 것이 쉽지 않다고 생각했지만 문제가 있는지 조차 모르고 넘어갔던 이번 일은 많은 생각을 하게 하였다.

가장 크게 느낀 것은 '코딩을 최대한 많이 해야겠다' 였다. 내가 배운 책이나 강의들에서도 싱글톤과 상속을 연관지어 설명한 부분은 보지 못 했고 나 또한 생각하지 못 했다. 내가 직접 코딩하면서 겪어가며 배워야 한다는 것이다.

그리고 스터디의 중요성 을 다시 생각하게 되었다. 다양한 시각에서 지식을 습득하는 것만 아니라 내가 전혀 생각지도 못한 문제를 알게 된다는 것이 정말 큰 경험이라고 생각한다. 이번 토이 프로젝트를 통해서도 많이 생각하고 많이 배웠는데 스터디를 통해서 다시 한번 더 배웠다고 생각한다.

개인적으로 사실 객체지향의 5대 원리 SOLID라고 외우면서 이런 내용이 있구나 정도만 했는데 이번에 내가 코딩하면서 처음 내가 객체지향적 문제를 만났고 이를 해결하려고 여러 방법을 생각하면서 공부했더니 단일 책임 원칙에 닿았다는 것이 굉장히 짜릿한 경험이었다.

도움 받은 곳들
스프링 입문을 위한 자바 객체 지향의 원리와 이해
자바 메모리
싱글톤 상속
단일 책임 원칙
객체지향의 사실과 오해

profile
코딩 시작

0개의 댓글