주노 님의 블로그

[Spring] 객체지향 설계의 5가지 원칙 (SOLID) 본문

공부/Spring

[Spring] 객체지향 설계의 5가지 원칙 (SOLID)

juno0432 2024. 7. 31. 00:38

블로그 정리, 깃허브 업로드

Q: 강의 수강한 내용을 블로그에 정리하고 싶어요! + 깃허브에 코드를 올려도 될까요?

 

학습한 내용을 본인의 생각으로 요약하고 정리하는 것은 괜찮습니다.

예제 코드나 강의자료도 설명을 위해서 일부분 발췌하는 것은 괜찮습니다.

출처는 해당 강의 링크를 꼭 남겨주세요.

 

유료 강의입니다. 따라서 코드 전체를 오픈하면 안됩니다. 깃허브를 사용한다면 혼자서 볼 수 있도록 private으로 사용해주세요.

 

아래 강의는 인프런 김영한님의 스프링 핵심 원리 - 기본편 에서 발췌한 내용입니다

 

SOLID는 객체지향 프로그래밍을 구현하는데 중요한 5가지 원칙으로

Single Responsibility Principle  : 단일 책임의 원칙

Open / Close Principle : 개방-폐쇄 원칙

Liskov Subsitution Principle : 리스코프 치환 원칙

Interface Segregation principle : 인터페이스 분리 원칙 

Dependency Inversion Principle : 의존관계 역전 원칙

가 있다

  • SRP (Single Responsibility Principle) - 단일 책임의 원칙
    한 클래스는 하나의 책임만 가져야 한다, 클래스 하나가 하나의 기능만 수행을하며.
    변경이 있을때 파급 효과가 적으면 단일 책임의 원칙을 잘 지킨것이다
    하지만 위 책임이라는 기준이 모호 할 수 있다.

    public class User {
        private String name;
        private String email;
    
        public void setName(String name) {
            this.name = name;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    
        public void saveToDatabase() {
            // 데이터베이스에 사용자 정보 저장
        }
    
        public void sendEmailNotification() {
            // 이메일 알림 전송
        }
    }
     
    위는 user 클래스 안에 이메일 알림전송, 데이터베이스에 사용자 정보를 저장하는 기능까지
    가지고 있는것을 볼 수 있다
    public class User {
        private String name;
        private String email;
    
        public void setName(String name) {
            this.name = name;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    }
    
    public class UserRepository {
        public void saveToDatabase(User user) {
            // 데이터베이스에 사용자 정보 저장
        }
    }
    
    public class EmailService {
        public void sendEmailNotification(User user) {
            // 이메일 알림 전송
        }
    }

    이렇게 유저클래스, 유저 레파지토리, 이메일서비스를 분리함으로써 단일 책임의 원칙을 준수한것을 볼 수 있다.

  • OCP( Open / Close Principle) - 개방 폐쇄의 원칙
    확장에는 열려 있으나, 변경에는 닫혀 있어야한다.
    어떤 기능을 추가한다고, 내부 기능을 바꾸는것을 지양한다는 뜻이다
    다형성을 이용하면 소프트웨어를 확장에는 열려있으나 변경에는 닫혀 있게 구현 할 수 있다
    김영한님의 스프링 핵심 원리 - 기본편 1주차 강의자료 중

    기존코드에서 다른코드로 변경을 한 상황이다. 이는 OCP원칙을 깨는 일이다
    객체를 생성하고, 연관관계를 맺어주는 별도의 조립 및 설정자가 필요하다 << 이는 스프링이 지원하는 기능이다.

  • LSP(Liskov Subsitution Principle) - 리스코프 치환 원칙
    프로그램 객체는 프래그램의 정확성을 깨트리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야한다.
    다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야한다는것.
    서브 타입은 언제나 자신의 기반 타입(부모 클래스)로 대체할 수 있어야한다.
    프로그램의 객체들이 부모 클래스 타입의 객체로 동작하도록 설계되어있다면
    만약 부모객체가 없어도 자식 클래스 객체 만으로 문제없이 동작해야한다는 것이다

    public class Bird {
        public void fly() {
            System.out.println("Flying");
        }
    }
    
    public class Ostrich extends Bird {
        @Override
        public void fly() {
            throw new UnsupportedOperationException("Ostriches can't fly");
        }
    }

    위 클래스를 보면 부모 클래스를 상속 받았지만
    부모클래스의 메서드와 맞지 않는 메서드가 생길 수 있다

    public abstract class Bird {
        public abstract void move();
    }
    
    public class FlyingBird extends Bird {
        @Override
        public void move() {
            System.out.println("Flying");
        }
    }
    
    public class Ostrich extends Bird {
        @Override
        public void move() {
            System.out.println("Running");
        }
    }

    이렇게 변경하면 부모 클래스의 어떤 자식이라도 구현할 수 있다
  • ISP(Interface Segregation principle) - 인터페이스 분리 원칙
    특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다
    인터페이스의 기능이 명확해지고 유연성이 높아진다
    클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 한다
    public interface Worker {
        void work();
        void eat();
    }
    
    public class HumanWorker implements Worker {
        @Override
        public void work() {
            // 사람 작업 코드
        }
    
        @Override
        public void eat() {
            // 사람 식사 코드
        }
    }
    
    public class RobotWorker implements Worker {
        @Override
        public void work() {
            // 로봇 작업 코드
        }
    
        @Override
        public void eat() {
            throw new UnsupportedOperationException("Robots don't eat");
        }
    }
     
    위 코드 예시를 보면 로봇은 먹을 필요는 없지만 인터페이스를 상속받아 eat을 구현해야한다

    public interface Workable {
        void work();
    }
    
    public interface Eatable {
        void eat();
    }
    
    public class HumanWorker implements Workable, Eatable {
        @Override
        public void work() {
            // 사람 작업 코드
        }
    
        @Override
        public void eat() {
            // 사람 식사 코드
        }
    }
    
    public class RobotWorker implements Workable {
        @Override
        public void work() {
            // 로봇 작업 코드
        }
    }

    위와같이 인터페이스를 작업, 먹기를 분리하면 필요없는 인터페이스의 메서드를 사용하지 않을 수 있다.

  • DIP(Dependency Inversion Principle) - 의존관계 역전 원칙
    고수준 모듈은 저수준 모듈에 의존해서는 안된다. 둘다 추상화에 의존해야한다
    추상화는 구체적인 사항에 의존해서는 안된다. 구체적인 사항이 추상화에 의존해야한다.
    클라이언트 코드가 구현 클래스를 바라보지말고 인터페이스에 의존하라는 뜻
    예를들어 운전자는 자동차 역할 그 자체에 대해서 알아야하는거지, k3에 대해 알라는건 아니다
    public class LightBulb {
        public void turnOn() {
            System.out.println("LightBulb on");
        }
    
        public void turnOff() {
            System.out.println("LightBulb off");
        }
    }
    
    public class Switch {
        private LightBulb lightBulb;
    
        public Switch(LightBulb lightBulb) {
            this.lightBulb = lightBulb;
        }
    
        public void operate() {
            lightBulb.turnOn();
        }
    }

    위 코드를 보자 switch 클래스는 LightBulb에 많이 의존하고 있는 모습을 볼 수 있다
    만약 LightBulb를 변경하면, Switch클래스도 변경해야한다

    public interface Switchable {
        void turnOn();
        void turnOff();
    }
    
    public class LightBulb implements Switchable {
        @Override
        public void turnOn() {
            System.out.println("LightBulb on");
        }
    
        @Override
        public void turnOff() {
            System.out.println("LightBulb off");
        }
    }
    
    public class Fan implements Switchable {
        @Override
        public void turnOn() {
            System.out.println("Fan on");
        }
    
        @Override
        public void turnOff() {
            System.out.println("Fan off");
        }
    }
    
    public class Switch {
        private Switchable device;
    
        public Switch(Switchable device) {
            this.device = device;
        }
    
        public void operate() {
            device.turnOn();
        }
    }

    고수준 모듈 switch와 저수준 모듈 bulb는 이제 인터페이스에 의존하며, 구체적인 사항(클래스)은
    추상화(인터페이스)에 의존하는것을 볼수 있다.