책임과 협력
책임(Responsibility)
- 책임은 한 객체가 특정하게 수행해야 하는 범위와 기능을 말한다.
- 사람이 “모닝” 차량을 운전해야 하는 상황을 프로그래밍해야 한다고 가정해본다.
- 먼저 사람이라는 객체를 정의한다.
- 사람은 “운전”이라는 행위를 하게 된다.
- 즉 “사람” 객체는 “운전하는 것”에 대한 “책임”이 있다.
1 |
|
- 이번에는 모닝 차량에 대한 객체를 만든다.
- 차량은 출발하고 멈추는 기능을 제공하는 게 일반적이다.
- 따라서 모닝 차량은 “가속”과 “감속”에 대한 책임이 있다고 말할 수 있다.
1 |
|
SRP(Single Responsibility Principle)
- 하나의 객체는 하나의 책임만 가지도록 설계하는 것이 일반적으로 좋다고 알려져 있다.
- 객체의 정체성이 명확하고, 변경에 용이하며, 추후 재사용 가능하고, 높은 응집도와 낮은 결합도를 유지할 수 있기 때문이다.
이를 SRP(Single Responsibility Principle)라고 한다.
협력
- 위에서 정의한 기능을 실제로 구현해본다.
- 먼저
User
는 구체적으로 다음처럼 구현해볼 수 있다.
1 |
|
- 그리고
MorningCar
는 다음처럼 구현할 수 있다.
1 |
|
- “사람이 운전하는 상황”은 결과적으로
User
객체와MorningCar
객체를 각자 책임에 맞게 설계하고,User
가MorningCar
객체를 호출하여 구현해냈다. - 이렇게 객체가 서로 필요에 따라 의존하는 것을 “협력”이라고 표현한다.
책임 주도 설계
- 객체 지향에서 상황에 필요한 객체들의 책임을 중심으로 시스템을 설계해나가는 방법을 책임 주도 설계라고 한다.
- 즉, 하나의 책임이 곧 하나의 객체가 되고, 책임이 곧 객체의 정체성이 되는 것이다.
- 책임주도 설계로 시스템을 설계해나가면, 객체들의 정체성이 명확해진다.
- 그리고 높은 응집도와 낮은 결합도를 유지하며, 시스템은 객체들의 협력으로 로직을 진행하게 된다.
추상화
추상화가 없을 때
- 위의 예시에서, 사람이 만약 ‘모닝’ 차량 말고 ‘포르쉐’ 차량도 운전하고 싶다.
그러면 다음처럼 코드를 수정해볼 수 있다.
1 |
|
- 그런데 만약 이후에 ‘포르쉐’ 말고도 ‘벤츠’, ‘BMW’ 등 더 다양한 차를 운전하고 싶다면 어떻게 해야 할까?
- 새로운 차량이 추가될 때마다
__init__()
함수 안에if .. else
문이 추가될 것이다.
추상화가 있을 때
- 사실 사람은 ‘특정 차’를 운전하는 게 아니라 그냥 ‘차’라는 개념 자체를 운전하는 것으로 생각해볼 수 있다.
- ‘모닝’, ‘포르쉐’, ‘BMW’ 등등.. 모두 공통적으로 ‘차’다.
- ‘차’는 모두 가속과 감속 기능을 제공하기 때문이다.
- 이렇게 구체적인 객체(물체)들로부터 공통점을 생각하여 한 차원 높은 개념을 만들어내는(생각해내는) 것을 추상화(abstraction)라고 한다.
- 이제 우리는 ‘차’ 라는 객체를 다음처럼 추상 클래스로 구현할 수 있다.
1 |
|
- ‘차’는 공통으로 가속, 감속 기능을 제공하므로 다음처럼 추상 메서드도 추가할 수 있다.
1 |
|
- ‘차’라는 추상화된 역할의 구현체인 ‘모닝’과 ‘포르쉐’ 차량은 해당 추상 클래스를 상속받아 구현할 수 있다.
1 |
|
- 이제 사람 입장에서는 구체적인 차량이 아닌 추상적인 차라는 객체만 알면 된다.
1 |
|
다형성
- 이제 사람은 다음처럼 상황에 따라 운전하고 싶은 차량을 설정할 수 있다.
1 |
|
- 현재
User
는 생성자에서 추상 클래스인Car
를 의존하고 있지만, 실제로 이 User를 사용하는 클라이언트에서는Car
가 아니라Car
의 자식 클래스인MorningCar
와PorscheCar
객체를 생성자에서 파라미터로 넘겨줄 수 있다. - 이처럼
User
입장에서Car
가 상황에 따라 그 형태가 달라질 수 있는데, 이런 특성을 다형성(polymorphism)이라고 한다.
또한, 이렇게 외부에서 실제로 의존하는 객체를 만들어 넘겨주는 패턴을 의존성 주입(dependency injection)이라고 부른다. - 다형성은 객체 지향의 꽃이라 불릴 만큼 중요한 특성이다.
- 추상 클래스 혹은 인터페이스로 객체를 상위 타입으로 추상화하고(위 예에서는 “차”가 바로 이런 상위 타입), 그 객체의 하위 타입들(모닝, 포르쉐 등)은 이러한 상위 타입의 추상 클래스나 인터페이스를 구현하도록 하면, 코드 설계가 전반적으로 유연해져 수정과 확장이 매우 용이해진다.
OCP(Open-Close Principle)
- OCP(Open-Closed Principle)는 “소프트웨어는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다는 원칙”이다.
- 쉽게 말해, 요구사항이 바뀌어 기존 코드를 변경해야 할 때, 기존 코드를 수정하지 않고 새로운 코드를 추가하는 것이 좋다는 것이다.
캡슐화
- 캡슐화(encapsulation)는 객체 내부의 데이터나 메서드의 구체적인 로직을 외부에서 모르고 사용해도 문제가 없도록 하는 특성이다.
캡슐화하지 않을 때
1 |
|
- 이제 MorningCar 객체를 다음처럼 사용한다.
1 |
|
- 이때 위 코드의 차량에 필요한 기름을 주유하기 위해
car.fuel
에 직접 접근하여+
연산을 하고있다.
또한, 차량의 남은 주유량을 퍼센트로 확인하기 위해car_max_fuel
에도 접근하여/
연산을 하고있다. - 현재
MorningCar
는 캡슐화를 지키지 않은 객체이다.
왜냐하면MorningCar
를 사용하는 쪽에서 주유를 하기 위해car.current_fuel
에 직접+
연산을 하고 있고, 남은 주유량 확인을 위해 직접 필요한 연산을 모두 하고 있기 때문이다. - 특히
car.current_fuel
에 직접 연산을 하게 되면, 자칫car.max_fuel
을 초과한 값이car.current_fuel
에 들어갈 수 있으므로, 이는 버그에 취약한 코드이다.
이렇게 캡슐화되지 않은 코드는 본인의 책임을 다하고 있지도 않으며(SRP 위반), 추후 코드 변화에도 매우 취약하다. - 만약
MorningCar
객체의fuel
변수의 이름이 다른 이름으로 바뀌게 되면MorningCar
를 사용하는 모든 코드를 수정해야 한다.
Getter
, Setter와
캡슐화
- 보통 객체 지향은 Java로 처음 배우곤 하는데, 이때 캡슐화를 공부하는 과정에서
Getter/Setter
메서드를 접하는 경우가 많다. - 객체의 인스턴스 변수는 모두
private
으로 두고, 이를Getter/Setter
메서드로 접근하라고 배우게 된다.
그리고 “캡슐화 =Getter/Setter
메서드 추가해주기”로 생각하게 되기도 한다. - 정확히 말하면,
Getter/Setter
메서드는 캡슐화를 돕는 방법 중 하나이지 캡슐화 그 자체가 아니다.(더욱이 항상 모든 인스턴스 변수에 대해Getter/Setter
를 다는 것은 좋지 않다.) - 캡슐화는 객체가 알고 있는 것을 외부에 알리지 않는 것이며, 필요한 경우에 한해 객체 내부가 알고있는 것의 일부를
Getter
메서드로 제공하거나,Setter
메서드로 변경하게 하는 기능을 제공하는 것이다. - 캡슐화는
Getter/Setter
메서드의 존재 여부로 결정되는 것이 아니라, 객체가 외부에서 알아도 되지 않을 내용을 잘 숨기고, 알아야 할 내용이나 제공하는 행동들을 필요에 맞게 제공하는 행동을 이야기한다. - 캡슐화가 잘된 객체는 사용하는 입장에서도 편하다.
캡슐화할 때
- 이제
MorningCar
를 캡슐화 해본다. - 우선 외부에서 이 객체에 필요로 하는 기능을 메서드로 제공하고, 객체의 속성을 직접 수정하거나 가져다 쓰지 않도록 해야한다.
- 객체 밖에서는 이러한 정보나 로직을 모르기 때문에, 이를 정보은닉이라고도 부른다.
1 |
|
- 이제
MorningCar
객체를 다음처럼 사용할 수 있다.
1 |
|
- 최종적으로
MorningCar
를 사용하는 클라이언트는MorningCar
의 속성에 직접 접근하여 연산할 필요가 없다.
그저MorningCar
가 제공하는 퍼블릭 메서드를 사용하면 된다. - 또한
MorningCar
의 속성fuel
이나max_fuel
등의 값이 바뀌어도 클라이언트의 코드는 바뀌지 않는다. - 따라서 수정에도 더 용이한 코드가 된다.
정리
- 객체 지향은 객체들의 책임과 협력으로 이루어진다.
- 추상화를 통해 객체들의 공통 개념을 뽑아내어 한 차원 더 높은 객체를 만들 수 있다.
- 객체를 사용하는 입장에서 과연 어떤 역할을 할 객체가 필요한지 생각해보고 추상회된 객체를 생각하면 된다.
- 객체를 추상화한 클래스를 만든 후, 이 클래스를 상속받아 실제 구체적인 책임을 담당하는 객체를 만들 수 있다.
- 다형성으로 코드는 수정과 확장에 유연해진다.
- 캡슐화를 통해 객체 내부의 정보와 구체적인 로직을 외부에 숨길 수 있다.
- 외부에선 그저 객체가 제공하는 공개 메서드를 사용하면 된다.
- 캡슐화로 코드는 수정과 확장에도 유연해진다.