본문 바로가기

컴퓨터언어/JAVA(자바)

[JAVA강좌] 25강 static과 싱글톤패턴

728x90
반응형

 

 

 

 

 

 

 

 

- 멤버변수로 선언한 것은 인스턴스가 새롭게 생성될 때마다 새롭게 사용을 합니다.

- 하지만 고유한 값(유일한 식별자)여야 하는 멤버변수도 있습니다. 

- 예를 들어, 주민번호나 학생번호 이런 것들입니다. 

- 주민번호를 나와 같은 번호를 가진 다른 사람이 있으면 안 되겠죠?

- 이때 static이라는 키워드를 붙여 모든 인스턴스가 공통으로 사용하게 처리해줘야 합니다. 

- 이번 강좌에서는 static을 이용해서 멤버변수나 메서드가 고유한 값을 갖도록 처리하는 방법을 공부해 보겠습니다.

 

 

 

 

 

 

 

 

 

 

1. Static 변수


- static 변수란 다른 용어로 '정적 변수'라고도 합니다. 

- 다른 멤버변수처럼 클래스 내부에 쓰지만 앞에 static 키워드를 붙입니다. 

//일반 멤버변수
(접근제어자) 자료타입 멤버변수명;

//static 멤버변수
(접근제어자) static 자료타입 멤버변수명;

 

- 클래스 내부에 선언하지만, 다른 멤버 변수처럼 인스턴스가 생성될 때마다 새로 생성되는 변수가 아닙니다. 

- static 변수는 프로그램이 실행되어 메모리에 올라갔을 때, 딱 한번 메모리 공간이 할당됩니다.

- 그리고 모든 인스턴스가 그 값을 공유하게 됩니다. 

- 이런 이유 때문에 static 변수를 클래스에 기반한 변수라고 해서 '클래스 변수'라고도 합니다.

 

 

 

 

 

1) static으로 공유할 때 발생되는 문제

- 그럼 static 키워드로 멤버변수를 처리하면 값을 어떻게 유지하는지 보도록 하겠습니다. 

- 학생 클래스를 생성하고 인스턴스들을 만들어 보겠습니다. 

- 패키지(ch17_static)과 클래스(Student.java)를 추가해 주세요. 

- Student.java에는 main () 함수를 실행시키지 않습니다. 

 

 

## Student.java

package ch17_static;

public class Student {
	public static int serialNum = 1000;			//고유한 값으로 유지할 시리얼넘버
	public int studentId;					//학생ID
	public String studentName;				//학생이름
	
	//작성하진 않았지만 기본 생성자가 실행됨
	
	//학생이름을 가져오는 메서드(게터)
	public String getStudentName() {
		return studentName;
	}
	
	//학생이름을 변경하는 메서드(세터)
	public void setStudentName(String studentName) {
		this.studentName = studentName;
	}
}

 

- 멤버변수는 시리얼넘버, 학생ID, 학생이름으로 3개를 선언했습니다.

- 그중 serialNum을 static으로 고정해서 모든 인스턴스가 공유할 수 있게 처리했습니다. 

- 생성자는 작성하지 않으면 기본 생성자가 실행됩니다.

- studentName을 이용한 게터/세터를 생성했습니다.

- 직접 작성해도 되고, 이클립스 기능을 이용해도 됩니다.

 

 

- 그럼 StudentMain.java에 인스턴스를 생성해 보겠습니다. 

- 이때는 main() 함수를 체크해서 생성해 주세요.

 

## StudentMain.java 코드

package ch17_static;

public class StudentMain {

	public static void main(String[] args) {
		//학생1 생성
		Student st01 = new Student();
		st01.setStudentName("김석진"); //이름 변경
		System.out.println(st01.getStudentName() + "의 시리얼넘버 : " + st01.serialNum);
		st01.serialNum++; //static변수인 serialNum에 1증가
		
		//학생2 생성
		Student st02 = new Student();
		st02.setStudentName("김태형"); //이름 변경
		System.out.println(st02.getStudentName() + "의 시리얼넘버 : " + st02.serialNum);
		
		//학생1을 다시 출력
		System.out.println(st01.getStudentName() + "의 시리얼넘버 : " + st01.serialNum);
	}
}

- 학생1을 생성 후 이름을 변경하고, serialNum를 출력했습니다.

- 그 후에 serialNum의 값을 1을 증가했습니다.

- 학생2를 생성 후 이름을 변경하고, serialNum를 출력했습니다.

- 그 후에 학생1의 serialNum를 다시 출력해 보겠습니다.

 

 

## StudentMain.java 결과

- 학생1의 첫 번째 시리얼 넘버는 1000인데, 마지막은 1001로 변경되어 있습니다.

- 이것은 모든 인스턴스가 serialNum을 공유하기 때문입니다. 

- serialNum은 고정돼서 학생 한 명 추가될 때마다 1이 증가되어야 하는 것은 맞지만, 바뀔 때마다 인스턴스의 고유한 값이 변경되면 안 됩니다.

 

 

 

 


 

 

 

 

 

2) 생성자를 통해 자동 변경 처리

- static으로 고정시킨 값은 모든 인스턴스가 공유하기 때문에 바로 사용하면 안 됩니다.

- 그래서 멤버변수를 처음부터 studentId를 따로 선언해 두었습니다.

- serialNum은 매번 1씩 증가하지만 그 값을 studentId에 담아 각각의 인스턴스는 고유한 학생ID를 갖도록 처리하겠습니다. 

 

 

## Student.java

package ch17_static;

public class Student {
	public static int serialNum = 1000;
	public int studentId;
	public String studentName;
	
	//serialNum을 바꾸고 studentId에 담는 생성자 생성
	//생성자가 생성될 때마다 serialNum이 1증가
	public Student() {
		serialNum++;
		studentId = serialNum;
	}
	
	public String getStudentName() {
		return studentName;
	}
	
	public void setStudentName(String studentName) {
		this.studentName = studentName;
	}
}

- 추가된 생성자 부분에만 주석을 달았습니다.

- 생성자가 생성될 때마다 자동으로 serialNum이 1 증가됩니다.

- 그리고 증가된 serialNum을 studentId 담아 해당 인스턴스만 사용하도록 처리합니다. 

 

 

## StudentMain.java 코드

package ch17_static;

public class StudentMain {

	public static void main(String[] args) {		
		//학생1 생성
		Student st01 = new Student();
		st01.setStudentName("김석진");
		System.out.println("현재 serialNum : " + st01.serialNum);
		System.out.println(st01.getStudentName() + "의 학생ID : " + st01.studentId);
		
		//학생2 생성
		Student st02 = new Student();
		st02.setStudentName("김태형"); 
		System.out.println("현재 serialNum : " + st02.serialNum);
		System.out.println(st02.getStudentName() + "의 학생ID : " + st02.studentId);
		
		//학생1을 다시 출력
		System.out.println("현재 serialNum : " + st01.serialNum);
		System.out.println(st01.getStudentName() + "의 학생ID : " + st01.studentId);
	}
}

- 이번에는 먼저 serialNum을 출력합니다.

- 그리고 아래는 studentId를 출력해 보겠습니다. 

 

 

## StudentMain.java 결과

- 그럼 마지막 serialNum은 여전히 바뀌어 있습니다.

- 하지만 studentId에 담은 값은 변경되지 않고 담겨 있는 것을 확인할 수 있습니다.

- 이렇게 고유한 값을 유지할 때 static변수를 사용하면 좋습니다. 

 

 

 


 

 

 

 

3) 클래스 변수

- 위의 코드들이 틀린 건 아니지만, 노란색으로 경고가 뜹니다.

- 왜냐면 인스턴스명을 쓰고 serialNum값을 가져왔기 때문입니다.

- 틀린 건 아니지만, static으로 선언한 클래스 변수는 인스턴스 생성과는 별개이므로 먼저 생성입니다.

- 그래서 클래스 변수라고 부릅니다. 

- 클래스명.멤버변수로 교체를 해보겠습니다. 

 

## StudentMain.java 코드

package ch17_static;

public class StudentMain {

	public static void main(String[] args) {		
		//학생1 생성
		Student st01 = new Student();
		st01.setStudentName("김석진");
		System.out.println("현재 serialNum : " + Student.serialNum);
		System.out.println(st01.getStudentName() + "의 학생ID : " + st01.studentId);
		
		//학생2 생성
		Student st02 = new Student();
		st02.setStudentName("김태형"); 
		System.out.println("현재 serialNum : " + Student.serialNum);
		System.out.println(st02.getStudentName() + "의 학생ID : " + st02.studentId);
		
		//학생1을 다시 출력
		System.out.println("현재 serialNum : " + Student.serialNum);
		System.out.println(st01.getStudentName() + "의 학생ID : " + st01.studentId);
	}
}

 

- 그럼 경고도 사라진 것을 확인할 수 있습니다.

- 결과는 같으니까 따로 보진 않겠습니다. 

- static 변수, 정적변수, 클래스 변수 모두 자바에서는 static을 앞에 붙인 변수라고 생각하면 됩니다. 

 

 

 

 

 

 

 

 

 

 

 

728x90
반응형

 

 

 

 

 

 

 

 

 

 

 

 

 

2. Static 메서드


- 일반 멤버변수를 위한 메서드가 존재하듯이, static 변수를 위한 메서드도 있습니다.

- 이런 메서드들을 static 메서드 혹은 클래스 메서드라고 부릅니다. 

 

 

 

 

1) 클래스 메서드 처리

- 그럼 위에서 한 예제에서 serialNum을 외부클래스에서 직접 참조하지 못하도록 private로 처리하겠습니다.

- private로 처리된 멤버변수의 값을 가져오거나 변경하려면 게터와 세터를 만들어줘야 합니다. 

 

## Student.java

package ch17_static;

public class Student {
	private static int serialNum = 1000;
	public int studentId;
	public String studentName;
	
	//serialNum을 바꾸고 studentId에 담는 생성자 생성
	//생성자가 생성될 때마다 serialNum이 1증가
	public Student() {
		serialNum++;
		studentId = serialNum;
	}
	
	public String getStudentName() {
		return studentName;
	}
	
	public void setStudentName(String studentName) {
		this.studentName = studentName;
	}

	public static int getSerialNum() {
		return serialNum;
	}

	public static void setSerialNum(int serialNum) {
		Student.serialNum = serialNum;
	}
}

 

- 변경한 것만 체크해서 보겠습니다.

- 이클립스에서 마우스 우클릭 - source를 통해 게터세터를 만들어도 static은 자동으로 붙습니다.

- 그럼 serialNum은 private 해졌기 때문에 외부 클래스인 StudentMain에서 가져다 쓸 수 없기 때문에 벌써 옆의 파일이 에러가 난 것을 확인할 수 있습니다. 

- 그래서 게터와 세터를 처리한 것이므로, 게터를 통해서 가져오면 됩니다.

 

 

## StudentMain.java 코드

package ch17_static;

public class StudentMain {

	public static void main(String[] args) {		
		//학생1 생성
		Student st01 = new Student();
		st01.setStudentName("김석진");
		System.out.println("현재 serialNum : " + Student.getSerialNum());
		System.out.println(st01.getStudentName() + "의 학생ID : " + st01.studentId);
		
		//학생2 생성
		Student st02 = new Student();
		st02.setStudentName("김태형"); 
		System.out.println("현재 serialNum : " + Student.getSerialNum());
		System.out.println(st02.getStudentName() + "의 학생ID : " + st02.studentId);
		
		//학생1을 다시 출력
		System.out.println("현재 serialNum : " + Student.getSerialNum());
		System.out.println(st01.getStudentName() + "의 학생ID : " + st01.studentId);
	}
}

 

- 변경한 것만 체크해서 보겠습니다.

- 그럼 에러가 사라지는 것을 확인할 수 있습니다. 

 

 

 


 

 

 

 

2) 클래스 메서드 내부에 멤버변수 사용X

- 클래스 메서드, 즉 static으로 만든 메서드 내부에는 다른 멤버변수를 가져올 수 없습니다.

- 이번에는 그냥 제가 이미지로만 보여드리겠습니다.

- 다른 멤버변수를 클래스 메서드에 담았더니, 에러가 뜹니다.

- 클래스 변수나 메서드들은 인스턴스가 생성되기 전에 생성이 되기 때문에 클래스 내부에서 선언한 멤버변수를 읽을 수 없습니다. 

- 그래서 변수의 유효 범위를 아는 것도 중요한 공부가 됩니다. 

 

 

 


 

 

 

 

3) 변수의 유효범위

- 지금까지 변수는 세 가지 종류를 배웠습니다. 

  1. 지역변수(로컬변수) : 함수나 메서드 안에서만 사용할 수 있는 변수
  2. 멤버변수(인스턴스변수) : 클래스 안에 작성해서 인스턴스가 사용하는 변수
  3. static변수(클래스변수) : 여러 인스턴스가 공통적으로 사용할 수 있는 변수 

## 변수에 따른 용도

변수 유형 선언 위치 사용 범위 메모리 생성과 소멸
지역 변수
(로컬 변수)
함수 내부에 선언 함수 내부에서만 사용 스택 함수가 호출될 때 생성되고 함수가 끝나면 소멸
멤버 변수
(인스턴스 변수)
클래스 멤버 변수로 선언 클래스 내부에서 사용하고 private이 아니면 참조 변수로 다른 클래스에서 사용가능 인스턴스가 생성될 때 힙에 생성되고, 가비지 컬렉터가 메모리를 수거할 때 소멸함
static 변수
(클래스 변수)
static 예약어를 사용해여 클래스 내부에 선언 클래스 내부에서 사용하고 private이 아이면 클래스이름으로 다른 클래스에서 사용가능 데이터영역 프로그램이 처음 시작할 때 상수와 함께 데이터 영역에 생성되고 프로그램이 끝나고 메모리를 해제할 때 소멸됨

 

 

 

 

 

 

 

 

 

 

 

 

 

 

3. Static 응용 - 싱글톤 패턴


- 프로그램을 구현하다 보면 인스턴스가 여러 개가 필요한 경우도 있고, 한 번만 필요한 경우도 있습니다.

- 객체지향 프로그램에서 인스턴스를 한 개만 생성하는 디자인 패턴을 싱글톤 패턴(singleton pattern)이라고 합니다.

- 디자인패턴이란 프로그램을 어떻게 구현해야 더 유연하고 재활용성이 높은 프로그램을 만들 수 있는지 정리한 내용입니다.

- 우리가 지금 사용할 싱글톤 패턴은 static을 응용하여 프로그램 전반에서 사용하는 인스턴스를 하나만 구현하는 방식입니다.

- 실무에서 많이 사용하는 패턴이므로 잘 익히 두면 좋습니다. 

- 그럼 회사가 있고, 직원들의 데이터를 관리하는 프로그램을 만든다고 보도록 하겠습니다.

- 직원 관련 클래스를 만들면 당연히 인스턴스를 여러 개로 만들어야 되겠죠?

- 그럼 직원 클래스는 싱글톤으로 만들지는 않습니다.

- 하지만 회사는 어떤가요? 하나만 생성해야 되겠죠?

- 그래서 회사 객체를 싱글톤 패턴으로 제작해 보겠습니다.

- 물론 여러 자회사를 가지고 있을 수도 있겠지만, 하나의 회사를 갖고 있다는 가정으로 보도록 하겠습니다. 

 

 

 

 

1) 생성자를 private로 제작

- 보통 생성자는 public으로 많이 만듭니다.

- 다른 파일에서 해당 클래스를 인스턴스로 많이 생성할 것이기 때문입니다.

- 하지만 싱글톤은 인스턴스를 하나만 생성한다고 했죠?

- 그렇기 때문에 private으로 제작하겠습니다.

 

- 그리고 보통 클래스에서는 기본생성자가 들어가고, 그것을 사용해도 돼서 안 만드는 경우가 있습니다.

- 하지만 싱글톤 패턴에서는 생성자를 반드시 생성해줘야 합니다. 

- 그럼 예제를 통해서 보도록 하겠습니다. 

 

- 패키지(ch17_static)에 클래스(Company.java)를 main() 없이 생성해 줍니다. 

 

 

## Company.java

package ch17_static;

public class Company {
	//생성자 생성 - 반드시 생성
	private Company() { }
}

 

 

 


 

 

 

 

 

2) 클래스 내부에 static으로 유일한 인스턴스 생성

- private 하게 생성자를 생성했으므로 외부에 인스턴스를 생성할 수 없습니다.

- 그래서 Company클래스 내부에 하나의 인스턴스를 생성합니다.

- 이 인스턴스가 프로그램 전체에서 사용할 유일한 인스턴스가 될 겁니다. 

- 인스턴스 역시 private으로 선언해서, 외부에서 사용하지 못하게 접근 제한을 해줘야 합니다. 

- 잘못해서 외부에서 인스턴스를 생성해서 오류가 발생되는 것을 방지해야 합니다. 

 

 

## Company.java

package ch17_static;

public class Company {
	//유일한 인스턴스 생성 - 앞에 private static 붙여주기
	private static Company company = new Company();
	
	//생성자 생성 - 반드시 생성
	private Company() { }
}

 

 

 

 


 

 

 

 

 

3) 외부 참조 public 메서드 생성

- 인스턴스를 하나를 만들지만, 인스턴스를 외부에서도 사용할 수 있게 해줘야 합니다.

- 그래서 public을 통한 메서드로 인스턴스를 얻을 수 있도록 게터로 처리해 줍니다. 

- 역시 이 게터도 static으로 선언해줘야 합니다. 

- 왜냐하면 메서드는 인스턴스 생성과는 상관없이 호출되어야 하기 때문입니다. 

 

 

## Company.java

package ch17_static;

public class Company {
	//유일한 인스턴스 생성 - 앞에 private static 붙여주기
	private static Company company = new Company();
	
	//생성자 생성 - 반드시 생성
	private Company() { }
	
	//public 게터 메서드 생성
	public static Company getCompany() {
		if(company == null) { //혹시 인스턴스를 생성하지 않았다면
			company = new Company(); //다시 인스턴스를 생성
		} 
		return company; //해당 메서드는 유일한 인스턴스를 반환
	}
}

 

 

 


 

 

 

4) 실제로 구현하는 클래스 파일 생성

- 싱글톤 패턴으로 만든 인스턴스를 불러올 수 있는 파일을 만들겠습니다.

- CompanyMain.java로 main() 함수를 체크해서 생성해 줍니다.

 

 

## CompanyMain.java

package ch17_static;

public class CompanyMain {

	public static void main(String[] args) {
		//Company company00 = new Company(); //인스턴스를 또 생성할 수 없음 - 에러
		
		//인스턴스를 각각의 변수에 담기
		Company company01 = Company.getCompany();
		Company company02 = Company.getCompany();
		
		//하지만 인스턴스는 하나라 같은 주소를 참조하는 것
		System.out.println(company01 == company02); //true
	}

}

 

- 첫 번째 줄은 에러라 앞에 주석을 달았습니다.

- 싱글톤 패턴으로 만들었기 때문에 new 키워드를 사용해 새로운 인스턴스를 만들 수 없습니다.

- 메서드를 통해 인스턴스를 각각의 변수에 담을 수는 있습니다.

- 하지만 비교해 보면 같은 주소를 참조하기 때문에 true의 결과를 받습니다. 

 

 

 

 

728x90
반응형