[계산기 만들기] SOLID 원칙

2024. 9. 9. 20:43·✏ Study/Java

💻 계산기 만들기

 

Step 1. 사칙 연산 수행하는 Calculator 클래스 생성

더하기, 빼기, 나누기, 곱하기 연산을 수행할 수 있는 Calculator 클래스를 만듭니다.

  • Calulator 클래스는 연산을 수행하는 반환 타입이 double인 calculate 메서드를 가지고 있습니다.
  • calculate 메서드는 String 타입의 operator 매개변수를 통해 연산자 매개값을 받습니다.
  • int 타입의 firstNumber, secondNumber 매개변수를 통해 피연산자 값을 받습니다.
  • calculate 메서드는 전달받은 피연산자, 연산자를 사용하여 연산을 수행합니다.

 힌트) if or switch 즉, 제어문을 통해 연산자의 타입을 확인하고 해당하는 타입의 연산을 수행하고 결과값을 반환합니다.


 

switch문으로 구현하였다가, Enhanced switch으로도 구현해 보았습니다.

package week3.hw;

// Step 1. 더하기, 빼기, 나누기, 곱하기 연산을 수행할 수 있는 Calculator 클래스를 만듭니다.
public class Calculator {

    // Step 1. Calculator 클래스는 연산을 수행하는 반환 타입이 double인 calculate 메서드를 가지고 있습니다.
    // calculate 메서드는 String 타입의 operator 매개변수를 통해 연산자 매개값을 받습니다.
    // int 타입의 firstNumber, secondNumber 매개변수를 통해 피연산자 값을 받습니다.
    // calculate 메서드는 전달받은 피연산자, 연산자를 사용하여 연산을 수행합니다.

    public double calculate(String operator, int firstNumber, int secondNumber) {
        // (1) 일반 switch문
//        switch (operator) {
//            case "+":
//                return firstNumber + secondNumber;
//            case "-":
//                return firstNumber - secondNumber;
//            case "/":
//                return ((double) firstNumber / secondNumber);
//            case "*":
//                return firstNumber * secondNumber;
//            default:    // operator 매개변수가 잘못 입력되었을 경우
//                System.out.println("Invalid operaor");
//                throw new IllegalArgumentException("Invalid operator");
//        }

        // (2) Enhanced Switch문
        double result = switch (operator) {
            case "+" -> firstNumber + secondNumber;
            case "-" -> firstNumber - secondNumber;
            case "/" -> ((double)firstNumber / secondNumber);
            case "*" -> firstNumber * secondNumber;
            default -> throw new IllegalArgumentException("Invalid operator");
        };
        return result;
    }
}

 


 

Step 2. Modulo 연산 추가

나머지 연산자(%)를 수행할 수 있게 Calculator 클래스 내부 코드를 변경합니다.

 힌트) 제어문 else if 에 나머지 연산자(%)를 추가합니다.


 

package week3.hw;

public class Calculator {

    public double calculate(String operator, int firstNumber, int secondNumber) {
    
        // (1) 일반 switch문
//        switch (operator) {
//            case "+":
//                return firstNumber + secondNumber;
//            case "-":
//                return firstNumber - secondNumber;
//            case "/":
//                return ((double) firstNumber / secondNumber);
//            case "*":
//                return firstNumber * secondNumber;
//
//            // Step 2. 나머지 연산자(%)를 수행할 수 있게 Calculator 클래스 내부 코드를 변경합니다.
//            case "%":
//                return firstNumber % secondNumber;
//
//            default:    // operator 매개변수가 잘못 입력되었을 경우
//                System.out.println("Invalid operator");
//                throw new IllegalArgumentException("Invalid operator");
//        }

        // (2) Enhanced Switch문
        double result = switch (operator) {
            case "+" -> firstNumber + secondNumber;
            case "-" -> firstNumber - secondNumber;
            case "/" -> ((double)firstNumber / secondNumber);
            case "*" -> firstNumber * secondNumber;
            // Step 2. 나머지 연산자(%)를 수행할 수 있게 Calculator 클래스 내부 코드를 변경합니다.
            case "%" -> firstNumber % secondNumber;
            default -> throw new IllegalArgumentException("Invalid operator");
        };
        return result;
    }
}

 

Step 3. 단일 책임 원칙(SRP), 클래스 간의 관계

AddOperation(더하기), SubstractOperation(빼기), MultiplyOperation(곱하기), DivideOperation(나누기) 연산 클래스를 만든 후 클래스 간의 관계를 고려하여 Calculator 클래스와 관계를 맺습니다.


    • 관계를 맺은 후 필요하다면 Calculator 클래스의 내부 코드를 변경합니다.
      • 나머지 연산자(%) 기능은 제외합니다.
    • Step 2 와 비교하여 어떠한 점이 개선되었는지 스스로 생각해 봅니다.
      • hint. 클래스의 책임(단일 책임 원칙)

 힌트)

  • AddOperation, SubstractOperation, MultiplyOperation, DivideOperation 연산 클래스들을 만듭니다.
  • 각각의 연산 타입에 맞게 operate 메서드를 구현합니다.
  • Calculator 클래스와 포함관계를 맺고 생성자를 통해 각각의 연산 클래스 타입의 필드에 객체를 주입합니다.
  • calculate 메서드에서 직접 연산을 하지 않고 주입받은 연산 클래스들의 operate 메서드를 사용하여 연산을 진행합니다.

더보기

[객체 지향 프로그래밍 및 설계의 5가지 기본 원칙] - SOILD 원칙

 

SRP(단일 책임 원칙), OCP(개방-폐쇄 원칙), LSP(리스코프 치환 원칙), ISP(인터페이스 분리 원칙), DIP(의존 역전 원칙)

코드를 확장하고 유지 보수가 더 쉬워지며, 불필요한 복잡성을 제거해 리팩토링에 소요되는 시간을 줄임으로써 프로젝트 개발의 생산성을 높일 수 있습니다.


1. 단일 책임 원칙 (Single Responsiblity Principle)

 

하나의 클래스(객체)는 단 하나의 책임만 가져야 한다는 원칙입니다. 여기 '책임'은 '기능'을 의미합니다.

 

이는 클래스(객체)가 변경되는 이유는 한 가지여야 한다는 것와 같습니다.

 

즉, 하나의 클래스(객체)는 하나의 기능을 담당하여 하나의 책임을 수행하는데 집중될 수 있도록 클래스를 분리하여 설계하라는 의미입니다.

이때 객체지향 설계의 핵심인 높은 응집도(하나의 책임을 명확히 정의하고 관련 기능을 잘 묶는 것)와 낮은 결합도(다른 클래스와의 의존성을 최소화하는 것)를 고려하여 분리하여야 합니다.

 

SRP의 궁극적인 목적은 프로그램의 유지보수성 향상입니다.

 

너무 많은 책임 분리로 책임이 파편화되어있는 경우 '산탄총 수술(Shotgun Surgery)'로 다시 응집력을 높여줍니다.


2. 개방 - 폐쇄 원칙 (Open - Closed Principle)

 

확장에는 열려있고 수정에는 닫혀있어야 한다는 원칙입니다.

 

즉, 새로운 변경 사항이 발생했을 때 유연하게 코드를 추가함으로써 쉽게 기능을 확장할 수 있게 확장에 대해서 개방적(Open)이고 객체를 직접적으로 수정하는 것은 제한하는 수정에 대해서는 폐쇄적(Closed)으로 설계되어야 한다는 의미입니다.

 

OCP는 추상화를 통한 관계 구축을 권장함으로써, 궁극적으로  다형성과 확장을 가능케 하는 객체지향의 장점인 유연성, 재사용성, 유지보수성 등을 활용할 수 있습니다.


3. 리스코프 치환 원칙 (Liskov Substitution Principle)

 

 서브 타입은 언제나 기반(부모) 타입으로 교체할 수 있어야 한다는 원칙입니다.

 

 즉, 부모 클래스 자리에 자식 클래스를 사용해도 코드의 원래 의도대로 잘 작동해야 한다는 의미입니다.

 

 LSP의 궁극적인 목적은 다형성을 통한 확장성 획득입니다.


4. 인터페이스 분리 원칙 (Interface Segregation Principle)

 

인터페이스를 클라이언트의 목적과 용도에 적절하게 분리하여, 목적과 용도에 적합한 인터페이스 만을 제공하여야 한다는 원칙입니다.

 

이를 위해서 이미 구성한 인터페이스는 변경하지 않습니다.

이후 수정사항이 발생하여도 별도의 인터페이스를 생성하여 해당 인터페이스를 주입받도록 구현하지, 인터페이스를 분리하지 않습니다.

 

SRP이 클래스의 단일 책임을 강조한다면, ISP는 인터페이스의 단일 책임을 강조하는 것입니다.


즉, SRP의 목표가 클래스 분리를 통하여 이루어진다면 ISP는 인터페이스 분리를 통하여 설계하는 원칙입니다.


5. 의존 역전 원칙 (Dependency Inversion Principle)

 

어떤 클래스를 참조해야 하는 상황이면 그 클래스를 직접 참조하는 것이 아니라 그 상위 요소를 참조하라는 원칙입니다.

 

즉, 구체적인 구현 클래스에 의존하지 말고 인터페이스나 추상 클래스에 의존하라는 의미입니다.

 

구체적인 하위 모듈에 클라이언트가 의존하게 되면, 하위 모듈에 변화가 있을 때마다 클라이언트나 상위 모듈의 코드를 수정해야 되는 문제가 발생합니다.

 


  1. AddOperation, SubstractOperation, MultiplyOperation, DivideOperation 연산 클래스를 생성하고, 각각의 연산 타입에 맞게 operate 메서드를 구현합니다. → SRP(Single Responsiblity Principle; 단일 책임 원칙)
  2. Calculator 클래스와 포함관계를 맺습니다.
  3. 생성자를 통해 각각의 연산 클래스 타입의 필드에 객체를 주입합니다.
  4. calculate 메서드에서 직접 연산을 하지 않고, 주입받은 연산 클래스들의 operate 메서드를 사용하여 연산을 진행합니다. → OCP(Open-Closed Principle; 개방-폐쇄 원칙)
package week3.hw;

public class AddOperation {
    // [Step 3]
    // 1. 각각의 연산 타입에 맞게 operate 메서드를 구현합니다.
    public double operate(int firstNumber, int secondNumber) {
        return firstNumber + secondNumber;
    }
}
package week3.hw;

public class SubstractOperation {
    // [Step 3]
    // 1. 각각의 연산 타입에 맞게 operate 메서드를 구현합니다.
    public double operate(int firstNumber, int secondNumber) {
        return firstNumber - secondNumber;
    }
}
package week3.hw;

public class MultiplyOperation {
    // [Step 3]
    // 1. 각각의 연산 타입에 맞게 operate 메서드를 구현합니다.
    public double operate(int firstNumber, int secondNumber) {
        return firstNumber * secondNumber;
    }
}
package week3.hw;

public class DivideOperation {
    // [Step 3]
    // 1. 각각의 연산 타입에 맞게 operate 메서드를 구현합니다.
    public double operate(int firstNumber, int secondNumber) {
        return (double) firstNumber / secondNumber;
    }
}
package week3.hw;

public class Calculator {

    // [Step 3] 클래스 다이어그램
    // 2. Calculator 클래스와 포함관계를 맺습니다.
    public AddOperation addOperation;
    public SubstractOperation substractOperation;
    public MultiplyOperation multiplyOperation;
    public DivideOperation divideOperation;

    // [Step 3]
    // 3. 생성자를 통해 각각의 연산 클래스 타입의 필드에 객체를 주입합니다.
    // Constructor
    public Calculator(
            AddOperation addOperation,
            SubstractOperation substractOperation,
            MultiplyOperation multiplyOperation,
            DivideOperation divideOperation) {
        this.addOperation = addOperation;
        this.substractOperation = substractOperation;
        this.multiplyOperation = multiplyOperation;
        this.divideOperation = divideOperation;
    }


    // [Step 3]
    // 4. calculate 메서드에서 직접 연산을 하지 않고, 주입받은 연산 클래스들의 operate 메서드를 사용하여 연산을 진행합니다.
    // Method
    public double calculate(String operator, int firstNumber, int secondNumber) {
        // (1) 일반 switch문
//        switch (operator) {
//            case "+":
//                return addOperation.operate(firstNumber, secondNumber);
//            case "-":
//                return substractOperation.operate(firstNumber, secondNumber);
//            case "/":
//                return divideOperation.operate(firstNumber, secondNumber);
//            case "*":
//                return multiplyOperation.operate(firstNumber, secondNumber);
//
//            // [Step 3] 나머지 연산자(%) 기능은 제외합니다.
//
//            default:    // operator 매개변수가 잘못 입력되었을 경우
//                System.out.println("Invalid operator");
//                throw new IllegalArgumentException("Invalid operator");
//        }

        // (2) Enhanced Switch문
        double result = switch (operator) {
            case "+" -> addOperation.operate(firstNumber, secondNumber);
            case "-" -> substractOperation.operate(firstNumber, secondNumber);
            case "/" -> divideOperation.operate(firstNumber, secondNumber);
            case "*" -> multiplyOperation.operate(firstNumber, secondNumber);
            // [Step 3] 나머지 연산자(%) 기능은 제외합니다.
            default -> throw new IllegalArgumentException("Invalid operator");
        };
        return result;
    }
}

 

 

Step 4. 추상화, 다형성

AddOperation(더하기), SubstractOperation(빼기), MultiplyOperation(곱하기), DivideOperation(나누기) 연산 클래스들을 AbstractOperation(추상 클래스)를 사용하여 추상화하고 Calculator 클래스의 내부 코드를 변경합니다.

    • Step 3 와 비교해서 어떠한 점이 개선되었는지 스스로 생각해 봅니다.
      • hint. 클래스 간의 결합도, 의존성(의존성 역전 원칙)
    • ❗️Calculator의 calculate 메서드의 매개변수가 변경되었습니다.

 힌트)

  • AbstractOperation 추상 클래스를 만들고 operate 추상 메서드를 만듭니다.
  • AddOperation, SubstractOperation, MultiplyOperation, DivideOperation 클래스들은 AbstractOperation 클래스를 상속받고 각각의 연산 타입에 맞게 operate를 오버라이딩 합니다.
  • Calculator 클래스는 4개의 연산 클래스들이 상속받고 있는 AbstractOperation 클래스만을 포함합니다.
  • 생성자 혹은 Setter를 사용하여 연산을 수행할 연산 클래스의 객체를 AbstractOperation 클래스 타입의 필드에 주입합니다.(다형성)
  • calculate 메서드에서는 더 이상 연산자 타입을 받아 구분할 필요 없이 주입 받은 연산 클래스의 operate 메서드를 통해 바로 연산을 수행합니다.

  1. AbstractOperation 추상 클래스를 만들고 operate 추상 메서드를 만듭니다.
  2. AddOperation, SubstractOperation, MultiplyOperation, DivideOperation 클래스들은 AbstractOperation 클래스를 상속받고 각각의 연산 타입에 맞게 operate를 오버라이딩 합니다.
  3. Calculator 클래스는 4개의 연산 클래스들이 상속받고 있는 AbstractOperation 클래스만을 포함합니다. → DIP(Dependency Inversion Principle; 의존 역전 원칙)
  4. 생성자 혹은 Setter를 사용하여 연산을 수행할 연산 클래스의 객체를 AbstractOperation 클래스 타입의 필드에 주입합니다.(다형성) → OCP(Open-Closed Principle; 개방-폐쇄 원칙)
  5. calculate 메서드에서는 더 이상 연산자 타입을 받아 구분할 필요 없이 주입 받은 연산 클래스의 operate 메서드를 통해 바로 연산을 수행합니다. → SRP(Single Responsiblity Principle; 단일 책임 원칙)
package week3.hw;

// [Step 4]
// 1. AbstractOperation 추상 클래스를 만들고 operate 추상 메서드를 만듭니다.
public abstract class AbstractOperation {
    public abstract double operate(int firstNumber, int secondNumber);
}
package week3.hw;

// [Step 4] 클래스 다이어그램
// 2. AbstractOperation 클래스를 상속받고 각각의 연산 타입에 맞게 operate를 오버라이딩
public class AddOperation extends AbstractOperation {
    @Override
    public double operate(int firstNumber, int secondNumber) {
        return firstNumber + secondNumber;
    }
}
package week3.hw;

// [Step 4] 클래스 다이어그램
// 2. AbstractOperation 클래스를 상속받고 각각의 연산 타입에 맞게 operate를 오버라이딩
public class SubstractOperation extends AbstractOperation {
    @Override
    public double operate(int firstNumber, int secondNumber) {
        return firstNumber - secondNumber;
    }
}
package week3.hw;

// [Step 4] 클래스 다이어그램
// 2. AbstractOperation 클래스를 상속받고 각각의 연산 타입에 맞게 operate를 오버라이딩
public class MultiplyOperation extends AbstractOperation {
    @Override
    public double operate(int firstNumber, int secondNumber) {
        return firstNumber * secondNumber;
    }
}
package week3.hw;

// [Step 4] 클래스 다이어그램
// 2. AbstractOperation 클래스를 상속받고 각각의 연산 타입에 맞게 operate를 오버라이딩
public class DivideOperation extends AbstractOperation {
    @Override
    public double operate(int firstNumber, int secondNumber) {
        return (double) firstNumber / secondNumber;
    }
}
package week3.hw;

public class Calculator {
    // Field //
    // [Step 4] 클래스 다이어그램
    // 3. Calculator 클래스는 4개의 연산 클래스들이 상속받고 있는 AbstractOperation 클래스만을 포함합니다.
    public AbstractOperation operation;

    // Constructor //
    // [Step 4]
    // 4. 생성자 혹은 Setter를 사용하여 연산을 수행할 연산 클래스의 객체를 AbstractOperation 클래스 타입의 필드에 주입합니다.(다형성)
    public Calculator(AbstractOperation operation) {
        setOperation(operation);
    }

    // Method //
    // [Step 4]
    // 4. 생성자 혹은 Setter를 사용하여 연산을 수행할 연산 클래스의 객체를 AbstractOperation 클래스 타입의 필드에 주입합니다.(다형성)
    // Setter
    void setOperation(AbstractOperation operation) {
        this.operation = operation;
    }

    // [Step 4] Calculator의 calculate 메서드의 매개변수 변경
    // 5. calculate 메서드에서는 더 이상 연산자 타입을 받아 구분할 필요 없이 주입 받은 연산 클래스의 operate 메서드를 통해 바로 연산을 수행합니다.
    public double calculate(int firstNumber, int secondNumber) {
        return operation.operate(firstNumber, secondNumber);
    }
}
package week3.hw;

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print("Enter the operation: ");
        String op = scanner.nextLine();
        System.out.print("Enter the first number: ");
        int num1 = scanner.nextInt();
        System.out.print("Enter the second number: ");
        int num2 = scanner.nextInt();

        switch (op) {
            case "+":
                AbstractOperation addOperation = new AddOperation();
                Calculator addCal = new Calculator(addOperation);
                System.out.println("[add] " + addCal.calculate(num1, num2));
                break;
            case "-":
                AbstractOperation substractOperation = new SubstractOperation();
                Calculator subCal = new Calculator(substractOperation);
                System.out.println("[sub] " + subCal.calculate(num1, num2));
                break;
            case "/":
                AbstractOperation divideOperation = new DivideOperation();
                Calculator divCal = new Calculator(divideOperation);
                System.out.println("[div] " + divCal.calculate(num1, num2));
                break;
            case "*":
                AbstractOperation multiplyOperation = new MultiplyOperation();
                Calculator mulCal = new Calculator(multiplyOperation);
                System.out.println("[mul] " + mulCal.calculate(num1, num2));
                break;
            default:    // operator 매개변수가 잘못 입력되었을 경우, 예외 처리
                System.out.println("Invalid operator");
                throw new IllegalArgumentException("Invalid operator");
        }
    }
}

 

저작자표시 비영리 변경금지 (새창열림)

'✏ Study > Java' 카테고리의 다른 글

[객체 지향 프로그래밍 및 설계의 5가지 기본 원칙] - SOILD 원칙  (0) 2024.09.09
'✏ Study/Java' 카테고리의 다른 글
  • [객체 지향 프로그래밍 및 설계의 5가지 기본 원칙] - SOILD 원칙
레이제로
레이제로
  • 레이제로
    한 걸음
    레이제로
  • 전체
    오늘
    어제
    • 분류 전체보기 (11)
      • ✍🏻 TIL (4)
        • 혼자 공부하는 컴퓨터 구조+운영체제 (4)
      • ✏ Study (3)
        • Problem Solving (1)
        • Spring (0)
        • Java (2)
        • Data Structure (0)
        • Algorithm (0)
      • 💻 Code Kata (4)
        • Algorithm (4)
        • SQL (0)
      • 📃etc. (0)
        • JS (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 개발 공부 외 이모저모
  • 공지사항

  • 인기 글

  • 태그

    Solid 원칙
    나눗셈
    length
    FastIO
    나머지
    프로그래머스
    length()
    size()
    음수
    코드카타
    java
    OOP
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
레이제로
[계산기 만들기] SOLID 원칙
상단으로

티스토리툴바