백엔드/JAVA

객체 지향 프로그래밍 이론(3) 및 주사위 게임 예제 실습

두개의 문 2023. 5. 9. 17:46
메소드 오버로딩

 
( 어제 배운 내용에 이어서 )
메소드 오버로딩 ( overloading )
 : 한 클래스 내에 같은 이름의 메서드를 여러 개 정의하는 것
   → 목적 ) 매개값을 다양하게 받아 처리하기 위함 
 · 조건
 ① 메서드 이름이 같아야 함
 ② 매개변수의 개수 또는 타입이 달라야 함
 ③ 반환 타입은 관계없음
· 오버로딩된 메소드를 호출할 경우, JVM이 매개값의 타입을 보고 호출할 메서드 선택
  JVM은 반환타입을 보고 메소드를 선택하지 않기 때문에 반환타입은 고려 사항이 아님
 
 
< 데이터 클래스 > 

package extend_class;

public class CheckReturnClass {
	
	public void add(int x, int y) {
		System.out.printf("\n%d + %d = %d", x, y, (x+y));
		return;
	}
    
	public void add(double x, double y) {
		System.out.printf("\n%.1f + %.1f = %.1f", x, y, (x+y));
		return;
	}
}

❶ 두 번째 add메서드에서 매개변수 타입이 double → 형식지시자 : %f
❷ 메서드의 반환타입이 없는 'void'이지만 return문이 있는 이유
    : 리턴값 지정을 위한 것이 아니라 메소드 실행을 강제 종료시키는 역할
 
 
< 실행 클래스 >

package extend_class;

public class CheckReturnApp {

	public static void main(String[] args) {
		// 1. 객체 생성
		CheckReturnClass crc = new CheckReturnClass();
		// 2. 참조변수를 이용해 메소드 호출
		crc.add(100, 100);
		crc.add(100.0, 200.0);
	}
}

 
< 출력 결과 >

100 + 100 = 200
100.0 + 200.0 = 300.0

 
 


메서드 작성 시 고려 사항

 
- 메소드 설계 시 최소한의 기능을 가능한 간단하게 작성
  → 메소드 내에서 다른 메소드 호출하기
- 반면에 변수의 이름은 직관적으로 변수명만 보고 판단할 수 있도록 길게 작성
 
① 메소드 설정 시, 전체적으로 어떤 메소드를 작성할지 글로 구현해보기
② 각각의 메소드의 선언부 먼저 작성 ( 틀 먼저 전부 만들기 )
③ 그 다음 차례대로 구현부 작성
 
 


인스턴스 멤버와 정적 멤버

 
- 자바는 클래스 멤버를 인스턴스 멤버와 정적 멤버로 구분해서 선언함
 
▶ 인스턴스 멤버 : 객체마다 가지고 있는 멤버로,
                          객체를 생성한 후 사용할 수 있는 필드와 메소드를 의미 ( 인스턴스 필드, 인스턴스 메소드 )
  · this : 객체 내부에서 인스턴스 멤버에 접근하기 위해 사용 → 객체 자신을 this라고 함
             주로 생성자와 메소드의 매개변수 이름이 필드와 동일한 경우 
             ( = 인스턴스 변수와 로컬변수 이름이 동일한 경우 )
             인스턴스 멤버인 필드임을 명시하고자 할 때 사용

Car ( String model ) {
        this.model = model ;
     인스턴스 변수    로컬변수
}

 
▶ 정적 멤버 : 클래스에 위치시키고 객체들이 공유하는 멤버로,
                       객체를 생성하지 않고 사용할 수 있는 필드와 메소드를 의미 ( 정적 필드, 정적 메소드 )
 ① 정적 멤버 선언 : 'static' 키워드 추가

public class 클래스 {
        // 정적 필드
        static 타입 필드 ;
        // 정적 메소드
        static 리턴타입 메소드 ( 매개변수 선언 ) { ... }
}

  → 정적 필드와 정적 메소드는 클래스에 고정된 멤버이므로 클래스 로더가 클래스 ( 바이트 코드 )를 로딩해서 메소드 메모리 영역에 적재          할 때 클래스별로 관리됨 
      ∴ 클래스의 로딩이 끝나면 바로 사용 가능
 
 ② 정적 멤버 사용
  -  클래스가 메모리로 로딩되면 바로 사용가능 → 클래스이름.필드 또는 클래스이름.메소드 형식으로 접근
 
 ③ 정적 메소드 선언 시 주의할 점
  - 정적 메소드의 경우, 객체 생성 없이 바로 사용 가능 → 그러므로 정적 메소드 선언 시, 인스턴스 필드나 인스턴스 메소드 사용할 수 없음
    ∵ 인스턴스 필드나 인스턴스 메소드의 경우, 객체를 생성한 후에 사용 가능하기 때문
  - 정적 메소드에서 객체 자신의 참조인 this 키워드도 사용 불가
  - 정적 메소드에서 인스턴스 멤버를 사용하고 싶다면, 객체를 먼저 생성한 후 참조변수를 통해 접근해야 함
 
 


그럼 언제 'static'을 붙여야 할까?

 
- 멤버변수란?
  · 클래스 영역에 선언된 변수를 의미
  · 변수 선언 시, 변수명 앞에 'static'이 붙으면, 클래스변수 ( static 변수 )
                                           'static'이 붙지 않으면, 인스턴스 변수
- 클래스 설계 시, 멤버변수 중 모든 인스턴스에서 공통으로 사용하는 것에 'static' 붙임
  → 생성되는 각각의 인스턴스의 경우, 서로 독립적인 인스턴스이므로 인스턴스 변수가 서로 다른 값을 유지함
      반면, 각각의 인스턴스에서 같은 값이 유지되어야 하는 변수에 'static'을 붙여 클래스 변수로 정의
- 클래스 변수 ( static 변수 )는 외부 클래스에서 인스턴스를 생성하지 않아도 사용 가능 
  → 클래스 변수는 클래스가 메모리에 올라갈 때 이미 자동적으로 생성되기 때문
- 클래스 메서드 ( static 메서드 )는 인스턴스 변수를 사용할 수 없음
  → 인스턴스 변수는 인스턴스 생성 후, 인스턴스가 반드시 존재해야만 사용 가능
      클래스 메서드는 인스턴스 생성없이 호출 가능하므로, 클래스 메서드가 호출되었을 때 인스턴스가 존재하지 않을 수도 있음
      ∴ 클래스 메서드에서 인스턴스 변수의 사용 불가
- 메서드 내에서 인스턴스 변수를 사용하지 않는다면, 'static'을 붙이는 것을 고려해보기
 
▶ 최종 정리
  ① 클래스의 멤버변수 중 모든 인스턴스에 공통된 값을 유지해야하는 것이 있는지 살펴보고 있으면, static 붙이기
  ② 작성한 메서드 중에서 인스턴스 변수나 인스턴스 메서드를 사용하지 않는 메서드에 static을 붙이는 것을 고려해보자
 
 


예시 - 주사위 객체

 
< 데이터 클래스 >

package game;

import java.util.Scanner;

public class Dice {
	// 1. 멤버 변수 설정
    		//        색상 : 주사위를 색상으로 구분하려는 목적
   		//   선택된 숫자 : 주사위 던졌을때 보이는 윗면의 숫자를 의미
	private String color;			
	private int selectedNumber; 
	
	// 2. 메소드
		// roll()   : 주사위가 굴러가서 나오는 난수 발생 장치 -> 리턴타입 : void
   		// choice() : 주사위가 생성한 난수값 확인 		   -> 리턴타입 : int
	
	
	void roll(){
		selectedNumber = (int)(Math.random()*6) + 1;	
	}
	int choice() {
		return selectedNumber;
	}
	
	// 확인 메서드 추가 ( 로그 )
	@Override
	public String toString() {
		String msg = 
			"주사위의 눈금 : " + selectedNumber ;
		return msg;
	}
	

❶ 변수는 항상 접근제어자 private로 지정 → 외부로부터 데이터 보호
❷ 메소드의 반환타입 및 매개변수 고려해보기
    - roll( ) 메서드 : 반환타입 필요없고, 난수 발생시킬 계획이므로 매개변수 필요 없음
    - choice ( ) 메서드 : roll( ) 메서드 결과값인 selectedNumber를 반환하므로 반환타입은 int, 매개변수는 필요없음
❸ 자바에서의 로그
   - 모든 객체는 로그 기능을 가져야 함 → 그 로그 기능에 해당하는 메소드가 toString( ) 메소드
   - toString( ) 메서드 : 모든 클래스 상속계층도의 최상위에 있는 조상클래스인 Object클래스가 가진 주요 메서드 중 하나
      → 인스턴스에 대한 정보를 문자열 ( String )로 제공할 목적으로 정의
          ∴ 인스턴스 변수에 저장된 값들을 문자열로 표현함
      → 이 메소드를 개선해봄 ( 개선 : 기본기능을 확장시킨다는 의미 )
          '@Override' 라는 Annotation '@'을 붙이고 시작
      ※ 접근제어자 public을 지정하지 않으면 오류 발생 
         : Cannot reduce the visibility of the inherited method from Object
          상속을 받아서 재정의를 하는 경우에 가시범위를 줄이는 것은 허용이 되지않습니다.
          → 해결 방법 : 누구나 접근할 수 있도록  toString ( ) 메서드의 접근제어자를 private로 지정
 


< 실행 클래스 >

package game;

public class DiceApp {

	public static void main(String[] args) {
		// 1. 객체 생성
		Dice dice = new Dice();
		
		// 2-1. dice 객체의 메소드 호출로 주사위 수 출력 
		dice.roll();
		int selectedDiceNumber = dice.choice();
		System.out.println(
				"선택된 주사위 눈금 : " + selectedDiceNumber );
		
		// 2-2. dice 객체의 로그 기능을 이용해 주사위 수 출력
		System.out.println(dice.toString());
	}

❶ 객체 생성 후, 참조변수를 이용해 인스턴스 멤버 사용 가능
❷-❶ dice 객체의 메소드 호출로 주사위 수 출력
❷-❷ dice 객체의 로그 기능을 이용해 주사위 수 출력
 


< 출력 결과 >

선택된 주사위 눈금 : 1 ( 난수 )
주사위의 눈금 : 1 ( 난수 )

 
 
< 실행 클래스 >
▷ 4개의 주사위 객체 생성해보자

		// 3. 4개의 주사위 객체 생성
			// 2번 주사위 객체 : dice2
		Dice dice2 = new Dice();
		dice2.roll();
		int selectedDiceNumber2 = dice2.choice();
			// 	2번 주사위의 수 출력 
		System.out.println(
				"선택된 주사위 눈금 : " + selectedDiceNumber2);
		System.out.println(dice2.toString());
		
			// 3번 주사위 객체 : dice3
		Dice dice3 = new Dice();
		dice3.roll();
		int selectedDiceNumber3 = dice3.choice();
			// 	3번 주사위의 수 출력 
		System.out.println(
				"선택된 주사위 눈금 : " + selectedDiceNumber3);
		System.out.println(dice3.toString());
		
			// 4번 주사위 객체 : dice4
		Dice dice4 = new Dice();
		dice4.roll();
		int selectedDiceNumber4 = dice4.choice();
			// 	4번 주사위의 수 출력 
		System.out.println(
				"선택된 주사위 눈금 : " + selectedDiceNumber4);
		System.out.println(dice4.toString());

→ 4개의 객체를 각각 생성한 후, 참조변수를 통해 메소드 호출 및 출력
 
< 출력 결과>

선택된 주사위 눈금 : 6
주사위의 눈금 : 6
선택된 주사위 눈금 : 6
주사위의 눈금 : 6
선택된 주사위 눈금 : 4
주사위의 눈금 : 4
선택된 주사위 눈금 : 6
주사위의 눈금 : 6

 

< 개선 사항 >
① 동일한 코드의 중복이 심각함 →코드의 중복 제거하자
    ∴ 배열 활용하자
② 선택된 주사위를 식별할 수 없음
   ∴ 처음에 선언한 변수 color를 이용해 객체를 식별해보자 → 생성자 이용

☞ 우선, 현재까지 만든 main 메소드 내 코드들을 playDiceIndividual( ) 메서드 내에 따로 분리해놓자
public static void main(String[] args) {
		playDiceIndividual();
	}	
    
public static void playDiceIndividual(){
		// 1. 객체 생성
		Dice dice = new Dice();
		
		// 2-1. dice 객체의 메소드 호출로 주사위 수 출력 
		dice.roll();
		int selectedDiceNumber = dice.choice();
		System.out.println(
				"선택된 주사위 눈금 : " + selectedDiceNumber );
		
		// 2-2. dice 객체의 로그 기능을 이용해 주사위 수 출력
		System.out.println(dice.toString());
		
		// 3. 4개의 주사위 객체 생성	
		//	   2번 주사위 객체 : dice2
		Dice dice2 = new Dice(); 
		dice2.roll();
		int selectedDiceNumber2 = dice2.choice();
		System.out.println(
				"선택된 주사위 눈금 : " + selectedDiceNumber2);
		System.out.println(dice2.toString());
		
		//     3번 주사위 객체 : dice3
		Dicegame dice3 = new Dicegame(); 
		dice3.roll();
		int selectedDiceNumber3 = dice3.choice();
		System.out.println(
				"선택된 주사위 눈금 : " + selectedDiceNumber3);
		System.out.println(dice3.toString());
		
		//     4번 주사위 객체 : dice4
		Dicegame dice4 = new Dicegame(); 
		dice4.roll();
		int selectedDiceNumber4 = dice4.choice();
		System.out.println(
				"선택된 주사위 눈금 : " + selectedDiceNumber4);
		System.out.println(dice4.toString());	
}​


    이 때, 주의 사항! playDiceIndividual( ) 메서드의 선언부에 'static' 반드시 포함되어야함
    포함시키지 않을 경우, 오류 발생 
    ( Cannot make a static reference to the non-static method playDiceIndividual() from the type DiceApp )
    해결 방법으로 Change 'playDiceIndividual()' to 'static'을 제안해줌
    ∵ main 메서드가 'static' 메서드이므로 'static'이 붙지 않은 메서드를 호출할 수 없음

 


< 실행 클래스 >
▷ 개선사항 1 ) 코드 중복 제거를 위한 배열 활용해 선택된 값 출력해보자

public static void playDiceByArray() {
		
		// System.out.println("^^");  -> main메서드에서 호출되는지 체크
		
		// 4. 배열 활용
		Dice [] dices = new Dice[4];
		
		// 	  주사위 객체 추가 루프
		for ( int i = 0 ; i < dices.length ; i++ ) {
			dices[i] = new Dice();
			dices[i].roll();
			System.out.printf("[%d] 선택된 값 : %d\n", i, dices[i].choice());
		}
}

❶ 객체 배열을 이용해 코드 중복 제거
    Dice [ ] dices = new Dice[4] ;  : 객체를 다루기 위한 참조변수들이 만들어진 것일뿐, 아직 객체 생성된 것 아님
    dices[0] = new Dice();              : 실제 객체를 생성해서 객체 배열의 각 요소에 저장하는 과정
    dices[1] = new Dice(); 
    dices[2] = new Dice(); 
    dices[3] = new Dice(); 
    → 이 과정을 for문으로 진행한 것
    ※ 객체 배열만 생성 후, 실제 객체 생성해서 배열의 각 요소에 저장하는 과정 잊지 말자
❷ roll( ) 메서드를 이용해 난수 발생시킨 후, choice( ) 메서드를 이용해 출력
 


< 출력 결과 >

[0] 선택된 값 : 1
[1] 선택된 값 : 1
[2] 선택된 값 : 2
[3] 선택된 값 : 1

 


▷ 개선사항 2) 인덱스 번호 대신 색상으로 식별해보자
< 데이터 클래스 >

	// 5-1. 인터페이스 생성해서 외부에서 멤버변수에 접근할 수 있는 통로 생성 
	public String getColor() {
		return color;
	}
	public void setColor(String color) {
		this.color = color;
	}
	public int getSelectedNumber() {
		return selectedNumber;
	}
	public void setSelectedNumber(int selectedNumber) {
		this.selectedNumber = selectedNumber;
	}
	
	// 5-2. 외부에서 주사위 색상을 선택하는 경우를 시뮬레이션(=모델링)
	//    하는 경우이므로 = 접근제어자 : public
	public void selectColor() {
		Scanner sc = new Scanner(System.in);
		// 안내문 출력
		System.out.println(
				"주사위 색상을 선택해주세요 ( 빨강, 파랑, 노랑, 초록 ) :> ");
		this.color = sc.nextLine();
	}

➎-➊ 속성 ( property ) 함수 이용해서 외부에서 간접적으로 변수에 접근하기
    - 생성시키는 방법 : 오른쪽 클릭 → Sources → Generate Getters and Setters → 속성함수 만들 변수 선택 및 위치 선택 → Finish

➎-➋ 외부에서 주사위 색상을 선택하도록 Scanner클래스 이용 ( import문 삽입 )
          → 입력된 색상을 필드 color에 저장
 
 
< 실행 클래스 >

	public static void playDiceByArray() {
		
		// 4. 배열 활용
		Dicegame [] dices = new Dicegame[4];
		
		// 주사위 객체 추가 루프
		for ( int i = 0 ; i < dices.length ; i++ ) {
			dices[i] = new Dice();
			// 5. 인덱스 번호 대신 색상으로 식별 
			dices[i].selectColor();
			dices[i].roll();
			System.out.printf(
					"[%s] %s\n",
					dices[i].getColor(),
					dices[i].toString());
			
			//System.out.printf("[%d] 선택된 값 : %d\n", i, dices[i].choice());
		}
	}

위의 Scanner 클래스로부터 입력받은 색상을 필드 color에 저장(➎-❷)했으므로, get( ) 메소드를 이용해 데이터의 값을 읽어 출력하자
 
 
< 실행 클래스 >

	public static void main(String[] args) {
		// playDiceIndividual();
		playDiceByArray();
		
		// 6. 프로그램 종료문 출력
		System.out.println(" *** END *** ");
	}

❻ 종료 안내문 출력
 
< 출력 결과 >

 

< 추가로 고려해볼 점 >
 - 지금까지 던져진 주사위는 총 몇 개일까?
   ( 배열의 개수, for문의 카운터 변수로 접근하는 방법 제외 )
   → count 변수는 전체 객체에서 공유하므로, 변수 선언 시 변수이름 앞에 'static'을 붙여 static 변수로 지정해 출력!

 
▷ 추가 ) 지금까지 던져진 주사위는 총 몇 개일까?
< 실행 클래스 >

public class Dicegame {
	// 1. 멤버 변수 설정
	private String color;
	private int selectedNumber;
	
	// 7-1. count 변수를 static 변수로 지정
	private static int count;
	Dicegame(){
		count++;
	}
    
    @Override
	public String toString() {
		// 7-2. 출력할 내용 변경
		String msg =
				"생성된 주사위 객체의 수 : " + count + "\n" +
				"주사위의 눈금 : " + selectedNumber;
		return msg;
	}

❼-❶ count 변수는 전체 객체에서 공유하는 변수이므로, 'static' 변수로 지정
          < 실행 클래스 >에서 객체가 생성될 때마다 new연산자가 기본생성자를 호출함
          → 기본생성자가 호출될 때마다 변수 count의 값을 1씩 증가시키는 방식으로 현재 생성된 객체의 수를 계산 
❼-❷ toString( ) 메소드에 출력할 내용 추가
 
< 출력 결과 >