Skip to content

Latest commit

 

History

History
366 lines (218 loc) · 16.2 KB

index.md

File metadata and controls

366 lines (218 loc) · 16.2 KB

Java

개요

이 글은 Java의 정석을 읽고 정리한 내용 입니다.

Java의 정석 - 남궁성


목차



6. 객체지향 프로그래밍 1


(1) 클래스와 객체

클래스는 객체를 정의해놓은 것으로 객체의 설계도라고 정의할 수 있다. 클래스는 객체를 생성하는 데 사용되며 객체는 클래스의 정의대로 생성된다.

객체의 사전적인 정의는 실제로 존재하는 것이지만, 프로그래밍에서의 객체는 클래스에 정의된 내용대로 메모리에 생성된 것을 의미한다.

클래스를 정의하고 객체를 생성하는 이유는 잘 만든 설계도가 있으면 제품을 만드는 것이 쉬어지는 것과 같이 클래스를 잘 선언하면 객체를 생성할 떄 어떻게라는 고민을 하지 않아도 된다.

JDK에서는 우리가 개발을 할 때 사용할 수 있는 다양하고 유용한 클래스를 기본적으로 제공하고 있어 우리는 이를 이용해 편하게 개발을 할 수도 있다.




클래스로부터 객체를 생성하는 과정을 클래스의 인스턴스화라고 하며, 클래스로부터 만들어진 객체는 인스턴스라고 한다. 인스턴스와 객체는 같은 의미이지만 객체는 모든 인스턴스의 대표적인 의미를, 인스턴스는 구체적인 의미를 가지고 있다.

객체에는 속성과 기능 2가지 요소로 이루어져 있고 하나의 객체는 다수의 속성과 기능을 가지는 것이 일반적이다. 즉 객체는 속성과 기능의 집합이며, 속성과 기능을 그 객체의 멤버라고 한다.

객체는 클래스를 기반으로 생성되기에 클래스에는 모든 속성과 기능이 정의되어있다.

인스턴스를 생성할 때는 일반적으로는 new 키워드를 이용한다.

class Tv{
    String name;
}

class Test{
    public statis void main(String args[]){
        Tv t = new Tv();
        t.name = "LG 티비";
    }
}

Tv t는 Tv 클래스 타입의 참조 변수 t를 선언한다. 하지만 아직은 공간이 있을 뿐 인스턴스가 생성되지 않았기에 이 참조 변수로 아무것도 할 수 없다. 그 후에 연산자 new를 이용해 Tv 클래스의 인스턴스가 빈 공간에 생성된다.

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




많은 수의 객체를 다룰 때는 객체들을 배열로 다루는 객체 배열을 사용할 수 있다. 객체 배열은 참조 변수들을 하나로 묶은 참조 변수 배열이라고 생각하면 된다.

Tv[] tvArr = new Tv[3];

위와 같은 코드에서는 tvArr에 모든 객체들이 참조되는 것이 아니라 객체의 주소를 배열에 저장하여 참조해 사용한다고 생각하면 된다.

또한 다형성을 이용하면 여러 타입의 객체를 하나의 배열로 다룰 수도 있다.


(2) 변수와 메서드


변수

변수는 클래스 변수, 인스턴스 변수, 지역 변수 3가지로 구분된다. 종류를 결정하는 것은 변수 선언의 위치이며 이에 따라 생성되는 시기가 다르다.

  1. 인스턴스 변수

    인스턴스 변수는 클래스 영역에서 선언되어있으며, 인스턴스가 생성되었을 때 변수가 생성된다. 인스턴스 변수를 읽거나 저장을 할려면 당연히 인스턴스를 생성해야하고 인스턴스는 독립적인 값이기에 서로 다른 값을 가질 수 있다.

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

  2. 클래스 변수

    클래스 변수는 인스턴스 변수와 같은 위치에 선언되지만 static 키워드와 함께 선언하며, 클래스가 메모리에 올라갈 때 생성된다. 인스턴스 변수는 인스턴스 마다 다른 값을 가질 수 있지만, 클래스 변수는 모든 인스턴스가 같은 저장 공간을 공유하게 된다.

    한 클래스의 모든 인스턴스가 같은 값을 공유해야할 때 사용하며, 인스턴스를 생성하지 않아도 사용할 수 있는 특징이 있다. public 키워드를 추가하면 전역 변수로 사용이 가능하다.

  3. 지역 변수

    지역 변수는 클래스 이외에 메서드 생성자 등의 존재하는 변수로 변수의 선언문이 수행되었을 때 생성된다.




메서드

메서드는 특정 작업을 수행하는 코드를 하나로 묶은 것이다. 메서드를 사용할 때는 작업을 수행하는 데 필요한 INPUT을 넣고 OUTPUT을 얻으면 되며 내부적으로 어떤 과정을 거쳐 결과를 만드는 지는 몰라도 된다. (물론 작업 플로우상이지 개발자는 알아야한다.) 예를 들어 println()의 작동 원리를 몰라도 우리는 잘 사용할 수 있다.

우리는 메서드를 사용하는 이유는 높은 재사용성, 중복된 코드의 제거와 프로그램의 구조화 등의 이점을 얻을 수 있다.

  1. 높은 재사용성

    한 번 만들어둔 메서드는 지속적으로 재사용이 가능하며, 타 로직에서도 호출이 가능하다.

  2. 중복된 코드의 제거

    개발을 하다 보면 중복된 코드를 자주 발견할 수 있는 데 이를 메서드로 분리를 하며 중복된 코드를 사용하던 곳에서 메서드를 사용하면 코드의 중복을 줄일 수도 있고 유지보수 할 때도 중복된 모든 코드를 수정하는 것이 아닌 메서드만 수정하면 된다.

  3. 프로그램의 구조화

    우리가 원하는 서비스 혹은 프로그램을 만들기 위해서는 생각보다 오랜 설계를 통한 적합한 구조를 만들어낸다. 이에 메서드를 사용하여 중복 코드를 줄이고 재사용성을 높이는 것은 프로그램을 구조화하는 것에 이점이 있다.


메서드는 선언부와 구현부로 이루어져있다.

int add (int a, int b) { //선언부
    return a + b; // 구현부
}

선언부에는 메서드의 이름과 INPUT으로 받을 수 있는 매개변수의 선언 그리고 해당 메서드의 반환 타입으로 구성되어있다. 메서드를 유지보수 할 때는 가능하면 선언부를 수정하지 않게 만드는 것이 좋다. 선언부를 수정하면 메서드를 호출한 모든 곳을 수정해야한다.

메서드의 매개변수를 선언 할 때는 개수의 제한은 거의 없지만 입력 받을 값의 개수가 많다면 배열이나 참조 변수를 활용할 수 있고, 입력 받을 값이 없다면 비어두어도 된다.

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

class MyMath  {
    long add(long a, long b){
        return a + b;
    }
}

MyMath mm = new MyMath();
long val = mm.add(1L, 2L); // 인스턴스를 생성하여 메서드 사용

우리가 메서드를 구현할 때 그 중에서 매개 변수를 받을 때는 매개변수의 값이 적절한 지 검사하는 것이 매우 중요하다. 해당 메서드를 호출하는 쪽이 알아서 잘하겠지라는 생각은 위험하기에 메서드 내부에서도 값에 대한 검증을 하는 것이 좋다.

int divide(int a, int b){
    if(b == 0){
        System.out.println("0으로 나눌 수 없다.");
        return 0;
    }

    return x /y;
}

JVM 메모리 구조


JVM은 Java Virtual Machine의 줄임말로 자바를 실행하기 위한 가상 기계다. Java는 OS에 종속적이지 않다는 특징이 있고, 이를 위해서는 Java를 실행시킬 어떤 것이 필요하다.

즉 OS에 종속받지 않고 CPU가 Java를 인식하고 실행할 수 있게 하는 가상 컴퓨터가 JVM이다.

Java의 소스코드, 원시코드인 .java의 파일들은 CPU가 인식을 하지 못 하기에 기계어로 컴파일을 해야한다.그러핟고 바로 기계어로 컴파일이 되는 것이 아닌 Java는 JVM으로 실행하기 때문에 JVM이 인식할 수 있는 Java bytecode(*.class)로 변환된다. 변환된 코드는 기계어가 아니기에 OS에서 바로 실행이 불가능하고 JVM이 OS가 bytecode를 이해할 수 있게 해석해주는 역할을 한다.


image


JVM은 클래스 로더, 실행 엔진, 런타임 데이터 영역으로 구성되어있다.

  1. Class Loader(클래스 로더)

    JVM내로 Class 파일을 로드하고, 링크를 통해 배치 작업을 수행하는 모듈로 런 타임시에 동적으로 클래스를 로드하고 jar 파일 내 저장된 클래스를 JVM 위에 탑재한다.

    즉 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크하는 역할을 한다.

  2. Execution Engine(실행 엔진)

    클래스를 실행시키는 역할로, Class Loader가 JVM내의 런타임 데이터 영역에 바이트 코드를 배치시키고 이것은 실행 엔진에 의하여 실행된다.

    Java bytecode는 기계가 바로 수행할 수 있는 언어보단 인간이 비교적으로 보기 쉬운 형태로 기술되었기에 기술 엔진은 이를 기계가 실행할 수 있는 형태로 변경한다.

  3. 런타임 데이터 영역(Runtime Data Area)

    런타임 데이터 영역은 프로그램을 수행하기 위해서 OS로부터 할당받은 메모리 공간을 의미한다.

    프로세스(process)
    사용자가 작성한 프로그램이 운영체제에 의하여 메모리 공간을 할당 받아 실행 중인 것을 의미하며, 데이터와 메모리 등의 자원과 스레드로 구성된다.
    
    쓰레드(Thread)
    프로세스 내에서 실제로 작업을 수행하는 주체를 의미하며, 모든 프로세스에는 1개 이상으 스레드가 존재하여 작업을 수행한다.
    
    2개이상의 쓰레드를 가진 프로세스를 멀티 스레드 프로세스라고 한다.
    
    

    image

    먼저 쓰레드는 PC Register와 JVM Stack, Native Method Stack로 구분된다.

    PC Register는 Thread가 생성 될 때 마다 생성되는 공간으로 스레드 마다 1개씩 존재한다. Thread가 어떤 부분을 어떤 영역으로 실행해야할 지에 대한 기록을 하는 부분으로 JVM 명령의 주소를 가진다.

    JVM Stack는 프로그램 실행과정에서 임시로 할당되었다가 메소드를 빠져나가면 바로 소멸되는 데이터를 저장하기 위한 영역으로 변수나 임시 데이터, 메서드의 정보를 저장한다고 보면 된다. 메서드 호출 시 마다 각각의 Stack이 생성되고 수행이 끝나면 삭제된다.

    Native Method Stack은 자바 프로그램이 컴파일되어 생성되는 Bytecode가 아니라 기계어로 작성된 프로그램을 실행시키는 영역이다. 커널이 스택을 잡아 독자적으로 프로그램을 실행시킨다.

    Heap 영역에서는 객체를 저장하는 가상 메모리 공간으로, new 연산자 등으로 생성되는 개체와 배열을 저장한다. 즉 인스턴스가 생성되는 공간이기에 인스턴스 변수도 이곳에 저장된다.

재귀호출

메서드의 내부에서 메드 자신을 다 호출하는 것을 재귀호출이라고 하고, 그러한 메서드를 재귀 메서드라고 한다.

void method(){
   method();
}

메서드를 호춣하는 것은 특정 위치에 저장되어있는 명령을 수행하는 것이기에, 자신을 호출하는 것과 다른 메서드를 호출하는 것에는 차이가 없다.

하지만 위와 같이 사용하게 되면 무한 반복에 빠지게 되기에 통상적으로 재귀 메서드에서는 조건문이 필수적이다.

그렇다면 재귀 메서드는 왜 반복문이 있는 데도 불구하고 사용하는 것인가?? 그 이유는 논리적인 간결함 때문이라고 한다.

대표적인 예시로는 팩토리얼을 구하는 것이다.

f(n) =  n * f(n-1),  f(1) = 1

이러한 위의 수식을 Java의 재귀 메서드를 사용하면 아래처럼 간단하게 구현이 가능하다.

int factorial(int n){
   if(n == 1) return 1;
   return n * factorial(n - 1);
}

클래스 메서드와 인스턴스 메서드

변수와 마찬가지로 메서드 앞에 static이 붙어있으면 클래스 메서드고, 붙어있지 않으면 인스턴스 메서드이다.

클래스 메서드도 클래스 변수와 마찬가지로, 객체를 생성하지 않고도 AClass.Method()와 같은 방식으로 호출이 가능하고, 인스턴스 메서드는 불가능하다.

클래스 메서드는 인스턴스와 관계 없는 메서드를 static을 붙여서 선언하고, 인스턴스 내부의 값들을 다루는 메서드들은 인스턴스 메서드로 정의하여 사용한다.

같은 클래스에 속한 멤버들 간에는 별도의 인스턴스를 생성하지 않더라고 서로 참조하거나 호출이 가능하지만 클래스 멤버가 인스턴스 멤버를 참조 하거나 호출할려면 인스턴스를 생성해야한다.

인스턴스 멤버가 존재하는 시점에 클래스 멤버는 항상 존재하지만 반대는 그렇지 않기 때문이다.


(3) 오버로딩


overloading(오버로딩)

클래스 내에서 메서드도 변수와 마찬가지로 서로 구별을 해야하기에 다른 이름을 가져야하지만, 매개변수의 개수, 타입이 다르면 같은 이름을 사용해서 메서드를 정의할 수도 있다.

이처럼 한 클래스 내에서 같은 이름의 메서드를 여러개 정의하는 것을 '메서드 오버로딩', '오버로딩'이라고 한다.

오버로딩을 사용하게 되면, 여러 개의 메서드들이 같은 이름을 사용할 수 있기에 매개변수가 다르다고 하더라도, 같은 기능을 수행한다는 것을 유추할 수 있다.


가변인자와 오버로딩


JDK 1.5부터 고정적인 개수로 사용해야했던 메서드의 매개변수를 동적으로 지정할 수 있게 되었는 데 이 기능을 가변인자라고 한다.

만약 여러 문자열을 하나로 결합해 반환하는 코드를 짠다고 가정했을 때 기존에는 아래와 같이 여러 메서드를 선언해야한다.

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){}

(4) 생성자


생성자는 인스턴스가 생성될 때 호출되는 인스턴스 초기화 메서드다. 즉 클래스를 이용하여 객체를 생성할 때 사용하는 메서드다.

생성자는 클래스 내부에 선언되며, 메서드의 이름은 클래스와 같아야하면서 리턴 값이 없어야한다.

class Car{

   // 생성자
   Car(){

   }
}

우리가 클래스를 이용하여 객체를 생성할 때, 생성자를 이용하기에 생성자가 인스턴스를 생성한다고 생각하는 경우가 있는 데, new 라는 키워드가 인스턴스를 생성하는 것이고, 생성자는 인스턴스를 초기화하는 것 뿐이다.

기본 생성자

Java에서 모든 클래스에는 1개 이상의 생성자가 선언이 되어있어야한다. 하지만 보통 공부를 할 때 기본 생성자를 사용하진 않는 데, 보통 컴파일러에서 기본적으로 기본 생성자를 제공하기 때문이다.

기본 생성자는 매개변수도 없고 아무런 내용도 없는 생성자다.

class Car{

   // 기본 생성자
   Car(){

   }
}

매개변수가 있는 생성자

생성자도 메서드이기에 매개변수를 넘겨 받아서 인스턴스의 초기화 작업에서 사용할 수 있다. 주로는 초기 값을 할당할 때 사용한다.

class Car{
   private String color;

   // 기본 생성자
   Car(){

   }

   // 매개변수 있는 생성자
   Car(String color){
      this.color = color;
   }
}