Chapter 06 - 클래스

김태원·2023년 1월 12일
0

객체 지향 프로그래밍

소프트웨어를 개발할 때 객체들을 먼저 만들고, 이 객체들을 하나씩 조립해서 완성된 프로그램을 만드는 기법을 객체 지향 프로그래밍(OOP - Object Oriented Programming)이라고 한다.

객체란?

객체(Object)란 물리적으로 존재하거나 개념적인 것 중에서 다른 것과 식별 가능한 것을 말한다.

객체는 속성과 동적으로 구성되는데 예를 들어 사람은 이름, 나이 등의 속성과 웃다, 걷다 등의 동작이 있다.
자바는 이러한 속성과 동작을 각각 필드(Field)와 메소드(Method)라고 부른다.


현실 세계의 객체를 소프트웨어 객체로 설계하는 것을 객체 모델링(Object Modeling)이라고 한다.
객체 모델링은 현실 세계의 대표 속성과 동작을 추려내어 소프트웨어 객체의 필드와 메소드로 정의하는 과정이라고 볼 수 있다.

객체의 상호 작용

현실 세계에서 일어나는 모든 현상은 객체와 객체 간의 상호 작용으로 이루어져 있다.

예를 들어 사람은 전자계산기의 기능을 이용하고, 전자계산기는 계산 결과를 사람에게 리턴하는 상호작용을 한다.

객체 지향 프로그램에서도 객체들은 다른 객체와 서로 상호작용하면서 동작한다.
객체들 사이의 상호작용 수단은 메소드이다.
객체가 다른 객체의 기능을 이용할 때 이 메소드를 호출한다.

메소드 호출은 다음과 같은 형태를 가지고 있다.

메소드(매개값1, 매개값2, ...);

메소드 호출을 통해 객체들은 데이터를 서로 주고받는다.
메소드 이름과 함께 전달하고자 하는 데이터를 괄호안에 기술하는데, 이러한 데이터를 매개변수(Parameter)라고 한다.

여기서 짚고 넘어가야 할 부분은 매개변수(Parameter)인자(Argument)의 차이이다.
매개변수는 메소드를 정의할때 사용하는 변수이며, 인자는 실제로 전달되는 값(value)이다.

예시를 보자

public int sum(int a, int b) {
    return a + b;
}

int c = sum(1, 2);

위 예시에서 int a, int b매개변수(Parameter)이고 12인자(Argument)이다.

객체 간의 관계

객체는 단독으로 존재할 수 있지만 대부분 다른 객체와 관계를 맺고 있다.
관계의 종류에는 집합 관계, 사용 관계, 상속 관계가 있다.

집합 관계

집합 관계는 완성품과 부품의 관계라고 할 수 있다.
예를 들어 자동차는 엔진, 타이어, 핸들 등으로 구성되므로 자동차와 부품들은 집합 관계라고 볼 수 있다.

사용 관계

사용 관계는 다른 객체의 필드를 읽고 변경하거나 메소드를 호출하는 관계를 말한다.
예를 들어 사람이 자동차에게 엑셀, 브레이크 등의 메소드를 호출하면 사람과 자동차는 사용 관계라고 볼 수 있다.

상속 관계

상속 관계는 부모와 자식 관계를 말한다. 자동차가 기계의 특징(필드, 메소드)을 물려받는다면 기계(부모)와 자동차(자식)는 상속 관계에 있다고 볼 수 있다.

객체 지향 프로그래밍의 특징

객체 지향 프로그램의 특징은 캡슐화, 상속, 다형성이다.

캡술화

캡슐화(Encapsulation)이란 객체의 데이터(필드), 동작(메소드)을 하나로 묶고 실제 구현 내용을 외부에 감추는 것을 말한다.

외부 객체는 객체 내부의 구조를 알지 못하며 객체가 노출해서 제공하는 필드와 메소드만 사용할 수 있다.

필드와 메소드를 캡슐화하여 보호하는 이유는 외부의 잘못된 사용으로 인해 객체가 손상되지 않도록 하는 데 있다.
자바는 캡슐화된 멤버를 노출시킬 것인지 숨길 것인지를 결정하기 위해 접근 제한자(Access Modifier)를 사용한다.

상속

객체 지향 프로그래밍에서는 부모 역할의 상위 객체와 자식 역할의 하위 객체가 있다.
부모 객체는 자기가 가지고 있는 필드와 메소드를 자식 객체에게 물려주어 자식 객체가 사용할 수 있도록 한다.

이것이 상속(Inheritance)이다. 상속을 하는 이유는 다음과 같다.

  • 코드의 재사용성을 높여준다.
    잘 개발된 부모 객체의 필드와 메소드를 자식이 그대로 사용할 수 있어 자식 객체에서 중복 코딩을 하지 않아도 된다.
  • 유지 보수 시간을 최소화시켜 준다.
    부모 객체의 필드와 메소드를 수정하면 모든 자식 객체들은 수정된 필드와 메소드를 사용할 수 있다.

다형성

다형성(Polymorphism)이란 사용 방법은 동일하지만 실행 결과가 다양하게 나오는 성질을 말한다.
자동차의 부품을 교환하면 성능이 다르게 나오듯이 프로그램을 구성하는 객체(부품)를 바꾸면 프로그램의 실행 성능이 다르게 나올 수 있다.

객체와 클래스

객체 지향 프로그래밍에서는 객체를 생성하려면 설계도에 해당하는 클래스가 필요하다.

클래스로부터 생성된 객체를 해당 클래스의 인스턴스라고 부른다.
그리고 클래스로부터 객체를 만드는 과정을 인스턴스화라고 한다.
동일한 클래스로부터 여러 개의 인스턴스를 만들 수 있는데, 이것은 동일한 설계도로 여러 대의 자동차를 만드는 것과 동일하다.

클래스 선언

클래스 선언은 객체 생성을 위한 설계도를 작성하는 작업이다.

클래스 선언은 소스 파일명과 동일하게 다음과 같이 작성한다.
클래스명.java

public class 클래스명 {
}

public class는 공개 클래스를 선언한다는 뜻이다.
클래스명은 Pascal Case로 작성한다.

하나의 소스 파일은 복수 개의 클래스 선언을 포함할 수 있다.
복수 개의 클래스 선언이 포함된 소스 파일을 컴파일 하면 바이트코드 파일(.class)은 클래스 선언 수만큼 생긴다는 점에 주의하자.
하나의 소스 파일에 복수 개의 클래스를 선언할 때 주의할 점은 소스 파일명과 동일한 클래스만 공개 클래스(public class)로 선언할 수 있다는 것이다.

특별한 이유가 없다면 소스 파일 하나당 클래스 하나를 선언하는 것이 좋다.

객체 생성과 클래스 변수

클래스로부터 객체를 생성하려면 객체 생성 연산자인 new가 필요하다.

new 연산자 뒤에는 생성자 호출 코드가 오는데, 클래스() 형태를 가진다.
new 연산자는 객체를 생성시킨 후 객체의 주소 를 리턴하기 때문에 클래스 변수에 다음과 같이 대입할 수 있다.
다음 그림은 클래스 변수가 생성된 객체를 참조하는 모양을 보여준다.

아래 예제를 보자.
Student.java

package ch06.sec04;

public class Student {
}

StudentExample.java

package ch06.sec04;

public class StudentExample {
	public static void main(String[] args) {
		Student s1 = new Student();
		System.out.println("s1 변수가 Student 객체를 참조합니다.");

		Student s2 = new Student();
		System.out.println("s2 변수가 또 다른 Student 객체를 참조합니다.");
	}
}

다음 그림은 예제를 실행했을 때 클래스 변수가 객체를 참조하는 모양을 보여준다.

클래스의 두 가지 용도

클래스에는 다음 두 가지 용도가 있다.

  • 라이브러리(library) 클래스: 실행할 수 없으며 다른 클래스에서 이용하는 클래스
  • 실행 클래스: main() 메소드를 가지고 있는 실행 가능한 클래스

클래스의 구성 멤버

생성자, 필드, 메소드를 클래스 구성 멤버라고 한다.

다음은 각 클래스 구성 멤버의 선언 형태이다.

필드

필드는 객체의 데이터를 저장하는 역할을 한다. 선언 형태는 변수 선언과 비슷하지만 쓰임새는 다르다.

생성자

생성자는 new 연산자로 객체를 생성할 때 객체의 초기화 역할을 담당한다. 선언 형태는 메소드와 비슷하지만, 리턴 타입이 없고 이름은 클래스 이름과 동일하다.

메소드

메소드는 객체가 수행할 동작이다. 다른 프로그래밍 언어에서는 함수라고 하기도 하는데, 객체 내부의 함수는 메소드라고 부른다.
메소드는 객체와 객체간의 상호 작용을 위해 호출된다.

필드 선언과 사용

필드는 객체의 데이터를 저장하는 역할을 한다. 객체의 데이터에는 고유 데이터, 현재 상태 데이터, 부품 데이터가 있다.

자동차 객체를 예로 들면 제작회사, 모델, 색깔, 최고 속도는 고유 데이터에 해당하고, 현재 속도, 엔진 회전 수는 상태 데이터 해당한다.
그리고 차체, 엔진, 타이어는 부품에 해당한다.

필드 선언

필드를 선언하는 방법은 변수를 선언하는 방법과 동일하다.
단, 반드시 클래스 블록에서 선언되어야만 필드 선언이 된다.

타입 필드명 [ = 초기값 ];

필드와 (로컬) 변수의 차이점

(로컬) 변수는 생성자와 메소드 블록에서 선언되며 생성자와 메소드 호출 시에만 생성되고 사용된다.
필드는 클래스 블록에서 선언되며, 객체 내부에서 존재하고 객체 내외부에서 사용 가능하다.

구분필드(로컬) 변수
선언 위치클래스 선언 블록생성자, 메소드 선언 블록
존재 위치객체 내부에 존재생성자, 메소드 호출 시에만 존재
사용 위치객체 내외부 어디든 사용생성자, 메소드 블록 내부에서만 사용

초기값을 제공하지 않을 경우 필드는 객체 생성 시 자동으로 기본값으로 초기화된다.
다음 표는 필드 타입별 기본값을 보여준다.

필드 사용

필드를 사용한다는 것은 필드값을 읽고 변경하는 것을 말한다.
클래스에서 필드를 선언했다고 해서 바로 사용할 수 있는 것은 아니다.
필드는 객체의 데이터이므로 객체가 존재하지 않으면 필드도 존재하지 않는다.

클래스로부터 객체가 생성된 후에 필드를 사용할 수 있다.
필드는 객체 내부의 생성자와 메소드 내부에서 사용할 수 있고, 객체 외부에서도 접근해서 사용할 수 있다.

객체 내부에서는 단순히 필드명으로 읽고 변경할 수 있지만 외부 객체에서는 참조 변수와 도트 연산자를 이용해서 필드를 읽고 변경해야 한다. 도트는 객체 접근 연산자로, 객체가 가지고 있는 필드나 메소드에 접근하고자 할 때 참조 변수 뒤에 붙인다.

생성자 선언과 호출

new 연산자는 객체를 생성한 후 연이어 생성자를 호출해서 객체를 초기화하는 역할을 한다.

객체 초기화란 필드 초기화를 하거나 메소드를 호출해서 객체를 사용할 준비를 하는 것을 말한다.

클래스 변수 = new 클래스();

생성자가 성공적으로 실행이 끝나면 new 연산자는 객체의 주소를 리턴한다.
리턴된 주소는 클래스 변수에 대입되어 객체의 필드나 메소드에 접근할 때 이용된다.

기본 생성자

모든 클래스는 생성자가 존재하며, 하나 이상을 가질 수 있다.
클래스에 생성자 선언이 없으면 컴파일러는 다음과 같은 기본 생성자를 바이트코드 파일에 자동으로 추가시킨다.

[public] 클래스() { }

클래스가 public class로 선언되면 기본 생성자도 public이 붙지만, 클래스가 public 없이 class로만 선언되면 기본 생성자에도 public이 붙지 않는다.

예를 들어 Car 클래스를 설계할 때 생성자를 생략하면 기본 생성자가 다음과 같이 생성된다.

// 소스 파일(Car.java)
public class Car {
}
// 바이트코드 파일(Car.class)
public class Car {
    public Car() { } // 자동 추가
}

그렇기 때문에 다음과 같이 new 연산자 뒤에 기본 생성자를 호출할 수 있다.

car myCar = new Car(); // 기본 생성자 호출

그러나 개발자가 명시적으로 선언한 생성자가 있다면 컴파일러는 기본 생성자를 초기화하지 않는다.

생성자 선언

객체를 다양하게 초기화하기 위해 개발자는 생성자를 다음과 같이 직접 선언할 수 있다.

클래스(매개변수, ...) {
    // 객체의 초기화 코드
}

생성자는 메소드와 비슷한 모양을 가지고 있으나, 리턴 타입이 없고 클래스 이름과 동일하다.
다음 예제를 보자

public class Car {
    // 생성자 선언
    Car(String model, String color, int maxSpeed){...}
}

위와 같이 생성자를 선언하면 아래와 같이 매개변수를 블록 내부로 전달할 수 있다.

Car myCar = new Car("그랜져", "검정", 300);

필드 초기화

객체마다 동일한 값을 갖고 있다면 필드 선언 시 초기값을 대입하는 것이 좋고, 객체마다 다른 값을 가져야 한다면 생성자에서 필드를 초기화하는 것이 좋다.
다음 예제를 보자

public class Korean {
    // 필드 선언
    String nation = "대한민국",
    String name;
    String ssn;
    
    // 생성자 선언
    public Korean(String n, String s) {
        name = n;
        ssn = s;
    }
}

생성자의 매개값은 new 연산자로 생성자를 호출할 때 주어진다.

korean k1 = new Korean("박자바", "011225-1234567");
korean k2 = new Korean("김자바", "991225-7654321");

생성자 오버로딩

매개값으로 객체의 필드를 다양하게 초기화하려면 생성자 오버로딩이 필요하다.
생성자 오버로딩이란 매개변수를 달리하는 생성자를 여러 개 선언하는 것을 말한다.
다음 예제를 보자

Car.java
package ch06.sec07.exam04;

public class Car {
    // 필드 선언
    String company = "현대자동차";
    String model;
    String color;
    int maxSpeed;

    // 생성자 선언
    Car() {
    }

    Car(String model) {
        this.model = model;
    }

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

    Car(String model, String color, int maxSpeed) {
        this.model = model;
        this.color = color;
        this.maxSpeed = maxSpeed;
    }
}
CarExample.java
package ch06.sec07.exam04;

public class CarExample {
    public static void main(String[] args) {
        Car car1 = new Car();
        System.out.println("car1.company : " + car1.company);
        System.out.println();

        Car car2 = new Car("자가용");
        System.out.println("car2.company : " + car2.company);
        System.out.println("car2.model : " + car2.model);
        System.out.println();

        Car car3 = new Car("자가용", "빨강");
        System.out.println("car3.company : " + car3.company);
        System.out.println("car3.model : " + car3.model);
        System.out.println("car3.color : " + car3.color);
        System.out.println();

        Car car4 = new Car("택시", "검정", 200);
        System.out.println("car4.company : " + car4.company);
        System.out.println("car4.model : " + car4.model);
        System.out.println("car4.color : " + car4.color);
        System.out.println("car4.maxSpeed : " + car4.maxSpeed);
    }
}

다른 생성자 호출

생성자 오버로딩이 많아질 경우 생성자 간의 중복된 코드가 발생할 수 있다.
매개변수의 수만 달리하고 필드 초기화 내용이 비슷한 생성자에서는 중복 코드를 많아지게 되는데 이 경우에는 공통 코드를 한 생성자에만 집중적으로 작성하고, 나머지 생성자에는 this(...)를 사용하여 공통 코드를 가지고 있는 생성자를 호출하는 방법으로 개선할 수 있다.

다음 예제를 보자

Car.java
package ch06.sec07.exam05;

public class Car {
    //  필드 선언
    String company = "현대자동차";
    String model;
    String color;
    int maxSpeed;

    Car(String model) {
        // 20라인 생성자 호출
        this(model, "은색", 250);
    }

    Car(String model, String color) {
        // 20라인 생성자 호출
        this(model, color, 250);
    }

    Car(String model, String color, int maxSpeed) {
        this.model = model;
        this.color = color;
        this.maxSpeed = maxSpeed;
    }
}
CarExample.java
package ch06.sec07.exam05;

public class CarExample {
    public static void main(String[] args) {
        Car car1 = new Car("자가용");
        System.out.println("car1.company : " + car1.company);
        System.out.println("car1.model : " + car1.model);
        System.out.println();

        Car car2 = new Car("자가용", "빨강");
        System.out.println("car2.company : " + car2.company);
        System.out.println("car2.model : " + car2.model);
        System.out.println("car2.color : " + car2.color);
        System.out.println();

        Car car3 = new Car("택시", "검정", 200);
        System.out.println("car3.company : " + car3.company);
        System.out.println("car3.model : " + car3.model);
        System.out.println("car3.color : " + car3.color);
        System.out.println("car3.maxSpeed : " + car3.maxSpeed);
    }
}

메소드 선언과 호출

메소드 선언은 객체의 동작을 실행 블록으로 정의하는 것을 말하고, 메소드 호출은 실행 블록을 실제로 실행하는 것을 말한다.

메소드 선언

다음은 메소드를 선언하는 방법을 그림으로 나타낸 것이다.

리턴 타입

리턴 타입은 메소드가 실행한 후 호출한 곳으로 전달하는 결과값의 타입을 말한다.
리턴값이 없는 메소드는 void로 작성해야 한다

void powerOn() {} // 리턴값이 없는 메소드 선언
double divide(int x, int y) {} // double 타입 값을 리턴하는 메소드 선언

메소드명

메소드명은 Camel Case로 작성한다.

매개변수

매개변수(Parameter)는 메소드를 호출할 때 전달한 인자(Argument)를 받기 위해 사용된다.

실행 블록

메소드 호출 시 실행되는 부분이다

메소드 호출

메소드를 호출한다는 것은 메소드 블록을 실행하는 것을 말한다.
클래스에서 메소드를 선언했다고 해서 바로 호출할 수 있는 것은 아니다. 메소드는 객체의 동작이므로 객체가 존재하지 않으면 메소드를 호출할 수 없다.

다음은 메소드 호출을 그림으로 나타낸 것이다.

가변길이 매개변수

메소드를 호출할 때에는 매개변수의 개수에 맞게 매개값을 제공해야 한다.
만약 메소드가 가변길이 매개변수를 가지고 있다면 매개변수의 개수와 상관없이 매개값을 줄 수 있다.

다음과 같이 선언한다

int sum(int ... values) {
}

다음과 같이 사용한다

int result = sum(1, 2, 3);

매개값들은 자동으로 배열 항목으로 변환되어 메소드에서 사용된다.
따라서 메소드 호출 시 직접 배열을 매개값으로 제공해도 된다.

int result = sum(new int[] {1, 2, 3});

메소드 오버로딩

메소드 오버로딩은 메소드 이름은 같되 매개변수의 타입, 개수, 순서가 다른 메소드를 여러개 선언하는 것을 말한다.
다음 예제를 보자

Calculator.java
package ch06.sec08.exam04;

public class Calculator {
    // 정사각형의 넓이
    double areaRectangle(double width) {
        return width * width;
    }

    // 직사각형의 넓이
    double areaRectangle(double width, double height) {
        return width * height;
    }
}
CalculatorExample.java
package ch06.sec08.exam04;

public class CalculatorExample {
    public static void main(String[] args) {
        // 객체 생성
        Calculator myCalcu = new Calculator();

        // 정사각형의 넓이 구하기
        double result1 = myCalcu.areaRectangle(10);

        // 직사각형의 넓이 구하기
        double result2 = myCalcu.areaRectangle(10, 20);

        System.out.println("정사각형 넓이=" + result1);
        System.out.println("직사각형 넓이=" + result2);
    }
}

인스턴스 멤버

필드와 메소드는 선언 방법에 따라 인스턴스 멤버와 정적 멤버로 분류할 수 있다. 인스턴스 멤버로 선언되면 객체 생성 후 사용할 수 있고, 정적 멤버로 선언되면 객체 생성 없이도 사용할 수 있다.

구분설명
인스턴스(instance) 멤버객체에 소속된 멤버
(객체를 생성해야만 사용할 수 있는 멤버)
정적(static) 멤버클래스에 고정된 멤버
(객체 없이도 사용할 수 있는 멤버)

다음 예제를 보자

public class Car {
    // 인스턴스 필드 선언
    int gas;
    
    // 인스턴스 메소드 선언
    void setSpeed(int speed) { ... }
}
Car myCar = new Car();
myCar.gas = 10;
myCar.setSpeed(60);

Car yourCar = new Car();
yourCar.gas = 20;
yourCar.setSpeed(80);


setSpeed() 메소드는 객체에 소속되어 있지만 포함되어 있지는 않다.
메소드는 코드의 덩어리이므로 객체마다 저장된다면 중복으로 인해 메모리 효율이 떨어진다.
따라서 메소드 코드는 메소드 영역에 두되, 객체 없이는 사용하지 못하도록 제한을 걸어둔 것이다.

정적 멤버

자바는 클래스 로더를 이용해서 클래스를 메소드 영역에 저장하고 사용한다. 정적 멤버란 메소드 영역의 클래스에 고정적으로 위치하는 멤버를 말한다.

그렇기 때문에 정적 멤버는 객체를 생성할 필요 없이 클래스를 통해 바로 사용이 가능하다.

객체마다 가지고 있을 필요성이 없는 공용적인 필드는 정적 필드로 선언하는 것이 좋다.
다음과 같이 선언할 수 있다.

public class Calculator {
    String color; // 계산기별로 다를 수 있음
    static double pi = 3.141592 // 모든 계산기에서 사용하는 원주율 값은 동일하다
}

인스턴스 필드를 이용하지 않는 메소드도 정적 메소드로 선언하는 것이 좋다.

public class Calculator {
    String color; // 인스턴스 필드
    void setColor(String color) { this.color = color; } // 인스턴스 메소드
    static int plus(int x, int y) { return x + y; } // 정적 메소드
    static int minus(int x, int y) { return x - y; } // 정적 메소드
}

정적 멤버 사용

정적 멤버는 다음과 같이 사용할 수 있다.

double result = 10 * 10 * Calculator.pi;

정적 필드와 메소드는 다음과 같이 객체 참조 변수로도 접근이 가능하다.

Calculator calc = new Calculator();
double result = 10 * 10 * calc.pi;

하지만 정적 요소는 클래스 이름으로 접근하는 것이 정석이므로 첫 번째 방법을 사용하자.

정적 블록

정적 필드는 다음과 같이 필드 선언과 동시에 초기값을 주는 것이 일반적이다.

static double pi = 3.141592;

하지만 복잡한 초기화 작업이 필요하다면 정적 블록을 이용한다.
다음과 같이 사용한다.

Television.java
package ch06.sec10.exam02;

public class Television {
    static String company = "MyCompany";
    static String model = "LCD";
    static String info;

    static {
        info = company + "-" + model;
    }
}
TelevisionExample.java
package ch06.sec10.exam02;

public class TelevisionExample {
    public static void main(String[] args) {
        System.out.println(Television.info);
    }
}

생성자에서 초기화를 하지 않는 정적 필드

정적 필드는 객체 생성 없이도 사용할 수 있기 때문에 생성자에서 초기화 작업을 하지 않는다. 생성자는 객체 생성 후 실행되기 때문이다.

인스턴스 멤버 사용 불가

정적 메소드와 정적 블록은 객체가 없어도 실행된다는 특징 때문에 내부에 인스턴스 필드나 인스턴스 메소드를 사용할 수 없다. 또한 객체 자신의 참조인 this도 사용할 수 없다.

정적 메소드와 정적 블록에서 인스턴스 멤버를 사용하고 싶다면 객체를 먼저 생성하고 참조 변수로 접근해야 한다.

final 필드와 상수

경우에 따라서는 값을 변경하는 것을 막고 읽기만 허용해야 할 떄가 있다. 이때 final 필드와 상수를 선언해서 사용한다.

final 필드 선언

다음과 같이 선언한다.

final 타입 필드 [=초기값];

final 필드에 초기값을 줄 수 있는 방법은 다음 두 가지 밖에 없다

  • 필드 선언 시에 초기값 대입
  • 생성자에서 초기값 대입

상수 선언

수학에서 사용되는 원주율이나 중력 가속도 등은 불변의 값이다.
이러한 불변의 값을 저장하는 필드를 자바에서는 상수(constant)라고 부른다.

상수는 객체마다 저장할 필요가 없고, 여러 개의 값을 가져도 안되기 때문에 static이면서 final인 특성을 가져아 한다.

따라서 다음과 같이 선언한다.

static final 타입 상수 [= 초기값];

상수 이름은 Upper Snake Case로 작성한다.

Earth.java
package ch06.sec11.exam02;

public class Earth {
    // 상수 선언 및 초기화
    static final double EARTH_RADIUS = 6400;

    // 상수 선언
    static final double EARTH_SURFACE_AREA;

    // 정적 블록에서 상수 초기화
    static {
        EARTH_SURFACE_AREA = 4 * Math.PI * EARTH_RADIUS * EARTH_RADIUS;
    }
}
EarthExample.java
package ch06.sec11.exam02;

public class EarthExample {
    public static void main(String[] args) {
        // 상수 읽기
        System.out.println("지구의 반지름: " + Earth.EARTH_RADIUS + "km");
        System.out.println("지구의 표면적: " + Earth.EARTH_SURFACE_AREA + "km^2");
    }
}

접근 제한자

경우에 따라서는 객체의 필드를 외부에서 변경하거나 메소드를 호출할 수 없도록 막아야 할 필요가 있다. 그렇게 함으로써 객체의 무결성을 유지하기 위해이다. 자바는 이러한 기능을 구현하기 위해 접근 제한자를 사용한다.


접근 제한자는 public, protected, private 세 가지 종류가 있다.

default는 접근 제한자가 아니라 접근 제한자가 붙지 않은 상태를 말한다.

접근 제한자제한 대상제한 범위
public클래스, 필드, 생성자, 메소드없음
protected필드, 생성자, 메소드같은 패키지이거나, 자식 객체만 사용 가능
(default)클래스, 필드, 생성자, 메소드같은 패키지
private필드, 생성자, 메소드객체 내부

Getter와 Setter

객체의 필드를 외부에서 마음대로 읽고 변경할 경우 객체의 무결성이 깨질 수 있다. Getter와 Setter는 private 필드를 외부에서 접근하기 위해 사용한다.

Getter 메소드는 주로 필드값이 객체 외부에서 사용하기에 부적절한 경우 적절한 값으로 변환하여 리턴하기 위해 사용된다.
Setter 메소드는 주로 필드값의 유효성을 검증하여 유효한 값만 저장하기 위해 사용된다.

다음은 Getter와 Setter의 예제이다.

Car.java
package ch06.sec14;

public class Car {
    // 필드 선언
    private int speed;
    private boolean stop;

    // speed 필드의 Getter/Setter 선언
    public int getSpeed() {
        return speed;
    }

    public void setSpeed(int speed) {
        if (speed < 0) {
            this.speed = 0;
		} else {
            this.speed = speed;
        }
    }

    // stop 필드의 Getter/Setter 선언
    public boolean isStop() {
        return stop;
    }

    public void setStop(boolean stop) {
        this.stop = stop;
        if (stop) this.speed = 0;
    }
}
CarExample.java
package ch06.sec14;

public class CarExample {
    public static void main(String[] args) {
        // 객체 생성
        Car myCar = new Car();

        // 잘못된 속도 변경
        myCar.setSpeed(-50);
        System.out.println("현재 속도: " + myCar.getSpeed());

        // 올바른 속도 변경
        myCar.setSpeed(60);
        System.out.println("현재 속도: " + myCar.getSpeed());

        // 멈춤
        if (!myCar.isStop()) {
            myCar.setStop(true);
        }
        System.out.println("현재 속도: " + myCar.getSpeed());
    }
}
출력 결과
현재 속도: 0
현재 속도: 60
현재 속도: 0

싱글톤 패턴

애플리케이션 전체에서 단 한개의 객체만 생성해서 사용하고 싶다면 싱글톤(Singleton) 패턴을 적용할 수 있다.
싱글톤 패턴의 핵심은 생성자를 private 접근 제한해서 외부에서 new 연산자로 생성자를 호출할 수 없도록 막는 것이다.

생성자를 호출할 수 없으니 외부에서 마음대로 객체를 생성하는 것이 불가능해진다.
대신 싱글톤 패턴이 제공하는 정적 메소드를 통해 간접적으로 객체를 얻을 수 있다.

다음 예제를 보자.

Singleton.java
package ch06.sec15;

public class Singleton {
    // private 접근 권한을 갖는 정적 필드 선언과 초기화
    private static final Singleton singleton = new Singleton();

    // private 접근 권한을 갖는 생성자 선언
    private Singleton() {
    }

    // public 접근 권한을 갖는 정적 메소드 선언
    public static Singleton getInstance() {
        return singleton;
    }
}
SingletonExample.java
package ch06.sec15;

public class SingletonExample {
    public static void main(String[] args) {
 		// Singleton obj1 = new Singleton(); // 컴파일 에러
 		// Singleton obj2 = new Singleton(); // 컴파일 에러

        // 정적 메소드를 호출해서 싱글톤 객체 얻음
        Singleton obj1 = Singleton.getInstance();
        Singleton obj2 = Singleton.getInstance();

        // 동일한 객체를 참조하는지 확인
        if (obj1 == obj2) {
            System.out.println("같은 Singleton 객체입니다.");
        } else {
            System.out.println("다른 Singleton 객체입니다.");
        }
    }
}

외부에서 객체를 얻는 유일한 방법은 getInstance() 메소드를 호출하는 것이다.
getInstance()메소드가 리턴하는 객체는 정적 필드가 참조하는 싱글톤 객체이다.
따라서 위 코드에서 obj1obj2가 참조하는 객체는 동일한 객체이다.

profile
개발이 재밌어서 하는 Junior Backend Developer

0개의 댓글