본문 바로가기
Today I Learned 2024. 9. 24.

(24.09.24) 객체 지향 설계 원칙, SOLID 원칙

이력서/자소서 작성과 면접 내용을 체계적으로 Notion에 정리하면서

추가적으로 정리하는 내용에 대해서 블로그에 하나씩 더 자세하게 나만의 글로 적고 학습하려고 한다.

 

객체 지향 프로그래밍을 위해 Java언어와 Spring 프레임워크를 사용하면서,

가장 많이 사용한 것이 IoC 제어의 역전과 DI 의존성 주입을 가장 중심적으로 고려하면서 프로젝트를 진행했다.

 

하지만, 객체지향의 디자인패턴을 다룰 때, SOLID 원칙을 고려한다고만 간단하게 알고 있었지,

정확하게 각 원칙을 정리할 필요가 있었기 때문에, 해당 부분을 찾아가고,

직접 정리하면서 최대한 기억을 하려고 했다.

 

간단할 수 있지만, Java  문법을 하면서 익혔던 polymorphism 등의 개념을 정확히 고려한 것을

그대로 프로그래밍 구조를 구축을 할때 활용을 할 수 있는 일종의 원칙이므로,

Java문법의 index를 Summary한다고 가정하고 정리를 할 수 있었다.

 


SOLID 원칙

  • 객체 지향 프로그래밍 설계에서 유지 보수, 확장을 위한 5가지 설계 원칙
  • 불필요한 복잡성과 이에 따른 리소스 낭비를 방지
  • 특정 Framework 또는 Library, 기술에 국한되지 않는 패턴이 아닌 일종의 원칙
    • 객체 지향을 기본으로 다루지 않아도 충분히 지켜야 하는 프로그래밍 설계 법칙
    • 적용 순서나 모든 원칙을 반드시 무조건적으로 지켜야할 필요는 X
  • 추상화, 상속, 다형성의 개념을 기본적으로 솰용

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

  • 하나의 클래스는 하나의 책임, 하나의 기능에 집중
    • 여러가지 책임을 가지는 설계 시, 클래스 변경에서 다른 책임에 의한 기능까지 영향을 끼칠 수 있으므로
    • 어떠한 법칙으로 분류가 된 것이 아니기 때문에 적용하는 Domain의 특성을 잘 활용할 수 있도록 클래스를 분리
  • 코드의 가독성을 높일 수 있는 원칙
  • 다른 원칙들의 가장 기본이 되는 원칙

Example

public class InvoicePrinter {
    public void print(Invoice invoice) {
        // 송장 출력 로직
    }
}
  • 위의 InvoicePrinter 클래스는 송장을 출력하는 기능을 담당, 해당 기능의 책임이 있는 것이지, 데이터를 전송받거나, 데이터를 가공하는 기능과는 상관 X ****

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

  • 확장 시에는, 열려있어야 하고, 수정/변경 시에는 닫형 있어야 하는 원칙
  • 새로운 기능의 추가는 코드 수정 없이 새로운 기능만으로 확장, 추가
    • 변경이 발생하여 기존 코드의 기능 및 에러의 가능성을 최소화 시키는 원칙
  • 일종의 interface의 역할을 활용

Example

public interface Shape {
    double area();
}

public class Rectangle implements Shape {
    public double area() {
        return length * width;
    }
}

// 새로 추가가 된 코드
public class Circle implements Shape {
    public double area() {
        return Math.PI * radius * radius;
    }
}

  • interface를 통해서 기존 코드를 수정하거나 변경하지 않고서도 Circle 클래스처럼 새로운 기능, 역할이 추가적으로 확장

3. LSP, Liskov Substitution Principle, 리스코프 치환 법칙

  • 자손 클래스는 언제나 조상 클래스로 대체가 가능할 수 있도록 하는 법칙
  • 상위 클래스에서 받은 메서드의 동작은 정의된 대로 유지시켜야
    • 상속의 유지
    • 자식 클래스에서 상위 클래스의 메서드를 사용시, 그대로 사용할수 있도록 해야하는 것 = 업캐스팅이 되어도 해당 메서드가 똑같이 작동이 되어야한다는 것
  • Overiding을 활용
    • 따라서 LSP를 지키면서 가능

Example

public class Bird {
    public void fly() {
        System.out.println("날 수 있습니다.");
    }
}

// 자손 클래스
public class Chicken extends Bird {
		// LSP 위반
    @Override
    public void fly() {
        throw new UnsupportedOperationException("날 수 없습니다.");
    }
}
  • 조상 클래스의 어떤 기능이 참 거짓 이런 것에 전혀 상관 없이, 자손 클래스에서 그 기능을 일관되게 사용하지 못하도록 하는 것이 위반

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

  • 인터페이스는 사용되지 않는 메서드를 포함하지 않는 최소한의 단위로 분리되어야한다는 원칙
    • 구현할 필요가 없는 불필요한 메서드까지 포함시켜서 불필요한 의존성이 발생을 방지
    • 적용할 클라이언트에 따라 분리가 기본 원칙
  • 추가로 계속 분리를 하는 것이 아닌, 분리된 상태를 유지시키는 것이
  • SRP와 같은 맥락에서 interface의 책임을 묻는 원칙

Example

// ISP 위반, Worker 인터페이스가 work, eat을 무조건 해야한다는 법은 없음
public interface Worker {
    void work();
    void eat();
}

// ISP를 지킨 인터페이스의 분리
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}
  • 따라서, Interface의 네이밍인 -able 형태 역시 특정 기능이나 동작을 할수 있음을 보여주는 것도 같은 맥락

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

  • 고수준 모듈(High-level modules)은 저수준 모듈(Low-level modules)에 의존하는 것이 아니라 추상화를 거쳐 서로 주입을 통해 의존 해야하는 법칙
    • 제어의 역전, 의존성 주입과 같은 맥락

Example

// DIP를 따른 설계

// Repository
public interface Repository {
    void save(Data data);
}

// Service
public class Service {
    private final Repository repository;

    // 생성자를 통한 의존성 주입
    public Service(Repository repository) {
        this.repository = repository;
    }
		
		// Service의 비지니스 로직
    public void process(Data data) {
        repository.save(data); // 인터페이스에 의존
    }
}


 

해당 원칙들은, 반드시 지켜져야만 하는 원칙이 아닌 "객체 지향" 즉, 인스턴스/객체 중심적으로 유연적인 코드를 작성하기 위한 가장 기본이 될 수 있는 구성이고,

때에 따라서는 굳이 원칙들을 지키지 않더라도 보다 효율적으로 작성되고 구현이 된 부분이 있을 수 있다는 점을 잊지 말아야 할 것이다.

 

운이 좋은 것인지.. 지금까지 여러 프로젝트를 진행했으나, 간단히 작성한다는 명목하에 코드나 리팩토링해서 하나로 합쳐무조건 좀더 간소화하는 방향으로 SRP, ISP 원칙을 어기는 의견보다

최대한 도메인(어플리케이션) 기능에 맞게 분리하자고 본인이 의견을 냈던 경험이 대다수라

 

나름대로 객체지향을 잘 지키려고 스스로 노력한 것이라고 생각이된다.