출제기준 세세항목: 객체지향(OOP) · 디자인패턴
예전 방식의 프로그램은 '먼저 이것을 하고, 다음에 저것을 한다'처럼 처리 순서를 중심으로 짜는 경우가 많았습니다. 이를 절차지향이라고 합니다. 규모가 작을 때는 이해하기 쉽지만, 프로그램이 커지면 데이터와 그 데이터를 고치는 코드가 여기저기 흩어져, 하나를 바꿀 때 어디까지 영향이 가는지 알기 어려워집니다.
객체지향(OOP, Object-Oriented Programming)은 프로그램을 '객체'라는 단위들이 서로 협력하는 모습으로 구성합니다. 객체는 자기 데이터와 그 데이터를 다루는 기능을 함께 가진 하나의 덩어리입니다. 쇼핑몰이라면 회원, 상품, 장바구니, 주문이 각각 객체가 되고, '장바구니야, 이 상품을 담아 줘'처럼 서로 부탁을 주고받으며 일을 처리합니다.
객체지향을 이루는 기본 용어를 정리해 보겠습니다. 객체가 가진 데이터를 속성(attribute)이라 하고, 객체가 할 수 있는 동작을 메서드(method) 또는 오퍼레이션(operation)이라고 합니다. 클래스(class)는 같은 종류의 객체들이 공통으로 가질 속성과 메서드를 정의한 틀이고, 그 틀로 실제로 만들어 낸 객체 하나하나를 인스턴스(instance)라고 합니다. 붕어빵 틀이 클래스라면, 틀에서 찍어 낸 붕어빵 하나하나가 인스턴스인 셈입니다. 객체에게 어떤 메서드를 실행해 달라고 보내는 요청은 메시지(message)라고 부릅니다.
객체지향 분석·설계 방법론 중에서는 럼바우(Rumbaugh)의 OMT가 시험에 자주 나옵니다. 시스템을 객체 모델링(객체와 관계, 객체 다이어그램), 동적 모델링(시간에 따른 상태 변화, 상태 다이어그램), 기능 모델링(데이터가 어떻게 처리되어 흐르는가, 자료 흐름도) 순서로 바라봅니다. 이 밖에 부치(Booch), 유스케이스를 강조한 제이콥슨(Jacobson) 방법 등이 있습니다.
캡슐화는 관련 있는 속성과 메서드를 하나의 객체(클래스)로 묶는 것입니다. 묶으면서 바깥에서 알 필요 없는 내부 사정은 감추고, 정해진 메서드를 통해서만 데이터에 접근하게 합니다. 이렇게 내부를 감추는 것을 정보 은닉이라고 합니다. 은행 계좌 객체라면 잔액 값을 밖에서 마음대로 바꾸지 못하게 숨기고, '입금'과 '출금' 메서드로만 바꾸게 하는 식입니다. 그러면 출금할 때 잔액이 모자라는지 검사하는 규칙을 한 곳에서 지킬 수 있습니다.
캡슐화의 효과는 '공통 모듈 설계'에서 배운 결합도와 이어집니다. 다른 객체는 메서드라는 입구만 알면 되므로, 내부 구현을 바꿔도 입구가 같으면 바깥은 영향을 받지 않습니다. 즉 결합도가 낮아지고, 고칠 때 생기는 부작용이 줄어듭니다.
추상화는 복잡한 대상에서 지금 필요한 핵심만 골라 남기고 나머지 세부 사항은 덜어 내는 것입니다. 쇼핑몰에서 '회원'을 설계할 때 실제 사람의 키나 좋아하는 음식까지 담지 않고, 이름·연락처·배송지처럼 서비스에 필요한 것만 남기는 것이 추상화입니다. 공통점을 뽑아 상위 개념으로 묶는 것도 추상화에 속합니다. 무엇을 추상화하느냐에 따라 기능(과정) 추상화, 자료 추상화, 제어 추상화로 나눠 부르기도 합니다.
상속은 이미 있는 클래스(상위 클래스, 부모)의 속성과 메서드를 새 클래스(하위 클래스, 자식)가 물려받는 것입니다. '동물' 클래스에 '먹는다', '잠잔다'를 정의해 두면 '개'와 '고양이' 클래스는 이를 다시 쓰지 않고 물려받은 뒤, 자기만의 특징만 덧붙이면 됩니다. 공통 코드를 한 곳에 두니 중복이 줄고 재사용이 쉬워집니다. 하위 클래스가 여러 상위 클래스를 동시에 물려받는 것을 다중 상속이라고 하며, 언어에 따라 허용하지 않기도 합니다.
다형성은 같은 메시지를 보내도 받는 객체에 따라 각자 알맞게 다르게 반응하는 성질입니다. '소리 내라'는 같은 요청에 개 객체는 짖고, 고양이 객체는 야옹 하고 웁니다. 보내는 쪽은 상대가 정확히 무엇인지 몰라도 '동물'로만 다루면 되므로, 새 동물이 추가되어도 보내는 쪽 코드를 고칠 필요가 없습니다.
다형성을 구현하는 방법으로 두 가지 용어를 꼭 구분해야 합니다. 오버라이딩(재정의)은 상위 클래스에서 물려받은 메서드를 하위 클래스가 같은 이름, 같은 매개변수로 다시 정의해 내용을 바꾸는 것입니다. 오버로딩(중복 정의)은 한 클래스 안에서 이름은 같지만 매개변수의 개수나 타입이 다른 메서드를 여러 개 두는 것입니다. 오버라이딩은 '물려받은 것을 고쳐 쓰기', 오버로딩은 '같은 이름을 여러 모양으로 두기'로 기억하면 됩니다.
클래스 사이에는 상속 말고도 여러 관계가 있습니다. 상속처럼 '개는 동물이다(is-a)'로 표현되는 관계를 일반화라고 합니다. '자동차는 바퀴를 가진다(part-of, has-a)'처럼 전체와 부분의 관계는 집합(집약)이라 하고, 그중 부분이 전체 없이는 존재하지 않을 만큼 강하게 묶인 경우를 합성(복합, 포함)이라고 합니다. 서로 알고 지내며 메시지를 주고받는 관계는 연관, 잠깐 사용하는 정도의 약한 관계는 의존이라고 부릅니다.
객체지향 언어로 짜기만 한다고 좋은 설계가 되지는 않습니다. 변경에 강하고 이해하기 쉬운 구조를 만들기 위해 널리 쓰이는 다섯 가지 원칙이 있고, 머리글자를 따서 SOLID라고 부릅니다.
단일 책임 원칙(SRP, Single Responsibility Principle)은 하나의 클래스는 하나의 책임만 가져야 한다는 원칙입니다. 다르게 말하면 클래스를 바꿔야 할 이유가 하나여야 합니다. 주문 클래스가 주문 처리와 영수증 출력과 이메일 발송을 모두 맡고 있다면, 이메일 양식이 바뀌어도 주문 클래스를 고쳐야 합니다. 개방-폐쇄 원칙(OCP, Open-Closed Principle)은 확장에는 열려 있고 수정에는 닫혀 있어야 한다는 원칙입니다. 새 기능을 넣을 때 기존 코드를 고치기보다 새 코드를 덧붙이는 방식으로 할 수 있어야 합니다.
리스코프 치환 원칙(LSP, Liskov Substitution Principle)은 상위 클래스가 쓰이는 자리에 하위 클래스 객체를 넣어도 프로그램이 올바르게 동작해야 한다는 원칙입니다. 하위 클래스가 상위 클래스의 약속을 깨뜨리면 상속을 잘못 쓴 것입니다. 인터페이스 분리 원칙(ISP, Interface Segregation Principle)은 클라이언트가 자기가 쓰지 않는 메서드에 의존하지 않도록, 큰 인터페이스 하나보다 용도별로 작은 인터페이스 여러 개로 나누라는 원칙입니다.
의존 역전 원칙(DIP, Dependency Inversion Principle)은 구체적인 구현이 아니라 추상화(인터페이스)에 의존하라는 원칙입니다. 주문 처리 코드가 'A카드사 결제 클래스'를 직접 부르면 결제사를 바꿀 때마다 주문 코드를 고쳐야 하지만, '결제 수단'이라는 인터페이스에만 기대게 하면 결제사가 바뀌어도 주문 코드는 그대로입니다. 다섯 원칙 모두 결국 '변경이 생겼을 때 고칠 곳을 적게, 좁게 만들자'는 같은 목표를 향합니다.
| 생성 (5) | 구조 (7) | 행위 (11) |
|---|---|---|
| 싱글톤 | 어댑터 | 책임 연쇄 |
| 팩토리 메서드 | 브리지 | 커맨드 |
| 추상 팩토리 | 컴포지트 | 인터프리터 |
| 빌더 | 데코레이터 | 반복자 |
| 프로토타입 | 퍼사드 | 중재자 |
| 플라이웨이트 | 메멘토 | |
| 프록시 | 옵서버 | |
| 상태 | ||
| 전략 | ||
| 템플릿 메서드 | ||
| 방문자 |
설계를 하다 보면 비슷한 문제가 프로젝트마다 반복해서 나타납니다. '이 객체는 딱 하나만 있어야 한다', '기존 클래스의 인터페이스가 새 코드와 맞지 않는다', '상태가 바뀌면 여러 곳에 알려야 한다' 같은 문제입니다. 디자인 패턴은 이런 자주 생기는 설계 문제와, 그것을 풀어 온 검증된 해법을 이름을 붙여 정리한 것입니다. 코드를 그대로 복사해 쓰는 부품이 아니라, 상황에 맞게 적용하는 설계의 틀입니다.
가장 널리 알려진 것은 GoF(Gang of Four)라고 불리는 네 명의 저자가 정리한 23개의 패턴입니다. 이들은 하나의 패턴을 이름, 문제(언제 쓰는가), 해법(어떻게 구성하는가), 결과(적용했을 때 얻는 것과 치르는 대가)라는 요소로 설명했습니다. 패턴에 이름이 있으면 팀원끼리 '여기는 옵서버로 하자'처럼 짧게 설계를 주고받을 수 있다는 장점도 큽니다.
GoF 패턴은 목적에 따라 세 가지로 나뉩니다. 생성 패턴은 객체를 어떻게 만들지를 다루고 5개입니다. 구조 패턴은 클래스와 객체를 어떻게 조합해 더 큰 구조를 만들지를 다루고 7개입니다. 행위 패턴은 객체들이 어떻게 책임을 나누고 서로 소통할지를 다루고 11개입니다. 시험에서는 어떤 패턴이 어느 분류에 속하는지를 묻는 문제가 많으니, 분류별 목록을 꼭 익혀 두세요.
디자인 패턴은 시스템 전체의 뼈대를 다루는 아키텍처 패턴(계층형, MVC 등)보다 작은 범위, 즉 클래스와 객체 수준의 설계를 다룹니다. 또 문제가 없는데 억지로 패턴을 넣으면 구조만 복잡해지므로, 문제를 먼저 보고 맞는 패턴을 고르는 것이 순서입니다.
싱글톤(Singleton)은 어떤 클래스의 인스턴스가 프로그램 전체에서 단 하나만 만들어지도록 하고, 어디서든 그 하나에 접근할 수 있게 하는 패턴입니다. 설정 정보나 로그 기록기처럼 여러 개 있으면 오히려 문제가 되는 객체에 씁니다.
팩토리 메서드(Factory Method)는 객체를 만드는 메서드를 상위 클래스에 정해 두되, 실제로 어떤 클래스의 객체를 만들지는 하위 클래스가 결정하게 하는 패턴입니다. 추상 팩토리(Abstract Factory)는 서로 관련된 객체들의 묶음(제품군)을 구체적인 클래스를 지정하지 않고 한꺼번에 만들 수 있는 인터페이스를 제공합니다. 예를 들어 '밝은 테마'와 '어두운 테마'마다 버튼, 입력창, 메뉴를 한 벌로 맞춰 만들어 주는 공장을 두는 식입니다.
빌더(Builder)는 복잡한 객체를 여러 단계로 나눠 조립하게 해서, 같은 조립 과정으로 서로 다른 결과물을 만들 수 있게 하는 패턴입니다. 프로토타입(Prototype)은 새 객체를 처음부터 만들지 않고, 미리 만들어 둔 원본 객체를 복제해서 만드는 패턴입니다. 생성 비용이 큰 객체를 여러 개 만들어야 할 때 유용합니다.
어댑터(Adapter)는 인터페이스가 맞지 않는 클래스를 중간에서 변환해 함께 쓸 수 있게 하는 패턴입니다. 해외 여행용 전원 변환 플러그처럼, 기존 코드를 고치지 않고 연결만 맞춰 줍니다. 브리지(Bridge)는 기능의 계층과 구현의 계층을 분리해, 둘을 서로 독립적으로 확장할 수 있게 하는 패턴입니다.
컴포지트(Composite)는 개별 객체와 그 객체들을 담은 묶음을 같은 방식으로 다룰 수 있도록 트리 구조로 구성하는 패턴입니다. 폴더 안에 파일과 다른 폴더가 함께 들어 있어도 '크기 계산'을 똑같이 요청할 수 있는 것이 예입니다. 데코레이터(Decorator)는 기존 객체를 감싸서 실행 중에 기능을 덧붙이는 패턴으로, 커피에 시럽과 우유를 차례로 추가하듯 기능을 겹겹이 쌓을 수 있습니다.
퍼사드(Facade)는 복잡하게 얽힌 여러 하위 시스템 앞에 단순한 창구 하나를 두어, 사용하는 쪽이 그 창구만 상대하게 하는 패턴입니다. 플라이웨이트(Flyweight)는 많은 객체가 공통으로 가진 부분(내재 상태)은 하나만 만들어 함께 쓰고, 객체마다 달라지는 부분(외재 상태)은 사용할 때 밖에서 따로 넘겨받게 해 메모리를 아끼는 패턴입니다. 프록시(Proxy)는 실제 객체 대신 대리 객체를 앞에 두어, 접근을 통제하거나 실제 객체가 필요할 때까지 생성을 미루는 등의 일을 맡기는 패턴입니다.
책임 연쇄(Chain of Responsibility)는 요청을 처리할 수 있는 객체들을 사슬처럼 이어 두고, 처리할 수 있는 객체가 나올 때까지 요청을 차례로 넘기는 패턴입니다. 커맨드(Command)는 요청 자체를 객체로 만들어, 요청을 저장하거나 줄 세우거나 실행 취소할 수 있게 합니다. 인터프리터(Interpreter)는 간단한 언어의 문법을 클래스로 표현하고 그 문장을 해석하는 패턴입니다. 반복자(Iterator)는 묶음의 내부 구조를 드러내지 않고도 원소를 하나씩 차례로 꺼내 볼 수 있게 합니다.
중재자(Mediator)는 여러 객체가 서로를 직접 부르지 않고 중재자 객체를 통해서만 소통하게 해, 얽힌 관계를 한곳으로 모으는 패턴입니다. 메멘토(Memento)는 객체의 내부를 드러내지 않으면서 특정 시점의 상태를 저장해 두었다가 되돌릴 수 있게 하는 패턴으로, 실행 취소 기능에 쓰입니다. 옵서버(Observer)는 한 객체의 상태가 바뀌면 그 객체를 지켜보는(구독한) 객체들에게 자동으로 알림이 가도록 하는 패턴입니다. 구독한 채널에 새 글이 올라오면 알림이 오는 구조를 떠올리면 됩니다.
상태(State)는 객체의 상태를 별도의 객체로 만들어, 상태가 바뀌면 같은 요청에도 행동이 달라지게 하는 패턴입니다. 전략(Strategy)은 같은 목적을 이루는 여러 알고리즘을 각각 캡슐화해 두고, 실행 중에 필요한 것으로 바꿔 끼울 수 있게 하는 패턴입니다. 결제 방식이나 할인 계산 방식을 상황에 따라 바꾸는 경우에 잘 맞습니다.
템플릿 메서드(Template Method)는 작업의 전체 순서(골격)는 상위 클래스에 정해 두고, 그중 일부 단계만 하위 클래스가 바꿔 정의하게 하는 패턴입니다. 방문자(Visitor)는 객체 구조는 그대로 두고, 그 구조에 적용할 새로운 연산을 별도의 방문자 객체로 분리해 추가할 수 있게 하는 패턴입니다. 행위 패턴은 개수가 많으니 '책임 연쇄, 커맨드, 인터프리터, 반복자, 중재자, 메멘토, 옵서버, 상태, 전략, 템플릿 메서드, 방문자'를 목록으로 한 번에 묶어 기억해 두세요.
쇼핑몰 주문 코드 안에 'if 카드면… else if 계좌이체면…'이 길게 이어져 있어, 간편결제를 추가할 때마다 주문 코드를 고쳐야 했습니다. '결제 수단' 인터페이스를 만들고 카드·계좌이체·간편결제를 각각 그 인터페이스를 구현한 클래스로 나누자(전략 패턴), 주문 코드는 인터페이스에만 의존하게 되었습니다(DIP). 이제 새 결제 수단을 넣을 때 주문 처리 코드는 그대로 두고 클래스를 하나 추가하면 됩니다(OCP). 어떤 결제 수단을 쓸지 고르는 지점 한 곳만 새 클래스를 알도록 손보면 됩니다.
새로 만드는 알림 기능은 send(메시지) 형태의 메서드를 기대하는데, 회사에서 쓰던 문자 발송 라이브러리는 transmit(번호, 내용, 옵션) 형태였습니다. 라이브러리를 고치지 않고, send를 받으면 transmit 형태로 바꿔 호출하는 어댑터 클래스를 하나 두어 두 코드를 연결했습니다.
사용자가 품절 상품에 '재입고 알림 신청'을 누르면 그 상품을 구독하는 목록에 등록됩니다. 재고가 0에서 1 이상으로 바뀌는 순간 상품 객체가 구독자 모두에게 알림을 보냅니다. 상품 코드는 누가 구독했는지 세부 사항을 몰라도 되고, 알림 방식(앱 푸시, 이메일)을 추가해도 상품 코드는 그대로입니다.