객체지향 개발 5대 원리를 파헤쳐 보자!

이진영·2023년 7월 27일
0
post-thumbnail

개요

객체지향의 5대 원리를 이해하고는 있었지만, 명확히 파헤쳐 보지는 못했다.

그렇기에 해당 글을 작성하면서 다시 되새김 하면서

좀 더 객체 지향적인 설계가 가능한 여러분이 됐으면 하는 마음에 해당 글을 작성합니다.

SOLID : 객체 지향 설계

먼저 객체 지향 설계란 무엇일까?

-> 말을 풀어서 설명을 하게 된다면 객체를 지향하는 설계라는 뜻이다.

해당 객체 지향 설계를 적용하게 되었을 때 장점!!

  • 코드 확장

  • 유지 보수

  • 불필요한 복잡성을 제거해 리팩토링에 드는 시간을 줄여서

결국에는 개발에 대한 생산성을 높이는 효과를 볼 수 있는 하나의 공통점을 가지고 있다.

어떻게 보면 이는 어느 특정 목표에 도달하기 위해서 지켜야 하는 원칙들 이라고 볼 수 있습니다. 이러한 원칙들을 통해서 더 빠르게 목표를 달성하게 만들 수 있기 때문이죠

예를 들어 필자는 어쩌다 보니 웨이트 트레이닝에 관한 취미를 깊게 가지고 있습니다. 그렇기에 근성장에 굉장히 민감합니다. 그렇기 매 끼니 마다 단백질을 먹어야 하는 규칙이 있습니다. 이러한 규칙을 지켜야 하지 비로소 빠른 근성장에 요인으로 적용될 수 있기 때문입니다.

이는 SOLID 원칙에서도 마찬가지입니다. 해당 원칙을 통해서 소비자들이 원하는 방향의 문제점 또는 개선 사항들을 빠르게 적용하기 위해서는 그냥 개발을 하는게 아닌 SOLID 원칙을 잘 지켜야 합니다. 그래야 보다 빠르게 접근을 하고 고쳐나갈 수 있기 때문이죠


1. SRP(단일책임의 원칙)

작성된 클래스는 하나의 기능만을 가지고 있으며 해당 클래스가 제공하는 모든 서비스는 그 하나의 책임을 수행하는 데 직중되어 있어야 한다는 원칙입니다.

적절한 책임을 분배함으로써 장점

  • 코드의 가독성 향상

  • 유지보수 용이

하지만 그렇게 쉬운 말이 아니다. 내가 아직 현업을 다녀 보지는 않았지만, 현직자가 말하길 '실무의 프로세스는 매우 복잡 다양하고 변경 또한 빈번하기 때문에 경험이 많지 않거나 도메인에 대한 업무를 이해가 부족하면 나도 모르게 SRP원리에서 멀어져 버리게 된다.'라고 하십니다.

그렇기에 매번 책임에대한 단어를 상기 시키면서 경험이 필요로한 원칙


적용 사례

public class weight{

	long user_id; // 고유 아이디
	String weight_name; // 웨이트 이름
    String int set; // 세트 수
    String int num; // 세트에 대한 갯 수
}

만약 내가 운동 기록에 대한 서비스를 만들고 싶다 했을 때 위와 같은 클래스를 만들었다고 가정을 하겠다. 위 그림에서 보았을 때 변화 요소라고 할 수 없는 고유정보는 user_id 입니다. weight 클래스에서는 weight_name은 있을 수 있지만 set , num 은 좀 더 변화가 필요로 하는 부분입니다.

왜냐? 명확하게 weight는 유저가 하는 운동 종목입니다. 이를 하위 class로 나뉘게 된다면 sum, num은 분리가 필요로 하게 됩니다. 운동 족목과 세트 수 와 개수는 명확하게 다른 역할을 하게 됩니다.

public class weight{
	long user_id; // 고유 아이디
	String weight_name; // 웨이트 이름
}

publc class weight_count{
    String int set; // 세트 수
    String int counter; // 세트에 대한 갯 수
}

위와 같은 설계가 단일 책임원칙을 지키고 있다고 볼 수 있습니다. weight와 weight_count는 명확하게 다른 역할을 하고 있는 상태가 됩니다.


주의 사항

  • 각 클래스는 하나의 개념을 나타내야 한다.

  • 위와 같은 설계 시에는 클래스에서 자신의 이름을 나타내는 일을 해야 하고 네이밍에 신경을 써야 한다.

  • 무조건 책임을 분리한다고 SRP가 적용되는 건 아니다.->응집도는 높게 결합도는 낮게 설계를 해야한다.

    만약 여러 책임이 많이 분산된 경우 몇 가지 책임에 대한 부분을 하나의 분산된 부모 클래스에서 기능을 처리해 버리는 게 나을 수도 있다.


2. OCP(개방패쇄의 원칙)

소프트웨어의 구성요소는 확장에는 열러있고, 변경에는 닫혀있어야 한다는 원리이며, 이러한 말은 변경을 위한 비용은 가능한 줄이고 확장을 위한 비용은 가능한 극대화해야 한다는 의미를 담고 있다.

--> 이러한 의미는 요구사항의 변경이나 추가사항이 발생하더라도, 기존 구성요소는 수정이 일어나지 말아야 하며, 오로지 확장해서 재사용할 수 있어야 한다는 뜻이다.


적용 방법

  1. 확장될 것과 변하지 않을 것을 엄격히 구분한다.

  2. 이 두 모듈이 만나는 지점에 인터페이스를 정의한다.

  3. 구현에 의존하기 보다 정의한 인터페이스에 의존하도록 코드를 작성 합니다.


적용 사례


기존에 단일책임 원칙에서는 위와 같은 방식으로 설명을 해드렸다면 이번에는 조금 다르게 설명을 해드리고자 한다.

먼저 위의 구조는 확장이 될 부분이 명확하게 있다.(목적에 따라 다름)
--> 운동에는 종류가 있기 때문!! 그렇기에 변경에 대한 부분을 확장에 열려 있는 구조로 만들어야 한다.

  1. weight라는 것을 기존에 name이 아닌 하나의 인터페이스를 지정한다.

  2. 운동의 종류를 인터페이스에 의존하게 만든다.


이슈

  1. 확장에 실패를 하게 된다면 관계가 오히려 더 복잡해질 수 있다.

    확장 조절에 실패할 경우 여러 가지 갈등 상황들이 발생할 수 있지만 이를 잘 포착하여 비장한 결단을 내릴 줄 아는 능력에 있습니다.

  2. 인터페이스는 가능하면 변경 되어서는 안됩니다.

    인터페이스를 정의할 때 여러 경우의 수에 대한 고려와 예측이 필요합니다. 물론 과도한 예측은 불필요한 작업을 만들고, 보통 이 불필요한 작업의 양은 상당히 크기 마련.. 따라서 설계자는 적절한 수준의 예측 능력이 필요로 하다.

  3. 인터페이스 설계에서 적당한 추상화 레벨을 선택해야 한다.

    추상화는 구체적이지 않은 정도의 의미론 약간 느슨한 개념을 갖고 있기에 '행위'에 대한 본질적인 정의를 통해 인터페이스를 식별해야 합니다.


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

좀처럼 쉽게 이해되지 않는 원칙 중 하나이다. 하지만 명확한 이름에서 개념을 한마디로 표현하면 "서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다는 원칙이다."라고 정의를 내릴 수 있습니다.

그렇다면 해당 말은 상속에서의 목표와 비슷하다.

📌구현상속(extends 관계)이든 인터페이스 상속(implements 관계)이든 궁극적으로는 다형성을 통한 확장성을 획득하는 것을 목표로 삼는다.

확장성이라 한다면 마치 OCP의 목표를 한다는 것과 마찬가지라는 것과 같다는 생각을 가질 수 있습니다. 하지만 이는 생각이 아닌 진실이다. 결국 LSP는 OCP를 구성하는 구조가 된다.

객체지향의 원리는 이렇게 서로서로 이용하기도 하고 포함하기도 하는 특징을 가지고 있습니다.

적용방법

  1. 만약 두 개체가 똑같은 일을 한다면 둘을 하나의 클래스로 표현하고 이들을 구분할 수 있는 필드를 둔다.

  2. 똑같은 연산을 제공하지만, 이들을 약간씩 다르게 한다면 공통의 인터페이스를 만들고 둘이 이를 구현합니다. (인터페이스 or 클래스 상속)

  3. 공통된 연산이 없다면 완전 별개인 2개의 클래스를 만듭니다.

  4. 만약 두 개체가 하는 일에 추가적으로 무언가를 더 한다면 구현 상속을 사용합니다.

적용 사례

LSP를 잘 적용한 대표적인 예시가 자바에서의 컬렉션 프레임워크이다. 추상 메서드를 각기 하위 자료형 클래스에서 implement하여 인터페이스 구현 규약을 잘 지키도록 미리 잘 설계되어 있기 때문이다.

public class Main{
    public static void main(String[] args){
    	
        // Collection 인터페이스 타입으로 변수 선언
        Collection data = new LinkedList();
        data = new HashSet(); // 중간에 전혀 다른 자료형 클래스를 할당해도 호환됨
        
		list.add(1); // 인터페이스 구현 구조가 잘 잡혀있기 때문에 add 메소드 동작이 각기 자료형에 맞게 보장됨
    }
}

우리는 어떻게 보면 평소에 코딩하면서 당연하게 사용했던 것이 사실은 LSP를 지키고 있기 때문에 가능했던 사실이다.

그리고 더 나아가 우리는 이러한 잘 지켜진 규칙을 변경과 확장에는 열려있는 구조를 띠고 있다는 사실까지 알 수 있는 Collection이 잘 짜인 예시라고 볼 수 있습니다.

적용 이슈!

  1. 혼돈될 여지가 없고 트레이드 오프를 고려해 선택한 것이라면 그대로 둡니다.

    만약 내가 LSP를 위반한 Collection이 있다 한다 그렇다면 어겼다고 해서 이를 내가 추가적인 메소드? 인터페이스를 통한 상속을 구현한다고 가정하자 그렇다면 이미 기존에 있던 메소드들에 대한 수정이 일어나게 되는데 이때는 손실을 감소하고 Warraper를 사용하는 것이 현명할 수 있다.

    📌 트레이드 오프 : 한 측면의 이득에 대한 대가로 품질, 수량 또는 속성 중 하나를 감소시키거나 잃는 상황에 따른 결정이다.

  2. 다형성을 위한 상속 관계가 필요 없다면 Replace width Delegation을 사용 즉 합성을 사용하는 것이 좋습니다.

    이는 상속에 있어 애매한 클래스를 의미하게 되는데 합성을 시도하게 났다. -> has-a

  3. 상속 구조가 필요하다면 Extract Subclass, Push Down Field, Push Down Method를 사용을 통해 리팩토링 기법을 이용하여 LSP를 준수하는 상속 계층 구조를 구성

  4. IS-A뿐만 아니라 관계를 맺음으로써 공유하는 부분 또는 공유되어야 할 연산들이 있는지 검토해야 한다.

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

자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원리 즉 어떤 클래스가 다른 클래스에 종속될 때는 가능한 최소한의 인터페이스만을 사용해야 한다.

"하나의 일반적인 인터페이스보다는, 여러 개의 구체적인 인터페이스가 낫다."라고 정의를 내릴 수 있다."

이는 단일 책임 원칙과도 같다. SRP가 하나의 클래스의 단일 책임을 강조한다면 ISP는 인터페이스의 단일 책임 원칙이다. 라고 칭할 수 있다.


적용방법

  1. 클래스 인터페이스를 통한 분리
  1. 객체 인터페이스를 통한 분리

적용 사례

사실 인터페이스는 SRP의 역할들이 하나씩 늘어나다 보면 계속해서 상속을 통해서 이를 구현을 하기 마련이다.

그렇다면 우리는 차라리 역할들을 여러 가지의 인터페이스를 나누는 상황이 발생하기 마련이다. 그럴 경우 싫든 좋든 간에 필요 없는 메소드도 구현해야 하는 경우가 발생하는 경우가 있다.

이는 명확하게 그림으로 알아보자!!

해당 그림은 규모가 너무 큰 객체를 상속했을 때 발생하는 문제와, 이를 인터페이스로 분리하여 해결하는 방법을 도식한 것이다.

먼저 A쪽을 보자!! 상속되는 구조를 가지고 있다는 것을 인지하고 가자

  1. 먼저 왼쪽 객체들은 아무 문제가 없다 하지만 이는 오른쪽으로 올라갈 수록 사용하지 않는 객체가 발생할 경우가 발생

  2. 이는 최악에 경우 필요 없는 메소드들도 구현을 해야하는 경우가 발생한다.

하지만 B쪽을 본다면 이야기가 달라진다. 각 객체가 필요한 인터페이스만을 상고하여 구현하기 때문에 각자 필요한 메소드들만 가지게 된다. 이것이 인터페이스 분리 원칙을 지향하는 바이다.


적용 이슈

  1. 구현된 클라이언트에 변경을 주지 말아야 합니다.

  2. 서로 다른 성격의 인터페이스를 명백히 분리합니다.


5. DIP(의존성 역전의 원칙)

DIP는 어떻게 보면 유지 보수하는 데 있어 확장은 필연적인 요소라고 볼 수 있는데 이러한 확장에 있어서 추상 class가 계속해서 생겨나는 데 있어 발생한 원칙이라고 볼 수 있다.

해당 원칙은 해당 Class를 직접 참조하는 것이 아닌 상위 요소로 참조하는 원칙이다.

즉 해당 말은 기존에 우리는 하위 클래스에서 접근하여 상위 클래스의 메소드들을 사용할 수 있었다. 하지만 DIP는 반대로 상위 인터페이스에서도 접근이 가능하다는 말이다.

상위의 인터페이스의 타입의 객체로 통신하는 원칙!


적용 방법 및 사례


위 그림은 우리가 기존에 배웠던 다형성을 위한 상속을 배운것을 토대로 위와 같은 방식을 구현했다고 가정을 하겠다. 하지만 이는 동물의 종류가 언제든지 늘어날 수 있다.


이를 Animal이라는 Interface를 만들어서 접근을 가능하게 만드는 방식이다. 이렇게 했을 때의 장점 또한 명확하다.

기존에는 일일이 우리는 추가 될 때마다 상속에 대한 작업을 해줘야 했지만, 이는 Animal이라는 특정 모듈을 만듦으로서 위와 같은 하나의 Animal만 접근하면 되기 때문에 매우 큰 장점이다.

하지만 이는 굳이 인터페이스에 국한되는 이야기는 절대 아니다. 아래와 같은 방식 또한 가능하다.

위 그림 또한 어떤 개발자가 어느 특정 클래스를 통해서 접근하는 것 또한 이는 DIP라고 볼 수 있는 사례이다.

// 인터페이스
interface Toy {}

class Robot implements Toy {}
class Lego implements Toy {}
class Doll implements Toy {}

// 클라이언트
class Kid {
	Toy toy; // 합성
    
    void setToY(Toy toy) {
    	this.toy = toy;
    }
    
    void play() {}
}

// 메인 메소드
public class Main {
	public static void main(String[] args) {
        Kid boy = Kid();
        
        // 1. 아이가 로봇을 가지고 놀 때
        Toy toy = new Robot();
        boy.setToy(toy);
        boy.play();
        
        // ...
        
        // 2. 아이가 레고를 가지고 놀 때
        Toy toy = new Lego();
        boy.setToy(toy);
        boy.play();
    }
}

마치면서

해당 SOLID 원칙들을 깊게 공부하면서 느끼는 거지만 어떻게 보면 우리는 이러한 일상에서 어떤 특정 문제를 보다 효율적이고 또는 유지보수하기 좋게 만들기 위해서 위와 같은 방식들을 무의식 속에서 많이들 사용했다. 는 생각이 든다.

그렇기에 해당 게시글을 통해서 많은 분이 내가 이렇게 썼던 부분들이 이러한 SOLID 원칙들을 고수해서 써나간 것을 많이들 느꼈으면 좋겠다. 이유는 뭔지는 알고 써야 하는 방법이기 때문이다.

그리고 더 나아가 이러한 방식의 활용은 정말 다양하다. 이렇게 코딩뿐만 아니라 다양하게 응용 됐을 거라고 믿어 의심치 않다. 공개된 API들도 이러한 사례를 지키는 경우가 정말 많다. 단지 우리가 모를 뿐. (EX - 소켓, 자바 스윙 컴포넌트)

그리고 이러한 자료를 제공해 준 분들에게 감사를 표한다.

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5%EC%9D%98-%EC%83%81%EC%86%8D-%EB%AC%B8%EC%A0%9C%EC%A0%90%EA%B3%BC-%ED%95%A9%EC%84%B1Composition-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-DIP-%EC%9D%98%EC%A1%B4-%EC%97%AD%EC%A0%84-%EC%9B%90%EC%B9%99

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-ISP-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EB%B6%84%EB%A6%AC-%EC%9B%90%EC%B9%99

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-OCP-%EA%B0%9C%EB%B0%A9-%ED%8F%90%EC%87%84-%EC%9B%90%EC%B9%99

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-LSP-%EB%A6%AC%EC%8A%A4%EC%BD%94%ED%94%84-%EC%B9%98%ED%99%98-%EC%9B%90%EC%B9%99

https://4z7l.github.io/2021/05/31/SOLID_LSP.html

https://minusi.tistory.com/entry/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5%EC%A0%81-%EA%B4%80%EC%A0%90%EC%97%90%EC%84%9C%EC%9D%98-has-a%EC%99%80-is-a-%EC%B0%A8%EC%9D%B4%EC%A0%90

https://blog.itcode.dev/posts/2021/08/14/open-closed-principle

https://madplay.github.io/post/coupling-and-cohesion-in-software-engineering

https://www.nextree.co.kr/p6960/

profile
내가 공부한 것들을 적는 공간

1개의 댓글

comment-user-thumbnail
2023년 7월 27일

감사합니다. 이런 정보를 나눠주셔서 좋아요.

답글 달기