본문 바로가기
개발자 꿈나무의 하루/01_Boot Camp

(네이버클라우드 부트캠프) 31일차 - 실습프로젝트(DIP, Stack, Queue)

by kk_naks 2024. 7. 9.

SOLID Pattern

SOLID Pattern 이란 ?

  • 객체지향 프로그래밍 및 설계에서 중요한 다섯가지 원칙
    • 원칙: 하나의 클래스는 하나의 책임
    • 의미: 각 클래스는 하나의 기능만 가져야 한다.
    • 예: 사용자 정보를 처리하는 클래스는 사용자 데이터베이스 접근과 같은 역할만 한다.
    • 2. Open/Closed Principle (OCP, 개방/폐쇄 원칙)
    • 원칙: 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.
    • 의미: 새로운 기능을 추가할 때 기존 코드를 변경하지 않고도 확장할 수 있어야 한다.
    • 예: 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 새로운 클래스를 추가하여 기능을 확장한다.
    • 3. Liskov Substitution Principle (LSP, 리스코프 치환 원칙)
    • 원칙: 서브타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.
    • 의미: 프로그램의 객체는 정확성을 깨트리지 않고 자식 클래스의 인스턴스로 대체할 수 있어야 한다.
    • 예: Rectangle 클래스를 상속받은 Square 클래스는 Rectangle 클래스로 교체해도 문제 없이 작동해야 한다.
    • 4. Interface Segregation Principle (ISP, 인터페이스 분리 원칙)
    • 원칙: 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
    • 의미: 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 분리해야 한다.
    • 예: 거대한 인터페이스 하나보다는 각 기능별로 인터페이스를 여러 개 만드는 것이 좋다.
    • 5. Dependency Inversion Principle (DIP, 의존 역전 원칙)
    • 원칙: 고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.
    • 의미: 구체적인 구현이 아니라 추상화된 인터페이스나 상위 클래스에 의존해야 한다.
    • 예: 데이터베이스 접근 로직을 직접 클래스에 구현하는 대신, 인터페이스를 통해 접근하여 다른 데이터베이스로 쉽게 교체할 수 있도록 한다.
  • 1. Single Responsibility Principle (SRP, 단일 책임 원칙)

SOLID 적용하기

  • 예제로 DIP 개념 파악하기
    alt text
    • 기존 Switch클래스는 FluoLamp클래스만 객체로 사용 가능하다.
    • 고수준 모듈이 저수준 모듈에 의존적이다.
    • 구체적인 구현이 아니라 추상화된 인터페이스나 상위클래스에 의존해야한다.
      alt text
    • 2. 상위클래스로 같은 타입 만들기
    • Lamp의 공통적 속성을 활용(Generic)하여 부모클래스를 생성한다.
    • Lamp의 다형성을 활용하여 모두 제어 가능하다.
    • Swtich는 lamp의 스위치 역할만 가능하다.
    • Switch의 새로운 기능 추가 시, Open/Closed Principle을 충족시키지 못한다.
      alt text
    • 3. 고수준의 상위클래스 만들기
    • 더 높은 수준의 상위클래스로 상속받으면 Switch로 제어가능
    • 상속의 클래스가 복잡해진다.
    • Switch로 제어안해도 되는 코드도 강제로 사용해야한다.
    • Interface Segregation Principle을 충족시키지 못한다.
      alt text
    • 4. ICP(Interface Segregation Principle) 전환
    • Switch클래스에서 사용할 Interface를 생성한다.
    • 제품군(Lamp, Fan)별 세부기능 가진 자식클래스를 생성하고 인터페이스를 구현한다.
    • 새로운 제품군이 들어오면 Switch클래스 내부에서 코드 수정이 필요하다.
    • Dependency Inversion Principle을 충족시키지 못한다.
      alt text
    • 5. DIP(Dependency Inversion Principle) 수정
    • ICP를 충족하는 인터페이스를 생성한다.
    • 인터페이스를 직접적으로 생성하지 않는다( = 의존객체를 만들지 않는다.)
    • 외부에서 객체를 생성해서 대입을 받는다.
  • 1. 기존 클래스 UML

실습프로젝트에 적용해보기

  • 실습 프로젝트 문제점
    • processMenu에서 menuTitle값이 수정되면 App()의 메서드도 역시 수정을 해야한다.
    • DIP를 적용하여 외부에서 값을 대입하면 의존성을 탈피 할 수 있다.
      코드 접기/펴기
      public class App {
      
         String[] mainMenus = new String[] {"회원", "프로젝트", "게시판", "공지사항", "도움말", "종료"};
         UserCommand userCommand = new UserCommand("회원");
         BoardCommand boardCommand = new BoardCommand("게시판");
         BoardCommand noticeCommand = new BoardCommand("공지사항");
         ProjectCommand projectCommand = new ProjectCommand("프로젝트", userCommand.getUserList());
         HelpCommand helpCommand = new HelpCommand();
      
         public static void main(String[] args) {
             new App().execute();
         }
      
         void execute() {
             // 생략 //
         }
      
         void processMenu(String menuTitle) {
             switch (menuTitle) {
             case "회원":
                 userCommand.execute();
                 break;
             case "프로젝트":
                 projectCommand.execute();
                 break;
             case "게시판":
                 boardCommand.execute();
                 break;
             case "공지사항":
                 noticeCommand.execute();
                 break;
             case "도움말":
                 helpCommand.execute();
                 break;
             default:
                 System.out.printf("%s 메뉴의 명령을 처리할 수 없습니다.\n", menuTitle);
             }
         }
      
         void printMenu() {
             // 생략 //
         }
      
         boolean isValidateMenu(int menuNo, String[] menus) {
             // 생략 // 
         }
      
         String getMenuTitle(int menuNo, String[] menus) {
             // 생략 //
         }
      }
  • 1. App() 파일
  • 코드 수정하기
     Map<String, Command> commandMap = new HashMap<>();
    • String과 Command를 받을 수 있는 Map구조를 선언한다.
    • String에는 메뉴명을 대입한다.
    • Command는 인터페이스로 다형성을 이용하여 구현체를 대입한다.
      public App() {
      commandMap.put("회원", new UserCommand("회원"));
      commandMap.put("게시판", new BoardCommand("게시판"));
      commandMap.put("공지사항", new BoardCommand("공지사항"));
      UserCommand userCommand = new UserCommand("회원");
      commandMap.put("프로젝트", new ProjectCommand("회원", userCommand.getUserList()));
      commandMap.put("도움말", new HelpCommand());
      }
    • 2. 생성자 만들기
    • put메서드를 이용하여 {Key : 메뉴명, Value : 구현체}를 대입한다.
      switch (menuTitle) {
        case "회원":
        userCommand.execute();
        break;
        case "프로젝트":
        projectCommand.execute();
        break;
        case "게시판":
        boardCommand.execute();
        break;
        case "공지사항":
        noticeCommand.execute();
        break;
        case "도움말":
        helpCommand.execute();
        break;
      }
    • 3. processMenu 수정하기
    • command 메소드로 넘어가기 위한 switch문을 다음과 같이 수정한다.4. App()에서 List 타입 넘기기
    • void processMenu(String menuTitle) { Command command = commandMap.get(menuTitle); if (command == null) { System.out.printf("%s 메뉴의 명령을 처리할 수 없습니다.\n", menuTitle); return; } command.execute(); }
    • 지금까지 각 command클래스에서 List를 만들어서 객체를 담았다.
    • App()의 List와 Command의 List 다른 객체를 참조하여 상호 호환이 불가하였다.
    • App()에서 List를 생성하고 매개변수로 Comman에 넘긴다.(OCP원칙)
    • App()에는 각 Command에서 사용할 객체를 생성하고, Command에서는 생성자를 통해 객체를 받는다.
    • ArrayList userList = new ArrayList(); LinkedList projectList = new LinkedList(); LinkedList noticeList = new LinkedList(); ArrayList boardList = new ArrayList(); public App() { commandMap.put("회원", new UserCommand("회원", userList)); commandMap.put("게시판", new BoardCommand("게시판", boardList)); commandMap.put("공지사항", new BoardCommand("공지사항", noticeList)); commandMap.put("프로젝트", new ProjectCommand("회원", projectList, userList)); commandMap.put("도움말", new HelpCommand()); } //각 Command에서 생성자도 수정
  • 1. hashMap구조 만들기

Stack과 Queue

Stack과 Queue의 개념

  • 스택과 큐는 컴퓨터 사이언스에서 사용되는 데이터 구조이다.
  • Stack : 스택은 LIFO(Last In, First Out) 구조를 따르며 마지막에 삽입된 요소가 가장 먼저 삭제되는 방식이다. 스택은 마치 한쪽 끝에서만 요소를 넣거나 뺄 수 있다.
    alt text
  • Queue : 큐는 FIFO(First In, First Out) 구조를 따르며 처음에 삽입된 요소가 가장 먼저 삭제되는 방식이다. 큐는 마치 줄을 서서 기다리는 형태이다.
    alt text

LinkedList로 Stack과 Queue구현하기

  • Stack 구현하기
      public class Stack extends LinkedList {
          //push구현하기
          public void push(Object obj) {
              add(obj);
          }
          //pop구현하기
          public Object pop() {
              return remove(size() - 1);
          }
          //empty구현하기
          public boolean isEmpty() {
              return size() == 0;
          }
      }
  • Queue 구현하기
      public class Queue extends LinkedList {
          //push구현하기
          public void offer(Object obj) {
              add(obj);
          }
          //pop구현하기
          public Object poll() {
              return remove(0);
          }
          //empty구현하기
          public boolean isEmpty() {
              return size() == 0;
          }
      }

실습프로젝트에 적용하기

  • Stack 적용하기
    • Stack은 Prompt에서 menuPath를 설정
      alt text
    • 2. App() 수정하기
    • 필드에 Stack menuPath를 설정한다.
    • execute에 "메인" push한다.
    • processMenu에서 menuPath를 매개변수로 넘긴다.
      코드 접기/펴기
      public class App {
        Stack menuPath = new Stack();
        // 이하 필드 생략 // 
        public App() { /* 생략 */  }
      
        public static void main(String[] args) {
            new App().execute();
        }
      
        void execute() {
            menuPath.push("메인");
            /* 생략 */  
        }
      
        void processMenu(String menuTitle) {
            /* 생략 */  
            command.execute(menuPath);
        }
      
        void processMenu(String menuTitle)  { /* 생략 */  }
      
        boolean isValidateMenu(int menuNo, String[] menus)  { /* 생략 */  }
      
        String getMenuTitle(int menuNo, String[] menus)  { /* 생략 */  }
      }
      3. interface, AbstractCommand, HelpCommand 수정하기
    • 각 클래스에 매개변수 선언
      코드 접기/펴기
      //Command interface
      public interface Command {
      void execute(Stack menuPath);
      }
      
      //AbstractCommand의 execute()
      public void execute(Stack menuPath) {
        menuPath.push(menuTitle);
        printMenus();
        while (true) {
            String command = Prompt.input(String.format("메인/%s>", menuTitle));
            if (command.equals("menu")) {
                printMenus();
                continue;
            } else if (command.equals("9")) { // 이전 메뉴 선택
                menuPath.pop();
                break;
            }
            try {
                int menuNo = Integer.parseInt(command);
                String menuName = getMenuTitle(menuNo);
                if (menuName == null) {
                System.out.println("유효한 메뉴 번호가 아닙니다.");
                continue;
                }
                processMenu(menuName);
            } catch (NumberFormatException ex) {
                System.out.println("숫자로 메뉴 번호를 입력하세요.");
            }
        }
      }
      //helpCommand
      public class HelpCommand implements Command {
        public void execute(Stack menuPath) {
            System.out.println("도움말입니다!");
        }
      }
      
      4. Stack 호출 메서드 작성
    • for문을 순회하면서 Stack의 배열을 탐색한다.
    • String타입은 immutable하기 때문에 StringBuffer(필드) 또는 StringBuilder(메서드안)를 사용한다.
    • menuPath 호출이 필요한 App, AbstractCommand에 적용한다.
    • String command = Prompt.input("%s>",getMenuTitle(menuPath)) 부분을 수정한다.
      코드 접기/펴기
      private String getMenuTitle(Stack menuPath){
        StringBuilder strBuilder = new StringBuilder();
        for(int i = 0; i < menuPath.size(); i++){
            if (strBuilder.length() > 0){
                strBuilder.append("/");
            }
            strBuilder.append(menuPath.get(i));
        }
        return strBuilder.toString();
      }
  • 1. 전체 흐름도
  • Queue 적용하기
    • Prompt.input()을 호출시에 Queue에 offer을 실행한다.
    • Queue를 차례대로 출력하는 메소드를 만든다.
    • 검색기록 출력 Command에 출력메소드를 넘긴다.
      alt text
    • 2. input메서드 수정
    • 기존 input메서드를 넘겨받은 매개변수와 키보드 입력변수 합쳐서 Queue에 offer
    • 검색기록이 20개를 넘기면, 기존 앞에서 부터 삭제3. 출력 메서드 작성
    • public static String input(String format, Object... args) { String promptTitle = String.format(format + " ", args); System.out.print(promptTitle); String input = keyboardScanner.nextLine(); if (promptTitle.endsWith(">")) { inputQueue.offer(promptTitle + input); } if (inputQueue.size() > 20) { inputQueue.poll(); } return input; }
    • Queue를 돌면서 들어온 순서대로 출력4. 호출 클래스 작성
    • public static void printHistory() { System.out.println("[명령내역]-----------------------------"); for (int i = 0; i < inputQueue.size(); i++) { System.out.println(inputQueue.get(i)); } System.out.println("-------------------------------------끝"); }
    • HistoryCommand 클래스 생성
    • public class HistoryCommand implements Command{ @Override public void execute(Stack menuTitle){ Prompt.printHistory(); } }
  • 1. 전체흐름도