컴퓨터공학/객체 지향 프로그래밍(Object-oriented programming)

[OOP] SOLID 5가지 원칙

RyanSin 2022. 5. 14. 19:31
반응형

-개요

안녕하세요. 이번 시간에는 객체지향에 5가지 원칙 SOLID원칙에 대해 알아보겠습니다.

 

가장 기본이 되며 SOLID원칙을 알아 두시면 나중에 도움이 됩니다.(참고로 저는... 알기 전후 소스 코드가 달라졌어요...)

 

또한 객체지향 언어로 개발 시 동료 개발자분들과 협업을 할 때 그리고 많은 면접 질문에도 종종 나와요 하하...

 

 

- 개념

SOLID라는 건 5가지 유형에 대한 앞 글자를 따서 만든 단어입니다. 그렇다면 원칙이라는 단어부터 짚고 넣어가겠습니다.

 

원칙이라는 단어를 아시는 분도 있겠지만 모르시는 분들도 있다고 생각이 들어서 작성하겠습니다.

 

원칙

원칙이란 "지켜야 하는 기본적인 규칙이나 법칙"이라고 나와있습니다.

 

출처: 네이버 국어사전

객체지향 프로그래밍을 할 때 우리가 일관되게 프로젝트 구조를 만들고, 이론에 맞게 클래스 파일을 생성해서 사용한다고 생각하시면 됩니다.

 

그렇다면 SOLID에 대해 알아보겠습니다.

 

클린 코드로 유명한 로버트 마틴이 좋은 객체지향 설계에 대해서 5가지 원칙을 정리하면서 만들어졌습니다.

  1. SRP : 단일 책임 원칙(Single responsibility principle)
  2. OCP : 개방-폐쇄 원칙(Open/closed principle)
  3. LSP : 리스 코프 치환 원칙(Liskov substitution principle)
  4. ISP : 인터페이스 분리 원칙(Interface segregation principle)
  5. DIP : 의존관계 역전 원칙(Dependency inversion principle)

위 다섯 가지 원칙에 대해서 자세히 설명해보겠습니다.

 

단일 책임 원칙 - SRP(Single responsibility principle)

단일 책임 원칙은 모든 클래스는 하나의 책임만 가져야 한다.

 

무슨 말이냐면, 하나의 클래스에 여러 가지 책임을 가지고 있으면 안 된다는 말입니다.

 

기본적으로 Java프로젝트를 생성하면 프로젝트를 컴파일하고 실행시켜주는 클래스가 있습니다.

바로 Main클래스입니다. Main클래스에 책임은 프로젝트 전체에 소스코드를 컴파일하고 컴파일을 통해 실행시켜줍니다.

 

만약 다른 책임을 설정했다면... 이건 SRP 원칙을 준수하지 않는다고 생각하시면 됩니다.

/**
 * @author Ryan
 * @description SOLID 원칙 - SRP : 단일 책임 원칙(Single responsibility principle)
 *                                 모든 클래스는 하나의 책임만 가져야 한다.
 *
 *                                 Main 클래스는 프로젝트 실행에 대한 책임을 갖는다.
 *                                 나의 책임이라는 것은 모호합니다. 책임이 클수도 있고, 작을수 있다.
 *                                 중요한 기준은 변경입니다. 변경이 있을 때 문제가 생긴다면 그건 단일 책임원칙을 잘 따른 부분이 압니다.
 */
public class Main {

    public static void main(String[] args) {

    }
}

 

개방 - 패쇄 원칙 - OCP(Open/closed principle)

개방 - 패쇄 원칙은 소프트웨어 요소는 확장에는 열려 있으나 수정에는 닫혀 있어야 한다는 원칙입니다.

 

무슨 말이냐면 인터페이스와 상속과 같은 기술을 사용해서 클라이언트 소스코드를 변경하지 않고 해당 인터페이스에 연관관계에 맞는 구현체 클래스를 설정하는 방식입니다.

 

즉, 다형성이라는 부분을 활용해서 이 부분을 해결합니다.

 

리스 코프 치환 원칙 - LSP(Liskov substitution principle)

객체 지향에서 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

 

다형성이라는 개념과, 단형 성이라는 개념이 있습니다.

  • 다형성 → 다양한 형태의 성질을 가질 수 있다.
  • 단형성 → 반대의 개념으로 프로그램 언어의 각 요소가 한 가지 형태만 가지는 성질

다형성을 통해 하위 클래스(자식 클래스)는 인터페이스 규약을 다 지켜야 한다는 원칙

다형성 원칙을 지키기 위한 원칙, Interface를 implements를 한 Class는 Interface를 믿고 사용하는 뜻입니다.

 

하나의 기능은 그 기능을 위해 존재합니다. 기능을 위배하는 행동은 원칙을 어기는 행위입니다.

 

인터페이스 분리 원칙 - ISP(Interface segregation principle)

하나의 인터페이스는 보다는 특정 클라이언트를 위한 인터페이스를 여러 개 범용적으로 만들어서 사용하는 게 좋다.

 

Ex) 식당 안에 점장, 주방장, 요리사, 홀을 담당하는 직원들이 있습니다. 각각에 책임과 역할이 분리되겠죠?

하지만 점장이나 주방장은 홀과 주방을 총괄하는 직책을 맡고 있습니다.  그렇기 때문에 요리사와 홀직원 기능을 다 알고 있습니다.

 

여기서 인터페이스 분리 원칙이라는 건 점장이라는 인터페이스 하나 홀직원 인터페이스 하나를 만들어 사용하라는 뜻입니다.

 

점장 -> 점장 & 홀직원 인터페이스를 implements

홀직원 -> 홀직원 인터페이스를 implements

 

위처럼 인터페이스를 분리를 통해 각각 기능을 나누서 구현할 수 있으며, 만약 점장 보다 높음 지점장이라는 클래스가 존재한다면 우리는 지점장 인터페이스를 만들고 지점장 클래스에 지점장 & 점장 & 홀직원 인터페이스를 가져다 사용하면 됩니다.

 

의존 관계 역전 원칙 - DIP( Dependency inversion principle)

우리는 객체지향 언어를 사용하면서 클래스를 만들고 제어하는데 중점을 두었던 기억이 있을 겁니다. (저도 그랬어요..)

 

하지만 DIP에 대한 개념을 이해하면 다형성에 대한 활용도가 높아지고 의존과 관계 설정에 중점을 두게 됩니다.

 

의존 관계 역전 원칙에 핵심은 "구체화인 클래스에 의존하는 게 아니라 추상화에 의존해야 한다"라는 게 핵심입니다.

 

쉽게 말하면 클라이언트 코드가 구현 클래스에 의존하는 게 아니라 인터페이스에 의존하라는 뜻입니다.

 

Ex) 위 예시(식당)를 다시 활용한다면 이렇게 설명할 수 있습니다.

 

식당에는 점장, 주방장, 요리사, 홀을 담당하는 직원이 있다고 했습니다. 여기서 점장, 주방장, 요리사, 홀직원 특정 실체를 지목한 부분이 아니라 추상적인 이름입니다. (이해가 잘 안 가시죠? 저도 이해하는데 좀 걸렸어요...)

 

점장이라는 직책에는 점장이라는 직책을 가진 사람이 올 수 있습니다.

 

종로점 라이언 점장 -> 강남점 점장으로 인사발령이 났습니다. 와 같이 종로점에 있던 라이언 점장은 강남점 점장으로 올 수 있습니다.

 

점장이라는 직책은 정말 추상적입니다. 그리고 이 추상적인 직책에는 연관관계가 있는 구현체가 올수 있습니다.

 

DIP는 구현체인 클래스에 의존하는 게 아니라... 추상화에 의존하는 겁니다.

 

- Ex) 예시 코드

SOLID원칙을 설명하기 위해 작성된 소스코드입니다. 혹시 이상한 부분이 있다면 댓글을 남겨주세요.

 

메서드 로직은 없습니다. SOLID원칙에 대한 설명이기 때문에 로직에는 중요하지 않기 때문입니다.

 

1. SRP : 단일 책임 원칙(Single responsibility principle)

package com.solid_example;


import com.solid_example.models.*;

/**
 * @author Ryan
 * @description SOLID 원칙 - SRP : 단일 책임 원칙(Single responsibility principle)
 *                                모든 클래스는 하나의 책임만 가져야 한다.
 *
 *                                Main 클래스는 프로젝트 실행에 대한 책임을 갖는다.
 *                                나의 책임이라는 것은 모호합니다. 책임이 클수도 있고, 작을수 있다.
 *                                중요한 기준은 변경입니다. 변경이 있을 때 문제가 생긴다면 그건 단일 책임원칙을 잘 따른 부분이 압니다.
 */
public class Main {

    public static void main(String[] args) {

        Manager ryanManger =  new Manager("Ryan");// 매니저
        Chef frodoChef = new Chef("Frodo ");// 주방장

        HallStaff tubeHallStaff = new HallStaff("Tube"); //홀직원
        KitchenStaff muziKitchenStaff = new KitchenStaff("muzi"); //주방직원

        CalculationStaff apeachCalculationStaff = new CalculationStaff("Apeach "); //계산 직원
        CleanseStaff neoCleanseStaffr = new CleanseStaff("Neo");// 세척 직원

        /**
         * @author Ryan
         * @description SOLID 원칙 - LSP - 리스코프 치환 원칙(Liskov substitution principle)
         *                                객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
         *
         *                                인터페이스로 설정했기 때문에 하위 구현체가 변경되도 규약을 지킬수 있습니다.
         */
        Schedule monday = new Schedule("월요일", tubeHallStaff, muziKitchenStaff, neoCleanseStaffr, apeachCalculationStaff);
        Schedule tuesday = new Schedule("화요일", ryanManger, frodoChef, tubeHallStaff, ryanManger);

        System.out.println("monday = " + monday);
        System.out.println();
        System.out.println("tuesday = " + tuesday);

    }
}

 

2. OCP : 개방-폐쇄 원칙(Open/closed principle)

package com.solid_example.models;

import com.solid_example.interfaces.Calculation;
import com.solid_example.interfaces.Cleanse;
import com.solid_example.interfaces.Serving;

/**
 * @author Ryan
 * @description 점장 클래스
 *
 *              SOLID 원칙 - SRP : 단일 책임 원칙(Single responsibility principle)
 *                                모든 클래스는 하나의 책임만 가져야 한다.
 *
 *                                점장 클래스는 홀을 총괄하면서 다른 홀직원에 업무를 도와줄 수 있다.
 *
 *                          OCP : 개방 - 패쇄 원칙(Open/closed principle)
 *                                소프트웨어 요소는 확장에는 열려 있지만, 수정에는 닫혀 있어야 한다.
 */
public class Manager implements Calculation, Serving, Cleanse {

    private String name;

    public Manager(String name) {
        this.name = name;
    }

    @Override
    public int makeCalculation(int money) {
        return 0;
    }

    @Override
    public void cleaning() {

    }

    @Override
    public void washingDishes() {

    }

    @Override
    public void toServeFood(Food food) {

    }

    @Override
    public void removeFood() {

    }

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

 

3. LSP : 리스 코프 치환 원칙(Liskov substitution principle)

/**
 * @author Ryan
 * @description SOLID 원칙 - LSP - 리스코프 치환 원칙(Liskov substitution principle)
 *                                객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
 *
 *                                인터페이스로 설정했기 때문에 하위 구현체가 변경되도 규약을 지킬수 있습니다.
 */
Schedule monday = new Schedule("월요일", tubeHallStaff, muziKitchenStaff, neoCleanseStaffr, apeachCalculationStaff);
Schedule tuesday = new Schedule("화요일", ryanManger, frodoChef, tubeHallStaff, ryanManger);

System.out.println("monday = " + monday);
System.out.println();
System.out.println("tuesday = " + tuesday);

 

4. ISP : 인터페이스 분리 원칙(Interface segregation principle)

package com.solid_example.interfaces;

/**
 * @author Ryan
 * @description SOLID 원칙 - ISP : 인터페이스 분리 원칙(Interface segregation principle)
 *                                하나의 인터페이스 보다는 특정 클라이언트를 위한 인터페이스를 여러 개 범용적으로 만드는게 좋다.
 *
 *                                계산이라는 인터페이스는 점장 클래스와 홀직원 클래스가 implements 해서 사용한다.
 *                                특정 클래스가 아닌 범용적으로 사용할 수 있게 한다.
 */
public interface Calculation {
    int makeCalculation(int money);
}

 

5. DIP : 의존관계 역전 원칙(Dependency inversion principle)

package com.solid_example.models;

import com.solid_example.interfaces.Calculation;
import com.solid_example.interfaces.Cleanse;
import com.solid_example.interfaces.Cook;
import com.solid_example.interfaces.Serving;


/**
 * @author Ryan
 * @description SOLID 원칙 - DIP : 프로그래머는 "추상화에 의존해야하며, 구체화(구현체)에 의존하면 안된다." 의존성 주입 원칙을 따르는 방법중 하나입니다.
 *
 *                                Serving, Cook, Cleanse, Calculation 은 특정 클래스가 아닌 Interface 입니다.
 *                                구체화(구현체)에 의존하지 않고 추상화에 의존하고 있습니다.
 *
 *                                이렇게 설정했기 때문에 해당 인터페스를 implements 한 클래스는 다형성에 의해 의존관계가 성립됩니다.
 */
public class Schedule {
    private String day; // 근무 요일
    private Serving serving; // 서빙직원
    private Cook cook; // 요리직원
    private Cleanse cleanse; // 세척직원
    private Calculation calculation; // 계산 직원

    public Schedule(String day, Serving serving, Cook cook, Cleanse cleanse, Calculation calculation) {
        this.day = day;
        this.serving = serving;
        this.cook = cook;
        this.cleanse = cleanse;
        this.calculation = calculation;
    }

    @Override
    public String toString() {
        return "Schedule{" +
                "day='" + day + '\'' +
                ", serving=" + serving +
                ", cook=" + cook +
                ", cleanse=" + cleanse +
                ", calculation=" + calculation +
                '}';
    }
}

 

 

소스 저장소

Github : https://github.com/Ryan-Sin/solid_example

 

GitHub - Ryan-Sin/solid_example

Contribute to Ryan-Sin/solid_example development by creating an account on GitHub.

github.com