객체지향 프로그래밍, 복사와 참조

객체지향 프로그래밍과 모델링에 대해 알아봅니다. 또한 값의 복사와 참조를 배우고, 언제 복사와 참조가 일어나는지 구분 할 수 있도록 합니다.
복사와 참조, 객체지향 프로그래밍, 모델링


객체 지향 프로그래밍

객체 지향 프로그래밍(OOP : Object Oriented Programming)은 코드를 작성하는 일종의 방법론, 패러다임입니다. OOP에서 객체는 어떤 물건이나 추상적인 개념을 의미합니다. 예를 들어 지난번 가계부 프로그램의 장부, 입출금 내역을 객체로 볼 수 있습니다. 객체는 속성(값)들의 집합으로 표현 됩니다. 예로 강아지를 객체로 표현한다면 그 객체는 견종, 나이, 이름 등의 속성과 짖는다, 달린다 등의 속성을 가질 수 있습니다. 현실의 물건이나 개념을 객체로 옮기는 과정을 모델링이라고 합니다. 이때는 현실의 요소들을 추상화하여 프로그램에 필요한 핵심적인 요소만을 코드로 옮기게 됩니다. 작성하는 코드가 말이 되는지(Human-Readable)를 생각하며 모델링하면 짜임새 있는 코드를 작성 할 수 있습니다.

객체는 속성(값)들의 집합

// 객체의 속성은 키-값의 쌍으로 이루어집니다.
var computer = {
  OS: "macOS",
  CPU: "Intel CPU",
  RAM: "8GB",
  created: "2013-10-21"
};

// 키는 "" 문자열로 표현해도 무관합니다.
var dog = {
  "name": "mong",
  "age": 6, 
  "color": "black and white"
};

객체의 속성에 접근하는 법

var obj = {
  number: 10,
  "some-key": "abc" // some-key: "abc" 의 경우는 some - key : "abc"로 인식해 오류가 발생합니다.
};

obj.number = 20;
console.log(obj.number); // 20

var name = "some-key";
console.log(obj[name]); // "abc"
console.log(obj["some-key"]); // "abc"

// obj.some-key 로 접근하면 obj.some - key 로 인식합니다.
// 이런 경우에는 obj["some-key"] 를 이용합니다.

객체는 속성으로 원시값을 가질 수도, 함수를 가질 수도 또는 다른 객체를 가질 수도 있습니다

var x = { a:1, b:"2" };

// 속성으로 값이나 함수를 가질 수 있습니다.
x.c = 10;
x.d = "abc"
x.e = function(){};

function someFunc(){}
var anotherFunc = function(){}

x.someName = someFunc;
x.anotherName = anotherFunc;

// 물론 속성으로 배열도 가질 수 있습니다.
x.z = [1,2,3];

// 속성으로 또 다른 객체도 가질 수 있습니다.
// 즉, 객체는 값으로 취급됩니다.
x.f = {
  g: 10,
  h: {
    deep: {
      deep2: {
        deep3: {
          x: 10
        }
      }
    }
  },
  i: function(){}
};

// 객체가 값이기 때문에 배열에 객체를 담을 수도 있습니다.
x.j = [
    {a:1,b:2},
    {a:2,b:3},
    4,
    5,
    "abc"
  ];

복사와 참조

이름은 메모리의 주소를 가리키는 심볼입니다.

var x = 10; // 값의 할당
var y = x; // 이름의 할당
console.log(y); // 10

위 코드 2번 줄에서 y라는 이름에 x라는 이름을 할당하고 있습니다. 이때는 x라는 이름이 가리키는 주소의 값이 y라는 이름이 가리키는 주소에 복사됩니다.

var x = 10;
var y = x;
y++;
console.log(y, x); // 11, 10

메모리 상에 x의 값과 y의 값은 별개로 존재합니다.

값 바꾸기?

function swap(a,b){
  var temp = a; // a를 잠시 담아두고
  a = b; // a는 b의 값을 할당
  b = temp; // b는 원래 a의 값을 할당
}

var x = 5;
var y = 10;
swap(x,y);
console.log(x,y); // 5,10 ???

swap이라는 함수가 실행될 때 인자로 받아온 a,b는 x,y의 주소가 아니라 스택 위에 복사된 x,y의 복사본을 가리키고있습니다.

배열 복사하기?

var x = [1,2,3];
var y = x;
y.push(4);

console.log(x); // [1,2,3,4] ???

y는 x가 가리키는 배열을 복사한 것이 아니라 x가 가리키는 배열을 그대로 가리키고있습니다. 이를 참조라고 합니다. 이처럼 할당에는 값을 메모리에 복사하여 가리키는 복사(Copy by value)와 원본의 주소를 그대로 가리키는 참조(Copy by reference)의 두가지 방식이 있습니다. 언어별로 복사와 참조를 명시적으로 제어 할 수 있는 경우(대표적으로 C언어의 포인터)도 있고, JavaScript처럼 암묵적으로 경우에 따라 처리하는 경우도 있습니다.

복사

JavaScript에서 원시값이라 불리는 "abc", 123, NaN, null, true, undefined 등에 대해서는 항상 복사가 일어납니다.

참조

JavaScript에서 원시값 외의 모든 객체에 대해서는 항상 참조가 일어납니다.

배열 복사하기!

var x = [1,2,3];
var y = [];
for(var i=0; i < x.length; i++) {
  y.push(x[i]); // push를 호출할 때 Number 값의 복사가 일어남
};
y.push(4);

console.log(x, y); // [1,2,3], [1,2,3,4]

// 이때는 x, y는 별개의 배열이 됩니다.

값 바꾸기!

function swap(a,b){ // 여기서 a,b는 아래의 x,y와 동일한 주소를 가리키고 있습니다.
  var temp = a.val; // 복사
  a.val = b.val; // 복사
  b.val = temp; // 복사
}

var x = {val: 5};
var y = {val: 10};
swap(x,y); // x,y는 참조
console.log(x.val,y.val); // 10,5

클래스와 인스턴스

지금에서야 밝히지만 우리가 지금까지 이용했던 String, Number, Array와 같은 값들도 모두 객체입니다. 각 객체들은 저마다 이런 저런 속성들을 갖고 있습니다.

주요 내장 객체와 그 속성

타입예시속성속성(함수)
Object{a:1, b:2}.a
.b
.toString()
String“abc”.length
[0]
.toString()
.indexOf(“a”)
.replace(“a","A”)
.split(“b”)
.substr(0,2)
Number123.55.toString()
.toFixed(2)
.toLocaleString()
Array[1,2].length
[0]
.toString()
.concat([3,4])
.indexOf(1)
.push(3)
.pop()
.splice(0,1)
.forEach(function(a){ … })
.sort(function(a,b){ … })
Functionfunction(a,b){ return a+b; }.length
.name
.toString()
.apply(x, [1,2])
.call(x, 1, 2)

JavaScript에서는 위 표처럼 Object 타입을 뼈대로 여러개의 타입들을 설계해두었습니다. 이 때 특정 타입의 객체를 생성하기 위한 설계도를 클래스(Class)라고 하고, 설계도를 바탕으로 만들어진 객체를 그 클래스의 인스턴스(Instance)라고 합니다. 언어마다 클래스를 구현하는 방식은 다양하지만 클래스는 기본적으로 생성자라고 불리는 함수를 뼈대로 갖고 있습니다. 클래스의 생성자를 통해서 다양한 초기 값을 가진 인스턴스들을 만들 수 있습니다.

타입이 바로 클래스 생성자

// Array는 Array 타입의 인스턴스를 만들기 위한 생성자 함수입니다.
// 즉 Array()를 통해서 실행할 수 있는 함수입니다.
// 하지만 인스턴스를 만들기 위해서 생성자 함수를 실행할 때는 new 키워드를 앞에 붙혀줍니다.

var x = new Array(1, 2, 3);
var x2 = [1, 2, 3]; // Array 인스턴스를 간략하게 생성하는 축약 문법(리터럴)입니다.

var y = new Object(); 
y.a = 1;
y.b = 2;
var y2 = {a:1, b:2}; // Object 인스턴스를 간략하게 생성하는 축약 문법(리터럴)입니다.

표준 내장 객체

클래스전역 객체설명리터럴
Object모든 객체의 기본이 되는 객체의 클래스{a:1,b:2}
Function함수 객체의 클래스function(){}
Array배열 객체의 클래스[1,2]
String문자열 객체의 클래스“abc”
Date날짜 및 시간을 다루는 객체의 클래스
Number숫자 객체의 클래스123
Boolean논리적 참과 거짓을 다루는 객체의 클래스true
RegExp정규표현식을 다루는 객체의 클래스/([a-z]*)/ig
Math수학 관련 값과 함수들이 담긴 단일 객체
JSONJSON 포맷의 텍스트를 다루는 함수들이 담긴 단일 객체

JavaScript의 표준 내장 객체 중 자주 사용되는 일부는 위 표와 같습니다. 객체들의 주요한 속성들은 앞으로 Node.js와 JavaScript를 공부해 나가면서 천천히 눈에 익히도록 하겠습니다.

클래스 정의하기

이제 우리가 직접 클래스와 생성자 함수를 정의하는 방법을 알아보겠습니다. 추상화하고자 하는 객체의 공통적인 속성과 인스턴스 별로 달라질 수 있는 초기값에 대해 생각해봅니다. 클래스의 이름은 첫글자를 대문자로, 인스턴스의 이름은 첫글자를 소문자로 하는 것이 이름을 구분하기에 좋습니다.

// 클래스 정의
class User{
  // 생성자
  constructor(name, type, email){
    
    // 인스턴스의 속성(값)
    this.name = name; // this는 생성될 인스턴스를 의미합니다.
    this.type = type;
    this.email = email;

    // 인스턴스의 속성(함수)
    this.speak2 = function(){
      console.log("I am "+this.name); // this는 이 함수를 호출하는 인스턴스를 의미합니다.
    }
  }

  // 인스턴스의 속성(함수)
  speak(){
    console.log("My name is "+this.name); // this는 이 함수를 호출하는 인스턴스를 의미합니다.
  }
}

// 인스턴스 생성
var user1 = new User('kim', 'admin', 'kim@benzen.io');
var user2 = new User('son', 'normal', 'son@benzen.io');

console.log(user1, user2);
user1.speak(); // My name is kim
user1.speak2(); // I am kim
user2.speak(); // My name is son

class 키워드는 최신 버전의 JavaScript인 ES6에서(이에 대해서는 이후에 다룹니다.) 등장한 키워드입니다. Node.js에서는 사용해도 문제 될 일이 없지만, 추후 JavaScript 파트에서는 웹 브라우저 호환성을 위해서 사용하지 않도록 하겠습니다.

메소드

위의 user1.speakuser1.speak2의 차이점에 대해 생각해봅시다. user1.speak2는 생성자 안에서 인스턴스에 직접 할당되는 함수이고, user1.speak는 클래스 안에서 정의된 유일한 함수입니다. 이때 user1.speak2user2.speak2는 메모리 상에서 각각 공간을 차지하는 코드가 됩니다.
반면에 user1.speakuser2.speak는 유일한 함수를 인스턴스끼리 공유하게 됩니다. 즉 표면적인 차이는 없지만 .speak 가 .speak2 에 비해 경제적입니다. 또 이런 공통적인 인스턴스의 속성(함수)을 메소드(Method)라고 부릅니다.

클래스 속성과 인스턴스 속성

class Article{
  constructor(title, contents){
    // 인스턴스 속성(값)
    this.title = title;
    this.contents = contents;
    this.created = new Date();

    // 클래스 속성(값)인 list 배열에 생성된 인스턴스를 넣기
    Article.list.push(this);
  }
  
  // 인스턴스 속성(함수)
  print(){
    console.log(this.title, this.contents, this.created);
  }

  // 클래스 속성(함수)
  static printAll(){
    // 여기서 this는 인스턴스가 아닌 Article 클래스를 의미합니다.
    console.log("Total "+ this.list.length + " articles");
    for (var i=0; i < this.list.length; i++){
      var article = this.list[i];
      article.print();
    }
  }
}

// 클래스 속성(값)
Article.list = [];

// 인스턴스를 생성하는 부분
var article1 = new Article('title1', 'contents1');
var article2 = new Article('title2', 'contents2');
article1.print();
Article.printAll();

/**
title1 contents1 Tue Oct 18 2016 16:47:35 GMT+0900 (KST)
Total 2 articles
title1 contents1 Tue Oct 18 2016 16:47:35 GMT+0900 (KST)
title2 contents2 Tue Oct 18 2016 16:47:35 GMT+0900 (KST)
**/

인스턴스에 값이나 메소드를 할당하는 것은 자연스러워 보입니다. 이를 인스턴스 속성, 인스턴스 메소드라고 합니다. 그런데 클래스 자체에 값이나 메소드를 할당 할 수도 있습니다. 이는 static 속성, static 메소드, 클래스 속성, 클래스 메소드 등으로 부릅니다.
클래스 속성은 해당 클래스 전체와 관련된 기능을 담당하도록 구현하는 것이 적절하고, 인스턴스 메소드는 해당 인스턴스의 고유한 속성과 관련된 기능을 구현하는 것이 자연스럽습니다.

예를 들어, 게시물을 추상화한 Article이라는 객체의 클래스가 있고 그 인스턴스인 article1article2가 있습니다. 이때 지금 생성된 모든 게시물 객체들을 전부 출력하는 기능이 필요하다면 article1.printAll 보다는 Article.printAll이 자연스럽습니다. 반면 특정 게시물 한개를 출력하는 기능은 Article.print 보다는 article1.print가 자연스럽습니다. N번째 게시물을 출력하는 .printByIndex(N)Article.printByIndex(2)이 자연스럽습니다.

이처럼 인스턴스 속성은 개별 인스턴스에서 참조 할 수 있는 속성이고, 클래스 속성은 인스턴스 없이 참조 할 수 있는 속성입니다. 필요와 목적에 따라서 자연스럽게 구현하면 됩니다.

모델링 연습

class AccountBook {
  constructor(name, author) {
    this.name = name;
    this.author = author;
    this.list = [];
    this.total = 0;
    AccountBook.instances.push(this);
  }

  deposit(comment, amount) {
    if (this.total + amount < 0) {
      throw new Error(`Not enough balance for ${this.name}`);
    }
    this.total += amount;
    this.list.push({
      comment: comment,
      amount: amount,
    });
  }

  print() {
    var result = `===${this.name} by ${this.author}===\n`;
    for(var i=0; i < this.list.length; i++) {
      var item = this.list[i];
      result += `${item.amount < 0 ? '출금' : '입금'}\t${item.comment}\t${item.amount}원\n`;
    }
    result += `===${this.total}===\n`;
    console.log(result);
  }
}
AccountBook.instances = [];
AccountBook.printAll = function() {
  for(var i=0; i < AccountBook.instances.length; i++) {
    var accountBook = AccountBook.instances[i];
    accountBook.print();
  }
};

var ac1 = new AccountBook('장부1', '김씨');
ac1.deposit('월급', 300);
ac1.deposit('집세', -150);
var ac2 = new AccountBook('장부2', '박씨');

AccountBook.printAll();

상속

상속(Inheritance)은 다른 클래스의 속성들을 이어 받는 것입니다. 하위 클래스에서 어떤 상위 클래스를 상속 받게 되면 상위 클래스의 속성들을 그대로 사용 할 수도 또 덮어 쓸 수도 있습니다. 상속은 객체간의 상하 관계를 표현하고 비슷한 클래스들을 묶어 (())추상화하고 재사용성을 높히기 위해서(()) 사용합니다.

예를 들어 여행 정보와 관련된 프로그램에서 버스와 비행기, 선박의 운송 수단이 있다고 할 때, 이 셋 모두 운송 수단이라는 공통점이 있습니다. 이 때 운송 수단이 갖는 공통적인 속성들을 기반으로 운송 수단이라는 상위 클래스를 모델링하면 견고한 추상화를 꾀할 수 있습니다.

상속과 다형성

class Transportation {
  constructor(id, capacity){
    this.id = id;
    this.capacity = capacity;
  }

  getPrice(){
    return 0;
  }
}

class Airplane extends Transportation { // extends 키워드를 이용합니다.
  constructor(id, capacity, seatClass){
    super(id, capacity); // super는 Transportation 클래스의 생성자를 의미합니다.
    this.seatClass = seatClass;
  }

  getPrice(){ // 상속받은 메소드를 덮어 쓰기
    var price = Airplane.prices[this.seatClass];
    if (!price) { // price가 undefined거나 0이거나 null이거나
      return -1;
    } else {
      return price;
    }
  }
}
Airplane.prices = {
  I: 100,
  B: 300,
  F: 500
};

class Ship extends Transportation {
  constructor(id, capacity, isCruise){
    super(id, capacity);
    this.isCruise = isCruise;
  }

  getPrice(){ // 상속받은 메소드를 덮어 쓰기
    return (this.isCruise) ? 200 : 50; // 삼항 연산자
  }
}

var air1 = new Airplane("747", 20, "F");
var air2 = new Airplane("747", 50, "B");
var ship1 = new Ship("Cruise88", 300, true);
var ship2 = new Ship("Ship39", 150, false);

var list = [air1, air2, ship1, ship2];
for(var i=0; i < list.length; i++) {
  // trans는 각기 Ship 혹은 Airplain 클래스의 인스턴스이지만, 모두 다 Transportation의 인스턴스는 맞습니다.
  var trans = list[i];
  console.log(trans.id, trans.capacity, trans.getPrice());
}

/**
747 20 500
747 50 300
Cruise88 300 200
Ship39 150 50
**/

위의 .getPrice()를 Airplane과 Ship에서 다르게 구현한 것처럼, 내부적인 로직은 다르더라도 표면적인 형태(함수의 인자나 리턴 값의 타입)가 동일한 성질을 다형성(Polymorphism)이라고 합니다.

모델링 연습

class Figure {
  getSize() {
    return 0;
  }
}

class Oval extends Figure {
  constructor(radius1, radius2){
    this.radius1 = radius1;
    this.radius2 = radius2;
  }

  getSize() {
    return this.radius1 * this.radius2 * Math.PI;
  }
}

class Circle extends Oval {
  constructor(radius) {
    super(radius, radius);
  }
}

class Rect extends Figure {
  constructor(width1, width2, height) {
    this.width1 = width1;
    this.width2 = width2;
    this.height = height;
  }

  getSize() {
    return ((this.width1 + this.width2) / 2 ) * this.height;
  }
}

class Rectangle extends Rect {
  constructor(width, height) {
    super(width, width, height);
  }
}

class Square extends Rect {
  constructor(width) {
    super(width, width);
  }
}

대부분의 고급(High-Level) 언어에서는 OOP를 지원하고 있습니다. 또한 기본적인 클래스와 상속의 개념에서 발전해서, 언어별로 이런 저런 키워드를 통해 추상 클래스, 인터페이스, 어댑터, Trait 등의 추가적인 개념을 제공하고 있습니다. 여기서 OOP를 더 깊게 다루지는 않습니다. 하지만 모든 원리는 추상화와 재사용성 혹은 사람이 읽을 수 있는 코드에 있습니다. 추상화와 재사용성의 원리에 입각해서 모델링을 하다가, 또 다른 도구의 필요성을 느끼신다면 그 때 OOP의 추가적인 개념들에 대해서 공부하셔도 충분하실 것으로 생각합니다.

이 강의를 포함한 커리큘럼
저자

김동욱

개발 경력 약 10년, 전문 분야는 웹 및 각종 응용 소프트웨어 플랫폼입니다. Codeflow를 운영하고 있습니다.

2018년 04월 10일 업데이트

지원되지 않는 웹 브라우저거나 예기치 않은 오류가 발생했습니다.