Java

[Java] 객체지향 프로그래밍 1 - 개념 / 클래스와 인스턴스

YouCanDoIt 2024. 3. 18. 22:36

객체지향 언어

객체 지향 이론의 기본 개념은 '실제 세계는 사물(객체)로 이루어져 있으며, 발생하는 모든 사건들은 사물간의 상호작용이다.'라는 것이다.

실제 사물의 속성과 기능을 분석한 다음, 데이터(변수)와 함수로 정의해서 실제 세계를 컴퓨터 속에 옮겨 놓은 것과 같은 가상 세계를 구현한다. 이 가상 세계에서 모의 실험을 해서 많은 시간과 비용을 절약할 수 있다.

객체지향 이론은 상속, 캡슐화, 추상화 개념을 중심으로 점차 구체적으로 발전되었으며 1995년 자바가 발표되고 객체지향 언어는 프로그래밍 언어의 주류로 자리 잡았다.

 

객체지향언어는 기존의 프로그래밍 언어와 다른 전혀 새로운 것이 아니라, 기존의 프로그래밍 언어에 몇가지 새로운 규칙을 추가한 보다 발전된 형태의 언어이다. 이러한 규칙들을 이용해서 코드 간에 서로 관계를 맺어 줌으로써 보다 유기적으로 프로그램을 구성하는 것이 가능해졌다. 

객체지향언어의 특징

객체지향 언어의 주요 특징은 다음과 같다.

 

1. 코드의 재사용성이 높다.

새로운 코드를 작성할 때 기존의 코드를 이용하여 쉽게 작성할 수 있다.

2. 코드의 관리가 용이하다.

코드간의 관계를 이용해서 적은 노력으로 쉽게 코드를 변경할 수 있다.

3. 신뢰성이 높은 프로그래밍을 가능하게 한다.

제어자와 메서드를 이용해서 데이터를 보호하고 올바른 값을 유지하도록 하며, 코드의 중복을 제거하여 코드의 불일치로 인한 오작동을 방지할 수 있다.

 

상속과 다형성과 같은 객체지향개념을 학습할 때 재사용성과 유지보수 그리고 중복된 코드 제거, 이 세가지의 관점에서 보면 보다 쉽게 이해할 수 있을 것이다. 

 

객체지향 프로그래밍

객체지향 프로그래밍은 프로그래머에게 거시적 관점에서 설계할 수 있는 능력을 요구하므로 객체 지향의 장점을 충분히 활용하는 코드를 작성해야한다. 이것이 처음부터 쉽지는 않다. 너무 객체지향 개념에 얽매여서 고민하기 보다 일단 프로그램을 기능적으로 완성한 다음, 어떻게하면 보다 객체지향적으로 코드를 개선할 수 있을지를 고민하여 점차 개선해 나가는 것이 좋다. 

 

클래스와 객체

클래스란 '객체를 정의해 놓은 것' 또는 '객체의 설계도 또는 틀'이라고 정의할 수 있다.

클래스는 객체를 생성하는 데 사용된다.

객체의 사전적 정의는 '실제로 존재하는 것'이다. 우리가 주변에서 볼 수 있는 책상, 의자 등 사물이 곧 객체이다.

객체지향이론에서는 사물과 같은 유형적인 것뿐만 아니라, 개념이나 논리와 같은 무형적인 것들도 객체로 본다.

프로그래밍에서의 객체는 클래스의 정의된 내용대로 메모리에 생성된 것을 뜻한다.

 

클래스와 객체의 관계는 다음과 같은 실생활에 비유할 수 있다.

TV설계도(클래스)는 TV라는 제품(객체)을 정의한 것이며, TV(객체)를 만드는데 사용된다.

클래스를 정의하고 클래스를 통해 객체를 생성하는 이유는 설계도를 통해서 제품을 만드는 이유와 같다.

하나의 설계도만 잘 만들어 놓으면 제품을 만드는 일이 쉬워진다.

제품을 만들 때마다 매번 고민할 필요없이 설계도대로만 만들면 되기 때문이다.

이와 마찬가지로 클래스를 한번만 잘 만들어 놓기만 하면, 매번 객체를 생성할 때마다 어떻게 객체를 만들어야 할지 고민하지 않아도 된다.

 

객체와 인스턴스

클래스로부터 객체를 만드는 과정을 클래스의 인스턴스화(instantiate)라고 하며, 어떤 클래스로부터 만들어진 객체를 그 클래스의 인스턴스(instance)라고 한다.

 

객체의 구성요소

객체는 속성과 기능, 두 종류의 구성요소로 이루어져 있으며, 일반적으로는 객체는 다수의 속성과 다수의 기능을 갖는다. 

즉, 객체는 속성과 기능의 집합이라고 할 수 있다. 

그리고 객체가 가지고 있는 속성과 기능을 그 객체의 맴버(구성원, member)라 한다.

클래스란 객체를 정의한 것이므로 클래스에는 객체의 모든 속성과 기능이 정의되어 있다.

속성과 기능은 다음과 같이 여러가지 용어가 있다.

이중에서도 '속성'보다는 '맴버변수'를, '기능'보다는 '메서드'를 주로 사용할 것이다.

속성(property) 맴버 변수(member variable), 특성(attribute), 필드(field), 상태(state)
기능(function) 메서드(method), 함수(function), 행위(behavior)

 

위의 내용을 토대로 Tv 클래스를 만들어 보면 다음과 같다.

속성(property) 크기, 길이 높이, 색상, 볼륨, 채널 등
기능(function) 켜기, 끄기, 볼륨 높이기, 볼륨 낮추기, 채널 변경하기 등

 

class Tv{
    //변수
    String color; //색깔
    boolean power; //전원 상태
    int channel; //채널
    
    //메서드
    void power(){power = !power;}
    void channelUp(){channel++;}
    void channelDown(){channel--;}
}

 

인스턴스의 생성과 사용

Tv 클래스를 선언한 것은 Tv설계도를 작성한 것에 불과하므로, Tv인스턴스를 생성해야 제품(Tv)을 사용할 수 있다.

다음과 같이 클래스로부터 인스턴스를 생성할 수 있다.

Tv t; //Tv클래스 타입의 참조변수 t선언
t = new Tv(); //Tv인스턴스를 생성한 후, 생성된 Tv인스턴스의 주소를 t에 저장

 

Tv를 사용하려면 Tv 리모콘을 사용해야하는 것처럼, Tv인스턴스를 사용하려면 Tv클래스 타입의 참조변수가 필요하다.

인스턴스는 참조변수를 통해서만 다룰 수 있으며, 참조변수의 타입은 인스턴스의 타입과 일치해야한다.

 

Tv t1 = new Tv();
Tv t2 = new Tv();

 

 

인스턴스의 생성과 사용방법은 다음과 같다.

1. Tv t;

Tv클래스 타입의 참조변수 t를 선언한다. 메모리에 참조변수 t를 위한 공간이 마련된다.

아직 인스턴스가 생성되지 않았으므로 참조변수로 아무것도 할 수 없다.

2. t = new Tv();

연산자 new에 의해 Tv클래스의 인스턴스가 메모리의 빈 공간에 생성된다.

주소가 0x100인 곳에 생성되었다고 가정하자.

이때 맴버 변수는 각 자료형에 해당하는 기본값으로 초기화된다.

color는 참조형이므로 null로, power는 boolean이므로 false로, channel은 int이므로 0으로 초기화된다.

그 다음에는 대입연산자(=)에 의해서 생성된 객체의 주소값이 참조변수 t에 저장된다.

이제는 참조변수 t를 통해 Tv인스턴스에 접근할 수 있다.

인스턴스를 다루기 위해서는 참조변수가 반드시 필요하다.

3. t.channel = 7;

참조변수 t에 저장된 주소에 있는 인스턴스의 멤버변수 channel에 7을 저장한다.

인스턴스의 맴버변수(속성)를 사용하려면 '참조변수.맴버변수'와 같이 하면된다.

4. t.channelDown();

참조변수 t가 참조하고 있는 Tv인스턴스의 channelDown 메서드를 호출한다.

channel Down메서드는 맴버변수 channel에 저장되어 있는 값을 1 감소시킨다.

void channelDown(){ --channel; }

 

같은 클래스로부터 생성되었을지라도 각 인스턴스의 속성(멤버변수)은 서로 다른 값을 유지할 수 있으며, 메서드의 내용은 모든 인스턴스에 대해 동일하다.

다음 그림을 통해 Tv의 두 인스턴스 t1과 t2가 서로 다른 주소를 가지고, 맴버 변수 channel이 다른 값을 갖는 것을 볼 수 있다.

객체 배열

많은 수의 객체를 다뤄야 할 때, 배열로 다루면 편리할 것이다.

객체 역시 배열로 다루는 것이 가능하며, 이를 '객체 배열'이라고 한다.

그렇다고 객체 배열 안에 객체가 저장되는 것은 아니고, 객체의 주소가 저장된다.

Tv tv1, tv2, tv3;

 

위 코드를 객체 배열을 사용하여 작성해보자.

Tv[] tvArr = new Tv[3];

 

길이가 3인 객체 배열 tvArr를 생성하면, 각 요소는 참조변수의 기본값인 null로 자동 초기화된다.

그리고 이 객체 배열은 3개의 객체, 정확히는 객체의 주소를 저장할 수 있다.

위 그림처럼, 객체 배열을 생성하는 것은 그저 객체를 다루기 위한 참조변수들이 만들어진 것일 뿐, 아직 객체가 저장되지 않았다.

객체를 생성해서 객체 배열의 각 요소에 저장하는 것을 잊으면 안된다.

다음과 같이 객체배열을 생성후 각 객체를 생성하자.

Tv tvArr = new Tv[3]; //참조변수 배열(객체 배열)을 생성
//객체를 생성해서 배열의 각 요소에 저장
tvArr[0] = new Tv();
tvArr[1] = new Tv();
tvArr[2] = new Tv();

 

배열에 초기화 블럭을 사용하면, 다음과 같이 한줄로 간단히 나타낼 수도 있다.

Tv[] tvArr = {new Tv(), new Tv(), new Tv()};

 

객체 수가 많을 때는 for문을 활용하면 된다.

Tv[] Arr = new Tv[100];

for(int i = 0; i < tvArr.length; i++){
	tvArr[i] = new Tv();
}

 

프로그래밍 관점에서 클래스의 정의

객체지향론 관점에서 클래스는 '객체를 생성하기 위한 틀'이며 '클래스는 속성과 기능으로 정의되어 있다'고 정의했다.

프로그래밍적인 관점에서 클래스의 정의와 의미를 알아보자.

 

1. 클래스 - 데이터와 함수의 결합

프로그래밍언어에서 데이터 처리를 위한 데이터 저장 형태의 발전 과정은 다음과 같다.

  • 변수 : 하나의 데이터를 저장할 수 있는 공간
  • 배열 : 같은 종류의 여러 데이터를 하나의 집합으로 저장할 수 있는 공간
  • 구조체 : 서로 관련된 여러 데이터를 종류에 관계없이 하나의 집합으로 저장할 수 있는 공간
  • 클래스 : 데이터와 함수의 결합 (구조체 + 함수)

서로 관련된 변수들을 저장하고 이들에 대한 작업을 수행하는 함수드을 함께 정의한 것이 바로 클래스이다.

 

2. 클래스 - 사용자정의 타입 (user-defined type)

프로그래밍 언어에서 제공하는 자료형(primitive type)외에 프로그래머가 서로 관련된 변수들을 묶어서 하나의 타입으로 새로 추가하는 것을 사용자정의 타입(user_defined type)이라고 한다.

기본형의 개수는 8개로 정해져 있지만 참조형의 개수가 정해져 있지 않은 이유는 이처럼 프로그래머가 새로운 타입을 추가할 수 있기 때문이다.

 

변수와 메서드

 

* 선언 위치에 따른 변수의 종류

변수는 클래스 변수, 인스턴스 변수, 지역변수 모두 세 종류가 있다.

변수의 종류를 결정짓는 중요한 요소는 '변수의 선언된 위치'이므로 변수의 종류를 파악하기 위해서는 변수가 어느 영역에 선언되었는지를 확인하는 것이 중요하다.

멤버 변수를 제외한 나머지 변수들은 모두 지역변수이며, 멤버 변수 중 static이 붙은 것은 클래스 변수, 붙지 않은 것은 인스턴스 변수이다.

class Variables{
	int iv; //인스턴스 변수
    static int cv; //클래스 변수 (static 변수, 공유 변수)
    
    void method(){
    	int lv = 0; //지역 변수
    }
}

위 코드에서는 모두 3개의 int 형 변수가 선언되었다.

iv와 cv는 클래스 영역에 선언되어 있으므로 멤버 변수이다. 그 중 cv는 키워드 static과 함께 선언되어 있으므로 클래스 변수이며, iv는 인스턴스 변수이다.

lv는 메서드인 method()내부, 즉 '메서드 영역'에 선언되어 있으므로 지역변수이다.

변수의 종류 선언 위치 생성 시기
클래스 변수
(class variable)
클래스 영역 클래스가 메모리에 올라갈 때
인스턴스 변수
(instance variable)
인스턴스가 생성되었을 때
지역 변수
(local variable)
클래스 영역 이외의 영역
(메서드, 생성자, 초기화 블럭 내부)
변수 선언문이 수행되었을 때

 

1. 인스턴스 변수(instance variable)

클래스 영역에 선언되며, 클래스의 인스턴스를 생성할 때 만들어진다.

그러므로 인스턴스 변수의 값을 읽어 오거나 저장하기 위해서는 먼저 인스턴스를 생성해야한다.

인스턴스는 독립적인 저장공간을 가지므로 서로 다른 값을 가질 수 있다.

인스턴스마다 고유한 상태를 유지해야하는 속성의 경우, 인스턴스 변수로 선언한다.

 

2. 클래스 변수(class variable)

클래스 변수는 모든 인스턴스가 공통된 저장공간(변수)을 공유하게 된다.

한 클래스의 모든 인스턴스들이 공통적인 값을 유지해야하는 속성의 경우, 클래스 변수로 선언해야 한다.

클래스 변수는 인스턴스변수와 달리 인스턴스를 생성하지 않고도 언제라도 바로 사용할 수 있다는 특징이 있으며,

'클래스이름.클래스변수'와 같은 형식으로 사용한다.

Vriable 클래스의 클래스 변수 cv를 사용하려면 'Variable.cv'와 같이 사용하면 된다.

클래스가 메모리에 '로딩(loading)'될 때 생성되어 프로그램이 종료될 때까지 유지되며, public을 앞에 붙이면 같은 프로그램내에서 어디서나 접근할 수 있는 전역변수(global variable)의 성격을 갖는다.

 

3. 지역변수(local variable)

메서드 내에 선언되며 매서드 내에서만 사용 가능하다. 메서드가 종료되면 소멸되어 사용할 수 없다.

for문 또는 while문의 블럭 내에 선언된 지역변수는, 지역변수가 선언된 블럭{}내에서만 사용 가능하며,

블럭{}을 벗어나면 소멸되어 사용할 수 없게 된다.

 

* 클래스 변수와 인스턴스 변수의 차이

클래스 변수와 인스턴스 변수의 차이를 이해하기 위해 게임 카드를 클래스로 나타낸다고 하자.

카드의 폭과 높이는 모두 같을 것이나, 무늬와 숫자는 각 카드마다 상이할 것이다.

이때 카드와 폭은 클래스 변수로 선언하고, 무늬와 숫자는 인스턴스 변수로 선언해야한다.

class Card{
	String kind; //무늬
    in number; //숫자
    
    static int width = 100; //폭
    static int height = 250; // 높이
}

 

class CardTest{
	System.out.println(Card.width); //클래스 변수는 객체 생성없이 '클래스이름.클래스변수'로 사용가능
    System.out.println(Card.height);
    
	Card c1 = new Card(); //인스턴스 생성
    c1.kind = "heart"; //인스턴스 변수의 값을 변경한다.
    c2.number = 7; 
    
    Card c2 = new Card();//인스턴스 생성
    c2.kind = "spade"; //인스턴스 변수의 값을 변경한다.
    c2.number = 4; 
    
    //클래스 변수의 값을 변경한다.
    c1.width = 50;
    c1.height = 80
}

 

Card의 인스턴스인 c1과 c2는 클래스변수를 공유하기 때문에, c1의 width와 height를 변경하면 c2의 width와 height의 값도 바뀐 것과 같은 결과를 얻는다. Card.width, c1.width, c2.width는 모두 같은 저장공간을 참조하므로 항상 같은 값을 갖게 된다.

클래스변수를 사용할 때는 Card.width와 같이 '클래스이름.클래스변수'의 형태로 하는 것이 좋다. 참조변수 c1, c2를 사용할 수 있지만 이렇게 하면 클래스 변수를 인스턴스변수로 오해하기 쉽기 때문이다.

 

* 메서드

메서드(method)는 특정 작업을 수행하는 일련의 문장들을 하나로 묶은 것이다.

어떤 값을 입력하면 이 값으로 작업을 수행해서 결과를 반환한다.

그저 메서드가 작업을 수행하는데 필요한 값만 넣고 원하는 결과를 얻으면 될 뿐, 이 메서드가 내부적으로 어떤 과정을 거쳐 결과를 만들어내는지 전혀 몰라도 된다. 그래서 메서드를 내부가 보이지 않는 '블랙 박스'라고도 한다.

 

*메서드를 사용하는 이유

메서드의 장점은 다음과 같다.

 

1. 높은 재사용성(reusability)

이미 만들어놓은 메서드는 몇번이고 호출할 수 있으며, 다른 프로그램에서도 사용이 가능하다.

 

2. 중복된 코드 제거

반복되는 문장들을 묶어서 메서드를 작성해서 한 문장으로 호출할 수 있다.

그러면, 전체 소스 코드의 길이도 짧아지고 변경사항이 발생했을 때 수정해야할 코드의 양이 줄어들어 오류가 발생할 가능성도 함께 줄어든다.

 

3. 프로그램의 구조화

큰 규모의 프로그램에서는 문장들을 작업단위로 나눠서 여러 개의 메서드에 담아 프로그램의 구조를 단순화 시키는 것이 필수적이다.

public static void main(String args[]){
	int[] numArr = new int[10];
    
    initArr(numArr); //1.배열을 초기화
    printArr(numArr); //2.배열을 출력
    sortArr(numArr); //3.배열을 정렬
    printArr(numArr); //4.배열을 출력
}

 

이처럼 main메서드는 프로그램의 전체 흐름이 한눈에 들어올 정도로 단순하게 구조화하는 것이 좋다.

그래야 나중에 문제가 발생해도 해당 부분을 쉽게 찾아서 해결할 수 있다.

처음에 프로그램을 설계할 때 내용이 없는 메서드를 작업단위로 만들어 놓고, 하나씩 완성해가는 것도 프로그램을 구조화하는 좋은 방법이다.

 

*메서드의 선언과 구현

메서드는 크게 두 부분, '선언부(header, 머리)'와 '구현부(body, 몸통)'로 이루어져 있다.

메서드 선언부는 '메서드 이름', '매개변수 선언', 그리고 '반환타입'으로 구성되어 있으며,

메서드가 작업을 수행하기 위해 어떤 값들을 필요로 하고 작업 결과로 어떤 타입의 값을 반환하는지에 대한 정보를 제공한다.

메서드의 선언부는 후에 변경사항이 발생하지 않도록 신중히 작성해야 한다.

메서드의 선언부를 변경하게 되면, 그 메서드가 호출되는 모든 곳도 같이 변경해야 하기 때문이다.

 

메서드의 선언부 다음에 오는 괄호{}를 '메서드의 구현부'라고 하는데, 여기에 메서드를 호출했을 때 수행될 문장들을 넣는다.

 

메서드 내에 선언된 변수들은 그 메서드 내에서만 사용할 수 있으므로 서로 다른 메서드라면 같은 이름의 변수를 선언해도 된다.

이처럼 메서드 내에 선언된 변수를 '지역변수(local variable)'이라고 한다.

 

메서드를 정의했어도 호출되지 않으면 아무일도 일어나지 않는다.

메서드를 호출하는 방법은 다음과 같다.

메서드이름(값1, 값2, ...); //메서드를 호출하는 방법

int result = add(3, 5); //int add(int x, int y)를 호출하고, 결과를 result에 저장

 

메서드를 호출할 때 괄호()안에 지정해준 값들을 '인자(argument)'또는 '인수'라고 하는데, 

인자의 개수와 순서는 호출된 메서드에 선언된 매개변수와 일치해야한다.

 

* 메서드의 실행 흐름

같은 클래스 내의 메서드끼리는 참조변수를 사용하지 않고도 서로 호출이 가능하지만 static 메서드는 같은 클래스 내의 인스턴스 메서드를 호출할 수 없다.

class MyMath{
	long add(long a, long b){ return a + b;}
    long subtract(long a, long b){return a - b;}
    long multiply(long a, long b){return a * b;}
    double divide(double a, double b){return a / b;}
}
MyMath mm = new MyMath(); //먼저 인스턴스를 생성한다.
long value = mm.add(1L, 2L); //메서드를 호출한다.

 

메서드가 호출되면 지금까지 실행 중이던 메서드는 실행을 잠시 멈추고 호출된 메서드의 문장들이 실행된다.

호출된 메서드의 작업이 모두 끝나면, 다시 호출한 메서드로 돌아와 이후의 문장들을 실행한다.

 

* 매개변수의 유효성 검사

메서드의 구현부{}를 작성할 때, 제일 먼저 해야할 일은 매개 변수의 값이 적절한지 확인하는 것이다.

'호출하는 쪽에서 알아서 적절한 값을 넘겨주겠지.'라는 생각을 절대로 가져서는 안된다.

타입만 맞으면 어떤 값도 매개변수를 통해 넘어올 수 있기 때문에, 가능한 모든 경우의 수에 대해 고민하고 그에 대비한 코드를 작성해야 한다.

다음 코드와 같이 매개변수의 유효성을 체크해야한다.

float divide(int x, int y){
	//작업을 하기 전에 나누는 수(y)가 0인지 확인한다.
    if(y==0){
    	System.out.println("0으로 나눌 수 없습니다.");
        return 0; //매개변수가 유효하지 않으므로 메서드를 종료한다.
    }
    return x / (float)y;
}

 

JVM의 메모리 구조

응용프로그램이 실행되면, JVM은 시스템으로부터 프로그램을 수행하는데 필요한 메모리를 할당받고 JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.

그 중 3가지 주요 영역(method area, call stack, heap)에 대해 알아보자.

 

1. 메서드 영역 (Method Area)

- 프로그램 실행 중 어떤 클래스가 사용되면, JVM은 해당 클래스의 클래스파일(*.class)을 읽어서 분석하여 클래스에 대한 정보(클래스 데이터)를 이곳에 저장한다. 이 때, 그 클래스의 클래스변수(class variable)도 이 영역에 함께 생성된다.

 

2. 힙(heap)

- 인스턴스가 생성되는 공간, 프로그램 실행 중 생성되는 인스턴스는 모두 이곳에 생성된다.

즉, 인스턴스변수(instance variable)도 이 영역에 함께 생성된다.

 

3. 호출 스택 (call stack 또는 execution task)

- 호출 스택은 메서드의 작업에 필요한 메모리 공간을 제공한다.

메서드가 호출되면, 호출스택에 호출된 메서드를 위한 메모리가 할당되며, 이 메모리는 메서드가 작업을 수행하는 동안 지역변수(매개변수 포함)들과 연산의 중간결과 등을 저장하는데 사용된다. 그리고 메서드가 작업을 마치면 할당되었던 메모리공간은 반환되어 비워진다.

 

메서드가 호출되면 수행에 필요한 만큼의 메모리를 스택에 할당받는다.

메서드가 수행을 마치고 나면 사용했던 메모리를 반환하고 스택에서 제거된다.

호출 스택의 제일 위에 있는 메서드가 현재 실행 중인 메서드이다.

아래에 있는 메서드가 바로 위의 메서드를 호출한 메서드이다.

 

+ 객체를 생성하지 않고도 메서드를 호출할 수 있으려면, 메서드 앞에 'static'을 붙여야 한다.

 

기본형 매개변수 & 참조형 매개변수

자바에서는 메서드를 호출할 때 매개변수로 지정한 값을 메서드의 매개변수에 복사해서 넘겨준다.

매개변수 타입이 기본형(primitive type)일 때는 기본형 값이 복사 되겠지만, 참조형(reference type)이면 인스턴스의 주소가 복사된다.

메서드의 매개변수를 기본형으로 선언하면 단순히 저장된 값만 얻지만, 참조형으로 선언하면 값이 저장된 곳의 주소를 알 수 있기 때문에 값을 읽어오는 것은 물로 값을 변경하는 것도 가능하다.

  • 기본형 매개변수 : 변수의 값을 읽기만 할 수 있다. (read only)
  • 참조형 매개변수 : 변수의 값을 읽고 변경할 수 있다. (read & write)

다음과 같이 참조형 매개변수를 활용하면 반환값이 없어도 메서드의 실행결과를 얻어 올 수 있다.

또 참조형 매개변수를 사용하면 여러 개의 값을 반환받는 것과 같은 효과를 얻을 수도 있다.

void add(int a, int b, int[] result){
	result[0] = a + b;
}

 

참조형 반환 타입

반환타입도 참조형이 될 수 있다.

반환타입이 참조형이라는 것은 반환하는 값의 타입이 참조형이라는 얘긴데, 모든 참조형 타입의 값은 '객체의 주소'이므로 그저 정수값이 반환되는 것일 뿐 특별할 것이 없다.

static Data copy(Data d){
	Data tmp = new Data(); //새로운 객체 tmp를 생성한다.
    tmp.x = d.x; //d.x의 값을 tmp.x에 복사한다.
    return tmp; //복사한 객체의 주소를 반환한다.
}

 

이 메서드의 반환타입이 'Data'이므로, 호출결과를 저장하는 변수의 타입 역시 'Data'타입의 참조변수이어야 한다.

Data d2 = copy(d);

 

재귀 호출(recursive call)

메서드의 내부에서 메서드 자신을 다시 호출하는 것을 '재귀호출(recursive call)'이라 하고, 재귀호출을 하는 메서드를 '재귀 메서드'라 한다.

void method(){
	method(); //재귀호출. 메서드 자신을 호출한다.
}

 

메서드 입장에서는 자기 자신을 호출하는 것과 다른 메서드를 호출하는 것은 차이가 없다.

'메서드 호출'이라는 것이 그저 특정 위치에 저장되어 있는 명령들을 수행하는 것일 뿐이기 때문이다.

호출된 메서드는 '값에 의한 호출(call by value)'을 통해, 원래의 값이 아닌 복사된 값으로 작업하기 때문에 호출한 메서드와 관계없이 독립적인 작업수행이 가능하다. 

그런데 위의 코드처럼 오로지 재귀호출뿐이면, 무한히 자기 자신을 호출하기 때문에 무한반복에 빠지게 된다.

무한반복문이 조건문과 함께 사용되어야 하는 것처럼, 재귀 호출도 조건문이 필수적으로 따라 다닌다.

void method(int n){
	if(n==0)
    	return 0; //n의 값이 0일 때, 메서드를 종료한다.
    System.out.println(n); 
    method(--n); //재귀호출. method(int n)을 호출
}

 

재귀함수가 무한히 호출되면, 결국 스택의 저장한계를 넘게 되어 '스택오버플로우 에러(Stack Overflow Error)'가 발생한다.

 

반복문은 그저 같은 문장을 반복해서 수행하는 것이지만, 메서드를 호출하는 것은 반복문보다 몇가지 과정, 예를 들면 매개변수 복사와 종료 후 복귀할 주소저장 등, 이 추가로 필요하기 때문에 반복문보다 재귀호출의 수행시간이 더 오래 걸린다.

 

그렇다면 왜 굳이 반복문 대신 재귀호출을 사용할까?

그 이유는 재귀호출이 주는 논리적 간결함 때문이다.

몇겹의 반복문과 조건문으로 복잡하게 작성된 코드가 재귀호출로 작성하면 보다 단순한 구조로 바뀔 수 있다.

아무리 효율적이라도 알아보기 힘들게 작성하는 것보다 다소 비효율적이라도 알아보기 쉽게 작성하는 것이 논리적 오류가 발생할 확률도 줄어들고 나중에 수정하기도 좋다.

 

클래스 메서드(static메서드)& 인스턴스 메서드

변수처럼, 메서드 앞에 static이 붙어 있으면 클래스 메서드이고 붙어있지 않으면 인스턴스 메서드이다.

클래스 메서드도 클래스 변수처럼, 객체를 생성하지 않고도 '클래스이름.메서드이름(매개변수)'와 같은 형식으로 호출할 수 있다.

반면 인스턴스 메서드는 반드시 객체를 생성해야만 호출할 수 있다.

그렇다면 클래스를 정의할 때, 어느 경우에 static을 사용해서 클래스 메서드로 정의해야 하는 것일까?

클래스는 '데이터(변수)와 데이터에 관련된 메서드의 집합'이므로, 같은 클래스 내에 있는 메서드와 맴버변수는 아주 밀접한 관계가 있다.

인스턴스 메서드는 인스턴스 변수와 관련된 작업을 하는, 즉 메서드의 작업을 수행하는데 인스턴스 변수를 필요로 하는 메서드이다.

인스턴스 변수는 인스턴스(객체)를 생성해야만 만들어지므로 인스턴스 메서드 역시 인스턴스를 생성해야만 호출할 수 있다.

반면 메서드 중에서 인스턴스와 관련없는(인스턴스 변수나 인스턴스 메서드를 사용하지 않는) 메서드를 클래스 메서드(static 메서드)로 정의한다.

 

**static과 instace 멤버 최종 정리

 

1. 클래스를 설계할 때, 멤버 변수 중 모든 인스턴스에 공통으로 사용하는 것에 static을 붙인다.

 

2. 클래스 변수(static변수)는 인스턴스를 생성하지 않아도 사용할 수 있다.

 

3. 클래스 메서드(static 메서드)는 인스턴스 변수를 사용할 수 없다.

 

4. 메서드 내에서 인스턴스 변수를 사용하지 않는다면, static을 붙이는 것을 고려한다.

 

- 클래스의 멤버변수 중 모든 인스턴스에 공통된 값을 유지해야하는 것이 있으면 static을 붙여준다

- 작성한 메서드 중에서 인스턴스 변수나 인스턴스 메서드를 사용하지 않는 메서드에 static을 붙일 것을 고려한다.

class MyMath2{
	long a, b;
    
    //인스턴스 변수 a, b를 이용해서 작업하므로 매개변수가 필요없다.
    long add(){return a + b} //a, b는 인스턴스 변수
    
    //인스턴스 변수와 관계없이 매개변수만으로 작업이 가능하다.
    static long add(long a, long b){return a + b;} //a, b는 지역변수
}

 

클래스 멤버와 인스턴스 멤버간의 참조와 호출

같은 클래스에 속한 멤버들 간에는 별도의 인스턴스를 생성하지 않고도 서로 참조 또는 호출이 가능하다.

단, 클래스 멤버가 인스턴스 멤버를 참조 또는 호출하고자 하는 경우에는 인스턴스를 생성해야 한다.

그 이유는 인스턴스 멤버가 존재하는 시점에 클래스 멤버는 항상 존재하지만, 클래스 멤버가 존재하는 시점에 인스턴스 멤버가 존재하지 않을 수도 있기 때문이다.

class TestClass{
	void instanceMethod(){} //인스턴스 메서드
    static void staticMethod(){} //static 메서드
    
    void instanceMethod2(){ //인스턴스메서드
    	instanceMethod(); //다른 인스턴스 메서드 호출
        staticMethod(); //static 메서드 호출
    }
    static void staticMethod2(){//static메서드
    	instanceMethod(); //에러!! 인스턴스 메서드를 호출할 수 없다.
        staticMethod(); //static 메서드는 호출할 수 있다
    }
}

 

인스턴스 멤버간의 호출에는 아무런 문제가 없다.

하나의 인스턴스 멤버가 존재한다는 것은 인스턴스가 이미 생성되어 있다는 것을 의미하며, 즉 다른 인스턴스 멤버들도 모두 존재하기 때문이다.

 

*reference

- 이 글은 독자적으로 작성한 글이 아닌 <자바의 정석 3rd Edition>(저자 남궁성)책의 내용을 참고 및 요약한 글입니다.

- 이 글은 온전히 학습 목적으로 작성되었습니다.