코딩 개발/Spring

SOLID (객체 지향 설계 원칙)

호소세 2023. 7. 6. 13:53
728x90
반응형

프로젝트를 끝내고 오느라 오랜만에 글을 작성하게 되었습니다. 프로젝트를 끝내고 Sping FrameWork를 배우기 시작했습니다! 👏👏👏

Spring FrameWork를 알아가기 앞서 '이것은 정말 중요하겠다' 라고 생각하는 개념을 알아보려고 합니다.

 

바로 SOLID 인데요.

SOLID란?

SOLID는 객체 지향 설계 원칙을 나타내는 다섯 가지 기본 원칙의 앞 글자를 딴 약어입니다. 이 원칙들은 소프트웨어 설계의 품질과 유연성을 높이고, 유지보수성과 확장성을 개선하는 데 도움을 줍니다. SOLID 원칙은 소프트웨어 개발에서 중요한 원칙들을 포괄하며, 객체 지향 프로그래밍 언어에서 특히 유용하게 적용됩니다.

 

Spring을 배우니까 이 원칙을 확인하지 않으면 불편한 상황이 많이 생길 것 같더라고요. 무슨 원칙이 있는지 알아볼까요?

 

S - 단일 책임 원칙 (Single Responsibility Principle)

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

L - 리스코프 치환 원칙 (Liskov Substitution Principle)

I - 인터페이스 분리 원칙 (Interface Segregation Principle)

D - 의존성 역전 원칙 (Dependency Inversion Principle)

 

 

S - 단일 책임 원칙 (Single Responsibility Principle)

클래스는 단 하나의 책임(기능)을 가져야 합니다. 하나의 클래스는 하나의 기능을 담당하여 하나의 책임을 수행하는데 집중되어야 합니다.

 

객체에 다른 기능의 메서드나 변수가 들어있다면 A를 고치면 B를 고쳐야 하고 C도 고치게 되는 상황이 오면서 또다시 A를 고쳐야 하는 안 좋은 상황까지 갈 수 있습니다. 따라서 무조건 클래스에 하나의 기능만을 넣어야 합니다.

 

// 파일을 나타내는 클래스
public class File {
    private String name;
    private String content;

    public File(String name) {
        this.name = name;
        this.content = "";
    }

    public String getName() {
        return name;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

// 파일을 저장하는 기능을 담당하는 클래스
public class FileManager {
    public void saveFile(File file) {
        // 파일을 저장하는 로직
    }
}

// 파일을 읽는 기능을 담당하는 클래스
public class FileReader {
    public String readFileContent(File file) {
        // 파일을 읽는 로직
        return file.getContent();
    }
}

// 파일을 삭제하는 기능을 담당하는 클래스
public class FileDeleter {
    public void deleteFile(File file) {
        // 파일을 삭제하는 로직
    }
}

파일 정보를 관리하는 File 클래스, 파일을 저장하는 FileManager 클래스, 파일을 읽는 FileReader 클래스, 파일을 삭제하는 FileDeleter 클래스로 각각 나눠 단일 책임(기능)을 가지게 하는 것입니다.

 

이렇게 파일의 정보와 파일 관리 기능을 각각 다른  클래스로 분리하면서 코드의 응집도를 높일 수 있고, 각 클래스는 변경되어야 하는 이유가 단 하나뿐이므로 유지보수성과 확장성을 향상할 수 있습니다.

 

 

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

개방 폐쇄 원칙(Open-Closed Principle, OCP)은 소프트웨어 개체(클래스, 모듈 등)는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다는 원칙입니다. 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있도록 설계해야 한다는 의미입니다.

 

확장 가능성을 지니며, 동시에 코드의 안정성과 일관성을 유지하는 것을 목표로 합니다.

public abstract class Animal {
    public abstract void makeSound();
}

// 개를 나타내는 클래스
public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("멍멍!");
    }
}

// 고양이를 나타내는 클래스
public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("야옹!");
    }
}

// 동물 소리 출력기 클래스
public class AnimalSoundPrinter {
    public void printAnimalSound(Animal animal) {
        animal.makeSound();
    }
}

동물의 유형을 계속해서 추가할 때 기존 코드를 수정할 필요 없이 Animal 클래스의 하위 클래스를 계속 생성하여 구현할 수 있습니다. 

public class Bird extends Animal {
    @Override
    public void makeSound() {
        System.out.println("짹짹!");
    }
}

위의 코드같이 새로운 동물( Bird )을 추가하여 makeSound의 내용을 오버라이딩하면 코드를 유지 관리하고 향후 확장하기 쉽게 만들 수 있습니다.

 

또 다른 예로 JDBC도 OCP 원칙을 따릅니다.

 

L - 리스코프 치환 원칙 (Liskov Substitution Principle)

하위 클래스(자식)의 객체는 항상 상위 클래스(부모)의 타입으로 교체할 수 있어야 한다는 원칙입니다.

상속 관계에서 자식 클래스는 부모 클래스의 행위를 모두 지켜야 하며, 자식 클래스의 동작은 부모 클래스를 사용하는 코드를 변경하지 않아야 합니다. LSP는 상속을 이용한 다형성의 원리를 지키고 일관성 있는 설계를 도모하는 중요한 원칙입니다.

 

리스코프 치환 원칙 위반 예시

public abstract class Animal {
    public abstract void makeSound();
}
// 개를 나타내는 클래스
public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("멍멍!");
    }
}
// 고양이를 나타내는 클래스
public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("야옹!");
    }
}
// 물고기를 나타내는 클래스
public class Fish extends Animal {
    @Override
    public void makeSound() {
        throw new Exception("물고기는 말을 안합니다 뻐끔")
    } catch (Exception e) {
            e.printStackTrace();
        }
}
// 동물 소리 출력기 클래스
public class AnimalSoundPrinter {
    public void printAnimalSound(Animal animal) {
        animal.makeSound();
    }
}

A개발자가 이러한 코드를 작성했다고 생각해 보면 아무것도 모르고 있던 B 개발자가 Fish 개를 가져와서 makeSound() 메서드를 실행하여 에러가 발생하는 참사가 생깁니다. 이렇게 되면 협업하는 개발자 사이의 신뢰가 깨지게 되므로

sound에 대한 것은 따로 Interface로 분리하여 코드를 리팩터링 해야 합니다.

 

abstract class Animal {
}

interface Speakable {
    void speak();
}

class Cat extends Animal implements Speakable {
    public void speak() {
        System.out.println("냐옹");
    }
}

class dog extends Animal implements Speakable  {
    public void speak() {
        System.out.println("멍멍");
    }
}

class Fish extends Animal {
}

이런 식으로 말이죠.

 

업캐스팅된 상태에서 부모의 메서드를 사용해도 동작이 의도대로 흘러가야 하는 것을 의미합니다.

Spring에서 Bean을 불러올 때도 많이 사용하더라고요. 나중에 Spring을 알아볼 때 확인해 봐요.

 

I - 인터페이스 분리 원칙 (Interface Segregation Principle)

인터페이스 분리 원칙(Interface Segregation Principle, ISP)은 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙입니다.

한 클래스가 자신이 필요로 하지 않는 인터페이스에 의존해서는 안된다는 것을 의미합니다. ISP는 인터페이스를 작은 단위로 분리하여 클라이언트가 필요한 메서드만 사용할 수 있도록 하는 것을 목표로 합니다. 이를 통해 결합도를 낮추고 의존성을 관리하여 시스템의 유연성과 확장성을 향상합니다.

 

클라이언트의 목적과 용도에 적합한 Interface 만을 제공하는 것이 목표입니다.

 

다만 한번 인터페이스를 분리하여 구성해 놓고 나중에 무언가 수정사항이 생겨서 또 인터페이스들을 분리하는 행위를 가하면 안 된다고 합니다.

인터페이스 분리 원칙 예제

public abstract class Animal {
    // 동물의 종류에 대한 정보와 관련된 속성과 메서드
}
public interface Movable {
    void eat();
}
public interface Eatable {
    void eat();
}
public interface Sleepable {
    void sleep();
}
public interface SoundMakable {
    void makeSound();
}

동물들의 행동을 하나하나 나누면서 다양한 동물 클래스를 생성할 수 있습니다. 위에 리스코프 원칙 위반사항을 바로 인터페이스 분리원칙으로 해결하면 되는 것입니다.

말을 할 수 없는 동물이 있으면 SoundMakable 인터페이스는 구현하지 않으면 되기 때문에 아주 편하게 클래스를 생성할 수 있습니다. 

 

D - 의존성 역전 원칙 (Dependency Inversion Principle)

의존성 역전 원칙(Dependency Inversion Principle, DIP)은 상위 모듈은 하위 모듈에 의존해서는 안 되며, 추상화된 인터페이스에 의존해야 한다는 원칙입니다.

구체적인 구현이 아닌 추상화된 인터페이스나 추상 클래스에 의존해야 합니다. 이는 고수준 모듈과 저수준 모듈 사이의 의존성을 역전시켜 유연하고 확장 가능한 설계를 도모합니다.

의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는, 변화하기 어려운 것 거의 변화가 없는 것에 의존해야 한다는 원칙입니다.

 

가장 좋은 예제가 게임에서 무기 교체라고 생각되어 인파님의 예제를 가져왔습니다.

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-DIP-%EC%9D%98%EC%A1%B4-%EC%97%AD%EC%A0%84-%EC%9B%90%EC%B9%99

 

💠 완벽하게 이해하는 DIP (의존 역전 원칙)

의존 역전 원칙 - DIP (Dependency Inversion Principle) DIP 원칙이란 객체에서 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라 그 대상의 상위 요소(추상 클래스

inpa.tistory.com

의존성 역전 원칙 예시

위반 예제

class OneHandSword {
    final String NAME;
    final int DAMAGE;

    OneHandSword(String name, int damage) {
        NAME = name;
        DAMAGE = damage;
    }

    int attack() {
        return DAMAGE;
    }
}

class TwoHandSword {
    // ...
}

class BatteAxe {
    // ...
}

class WarHammer {
    // ...
}

RPG 게임에서 무기를 변경하여 착용해야 하는 경우가 있습니다. 이 무기들을 따로따로 Class로 만들어서 생성해 놓으면 문제가 발생합니다. 

class Character {
    final String NAME;
    int health;
    OneHandSword weapon; // 의존 저수준 객체

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

    int attack() {
        return weapon.attack(); // 의존 객체에서 메서드를 실행
    }

    void chageWeapon(OneHandSword weapon) {
        this.weapon = weapon;
    }

    void getInfo() {
        System.out.println("이름: " + NAME);
        System.out.println("체력: " + health);
        System.out.println("무기: " + weapon);
    }
}

어떠한 캐릭터가 무기를 끼는데 따로 분리되어 있으면 한손검을 낀 캐릭터 객체, 망치를 낀 객체, 양손검을 낀 객체를 다 따로 만들어서 만들어 줘야 합니다. changeWeapon 메서드를 오버로딩해서 따로 만들어줘야 할 것입니다. 반복되는 작업이 계속되겠죠.

그리고  무기가 5만 개가 있다고 생각하면 미쳐버리는 것입니다.

따라서 무기 자체를 추상화하여 하나로 묶어 놓으면 업캐스팅을 해서 사용할 수 있게 됩니다.

interface Weaponable {
    int attack();
}

class OneHandSword implements Weaponable {
    final String NAME;
    final int DAMAGE;

    OneHandSword(String name, int damage) {
        NAME = name;
        DAMAGE = damage;
    }

    public int attack() {
        return DAMAGE;
    }
}

class TwoHandSword implements Weaponable {
	// ...
}


class BatteAxe implements Weaponable {
	// ...
}

class WarHammer implements Weaponable {
	// ...
}
class Character {
    final String NAME;
    int health;
    Weaponable weapon; // 의존을 고수준의 모듈로

    Character(String name, int health, Weaponable weapon) {
        this.NAME = name;
        this.health = health;
        this.weapon = weapon;
    }

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

    void chageWeapon(Weaponable weapon) {
        this.weapon = weapon;
    }

    void getInfo() {
        System.out.println("이름: " + NAME);
        System.out.println("체력: " + health);
        System.out.println("무기: " + weapon);
    }
}

changeWeapon 메서드의 변수를 Weaponable 인터페이스로 받게 하면서 무기들을 업캐스팅을 시켜주는 겁니다.

그러면 코드의 중복 없이 하나의 코드로 모든 무기를 받아올 수 있는 것입니다.

 

누가 봐도 코드가 짧아지고 확장하기도 좋아집니다.

 

소감

Spring을 배우기 전 꼭 알아야 하는 원칙들입니다. 물론 Java를 처음 시작하기 전에 알아도 좋겠지만, 역시 객체지향 프로그램이 뭔지 어느 정도 알고 이 개념을 배우면 훨씬 도움이 잘됩니다. 왜냐하면 추상적인 개념들이기 때문에 코드의 예시가 좀 필요합니다. 그러니 기본적인 Java 개념을 알고 나서 이 내용을 보면 더 좋을 것 같습니다.

이번에도 좋은 글이 있어 가져왔습니다. 경제 관련 이야기는 한번도 안드린것 같아서 경제관련 좋은 말씀이 있어서 가져왔습니다.

이건희 회장님의 말씀이신데, 돈을 많이 버는 것도 당연히 중요하지만 그 많은 돈을 담을 그릇을 만들어야 한다는 좋은 말입니다. 자신이 가지고 있는 돈의 그릇이 한계를 넘어가면 깨져버려 병이 나거나 안 좋은 일이 생길 수 있다고 합니다. 그러니 우리는 항상 마인드 세팅을 통해서 돈을 담을 수 있는 그릇을 키워나가 봅시다.


도움 받은 글 : https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84%EC%9D%98-5%EA%B0%80%EC%A7%80-%EC%9B%90%EC%B9%99-SOLI D#%EC%9D%98%EC%A1%B4_%EC%97%AD%EC%A0%84_%EC%9B%90%EC%B9%99_-_dip_ dependency_inversion_principle

 

💠 객체 지향 설계의 5가지 원칙 - S.O.L.I.D

객체 지향 설계의 5원칙 S.O.L.I.D 모든 코드에서 LSP를 지키기에는 어려움. 리스코프 치환 원칙에 따르면 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대신하더라도 의도에 맞게 작동되어

inpa.tistory.com

https://velog.io/@seculoper235/DB-%EC% 9E%91% EC%97%85-JDBC

반응형