객체지향 설계 5원칙 - SRP, OCP, LSP,ISP

장숭혁·2024년 6월 12일
0

TIL작성

목록 보기
45/60
post-thumbnail

1. SRP(단일 책임 원칙)

결합도는 낮추고 응집도를 높여야 한다.

  • 결합도 : 클래스 간에 얼마나 많이 의존하고 있는가를 나타낸다. 상호 의존도가 낮을수록 코드의 유지・보수가 유리해진다. 끈끈이가 서로 붙듯이 결합도 높은 클래스 간 강하게 연결되어 있다면 떼어낼 때 끈끈이를 완전히 제거해야 되는 것처럼 코드를 갈아 엎어야 될 수 있을것이다. 포스트 잇처럼 필요에 의해서 붙였다 쉽게 땔 수 있어야 한다.

  • 응집도 : 얼마나 많이 단일 클래스에서 책임(기능)을 가지고 있는지 나타낸다. 단일 클래스에 하나의 책임을 가지면 응집도가 가장 높은 상태에 해당되며 이 역시 유지・보수에 유리하다. 응집도가 낮은 상태는 서로 관련 없는 기능들이 하나의 클래스에 집중되어 작성된 상태이다.

🚧 결합도가 높은 코드😫

class Student

// B 클래스가 student 클래스에 강하게 결합된 경우
class B {
    private Student student;

    public B(Student student) {
        this.student = student;
    }

    public void 데이터_조작하기() {
        // B 클래스가 A 클래스의 내부 구현에 강하게 의존하고 있음
        int value = student.getData();
        value *= 2;
    }
}
  • 코드의 유지 보수가 어려워진다.(둘 중 하나를 수정하려고 할때 같이 수정해야할 가능성이 크다.)
  • 강하게 결합되어 있기 때문에 단위테스트를 하기 어려워진다.

🚧 결합도가 낮은 코드🤩

class Student // 한 학생의 정보를 저장하는 클래스

class StudentDatabase // student들을 List로 저장하는 클래스

class StudentFileManager //student List를 파일에 저장하는 클래스
  • 결합도가 낮은 코드는 서로의 기능에 영향을 미치지 않고 데이터만 서로 교환한다.

⛏(Worst)내용 결합 (Content Coupling):

어떤 클래스의 변수나 메소드를 직접적으로 사용하는 경우 강하게 결합되어 있으며 제일 좋지 않은 결합 방식이다.

⛏공통 결합 (Common Coupling):

여러 클래스가 하나의 전역 변수(static)에 강하게 결합되어 있는 경우

⛏제어 결합 (Control Coupling):

외부의 클래스에서 한 클래스의 제어를 넘겨주는 것, flag을 설정하여 true일 경우와 false 일 경우 다른 동작을 지시할 수 있다.

⛏(Moderate)스탬프 결합 (Stamp Coupling):

클래스의 매개변수로 배열이나 오브젝트, 스트럭처가 전달되어 실행되는 방식

⛏자료 결합 (Data Coupling):

순수한 데이터 요소를 전달받아 실행하도록 하는 방식이다.

⛏외부 결합 (External Coupling):

클래스가 import를 통해서 연결되거나 구체적인 클래스를 통해서 연결될 때

SummaryReport summaryReport = new SummaryReport();
ReportManager reportManager = new ReportManager(summaryReport);

⛏(Best) 메시지 결합 (Message Coupling):

순수하게 데이터를 통해서 객체간 통신하는 방식, 메시지를 통해서만 상호작용한다.

Report summaryReport = new SummaryReport();
Report detailReport = new DetailReport();
Report customReport = new CustomReport("This is a custom report.");

🚧 응집도가 낮은 코드😫

class Utility{
	public void addStudent(String name, int age) 				// 학생 추가
    public List<Student> getStudents() 			 				// 학생 List 받기
    public void saveToFile(String filename) throws IOException //학생 데이터를 파일에 저장하기
    public void saveToDatabase() 								//학생 데이터를 데이터베이스에 저장하기
    public void deleteFromDatabase(String name) 				//데이터베이스에서 학생 데이터 삭제하기
    public void printStudents() 								// 학생들읠 목록을 출력하기

}
  • 하나의 클래스에 많은 책임이 담겨 있어 유지보수성과 재사용성이 떨어진다.
  • 코드 수정시 오류가 발생될 위험이 커진다.

🚧 응집도가 높은 코드🤩

class Student {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}
  • Student 클래스 안에 관련된 기능만 추가하였다.

  • 다른 프로그램에서 재사용이 용이하다.

  • 코드 변경시 다른 클래스에 영향을 끼치지 않는다.

  • 우연 응집도에 가까운 클래스는 낮은 응집도를 가지고 있으며 기능 응집도에 가까울 수록 높은 응집도를 가지게 된다.

    ⛏(Worst) 우연 응집도 (Coincidental Cohesion)

  • 클래스 안 구성 요소들이 우연히 같이 있는 경우(서로 관련이 하나도 없음)

    ⛏논리 응집도 (Logical cohesion)

  • 논리적으로 비슷한 , 관련된 기능들이 한 클래스에 모여 있지만 서로 관계가 밀접하지 않는 경우

    ⛏시간적 응집도 (Temporal Cohesion)

  • 특정 시점에 수행해야 하는 기능들이 한 클래스에 모여 있는 경우

  • 서로 연관이 적을 수 있다.

    ⛏(Moderate) 절차 응집도 (Procedural Cohesion)

  • 특정 순서로 수행되는 기능들이 한 클래스에 응집되어 있는 경우

  • 하나의 클래스에 있는 메소드들을 여러 개 호출하는 경우

    ⛏통신 응집도 (Communication Cohesion) 

  • 동일한 데이터를 입력받아 다르게 수행하는 기능들이 모여있다.

  • 같은 파일에서 데이터를 읽고, 데이터를 처리하며, 데이터를 출력하는 작업이 한 모듈에 포함된 경우

    ⛏순차 응집도 (Sequential Cohesion)

  • 한 기능의 출력이 다른 기능의 입력값이 되어 그 기능을 수행하는 경우

  • 데이터 입력, 데이터 처리, 데이터 출력이 순차적으로 연결된 모듈.

    ⛏(Best) 기능 응집도 (Functional Cohesion)

  • 한 클래스의 기능들이 동일한 목표를 위해 수행되는 경우에 해당된다.

  • 가장 높은 응집도. 모듈이 하나의 기능에 집중되어 있어 이해와 유지보수가 용이








    2. OCP(개방 폐쇄 원칙)

    기존의 코드를 변경하지 않으면서 확장에는 열려있어야 한다. 즉 수정은 폐쇄, 확장은 개방 되어야 한다. 확장은 새로운 기능의 추가를 의미한다.

  • 인터페이스나 추상 클래스로 상속받아 구현된 클래스를 주입함으로써 구현됨

  • OCP 원칙을 따른 JDBC 인터페이스, Mybatis, Hibernate 등에서 찾아 볼 수 있다.

    새로운 요구사항이나 변경사항이 발생할때마다 내부의 코드를 변경하면 버그 발생 가능성을 높이게 된다. 이때 중간에 인터페이스나 추상클래스를 두고 그것을 상속・구현한 클래스를 주입하는 방식으로 확장성을 향상시키고 유지・보스를 용이하게 한다.
    클래스를 주입하는 방식은 DIP(의존 역전 원칙)과 관련이 있다.

    🚧 예시

인터페이스 작성 및 동작 메소드 선언

interface MessagePrinter {
    void print(String message);
}

이 인터페이스를 구현한 여러 클래스 작성

class A_MessagePrinter implements MessagePrinter {
   @Override
   public void print(String message) {
       System.out.println("A: " + message);
   }
}
class B_MessagePrinter implements MessagePrinter {
    @Override
    public void print(String message) {
        System.out.println("B: " + message);
    }
}

선언하는 방식 , 주입하는 방식

public class Main {
   public static void main(String[] args) {
       
       MessagePrinter aPrinter = new AMessagePrinter();
       
       MessagePrinter bPrinter = new BMessagePrinter();
   }
}

객체지향 설계 원칙과 관련된 그레디 부치(Grady Booch)의 조언
1. 추상화란 다른 모든 종류의 객체로부터 식별될 수 있는 객체의 본질적인 특징이다.
2. 객체 지향 프로그래밍의 목적은 소프트웨어 구성 요소의 재사용을 촉진하는 것이다.
3. 좋은 설계는 단순성, 유연성, 견고성의 상충하는 요구 사이에서 적절한 균형을 찾는 것이다.
4. 다형성은 서로 다른 객체들이 동일한 메시지에 대해 각자의 방식으로 응답할 수 있는 능력입니다.






3. LSP(리스코프 치환 원칙)

⛏서브타입은 언제나 자신의 상위 타입으로 교체될 수 있어야 한다.

class Bird {
    public void fly() {
        System.out.println("Bird is flying");
    }
}

class Sparrow extends Bird {
    @Override
    public void fly() {
        System.out.println("Sparrow is flying");
    }
}

public class Main {
    public static void main(String[] args) {
        Bird bird = new Sparrow(); // 상위 타입 변수에 하위 타입 인스턴스 할당
        bird.fly(); // "Sparrow is flying" 출력
    }
}

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

class Rectangle {
    protected int width, height;
    
    public void setWidth(int width) {
        this.width = width;
    }

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

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

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // 정사각형의 특성에 맞게 높이도 설정
    }

    @Override
    public void setHeight(int height) {
        this.height = height;
        this.width = height; // 정사각형의 특성에 맞게 너비도 설정
    }
}

public class Main {
    public static void main(String[] args) {
        Rectangle rect = new Square(); // 상위 타입 변수에 하위 타입 인스턴스 할당
        rect.setWidth(5);
        rect.setHeight(10);
        System.out.println(rect.getArea()); // 예상되는 출력값은 50이지만, 실제로는 100
    }
}

⛏다형성을 지원하기 위한 원칙

interface Shape {
    double getArea();
}

class Circle implements Shape {
    private double radius;
    
    public Circle(double radius) {
        this.radius = radius;
    }
    
    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

class Rectangle implements Shape {
    private double width, height;
    
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    
    @Override
    public double getArea() {
        return width * height;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape shape = new Circle(5); // Circle로 교체
        System.out.println("Circle Area: " + shape.getArea());
        
        shape = new Rectangle(4, 5); // Rectangle로 교체
        System.out.println("Rectangle Area: " + shape.getArea());
    }
}

⛏하위 클래스는 인터페이스 규약을 지켜서 작성되어야 한다.(어떤 구현체를 사용하든 호출부에서 기대하는대로 동작되어야 한다.)

interface Vehicle {
    void startEngine();
}

class Car implements Vehicle {
    @Override
    public void startEngine() {
        System.out.println("Car engine started");
    }
}

class ElectricCar implements Vehicle {
    @Override
    public void startEngine() {
        System.out.println("Electric Car started silently");
    }
}

public class Main {
    public static void main(String[] args) {
        Vehicle vehicle = new Car();
        vehicle.startEngine(); // "Car engine started" 출력
        
        vehicle = new ElectricCar();
        vehicle.startEngine(); // "Electric Car started silently" 출력
    }
}

⛏상속을 통한 재사용은 기반 클래스와 서브 클래스 사이에 IS-A 관계가 있을 경우로만 제한되어야 한다.

class Animal {
    public void makeSound() {
        System.out.println("Animal sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Bark");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog(); // IS-A 관계
        animal.makeSound(); // "Bark" 출력
    }
}

⛏그 외의 HAS-A 관계 등에서는 인터페이스 합성을 이용해 재사용 해야한다.

interface Engine {
    void start();
}

class GasEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Gas engine started");
    }
}

class ElectricEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Electric engine started");
    }
}

class Vehicle {
    private Engine engine;
    
    public Vehicle(Engine engine) {
        this.engine = engine;
    }
    
    public void startEngine() {
        engine.start();
    }
}

public class Main {
    public static void main(String[] args) {
        Vehicle gasVehicle = new Vehicle(new GasEngine());
        gasVehicle.startEngine(); // "Gas engine started" 출력
        
        Vehicle electricVehicle = new Vehicle(new ElectricEngine());
        electricVehicle.startEngine(); // "Electric engine started" 출력
    }
}

  • 실제 리스코프 치환 원칙을 지켜 프로그래밍을 하기에는 애매함이 많을 수 있다. 새 클래스의 메소드로 fly를 설정했지만 펭귄은 날 수 없으므로 예외가 발생하며 클래스를 사용하려 할때 현실에 맞게 사용하려면 클래스를 재정의 해주어야 할 수 있다.

🚧 예시

  • 상위 클래스에서는 0원이상 있으면 결제 처리 로직에 진입하도록 설정했지만 현실에 맞게 10원 이상 있을때 넘어가도록 재정의 하였음
class Payment {
    public void process(int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        // 결제 처리 로직
    }
}

class OnlinePayment extends Payment {
    @Override
    public void process(int amount) {
        if (amount < 10) {
            throw new IllegalArgumentException("Minimum amount for online payment is 10");
        }
        // 온라인 결제 처리 로직
    }
}
  • OnlinePayment는 다른 곳에서는 재사용이 불가능 할 수 있으므로 Payment 자체를 재설계 하여야 한다.

4.ISP(인터페이스 분리 원칙)

  • 클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다.
  • 프로젝트 요구 사항과 설계에 따라서 SRP(단일 책임 원칙)/ ISP 를 선택한다.

🚧 ISP 적용 전

interface Worker {
    void work();
    void eat();
    void sleep();
}

class HumanWorker implements Worker {
    public void work() {
        // 작업하는 로직
    }
    
    public void eat() {
        // 식사하는 로직
    }
    
    public void sleep() {
        // 잠자는 로직
    }
}

class RobotWorker implements Worker {
    public void work() {
        // 작업하는 로직
    }
    
    public void eat() {
        throw new UnsupportedOperationException("로봇은 먹을 수 없습니다.");
    }
    
    public void sleep() {
        throw new UnsupportedOperationException("로봇은 잠을 자지 않습니다.");
    }
}

  • 자신이 사용하지 않는 기능들을 억지로 구현 하거나 예외를 던져야 할 수 있다.

🚧 ISP 적용 후

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

interface Sleepable {
    void sleep();
}

class HumanWorker implements Workable, Eatable, Sleepable {
    public void work() {
        // 작업하는 로직
    }
    
    public void eat() {
        // 식사하는 로직
    }
    
    public void sleep() {
        // 잠자는 로직
    }
}

class RobotWorker implements Workable {
    public void work() {
        // 작업하는 로직
    }
}

  • 유, 재, 변, 의 에 좋다.

    • 유지 보수성 향상

    • 재사용성 증가

    • (코드)변경 용이

    • 의존성 감소

⛏ SRP와 상충되는 점?

SRP는 클래스의 단일 책임 원칙으로서 한 클래스의 기능들이 동일한 목표를 위해 수행되는 경우를 목표로 한다. ISP와 SRP는 결합성 측면에서는 상충되지 않으나 응집도 측면에서 상충될 수 있다. Worker 인터페이스는 work(), eat(), sleep() 으로 메소드가 구성되어 있어 SRP 원칙에는 문제가 없으나 Robot은 eat(),과 sleep()이 필요없으므로 ISP에는 문제가 생길 수 있다.
SRP는 클래스에 명확한 책임을 갖게하여 응집도를 높이고, ISP는 클라이언트에 맞춘 인터페이스를 제공하여 불필요한 의존성을 줄인다.

profile
코딩 기록

0개의 댓글

관련 채용 정보