[Java] 06. 클래스

JK·2024년 4월 13일
0

[Java]

목록 보기
6/11

객체 지향 프로그래밍

객체들을 먼저 만들고, 이 객체들을 하나씩 조립해서 완성된 프로그램을 만드는 기법을 객체 지향 프로그래밍이라고 한다.

객체

객체(object)물리적으로 존재하거나 개념적인 것 중에서 다른 것과 식별 가능한 것을 말한다. 객체는 속성과 동작으로 구성되는데, 자바에서 속성은 필드(field), 동작은 메소드(method)라고 한다.

객체 모델링(object modeling)은 현실 세계의 객체를 소프트웨어 객체로 설계하는 것이다. 객체 모델링을 통해 현실 세계 객체의 속성과 동작을 추려내어 소프트웨어 객체의 필드와 메소드로 정의한다.

객체의 상호작용

객체 지향 프로그래밍에서는 객체 간의 상호작용을 통해 프로그램이 동작한다. 이 때 객체 간의 상호작용 수단은 메소드이다. 객체는 아래와 같은 메소드 호출을 통해 객체 간의 데이터를 주고 받는다.

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

매개값은 메소드를 실행할 때 필요한 값으로, 메소드 내부로 전달되는 값이다. 리턴값은 메소드의 실행 결과로, 메소드를 호출한 곳에 돌려주는 값이다.

객체 간의 관계

객체 지향 프로그래밍에서 객체는 서로 다양한 관계를 맺는다. 객체 간의 관계에는 집합 관계, 사용 관계, 상속 관계가 있다.

집합 관계는 완성품과 부품의 관계이다. 한 객체가 다른 객체를 완성시키기 위한 구성 요소, 즉 부품 중 하나일 때 두 객체를 집합 관계라고 한다.

사용 관계는 다른 객체의 필드를 읽고 변경하거나 메소드를 호출하는 관계이다. 사람이 전화하기 위해 전화기를 사용하듯이, 한 객체가 특정 동작을 위해 다른 객체의 필드를 수정하거나 메소드를 호출하면 두 객체는 사용 관계에 있다고 할 수 있다.

상속 관계는 부모와 자식 관계를 말한다. 상속 관계에서는 부모 객체의 필드, 메소드 등을 자식 객체가 물려받아 사용한다.

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

첫번째 특징은 캡슐화(Encapsulation)로, 객체의 데이터(필드), 동작(메소드)을 하나로 묶고 실제 구현 내용을 외부에 감추는 것을 말한다. 이는 외부의 잘못된 사용으로 객체가 손상되는 것을 막기 위한 것이다. 자바에서는 캡슐화를 위해 접근 제한자(Access Modifier)를 사용한다.

두번째 특징은 상속으로, 부모 객체가 가지고 있는 필드와 메소드를 자식 객체에 물려주어 자식 객체가 사용할 수 있도록 하는 것을 말한다. 상속을 사용하는 이유는 상속을 통해 중복되는 코드를 줄여서 코드의 재사용성을 높일 수 있기 때문이다. 또한 부모 객체의 코드를 수정하면 이를 상속하는 자식 객체에도 반영되기 때문에 유지 보수 시간을 최소화할 수 있다.

세번째 특징은 다형성(Polymorphism)으로, 사용방법은 동일하지만 실행 결과가 다양하게 나오는 성질을 말한다. 다형성은 상속과 인터페이스 구현을 통한 자동 타입 변환과 재정의 기술을 통해 구현할 수 있다.


객체와 클래스

클래스(class)는 객체를 생성하기 위한 설계도에 해당한다. 인스턴스(instance)는 이 클래스를 통해 생성한 객체이고, 클래스로부터 객체를 만드는 과정을 인스턴스화라고 한다.

클래스 선언

클래스 선언은 객체 생성을 위해 설계도를 작성하는 작업이다. 객체를 생성하는 방법(생성자), 가져야 할 데이터(필드), 객체의 동작(메소드)을 정의한다.

[클래스명.java]
public class 클래스명 {
}

클래스명은 소스 파일명과 동일하게 작성한다. 첫 문자를 대문자로 하고 캐멀 스타일로 작성하며, 숫자를 포함할 수는 있지만 첫 문자는 숫자가 될 수 없고 특수 문자 중 $, _를 포함할 수 있다.

하나의 소스 파일에 복수 개의 클래스 선언을 포함할 수도 있지만, 소스 파일을 컴파일할 경우 바이트코드 파일은 클래스 선언의 개수만큼 생긴다. 또한 하나의 소스 파일에 여러 개의 클래스 선언을 할 경우 소스 파일명과 동일한 클래스만 공개 클래스로 선언할 수 있다는 것에 주의해야 한다.

객체 생성과 클래스 변수

객체를 생성할 때는 객체 생성 연산자인 new 연산자를 통해 생성한다. new 연산자는 뒤에 오는 생성자를 통해 객체를 생성한 뒤에 객체의 주소를 리턴한다.

클래스의 용도

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

일반적으로 자바 프로그램은 하나의 실행 클래스와 여러 개의 라이브러리 클래스로 구성된다.


클래스의 구성 멤버

public class ClassName {
  // 필드 선언
  int fieldName;

  // 생성자 선언
  ClassName() { ... };

  // 메소드 선언
  int methodName() { ... };
}

클래스는 생성자, 필드, 메소드로 구성된다.

필드 선언과 사용

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

필드 선언

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

변수 선언 방법과 동일하지만 반드시 클래스 블록에서 선언되어야 한다. 타입은 필드에 저장할 데이터의 종류를 결정한다. 필드명은 첫 문자를 소문자로 하고 캐멀 스타일로 작성하는 것이 관례이다. 초기값은 생략이 가능하지만 제공하지 않을 경우 기본값으로 초기화된다.

필드 사용

참조변수명.필드명;

필드를 사용하는 것은 필드값을 읽고 변경하는 것을 말한다. 객체 내부에서 필드를 사용할 경우에는 필드명으로 접근하면 되지만, 외부 객체에서 필드에 접근할 경우 참조 변수와 도트(.) 연산자를 이용해야 한다.

도트(.) 연산자는 객체 접근 연산자로, 객체가 가지고 있는 필드나 메소드에 접근하고자 할 때 참조 변수 뒤에 붙인다.

생성자 선언과 호출

생성자(constructor)는 new 연산자로 객체를 생성할 때 객체의 초기화 역할을 담당한다. 리턴 타입이 없고 이름은 클래스명과 동일하다.

객체 초기화는 필드 초기화를 하거나 메소드를 호출해서 객체를 사용할 준비를 하는 것을 말한다. 생성자가 성공적으로 실행이 끝나면 new 연산자는 객체의 주소를 리턴한다.

모든 클래스는 반드시 하나 이상의 생성자를 가져야 한다. 클래스에 생성자 선언이 없을 경우 컴파일러는 아래와 같이 매개변수가 없는 기본 생성자(Default Constructor)를 추가한다.

[public] 클래스() {}

접근 제한자는 Default(접근 제한자 없음)나 public만 가능하고, 클래스의 접근 제한자와 같게 생성된다.

생성자 선언

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

생성자는 메소드와 비슷한 모양을 가지지만 리턴 타입이 없고 클래스 이름과 동일하다. 매개변수는 new 연산자로 생성자를 호출할 경우 매개값을 생성자 블록 내부로 전달하는 역할을 한다.

필드 초기화

클래스를 통해 생성한 객체가 모든 동일한 값의 필드를 가져야 할 경우를 제외하고, 객체마다 다른 값의 필드를 가진다면 생성자에서 필드를 초기화하는 것이 좋다.

생성자의 매개변수명은 주로 필드명과 동일한 이름을 사용한다. 이 때 필드와 매개변수를 구분하기 위해 필드명 앞에 this 키워드를 사용한다. this는 현재 객체를 의미한다.

생성자 오버로딩

Car(String model) {
  this(model, "은색", 250);  // 다른 생성자 호출
  // 추가적인 실행문 작성 가능
}

Car(String model, String color) {
  this(model, color, 250);  // 다른 생성자 호출
}

// 중복되는 코드
Car(String model, String color, int maxSpeed) {
  this.model = model;
  this.color = color;
  this.maxSpeed = maxSpeed;
}

생성자 오버로딩은 매개변수를 달리하는 생성자를 여러 개 선언하는 것을 의미한다. 매개값으로 객체의 필드를 다양하게 초기화하기 위해 필요하다.

생성자 오버로딩 시 매개변수의 타입, 개수, 순서가 달라야 한다. 만약 타입, 개수, 순서가 같고 매개변수명만 다른 경우는 오버로딩으로 처리되지 않는다.

생성자 오버로딩이 많아질 경우, 중복된 코드를 줄이기 위해 다른 생성자를 호출할 수 있다.

메소드 선언과 호출

메소드(method)는 객체가 수행할 동작을 의미한다. 객체와 객체 간의 상호작용을 위해 호출된다. 따라서 메소드 선언은 객체의 동작을 실행 블록으로 정의하는 것이고, 호출은 실행 블록을 실제로 실행하는 것이다.

메소드 선언

리턴타입 메소드명 (매개변수, ... ) {
  실행코드;
}

리턴타입은 메소드가 실행된 후 호출한 곳으로 전달하는 결과값의 타입을 말한다. 만약 리턴값이 없다면 void로 작성한다. 메소드명은 첫 문자를 소문자로 시작하고 캐멀 스타일로 작성한다. 매개변수는 메소드를 호출할 때 전달할 매개값을 받기 위해 사용하는 것으로, 없다면 생략할 수 있다.

return 문

메소드의 실행을 강제로 종료하고 호출한 곳을 돌아간다는 의미이다. 메소드 선언에 리턴 타입이 있을 경우 반드시 return 문 뒤에 리턴값을 추가로 지정해야 한다. 만약 return 문 이후에 실행문을 작성할 경우, 해당 실행문은 실행되지 않기 때문에 'Unreachable code'라는 컴파일 에러가 발생한다.

메소드 호출

메소드는 객체의 동작이므로 객체가 존재하지 않는다면, 즉 객체가 생성되지 않았다면 메소드를 호출할 수 없다.

객체 내부에서 호출할 경우에는 메소드명으로 호출할 수 있지만, 객체 외부에서 호출할 경우 참조변수와 도트(.) 연산자를 통해 호출해야 한다.

메소드가 매개변수를 가지고 있을 경우에는 호출할 때 매개변수의 타입과 수에 맞춰 매개값을 제공해야 한다.

가변길이 매개변수

int sum(int ... values) {
}

가변길이 매개변수는 메소드를 호출할 때 매개변수의 개수에 상관없이 매개값을 주기 위해 사용한다. 매개값을 쉼표로 구분하여 개수와 상관없이 제공할 수 있다.

가변길이 매개변수를 사용하는 메소드를 호출할 때, 매개값들은 자동으로 배열 항목으로 변환되어 메소드 내부에서 사용한다. 그렇기 때문에 매개값을 제공할 때 배열로 제공하는 것도 가능하다.

메소드 오버로딩

메소드 오버로딩은 메소드 이름은 같되 매개변수의 타입, 개수, 순서가 다른 메소드를 여러 개 선언하는 것을 말한다. 다양한 매개값을 처리하기 위해 사용한다.

가장 대표적인 메소드 오버로딩의 예시로는 System.out.println() 메소드가 있다. 매개값으로 어떤 타입의 값을 주어도, 주어진 매개값의 타입에 따라 오버로딩된 println() 메소드 중 하나를 실행하는 것이다.


인스턴스 멤버와 정적 멤버

필드와 멤버는 선언 방법에 따라 인스턴스 멤버와 정적 멤버로 분류한다.

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

인스턴스 멤버

public class Car {
  // 인스턴스 필드 선언
  int gas;

  // 인스턴스 메소드 선언
  void setSpeed(int speed) { ... };
}

자바에서 필드는 각 객체마다 따로 존재한다. 하지만 메소드는 각 객체마다 따로 존재하지 않고 메소드 영역에 저장되어 공유된다. 객체마다 따로 저장한다면 중복 저장으로 인해 메모리 효율이 떨어지기 때문이다.

정적 멤버

자바는 클래스 로더(loader)를 이용해서 클래스를 메소드 영역에 저장하고 사용한다. 정적 멤버는 인스턴스를 따로 생성하지 않아도, 클래스가 메모리에 올라갈 때 자동적으로 생성된다.

정적 멤버 선언

public class 클래스 {
  // 정적 필드 선언
  static 타입 필드 [ = 초기값 ];

  // 정적 메소드
  static 리턴타입 메소드 ( 매개변수, ... ) { ... };
}

정적 멤버로 선언할 경우 static 키워드를 붙여야 한다. 객체마다 따라 가지고 있을 필요성이 없는, 공용적인 필드는 메모리 효율을 위해 정적 필드로 선언하는 것이 좋다. 또한 인스턴스를 이용하지 않는 메소드도 정적 메소드로 선언하는 것이 좋다.

정적 멤버 사용

클래스가 메모리로 로딩되면 바로 정적 멤버를 사용할 수 있다. 클래스 이름과 함께 도트(.) 연산자로 접근하는 것이 정석이지만, 객체 참조 변수로도 접근이 가능하다.

정적 블록

정직 필드는 객체 생성 없이도 사용이 가능하기 때문에 생성자에서 초기화 작업을 하지 않는다. 같은 이유로 인스턴스 필드, 인스턴스 메소드, this 키워드 사용이 불가능하다. 만약 인스턴스 멤버를 사용하고 싶다면 객체 생성 후 참조 변수로 접근해야 한다.

정적 필드는 필드 선언과 동시에 초기값을 주는 것이 일반적이다. 하지만 만약 복잡한 초기화 작업이 필요하다면 정적 블록(static block)을 이용할 수 있다.

static {
  ...
}

정적 블록은 클래스가 메모리로 로딩될 때 자동으로 실행되고, 여러 개가 있을 경우 선언된 순서대로 실행된다.


final 필드와 상수

인스턴스 필드와 정적 필드는 언제든지 값을 변경할 수 있다. 만약 값을 변경하는 것을 막고 읽기만 허용하고 싶다면 final 필드와 상수를 선언해야 한다.

final 필드 선언

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

초기값이 저장되면 그것이 최종적인 값이 되어 프로그램 실행 도중 값을 변경할 수 없다.

final 필드에 초기값을 주는 방법은 두 가지가 있다. 하나는 필드 선언 시에 초기값을 대입하는 방법이고, 하나는 생성자에서 초기값을 대입하는 방법이다. 만약 초기값을 주지 않고 final 필드를 남겨두면 컴파일 에러가 발생한다.

상수 선언

상수(constant)는 불변의 값을 저장하는 필드이다. 객체마다 저장할 필요가 없고, 여러 개의 값을 가져도 안되기 때문에 static이면서 final인 특성을 가진다.

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

초기값은 선언 시에 주는 것이 일반적이지만 복잡한 초기화 과정이 필요한 경우 정적 블록에서 초기화할 수 있다. 상수 이름은 모두 대문자로 작성하고 여러 단어일 경우 언더바(_)로 연결한다. 상수에 접근하는 방식은 정적 필드에 접근하는 방식과 같다.

패키지

패키지는(package)는 자바에서 단순히 디렉토리의 의미가 아니다. 클래스의 일부분이자 클래스를 식별하는 용도로 사용된다. 주로 개발 회사 도메인 이름의 역순으로 생성되고, 상위 패키지와 하위 패키지를 도트로 구분한다.

클래스를 식별하는 용도로 사용되기 때문에 클래스의 전체 이름에 포함된다. 또한 패키지에 속한 바이트코드 파일은 다른 디렉토리로 이동시킬 수 없다.

패키지 선언

package 상위패키지.하위패키지;

public class 클래스명 { ... }

패키지 디렉토리는 클래스를 컴파일하는 과정에서 자동으로 생성된다. 패키지 선언을 할 때는 package 키워드와 함께 패키지 이름을 기술한다. 항상 소스 파일의 최상단에 위치해야 한다.

패키지 이름은 모두 소문자로 작성하는 것이 관례이고, 회사 도메인의 역순으로 작성하고 마지막에는 프로젝트 이름을 붙이는 것이 일반적이다.

import 문

같은 패키지에 있는 클래스는 조건 없이 사용이 가능하지만 다른 패키지에 있는 클래스를 사용할 경우에는 import 문을 사용하여 어떤 패키지의 클래스를 사용하는지 명시해야 한다.

import 문은 패키지 선언과 클래스 선언 사이에 작성하고, import 키워드 뒤에 사용하고자 하는 클래스의 전체 이름을 기술한다. 동일한 패키지에 포함된 다수의 클래스를 사용할 경우에는 클래스 이름을 생략하고 *을 사용할 수 있다. 하지만 하위 패키지는 포함하지 않기 때문에 하위 패키지에 있는 클래스를 사용할 경우 별도로 기술해야 한다.

import com.hankook.*;
import com.hankook.project.*;

서로 다른 패키지의 동일한 이름을 가진 클래스를 동시에 사용할 경우, 클래스의 전체 이름을 사용하여 컴파일러에게 어떤 패키지의 클래스를 사용하는지 알려줘야 한다. 전체 이름을 사용할 경우에는 import 문은 사용하지 않아도 된다.

접근 제한자(Access Modifier)

중요한 필드나 메소드가 외부로 노출되지 않도록 접근을 제한하는 역할을 한다. 객체의 무결성(결점이 없는 성질)을 유지하기 위해 사용한다.

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

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

Getter와 Setter

객체 지향 프로그래밍에서는 외부에서 직접적으로 필드에 접근하는 것을 막고, 메소드를 통해 필드에 접근하는 것을 선호한다.

Getter는 외부에서 객체의 필드를 읽을 때 필드값이 객체 외부에서 사용하기 부적절한 경우 적절한 값으로 변환하여 리턴하는 메소드이다.

Setter는 객체의 무결성 유지를 위해 필드에 저장할 데이터를 검증해 유효한 값만 저장되도록 하는 메소드이다.

기본적으로 Getter는 get + 필드이름(첫 글자 대문자), Setter는 set + 필드이름(첫 글자 대문자)으로 짓지만 필드 타입이 boolean일 경우에는 Getter의 이름을 is + 필드이름(첫 글자 대문자)으로 짓는 것이 관례이다.

싱글톤 패턴

public class 클래스 {
  // private 접근 권한을 갖는 정적 필드 선언과 초기화
  private static 클래스 singleton = new 클래스();  // 자신의 타입으로 정적 필드 선언하고 객체 생성해서 초기화

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

  // public 접근 권한을 갖는 정적 메소드 선언
  public static 클래스 getInstance() {  // 외부에서 객체를 얻는 유일한 방법
    return singleton;
  }
}

애플리케이션 전체에서 단 한 개의 객체만 생성해서 사용하고 싶을 때 적용한다. 생성자를 private 접근 제한자로 선언하여 외부에서 new 연산자로 생성자를 호출할 수 없도록 한다.

생성자를 외부에서 호출할 수 없기 때문에 외부에서 객체를 생성할 수 없고, 싱글톤 패턴이 제공하는 정적 메소드를 통해 간접적으로 객체를 얻어야 한다.


"한빛 미디어 출판 도서, 이것이 자바다"를 읽고 학습한 내용을 토대로 작성되었습니다.

0개의 댓글