본문 바로가기
언어/JAVA

[클래스] 인스턴스 멤버, 정적 멤버, final, package, Getter Setter, 싱글톤 패턴

by 코딩맛집 2023. 1. 27.

6.9 인스턴스 멤버

필드와 메소드는 선언 방법에 따라 인스턴스 멤버와 정적 멤버로 분류할 수 있다.

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

 

인스턴스 멤버 선언 및 사용

public class Car {
    //인스턴스 필드 선언
    int gas;
    
    //인스턴스 메소드 선언
    void seetSpeed(int speed) { }
}

gas 필드와 setSpeed() 메소드는 인스턴스 멤버이기 때문에 외부 클래스에서 사용하기 위해서는 Car 객체를 먼저 생성하고 참조 변수로 접근해서 사용해야 한다.

gas필드는 객체마다 따로 존재하며, setSpeed() 메소드는 각 객체마다 존재하지 않고 메소드 영역에 저장되고 공유된다.

메소드는 코드의 덩어리이므로 객체마다 저장한다면 중복 저장으로 인해 메모리 효율이 떨어진다. 따라서 메소드 코드는 메소드 영역에 두되 공유해서 사용하고, 이때 객체 없이는 사용하지 못하도록 제한을 걸어둔 것이다.

 

this 키워드

객체 내부에서는 인스턴스 멤버에 접근하기 위해 this를 사용할 수 있다. 객체는 자신을 'this'라고 한다. 생성자와 메소드의 매개변수명이 인스턴스 멤버인 필드명과 동일한 경우, 인스턴스 필드임을 강조하고자 할 때 this를 주로 사용한다.

 

6.10 정적 멤버

자바는 클래스 로더를 이용해서 클래스를 메소드 영역에 저장하고 사용한다. 정적 멤버란 메소드 영역의 클래스에 고정적으로 위치하는 멤버를 말한다. 그렇기 때문에 정적 멤버는 객체를 생성할 필요 없이 클래스를 통해 바로 사용이 가능하다. 

 

정적 멤버 선언

정적 필드와 정적 메소드로 선언하려면 static 키워드를 추가하면 된다. 객체마다 가지고 있을 필요성이 없는 공용적인 필드는 정적 필드로 선언하는 것이 좋다. 

public class 클래스 {
    String color;  //계산기별로 색깔이 다를 수 있다.
    static double pi = 3.14159; //계산기에서 사용하는 파이 값은 동일하다.
    }

인스턴스 필드를 이용하지 않는 메소드는 정적 메소드로 선언하는 것이 좋다. 예를 들어 Calculator의 plus() 메소드는 외부에서 주어진 매개값들을 가지고 처ㅣ하므로 정적 메소드로 선언하는 것이 좋다. 그러나 인스턴스 필드인 color를 변경하는 setColor() 메소드는 인스턴스 메소드로 선언해야 한다.

public class Calculator2 {
    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;} //정적 메소드
}

 

정적 멤버 사용

클래스가 메모리로 로딩되면 정적 멤버를 바로 사용할 수 있는데, 클래스 이름과 함께 도트(.) 연산자로 접근하면 된다. 예를 들어 Calculator 클래스가 다음과 같이 작성되었다면,

public class Calculator {
    static double pi = 3.14159;
    static int plus (int x, int y) {}
    static int minus (int x, int y) {}
    }

정적 필드 pi와 정적 메소드 plus(), minus()는 다음과 같이 사용할 수 있다.

double result1 = 10*10*Calculator.pu;
int result2 = Calculator.plus(10, 5);
int result3 = Calculator.minus(10, 5);

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

Calculator myCalcu = new Calculator();
double result1 = 10*10*myCalcu.pu;
int result2 = myCalcu.plus(10, 5);
int result3 = myCalcu.minus(10, 5);

하지만 정적 요소는 클래스 이름으로 접근하는 것이 정석이다. 이클립스에서는 정적 멤버를 객체 참조 변수로 접근했을 경우, 경고 표시를 낸다.

 

정적블록

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

static double pi = 3.14159;

하지만 복잡한 초기화 작업이 필요하다면 정적 블록을 이용해야 한다. 다음은 정적 블록의 형태를 보여준다.

static {

}

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

 

인스턴스 멤버 사용 불가

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

public class ClassName {
    //인스턴스 필드와 메소드 선언
    int filed1;
    void method1(){ }

    //정적 필드와 메소드 선언
    static int field2;
    static void method2() { }

    //정적 블록 선언
    static {
        //컴파일 에러
//        field1 = 10;
//        method1();
        field2 = 10;
        method2();
    }

    //정적 메소드 선언
    static void Method3() {
        //컴파일 에러
//        this.filed1 = 10;
//        this.method1();

        field2 = 10;
        method2();
    }
}

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

static void Method3() {
    //객체 생성
    ClassName obj = new ClassName();
    
    //인스턴스 멤버 사용
    obj.field1 = 10;
    obj.method1();
}

main() 메소드도 동일한 규칙이 적용된다. main() 메소드도 정적 메소드이므로 객체 생성 없이 인스턴스 필드와 인스턴스 메서도를 main() 메소드에서 바로 사용할 수 없다. 따라서 다음과 같이 작성하면 컴파일 에러가 발생한다.

public class Car {
    //인스턴스 필드 선언
    int speed;
    
    //인스턴스 메소드 선언
    void run() { }
    
    //메인 메소드 선언
    public static void main(String[] args ){
        speed = 60; //컴파일에러
        run(); //컴파일 에러
        }
    }

main() 메소드를 올바르게 수정하면 다음과 같다.

public static void main(String[] args)  {

    Car myCar = new Car();
    myCar.speed = 60;
    myCar.run();
    }

 

6.11 final 필드와 상수

인스턴스 필드와 정적 필드는 언제든지 값을 변경할 수 있다. 그러나 경우에 따라서는 값을 변경하는 것을 막고 읽기만 허용해야 할 떄가 있다. 이떄 final 필드와 상수를 선언해서 사용한다.

 

final 필드 선언

final 필드는 초기값이 저장되면 이것이 최종적인 값이 되어서 프로그램 실행 도중에 수정할 수 없게 된다.

final 타입 필드 = 초기값;

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

 

1. 필드 선언 시에 초기값 대입

2. 생성자에서 초기값 대입

 

고정된 값이라면 필드 선언 시에 주는 것이 제일 간단하다. 하지만 복잡한 초기화 코드가 필요하거나 객체 생성 시에 외부에서 전달된 값으로 초기화한다면 생성자에서 해야 한다. 이 두 방법을 사용하지 않고 final 필드를 그대로 남겨두면 컴파일 에러가 발생한다.

 

상수 선언

불변의 값을 저장하는 필드를 자바에서는 상수라고 부른다.

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

static final 타입 상수 = 초기값;

초기값은 선언 시에 주는 것이 일반적이지만, 복잡한 초기화가 필요할 경우에는 정적 블록에서 초기화할 수도 있다.

static final 타입 상수;
static {
    상수 = 초기값;
    }

상수 이름은 모두 대문자로 작성하는 것이 관례이다. 만약 서로 다른 단어가 혼합된 이름이라면 언더바로 단어들을 연결한다. 또한 상수는 정적 필드이므로 클래스로 접근해서 읽을 수 있다.

 

6.12 패키지

자바의 패키지는 단순히 디렉토리만을 의미하지 않는다. 패키지는 클래스의 일부분이며, 클래스를 식별하는 요도로 사용된다.

패키지는 주로 개발 회사의 도메인 이름의 역순으로 만든다. 예를 들어 mycompany.com  회사의 패키지는 com.mycompany로, yourcompany.com 회사의 패키지는 com.yourcompany로 만든다. 이렇게 하면 두 회사에서 개발한 Car 클래스가 있을 경우 다음과 같이 관리할 수 있다.

패키지는 상위 패키지와 하위 패키지를 도트로 구분한다. 도트는 물리적으로 하위 디렉토리임을 뜻한다. com은 상위 디렉토리, mycompany는 하위 디렉토리이다.

 

패키지는 클래스를 식별하는 용도로 사용되기 때문에 클래스의 전체 이름에 포함된다. 예를 들어 Car 클래스가 com.mycompany 패키지에 속해 있다면 Car 클래스의 전체 이름은 com.mycompany.Car가 된다.

 

패키지에 속한 바이트코드 파일(~.class)은 따로 뗴어내어 다른 디렉토리로 이동할 수 없다. 예를 들어 Car 클래스가 com.mycompany 패키지에 소속되어 있다면 다른 디렉토리에 Car.class를 옮겨 저장할 경우 Car 클래스를 사용할 수 없게 된다.

 

패키지 선언

패키지 디렉토리는 클래스를 컴파일하는 과정에서 자동으로 생성된다. 컴파일러는 클래스의 패키지 선언을 보고 디렉토리를 자동 생성시킨다. 패키지 선언은 package 키워드와 함께 패키지 이름을 기술한 것으로, 항상 소스 파일 최상단에 위치해야 한다. 패키지 이름은 모두 소문자로 작성하는 것이 관례이다. 그리고 패키지 이름이 서로 중복되지 않도록 회사 도메인 이름의 역순으로 작성하고, 마지막에는 프로젝트 이름을 붙여주는 것이 일반적이다.

 

소스 파일(~.java)이 저장되면 이클립스는 자동으로 컴파일해서 ~/bin 디렉토리에 패키지 디렉토리와 함께 바이트코드 파일을 생성한다. 만약 패키지 선언이 없다면 이클립스는 클래스를 default package에 포함시킨다. 패키지가 없다는 뜻이다. 그러나 어떤 프로젝트든 패키지 없이 클래스를 만드는 경우는 드물다.

 

import 문

같은 패키지에 있는 클래스는 아무런 조건 없이 사용할 수 있지만, 다른 패키지에 있는 클래스를 사용하려면 import문을 이용해서 어떤 패키지의 클래스를 사용하는지 명시해야 한다.

package com.mycompany;

import com.hankook.Tire;

public class Car {

    //필드 선언 시 com.hankook.Tire 클래스를 사용
    Tire tire = new Tire();
    }

import 문의 위치와 선언한 규칙을 기억하자. 만약 동일한 패키지에 포함된 다수의 클래스를 사용해야 한다면 클래스 이름을 생략하고 *를 사용할 수 있다.

import문은 하위 패키지를 포함하지 않는다. 따라서 com.hankook 패키지에 있는 클래스도 사용해야 하고 com.hankook.project 패키지에 있는 클래스도 사용해야 한다면 다음과 같이 두 개의 import문이 필요하다.

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

만약 서로 다른 패키지에 동일한 클래스 이름이 존재한다고 가정하면 클래스의 전체 이름을 사용해서 정확히 어떤 패키지의 클래스를 사용하는지 알려야 한다. 클래스 전체 이름을 사용할 경우 import문은 필요 없다.

com.hankook.Tire tire = new com.hankook.Tire();

(import문 자동 추가 기능 p260)

 

6.13 접근 제한자

경우에 따라서는 객체의 필드를 외부에서 변경하거나 메소드를 호출할 수 없도록 막아야 할 필요가 있다. 중요한 필드와 메소드가 외부로 노출되지 않도록 해 객체의 무결성을 유지하기 위해서이다. 자바는 이러한 기능을 구현하기 위해 접근 제한자를 사용한다. 접근 제한자는 public, protected, private의 세 가지 종류가 있다.

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

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

 

클래스의 접근 제한

클래스를 어디에서나 사용할 수 있는 것은 아니다. 클래스가 어떤 접근 제한을 갖느냐에 따라 사용 가능 여부가 결정된다.

클래스를 선언할 때 public 접근 제한자를 생략했다면 클래스는 default 접근 제한을 가진다.

 

생성자의 접근 제한

객체를 생성하기 위해 생성자를 어디에서나 호출할 수 있는 것은 아니다. 생성자가 어떤 접근 제한을 갖느냐에 따라 호출 가능 여부가 결정된다. 생성자는 public, default, private 접근 제한을 가질 수 있다.

접근 제한자 생성자 설명    
public 클래스{ } 모든 패키지에서 생성자를 호출할 수 있다.
=모든 패키지에서 객체를 생성할 수 있다.
  클래스{ } 같은 패키지에서만 생성자를 호출할 수 있다.
= 같은 패키지에서만 객체를 생성할 수 있다.
private 클래스{ } 클래스 내부에서만 생성자를 호출할 수 있다.
= 클래스 내부에서만 객체를 생성할 수 있다.

 

#A.java

package ch06.sec13.exam02.package1;

public class A {
    //필드 선언
    A a1 = new A(true);
    A a2 = new A(1);
    A a3 = new A("문자열");
    
    //public 접근 제한 생성자 선언
    public A(boolean b) {
    }
    
    //default 접근 제한 생성자 선언
    A(int b) {
    }
    
    //private 접근 제한 생성자 선언
    private A(String s) {
    }
  }
#B.java

package ch06.sec13.exam02.package1; //패키지 동일

public class B {
    //필드 선언
    A a1 = new A(true);
    A a2 = new A(1);
    A a3 = new A("문자열"); //x private 생성자 접근 불가(컴파일 에러)
    }
#C.java

package ch06.sec13.exam02.package2; //패키지 다름
import ch06.sec13.exam02.package1.*;

public class C {
    //필드 선언
    A a1 = new A(true);
    A a2 = new A(1); //x defualt 생성자 접근 불가(컴파일 에러)
    A a3 = new A("문자열"); //x private 생성자 접근 불가(컴파일 에러)
    }

 

필드와 메소드의 접근 제한

필드와 메소드도 어떤 접근 제한을 갖느냐에 따라 호출 여부가 결정된다. 필드와 메소드는 public, default, private 접근 제한을 가질 수 있다.

접근 제한자 생성자 설명    
public 필드
메소드{ }
모든 패키지에서 필드를 읽고 변경할 수 있다.
모든 패키지에서 메소드를 호출할 수 있다.
  필드
메소드{ }
같은 패키지에서 필드를 읽고 변경할 수 있다.
같은 패키지에서 메소드를 호출할 수 있다.
private 필드
메소드{ }
클래스 내부에서만 필드를 읽고 변경할 수 있다.
클래스 내부에서만 메소드를 호출할 수 있다.

 

6.14 Gettr와 Setter

객체 지향 프로그래밍에서는 직접적인 외부에서의 필드 접근을 막고 대신 메소드를 통해 필드에 접근하는 것을 선호한다. 그 이유는 메소드는 데이터를 검증해서 유효한 값만 필드에 저장할 수 있기 때문이다. 이러한 역할을 하는 메소드가 Setter이다.

다음 코드를 보자. speed 필드는 pricate 접근 제한을 가지므로 외부에서 접근하지 못한다. speed 필드를 변경하기 위해서는 Setter인 setSpeed() 메소드를 이용해야 한다. setSpeed() 메소드는 외부에서 제공된 변경값을 if문으로 검증하는데, 음수일 경우 0을 필드값으로 저장한다.

private double speed;

public coid setSpeed(double speed){
    if(speed < 0){
        this.speed = 0;
        return;
    } else {
        this.speed = speed;
    }
}

외부에서 객체의 필드를 읽을 떄에도 메소드가 필요한 경우가 있다. 필드값이 객체 외부에서 사용하기에 부적절한 경우, 메소드로 적절한 값으로 변환해서 리턴할 수 있기 때문이다. 이러한 역할을 하는 메소드가 Getter이다.

다음 예시를 보자. speed 필드는 private 접근 제한을 가지므로 외부에서 읽지 못한다. speed 필드를 읽기 위해서는 Getter인 getSpeed() 메소드를 이용해야 한다. getSpeed() 메소드는 마일 단위의 필드 값을 km 단위로 변환해서 외부로 리턴한다.

private double speed; //speed의 단위는 마일

public double getSpeed() {
    double km = speed*1.6; //마일을 km 단위로 환산 후 외부로 리턴
    return km;
    }

다음은 Getter와 Setter의 기본 작성 방법을 보여준다. 필요에 따라 Getter에서 변환 코드를 작성하거나 Setter에서 검증 코드를 작성할 수 있다.

private 타입 fieldName;

//Getter
public 타입 getFieldName() {
    return fieldName;
}
//Setter
public void setFieldName(타입 fieldName) {
    this.fieldName = fieldName;
}

필드 타입이 boolean일 경우에는 Getter는 is로 시작하는 것이 관례이다. 

6.15 싱클톤 패턴

애플리케이션 전체에서 단 한 개의 객체만 생성해서 사용하고 싶다면 싱글톤 패턴을 적용할 수 있다. 싱글톤 패턴의 핵심은 생성자를 private 접근 제한해서 외부에서 new 연산자로 생성자를 호출할 수 없도록 막는 것이다. 생성자를 호출할 수 없으니 외부에서 마음대로 객체를 생성하는 것이 불가능해진다. 대신 싱글톤 패턴이 제공하는 정적 메소드를 통해 간접적으로 객체를 얻을 수 있다.

public class 클래스 {
    //private 접근 권한을 갖는 정적 필드 선언과 초기화
    private static 클래스 singleton = new 클래스(); // 1
    
    //private 접근 권한을 갖는 생성자 선언
    private zmffotm() {}
    
    //public 접근 권한을 갖는 정적 메소드 선언
    public static 클래스 getInstance() {  // 2 
        return singleton;
        }
    }

1에서는 자신의 타입으로 정적 필드를 선언하고 미리 객체를 생성해서 초기화시킨다. 그리고 private 접근 제한자를 붙여 외부에서 정적 필드값을 변경하지 못하도록 막는다.

2에서는 정적 필드값을 리턴하는 getInstance() 정적 메소드를 public으로 선언한다. 외부에서 객체를 얻는 유일한 방법은 getInstance() 메소드를 호출하는 것이다. getInstance()메소드라 리턴하는 객체는 정적 필드가 참조하는 싱클톤 객체이다. 따라서 아래 코드에서 변수1과 변수2가 참조하는 객체는 동일한 객체가 된다.

클래스 변수1 = 클래서.getInstance();
클래스 변수2 = 클래스.getInstance();

'언어 > JAVA' 카테고리의 다른 글

[인터페이스]  (0) 2023.01.30
[상속]  (0) 2023.01.28
[클래스]  (0) 2023.01.26
[객체 지향 프로그래밍]  (0) 2023.01.25
[참조 타입] 배열(Array)  (2) 2023.01.24