[스프링 기초]객체 지향 설계의 5가지 원칙(SOLID)

LTEN·2022년 7월 22일
0

스프링 기초

목록 보기
1/6

※본 글은 김영한님의 '자바 스프링 완전정복 시리즈' 강의를 바탕으로 작성한 글입니다.

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

'로버트 마틴'은 좋은 객체 지향 설계의 5가지 원칙을 다음과 같이 정리했습니다.

  • SRP(Single Responsibility Principle), 단일 책임 원칙
  • OCP(Open/Closed Principle), 개방-폐쇠 원칙
  • LSP(Liskov Substituion Priciple), 리스코프 치환 원칙
  • ISP(Interface Segregation Principle), 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle), 의존관계 역전 원칙

각 원칙의 앞글자를 따 SOLID 라고 표현합니다.

이제부터 각각의 내용에 대해 좀 더 자세히 살펴 보겠습니다.

SRP(Single Responsibility Principle)

: 하나의 클래스는 하나의 책임만 가져야 한다.

이름 그대로 하나의 클래스가 하나의 책임만을 가져야 한다는 원칙입니다.
여기서 '책임'이라는 것은 어떤 의미일까요?

A 라는 클래스가 존재하고, 데이터를 입력 받고 출력하는 역할을 할 때, 이때 A 클래스가 '읽기'와 '쓰기'에 책임이 있다고 표현합니다.

SRP는 위 예시와 같은 경우와 같이 하나의 클래스에 여러 책임을 주는 것보다, 읽기만을 책임지는 클래스 등으로 나눠야 한다는 것을 의미합니다.

사실 '책임'이라는 것은 상황에 따라 그 범위가 애매해 질 수 있고, 그 기준은 변경의 파급효과로 생각하면 됩니다.

결국 하나의 클래스가 너무 많은 책임을 갖게 되면 일부 기능(책임)만 수정하려해도 클래스 자체가 수정되므로, 책임에 따라 적절하게 클래스로 구분해야 된다는 의미입니다.

OCP(Open/Closed Priciple)

: 확장에는 열려있으나, 변경에는 닫혀야 한다.

확장은 가능하나 수정은 하지 않아야 된다는 말 자체가 모순적으로 보입니다.

이해를 위해 조금 더 풀어쓰면 다음과 같이 표현할 수 있습니다.

'Client의 수정에는 닫힌 상태로, 역할의 수행체는 확장할 수 있다.'

다형성을 이용한 다음 예시를 통해, 어떻게 그러한 것이 가능한 것이고 유용한지 살펴보겠습니다.

interface Repository{
    public void save();
}
class MemoryRepository implements Repository{
    public void save(){
        // 메모리에 저장
    }
}
class DBRepository implements  Repository{
    public void save(){
        // DB에 저장
    }
}

먼저 위와 같이 Repository를 구현한 2개의 클래스가 존재하고,

class Service{
    // Repository repository = new MemoryRepository();
    // 구현체만 갈아끼우기
    Repository repository = new DBRepository();
    
    void save(){
    // save 메서드의 변경 X
        repository.save();
    }
}

기존에는 Service에서 메모리에 데이터를 저장하다가, DB에 저장하도록 확장하고 싶을 때, repository의 구현체만 갈아끼우면 확장이 가능합니다.

Service.save 에 대한 수정은 닫힌 상태로, Service 클래스의 기능이 메모리에 저장하는 것에서 DB에 저장하는 것으로 확장되었습니다.

사실 위 예제에서 구현체를 갈아끼우는 것 또한 수정이므로 OCP를 위반한다고 볼 수 있습니다.
(이러한 문제를 해결하여 좋은 객체 지향 설계를 할 수 있도록 도와주는 것이 것이 스프링 프레임워크의 본질이라고 할 수 있고, 이에 대해서 다음 글에 추가로 정리하겠습니다. )

LSP(Liskov Substitution Principle)

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

단순하게 정리하면, 인터페이스 규약을 잘 지키라는 것입니다.

극단적인 예시로, '앞으로 가기'라는 기능이 필요한 front()라는 메서드에서 뒤로 가기의 기능을 구현하면 안됩니다.
구현체에 따라 속도는 다르더라도 그 기능은 무조건 보장되어야 합니다.

ISP(Interface Segregation Principle)

: 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.

바꿔말해서 인터페이스를 나눌 때, 클라이언트의 의존 범위에 따라 나누면 된다는 의미입니다.

인터페이스 A에 a(), b(), c()가 존재하고 클라이언트 B가 b() 만을 이용한다고 하면, ISP에 따라 인터페이스 A를 더 나눠야합니다.

DIP(Depedency Inversion Principle)

: 추상화에 의존하고, 구체화에 의존하면 안된다.

인터페이스만 아는 상태로 이용할 수 있어야 된다고 생각하면 됩니다. 구체화된 메서드에 의존하면 해당 메서드를 유연하게 유지보수할 수 없기 때문입니다.
ex) 다른 구현체로 변경하면 오류가 발생

class Service{
    // Repository repository = new MemoryRepository();
    // 구현체만 갈아끼우기
    Repository repository = new DBRepository();
    
    void save(){
    // save 메서드의 변경 X
        repository.save();
    }
}

또한 위의 OCP 예시에서와 같이 직접적으로 DBRepository()와 같은 구현체에 의존하고 있는 경우가 없어야 된다는 것입니다.
이 또한 스프링을 통해 해결이 가능합니다.

profile
백엔드

0개의 댓글