[객체지향] SOLID 원칙이란?

rokong.me·2023년 1월 29일
0

객체지향

목록 보기
1/2

1. SOLID란?

  • SOLIDSRP, OCP, LSP, ISP, DIP의 앞 글자들을 딴 용어이다.
  • SRP (Single Responsibility Principle) - 단일 책임 원칙
  • OCP (Open-Closed Principle) - 개방 폐쇄 원칙
  • LSP (Liscov Subsitution Principle) - 리스코프 치환 원칙
  • ISP (Interface Segregation Principle) - 인터페이스 분리 원칙
  • DIP (Dependency Inversion Principle) - 의존 관계 역전 원칙

2. 객체 지향 설계를 잘하면 얻는 이점

  • 유지보수가 쉬워진다.
  • 확장성이 좋아진다.
  • 재사용성이 상승한다.
  • 자연적인 모델링이 가능해진다.
  • 클래스 단위로 모듈화 해서 대형 프로젝트 개발이 용이해진다.

3-1. SRP (Single Responsibility Principle)

한 클래스는 하나의 책임을 가져야 한다.
클래스를 변경하는 이유는 단 하나여야 한다.
이를 지키지 않으면, 한 책임의 변경에 의해 다른 책임과 관련된 코드에 영향을 미칠 수 있음

// 잘못된 예제 -> '한 클래스는 하나의 책임을 가져야 한다' 위반
interface Employee_X {
    // 경리팀 업무
    void calculatePay();

    // 인사팀 업무
    void reportHours();

    // DB 관리자 업무
    void save();
}

// 올바른 예제 -> '한 클래스는 하나의 책임을 가져야 한다'
interface Employee_O {
    void do_something();
}

// 인사 업무 클래스
class HourReporter implements Employee_O {

    @Override
    public void do_something() {
        System.out.println("HourReporter.do_something");
    }
}

// 경리 업무 클래스
class PayCalculator implements Employee_O {

    @Override
    public void do_something() {
        System.out.println("PayCalculator.do_something");
    }
}

// DB 관리자 업무 클래스
class EmployeeSaver implements Employee_O {

    @Override
    public void do_something() {
        System.out.println("EmployeeSaver.do_something");
    }
}

3-2. OCP (Open-Closed Principle)

클래스 확장에는 개방적이어야 하고, 변경에는 폐쇄적이어야 한다.
즉, 기존 코드를 변경하지 않고 기능을 수정하거나 추가할 수 있도록 설계해야 함.
이를 지키지 않으면, instanceof와 같은 연산자를 사용하거나, 다운 캐스팅이 발생한다.

class BadDeveloper {
    public void eatSleepDevelopRest() {
        System.out.println("eating");
        System.out.println("sleeping");
        System.out.println("developing");
        System.out.println("restring");
    }
}

// 만약 다른 일을 추가해야 한다면?
// -> 기존 코드를 변경하면서 기능을 추가해야한다.
class BadDeveloper {
    public void eatSleepDevelopSomeWorksRest() {
        System.out.println("eating");
        System.out.println("sleeping");
        System.out.println("developing");
        System.out.println("some job 1");
        System.out.println("some job 2");
        System.out.println("restring");
    }
}

class GoodDeveloper {
    void eat() {
        System.out.println("eating");
    }

    void sleep() {
        System.out.println("sleeping");
    }

    void develop() {
        System.out.println("developing");
    }

    void rest() {
        System.out.println("resting");
    }
}

// 만약 다른 일을 추가해야 한다면?
// -> 기존 코드를 변경하지 않고 기능을 추가할 수 있다.
class GoodDeveloper {
    void eat() {
        System.out.println("eating");
    }

    void sleep() {
        System.out.println("sleeping");
    }

    void develop() {
        System.out.println("developing");
    }

    void some_job_1() {
        System.out.println("some job 1");
    }

    void some_job_2() {
        System.out.println("some job 2");
    }
    
    void rest() {
        System.out.println("resting");
    }
}

3-3. LSP (Liscov Subsitution Principle)

하위 타입 객체는 상위 타입 객체에서 가능한 행위를 수행할 수 있어야 함.
즉, 상위 타입 객체를 하위 타입 객체로 치환해도 정상적으로 동작해야 함.
상속관계에서는 꼭 일반화 관계 (IS-A)가 성립해야 한다는 의미 (일관성 있는 관계인지)
상속관계가 아닌 클래스들을 상속관계로 설정하면 이 원칙이 위배됨. (재사용 목적으로 사용하는 경우)
리스코프 치환 원칙을 지키지 않으면 개방 폐쇄 원칙을 위반하게 된다.
따라서 상속 관계를 잘 정의하여 LSP 원칙이 위배되지 않도록 설계해야 한다.

// LSP를 위반한 예제
class Rectangle {
    private int width;
    private int height;

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(getWidth());
    }

    @Override
    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(getHeight());
    }
}


public class Main {
    public static void main(String[] args) {
        // 하위 타입 객체는 상위 타입 객체에서 가능한 행위를 수행할 수 없음!
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(10);
        rectangle.setHeight(5);

        System.out.println("rectangle = " + rectangle.getArea());

        Rectangle square = new Square();
        square.setWidth(10);
        square.setHeight(5);

        System.out.println("rectangle = " + square.getArea()); // 25 출력!
    }
}


// LSP를 준수한 코드
// Shape 클래스
class Shape {
    private int width;
    private int height;

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

// 직사각형 클래스
class Rectangle extends Shape {
    public Rectangle(int width, int height) {
        setWidth(width);
        setHeight(height);
    }
}

// 정사각형 클래스
class Square extends Shape {
    public Square(int length) {
        setWidth(length);
        setHeight(length);
    }
}

public class Main {
    public static void main(String[] args) {
        // 이제 더이상 Rectangle과 Square가 상속 관계가 아니므로, 리스코프 치환 원칙의 영향에서 벗어남.
        Shape rectangle = new Rectangle(10, 5);
        Shape square = new Square(5);
        
        System.out.println(rectangle.getArea());
        System.out.println(square.getArea());
    }
}

3-4. ISP (Interface Segregation Principle)

클라이언트는 자신이 사용하는 메소드에만 의존해야 한다는 원칙.
한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 않아야 함.
하나의 통상적인 인터페이스보다는 차라리 여러 개의 세부적인 인터페이스가 나음.
인터페이스는 해당 인터페이스를 사용하는 클라이언트를 기준으로 잘게 분리되어야함.

// ISP을 위반한 예제 -> 인터페이스 분리가 안되어있음.
interface AllInOneDevice {
    void print();

    void copy();

    void fax();
}

class SmartMachine implements AllInOneDevice {
    @Override
    public void print() {
        System.out.println("print");
    }

    @Override
    public void copy() {
        System.out.println("copy");
    }

    @Override
    public void fax() {
        System.out.println("fax");
    }
}

class PrinterMachine implements AllInOneDevice {
    @Override
    public void print() {
        System.out.println("print");
    }

    @Override
    public void copy() {
        throw new UnsupportedOperationException();
    }

    @Override
    public void fax() {
        throw new UnsupportedOperationException();
    }
}

// ISP를 준수한 예제 -> 인터페이스를 분리
interface PrinterDevice {
    void print();
}

interface CopyDevice {
    void copy();
}

interface FaxDevice {
    void fax();
}

class SmartMachine implements PrinterDevice, CopyDevice, FaxDevice {
    @Override
    public void print() {
        System.out.println("print");
    }

    @Override
    public void copy() {
        System.out.println("copy");
    }

    @Override
    public void fax() {
        System.out.println("fax");
    }
}

class PrinterMachine implements PrinterDevice {
	@Override
    public void print() {
    	System.out.println("print");
    }
}

3-5. DIP (Dependency Inversion Principle)

의존 관계를 맺을 때, 변하기 쉬운 것 (구체적인 것) 보다는 변하기 어려운 것 (추상적인 것)에 의존해야함.
구체화된 클래스에 의존하기 보다는 추상 클래스나 인터페이스에 의존해야 한다는 뜻.
즉, 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 됨.
저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 함.
저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요없는 형태가 이상적

// DIP를 위반한 예제

// 한손검 객체
class OneHandSword {
    private final String name;
    private final int damage;

    public OneHandSword(String name, int damage) {
        this.name = name;
        this.damage = damage;
    }

    public int attack() {
        return damage;
    }

    @Override
    public String toString() {
        return "OneHandSword{" +
                "name='" + name + '\'' +
                '}';
    }
}

// 한손검을 제외한 다른 무기를 사용하려면 Character의 코드를 변경해야 한다. OneHandSword에 의존성을 가진다.
class Character {
    private final String name;
    private int health;
    private OneHandSword weapon;

    public Character(String name, int health, OneHandSword weapon) {
        this.name = name;
        this.health = health;
        this.weapon = weapon;
    }

    public int attack() {
        return weapon.attack();
    }

    public void damaged(int amount) {
        health -= amount;
    }

    public void changeWeapon(OneHandSword weapon) {
        this.weapon = weapon;
    }

    @Override
    public String toString() {
        return "Character{" +
                "name='" + name + '\'' +
                '}';
    }
}

// DIP를 준수한 예제
// Attackable라는 고수준 모듈을 추가함으로써 Character가 OneHandSword를 받는게 아닌 Attackable을 받게 수정

// 공격 인터페이스
interface Attackable {
    int attack();

    @Override
    String toString();
}

class OneHandSword implements Attackable {
    private final String name;
    private final int damage;

    public OneHandSword(String name, int damage) {
        this.name = name;
        this.damage = damage;
    }

    @Override
    public int attack() {
        return damage;
    }

    @Override
    public String toString() {
        return "OneHandSword{" +
                "name='" + name + '\'' +
                '}';
    }
}

class Character {
    private final String name;
    private int health;
    private Attackable weapon;

    public Character(String name, int health, Attackable weapon) {
        this.name = name;
        this.health = health;
        this.weapon = weapon;
    }

    public int attack() {
        return weapon.attack();
    }

    public void damaged(int amount) {
        health -= amount;
    }

    public void changeWeapon(Attackable weapon) {
        this.weapon = weapon;
    }

    @Override
    public String toString() {
        return "Character{" +
                "name='" + name + '\'' +
                '}';
    }
}

출처

0개의 댓글