본문 바로가기
개념서/Java

[Java] 객체지향 프로그래밍 I

by 사서T 2022. 6. 6.

객체지향언어의 등장배경

   객체지향이론의 기본 개념은 '실제 세계는 사물(객체)로 이루어져 있으며, 발생하는 모든 사건들은 사물간의 상호작용이다.'라는 것이다. 실제 세계를 모방한 가상 세계에서 모의실험을 함으로써 많은 시간과 비용을 절약할 수 있다는 점과 프로그램의 규모가 점점 커지고 사용자들의 요구가 빠르게 변화해가는 상황을 절차적 언어로는 극복하기 어렵다는 한계로 객체지향언어가 주류로 자리 잡았다.


객체지향언어란?

   객체지향언어는 기존 관점에서 벗어나 객체들의 모임으로 파악하고, 객체 간에 상호작용이 발생한다는 개념이다. 객체지향언어는 기존의 프로그래밍 언어에 몇 가지 새로운 규칙을 추가한 보다 발전된 형태의 것이다. 추가된 규칙으로 코드 간에 서로 관계를 맺어 줌으로써 보다 유기적으로 프로그램을 구성하는 것이 가능해졌다. 객체지향언어의 장점은 다음과 같다.

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

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

 

   객체지향언어에는 4대 특성이 있는데 이후 내용에서 자세히 다룰 예정이기 때문에 여기선 4대 특성에 어떤 것들이 있는 지 명시만 하였다. 이 4대 특성이 무엇인지 이해하고 왜 중요한 지를 파악한다면 객체지향언어를 제대로 이해했다고 할 수 있다.

  • 캡슐화
  • 상속
  • 추상화
  • 다형성

클래스와 객체, 인스턴스

   클래스분류에 대한 개념이며 생성할 사물에 대한 정보를 포함하고 있고, 객체클래스의 정보 기반으로 생성된 실체를 의미한다. 인스턴스어떤 클래스로부터 만들어진 객체이다. 객체와 인스턴스가 같다고 생각할 수 있지만, 객체는 인스턴스보다 포괄적인 개념이며 인스턴스는 특정 클래스로 좁혀져 좀 더 구체적인 개념이라고 이해하면 된다. 아래 분류도를 이용해 좀 더 자세히 알아보자.

 

 

   척추동물이라는 클래스를 구현했다면 클래스는 척추동물에 대한 정보와 행동을 정의하고 있다. 이 클래스를 이용해 객체를 생성하고 이 과정을 인스턴스화라고 한다. 생성된 객체를 척추동물 클래스의 인스턴스라고 한다. 만약 어류 클래스에서 만들어지면 어류 클래스의 인스터스이다.

 

class 포유류 {
    //포유류 클래스가 가진 정보
    private age;
    
    //포유류 클래스가 가진 행동
    public void run() {
        System.out.println("포유류는 새끼를 낳고 젖을 먹인다.");
    }
}

클래스명 변수명 = new 클래스명();
//클래스의 객체 생성

포유류 고래 = new 포유류();
//포유류 고래 : 포유류라는 클래스 타입을 이용해 선언
//new 포유류() : 포유류라는 실제 인스턴스를 생성(인스턴스화)

고래.run();
//"포유류는 새끼를 낳고 젖을 먹인다." 출력

 

   객체는 속성과 기능, 두 종류의 구성요소로 이루어져 있다. 속성은 멤버변수, 특성, 필드, 상태라고 하며 클래스에서 정의한 private age가 그 예이다. 기능은 메서드, 함수, 행위라고 하며 클래스에서 정의한 run()이 해당한다.

 

※ 클래스와 객체는 유형의 것뿐만 아니라 무형의 개념도 정의할 수 있다.

※ 프로그래밍 관점에서 보면 클래스란 구조체와 함수가 결합된 형태이다.

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

※ 같은 자료형의 많은 데이터를 다룰 때 배열을 이용하듯이, 객체도 배열로 다룰 수 있다. 이전 배열에는 값이 저장되었다면 객체 배열에는 객체의 주소가 저장된다.

※ 클래스와 객체에 대해 더 자세히 이해하고 싶다면 을 읽어보자.


변수

   변수의 선언된 위치에 따라 클래스 변수, 인스턴스 변수, 지역 변수로 나뉜다. 클래스 영역 이외에 위치한 변수를 지역 변수라고 하며 그 외의 변수를 멤버 변수라고 한다. 멤버 변수는 앞에 static이 붙으면 클래스 변수, 아니면 인스턴스 변수로 구분한다.

class tmp {
    static int a;	//클래스 변수
    int	b;		//인스턴스 변수
    
    void method() {
        int c;	//지역 변수
    }
}

1. 클래스 변수

  • 인스턴스 변수 앞에 static이 붙은 형태
  • 모든 인스턴스가 공유
  • 인스턴스를 생성하지 않고 바로 사용(접근) 가능
  • 클래스가 로딩될 때부터 프로그램이 종료될 때까지 유지
  • 클래스 변수에 대한 접근은 '클래스명.클래스 변수' 형태가 권장
  • 전역 변수의 성격의 가짐

2. 인스턴스 변수

  • 클래스의 인스턴스 생성 시 만들어짐
  • 인스턴스마다 독립된 저장 공간을 가짐
  • 사용하기 위해선 인스턴스 생성이 필수

3. 지역 변수

  • 주로 메서드 내에서 선언되며 메서드 종료시 같이 소멸
  • 블럭 {} 내에서 선언한 지역 변수는 해당 블럭 종료시 같이 소멸

메서드

   특정 작업을 수행하는 일련의 문장들을 하나로 묶은 것으로 return문을 이용해 결과를 반환하는 메서드와 반환하지 않는 메서드가 존재한다. 우선 메서드를 사용하는 이유에 대해 알아보자.

  • 높은 재사용성 : 미리 구현해놓은 Java API를 활용하듯이 필요한 메서드를 몇 번이고 호출할 수 있고, 다른 프로그램에서도 사용이 가능하다.
  • 중복된 코드의 제거 : 높은 재사용성에서도 설명했듯이 여러 부분에서 호출이 가능하다는 점은 메서드의 내용이 해당 부분에 필요하다는 것이다. 즉 동일한 내용의 코드를 메서드 단위로 호출하는 방식으로 코드 중복을 피할 수 있다.
  • 프로그램의 구조화 : 메서드 단위로 분할하면 메서드의 기능을 특정 지을 수 있다. 해당 메서드에 대한 수정이 필요한 경우, 메서드가 여러 부분에서 호출되어도 실제로 구현된 메서드의 내용만 수정하면 되기에 효율적인 유지보수가 가능하다.
//기본 형태
반환타입 메서드이름 (타입 변수명, 타입 변수명, ...) {
    //수행할 코드
}

//반환이 없는 경우
//인스턴스 메서드이기 때문에 인스턴스 생성 후 사용 가능
void method1(int a, int b) {
    System.out.println(a + " " + b);
    return ;	//반환타입이 void인 경우 return문이 없으면 컴파일러에서 알아서 추가
}

//반환이 있는 경우
//인스턴스 메서드이기 때문에 인스턴스 생성 후 사용 가능
int method2(int a, int b) {
    return a + b;
}

//클래스 메서드
//변수와 마찬가지로 인스턴스 생성없이도 사용이 가능하다. ex) Math.random()
static int method3(int a, int b) {
    return a + b;
}

//반환타입이 참조형인 경우
Car method4(String name) {
    Car car = new Car();
    car.name = name;
    return car;
}

※ 메서드의 각 매개변수의 타입은 생략이 불가능하다.

※ 반환타입은 일치하거나 적어도 자동 형변환이 가능해야 한다.

※ 반환타입이 void인 경우 메서드의 마지막에 return;이 자동적으로 추가된다.

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

 

   메서드에 인자를 넣어 호출하면 메서드의 블럭{}의 내용이 수행된다.

//메서드는 위의 예제를 사용
int result = 클래스명.method3(1, 2);

//result는 3

 

※ 메서드 작성 시에는 매개변수에 대한 유효성을 검증하는 코드를 작성하자.


JVM의 메모리 구조

   응용프로그램 실행 후, JVM은 시스템으로부터 프로그램을 수행하는데 필요한 메모리를 할당받고 용도에 따라 여러 영역으로 나누어 관리한다. 그 중 3가지 주요 영역이 method area, call stack, heap이다.

 

1. method area, static area, class area

   프로그램 실행 중 어떤 클래스가 사용되며, JVM은 해당 클래스의 클래스파일을 읽고 분석하여 클래스 데이터를 이 곳에 저장(로드)한다. 이 때, 클래스변수도 이 영역에 함께 생성된다. 클래스를 미리 로드해두지 않고 사용될 때 로드하는 이유는 메모리 사용의 효율성 때문이다. 아직 사용하지 않는 클래스 데이터를 미리 올려두는 것은 낭비이기 때문이다.

 

2. call stack

   메서드의 작업에 필요한 메모리 공간을 제공한다. 메서드가 호출되면, 호출스택에 호출된 메서드를 위한 메모리가 할당되며, 이 메모리는 메서드가 작업을 수행하는 동안 지역 변수(매개변수 포함)들과 연산의 중간 결과 등을 저장하는데 사용된다. 작업 종료후 할당받은 메모리를 반환한다.

아래에 있는 메서드는 바로 위의 메서드를 호출할 메서드이고, 맨 위의 메서드는 현재 실행 중인 메서드를 의미한다.

 

출처:https://velog.io/@jeong11/Java-OOP-callstack

 

3. heap

   인스턴스와 인스턴스변수들이 생성되는 공간이다. 인스턴스 생성 시 마다 인스턴스는 이 공간에 독립된 메모리를 할당 받는다.


오버로딩

   메서드도 변수와 마찬가지로 같은 클래스 내에서 서로 구별될 수 있어야 하기 때문에 각기 다른 이름을 가져야 한다. 하지만 자바에서는 이름이 같더라도 매개변수의 개수 또는 타입이 다르면, 같은 이름을 사용해서 메서드를 정의할 수 있다. 이처럼, 한 클래스 내에 같은 이름의 메서드를 여러 개 정의하는 것을 '메서드 오버로딩' 또는 '오버로딩'이라 한다.

 

<조건 정리>

  • 메서드의 이름이 같아야 한다.
  • 매개변수의 개수 또는 타입이 달라야 한다.
  • 반환 타입은 영향을 끼치지 않는다.
int add(int a, int b);	//기본 형태
int add(int x, int y);	//실패
int add(long a, int b);	//OK (매개변수의 타입이 다름)
long add(int a, int b);	//실패

 

   오버로딩이 가능해지면서 같은 기능을 가진 메서드 명을 통일할 수 있게 되었다. 이는 매개변수의 타입이나 개수에 따라 사용해야 하는 메서드의 명을 따로 기억할 필요 없이 하나의 메서드 명을 사용할 수 있다는 것이다.


가변인자

   가변인자란 매개변수의 개수를 동적으로 지정해주는 기능이다. '타입... 변수명'과 같은 형식으로 선언하며, 다른 매개변수가 존재하는 경우 가변인자는 매개 변수 중에서 제일 마지막에 선언해야 한다.

 

//기본 형태
public 반환타입 메서드명(타입... 변수명) { ... }

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

//가변인자의 활용
//변환 전
int add(int a, int b);
int add(int a, int b, int c);
int add(int a, int b, int c, int d);

//변환 후
int add(int... a);

 

※ 가변인자는 내부적으로 배열을 이용하여 메서드 호출시마다 배열이 새로 생성되기 때문에 비효율적일 수 있다.

 

<가변인자 사용시 주의 예제>

//이 상황의 경우 메서드를 호출할 때 구별이 불가능하기 때문에 에러 발생

public int add(int a, int... numbers) { ... }

public int add(int... numbers, int a) { ... }

 

※ 가능하면 가변인자를 사용한 메서드는 오버로딩을 피해야 한다.


생성자

   생성자는 인스턴스가 생성될 때 호출되는 인스턴스 초기화 메서드로 인스턴스의 변수들을 초기화하거나, 인스턴스 생성 시에 실행돼야 할 작업을 위해서 사용된다. 생성자 역시 클래스 내에 선언되며, 리턴값이 없고 void는 표시하지 않으며, 메서드와 유사한 구조를 같는다.

//기본 형태
class 클래스명 {
    클래스명(타입 변수명, 타입 변수명, ...) {
        //인스턴스 변수의 초기화 코드
        //인스턴스 생성 시 수행할 코드
    }
}

//예시
class Car {
    //기본 생성자
    Car() { ... }
    
    //매개변수가 있는 생성자
    Car(String name, int speed) { ... }
}

//기본 생성자를 이용한 Car 인스턴스 생성
Car car1 = new Car();

//매개변수 생성자를 이용한 Car 인스턴스 생성
Car car2 = new Car("sonata", 120);

//1. 연산자 new에 의해 heap 영역에 Car 클래스의 인스턴스가 생성된다.
//2. 생성자 Car(String name, int speed)가 호출되어 수행된다.
//3. 연산자 new의 결과로 생성된 Car 인스턴스의 주소가 반환되어 참조변수 car2에 저장된다.

new 연산자가 인스턴스를 생성하는 것이고, 생성자는 인스턴스 변수들을 초기화하는 역할을 수행한다.

클래스 내에 무조건 하나 이상의 생성자가 존재해야 하며, 정의된 생성자가 하나도 없는 경우는 컴파일러가 자동으로 기본 생성자를 추가한다. (생성자가 하나라도 있으면 컴파일러는 기본 생성자를 따로 추가하지 않는다.)

 

   생성자에서 다른 생성자를 호출하는 경우 this를 사용한다. this는 참조변수로 인스턴스 자신을 가리키기 때문에 인스턴스가 생성되지 않고 사용할 수 있는 static 메서드의 경우 사용이 불가능하다.

class Car {
    String name;
    int speed;
    
    Car() {
        //수정 전
        name = "test";		//실패 : 다른 생성자의 호출 이전의 초기화 작업은 무의미해질 수 있음
        Car("sonata", 120);	//실패 : 생성자에서 다른 생성를 호출하기위해선 this를 사용
        
        //수정 후
        this("sonata", 120);	//OK
    }
    ...
    
    Car(String name, int speed) {
        this.name = name;	//같은 변수명을 사용해도 구분이 가능해지며 직관적으로 관계 파악이 가능
        this.speed = speed;
    }
    
    public static void method1() {
        System.out.println(this.name);	//실패 : 인스턴스가 생성되지 않은 경우에도 접근 가능
    }
    
    public void method2() {
        System.out.println(this.name); //OK
    }
    
    //추가 매개변수로 인스턴스를 받아 복사하기
    //Object 클래스에 정의된 clone 메서드를 이용해서 간단히 복사할 수도 있다.
    Car(Car car) {
        this.name = car.name;
        this.speed = car.speed;
    }
}

초기화

   변수를 선언하고 처음으로 값을 저장하는 것을 '변수의 초기화'라고 한다. 멤버변수는 자동적으로 자료형에 맞는 기본값으로 초기화되지만, 지역변수는 사용하기 전에 반드시 초기화해야 한다.

class Test {
    int x;		//자동 초기화
    int y = x;	//y는 x로 초기화
    
    public void method() {
        int x1;		//선언만 된 경우
        int y1 = x1;	//실패 : x1이 초기화되지 않음
    }
}

   멤버변수의 초기화 방법은 3가지(명시적 초기화, 생성자, 초기화 블럭)가 있다. 생성자는 이미 앞에서 설명했기에 명시적 초기화와 초기화 블럭만 다루도록 한다.

 

1. 명시적 초기화

   변수 선언과 동시에 초기화하는 방법으로 가장 기본적이면서 간단하다.

class Car {
    String name = "sonata";	// 기본형 변수의 초기화
    int speed = 120;		// 기본형 변수의 초기화
    Engine e = new Engine();	// 참조형 변수의 초기화
}

2. 초기화 블럭

   좀 더 복잡한 초기화 방법이 필요할 때 사용한다. 초기화 블럭에는 '클래스 초기화 블럭'과 '인스턴스 초기화 블럭' 두 가지가 있다. 이름에서 알 수 있듯이 클래스 초기화 블럭은 클래스가 처음 로딩될 때 한 번 수행되고, 인스턴스 초기화 블럭은 인스턴스가 생성될 때마다 수행된다.

class 클래스명 {
    static { //클래스 로드 시 수행할 내용 }	//클래스 초기화 블럭
    
    { //인스턴스 생성 시 수행할 내용 }		//인스턴스 초기화 블럭
}

 

※ 인스턴스 변수의 초기화는 주로 생성자를 이용하고, 인스턴스 초기화 블록은 모든 생성자에서 공통으로 수행돼야 하는 코드를 넣는데 사용한다.

 

   다양한 초기화 방법을 확인했으니 초기화 순서를 정리해보자.

 


Q&A

Q. 객체지향언어란 무엇인가?

A. 기존 관점에서 벗어나 객체들의 모임으로 파악하고, 객체 간에 상호작용이 발생한다는 개념이다.

 

Q. 객체지향어언의 4대 특성은?

A. 캡슐화, 상속, 추상화, 다형성이 있다.

 

Q. 클래스와 객체, 인스턴스란?

A. 클래스는 분류에 대한 개념으로 생성할 객체의 정보를 정의하고, 객체는 임의의 클래스로부터 생성된 실체이며, 인스턴스는 특정 클래스로부터 생성된 객체를 의미한다.

 

Q. 클래스 변수, 인스턴스 변수, 지역 변수란 무엇인가?

A. 메서드 내에서 사용되고 메서드의 종료와 함께 소멸되는 변수를 지역 변수라 하고, 이외를 멤버변수라하며 클래스 변수, 인스턴스 변수가 포함된다. 클래스 변수는 앞에 static이 붙으며 인스턴스 생성없이 접근이 가능하고, 인스턴스 변수는 인스턴스 생성 후에 접근이 가능하다.

 

Q. 메서드를 사용하는 이유는?

A. 재사용성, 중복 제거, 프로그램 구조화 or 효율적인 유지보수가 가능하다.

 

Q. 스태틱 영역, 스택 영역(call stack), 힙 영역이란 무엇인가?

A. 스태틱 영역은 클래스 데이터가 저장되는 영역이고, 스택 영역은 호출되는 메서드의 연산을 위한 데이터가 저장되는 영역, 힙 영역은 인스턴스가 저장되는 영역이다.

 

Q. 클래스 전부를 미리 로드하지 않고 클래스가 사용될 때 로드하는 이유는 무엇인가?

A. 메모리의 효율성 때문이다. 아직 사용하지 않는 데이터가 메모리를 차지하고 있는 것은 낭비이므로 필요할 때 데이터를 로드하는 것이 효율적이다.

 

Q. 오버로딩이란?

A. 매개변수의 개수나 타입을 다르게 하여 이름이 같은 메서드를 여러 개 정의하는 것이다.

 

Q. 생성자란?

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


참고자료

'개념서 > Java' 카테고리의 다른 글

[Java] package와 import  (0) 2022.06.10
[Java] 상속  (0) 2022.06.10
[Java] 조건문, 반복문, 배열  (0) 2022.05.28
[Java] 연산자  (0) 2022.05.27
[Java] 변수  (0) 2022.05.23

댓글