
자바스크립트는 객체 기반(Object-based) 언어이기 때문에 객체의 생성 원리와 구조를 이해하는 것이 중요하다.
객체지향 개념을 이해하면 코드 구조를 더 명확하게 설계할 수 있고, 복잡한 프로그램을 구성할 때 데이터와 기능을 논리적으로 묶어 관리할 수 있다.
또, 현대 웹 개발에서 널리 사용되는 TypeScript와 React 역시 객체지향적 개념과 구조를 기반으로 동작한다.
따라서 객체지향 철학을 이해하는 것은 프로그램 설계 방식 자체를 이해하는 과정이라고 볼 수 있다.
구조적 프로그래밍은 프로그램을 함수 중심으로 분해하여 설계하는 방식이며,
이 방식은 구조가 단순한 프로그램에서는 효율적이지만, 프로그램이 커질수록 데이터와 기능이 분리되어 관리가 어려워질 수 있다.
객체지향 프로그래밍(OOP)은 프로그램을 객체 중심으로 설계하는 방식이다.
하나의 프로그램에 버튼, 창, 메뉴와 같은 객체들이 존재한다고 가정하였을 때,
해당 객체들이 이벤트를 통해 서로 동작하면서 프로그램이 실행된다.
추상화는 여러 객체에서 공통적인 특징만을 추출하여 개념화하는 과정이다.
복잡한 현실 세계의 대상에서 핵심적인 속성만 남기고 불필요한 세부 사항을 제거하는 것이다.
현실 세계에는 푸들, 포메라니안, 골든 리트리버 등 다양한 종류의 개가 존재한다.
하지만 이들을 하나의 개념으로 묶으면 개(Dog) 라는 추상적인 개념을 만들 수 있다.
추상화 개념은 철학에서도 등장한다.
고대 철학자 플라톤은 현실 세계의 개별적인 사물들 뒤에는 본질적인 형태(이데아) 가 존재한다고 보았다.
우리가 보는 다양한 개들은 모두 서로 다르지만 그 뒤에는 개라는 본질적인 개념 이 존재한다.
위와 같은 개념을 이데아(idea) 라고 하며, 이러한 사고 방식이 객체지향에서의 추상화 개념과 연결된다.
객체지향 프로그래밍에서는 현실 세계의 객체를 모델링할 때 추상화를 사용한다.
이 방식은 다음과 같은 장점이 있다.
캡슐화는 데이터를 외부로부터 숨기고 필요한 기능만 공개하는 객체지향 설계 원칙이다.
단순히 데이터를 숨기는 것이 아니라 데이터와 이를 처리하는 기능을 하나의 단위로 묶어 보호하는 것을 의미한다.
객체지향에서 클래스는 클래스 = 데이터 + 메소드 와 같은 구조를 가진다.
예를 들어 객체 내부에는 은닉된 데이터가 존재하고, 외부에서는 멤버 함수(메소드) 를 통해서만 접근할 수 있다.
이 구조는 실제 캡슐처럼 내용물을 보호하는 구조와 유사하다.
클래스는 객체를 만들기 위한 설계도이다.
사전적인 의미로는 분류 또는 집합 이라는 뜻을 가지며,
프로그래밍에서는 사용자가 정의한 새로운 데이터 타입을 의미한다.
즉 사용자 정의 데이터 타입(User Defined Data Type)인 것이다.
멤버 변수 (Field)
객체의 상태(State) 를 나타낸다.
'사람'을 객체로 표현하자면, 이름, 나이, 키 와 같은 것들이 멤버 변수가된다.
멤버 함수 (Method)
객체가 수행할 수 있는 행동(Behavior) 을 나타낸다.
'사람'을 객체로 표현하자면, 걷는다, 말한다, 공부한다 와 같은 것들이 멤버 함수가된다.
이와 같이 현실 세계의 객체를 분석하여 '상태'는 변수로 '행동'은 메소드 로 표현하는 과정이 객체 모델링이다.
클래스는 다음과 같은 형태로 선언한다.
class 클래스이름
{
접근지정자 데이터타입 멤버변수;
접근지정자 데이터타입 메소드();
생성자
소멸자
}
접근 지정자는 클래스 내부 데이터에 대한 접근 범위를 제어하는 기능이다.
대표적인 접근 지정자는 다음과 같다.
| 구분 | 설명 |
|---|---|
| public | 모든 곳에서 접근 가능 |
| protected | 상속 관계에서만 접근 가능 |
| internal | 같은 프로젝트 내부에서 접근 가능 |
| private | 클래스 내부에서만 접근 가능 |
특히 private 은 캡슐화를 구현할 때 가장 중요한 접근 지정자이다.
클래스를 통해 선언된 변수를 객체(Object) 라고 한다.
class Dog
{
private char pudle, pome, jindo;
public void bark();
}
위와 같은 클래스 선언문에서 Dog → 클래스, a → 객체 이다.
즉 객체는 클래스로부터 생성된 실체(instance) 이다.
객체는 일반적으로 다음과 같이 생성된다.
Dog a = new Dog();
이 과정에서 메모리 구조는 다음과 같이 구성된다.
Stack의 변수는 Heap에 생성된 객체를 참조(reference) 하게 된다.
Stack 영역 Heap 영역
a의 값 Dog의 주소
--------- ---------
| 0x1234 | --> | 0x1234 |
--------- ---------
| | | 0x5678 |
--------- ---------
| | | 0x1234 |
--------- ---------
인터페이스는 메서드의 목록만 정의하고 실제 구현은 포함하지 않는 명세(Specification) 이다.
즉 인터페이스는 어떤 기능이 존재해야 하는지만 정의하고, 실제 동작은 구현 클래스에서 작성한다.
인터페이스는 다음과 같은 형태로 정의된다.
interface Phone
{
void call();
void message();
void wifi();
}
접근지정자 interface 인터페이스이름
{
메서드 선언;
}
인터페이스를 사용하는 클래스는 해당 메서드를 반드시 구현해야 한다.
class Smartphone : Phone
{
public void call() { Console.WriteLine("통화 기능 실행"); }
public void message() { Console.WriteLine("문자 메시지 실행"); }
public void wifi() { Console.WriteLine("와이파이 연결"); }
}
인터페이스는 다음과 같은 목적을 가진다.
아키텍트 -> 인터페이스 정의 / 실무자 → 인터페이스 구현 이런 방식으로 작업 분리가 가능하다. 모든 변수는 선언 후 반드시 초기화가 필요하다.
객체 역시 클래스 기반으로 생성된 하나의 변수이기 때문에 초기화 과정이 필요하다.
객체가 생성될 때 초기화를 담당하는 특별한 메서드를 생성자(Constructor) 라고 한다.
class Dog
{
public Dog()
{
// 객체 초기화
}
}
// 객체 생성 시 생성자가 자동 호출된다.
Dog a = new Dog();
위 코드가 실행되면 내부적으로 다음 과정이 발생한다.
상속은 이미 만들어진 클래스를 기반으로 새로운 클래스를 만드는 객체지향 개념이다.
부모가 자식에게 재산을 상속하듯, 객체지향에서도 부모 클래스의 속성과 기능을 자식 클래스가 물려받는다.
상속은 아래와 같이 보통 계층 구조(Hierarchy) 형태를 가진다.
생물
├ 동물
│ ├ 개
│ └ 고양이
└ 식물
├ 장미
└ 개나리
상위 하이라키로 올라갈수록 추상적인 개념이고
하위 하이라키로 내려갈수록 구체적인 개념이 된다.
상속은 다음과 같이 표현된다.
// C# 방식
class Dog : Animal
{
}
접근지정자 클래스이름 : 부모클래스
{
}
// Java 방식
class Dog extends Animal
{
}
접근지정자 클래스이름 extends 부모클래스
{
}
상속을 사용하는 이유는 다음과 같다.
실제 프로젝트에서는 다음과 같은 상황에서 상속이 활용된다.
새로운 프로젝트 B가 시작되었는데, A 프로젝트 기능과 유사한 구조를 가진다고 가정한다.
이 경우 새로운 B 프로젝트는 기존 A 프로젝트 기능을 상속받고 새로운 기능만 추가하면 된다.
B 프로젝트
├ A 프로젝트 기능 1,2,3,4
└ 추가 기능 5,6
위와 방식은 코드 중복을 줄이고 개발 속도를 높인다.
객체지향 프로그래밍에서 다형성(Polymorphism) 은 하나의 인터페이스나 이름이 상황에 따라 여러 형태로 동작할 수 있는 특성을 의미한다.
대표적으로 오버로딩(Overloading) 과 오버라이딩(Overriding) 두 가지 방식으로 구현된다.
오버로딩은 같은 이름의 함수라도 전달 인자의 타입이나 개수가 다르면 서로 다른 함수로 동작하도록 정의하는 것을 의미한다.
함수 이름은 같지만 매개변수(parameter)가 다르면 다른 함수로 구분된다.
사전적 의미로 ‘과적하다’, ‘적재하다’ 라는 의미를 가지며,
프로그래밍에서는 하나의 이름에 여러 기능을 담는 것을 의미한다.
class Calculator
{
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
}
위의 예시 함수는 모두 같은 이름의 함수지만 매개변수가 다르기 때문에 다른 함수로 동작한다.
오버라이딩은 부모 클래스에서 정의된 메서드를 자식 클래스에서 재정의하여 사용하는 것을 의미한다.
부모 클래스의 기능을 자식 클래스에서 새롭게 구현하여 덮어쓰는 것이라는 개념이다.
오버라이딩은 상속 관계가 반드시 존재해야 한다.
class Animal {
void sound() {
System.out.println("동물이 소리를 낸다");
}
}
class Dog extends Animal {
void sound() {
System.out.println("멍멍");
}
}
Animal.sound() → 기본 동작
Dog.sound() → 자식 클래스에서 재정의
| 구분 | 오버로딩 | 오버라이딩 |
|---|---|---|
| 개념 | 같은 이름의 함수 여러 개 정의 | 부모 메서드를 자식이 재정의 |
| 관계 | 같은 클래스 내부 | 상속 관계 필요 |
| 매개변수 | 달라야 함 | 동일해야 함 |
| 목적 | 함수 사용 편의성 | 기능 확장 및 재정의 |
람다식은 익명 메서드를 간결하게 표현하는 함수 표현식이다.
별도의 메서드를 정의하지 않고 필요한 위치에서 바로 함수를 작성하는 방식이다.
익명 메서드는 이름이 없는 메서드이다.
메서드를 미리 정의하지 않아도 돼서 코드가 간결해지고 일회성 로직에 적합하다.
람다식 기본 구조는 아래와 같다.
(인수) => 표현식 또는 명령문