ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 우테코 레벨 1이 끝나고 다시 보는 SOLID
    백엔드/객체지향 | 디자인패턴 2023. 4. 6. 00:55
    반응형

     

    https://hello70825.tistory.com/530

     

    [SOLID] 객체지향 설계 원칙

    우아한테크코스 프리코스를 할 때, 인터넷에서 뒤적뒤적 거리면서 공부를 했었지만 간단한 코드와 글만 봐서는 이해가 되지 않았었다. 그런데 지금 객체지향 책을 읽어보니 이해가 되어서 나중

    hello70825.tistory.com

     

    우테코 프리코스가 끝나고, 본 과정을 시작하기 직전에 SOLID에 대한 글을 포스팅 했었다. 하지만 이때에는 실제 코드로 적용을 하면 어떻게 될지 제대로 가늠이 안갔는데, 레벨 1을 진행하면서 어느정도는 알게 되었다고 말할 수 있다.

     

    TDD를 하면서 SRP가 무엇인지 제대로 알게 되었고, 체스 미션에서 다중 if문을 제거하면서 OCP가 얼마나 중요한지 알게 되었다. 이외에도 다른 원칙들이 있지만..  레벨 1 인터뷰에서 SRP, OCP, DIP에 대한 이야기를 했었는데, 말을 깔끔하게 제대로 못했다는 느낌이 들었다. 특히 인터뷰 내 다음 순서인 크루가 ChatGPT처럼 바로바로 깔끔한 답변을 해서 더 크게 느껴진 것일 수도 있다 😀


     

    1. 단일 책임 원칙 (SRP)


    한 클래스, 한 메서드는 하나의 책임을 가져야 한다.

     

    단일 책임 원칙을 하면서 느낀 점은 유지보수가 쉬워진다는 것이고, 코드의 신뢰도를 높히게 되는 과정이라고 생각한다.

    단일 책임 원칙을 최대한 지키는 방법은 TDD를 하는 것과 이름에 의미를 분명하게 만드는 것이다. TDD의 경우엔 가장 작은 단위의 테스트를 만들기 때문에 시간이 오래 걸리지만,  그만큼 엄격하게 SRP를 준수할 수 있게 된다. 이 과정에서 이름을 분명하게 짓는다면 유지보수하는 사람의 입장에서는 유지보수하는 시간이 빨라지게 된다.

     

    코드의 신뢰도가 낮아진다면 어떻게 될까?

    먼저 코드의 신뢰도가 낮아지게 된다면 아래와 같은 내용으로 작성되어 있을 것이다.

    어떤 메서드 이름은 findById인데, 이것은 id 값을 통해 어떤 객체를 찾아주는 역할을 한다. 하지만 DB에 존재하지 않는 id로 검색하게 됐을 때, 내부 코드에 create()가 존재하고, 새로운 객체의 정보를 db에 저장한 뒤에 객체를 반환하게 된다고 해보자.

    이러면 findById는 없는 id를 검색하게 될 경우, 해당 id를 가진 객체를 생성해서 반환하게 된다는 책임을 가지게 된다.

     

    다른 사람이 내 코드를 볼 때, 위와 같이 SRP를 지키지 않는 코드가 여러 개가 발견된다고 해보자. 그러면 메서드 명을 그대로 믿지 않고, 이 메서드는 어떤 역할을 하는지 코드 분석을 하게 된다.

    SRP를 지키는 코드라면 빠르게 볼 수 있는데, 신뢰도가 낮은 코드로 인해 코드를 읽는데 오랜 시간이 걸리게 된다.

    이 말은 코드의 가독성이 낮아지게 된다는 것이고, 낮은 응집도와 높은 결합도 때문에 코드 변경에 취약해진다. 거기다가 테스트 코드를 작성하려고 할 때, 경우의 수가 여러가지로 나뉘기 때문에 테스트 코드도 복잡해지고, 많은 시간을 투자해야한다.


     

     

    2. 개방 폐쇄 원칙 (OCP)


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

     

    디자인 패턴을 적용할 정당성을 찾는다면 제일 먼저 떠오르는 원칙이다. 그만큼 객체지향 설계 원칙에서 SRP와 함께 가장 중요한 원칙이라고 생각한다. 

    맨 위에 있는 문장을 쉽게 이야기하면 새로운 기능을 추가할 때, 기존 코드를 변경하지 않고도 만들 수 있도록 하는 것이다.

    물론 레벨 1 기간에서 기존 코드를 완전히 변경하지 않는다는 것은 말이 안되지만, 최소한으로 코드 변경이 일어나면서 기능을 추가했었다.

     

    개방 폐쇄 원칙을 지키지 않았다면 아래의 문제점이 생기게 된다.

     

    - 비슷한 if-else 존재

    instanceof나 다운 캐스팅을 사용하게 된다면 상위 클래스에서 만든 기존 코드를 계속 수정해야 한다. 아마 if문의 조건에 instanceof를 사용해 하위 타입인지 확인하고, 하위 클래스에 맞는 로직을 실행할 것이다.

    참고로 if문 조건에 instanceof를 사용하는게 아니라 is○○○로 특정 객체인지 확인하는 것도 OCP를 위반한다. 이런 경우 나는 Map을 사용하여 객체를 key로, 메서드를 함수형 인터페이스를 사용하여 value로 저장하는 것이다.

     

    - instanceof, 다운 캐스팅

    다운 캐스팅을 하는 경우엔 해당 하위 클래스만 가지는 메서드를 호출하는 경우도 있는데, 이때에는 리스코프 치환 원칙을 위배하게 된다. 그래서 리스코프 치환 원칙을 위배하게 된다면 높은 확률로 개방 폐쇄 원칙을 위배한다고 생각하면 된다.

    리스코프 치환 원칙(LSP)을 위배하면서 개방 폐쇄 원칙(OCP)을 지키는 경우가 존재하는데, 그건 바로 기본 클래스를 상속하는 것이다.

    예시를 들자면 상태 패턴의 상위 클래스를 인터페이스가 아닌 기본 클래스로 만드는 것이다. 상태 패턴을 구현하게 된다면 특정 상태에서 어떤 메서드는 지원하지 않기 때문에 UnsupportedOperationException을 던지는 경우가 존재한다. 이 경우 기본 클래스에서는 작동하는 메서드인데, 하위 클래스는 작동하지 않으므로 LSP를 위배한다고 할 수 있다. 하지만 OCP는 만족한다.


     

     

     

    3. 리스코프 치환 원칙(LSP)


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

     

    OCP에서 말한 다운 캐스팅의 상황처럼 상위 클래스에서 어떤 메서드가 정상적으로 잘 작동하면, 하위 클래스에서 같은 메서드도 정상적으로 잘 작동해야 한다. 간단하게 말해 다형성을 지켜야 한다는 것이다.

     

    리스코프 치환 원칙을 지키기 위한 안전장치로는 클래스 파일을 설계할 때, final 키워드와 abstract 키워드 중에 하나를 무조건 붙이는 것이다.

    final의 경우에는 해당 클래스가 상위 클래스일 경우, 하위 클래스가 없다는 것을 보장할 수 있으므로 LSP를 배제할 수 있게 된다.

    abstract의 경우엔 추상 메서드를 하위 클래스에서 오버라이딩하기 때문에 어느 정도는 LSP를 지킬 수 있게 된다. 하지만 완벽하게 LSP를 지키기 위해서는 특별한 일이 아닌 이상, 하위 클래스에서만 따로 동작하는 public 메서드를 만들지 않아야 한다.

     

    하지만 추상 클래스를 사용하는 것은 확실한 IS-A 관계일 때에만 사용해야 한다. 거기다가 추상 클래스를 사용하는 것만으로도 결합도가 높아지기 때문에 계층 구조의 상속 깊이가 1단계에만 사용하는 것을 권장하고 있다. 그래서 웬만하면 상속이 아닌 조합을 사용하는 것이 좋다.


     

     

     

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


    클라이언트는 자신이 사용하는 메서드에만 의존해야 한다.

     

    ISP는 SRP의 인터페이스 버전이라고 생각하면 된다.

    ISP의 예시를 들어보면 휴대폰이라는 인터페이스 대신 통화의 기능을 가진 인터페이스 + 문자 기능을 가진 인터페이스 + 인터넷 접속이 가능한 인터페이스들로 나누어서 필요한 클래스에 조합하는 것이다.

     

    ISP의 장점이라면 큰 추상화의 interface 경우에는 나중에 기능이 추가될수록 코드가 비대해진다는 단점을 가지고 있다. 하지만 ISP는 기능들을 각각의 인터페이스로 전부 분리했으므로 클래스 파일이 많아지지만 역할이 분명해진다는 장점을 가진다.

    단점은 스마트폰 클래스를 만드는데 implement 통화, 문자, 인터넷을 하게 된다면 스마트폰이 여러 책임을 가지게 되므로 SRP 위반이라고 할 수 있다. 이것도 관점에 따라 스마트폰이 세 가지를 아우르는 하나의 큰 추상화라고 생각할 수도 있지만, 아직까지는 ISP랑 SRP는 서로 다른 것처럼 보인다.


     

     

     

    5. 의존 역전 원칙 (DIP)


    고수준 모듈이 저수준 모듈의 구현에 의존하지 말고, 저수준 모듈이 고수준 모듈의 추상 타입에 의존해야 한다.

     

    어떤 클래스 내부 코드 안에 특정 인터페이스를 상속하는 하위 클래스를 직접적으로 명시하지 말고, 특정 인터페이스만을 명시하라는 것이다. DIP를 지킬 수 있는 방법중에 하나가 의존성 주입을 하는 것이다.

     

    무조건 의존성 주입을 하는 것은 아니고, 다른 것도 존재한다.

    예를 들어서 우리가 보통 HashMap을 인스턴스화 할 때, 아래처럼 코드를 작성하게 되는데, 이것도 DIP중에 하나라고 할 수 있다.

    Map<String, String> abc = new HashMap<>();

    여기서 abc는 HashMap이 아닌 Map에 의존하고 있기 때문에 DIP를 만족할 수 있다고 한다.

     

    이것도 의존성 주입으로 코드를 작성할 수 있는데, 리뷰어분들마다 '지금 레벨에서는 굳이 적용할 필요가 없다', '한 번 적용 해봐야 한다'로 서로 의견이 다른 것 같다.


    반응형

    댓글

Designed by Tistory.