SOLID 원칙

2024. 1. 10. 02:49백엔드/Spring Boot

이전에 디자인 패턴과 SOLID 원칙에 대하여 게시글을 작성했는데, 이번에는 SOLID 원칙별 예시 코드와 함께 정리해보려고 한다. 

 

# SOLID 원칙이란? 

SOLID 원칙은 객체 지향 프로그래밍의 다섯 가지 기본 원리로 SRP, OCP, LSP, ISP, DIP 총 5가지로 구성되어 있으며 이를 준수하여 코드를 작성하면 유연하고 확장 가능한 아키텍처를 구축할 수 있다.

SOLID 원칙은 개발자들에게 일관된 설계 구조를 제공하여 효율적이고 유지보수 가능한 코드를 작성하는 데 도움을 준다.

 

# SOLID 원칙

1.  단일 책임 원칙 - Single Responsibility Principle / SRP

: 하나의 클래스는 하나의 책임(기능)만을 가져야 하고, 클래스를 변경해야 하는 이유는 단 하나여야 한다.

 

 

[SRP 원칙을 위반한 코드]

  •  클래스는 선수의 정보에 대해 관리하는데 주로 메디컬 팀이 진행하는 `치료`라는 행위까지 가지고 있다. 이는 하나의 클래스는 하나의 책임을 가져야 한다는 SRP 원칙을 위반한다. 
public class BaseballPlayerErr {
    private String name;
    private int age;
    private int uniformNumber;

    public BaseballPlayerErr(String name, int age, int uniformNumber) {
        this.name = name;
        this.age = age;
        this.uniformNumber = uniformNumber;
    }

    public String getName() {
        return name;
    }

    // 공을 치다.
    public void hit() {}

    // 뛰다.
    public void run() {}

    // 치료 하다.
    public void treat() {}
}

class BaseballPlayerErrMain {
    public static void main(String[] args) {
        BaseballPlayerErr player = new BaseballPlayerErr("감자도리", 24, 21);
        player.hit();
        player.run();
        player.treat();
    }
}

 

[SRP 원칙을 준수한 코드]

  • 메디컬팀 클래스를 생성하여 해당 클래스에 치료라는 메서드를 생성하여 SRP의 원칙을 준수하였다. 
public class BaseballPlayer {
    private String name;
    private int age;
    private int uniformNumber;

    public BaseballPlayer(String name, int age, int uniformNumber) {
        this.name = name;
        this.age = age;
        this.uniformNumber = uniformNumber;
    }

    public void hit() {
        System.out.println(name + " hit!");
    }

    public String getName() {
        return name;
    }
}

class MedicalTeam {
    public void treat(BaseballPlayer player) {
        System.out.println(player.getName() + " ... treat ...");
    }
}


class BaseballPlayerMain {
    public static void main(String[] args) {
        BaseballPlayer player = new BaseballPlayer("John Doe", 20, 21);
        player.hit();

        MedicalTeam medicalTeam = new MedicalTeam();
        medicalTeam.treat(player);
    }
}

 


 

2. 개방/폐쇄 원칙 - Open/Closed Principle / OCP

: 새로운 기능을 추가하거나 변경할 때 기존의 코드를 수정하지 않고도 가능해야 한다.

 

 

[OCP 원칙을 위반한 코드]

  • 야구선수의 포지션이 추가 될 때마다 ProtectiveGear 클래스를 수정해야 하므로 OCP의 확장에는 개방, 수정에는 폐쇄 되어야 하는 OCP 원칙을 위반한다. 
public class BaseballPlayerErr {
    String position;

    public BaseballPlayerErr(String position) {
        this.position = position;
    }
}

// 보호 장비 관련 클래스
class ProtectiveGear {
    void wear(BaseballPlayerErr player) {
        
        switch (player.position) {
            case "Hitter" :
                    System.out.println("야구 베트를 착용합니다.");
                break;
            case "Pitcher" :
                System.out.println("야구공과 글러브를 착용합니다.");
                break;
            case "Catcher" :
                System.out.println("포수 장비 세트를 착용합니다.");
                break;
            default:
                System.out.println("착용할 장비가 없습니다.");
                break;
        }
    }
}

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

        ProtectiveGear protectiveGear = new ProtectiveGear();

        BaseballPlayerErr hitter = new BaseballPlayerErr("Hitter");
        protectiveGear.wear(hitter);

        BaseballPlayerErr pitcher = new BaseballPlayerErr("Pitcher");
        protectiveGear.wear(pitcher);

        BaseballPlayerErr catcher = new BaseballPlayerErr("Catcher");
        protectiveGear.wear(catcher);
    }
}

 

 

[OCP 원칙을 준수한 코드]

  • 포지션별 클래스를 생성 후 ProtectiveGear 인터페이스를 상속하여 포지션이 추가되어도 기존 코드 수정이 필요하지 않도록 수정하여 OCP 원칙을 준수하였다. 
interface ProtectiveGear {
    void wear();
}

class Hitter implements ProtectiveGear {
    @Override
    public void wear() {
        System.out.println("야구 베트를 착용합니다.");
    }
}

class Pitcher implements ProtectiveGear {
    @Override
    public void wear() {
        System.out.println("야구공과 글러브를 착용합니다.");
    }
}

class Catcher implements ProtectiveGear {
    @Override
    public void wear() {
        System.out.println("포수 장비 세트를 착용합니다.");
    }
}

class BaseballPlayerMain {
    public static void main(String[] args) {
        Hitter hitter = new Hitter();
        hitter.wear();

        Pitcher pitcher = new Pitcher();
        pitcher.wear();

        Catcher catcher = new Catcher();
        catcher.wear();
    }
}

 

 


 

3. 리스코프 치환 원칙 - Liskov Substitution Principle / LSP

: 부모 객체와 자식 객체가 있을 때 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있다. 

 

 

[LSP 원칙을 위반한 코드]

  • Swimmer 클래스는 run 메소드를 오버라이드 하지 않았음에도 불구하고 main 메서드를 실행시켰을 때 run 메서드가 실행되고 있다. 이는 올바르지 않은 상속관계로 LSP 원칙을 위반한다. 
class Athlete {
    void run() {
        System.out.println("땅에서 뜁니다!");
    }
}

class Swimmer extends Athlete {
    // 수영선수는 땅에서 뛸 수 없으므로 run 메서드를 오버라이드 하지 않음
}


public class Main {
    public static void main(String[] args) {
        Athlete athlete = new Swimmer();
        athlete.run();
    }
}

 


 

4. 인터페이스 분리 원칙 - Interface Segregation Principle / ISP

: 클라이언트는 사용하지도 않는 메서드에 의존하게 만들어서는 안되고 큰 인터페이스들을 더 작은 단위로 분리시켜야 한다. 

 

 

[ISP 원칙을 위반한 코드]

  • DeveloperWhoHatesExcercise 클래스는 crossfit 메서드가 필요하지 않은데에도 상속을 받게 되어 ISP 원칙을 위반한다. 
interface Developer {
    void study();
    void develop();
    void crossfit();
}

class DeveloperWhoHatesExcercise implements Developer {
    @Override
    public void study() {
        
    }

    @Override
    public void develop() {

    }
    
    // crossfit 메서드를 사용하지 않는데도 상속을 받게됨 
    @Override
    public void crossfit() {

    }
}

class DeveloperWhoLovesExcercise implements Developer {
    @Override
    public void study() {
        
    }

    @Override
    public void develop() {

    }

    @Override
    public void crossfit() {

    }
}

 

 

[ISP 원칙을 준수한 코드]

  • Developer 인터페이스에서 crossfit 메서드를 분리하여 새로운 HealthyPerson 인터페이스로 만들었고, 각각의 클래스에서 필요한 인터페이스만 구현하여 ISP 원칙을 준수하였다. 
// 운동과 관련된 인터페이스
interface HealthyPerson {
    void crossfit();
}
// 개발자와 관련된 인터페이스
interface Developer {
    void study();
    void develop();

}

class DeveloperWhoHatesExcercise implements Developer {
    @Override
    public void study() {

    }

    @Override
    public void develop() {

    }
}

class DeveloperWhoLovesExcercise implements Developer, HealthyPerson {
    @Override
    public void study() {

    }

    @Override
    public void develop() {

    }

    @Override
    public void crossfit() {

    }
}

 


 

5. 의존성 역전 원칙 - Dependency Inversion Principle / DIP

: 고수준 모듈은 저수준 모듈에 의존해서는 안되며, 모두 추상화에 의존해야 한다.

 

 

[DIP 원칙을 위반한 코드]

  • Zoo 클래스가 Dog 클래스에 직접 의존을 하고 있기 때문에 DIP 원칙을 위반한다. 
// 고수준 모듈
class Zoo {
    private Dog dog;

    public Zoo() {
        this.dog = new Dog(); // 저수준 모듈에 직접 의존
    }

    public void makeSound() {
        dog.bark();
    }
}

// 저수준 모듈
class Dog {
    public void bark() {
        System.out.println("Woof!");
    }
}

public class DIPViolationExample {
    public static void main(String[] args) {
        Zoo zoo = new Zoo();
        zoo.makeSound();
    }
}

 

 

[DIP 원칙을 준수한 코드]

  • Zoo 클래스가 Animal 인터페이스에 의존하도록 수정하여 DIP 원칙을 준수하였다. 
// 고수준 모듈이 의존하는 추상화(interface)
interface Animal {
    void makeSound();
}

// 저수준 모듈이 구현하는 추상화
class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}

// 고수준 모듈이 저수준 모듈에 직접 의존하지 않음
class Zoo {
    private Animal animal;

    public Zoo(Animal animal) {
        this.animal = animal;
    }

    public void makeSound() {
        animal.makeSound();
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Zoo zoo = new Zoo(dog);
        zoo.makeSound();
    }
}