본문 바로가기
WEB/Spring

[Spring] 객체지향 설계의 5원칙 (SOLID) #다형성 #의존관계 #인터페이스 분리

by 정권이 내 2023. 1. 11.

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

 

SOLID란 클린코드의 저자인 로버트 마틴이 제시한 좋은 객체지향 설계의 5가지 원칙을 정리한 내용입니다.

  • SRP: 단일 책임 원칙(single responsibility principle)
  • OCP: 개방-폐쇄 원칙(Open/Closed principle)
  • LSP: 리스코프 치환 원칙(Liskov substituion principle)
  • ISP: 인터페이스 분리 원칙(Interface segregation principle)
  • DIP: 의존관계 역전 원칙(Dependency inversion principle)

 

1. SRP, 단일 책임 원칙

단일 책임 원칙이란 한 클래스는 하나의 책임만을 갖는다는 의미입니다. 하나의 책임이란 동작을 뜻하며 의도한 동작과 무관한 동작은 클래스에 포함되어서는 안된다는 것입니다.

만약 Car 라고 하는 자동차의 특성만을 다루는 클래스가 있다고 해봅시다.

class Car {
    public void move() {
        ...
    }
    
    public void stop() {
        ...
    }
    
    public void headLight() {
        ...
    }
    
    public void wiper() {
        ...
    }
    
    ...
    ...
}

 

Car 클래스 내부에는 자동차의 역할과 관련된 여러개의 메서드들이 있는데 이 클래스에 하이패스나 주차장 정산을 위한 결제 관련 기능을 추가 하면 어떻게 될까요?

자동차의 근본적인 역할과는 관련이 없는 결제 관련 기능들이 추가된다면 Car 클래스는 자동차의 역할뿐만 아니라 결제 역할까지도 맡게 되므로 단일 책임 원칙을 지키지 못하게됩니다.

 

단일 책임 원칙을 잘 지켜야 추후에 유지보수할때 편리하고 사이드 이펙트를 방지할수 있습니다.

 

2. OCP, 개방-폐쇄 원칙

OCP는 "확장에는 열려있고 변경에는 닫혀있어야 한다." 로 함축할수있는데 어떤 의미인지 알아보겠습니다.

MemberService 라는 서비스 클래스가 있고 내부에서 MemberRepository 객체를 선언하는데 저장방식에 따라 MemoryMemberRepository 혹은 JdbcMemberRepository를 사용할수 있습니다.

public class MemberService {
	MemberRepository m = new MemoryMemberRepository();
    ...
}

or

public class MemberService {
	MemberRepository m = new JdbcMemberRepository();
    ...
}

 

이때 MemberService 입장에서는 레포지토리 선택에 대해서는 확장이 열려있지만 어떤 레포지터리를 사용할지에 대해서는 직접적으로 MemberService 클래스 내부에서 변경 해야하므로 변경에 대해서 닫혀있지는 않게 됩니다.

이 문제를 해결하려면 MemberService와 MemberRepository 객체간 연관관계를 맺어주는 별도의 설정자가 필요합니다. 이 설정자의 역할을 하는것이 Spring Container 이고 IOC, DI 개념이 사용됩니다.

 

3. LSP, 리스코프 치환 원칙

프로그램의 객체는 프로그램의 정확성을 깨지않으면서 하위타입의 인스턴스로 바꿀수 있어야 한다. 말이 조금 어려운데 단순하게 생각하면 특정 인터페이스를 상속받은 하위 클래스는 인터페이스의 규약을 지켜야하는 것입니다.

컴파일이 성공한다고 해서 무조건 프로그램이 정상인것이 아니라 인터페이스의 기능이 규약에 맞게 정상적으로 수행되야 합니다.

 

public interface Vehicle {
    // 전진 메서드
    void excel();
    // 후진 메서드
    void reverse();
}
public class Truck extends Vehicle {
    
    @Override
    public void excel() {
        goBack();
    }
    
    @Override
    public void reverse() {
        goForward();
    }
    // 인터페이스 규약과 일치하지 않는 기능 구현
}

 

만약 Vehicle 인터페이스의 Excel 이라는 기능이 전진, Reverse는 후진의 기능을 수행하는것이 인터페이스의 규약일때 이를 상속받은 하위 클래스에서 기능을 반대로 구현하여 Excel에서 후진을하고 Reverse에서 전진을 한다면 이는 리스코프 치환 원칙을 위배하는것입니다.

 

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

인터페이스 분리 원칙은 "클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다"는 원칙입니다. 인터페이스에 여러가지 기능을 추가하였는데 실제 클라이언트에서 사용하는 메서드가 절반밖에 되지 않는다면 인터페이스가 비효율적으로 설계된것이고 인터페이스를 분리하여 클라이언트가 반드시 사용할 메서드만 구현하는것입니다.

 

public interface Vehicle {
    void excel();
    void reverse();
    void flying();
    void landing();
}

위의 코드처럼 Vehicle 이라는 인터페이스에는 excel(), reverse(), flying(), landing() 각각 전진, 후진, 이륙, 착륙에 대한 기능이 구현되 있고 Car, AirPlane 이라는 인터페이스에서 Vehicle 인터페이스를 상속받았습니다.

img

 

비행기인 AirPlane 인터페이스에서는 Vehicle 인터페이스의 모든 기능을 구현할수 있지만 자동차인 Car 인터페이스에서는 이륙과 착륙에 해당하는 flying, landing 메서드는 불필요한 기능입니다. 먼 미래에는 자동차가 날수도 있겠지만 현재는 그런기술이 없으므로 인터페이스를 분리하여 기능을 세분화할 필요가 있습니다.

 

5. DIP, 의존관계 역전 원칙

프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다." 쉽게 이야기하면 구현 클래스에 의존하지 말고 인터페이스에 의존 하라는 뜻입니다.

 

img

역할(Role)에 의존해야 한다는 것과 같은말인데 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할수 있다는 것입니다. 데이터 저장 기능을 추상화한 MemberRepository 인터페이스가 있을때 해당 인터페이스를 상속받은 클래스는 Jdbc, Memory 등등 여러 방식으로 구현이 되었겠지만 클라이언트 입장에서는 MemoryRepository 인터페이스에만 의존하도록 하는것입니다.

 

위에 1번에서 설명한 OCP(개방 폐쇄 원칙)의 예제 코드를 다시 보면 DIP 원칙을 위반하는것을 확인할수 있습니다.

public class MemberService {
    // MemberService 클라이언트가 구현 클래스를 직접 선택
	MemberRepository m = new MemoryMemberRepository();
    ...
}

or

public class MemberService {
    // MemberService 클라이언트가 구현 클래스를 직접 선택
	MemberRepository m = new JdbcMemberRepository();
    ...
}

 

MemberService 클래스에서 MemberRepository 인터페이스를 의존함과 동시에 구현 클래스들도 의존하기 때문입니다. 그래서 스프링에서는 OCP 와 DIP 원칙을 동시에 지킬수 있도록 DI(Dependency Injection), 의존관계 주입 기술을 사용합니다.

 

 

출처 : 인프런 - 우아한 형제들 기술이사 김영한의 스프링 완전 정복 (스프링 핵심원리 - 기본 편)

반응형

댓글