SOLID 원칙

수박참외메론·2022년 8월 15일
1

인프런 김영한 강사님의 수업을 정리한 포스팅입니다.

SOLID 원칙이란

클린코드로 유명한 Robert Martin 이 좋은 객체 지향 설계의 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

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

그치만 하나의 책임이라는 것이 굉장히 모호한 단어다. 책임이 클 수도 있고, 작을 수도 있고 문맥에 따라서 굉장히 다르다.

여기서 SRP 를 얼마나 잘 지키면서 설계했는지의 판단 기준은 바로 변경이다.

변경이 있을 때 파급이 적으면 SRP 를 잘 지키면서 설계했다는 반증이다.

OCP

소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.

모순적이어서 이해하기 힘들 수 있다. 코드를 변경할때 변경하면 안된다는게 무슨 말인가?

이 OCP 에 관련된 예제는 다형성 과 관련된 예시에서 확인할 수 있다.

다형성이란?

다형성의 실용적인 예시로 자동차를 들 수 있다. 자동차들은 차의 기종과 상관없이 핸들, 브레이크, 엑셀 등의 공통적인 특성을 무조건 가지는 기계로 외형이나 다른 기능들로 구별되는 기계 장치이다.
우리가 위의 그림처럼 K3 를 타다가 아반떼나 테슬라로 차를 바꾼다 하더라도 운전을 할 수 있는 점이, 다형성이라고 볼 수 있다.

공연도 마찬가지이다. 공연에는 따로 역할이 있고 그 역할을 맡은 실제 배우가 있기 마련인데, 로미오나 줄리엣의 역할을 장동건이 하든, 김태희가 하든 역할 자체는 변하지 않는다.
여기서는 이 역할 이 다형성을 가지는 예시이다.

Spring 에서의 OCP

controller - service - repository 구조의 spring 을 생각해보자.
이 때 우리는 어떤 DB 를 사용할지 정하지 않은 상태에서 일단 개발을 해야한다고 가정하자.

그럼 최대한 DB 관련된 repository 코드를 일단 제일 간단한 메모리 DB 를 사용하고, 이를 service 에서 사용한다고 치자.

public class MemberService {
	private MemberRepository memberRepository = new MemoryMemberRepository();
}

이 때 실제 DB 를 정하고 이를 JDBC 로 관리하기로 했다고 해보자. 그럼 service 에서 보고 있는 repository 는 아래와 같이 변해야 할 것이다.

우리는 변경없이 DB 를 바꾸고 싶은 상황이다.

그러면 저렇게 repository 의 구현체인 MemoryMemberRepositoryJdbcMemberRepository 를 그냥 갈아끼워도 다른 코드에 문제가 없을 만큼 비슷하게 기능을 하는 repository 를 구현해야 할 것이다.

하지만 우리가 최대한 할 수 있는 것은

public class MemberService {
	//private MemberRepository memberRepository = new MemoryMemberRepository();
   private MemberRepository memberRepository = new JdbcMemberRepository();
}

이렇게 service.java , 즉 클라이언트 파일을 바꿀 수 밖에 없는 상황이 발생하는 것이다.

이렇듯 Interface(MemberRepository) 에 구현 객체 (MemoryMemberRepository) 를 부여하는 다형성을 사용했음에도 불구하고 OCP 원칙을 지킬 수 없는 상황 이 발생할 수 밖에 없게 된다.

그럼 어떻게 해야 OCP 를 지킬 수 있을까?

이는 Spring Container 가 해결해 주는 중요한 요소로 DI 나 IOC 컨테이너를 활용해서 우리의 클라이언트 코드를 변경하지 않고도 변수에 구현체를 자동으로 할당하여 OCP 가 지켜지고, 더욱 객체지향적 코드를 생산할 수 있게 된다.

자세한 내용은 여기 에 정리한 바 있다.

LSP

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

다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것으로 인터페이스를 구현한 구현체를 믿고 사용하려면 이 원칙이 필요하다.

예를 들어 자동차 인터페이스의 엑셀은 앞으로 가라는 기능이므로, 뒤로 가게 구현하면 LSP 위반이다. 속도가 자동차마다 달라질 순 있어도 느리더라도 앞으로 가야지 LSP 를 지킨 것이라 볼 수 있다.

ISP

특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.

기능이 너무 많으면 관리하기 복잡하기도 하고 해서 기능별로 적당한 크기로 나누는 게 중요하다는 원칙.

예를 들어 자동차 인터페이스는 운전 관련 인터페이스(기능), 정비 관련 인터페이스(기능) 으로 분리하여 사용자 클라이언트가 운전자 클라이언트, 정비사 클라이언트로 분리하여 관리가 용이해진다.

이렇게 기능별로 인터페이스를 쪼개게 되면 명확해지고 대체 가능성이 높아진다. 덩어리가 크면 인터페이스를 다 구현하는데 부담이 되는 반면, 적은 기능만을 구현하면 되면 구현체를 만드는데 부담이 되지 않으므로.

스프링도 코드를 까보면 실제로 엄청 잘게 나눠져 있다고 한다.

DIP

구체화에 의존하지 말고 추상화에 의존하는 것이 좋다.

앞서 설명했던 이 그림에서 다시 설명이 가능한데, 운전자는 자동차 역할에 대해 알아야지 k3 를 깊게 알 필요가 없다. (물론 알면 좋긴 하겠지만)

이게 가능하려면 역할에 의존하게 해야지, 실제 구현체에 의존하면 변경이 매우 힘들어진다.

코드 예시

하지만 앞서 OCP 에서 설명한 MemberService 클래스는 MemberService 클라이언트가 구현 클래스(JdbcMemberRepository)를 직접 선택함으로 인터페이스와 구현 클래스 동시에 의존한다.

 public class MemberService {
    private MemberRepository memberRepository = new JdbcMemberRepository();
 }

그래서 이는 DIP 를 위반하였다고 볼 수 있다.

그럼 어떻게 해야지 의존안하도록 코드를 작성할 수 있을까?

Spring 에서는 이도 마찬가지로 코드에 직접 구현체를 달아주지 않더라도 자동으로 spring container 가 달아주도록 설계하여 DIP 를 지키도록 하였다.

이는 spring 을 공부해가며 차차 정리해 나가도록 하자.

정리

  • 객체 지향의 핵심은 다형성
  • 다형성 만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수 없다.
  • 다형성 만으로는 구현 객체를 변경할 때 클라이언트의 코드 변경을 막을 수 없다.
  • 다형성 만으로는 OCP, DIP 를 지킬 수 없다.
  • 뭔가가 더 필요하다.
profile
하루하루는 성실하게 인생전체는 되는대로

0개의 댓글