(작성중) 객체지향 프로그래밍의 설계 원칙, SOLID

Alex Moon·2022년 8월 18일
0

Java

목록 보기
3/4

유지보수 작업을 하다보면 최적화나 버그 fix 등의 이유로 코드 일부를 조금 수정 했는데, 전혀 예상하지 못한 엉뚱한 곳에서 에러가 나는 경험을 해본 적이 있을 것이다. 이런 프로그램들은 경험상 객체지향 언어의 특성을 고려하지 않고, 그냥 원하는 결과만 얻을 수 있게 마구잡이로 개발되어 있었다. 그러다보니 유지보수하는데 발생하는 문제점이 너무 많아서, 새로 만드는 것 외에는 해결방안이 없었다.

그렇다면 어떻게 해야 객체지향 언어 특성에 맞게 개발한 것인지, 유지보수성과 확장성을 가진 유연한 프로그램을 만들 수 있는지 알아보자.

객체지향 프로그래밍의 설계 원칙은 로버트 마틴이 2000년대 초반에 명명한 다섯 가지 기본 원칙이다. 프로그램 유지 보수와 확장이 쉬운 시스템을 만들기 위해 고안되었다. 또한 이 원칙들은 애자일 소프트웨어 개발에 필수적으로 필요한 요소이다.

🎫 단일 책임 원칙(Single Responsibility Principle)

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

이 원칙의 목적은 응집도를 높히고 결합도는 낮추는 것이다. 즉, 모듈을 캡슐화하여 독립성을 높히고, 각 모듈을 적재적소에 쉽게 사용할 수 있어야 한다는 것이다. 이 원칙은 클래스가 많아지고 구조가 복잡해지더라도 꼭 지켜져야 한다. 또한 모듈의 목적이 명확해야 하고, 정해진 기능을 정확하게 수행할 수 있어야 한다.

예시로 치킨집 주문 클래스를 봐보자.

class Order {
    public void insert() { ... }
    public void update() { ... }
    public void remove() { ... }
    
    public int calculateNetIncome() { ... }
    public int calculateCost() { ... }
    ...
}

이 클래스의 경우 insert, update, remove 메서드는 DB와 관련이 있고, calculate 메서드들은 비즈니스 로직과 관련이 있다. 결과적으로 하나의 클래스가 2개의 책임을 가지게 되는 셈이다. 이로인해 내부적으로 2가지 책임에 해당하는 데이터들을 가지고 있어야 해서 응집도가 떨어진다. 외적으로는 이 클래스를 DB 관련 로직과 비즈니스 관련 로직에 사용하게 되므로 외부 결합도 또한 높아진다.

그렇다면 위 클래스를 다음과 같이 책임을 나누어 보자.

// DB 관련 책임
class Order {
    public void insert() { ... }
    public void update() { ... }
    public void remove() { ... }
}

// 비즈니스 관련 책임
class OrderCalculate {
    public int calculateNetIncome() { ... }
    public int calculateCost() { ... }
    ...
}

이렇게 단일 책임으로 클래스를 작성하게 되면, 내부적으로 필요한 데이터만 가지고 있게 되어 응집도를 높일 수 있다. 외부적으로도 클래스가 1가지 역할만 하기에 사용처가 명확해져 결합도 또한 낮출 수 있게 된다. 추가적으로 코드 라인수가 줄어들어드니 가독성이 좋아지고, 클래스의 의도를 파악하기 쉬워진다.




🪢 개방-폐쇄 원칙(Open-closed Principle)

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

이 원칙은 기존 코드에 대한 변경을 최소화하면서 자유롭게 확장할 수 있도록 시스템을 구조화하는 것으로 객체 지향 프로그래밍의 핵심이라고 할 수 있다. 단일 책임 원칙이 하나의 클래스의 내부적 설계라면, 이 원칙은 추상화와 다형성을 활용해 객체들의 관계를 형성하고 조직화하여 객체의 독립성을 높히는 외부적 설계라고 볼 수 있다.

이 원칙에 맞춰 설계하기 위해서 제일 먼저 해야할 일은 변하는 것(확장)과 변하지 않는 것(폐쇄)을 구분하는 것이다.
변하지 않는 것은 추상화(일반화)될 내용들로 도형, 동물 등과 같은 상위 개념들이다. 그리고 변하는 것은 상위 개념을 상속받아 다형성될 내용들로 삼각형, 사각형, 고양이, 개과 같은 하위 개념들이다. 이렇게 상하 개념으로 분류하여 설계하면 객체들을 조직화할 수 있다.

이렇게 조직화를 하면 새로운 모듈을 추가하거나 수정할 때, 클라이언트는 Animal에만 접근하게 되므로 기존 코드를 고치지 않아도 된다.

예를 들어 PG사별 결제 서비스를 개발한다고 가정해보자.

public interface PaymentService {
    public void startProcess();
}

클라이언트가 사용할 인터페이스를 만들어주고, 실제 사용될 클래스는 인터페이스를 상속하여 만들어준다.

public class KGInicis implements PaymentService {
    @Override
    public void startProcess() { ... }
}

public class NaverNPay implements PaymentService {
    @Override
    public void startProcess() { ... }
}

public class KakaoPay implements PaymentService {
    @Override
    public void startProcess() { ... }
}

클라이언트는 아래와 같이 PaymentService에만 접근하여 결제 프로세스를 진행하게 된다.

public CartService {
    private Cart cart;
    
    public requestPayment() {
        /* 
        PGFactory는 비즈니스 로직을 가진 클래스로 
        Cart에 있는 PG사 정보에 맞게 PaymentService를 생성해준다.
        */
        PaymentService paymentService = PGFactory.makePaymentService(cart);
        paymentService.startProcess();
    }
}

여기서 한가지 참고해야할 점은 PGFactory이다.
코드를 작성하다보면 어떤 PG사에 해당하는 객체를 생성해야 하는지 판단하는 로직은 필연적으로 생긴다. 하지만 이 부분이 requestPayment 메서드에 있으면 SRP 원칙에 위배된다. 메서드에도 SPR 원칙을 적용해야하기 때문이다. 그래서 PG사 판단 로직을 PGFactory 클래스를 만들어 따로 뺐다.
결과적으로 PG사 관련 이슈가 발생할 경우 PGFactory 수정과 PaymentService 상속하는 신규 클래스를 생성하면 되고, 기존 requestPayment 메서드의 로직은 수정하지 않아도 된다.




🎭 리스코프 치환 법칙(Liskov Substitution Srinciple)

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

글이 날라가서 추후 다시 작성 예정..




🎨 인터페이스 분리 원칙 (Interface Segregation Principle)

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

이 원칙은 하나의 인터페이스에 여러개의 메서드를 만드는 것보다 인터페이스의 목적에 따라 메서드를 여러개의 인터페이스로 나눠 분산시키는 것을 제안한다. 왜냐하면 상황에 따라서 특정 몇몇의 메서드들만 필요한 클래스들이 존재하게 될 경우, 해당 클래스는 필요하지 않은 메서드들까지 구현을 해야하기 때문이다. 만약 이럴 경우에 코드적으로 가독성이 떨어지게 되고, 하나의 인터페이스가 여러 클래스들과 의존성을 가지게 되어 추후 인터페이스를 수정하기 어려워진다.




🐋 의존관계 역전 원칙 (Dependency Inversion Principle)

프로그래머는 구체화에 의존하지 말고 추상화에 의존해야 한다.

이 원칙은 2가지 의미를 내포하고 있다.
첫번째는 상위 모듈이 하위 모듈을 사용할 때는 구체 클래스를 직접적으로 사용하지 말고 추상화에 의존하여 사용해야 한다는 것이다.
우리가 개발할 때는 DB와 커넥션하여 데이터를 처리하는 등의 하위 모듈들을 먼저 개발한다. 그리고 이 하위 모듈들을 활용하여 비즈니스 로직을 구현하는 상위 모듈을 작성하게 된다. 이 때 상위 모듈은 하위 모듈을 사용할 때 직접적으로 하위 모듈에 접근하지 말고, 추상 클래스를 통해서 접근해야 한다.
왜냐하면 하위 모듈들은 빈번하게 변경하게 되는데, 구체 클래스를 직접적으로 사용하게 되면 의존성이 높아지기 때문이다. 그래서 상위 모듈을 추상 클래스에 의존하게 하고, 하위 모듈은 필요에 따라 변경할 수 있도록 한다.

두번째는 추상화는 세부사항에 의존하지 말고 세부사항이 추상화에 의존해야 한다는 것이다. 이 것은 추상 클래스에서 필요한 메서드들을 정의하고, 여기에 맞춰 구체화 해야 한다는 의미이다.
만약 구체화된 내용을 기반으로 추상 클래스를 작성할 경우, 특정 형태에 맞춰 추상화 되다보니 추상화가 세부 구현에 의존하게 되어 버린다. 그러면 추후 구체 클래스를 변경하거나 교체하게 될 경우에 자유롭지 못하게 된다.

profile
느리더라도 하나씩 천천히. 하지만 꾸준히

0개의 댓글