객체지향 설계의 모든것
# 자바스크립트에서의 객체지향 설계를 이해하고 각 핵심 개념을 이해해봅시다.
2024년 10월 19일
학습 배경
취업 준비 과정에서 다양한 기업의 면접을 경험하면서, 객체지향 설계의 원리와 그 의미에 대한 질문을 자주 받았습니다. 답변을 막힘없이 이어갈 수는 있었지만, 간결하고 명확하게 설명할 수 있는 깊이 있는 이해가 부족하다고 느꼈습니다. 이에 대한 학습의 필요성을 느끼게 되었고, 이번 포스팅을 통해 객체지향 설계에 대한 체계적인 학습과 더불어 그 의미를 명확히 이해하고자 합니다.
객체지향 설계 OOP 란
객체지향 프로그래밍(OOP)은 데이터를 중심으로 그 데이터를 처리하는 방법을 객체로 캡슐화하여 소프트웨어의 재사용성, 확장성, 유지보수성을 높이는 기법입니다. 자바스크립트는 프로토타입 기반의 언어로 이러한 객체 지향 프로그래밍을 지원합니다. OOP의 가장 큰 장점은 코드의 중복을 최소화하고, 모듈화를 통해 프로젝트의 복잡성을 효과적으로 관리할 수 있다는 점입니다.
자바스크립트에서는 클래스, 상속, 캡슐화, 다형성과 같은 OOP의 핵심 개념이 활용됩니다. 특히, ES6부터는 클래스 문법이 도입되어, 전통적인 객체 지향 프로그래밍 언어에서 사용하는 클래스 기반의 구조를 손쉽게 구현할 수 있게 되었습니다. 이를 통해 더 직관적이고 명확한 객체 지향 설계가 가능해졌습니다. ES6의 클래스 문법은 개발자들이 객체 지향 설계를 보다 쉽게 이해하고 적용할 수 있도록 도와줍니다.
OOP를 활용함으로써 코드의 구조를 더욱 체계적이고 논리적으로 설계할 수 있으며, 이는 곧 소프트웨어의 품질 향상으로 이어집니다. 이러한 객체 중심의 사고방식은 복잡한 문제를 해결하는 데 큰 도움을 줄 수 있습니다.
클래스와 인스턴스
클래스는 한마디로 객체를 생성하기 위한 템플릿입니다. 클래스는 데이터와 그 데이터를 처리하는 메소드를 포함하며, 이 클래스로부터 생성된 객체를 인스턴스라고 부릅니다. 쉽게 말해, 클래스는 일종의 '붕어빵 틀' 같은 역할을 하며, 이 틀을 통해 여러 객체를 생성해낼 수 있습니다.
❗️객체(Object)란 여러 속성을 하나의 단위로 묶은 데이터 타입입니다. 각 속성은 key와 value 형태로 저장됩니다.
자바스크립트에서 클래스를 사용하여 객체를 생성하는 방법을 아래 예시를 통해 살펴보겠습니다
위 코드에서 Person
클래스는 name
과 age
라는 두 개의 속성을 가지며, greet
라는 메소드를 통해 객체의 정보를 출력할 수 있습니다. person1
과 person2
는 각각 클래스로부터 생성된 인스턴스이며, 각 인스턴스는 고유의 name
과 age
를 가집니다.
이처럼 클래스를 통해 같은 형식의 여러 객체를 효과적으로 생성할 수 있으며, 코드의 재사용성을 높일 수 있습니다. 클래스 기반의 설계는 유지보수가 용이하고, 확장이 쉬운 코드를 작성하는데 큰 도움이 됩니다.
객체지향 프로그래밍의 특징, 상속
상속은 객체지향 프로그래밍에서 코드의 재사용성을 높이기 위해 상위 클래스의 속성(변수)과 메소드(기능)를 하위 클래스가 물려받는 것을 의미합니다. 상속을 통해 공통된 기능과 속성을 다양한 클래스에서 사용할 수 있게 되어, 코드의 중복을 줄이고 유지보수를 향상 시킬 수 있습니다.
예를 들어, 우리가 '인간'이라는 클래스가 이미 존재한다고 가정해보겠습니다. 이 '인간' 클래스의 속성과 메소드를 활용하여 '축구선수'라는 클래스를 만들 수 있습니다. "공통적인 부분은 최대한 상속해서 쓰자"가 상속의 핵심 모토입니다. 자식 클래스는 부모 클래스를 상속받아 부모의 속성과 메소드를 그대로 사용하거나, 필요에 따라 오버라이딩(Overriding)하여 독자적인 기능을 추가할 수 있습니다.
자바스크립트에서 상속을 구현하는 방법을 아래와 같이 예시로 들어보겠습니다
위의 예시에서 SoccerPlayer
는 Human
클래스를 상속받고 있으며, name
과 age
속성은 Human
클래스에서 물려받습니다. SoccerPlayer
클래스만의 추가적인 속성인 position
과 메소드 play
는 별도로 정의되었습니다. super()
키워드는 부모 클래스의 생성자를 호출하여 상속받은 속성들을 초기화하는 데 사용됩니다.
오버라이딩(Overriding)
오버라이딩은 자식 클래스에서 부모 클래스에 정의된 메소드를 재정의하여 사용하는 것을 말합니다. 이는 상속 관계에서 주로 활용되며, 부모 클래스의 기본 동작을 자식 클래스에 맞게 변경할 수 있습니다. 오버라이딩 된 메소드는 같은 이름과 매개변수를 가지며, 자식 클래스의 인스턴스에서 호출될 때 부모 클래스의 메소드를 대신하여 실행됩니다.
오버로딩(Overloading)
오버로딩은 같은 이름을 가진 메소드를 여러 개 정의하고, 매개변수의 타입이나 개수를 다르게 함으로써 각각 다른 기능을 수행하도록 하는 것입니다. 주의할 점은, 자바스크립트는 원래 함수 오버로딩을 기본적으로 지원하지 않기 때문에 매개변수나 타입에 따라 다른 동작을 수행하도록 함수 내부에서 조건문을 사용하여 구현해야합니다.
❗️ 즉 **오버라이딩(Overriding)**은 "부모의 것을 자식이 덮어쓴다"라고 이해할 수 있습니다. 부모 클래스에 이미 정의된 메소드를 자식 클래스에서 새롭게 정의하여, 자식 클래스의 특징에 맞게끔 동작을 변경하는 것을 말합니다. 예를 들어, 동물 클래스가 "소리 내기"라는 메소드를 가지고 있다면, 이를 상속받은 개 클래스는 "동물이 내는 소리" 대신 "멍멍"이라고 소리를 낼 수 있도록 메소드를 변경할 수 있습니다.
**오버로딩(Overloading)**은 "같은 이름의 메소드가 다양한 상황에 맞춰 다르게 작동한다"라고 이해할 수 있습니다. 같은 이름의 메소드라도 입력받는 매개변수의 수나 종류에 따라 다른 기능을 수행하도록 여러 버전을 정의하는 것입니다. 자바스크립트는 기본적으로 메소드 오버로딩을 지원하지 않고, 필요에 따라 조건문을 통해 비슷한 기능을 구현하게 됩니다.
객체지향 프로그래밍의 특징, 추상화
추상화는 복잡한 시스템을 이해하고 설계하는데 중요한 역할을 합니다. 추상화는 객체지향 설계에서 구체적인 세부사항을 감추고, 필요한 핵심 개념만을 드러내어 복잡성을 줄이는 기법입니다.
추상화를 통해 복잡한 시스템을 단순화 할 수 있는데 예를 들어, 자동차를 운전할 때 우리는 핸들, 페달, 그리고 기어와 같은 기본적인 조작법에만 집중합니다. 엔진의 내부 구조나 연료의 흐름을 알 필요는 없습니다. 이처럼 추상화는 사용자가 필요로 하는 기능에만 집중할 수 있도록 도와줍니다.
추상화는 보통 클래스와 인터페이스를 통해 구현됩니다. 예를 들어, 다양한 종류의 '동물'을 표현하는 프로그램에서, '동물'이라는 공통의 속성과 메소드를 가진 추상적인 클래스나 인터페이스를 정의할 수 있습니다. 각각의 동물(예: 개, 고양이)은 '동물' 클래스를 상속받아 자신만의 구체적인 구현을 가질 수 있습니다. 이에 따라 각 동물은 자신의 방식대로 행동하지만, 외부에서는 동일한 '동물'로 취급할 수 있게 됩니다.
객체지향 프로그래밍의 특징, 캡슐화
캡슐화는 객체지향 프로그래밍에서 데이터를 보호하고, 불필요한 데이터 노출을 방지하기 위한 기법입니다. 캡슐화를 통해 클래스 내부의 데이터와 메소드를 외부로부터 숨기고, 클래스가 제공하는 인터페이스를 통해서만 접근할 수 있도록 합니다.
캡슐화는 객체의 속성과 메소드를 하나의 단위로 묶고, 외부에서 접근할 필요가 없는 세부 구현은 감추는 것을 의미합니다. 이는 마치 캡슐에 필요한 약만 담고 외부로부터 보호하는 것과 같습니다. 필요한 경우에만 약을 꺼내 쓸 수 있듯이, 객체 내부의 데이터는 지정된 메소드를 통해서만 접근할 수 있습니다.
캡슐화는 클래스 내에서 접근 제한자를 사용하여 구현됩니다. 일반적으로 public
, protected
, private
과 같은 키워드를 사용하여 접근 범위를 지정합니다. 예를 들어, 자바스크립트에서 클래스 내의 변수는 외부에서 직접 접근할 수 없도록 let
이나 const
를 사용하고, 외부에서 접근 가능한 메소드를 통해서만 값을 설정하거나 읽어올 수 있습니다.
위의 예시에서 #balance
프라이빗 필드(private field)는 외부에서 직접 접근할 수 없으며 클래스 내부의 메소드를 통해서만 접근 및 수정할 수 있습니다.
객체지향 프로그래밍의 특징, 다형성
다형성은 하나의 인터페이스를 통해 여러 객체가 서로 다른 방식으로 동작할 수 있게 하는 것입니다. 이로 인해 프로그램 설계가 유연해지고, 변경 및 확장이 용이해집니다. 실세계 비유를 통해 다형성의 개념을 쉽게 이해할 수 있습니다.
운전자와 자동차
- 운전자와 자동차라는 예를 들어보겠습니다. 운전자는 자동차를 운전하는 역할을 합니다. 이때 자동차라는 역할은 여러 가지 구현체를 가질 수 있습니다. 예를 들어, K3, 아반떼, 테슬라 같은 자동차들이 있습니다. 운전자는 K3를 운전하다가 아반떼나 테슬라로 바꾸더라도, 자동차의 역할이 변하지 않기 때문에 동일하게 운전할 수 있습니다.
- 이 상황에서 중요한 것은, 운전자는 자동차를 어떻게 운전해야 하는지(역할)만 알면 되지, 그 자동차의 내부 구조와 설계(구현)에 대해 알 필요가 없다는 점입니다. 즉, 운전자는 자동차가 K3인지, 아반떼인지, 테슬라인지 알지 못해도 운전할 수 있습니다.
- 이와 같이 클라이언트(운전자)는 인터페이스(자동차의 운전 방법)에만 의존하고, 내부의 구현체가 변경되더라도 영향을 받지 않게 됩니다.
JavaScript에서는 다형성을 클래스 상속, 메서드 오버라이딩, 그리고 동적 타입 특성을 통해 쉽게 구현할 수 있습니다. 아래는 JavaScript에서 Vehicle
인터페이스를 이용해 다양한 자동차 클래스를 구현하고, 이를 통해 다형성을 활용하는 예제입니다.
위 예제에서 Vehicle
은 역할을, K3
, Avante
, Tesla
는 각기 다른 구현을 담당합니다. startDriving
함수는 Vehicle
객체를 받아 drive
메서드를 호출하지만, 실제로 어떤 구체적인 자동차가 사용될지는 알 필요가 없습니다. 즉, Vehicle
이라는 역할만 알면 K3
, Avante
, Tesla
등 구현체의 변경에 영향을 받지 않습니다.
즉, 다형성은 클라이언트와 구현체의 결합을 줄이고, 인터페이스를 기반으로 객체 간의 협력을 효율적으로 설계하는 데 중요한 역할을 하는데, 이를 통해 코드의 가독성, 유지보수성, 확장성을 크게 향상시킬 수 있습니다.
상속 vs 다형성
상속(Inheritance)
- 정의: 하나의 클래스가 다른 클래스의 속성(데이터)과 메소드(동작)를 물려받는 것을 의미합니다
- 목적: 코드의 재사용성과 확장성을 높이는 데 중점을 둡니다. 기본 클래스(부모 클래스)의 기능을 하위 클래스(자식 클래스)에게 상속하여, 중복 코드를 줄이고 일관성을 유지할 수 있게 합니다.
- 구현 방법: 한 클래스가 다른 클래스의 속성과 메소드를 상속받음으로써 구현됩니다. 예를 들어,
Cat
클래스가Animal
클래스를 상속받아Animal
의 속성과 메소드를 사용할 수 있습니다.
다형성(Polymorphism)
- 정의: 다형성은 동일한 메소드나 연산자가 다양한 객체 타입에서 다르게 동작하도록 하는 것. 즉, 같은 이름의 메소드가 객체의 타입에 따라 다른 구현을 사용할 수 있다는 의미입니다.
- 목적: 객체를 일관성 있게 다루고, 유연한 코드 작성을 가능하게 하여 코드 확장성과 재사용성을 높이는 데 중점을 둡니다.
- 구현 방법: 주로 메소드 오버라이딩과 인터페이스를 통해 구현됩니다. 다양한 클래스가 같은 인터페이스 또는 부모 클래스의 메소드를 오버라이딩하여 자신만의 동작을 정의할 수 있습니다.
상속과 다형성의 관계
- 연관성: 다형성은 일반적으로 상속을 기반으로 구현됩니다. 상속을 통해 자식 클래스는 부모 클래스의 메소드를 오버라이딩할 수 있으며, 이를 통해 다형성을 구현할 수 있습니다.
- 차이점: 상속은 클래스 간의 구조를 설계하고 속성과 메소드를 공유하는 데 중점을 두는 반면, 다형성은 같은 인터페이스를 통해 다양한 객체를 유연하게 처리하는 데 중점을 둡니다.
React, Next에서의 객체지향 설계 적용
React 및 Next.js와 같은 현대적인 프론트엔드 프레임워크는 주로 함수형 프로그래밍의 패러다임을 많이 활용하지만, 객체지향 프로그래밍(OOP)의 개념도 여전히 사용할 수 있습니다. 두 접근 방식은 서로 배타적이지 않으며, 적절하게 조합하여 사용할 수 있습니다.
- 컴포넌트 기반 아키텍처: React와 Next.js는 컴포넌트를 중심으로 설계되며, 컴포넌트는 본질적으로 객체입니다. 컴포넌트는 상태(state)와 생명주기(lifecycle)를 관리하는 논리를 캡슐화하여, 재사용 가능하고 유지보수하기 쉬운 단위를 제공합니다.
- 상속과 컴포지션: React는 상속보다는 컴포지션(조합)을 권장하지만, 조합 역시 OOP의 중요한 설계 기법입니다. 여러 컴포넌트를 조합하여 더욱 복잡한 컴포넌트를 구성할 수 있으며, 이를 통해 코드 재사용성을 높일 수 있습니다.
- 상태 관리: 상태가 복잡한 애플리케이션에서는 객체지향적인 설계를 통해 상태를 체계적으로 관리할 수 있습니다. 예를 들어, Redux 같은 상태 관리 라이브러리는 액션과 리듀서의 객체지향적인 설계 패턴을 사용하여 예측 가능한 상태 변화를 관리합니다.
결론적으로, React와 Next.js에서는 함수형 프로그래밍의 패러다임이 많이 활용되고 있지만, 객체지향 설계의 원칙과 기법도 여전히 중요한 역할을 할 수 있습니다. 나는 객체지향을 적용할거야, 함수형 프로그래밍을 적용할거야 하는게 아닌 두 가지 접근 방식의 장점을 이해하고, 각 프로젝트의 요구사항에 맞게 선택적으로 사용하는 것이 중요하다고 생각합니다.