Java

객체지향적 설계를 위한 5가지 SOLID 원칙

Big Iron 2023. 2. 24. 17:06

단일 책임 원칙 위반 증상

  • 기능 변경이 발생했을 때 연쇄적으로 코드를 수정해야 합니다.
  • 기능이 너무 복잡해서 재사용하기 어렵습니다.
  • 메서드의 크기가 비대해 집니다

해결 방법

  • 기능별로 클래스를 구현한다.
  • 상속을 통해 각 클래스들을 이용한다.
public class AddOperation {
    public int operate(int firstNumber, int secondNumber){
        return firstNumber + secondNumber;
    }
}// 덧셈 클래스 구현 

public class SubstractOperation {
    public int operate(int firstNumber, int secondNumber){
        return firstNumber - secondNumber;
    }
}// 뺄셈 클래스 구현

public class MultiplyOperation {
    public int operate(int firstNumber, int secondNumber){
        return firstNumber * secondNumber;
    }
}// 곱셈 클래스 구현

public class DivideOperation {
    public int operate(int firstNumber, int secondNumber){
        return firstNumber / secondNumber;
    }
}// 나눗셈 클래스 구현

public class Calculator {
    private AddOperation addOperation;
    private SubstractOperation substractOperation;
    private MultiplyOperation multiplyOperation;
    private DivideOperation divideOperation;
    // 클래스를 변수의 타입으로 선언

    public Calculator(AddOperation addOperation, SubstractOperation substractOperation, MultiplyOperation multiplyOperation, DivideOperation divideOperation) {
        this.addOperation = addOperation;
        this.substractOperation = substractOperation;
        this.multiplyOperation = multiplyOperation;
        this.divideOperation = divideOperation;
    } // 생성자

    public int calculate(String operator, int firstNumber, int secondNumber) {
        int answer = 0;

        if(operator.equals("+")){
            answer = addOperation.operate(firstNumber, secondNumber);
        }else if(operator.equals("-")){
            answer = substractOperation.operate(firstNumber, secondNumber);
        }else if(operator.equals("*")){
            answer = multiplyOperation.operate(firstNumber, secondNumber);
        }else if(operator.equals("/")){
            answer = divideOperation.operate(firstNumber, secondNumber);
        } // 연산자 판별

        return answer;
    }
}

public class Client {
    public static void main(String[] args) {
        Calculator calculator = new Calculator(
                new AddOperation(),
                new SubstractOperation(),
                new MultiplyOperation(),
                new DivideOperation()
        );

        int firNum = 140;
        int secNum = 60;

        String operator = "+";
        int answer = calculator.calculate(operator, firNum, secNum);
        System.out.println(operator + " answer = " + answer);

        operator = "-";
        answer = calculator.calculate(operator, firNum, secNum);
        System.out.println(operator + " answer = " + answer);

        operator = "*";
        answer = calculator.calculate(operator, firNum, secNum);
        System.out.println(operator + " answer = " + answer);

        operator = "/";
        answer = calculator.calculate(operator, firNum, secNum);
        System.out.println(operator + " answer = " + answer);
    }
}
  1. 각각의 연산 클래스 구현.
  2. 클래스를 변수의 타입으로 선언.
  3. 생성자를 통해 클라이언트에서 파라미터 사용 준비
  4. 연산자 판별 - 값 출력

개방-폐쇄 원칙 위반 증상

  • 기능 확장을 할 때마다 기존코드를 수정해야 한다.
  • if else 블록이 자주 등장합니다.
  • 기능 확장을 위한 코드 수정을 할 때 여러 클래스에서 다발적으로 진행이 됩니다.

해결 방법

  • 변화되는 부분을 추상화해서 변화를 고정 시킨다.
  • 기능이 추가될 때 클래스의 상속을 통해서 하위 클래스에서 기능을 구현 하도록 한다.
  • 기존코드를 수정하지 않아도 객체 상속의 다형성 원리에 의해 기능이 확장 되도록 합니다.
  • 연산 클래스는 추상화된 부모 클래스를 상속받아 기능별로 구현합니다.
    ``
    public abstract class AbstractOperation {
    public abstract int operate(int firstNumber, int secondNumber);
    }// 고정된 부분 - 추상 메서드 선언.

public class AddOperation extends AbstractOperation {
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber + secondNumber;
}
} // 추상메서드 오버라이드 - 덧셈 구현

public class SubstractOperation extends AbstractOperation{
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber - secondNumber;
}
}// 추상메서드 오버라이드 - 뺄셈 구현

public class MultiplyOperation extends AbstractOperation{
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber * secondNumber;
}
}// 추상메서드 오버라이드 - 곱셈 구현

public class DivideOperation extends AbstractOperation{
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber / secondNumber;
}
}// 추상메서드 오버라이드 - 나눗셈 구현

public class Calculator {

// 추상화된 부모 클래스를 포함관계로 사용한다.
private AbstractOperation operation;

public void setOperation(AbstractOperation operation) {
    this.operation = operation;
}

// 연산 기능을 추상화된 부모클래스에 의존하여 처리한다.
public int calculate(int firstNumber, int secondNumber) {
    return operation.operate(firstNumber, secondNumber);
}

}

public class Client {
public static void main(String[] args) {

    Calculator calculator = new Calculator();

    int firNum = 140;
    int secNum = 60;

    calculator.setOperation(new AddOperation());
    int answer = calculator.calculate(firNum, secNum);
    System.out.println(" + answer = " + answer);

    calculator.setOperation(new SubstractOperation());
    answer = calculator.calculate(firNum, secNum);
    System.out.println(" - answer = " + answer);

    calculator.setOperation(new MultiplyOperation());
    answer = calculator.calculate(firNum, secNum);
    System.out.println(" * answer = " + answer);

    calculator.setOperation(new DivideOperation());
    answer = calculator.calculate(firNum, secNum);
    System.out.println(" / answer = " + answer);
}

}


1.  각 클래스별 공통 부분을 추상클래스로 선언.
2.  각 클래스에 추상메서드 오버라이드 사용 - 연산식
3.  추상화된 부모클래스 변수 타입으로 선언.
4.  부모클래스에 의존해 기능 처리.

---

### 리스코프 치환 원칙 위반 증상
- 객체의 타입을 확인.(instanceof)
- 자식 클래스명이 연관되거나 의존성이 있는 클래스에서 자주 발생.

### 해결 방법
- 부모 와 자식 클래스 사이의 행위가 일관성이 있도록 추상화를 좀 더 정교하게 구현합니다.
- 연산 기능을 추상화한 부모 클래스에 피연산자 값의 유효성 검사를 진행하는 메서드를 추가해 줍니다.
- 계산기 클래스에서는 이 메서드를 사용하여 유효성 검사를 진행하고 이 유효성 검사가 필요한 자식 클래스에서는 이 추가된 유효성 검사 조건을 구체화 합니다.

public abstract class AbstractOperation {
public abstract int operate(int firstNumber, int secondNumber);
public boolean isInvalid(int firstNumber, int secondNumber) {
return false;
}
} // 확인용 isInvalid 메서드 선언 / 공통 부분 추상 메서드

public class AddOperation extends AbstractOperation {
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber + secondNumber;
}
}// 추상클래스 오버라이드 - 연산식 반환

public class SubstractOperation extends AbstractOperation {
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber - secondNumber;
}
}// 추상클래스 오버라이드 - 연산식 반환

public class MultiplyOperation extends AbstractOperation {
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber * secondNumber;
}
}// 추상클래스 오버라이드 - 연산식 반환

public class DivideOperation extends AbstractOperation {
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber / secondNumber;
}// 추상클래스 오버라이드 - 연산식 반환

@Override
public boolean isInvalid(int firstNumber, int secondNumber) {
    if (secondNumber == 0) {
        return true;
    } else {
        return false;
    }
}// 유효성 검사

}

public class Calculator {
// 연산 기능을 추상화된 부모클래스에 의존하여 처리한다.
public int calculate(AbstractOperation operation, int firstNumber, int secondNumber) {
// instanceof 로 DivideOperation 를 따로 구분하지 않고 처리 가능
if(operation.isInvalid(firstNumber, secondNumber)){
return -99999;
}
return operation.operate(firstNumber, secondNumber);
}
}

public class Client {
public static void main(String[] args) {

    Calculator calculator = new Calculator();

    int firNum = 140;
    int secNum = 60;

    int answer = calculator.calculate(new AddOperation(), firNum, secNum);
    System.out.println(" + answer = " + answer);

    answer = calculator.calculate(new SubstractOperation(), firNum, secNum);
    System.out.println(" - answer = " + answer);

    answer = calculator.calculate(new MultiplyOperation(), firNum, secNum);
    System.out.println(" * answer = " + answer);

    answer = calculator.calculate(new DivideOperation(), firNum, secNum);
    System.out.println(" / answer = " + answer);
}

}


1.  확인용 메서드 선언.
2.  공통 부분 추상 클래스 사용
3.  각 클래스별 오버라이드 - 연산
4.  유효성 검사

---

### 인터페이스 분리 원칙 증상
- 필요하지 않은 기능을 강제로 구현해야하는 상황이 발생.
- 필요하지 않은 혹은 사용 못 하는 기능이 강제로 구현되어 사용하지 못하도록 예외처리를 해야하는 상황이 발생할 수 있음.

### 해결 방법
- 필요하지 않은 기능을 강제로 구현하지 않도록 인터페이스를 분리.
- 연산 결과를 보여주는 방법마다 인터페이스 구현.

public abstract class AbstractOperation {
public abstract int operate(int firstNumber, int secondNumber);
public abstract String getOperator();
} // 공통 부분 추상메서드 선언.

public class AddOperation extends AbstractOperation {
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber + secondNumber;
}

@Override
public String getOperator() {
    return "+";
}

}// 추상메서드 오버라이드

public class SubstractOperation extends AbstractOperation {
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber - secondNumber;
}

@Override
public String getOperator() {
    return "-";
}

}// 추상메서드 오버라이드

public class MultiplyOperation extends AbstractOperation {
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber * secondNumber;
}

@Override
public String getOperator() {
    return "*";
}

}// 추상메서드 오버라이드

public class DivideOperation extends AbstractOperation {
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber / secondNumber;
}

@Override
public String getOperator() {
    return "/";
}

}// 추상메서드 오버라이드

public interface DisplayResult {
public abstract void displayResult(AbstractOperation operation, int firstNumber, int secondNumber);
} // 각각의 기능에 맞는 인터페이스 선언.

public interface DisplayWithOperator {
public abstract void displayResultWithOperator(AbstractOperation operation, int firstNumber, int secondNumber);
}

// 연산 결과만 출력
public class DisplayTypeA extends Calculator implements DisplayResult{
@Override
public void displayResult(AbstractOperation operation, int firstNumber, int secondNumber) {
int answer = calculate(operation, firstNumber, secondNumber);
System.out.println(answer);
}
}

// 연산 과정을 포함한 출력
public class DisplayTypeB extends Calculator implements DisplayWithOperator{
@Override
public void displayResultWithOperator(AbstractOperation operation, int firstNumber, int secondNumber) {
int answer = calculate(operation, firstNumber, secondNumber);
String operator = operation.getOperator();
System.out.println(firstNumber + " " + operator + " " + secondNumber + " = " + answer);
}
}

public class Calculator {
public int calculate(AbstractOperation operation, int firstNumber, int secondNumber){
return operation.operate(firstNumber, secondNumber);
}
}

public class Client {
public static void main(String[] args) throws Exception {
int firNum = 140;
int secNum = 60;

    // 연산 결과만 출력
    DisplayTypeA displayTypeA = new DisplayTypeA();
    displayTypeA.displayResult(new AddOperation(), firNum, secNum);

    // 연산 과정까지 출력
    DisplayTypeB displayTypeB = new DisplayTypeB();
    displayTypeB.displayResultWithOperator(new AddOperation(), firNum, secNum);
}

}


1.  공통 부분 추상 메서드.
2.  필요한 기능이 있는 인터페이스 구현.
3.  각 클래스별 인터페이스 상속

---

### 의존성 역전 원칙을 위반 증상
- 저수준 모듈에서 변경이 발생되면 고수준 모듈에 수정사항이 발생.

### 해결 방법
- 고수준 모듈의 변화되는 부분을 추상화한다.
- 저수준 모듈을 추상화에 의존시킨다.
- 계산기 클래스에 추상화된 부모 클래스를 포함시킨다.
- 연산 클래스는 추상화된 부모 클래스를 상속받아 기능별로 구현.

public abstract class AbstractOperation {
public abstract int operate(int firstNumber, int secondNumber);
}// 공통 부분 추상클래스 선언.

public class AddOperation extends AbstractOperation {
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber + secondNumber;
}
} // 각 클래스별 추상메서드 오버라이드 연산

public class SubstractOperation extends AbstractOperation {
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber - secondNumber;
}
} // 각 클래스별 추상메서드 오버라이드 연산

public class MultiplyOperation extends AbstractOperation {
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber * secondNumber;
}
}// 각 클래스별 추상메서드 오버라이드 연산

public class DivideOperation extends AbstractOperation {
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber / secondNumber;
}
}// 각 클래스별 추상메서드 오버라이드 연산

public class Calculator {

// 추상화된 부모 클래스를 포함관계로 사용한다.
private AbstractOperation operation;

public void setOperation(AbstractOperation operation) {
    this.operation = operation;
}

// 연산 기능을 추상화된 부모클래스에 의존하여 처리한다.
public int calculate(int firstNumber, int secondNumber) {
    return operation.operate(firstNumber, secondNumber);
}

}

public class Client {
public static void main(String[] args) {

    Calculator calculator = new Calculator();

    int firNum = 140;
    int secNum = 60;

    calculator.setOperation(new AddOperation());
    int answer = calculator.calculate(firNum, secNum);
    System.out.println(" + answer = " + answer);

    calculator.setOperation(new SubstractOperation());
    answer = calculator.calculate(firNum, secNum);
    System.out.println(" - answer = " + answer);

    calculator.setOperation(new MultiplyOperation());
    answer = calculator.calculate(firNum, secNum);
    System.out.println(" * answer = " + answer);

    calculator.setOperation(new DivideOperation());
    answer = calculator.calculate(firNum, secNum);
    System.out.println(" / answer = " + answer);
}

}

```

  1. 공통 부분 추상클래스 선언
  2. 각 클래스별 상속,오버라이딩 연산 구현
  3. 부모 클래스 타입 선언.
  4. 3번을 이용한 값 출력.