jhhan의 블로그

객체지향 설계의 원칙 5가지 본문

기타

객체지향 설계의 원칙 5가지

jhhan000 2020. 12. 13. 22:23

이번 포스트에서는 객체지향 설계의 원칙 5가지에 대해 알아보겠습니다.

 

(이 원칙은 면접에서도 물어볼 수 있다고 하니 알아두면 유용할 것 같습니다.)

 

클린 코드로 유명한 '로버트 마틴' 이라는 사람이 좋은 객체 지향 설계의 5가지 원칙

-> 기존에 있던 내용들을 알아보기 쉽게 정리한 것이라고 합니다.

줄여서 SOLID 라고 합니다.

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

이제 이것에 대해 하나씩 알아보겠습니다.

 

1. SRP: 단일 책임 원칙(Single Responsibility Principle)

'하나의 클래스는 하나의 책임만 가져야 한다.' -> 라고 말할 수 있습니다.

하지만, 하나의 책임이라는 말은 너무 모호합니다.

코드 관점에서 더 이해하려면 -> '변경'을 했을 때, 파급효과가 적으면 SRP를 잘 지켰다고 할 수 있습니다.

ex) A라는 Class를 하나 변경했습니다. -> B Class, C Controller, 프론트에 해당하는 D파일, E 라는 DB... 등등 바꿔야 할 부분이 많다면 SRP가 제대로 지켜진 것이 아닙니다.

ex) A라는 Class를 하나 변경했습니다. -> A Class 이외에는 변경할 부분이 없다면 SRP가 제대로 지켜졌다고 할 수 있습니다.

제가 이전에 만들어봤던 컨트롤러 파일입니다.(이전에 올린 포스트에 이게 있을 수도 있을 것 같습니다.. 이전에 올렸던 포스트의 코딩 내용을 그대로 가져왔거든요.)

만약 저 코드에서 create이라는 함수를 변경하는데 ->

  1. 다른 클래스 OR 모델들이 같이 변경되야 함 -> SRP가 제대로 안 지켜짐
  2. 다른 클래스 OR 모델들의 변경이 필요없음 -> SRP가 제대로 지켜짐

이라고 볼 수 있습니다.

(...어쩌면 예시가 잘못 됐을 수도 있지만,, 그래도 이런 방법으로 접근하면 됩니다.)

 

2. OCP: 개방-폐쇄 원칙(Open/Closed Principle)

[5가지 원칙 중에서 가장 중요하다고 볼 수 있습니다.]

'소프트웨어 요소는 확장에는 열려있으나, 변경에는 닫혀 있어야 한다.' -> 라고 직관적으로 말할 수 있습니다.

-> 근데 이 말에는 모순이 있는 듯 합니다.

코드 확장을 하기 위해서는 당연히 기존 코드의 변경이 있을 수 밖에 없습니다.

그래서 다형성의 측면에서 생각해봐야 합니다.

  • 인터페이스 1개가 존재
  • 인터페이스를 구현한 A클래스 -> 기능 구현
  • 인터페이스를 구현한 B클래스 -> 기능 구현
  • 이 때 B클래스를 추가(확장)하는 것이므로 OCP를 만족하려면 코드의 변경이 없어야 한다.
  • 만약 변경을 해야한다면 OCP를 제대로 지켜지지 않은 것

이전에 짰던 코드를 살펴보겠습니다.

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource);
        return new JdbcTemplateMemberRepository(dataSource);
        return new JpaMemberRepository(em);
    }

이전에 SpringConfig라는 클래스를 만들고 그 안에 들어있던 일부입니다.

MemberRepository라는 인터페이스를 만들었었고, 그 구현체로는 Memory~, Jdbc~ JdbcTemplate~, Jpa~로 4개를 만들었습니다.

그런데 return으로 각각 다 쓰고 있죠?

이는 제가 클래스를 하나 만들 때마다 저렇게 추가해서 사용했다는 것입니다.

즉, OCP를 제대로 지키지 않고 코딩을 했다는 것입니다...(머, 그럴수도 있죠.)

여기서 저는

  • MemberRepository memberRepository = new MemoryMemberRepository(); // 기존
  • MemberRepository memberRepository = new JdbcMemberRepository(); // 변경

이런 식으로 사용한 것 같습니다.

이렇게 코드를 짜는 것은 클라이언트가 구현 클래스를 직접 선택하는 코드입니다.

OCP를 제대로 지키려면 이렇게 짜면 안됩니다. ㅎ

제대로 지켜지려면 -> 객체 생성, 연관관계, 설정자 등이 필요합니다.

-> 그래서 스프링에서는 IOC , DI 등을 제공합니다.

저도 아직 공부하는 중이어서 이 코드를 어떻게 바꾸는지는 아직 모르겠습니다...
(알게 되면 다시 포스트 작성을 해보겠습니다.)

어쨌든 OCP는 확장(추가)은 가능하나, 변경은 하지 말 것 이라는 것을 기억하셔야 합니다.

 

3. LSP: 리스코프 치환 원칙(Liskov Substitution Principle)

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

말만 보면 너무 어렵습니다. 좀 더 쉽게 이해해 보겠습니다.

  1. 자동차 객체가 있다고 가정해보겠습니다.
  2. 그리고 이 객체는 악셀 이라는 함수가 있다고 해봅니다.
  3. 악셀을 실행하면 현재 속력에서 '플러스'가 되게 실행이 될 것입니다.
  4. 그런데 !! 악셀이라는 함수가 실행될 때, 속력이 줄어들게 코드를 짤 수 있습니다.
  5. 컴파일 상에서도 문제가 없겠죠? 단순히 속력을 줄이는 것 뿐이니까요..
  6. 하지만, 일반적이라면 악셀이 실행되면 속력이 증가된다고 생각합니다.
  7. LSP는 이런 기능들을 보장하게 해주는 원칙입니다.
  8. 그래서 악셀 이라는 함수는 무조건 속력이 증가해야 합니다.
  9. 속력이 감소하게 된다면 LSP를 위반하는 것입니다.

인터페이스에 기능이 하나 있고, 구현체가 이 기능을 믿고 사용하려면 -> LSP를 지켜야 합니다.

4. ISP: 인터페이스 분리 원칙(Interface Segregation Principle)

인터페이스 여러 개가 범용 인터페이스 하나보다 낫다 -> 라고 생각하시면 됩니다.

하나의 인터페이스 안에 다양한 기능보다는 인터페이스를 여러개로 나누어서 기능을 분리하는게 낫다는 것입니다.

  1. 자동차 인터페이스가 있다고 해봅니다.
  2. 운전 인터페이스, 정비 인터페이스로 분리를 해봅니다.
  3. 그러면 사용자 클라이언트는 운전자 클라이언트, 정비사 클라이언트로 분리가 가능해집니다.
  4. 이렇게 되면 정비 인터페이스가 변하더라도 정비사 클라이언트에만 영향을 주지, 운전자 클라이언트에는 영향을 주지 않습니다.

ISP를 잘 지켜서 코드를 짠다면 -> 인터페이스가 조금 더 명확해지고, 대체 가능성이 높아집니다.

그리고 인터페이스가 여러 개로 나뉘게 되면, 그만큼 기능들도 적게 들어가기 때문에, 구현체를 만들 때 기능 구현도 줄어들게 될 것입니다!

실제로 스프링 코드를 분석하면 정말 잘게잘게 인터페이스가 쪼개져 있다고 하네요! 시간이 된다면 확인해 보는 것도 괜찮을 것 같습니다!

 

5. DIP: 의존관계 역전 원칙(Dependency Inversion Principle)

코드는 추상화에 의존해야하고, 구체화에 의존하면 안된다 -> 라고 말할 수 있습니다.

즉, 코드가 구현 클래스가 아닌 인터페이스에 의존해야 한다는 뜻입니다.

이 원칙 역시 2번째 원칙만큼 중요합니다. 2번째 원칙과 관련된 부분이 좀 있기도 해서 그럽니다.

// MemberRepository mr = new MemoryMemberRepository();
   MemberRepository mr = new JdbcMemberRepository();

이 코드를 보면,

MemberRepository 라는 인터페이스에만 의존해야 하고,

MemoryMemberRepository 혹은 JdbcMemberRepository에 의존하면 안됩니다.

DIP를 잘 지킨다면 인터페이스에만 의존하기 때문에 구현체를 유연하게 변경이 가능합니다.

하지만 위의 코드는 인터페이스와 구현 클래스 동시에 의존하고 있습니다.

그래서 구현체가 변경이 될 때 코드 변경이 힘들게 됩니다.. 위의 코드는 DIP가 지켜지지 않은 것이죠...

 

 

이렇게 객체지향의 설계 5가지 원칙을 살펴봤습니다.

이 5가지 원칙 중 핵심적인 2가지는 2번째 원칙과 5번째 원칙입니다.

객체 지향의 핵심으로 다형성이 있긴 하지만, 다형성 만으로는 OCP, DIP를 지킬 수 없습니다.

스프링이 이 부분을 보완해 줄 수 있다고 합니다.

관련 글이 올라간다면 이제는 Spring 탭에 올라갈 것으로 보이네요.

 

 

 

공부 힘드네요... ㅎㅎㅎ

 

이렇게 마치겠습니다.

'기타' 카테고리의 다른 글

사이드 프로젝트...?  (0) 2024.01.18
DBeaver 설치 + MariaDB 연결  (4) 2021.04.12
GitHub 과 Slack 연동  (0) 2020.07.12