이전 포스팅에서 객체지향이 추구하는 4가지 특징에 관하여 설명하였다.

이번에는 4가지 특징에 동반되는 5가지 원칙에 대하여 설명하겠다.

OOP의 5대원칙은 각각의 앞글자를 따서(SOLID)라고 불리며 여러 디자인패턴은 5가지 원칙을 준수하며 만들어졌단걸 기억하면 이해하기 더욱 쉬울것이다.

 

SRP(Single Responsibility Principle): 단일 책임 원칙 - 객체는 1개의 책임만 가져야한다

OCP(Open Closed Priciple): 개방 폐쇄 원칙 - 확장에 열려있어야하며 수정엔 닫혀있어야 한다.

LSP(Listov Substitution Priciple): 리스코프 치환 원칙 - 서브타입은 언제나 부모타입으로 교체할수 있어야한다.

ISP(Interface Segregation Principle): 인터페이스 분리 원칙 - 적재적소에 맞도록 인터페이스를 설계해야한다.

DIP(Dependency Inversion Principle): 의존 역전 원칙 - (구현된)클래스를 참조하는게 아닌 그 상위요소로 참조하라


SRP(Single Responsibility Principle) - 단일 책임 원칙

책임은 기능이라고 이해하면 쉽다.

1개의 클래스는 1개의 책임(기능)만 갖고 있어야 한다는 원칙이다.

그렇다면 어째서 이런 원칙을 지켜야하는가?

 

유지보수 VS 성능

개발자 입장에서만 생각한다면 코드 전체를 여러 클래스가 아닌 한 클래스에 전부 몰아넣어서 확인한다면 외부참조 등 여러가지 문제에서 해방될수 있을것이다 (성능을 우선시한 결과이며 코드의 가독성, 유지보수등의 문제는 논외로 치자..)

하지만 단순한 계산프로그램이 아닌 100개의 기능을 구현하였고 이것이 얽히고 섥혀 난잡하게된 상황에서 1개의 기능을 수정한 경우는 큰 에러가 발생할것이다.

 

한가지 예시를 들어보겠다. 

상단의 사진은 한 게임의 대미지 계산공식으로 해당 공식에선 여러 변수들이 계산되는걸 볼수있는데(캐릭터 스텟, 몬스터 방어력으로 2개이상 참조) 만일 일정시간동안 입는 대미지별로 몬스터의 방어력이 달라지는 기능을 넣는다고 가정해보자.(기능변경으로 단일책임 원칙 위배) -> 레이드시 보스의 순삭을 막기위한 기능이다

 

이 기능이 정상적으로 작동이 된다면 문제가 없겠지만 에러가 발생하는경우 어디서 에러가 발생하였는지 찾을때 굉장히 어려움이 발생할것이고 이러한 기능을 방지하고자 만들어진게 SRP이다. (유지보수 우선)

 

또한 이전의 카카오 화재사태처럼 코드가 소실된경우 이러한 에러발생시 이와 관련된 에러를찾을때 굉장한 시간이 걸리는데 SRP를 지켜가며 설계한경우 문제되는 부분만 캐치하여 코드만 기입하면 되기에 DR(Disaster Recovery) 에 대응하는 시간또한 줄어든다고 볼수있다.

 

(SRP는 모듈화를 최대한 지향한다고 생각하면 된다.)

(모듈화 - SW설계에서 기능단위로 분해, 추상화하여 재사용 및 공유가능한 수준으로 만들어진 단위로 모듈화가 강해질수록 다른 객체와의 의존,연관성이 줄어든다.)

 

장점

클래스의 책임영역이 확실하다 -> 책임 변경(기능수정,추가 등)에 따른 연쇄변경에서 자유롭다 (한개만 바꿔도 다른곳 신경 안써도 된다)

응집도(cohesion)강화, 결합도(coupling)약화, 가독성 향상, 유지보수 용이

(응집도 -  모듈 내부의 기능적인 집중정도 // 결합도 - 모듈 상호간 의존하는 정도)

 

요점정리

1. 클래스는 1개의 책임(1개의 기능)만 갖는다

2. 클래스는 캡슐화를 하여야한다.

3. 클래스를 변경하려는 이유는 단 하나(필요할때마다 구현) 해야한다.

4. 유지보수 VS 성능에서 유지보수가 이긴결과이다.


 

OCP(Open Closed Priciple) - 개방 폐쇄 원칙

SW개발작업중 1개의 모듈에 수정을 가할경우 그 모듈을 이용하는 다른 모듈을 전부 고쳐야한다면 수정하기에 굉장히 어려운 작업일것이다.

이러한 불편함을 해소하고자 세운 원칙이 OCP이다.

확장에 열려있어야하며 수정엔 닫혀있어야 한다. (추상화라고 생각하고 이해하면 쉽다)

 

먼저 확장과 변경에 대한 설명부터 시작하겠다.

확장 -> 모듈의 확장성(파생)을 보장하는것 (새로운 변경사항 발생시 코드를 기능을 추가할수 있는것 -> 추상메서드 구현)

변경 -> 객체를 직접적으로 수정하는건 제한해야한다(새로운 변경사항이 발생했을때 객체를 직접적으로 수정하지 않게 해야한다 -> 추상메서드를 수정하지말고 구현메서드를 수정해라)

 

위의 요약을 해석하자면 원래코드(추상메서드)를 변경없이 기존 코드에 새로운 코드를 추가함 또는 상속받음으로써 기능의 추가 및 변경이 가능하다. (추상메서드의 구현)

즉, 다형성을 활용하여 생성하는 클래스를 최소한으로 하되 상속 및 추상클래스를 구현하는 방식으로 설계하라 

 

이것을 가장 잘 확인해볼수 있는것이 자바의 기본API이다. (개인적인 의견입니다!...)

우리는 필요할때마다 자바에 내장된 각종 API를 가져와서 사용하고 필요하면 수정Override하여 API 자체를 수정하는 작업은 기피하고있는데 이것이 가장 OCP를 잘지키고있는 모습이라 생각한다.

 

여담으로 하단에 서술할 DIP는 OCP를 기반으로 작성 된다고 한다.

 

요점정리

1. 다형성과 확장을 최대한 지향하는것이다.

2. 기능이 필요할경우 추상화 및 상속을 활용하여 필요할때마다 구현하되 필요한 기능은 상위클래스가 아닌 하위클래스를 변경할것 

 


LSP(Listov Substitution Priciple): 리스코프 치환 원칙

상위객체를 하위객체로 치환하여도 상위타입을 사용하는 프로그램은 정상동작해야한다.

즉, 언제든 자식객체가 부모타입으로 교체하여도 문제없이 돌아가야한다.(다형성)

 

이를 조금더 자세히 설명하자면 다음과 같다.

 

정사각형 클래스가 직사각형 클래스를 상속하면 정사각형의 특징인 "네 변의 길이는 동일하다" 라는 특징과 그렇지 않은 직사각형의 차이로 인하여 직사각형을 정사각형 클래스로 치환해서 사용할 때, 두 클래스의 특징 차이 때문에 오류가 날 수 있다는 의미이다.

 

부모객체와 이를 상속한 자식객체가 잇을떄 부모객체를 호출하는 동작에서 자식객체가 부모객체를 완전히 대체할수 있다는 원칙이다. -> 자식객체의 확장이 부모객체의 방향을 온전히 따르도록 권고하는 원칙

 

요점정리

부모클래스의 인스턴스를 사용하는 위치에 자식클래스의 인스턴스를 대신 사용해도 정상작동해야 한다. (부모클래스의 구현조건사항을 자식클래스도 따라야한다. -> 오버라이딩할때 다형성을 지켜지도록 설계하자)


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

큰덩어리의 인터페이스를 작은단위로 분리시켜 필요한 메서드만 사용하게 해야한다. (적재적소에 맞도록 인터페이스를 설계해야한다.)

 

예를들어 OS는 버전이 올라가면서 여러가지 기능을 지원하기 시작했는데 현재 window 98에 없던 블루투스 기능이 현재(window 22)는 구현되어 있다 가정해보자. (이하 A 인터페이스 -> 1998버전, B 인터페이스 -> 2022버전)

업데이트 방식을 인터페이스로 구현한다고 가정해보면 A 인터페이스를 사용할경우 22버전,98버전 모두 사용에 문제가 없으나 (2022의 블루투스 기능은 추가로 구현하면 되기에 문제가 되지않음) B 인터페이스를 사용할경우 98버전에선 사용할수도 없는 기능인 블루투스기능을 구현해야하는 불편함이 생긴다. (불필요한 코드가 늘어난다)

 

 

즉, 인터페이스를 너무 크게 만들면 나중에 이를 사용할때 사용안하는 코드를 구현하는데 불필요한 코드가 늘어나니 가능하면 최소단위로 만들어라 이것이 ISP의 주요목적이다. (인터페이스는 다중상속을 지원한다)

 

요점정리

1. 인터페이스를 너무 크게(기능이 많게) 만들면 나중에 이를 구현할때 사용안하는 코드를 구현하는데 불필요한 코드가 늘어나니 가능하면 최소단위로 만들어라

2. 한번 분리한 인터페이스를 추가 수정사항이 생겨서 또 이것을 분리하는행위는 하지말아라(한번 구성하였는데 또 분리한다면 기존에 사용되던 인터페이스를 구현한 객체들도 전부 수정이 필요하기 때문이다)


DIP(Dependency Inversion Principle) - 의존 역전 원칙

(구현된)클래스를 참조하는게 아닌 그 상위요소로 참조하라

추상클래스, 추상메서드를 우선적으로 사용할것이며 구현클래스는 가능하면 건드리지 말아라.

의존 - 객체끼리 서로 참조등으로 A를 실행하기 위해선 B가 필요한경우 A는 B에 의존한다고 한다 (내가 메세지를 보내기 위해서는 카카오톡이 필요하다 -> 내가 메세지를 보내는 행동을 위해선 카카오톡이 필요(의존)한다)

의존성주입 - 클래스 외부에서 의존되는것을 대상 객체의 IV에 주입하는것

 

이것을 조금더 풀어쓰면 상위모듈(부모개체), 하위모듈(자식개체) 모두 추상화에 의존해야하며 상위모듈이 하위모듈에 의존하면 안된다.

즉, 의존관계를 형성할때 구체적인것(변하기 쉬운 구현객체)보단 추상적인것(변화하기 어려운 추상객체)에 의존해야하며(상위모듈(부모개체), 하위모듈(자식개체) 모두 추상화에 의존해야하며) 저수준의 모듈이 변경되어도 고수준 모듈은 타격을 입지 않는 형태가 좋다. (상위모듈이 하위모듈에 의존하면 안된다)

 

요점정리

참조가 필요할경우 가능하다면 구현된 클래스가 아닌 그것의 상위요소 (추상클래스 및 인터페이스)를 우선적으로 참조하라 (구현된 클래스가 수정이 되는경우 이것을 참조하는 객체도 수정이 필요하기 때문이다)


'CS' 카테고리의 다른 글

프로세서, 프로세스  (0) 2023.03.21
주소 바인딩  (0) 2023.03.18
제네릭(Generic)  (0) 2023.03.14
스택트레이스(Stack Trace)  (0) 2023.02.26
객체 지향 프로그래밍 (OOP) - 1  (0) 2022.12.25

+ Recent posts