본문 바로가기
카테고리 없음

클래스6 (접근 제한자, 싱글톤, final)

by 낭만 코딩 2024. 8. 26.

1. 접근 제한자

2. 싱글톤

3. final

 

 

 

1. 접근 제한자

접근 제한자(access modifier), 말 그대로 접근 제한하는 용도로 사용됩니다. 자바 어플리케이션은 main() 메서드가 없는, 직접 실행하는 클래스가 아닌, 다른 곳에서 사용되는 클래스로 만들어지는 경우가 많은데, 이런 클래스는 라이브러리 형태로 사용되는 것입니다.

클래스를 설계할 때 다른 아무 곳에서나 접근해서 사용할 수 있게 하거나 지정한 곳에서만 접근해서 사용 가능할 수 있도록 설계할 수 있습니다. 접근 제한자의 종류는 public protected, (default), private 네가지 종류가 있습니다. 여기서 default는 실제 default 라는 키워드가 아니라 키워드가 없는 것을 말합니다.

 

 

이 그림에서 보듯이 public이 가장 넓은 범위를 가지고 있고, 순서대로 public -> protected -> (default) -> private 순으로 좁아집니다.

먼저 public은 단어 그대로 공용의 의미입니다. 아무나, 어디에서도 사용 가능하다는 의미가 됩니다. protected는 같은 패키지이거나, 뒤에서 배울 상속관계의 클래스에서 사용 가능하고, default는 같은 패키지에서만 사용 가능합니다. 가장 안쪽의 private은 같은 클래스 내부에서만 사용 가능합니다

 

접근 제한자 접근 제한 범위
모든 클래스 상속관계 동일패키지 동일클래스
public O O O O
protected X O O O
(default) X X O O
private X X X O

 

가장 많이 사용하는 접근 제한자는 public, private 입. 우선 이 두개를 정확히 기억해 두세요.

public은 공용이라서 아무데서나 다 사용할 수 있고, private은 반대로 개인적인 것이기 때문에 해당 클래스 내부에서만 사용할 수 있습니다.

defaultprotected는 공통적으로 같은 패키지인 경우 사용 가능한데, protected만 뒤에서 배울 상속관계(정확히 얘기하면 자식 클래스)에서 사용 가능합니다.

접근제한자를 사용할 수 있는 곳은 클래스, 메서드/생성자, 변수에 사용할 수 있는데, 이중(중첩) 클래스는 publicdefault만 사용 가능합니다. privateprotected는 사용할 수 없습니다.

 

package test;

public class ClassA {
	
	public static void main(String[] args) {
	
		ClassB cb = new ClassB();
		cb.print();
		
	}
	
	public void print() {
		System.out.println("여기는 ClassA");
	}
	
}

class ClassB {
	void print() {
		System.out.println("여기는 ClassB");
	}
}

 

실행 결과

여기는 ClassB

 

ClassA앞에는 public이라는 접근제한자가 명시되어 있는데, ClassB는 접근제한자가 생략되어 있습니다. default라는 얘기입니다. 그래서 같은 패키지에 있는 ClassAmain() 메서드에서 ClassB를 import 없이 사용할 수 있습니다. 이번엔 다른 패키지에 클래스를 생성해 보겠습니다.

package test.test2;

import test.*;

public class ClassC {
	public static void main(String[] args) {
		
		ClassA ca = new ClassA();
		ca.print();
		
		//ClassB cb = new ClassB(); // 접근제한자 때문에 에러
		
	}
}

 

ClassC가 있는 패키지는 test.test2 입니. ClassB 클래스와는 다른 패키지에 있기 때문에 test 패키지에 있는 default 접근 제한자인 ClassB를 사용하려고하면 에러가 발생합니다.

 

클래스 외에도 생성자나 메서드, 변수에도 4가지 접근 제한자를 모두 사용할 수 있습니다. 특히 생성자에 접근 제한자를 활용하면 객체 생성을 제어할 수 있게 됩니다.

생성자를 정의하지 않아 자동으로 생성된 기본 생성자는 해당 클래스의 접근 제한자와 동일하게 생성됩니다.

자주 사용되는 publicprivate 접근제한자만 잘 기억하고 나머지는 코딩하다 보면 자연스럽게 외워 질테니, 너무 어렵게 생각하지 마세요. 초반 실수는 이클립스가 친절하게 알려줄테니...

 

 

2. 싱글톤

위에서 배운 접근제한자 중 private 접근 제한자를 활용해서 싱글톤(singleton)을 배워보겠습니다. 자바 프로그램에서 객체의 무분별한 생성을 막기 위해 싱글톤을 사용하는데, 객체를 생성할 때 new 연산자를 이용해 생성할 때 실행된 횟수만큼 새로운 객체가 생성되기 때문에 시간이 흐르면 흐를수록 메모리가 부족해지거나, 시스템이 느려지는 현상이 생길 수 있습니다. 이럴 때 특정 클래스는 하나의 객체만 생성되도록 프로그래밍을 하는 기법이 있는데, 이를 싱글톤 기법이라 하고, 이 객체를 싱글톤 객체라고 부릅니다.

new 연산자는 생성자를 통해 객체를 생성하므로, 생성자에 private 접근 제한자를 붙여 외부 클래스에서는 실행할 수 없도록 제한을 둡니다. private은 클래스 내부에서만 실행할 수 있으므로, static 변수로 객체를 생성해 두는 것입니다. 그리고 static 메서드를 통해 이 객체를 리턴하도록 정의하면 단 하나의 객체만 생성해서 사용하게 됩니다. 싱글톤 객체를 생성하는 예제를 확인해 보겠습니다.

 

public class Singleton {

	// static 변수
	private static Singleton instance = new Singleton();
	
	// 생성자에 private 접근 제한자
	private Singleton() {
		System.out.println("객체 생성");
	}
	
	// static 메서드
	public static Singleton getInstance() {
		System.out.println("객체 리턴");
		return instance;
	}
}

 

private 접근 제한자를 갖는 static 변수로 instance 라는 변수에 객체를 생성하고, new Singleton() 은 생성자로 접근제한자를 private으로 선언했기 때문에, 해당 클래스 내부에서만 실행이 가능합니다.

static 변수는 클래스가 로드될 때 초기 한번만 실행되기 때문에, 이미 객체는 생성되어 공유할 수 있는 변수가 되어 있는 것입니다. 객체가 생성되는 시점을 확인하기 위해 객체 생성이라고 출력하였고, getInsatnce() 메서드는 public 접근제한자로 외부에서는 이 메서드를 통해서만 이미 만들어 놓은 객체를 가져갈 수 있도록 정의했습니다. 이 메서드 역시 static 메서드로 객체 생성 없이 직접 호출할 수 있는 메서드입니다. 이 클래스를 사용하는 SingletonMain 클래스를 만들어 보겠습니다.

 

public class SingletonMain {

	public static void main(String[] args) {
		//Singleton s = new Singleton(); // 에러 발생
		
		Singleton s1 = Singleton.getInstance();
		Singleton s2 = Singleton.getInstance();
		Singleton s3 = Singleton.getInstance();

	}

}

 

실행 결과

객체 생성
객체 리턴
객체 리턴
객체 리턴

 

SingletonMain 클래스의 main() 메서드에서 Singleton 클래스의 객체를 생성하려고 하면 에러가 나게 됩니다. new 연산자 뒤의 생성자가 private 접근 제한자이기 때문이죠. 그래서 static 메서드인 getInstance() 메서드를 통해 s1, s2, s3 객체에 각각 대입했다. 실행 결과를 보면 객체 생성이라는 문자열은 생성자에서 출력하는데, 초기 한번만 출력됐고 나머지는 객체 리턴만 출력 됐다. s1, s2, s3는 변수명은 다르지만 모든 같은 객체이다. 참조 자료형이기 때문입니다.

 

객체를 비교하는 코드를 추가해 보겠습니다.

public class SingletonMain {

	public static void main(String[] args) {
		//Singleton s = new Singleton(); // 에러 발생
		
		Singleton s1 = Singleton.getInstance();
		Singleton s2 = Singleton.getInstance();
		Singleton s3 = Singleton.getInstance();
		
		System.out.println(s1 == s2);
		System.out.println(s2 == s3);

	}

}

 

실행 결과

객체 생성
객체 리턴
객체 리턴
객체 리턴
true
true

 

s1과 s2와 s3가 모두 같습니다. 다른 말로 모두 같은 주소를 참조하고 있습니다. 이렇듯 싱글톤은 객체를 한번만 생성하고 메서드를 통해 이미 생성된 객체를 가져다 쓰는 용도로만 사용합니다.

 

 

하지만, 싱글톤이 만능은 아닙니다. 객체가 무분별하게 생성되지 않고, 메모리도 절약할 수 있어 좋은게 아닐까? 생각될 수 있는데, 모든 객체를 싱글톤으로 개발하지는 않습니다. 예를 들어, 회원 객체 Member 라는 클래스로 각 회원의 객체를 생성해야 하는데, 회원 100명을 하나의 객체에 담을 수는 없습니다. 100개의 객체에 담아야 모두 다른 회원의 값이 저장되기 때문입니다. 싱글톤은 보통 기능적인 요소가 많은 클래스, 유틸 등에 많이 사용됩니다.

 

 

3. final

final은 마지막이라는 뜻이 있습니다. 의미 그대로 final 변수는 마지막 변수라는 뜻입니다. 다른 값으로 변경 할 수 없다는 뜻이죠. final 키워드는 클래스, 메서드, 변수 앞에 붙일 수 있습니다. 클래스와 메서드 앞에 붙은 final 키워드는 뒤에서 다시 자세히 다룰테니 여기서는 간단히 어떤 종류들이 있는지만 살펴보고 넘어가도록 하겠습니다.

 

final 클래스

마지막 클래스로 상속이 불가능한 클래스입니다. 부모한테 자식이 물려 받을 수 있는 상속관계가 있는데, final 클래스는 더이상 자식에게 상속시킬 수 없게 됩니다. 대표적인 final 클래스로는 String, Math가 있습니다. 그래서 String 클래스는 상속받을 수 없습니다.

 

final 메서드

마지막 메서드로 재정의가 불가능한(오버라이딩) 메서드입니다. 상속관계에서 자식은 부모의 메서드를 재정의(변경)할 수 있는데 final이 붙은 메서드는 변경할 수 없게 됩니다.

 

final 변수, 상수

마지막 변수, 값이 변경되지 않는 변수를 말합니다. final 변수는 초기값을 지정한 후 변경 할 수 없는데 생성자에서 초기화한번은 가능합니다.

 

public class FinalEx {

	public static void main(String[] args) {
		
		Final f = new Final();
		//f.number = 200; // 에러	
	}
	
}

class Final {
	final int number;
	
	Final() {
		number = 100;
	}
}

 

Final 클래스는 final 키워드로 선언되어 있는데, 생성자에서 number = 100으로 초기화 해주고 있습니다. f.number에 200이라는 값을 대입하려고 하면 에러가 납니다. final 키워드로 선언된 변수는 더 이상 값을 변경할 수 없기 때문입니다. final은 마지막이라는 뜻인데, 생성자를 통해 초기화를 가능하게 해준 이유는 객체마다 다른 값을 가질 수 있도록 하기 위함입니다.

 

상수(static final)

상수란 항상 같은 수입니다. 원주율처럼 이미 고정된 값은 변하지도 않고, 변하면 안되는 값입니다. 이를 상수라고 부르며, 모든 곳에서 공유되야하는 값입니다. 그래서 공유되기 위해 static을 붙이고, 마지막이기 때문에 final을 붙여 static final을 함께 씁니다. 인스턴스 블록에서도 초기값을 지정할 수는 있지만, 보통은 선언할 때 초기값을 지정해 주는 경우가 많습니다. 그리고 상수는 관례적으로 변수명을 모두 대문자로 사용합니다. 만약 두 단어 이상으로 연결되는 경우 단어와 단어 사이를 _로 구분합니다.

 

public class ConstantSample {
	
	static final double CARD_COMMISSION = 1.5;

	public static void main(String[] args) {
		
		System.out.println("원주율 : "+Math.PI);
		System.out.println("카드 수수료율 : "+CARD_COMMISSION);
		// CARD_COMMISSION = 1.8; // 에러

	}

}

 

실행 결과

원주율 : 3.141592653589793
카드 수수료율 : 1.5

 

static finalCARD_COMMISSION 이라는 필드에 1.5 실수값으로 초기화면 상수를 선언했습니다.

main() 메서드 안에서는 Math라는 클래스의 PI라는 원주율 값이 담긴 상수를 출력해봤는데, Math 클래스도 자바에서 기본제공하는 클래스이지만 상수를 모두 대문자로 정의해둔 것을 알 수 있습니다.

CARD_COMMISSION은 카드 수수료율이 담긴 상수인데, 절대 변하지 않는 값은 아닙니다. 자주 변경되진 않겠지만 원주율처럼 불변의 값이라고는 할 수 없습니다. 카드사의 정책에 따라 가끔 바뀌는 경우가 생기기 때문입니다.

그러면 왜 static final로 선언했을까요. 카드 수수료율은 프로그램 중간 중간에 실수나, 또는 악의에 의해 변경되서는 안되는 값입니다. 어떤 경우라도 현재 프로그램 내에서는 바뀌면 안되는 값이기 때문입니다. 정말 카드사의 정책에 따라 수수료율이 조정이 된다면 개발자가 직접 소스코드로 상수의 초기값을 변경하는 것이 안전할 수 있습니다. 이렇게 절대로 변하지 않는 값이거나, 변하면 안되는 값인 경우 상수(static final)을 사용한다는 점 기억하세요.