728x90

📌 SOLID 원칙

객체 지향적으로 설계하는데에 있어 기본이 되는 SOLID원칙에 대해서 알아보자.
SOLID는 각 원칙의 첫번째 글자를 따서 만든 것이다.

  • 단일 책임 원칙 (Single responsibility principle; SRP)
  • 개방-폐쇄 원칙 (Open-closed principle; OCP)
  • 리스코프 치환 원칙 (Liskov substitution principle; LSP)
  • 인터페이스 분리 원칙 (Interface segregation principle; ISP)
  • 의존 역전 원칙 (Dependency inversion principle; DIP)

이 원칙들이 서로 다른 내용이라 하기에는 밀접하게 연관되어 있으므로 한꺼번에 같이 이해해야 할 것이다.

📌 단일 책임 원칙

객체 지향은 책임을 객체에게 할당하는데에 있다. 객체로 존재하게 하는 이유가 책임인데

이 원칙은 원칙 이름에서도 알 수 있듯 책임과 관련되어 있다.

📢 클래스는 단 한 개의 책임을 가져야 한다.

하나의 클래스가 여러 책임을 갖게되면 책임마다의 이유가 발생하기 때문에 클래스 변경이 잦아질 수 있다.

그래서 한개의 이유로만 클래스가 변경되려면 하나의 책임만을 가져야 한다.

지금 진행하고있는 TDD강좌를 들으면서 SOLID원칙 그리고 디자인 패턴에 대해 많이 학습하는것 같다.

1-2-3 과정으로 진행되는 과정이 예를 들어 있다고 한다면

public class Process {
    public void one() {
        /// 1...
        String test = two();
        three(test);
    }


    public void two() {
        /// 2...
    }

    public void three(String test) {
        /// 3...
    }
}

이렇게 작업을 하게되면, 연쇄적으로 코드 수정을 해야할 수 밖에 없다.

이것은 코드를 분리만했지 절차지향과 다를게 없다.

여기서 만약 String타입의 인자가 아니라 다른타입으로 오게되면 twothree 모두 바꿔야한다.

극단적인 예시이지만, 같은 작업을 수행하는 것이라면 괜찮지만
데이터를 불러오고, 가공하고, 보여준다 라면 얘기는 다르다.

그러면 클래스마다 1개씩 잘라서 분리를 해주어야 한다.

그래서 최종적으로 동작하는 클래스에서 조립하는 느낌으로 구현을 한다고 생각하면 이해가 쉬울 듯 하다.

📌 개방-폐쇄 원칙

개방, 폐쇄 말만들으면 열고 닫는것.

확장에는 열려 있어야 하고, 변경에는 닫혀있어야 한다.

말이 이렇게만 보면 어렵다.

기능을 변경하거나 확장은 할수 있되, 그 기능을 사용하는 코드는 수정을 하지 않아야 된다.

이부분은 추상화와 관련이 깊은것 같다. 사용하는 코드를 수정하지 않아야한다 라는 원칙이기 때문에

인터페이스 또는 추상클래스를 사용했을 시에 구현체에 따라 다르게 동작할 수 있게 만듬으로써

변경 확장이 용이하며, 코드 수정은 하지 않게된다.

개방 폐쇄 원칙이 깨질때의 증상 1 - 다운 캐스팅

이것은 다운 캐스팅을 할 경우에 그런데, instanceof 로 타입 다운을 시켜 검증을 하면

발생하는 것인데

내 경우에는 볼링게임 여기서 사용했었다.

객체마다 다르게 동작할 수 있는 무언가가 있다면 그 메소드를 추상화하여 동작하게 만들어야한다.

개방 폐쇄 원칙이 깨질때의 증상 2 - if-else

public void draw() {
    if (index == 1) {
        // 1조건
    }else if (index == 2) {
        // 2조건
    }
    ...
}

위 코드처럼 조건이 하나씩 늘게되면 else if로 늘려서 추가할 경우이다.

draw에서 그냥 메소드를 통일하고

Pattern이라는 인터페이스를 생성하고 방식에따라 다른 로직을 구현하고 메서드는 통일시키는 식으로

추가를 해주자.

개방 폐쇄 원칙은 유연함에 대한 것

이 원칙은 변화가 예상되는 것들을 추상화해서 유연함을 얻도록 해줬다.

그러니까 추상화를 하지 않거나 아직 개념이 부족해 못한다면

개방-폐쇄 원칙을 지킬 수 없게 되어 기능 변경이나 확장을 어렵게 만든다는 것이다.

내가 퇴사했던 회사에서 딱 이렇게 개발을 했었다. 오히려 이게 쉽다면서... 난 아니었는데 😥

각설하고, 코드의 변화 요구가 발생하면 이 구현부를 추상화해서 지금의 이 원칙에 맞게 수정할 수 있는지

항상 생각하면서 개발해야겠다.

📌 리스코프 치환 원칙

윗부분의 개방폐쇄에선 추상화 그리고 다형성을 이용하여 구현되었다.

이번에 알아보는 이 리스코프 치환 원칙은 개방 폐쇄를 조력해주는(?) 다형성에 관한 원칙이다.

상위 타입의 객체를 하위 타입의 객체로 치환하여도

상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

이거는 예제를 따로 개발자가 반드시 정복해야할 객체지향과 디자인 패턴에서 인용하였다.

부모 클래스와 자식 클래스가 있다고 가정. 특정한 메서드는 상위 타입인 부모클래스를 이용할 것이다.

public void someMethod(SuperClass sc) {
    sc.someMethod();
}

someMethod는 부모타입의 객체를 사용하고 있는데, 이 메소드에 하위타입 객체를 전달해도 someMethod

정상적으로 동작해야 한다. 이것이 바로 리스코프 치환 원칙이다.

중요❗❗❗ 이 원칙이 제대로 지켜지지 않는다면 개방-폐쇄역시 지켜지지 않는다.

개념적으로 상속 관계에 있는 두개의 클래스가 있더라도 막상 구현했을때는 상속 관계가 아닐 수도 있는 것들이 있다.

이럴 때는 상속이 아니라 각자 다른 타입으로 매칭을 시켜줘야한다.

📌 인터페이스 분리 원칙

인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.

이 원칙또한 앞에서와 연관이 있는데 결국 용도에 맞게 인터페이스를 분리하는 것은

단일 책임 원칙과 연결이 된다. 하나의 타입에 여러 기능이 섞일 경우

한가지의 기능 변화로 인해 다른 기능이 영향을 받을 가능성이 높아진다.

그래서 인터페이스를 분리한다면 한 기능에 대한 변경의 리스크를 줄일 수 있게 된다.

그리고 인터페이스의 분리가 잘되어 있다면 즉, 단일 책임 원칙이 잘 지켜질때

구현클래스들의 재 사용성이 높아질 수 있기 때문에

결국 이 원칙은 인터페이스와 구현 클래스의 재사용성을 높인다는 효과를 가지게 된다.

원칙은 클라이언트에 대한 것

인터페이스 분리 원칙은 클라이언트 입장에서 인터페이스를 분리하란 원칙이다.

클라이언트가 사용하는 기능들을 중심으로 인터페이스를 분리하여 클라이언트에서 발생하는 인터페이스

변경의 여파가 다른 클라이언트에 미치는 것을 최소화 할 수가 있다.

📌 의존 역전 원칙

고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.

저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.

고수준 모듈은 저수준 모듈을 조합해서 한 모듈을 만들어 놓은 형태이고

저수준 모듈은 도메인단위로 분리했을 때 상세한 행동 하나하나를 정의한 것을 말한다.

고수준 모듈이 저수준 모듈에 의존할 떄의 문제

내가 생각하는 이 SOLID 원칙. 어떤 법칙을 읽던간에 무조건 인터페이스가 생각나는게 당연한걸까❓

이것도 내가 생각하기엔 추상화가 답인것 같다.

구현하는 비즈니스 클래스가 여러개가 있다고 한다면 개방폐쇄의 예시처럼 조건일때 다른 클래스 로직을 불러와

실행하는 것이 될것이다.

이런 방식이 프로그램을 변경하는데 너무 어렵게 만든다. 이 저수준 모듈이 변경됨에 따라

고수준은 변경이 되지 않게끔 하려면 나오는 원칙이 의존 역전 원칙이다.

그러니까 이 의존 역전 원칙이 리스코프나 개방-폐쇄를 따르는 설계를 만들어주는 밑바탕이 되는 것으로 볼 수 있다.

의존 역전

마땅한 코드 예시가 없어서 책에서 한번더 가져다 쓴다.

public class FlowController {
    public void process() {
        //소스 코드에서 FileDataReader에 대한 의존 발생
        FileDataReader reader = new FileDataReader();
    }
}

public class FileDataReader implements ByteSource {
    ...
}

이런 구조가 있다면

소스코드의 의존은 아래와 같다.

image
{: text-center}

그런데 런타임에서 본다면
FlowController -> FileDataReader 가 되는것이다.

왜냐면 컨트롤러가 시작점이고 거기서 하나씩 로드해서 참조하기 때문에

이런 구조가 나온다.

왜?

이유는 그냥 단순하다. 절차지향으로 생각해보면 저 Controller를 동작시켜야 아래가 동작하지 않는가?

정리

지금까지 SOLID 원칙에 대해 알아보았다. 결국 이 원칙이 말하는 바는

정말 원초적으로 생각하고 따지자면 유지보수하기 좋은 코드를 만드는 밑바탕이다.

왜냐면 요구사항이 변화하는것에 맞춰 유연하게 변경하니까 가 가장크다.

요즘 자바 공부에 재미를 붙였다.

이렇게 하나하나 습득해서 위로 올라가고 싶다.

나는 아직 자바가 고프다 ㅋㅋㅋㅋㅋㅋ

이번 포스팅 마치도록 하겠다.

728x90

'Java' 카테고리의 다른 글

디자인 패턴 - Bridge Pattern  (0) 2022.08.07
Oauth 이슈  (0) 2022.08.07
연산자  (0) 2022.08.06
[Java] 데이터타입, 변수, 배열  (0) 2022.08.06

+ Recent posts