Java :: 객체지향 프로그래밍

Juwon Lee·2022년 7월 21일
0
post-thumbnail

남궁성 저자의 Java의 정석 3판 스터디

객체지향에 대해 놓치고 있는 부분을 다시 짚어보자.

객체지향이론의 기본 개념은 “실제 세계는 사물(객체)로 이루어져 있으며,

발생하는 모든 사건들은 사물간의 상호작용이다.” 라는 것이다.

객체지향언어의 주요 특징은 아래와 같다.

1. 코드의 재사용성이 높다.
2. 코드의 관리가 용이하다.
3. 신뢰성이 높은 프로그래밍을 가능하게 한다.

그 중 객체지향언어의 가장 큰 장점은 “코드의 재사용성이 높고 유지보수가 용이” 하다는 것 !

객체지향개념을 학습할 때는 재사용성유지보수 그리고 중복된 코드의 제거

이 세 가지 관점에서 보면 보다 쉽게 이해할 수 있을 것이다.

클래스와 객체


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

객체는 실제로 존재하는 것 이다.

주변에서 쉽게 볼 수 있는 책상, 의자와 같은 사물들이 곧 객체이고,

사물과 같은 유형적인 것 뿐만 아니라 개념이나 논리 같은 무형적인 것들도 객체로 간주한다.

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

일상생활에서 예를 들면, TV 설계도는 클래스이고 TV 라는 제품은 객체가 되는 것이다.

인스턴스


클래스로부터 객체를 만드는 과정을 클래스의 인스턴스화(instantiate) 라고 하고,

클래스로부터 만들어진 객체를 그 클래스의 인스턴스(instance) 라고 한다.

객체의 구성요소 - 속성과 기능


객체는 속성과 기능 두 종류의 구성요소로 이루어져 있다.

클래스는 객체의 설계도이므로, 클래스에는 객체의 모든 속성과 기능이 정의되어 있다.

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

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

위에서 예를 들었던 TV 의 클래스를 간단하게 만들어보자.

class Tv {
	String color;
	boolean power;
	int channel;

	void power() { power = !power; }
	void channelUp() { channel++; }
	void channelDown() { channel--; }
}

인스턴스의 생성과 사용


위에서 만든 것은 클래스(설계도)이므로, 인스턴스를 생성해야 제품을 사용할 수 있다.

클래스로부터 인스턴스를 생성하는 것은 일반적으로 아래와 같이 한다.

클래스명 변수명;
변수명 = new 클래스명();
// 실제 Java 코드
Tv tv;
tv = new Tv();
tv.channel = 7; // 채널을 7로 설정
tv.channelDown(); // 채널을 하나 내림

위에서 tv 라는 인스턴스를 생성했다.

주의해야 할 점은 TV tv; 부분은 단순히 참조변수를 만든 것이고,

실제 객체는 new 키워드를 이용해 생성한다.

tv = new Tv(); 코드를 통해 실제 인스턴스를 생성하고, 인스턴스의 주소값을 참조변수에 저장한다.

인스턴스의 속성을 사용하려면 참조변수.멤버변수 와 같이 하면 되고,

인스턴스의 메서드를 호출하려면 참조변수.메서드 를 호출하면 된다.

참조변수와 실제 인스턴스의 관계를 이해했다면 아래 코드를 보자.

Tv tv1 = new Tv();
Tv tv2 = new Tv();

tv2 = tv1;

두 개의 Tv 인스턴스를 만든 후, tv1 에 저장되어 있던 값을 tv2 에 저장한다.

그러면 tv2 변수 역시 tv1 의 인스턴스를 참조하게 되고,

tv2가 원래 참조하고 있던 인스턴스는 더 이상 사용할 수 없게 된다.

참조변수가 하나도 없는 인스턴스는 더 이상 사용되어질 수 없으므로

Garbage Collector 에 의해 자동으로 메모리에서 제거된다.

참고로 객체 역시 배열로 다룰 수 있다.

Tv[] tvArr = new Tv[3];

여기서도 주의해야 할 점은, 객체 배열은 생성했지만 실제 객체는 생성하지 않았다는 것이다.

참조변수 배열만 만든 것이므로 각각의 객체를 new 로 생성해줘야 한다.

Tv[] tvArr = { new Tv(), new Tv(), new Tv() };
// 또는 반복문을 이용해 아래처럼 생성하기도 가능
for(int i = 0; i < tvArr.length; i++) {
	tvArr[i] = new Tv();
}

여기서 클래스의 정의와 의미를 조금 더 살펴보자.

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

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

데이터는 데이터끼리, 함수는 함수끼리 따로 다루어졌었지만,

객체지향 언어에서는 데이터와 함수를 하나의 클래스에 정의해서

관계가 깊은 변수와 함수들을 함께 다룰 수 있게 했다.

예를 들어 Java 의 String 클래스를 살펴보자.

public final class String implements java.io.Serializable, Comparable {
	private char[] value; // 문자열을 저장하기 위한 공간
	
	public String replace(char oldChar, char newChar) {
		...
		char[] val = value; // 같은 클래스 내의 변수를 사용해서 작업
		...
	}
	...
}

위 코드는 실제 String 클래스의 실제 소스 일부분이다.

클래스 내부에 value 라는 char 형 배열이 있고,

문자열을 다루는 데 필요한 함수들을 함께 정의해 놓았다.

이렇게 하면 변수와 함수가 서로 연결되어서 작업이 명확해진다.

변수와 메소드


멤버변수에는 세 가지 종류가 있다.

1. 클래스변수
2. 인스턴스변수
3. 지역변수

멤버변수를 제외한 나머지 변수들은 모두 지역변수이고,

멤버변수 중 static 이 붙은 것은 클래스 변수,

붙지 않은 것은 인스턴스 변수이다.

아래 예시를 보자.

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

이를 정리하면 아래와 같다.

  1. 클래스 변수

    인스턴스변수 앞에 static 을 붙이면 된다.

    인스턴스마다 독립적인 저장공간을 가지는 인스턴스 변수와는 달리,

    클래스 변수는 모든 인스턴스가 공통된 변수를 공유하게 된다.

    클래스 변수는 인스턴스를 생성하지 않고도 바로 사용할 수 있다.

    클래스가 메모리에 loading 될 때 생성되어 프로그램이 종료될 때까지 유지된다.

    public 키워드를 추가하면 프로그램 내 어디서나 접근할 수 있는 전역변수의 성격을 가진다.

  2. 인스턴스 변수

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

    인스턴스 변수의 값을 읽어오거나 저장하려면 먼저 인스턴스를 생성해야 한다.

  3. 지역 변수

    메소드 내에 선언되어 메소드 내에서만 사용가능하며,

    메소드가 종료되면 소멸된다.

메서드는 수행할 특정 작업들을 하나로 묶은 것이다.

메서드에 넣을 입력값과 반환하는 출력값만 알면 되고, 내부의 과정은 몰라도 된다.

그래서 메서드를 내부가 보이지 않는 Black Box 라고도 한다.

변수에서 그랬던 것 같이, 메서드 앞에 static 이 붙어있으면 클래스 메서드이고

붙어있지 않으면 인스턴스 메서드이다.

인스턴스 메서드는 인스턴스 변수와 관련된 작업을 하는,

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

반면에 인스턴스와 관계없는 (인스턴스 변수나 인스턴스 메서드를 사용하지 않는) 메서드를

클래스 메서드(static 메서드)로 정의한다.

클래스 메서드를 사용할 때 고려해볼만 한 점들을 나열해보았다.

  • static 메서드는 인스턴스 변수를 사용할 수 없다. 인스턴스 변수는 인스턴스가 반드시 존재해야만 사용할 수 있는데, 클래스 메서드는 인스턴스 생성 없이 호출이 가능하므로 클래스 메서드가 호출되었을 때 인스턴스가 존재하지 않을 수도 있다. 그래서 클래스 메서드에서 인스턴스 변수의 사용 금지 !
  • 메서드 내에서 인스턴스 변수를 사용하지 않는다면, static 을 붙이는 건 어떨까 ? 메서드의 작업 내용 중 인스턴스 변수를 필요로 하면 static 을 붙일 수 없다. 반대로 인스턴스 변수가 필요하지 않다면 static 을 붙일 수 있다. static 이 아닌 인스턴스 메서드는 실행 시 호출되어야 할 메서드를 찾는 과정이 추가적으로 필요하기 때문에 시간이 더 걸리고, static 메서드는 호출 시간이 짧아지기 때문에 성능이 향상된다.

이번엔 클래스 멤버와 인스턴스 멤버간의 참조와 호출을 살펴보자.

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

클래스 멤버가 인스턴스 멤버를 참조하거나 호출하는 경우에는 꼭 인스턴스를 생성해야 한다.

그 이유는 인스턴스 멤버가 존재하는 시점에 클래스 멤버는 항상 존재하지만,

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

class TestClass {
	void instanceMethod() {}
	static void staticMethod() {}

	void instanceMethod2() {
		instanceMethod();
		staticMethod();
	}

	static void staticMethod2() {
		instanceMethod(); // 여기서 에러 !
		staticMethod();
	}
}

위 코드를 실행해보면, static 메서드에서 instance 메서드를 호출했을 때 에러가 나는 것을 확인할 수 있다.

이 상황은 변수와 메서드 간의 호출에서도 동일하게 나타난다.

오버로딩


자바에서는 한 클래스 안에서 이미 사용하려는 이름과

같은 이름을 가진 메서드가 있더라도 매개변수의 개수나 타입이 다르면,

같은 이름을 사용해서 메서드를 정의할 수 있다.

한 클래스 안에서 같은 이름의 메서드를 여러 개 정의하는 것을

오버로딩 (overloading) 이라고 한다.

  • 메서드 이름이 같아야 함
  • 매개변수의 개수 또는 타입이 달라야 함

아래는 오버로딩일 때와 아닐 때를 구분한 예이다.

int add(int a, int b) {}
int add(int x, int y) {}

위의 두 메서드는 매개변수의 이름만 다를 뿐 매개변수의 타입이 같기 때문에

오버로딩이 성립하지 않고 add(int, int) is already defined

라는 메시지가 나타난다.

long add(int a, long b) {}
long add(long a, int b) {}

두 메서드 모두 int, long 매개변수가 하나씩 선언되어 있지만

서로 순서가 다른 경우이다.

호출 시 매개변수의 값에 의해 호출될 메서드가 구분될 수 있으므로

오버로딩으로 간주한다.

int add(int a, int b) {}
long add(long, long b) {}
long add(int[] a) {}

위 메서드들은 모두 바르게 오버로딩 되어 있다.

같은 일을 하지만 매개변수를 다르게 해야 하는 경우,

위와 같이 이름은 같고 매개변수를 다르게 해서 오버로딩을 구현한다.

가변인자(varargs)


기존에는 매개변수 개수가 고정적이었지만

JDK1.5부터 동적으로 지정해줄 수 있게 되었다.

가변인자는 타입... 변수명 형식으로 선언하고,

printStream 클래스의 printf() 가 대표적인 예이다.

public PrintStream printf(String format, Object... args) {...}

가변인자 외에 또 매개변수가 있으면, 가변인자를 매개변수 중에서

제일 마지막에 선언해야 한다. 그렇지 않으면 컴파일 에러가 발생 !

// 컴파일 에러 ! 가변인자는 항상 마지막 매개변수로
public PrintStream printf(Object... args, String format) {...}

예를 들어 여러 문자열을 하나로 결합해서 반환하는 메서드를 작성한다고 하자.

원래라면 아래와 같이 매개변수의 개수를 다르게 해서

오버로딩 해야 한다.

String concatenate(String s1, String s2) {...}
String concatenate(String s1, String s2, String s3) {...}
String concatenate(String s1, String s2, String s3, String s4) {...}

이럴 때, 가변인자를 사용하면 메서드 하나로 대체할 수 있다.

String concatenate(String... str) {...}

가변인자는 내부적으로 배열을 생성한다.

가변인자는 편리하지만, 메서드를 호출할 때마다 배열을 새로 생성하는

비효율이 숨어있기 때문에 꼭 필요한 경우에만 가변인자를 사용하는 것을 추천한다.

생성자


생성자는 인스턴스가 생성될 때 호출되는 인스턴스 초기화 메서드 이다.

  • 생성자의 이름은 클래스의 이름과 같아야 한다.
  • 생성자는 리턴 값이 없다.
class Card {
	Card() {
		... // 매개변수가 없는 생성자
	}

	Card(String k, int num) {
		... // 매개변수가 있는 생성자, 오버로딩
	}
}

클래스에 생성자를 정의하지 않아도

컴파일러가 기본 생성자 (default constructor) 를 추가해서 컴파일 한다.

Card() {}

위와 같이 매개변수와 내용이 아무것도 없는 간단한 생성자를 기본으로 추가해주는데,

다른 생성자를 정의하는 경우 기본 생성자는 추가되지 않는다.

즉, 기본 생성자가 컴파일러에 의해서 추가되는 경우는

클래스에 정의된 생성자가 하나도 없을 때 뿐이다.

생성자에서 다른 생성자를 호출할 수도 있다.

단 조건이 있다.

  • 생성자의 이름으로 클래스 이름 대신 this 를 사용
  • 한 생성자에서 다른 생성자를 호출할 때는 반드시 첫 줄에서만 호출 가능

예를 들어 아래 코드를 보자

Car(String color) {
	door = 5;
	Car(color, "auto", 4); // 에러: 생성자의 두 번째 출에서 다른 생성자 호출
	// this(color, "auto", 4); 로 해야 한다
}

생성자 내에서 초기화 작업 도중에 다른 생성자를 호출하면,

호출된 다른 생성자에서도 멤버변수들의 값을 초기화하기 때문에

다른 생성자를 호출하기 이전 초기화 작업이 무의미해질 수 있기 때문에

생성자 안에서 다른 생성자는 첫 줄에서만 호출이 가능하다.

멤버변수를 초기화 할 때 생성자가 아닌 초기화 블럭을 사용하는 방법도 있다.

초기화 블럭은 클래스 초기화 블럭과 인스턴스 초기화 블럭으로 나뉘는데,

클래스 초기화 블럭은 클래스가 메모리에 처음 로딩될 때 한번,

인스턴스 초기화 블럭은 생성자처럼 인스턴스를 생성할 때마다 수행된다.

아래 예를 보자

Car() {
	count++; //중복
	serialNo = count; //중복
	color = "white";
	gearType = "Auto";
}

Car(String color, String gearType) {
	count++; //중복
	serialNo = count; //중복
	this.color = color;
	this.gearType = gearType;
}

위 코드를 보면 모든 생성자에 공통으로 수행해야 하는 문장이 있다.

이런 경우 인스턴스 블럭에 넣어주면 코드가 더 간결해진다.

{
	count++;
	serialNo = count;
}	

Car() {
	color = "white";
	gearType = "Auto";
}

Car(String color, String gearType) {
	this.color = color;
	this.gearType = gearType;
}

초기화하는 방법 별 초기화시점을 정리해보자

기본값 -> 명시적 초기화 -> 초기화 블럭 -> 생성자

class InitTest {
	static int cv = 1;
	int iv = 1;
	
	static { cv = 2; }
	{ iv = 2; }

	InitTest() { iv = 3; }
}

위 코드를 보고 순서를 그려볼 수 있어야 한다.

클래스 초기화의 경우,

static int cv = 1static 초기화 블럭 의 순서로 진행된다.

인스턴스 초기화의 경우,

int iv = 1인스턴스 초기화 블럭생성자 순서로 진행된다.

profile
✏️ 하루의 기록

0개의 댓글