이전 포스팅에서 객체지향 프로그래밍이 등장하게 된 배경을 기반으로 OOP 의 핵심 키워드와, 4가지 큰 특징에 대해서 알아보았다. 이번 포스팅에선 객체지향 프로그래밍의 특성과 장점을 최대한으로 끌어올리기 위해 프로그램을 어떻게 설계해야 하는 지에 대한 이야기를 다뤄본다.
객체지향 설계과정
- 요구사항 (제공해야 할 기능) 을 찾고 세분화 한다. 그리고 그 기능을 알맞은 객체로 할당한다.
- 기능을 구현하는 데에 필요한 데이터를 객체에 추가한다.
- 해당 데이터를 이용하는 기능을 구현한다. (기능은 최대한 캡슐화)
- 객체 간에 어떻게 메소드 호출을 주고받을 지 결정한다.
객체지향 설계원칙
흔히 SOLID 라고 부르는 5가지 설계원칙이 존재한다. 솔직히 원문 그대로 해석하면 외계어가 따로 없다. 하나씩 살펴보도록 하자. 같은 개념을 반복하여 풀어나가는 방식을 택했기 때문에, 이해가 될 때까지 설명을 읽어보고 다음 개념으로 넘어가도 좋다.
SRP (Single Responsibility) 단일 책임 원칙
- 클래스는 단 한개의 책임을 가져야 함
- 클래스를 변경하는 이유는 단 하나여야 함
- 이를 지키지 않으면, 한 책임의 변경에 의해 다른 책임과 관련된 코드에 영향을 미칠 수 있음
→ 이렇게 되면 유지보수가 매우 비효율적
책임 이라는 말이 너무 추상적이다..
SRP 에서 이야기하는 책임이란, '기능' 정도로 생각하면 된다. 만약 한 클래스가 수행할 수 있는 기능 (책임) 이 여러 개라면, 클래스 내부의 함수끼리 강한 결합을 발생할 가능성이 높아진다. 응집도는 높고 결합도는 낮은 프로그램을 설계하는 것이 비로소 객체지향 설계의 핵심인데, 이것이 위반되는 것이다. 새로운 요구사항이나 프로그램 변경에 의해 클래스 내부의 동작들이 연쇄적으로 변경되어야 할 수도 있다. 이는 유지보수가 비효율적이므로, 책임을 잘게 쪼개어 분리시킬 필요가 있다.
예를 들어 어떤 클래스내에 A 라는 메소드가 있고, 이 A 메소드는 A 메소드의 결과를 기반으로 B 메소드를 호출하며, B 메소드는 B 메소드의 결과를 기반으로 C 메소드를 호출하도록 구현이 되어있다고 해보자. 이 때 만약 A 메소드의 동작이 일부 수정된다고 할 때, B 와 C 메소드를 전부 바꿔야 할 상황이 발생할 수 있다. 유지보수가 매우 비효율적인 것이다. 따라서 이들을 모두 분리할 필요가 있다.
OCP (Open-Closed) 개방-폐쇄 원칙
- 확장에는 열려있어야 하고, 변경에는 닫혀 있어야 함
- 즉, 기존의 코드를 변경하지 않고 기능을 수정하거나 추가할 수 있도록 설계해야 함
- 이를 지키지 않으면 instanceof 와 같은 연산자를 사용하거나, 다운 캐스팅 발생
어떤 모듈의 기능을 하나 수정할 때, 그 모듈을 이용하는 다른 모듈들 역시 줄줄이 고쳐야 한다면 유지보수가 복잡할 것이다. 따라서 개방 폐쇄 원칙을 잘 적용하여 기존 코드를 변경하지 않아도 기능을 새롭게 만들거나 변경할 수 있도록 해야 한다.
그렇지 않으면 객체지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성 등을 모두 잃어버리는 셈이고, OOP를 사용하는 의미가 사라지게 된다.
OCP 는 추상화 (인터페이스) 와 상속 (다형성) 등을 통해 구현해낼 수 있다. 자주 변화하는 부분을 추상화함으로써 기존 코드를 수정하지 않고도 기능을 확장할 수 있도록 함으로써 유연함을 높이는 것이 핵심이다.
LSP (Liskov Substitution) 리스코프 치환 원칙
- 하위 타입 객체는 상위 타입 객체에서 가능한 행위를 수행할 수 있어야 함
→ 즉, 상위 타입 객체를 하위 타입 객체로 치환해도 정상적으로 동작해야 함 - 상속관계에서는 꼭 일반화 관계 (IS-A) 가 성립해야 한다는 의미 (일관성 있는 관계인지)
- 상속관계가 아닌 클래스들을 상속관계로 설정하면, 이 원칙이 위배됨 (재사용 목적으로 사용하는 경우)
결국은, 리스코프 치환 원칙을 지키지 않으면 개방 폐쇄 원칙을 위반하게 되는 것이다. 기능 확장을 위해 기존의 코드를 여러 번 수정해야 할 것이다. 따라서 상속 관계를 잘 정의하여 LSP 원칙이 위배되지 않도록 설계해야 한다.
ISP (Interface Segregation) 인터페이스 분리 원칙
- 클라이언트는 자신이 사용하는 메소드에만 의존해야 한다는 원칙
- 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 않아야 함
→ 하나의 통상적인 인터페이스보다는 차라리 여러 개의 세부적인 (구체적인) 인터페이스가 나음 - 인터페이스는 해당 인터페이스를 사용하는 클라이언트를 기준으로 잘게 분리되어야 함
각 클라이언트가 필요로 하는 인터페이스들을 분리함으로써, 클라이언트가 사용하지 않는 인터페이스에 변경이 발생하더라도 영향을 받지 않도록 만들어야 하는 것이 핵심이다.
DIP (Dependency Inversion) 의존 역전 원칙
- 의존 관계를 맺을 때, 변하기 쉬운 것 (구체적인 것) 보다는 변하기 어려운 것 (추상적인 것)에 의존해야 함→ 구체화된 클래스에 의존하기 보다는 추상 클래스나 인터페이스에 의존해야 한다는 뜻
- 즉, 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 됨
- 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 함
- 저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요없는 형태가 이상적
총 정리
SRP 와 ISP 는 객체가 커지는 것을 막아준다. 객체가 단일 책임을 갖도록 하고 클라이언트마다 특화된 인터페이스를 구현하게 함으로써 한 기능의 변경이 다른 곳까지 미치는 영향을 최소화하고, 이는 기능 추가 및 변경에 용이하도록 만들어 준다.
LSP 와 DIP 는 OCP 를 서포트한다. OCP 는 자주 변화되는 부분을 추상화하고 다형성을 이용함으로써 기능 확장에는 용이하되 기존 코드의 변화에는 보수적이도록 만들어 준다. 여기서 '변화되는 부분을 추상화'할 수 있도록 도와주는 원칙이 DIP 이고, 다형성 구현을 도와주는 원칙이 LSP 인 것이다.
'JAVA Programming' 카테고리의 다른 글
[89] 이클립스 스킨 다운 방법 , 자바 API 보는 방법 , 이클립스 단축키 (0) | 2020.08.07 |
---|---|
[88] MultiThread 의 deadlock 과 wait() , notify() 와 notifyAll()- while사용 차이 (0) | 2020.08.04 |
[88] MultiThread 의 deadlock 과 wait() , notify() 와 notifyAll()- while사용 차이 (0) | 2020.08.04 |
[87] Multi Thread 임계영역 , 동기화 2가지 방법(메소드,블록) (0) | 2020.08.04 |
[86] Thread 종료하기 . boolean 으로 while문 true 시 인터럽트발생 (0) | 2020.08.03 |