회고록 블로그

[공부 필기] Java 기본 공부하기 (6) 본문

2. 프로그래밍 언어 공부/Java

[공부 필기] Java 기본 공부하기 (6)

김간장 2021. 9. 15. 15:07

화이자 백신을 맞고 부작용(?)으로 고생, 고생, 생고생을 하다가

드디어 살만해져서 다시 공부하려고 돌아왔다.

 

오태식이 돌아왔구나 (이미지 출처: 영화 해바라기)

 

사실 아직 후유증이 남아있지만, 몸이 불편하다고 공부를 게을리 할 수 없다.


공부 중인 강의 : 윤성우 선생님, 윤성우의 열혈 Java 프로그래밍 강의, https://cafe.naver.com/cstudyjava

 

윤성우의 프로그래밍 스터디그룹 [C/... : 네이버 카페

윤성우의 스터디 공간입니다. C와 JAVA를 공부하시는 분들은 모두 들어오세요. ^^

cafe.naver.com


1. 메소드의 재귀 호출

   ㄱ. 일반적으로 접하는 분야에서 재귀는 잘 사용하지 않으나, 이해 해야하는 학문(?)임

        따라서, 지금 당장은 아니더라도 언젠간은 꼭 이해하는 과정이 필요함

   ㄴ. 재귀 호출은 굉장히 '수학적'으로 봐야함

        특히, 수학의 팩토리얼(5!, 4!, 3! 등)이 재귀 호출과 비슷함

n! = n * (n-1)!

   ㄷ. 일반적으로 재귀 호출을 어려워하는 이유는 '재귀적인 사고' 때문이라고 함

n! = n * (n-1)! 이라는 수학 공식은 이해를 하고 넘어가지만,
그 공식을 코드(함수)로 옮겼을 땐, n! = n * (n-1)! 이라는 공식을 이해하지 못함
= '이 함수(코드) 자체가 팩토리얼 기능을 구현한 것인데,
    그 함수에서 또 팩토리얼을 쓴다고?'라며 혼란스러워짐

   ㄹ. 아래 코드와 같은 코드가 있다고 가정하며, 사용자는 factorial 함수에 값 2를 전달했다고 가정함

       → factorial(2)를 한 줄씩 처리하다가 "factorial(n-1)"을 만나면 처리하고 있던 factorial(2) 함수는 그대로 두고

           factorial 함수를 사본으로 복사해와서 거기에 값으로 1을 전달함

           그러면 factorial(1) 함수를 처리하게 되고,

           모두 처리하고나면 다시 리턴값과 함께 factorial(2) 함수로 돌아와서 마무리를 지음

   ㅁ. 코드 예시

public class Practice {
	public static void main(String[] args) {
		// 1~10까지의 숫자를 모두 더한 값을 구하고 싶을 때
		// 물론 n*(n+1)/2의 공식을 이용하면 훨씬 쉽지만 재귀함수 사용해봄
		int num = 10;
		System.out.println(add(num));
	}
	public static int add(int n) {
		if(n == 1) {
			return 1;
		}
		else {
			return (n + add(n-1));
		}
	}
}

 

2. 클래스 ###드디어 클래스에 왔다###

   ㄱ. 클래스(class)를 이해하는 방법은 여러가지가 있음

        1) 학문적으로 접근하기

        2) 절차지향적 방법으로 해석하기

            → 1과 2는 보통 프로그래밍 언어를 알고있다는 전제하에 한 학기 분량으로 '객체지향'에 대해서 공부를 함

            → 복잡한 프로그램을 만들수록 학문적 접근이 필요하지만, 우선은 언어를 활용하기 위해 3을 위주로 공부함

        3) 프로그램의 기본 구성을 근거로 이해하기

   ㄴ. 세상에는 많은 프로그램(게임, 응용프로그램, 웹 등)이 있음

        그 프로그램들을 쪼개고 쪼개다보면 그 안에는 공통적으로 "데이터"와 "기능"으로 구성되어 있음

        → 데이터 : 프로그램상에서 유지하고 관리해야 할 데이터

        → 기능 : 데이터를 처리하고 조작하는 기능

   ㄷ. 아주 간단하게 보자면 이 데이터와 기능을 묶어서 클래스(class)라고 할 수 있음

        → 클래스는 '틀'이라고 할 수 있음

        → 그리고 그 틀(클래스)을 이용해서 실제의 값을 메모리에 올려 만든 것이 '인스턴스(객체)'임 (*부가설명1 참고)

   ㄹ. 클래스는 'class' 키워드를 가장 앞에 붙이고, 인스턴스는 'new' 키워드를 이용함

        → 클래스 이름은 파일의 이름과 동일해야함

        → e.g.) 파일의 이름이 'BankAccount.java' 이라면 클래스 이름은 'BankAccount'이어야함

// 클래스
class BankAccount {
	......
}
// 인스턴스 생성
new BankAccount(); // BankAccount 인스턴스 하나
new BankAccount(); // BankAccount 인스턴스 둘
new BankAccount(); // BankAccount 인스턴스 셋
new BankAccount(); // BankAccount 인스턴스 넷

 

   ㅁ. 인스턴스는 생성만 하고 끝나는게 아니라 참조변수가 있어야함

        → 인스턴스를 여러개 만들면, 그 공간이 메모리 어딘가에 각각 할당됐을것임

        → 특정 인스턴스에 접근해서 메소드를 호출하고 싶다면 그 인스턴스를 부를 수 있는 '이름/별명'이 필요함

        → 그래서 각각의 인스턴스에게 이름/별명과도 같은 '변수'를 선언해줌 (= 참조변수)

   ㅂ. 참조변수에는 "값"이 아니라 "주소"가 저장됨

// 참조변수 선언
BankAccount account1; // 참조변수 account1 선언
BankAccount account2; 

/* 설명
* 마치 int account2로 변수 선언 하듯이 
* 참조변수도 "[클래스이름] [별명]"으로 선언함
*/
// 인스턴스 가리키기
account1 = new BankAccount(); 

/* 설명
* new BankAccount() 코드를 통해 인스턴스를 하나 생성함
* new BankAccount()를 통해 인스턴스를 생성하면 '인스턴스 주소값'이 반환됨
* 그 주소값이 account1 참조변수에 저장됨
*/
// 인스턴스 호출
account1.withdraw(1000); // account1이 참조하는 인스턴스의 withdraw 메소드 호출

/* 설명
* account1이 참조하는 인스턴스의 withdraw 메소드를 호출하는 문장
*/

   ㅅ. 참조변수도 일반 변수처럼 저장된 값을 변경할 수 있음 (**부가설명2 참고)

BankAccount account1 = new BankAccount();
BankAccount account2;

... (중략) ...

account1 = new BankAccount(); // account1이 새 인스턴스 참조함

... (중략) ...

account2 = account1; // account2는 account1과 같은 인스턴스를 참조함

   ㅇ. 매개변수 위치에 참조변수가 선언될 수도 있음 (마치 일반 변수처럼)

        → 인자(argument)로 참조변수가 쓰인다면, 해당 참조변수에 저장되어 있는 '인스턴스 주소값'이 전달됨

        → 따라서, account1과 acc 참조변수는 같은 '인스턴스 주소값'을 가지고 있는 상태임

BankAccount account1 = new BankAccount();

checkCount(account1); // 주소값이 전달됨
public static void checkCount(BankAccount acc) { // 매개변수로 참조변수 acc가 선언됨
	// 생략
}

   ㅈ. Java에서는 이 주소값을 '참조값'이라고 표현함

   ㅊ. 참조변수에는 주소값(참조값) 뿐만 아니라 null도 대입이 가능함

       (null은 참조하는 인스턴스가 없거나, 원래 참조하고 있던 인스턴스와 연결을 끊을 때 사용함)

 

3. String 클래스

   ㄱ. 문자열은 사실 '인스턴스'로 저장이 됨

          → 가장 많이 봤던 System.out.println("Hello"); 에서 Hello는 String 인스턴스임

          → 즉, Java에는 기본적으로 정의되어있는 "String" 클래스가 있음

          → JVM이 문자열을 만나는 순간, String 인스턴스를 생성하고 그 인스턴스의 주소값(참조값)을 반환함

          → 즉, 위의 예시에서 "Hello" 부분은 참조값으로 변환되는 것

   ㄴ. 코드 예시

String str1 = "Hello";
String str2 = "Everyone";

          → str1과 str2는 String형 참조변수라고 할 수 있음

          → JVM은 "Hello"라는 문자열의 인스턴스(String 클래스)를 만듦, "Everyone"도 동일함

               인스턴스를 만들어서 정의하고, 인스턴스의 참조값(주소값)을 반환함

               str1과 str2에는 각각의 인스턴스 참조값(주소임)이 저장됨

 

# String str1 = "Hello"와 String str2 = new String("Hello")의 차이?

> 둘의 차이는 아래의 내용을 참고할 것

> 참고 자료 : https://starkying.tistory.com/entry/what-is-java-string-pool

 

String Constant Pool이란? | Java String Pool

Java에서 String 객체를 생성하는 방법은 2가지가 있다. 첫번째는 String literal, 즉 큰 따옴표("")를 사용하는 것이고, 두번째는 new 연산자를 사용하는 것이다. 두 방법에는 어떤 차이가 있을까? 간단한

starkying.tistory.com

 

4. 생성자

   ㄱ.객체지향 문법과 별개로 클래스 정의 모델(학문적인 접근 방법)을 알아둘 필요가 있음

         → e.g. 클래스 정의 모델1 : 각각의 인스턴스를 구분할 수 있는 구분자를 클래스 정의 시 포함해줄 것

         → e.g. 클래스 정의 모델2 : 인스턴스 안에 있는 데이터들은 처음에 반드시 초기화 해줄 것

class BankAccount {
	String accNumber;	//계좌번호이면서 인스턴스를 구분하기 위한 식별자
    int account = 0;
    
    // 데이터 초기화
    public void init(String acc, int num) {
    	accNumber = acc;
        account = num;
    }
    ...
}
//main 메소드
BankAccount kim = new BankAccount(); // 계좌생성
kim.init("123-45-678", 1000); // 초기화

ㄴ. 클래스 정의 시에는 초기화를 위한 메소드가 필요함

          → 인스턴스 생성 시 딱 한번만 초기화 되어야함

          → 위의 코드처럼 직접 메소드를 만들 필요 없이 Java에서 인스턴스 생성 시 자동으로 호출해서

                초기화할 수 있는 메소드를 이미 만들어놓았음 (=생성자)

class BankAccount {
	String accNumber;	//계좌번호이면서 인스턴스를 구분하기 위한 식별자
    int account = 0;
    
    // 생성자
    public BankAccount(String acc, int num) {
    	accNumber = acc;
        account = num;
    }
    ...
}
//main 메소드
BankAccount kim = new BankAccount("123-45-678", 1000); // 계좌 생성과 동시에 초기화

   ㄷ. 생성자는 조건이 있음

          1) 생성자 이름은 '클래스 이름'과 동일할 것

          2) 생성자는 반환 값(return 값)이 없을 것 (반환형도 표시하지 않을 것)

   ㄹ. 컴파일러는 사용자가 생성자를 정의해놓지 않으면 자동으로 생성자를 삽입해놓음 (= 디폴트 생성자)

         → 디폴트 생성자는 안에 코드도 비어있고, 매개변수도 없음

         → 하지만, 디폴트 생성자를 이용하기보다는 가급적 생성자를 이용해서 데이터들을 초기화 해주는 것이 좋음

 

5. 클래스 이름 짓기

   ㄱ. 클래스는 첫문자를 '대문자'로 시작(Camel Case 모델)

   ㄴ. 메소드, 변수는 첫문자를 '소문자'로 시작, 두번째 단어부터는 첫문자를 '대문자'로 시작

   ㄷ. 상수는 모든 문자를 '대문자'로 구성, 단어와 단어 사이는 '언더바' 사용

 

6. 클래스 Path

   - Java는 소스파일(*.java)에 두개 이상의 클래스(class)를 작성할 수 있는데

      컴파일 해서 클래스파일(*.class)이 될 때는 각각의 클래스 파일로 생성이 됨

   - 또한 실행되는 클래스파일 중에는 반드시 main메소드가 포함된 클래스파일이 하나 있어야함

   - 보통, 별도의 경로 설정이 없으면 그 클래스파일들이 같은 위치(디렉터리)에 생성됨

   - 이때 만약 생성된 클래스파일을 각각 다른 위치에 둔다면, JVM은 필요한 클래스 파일을 찾지 못해 에러를 보냄

   - class path를 사용하면 다른 디렉터리에 있는 클래스 파일을 찾아와서 실행시킬 수 있음

   - 클래스 패스 설정 방법

      ㄱ. 절대경로로 설정하는 방법 ('절대경로'라서 권장하지는 않음)

             → 세미콜론(;)은 구분자임

             → 온점(.)은 현재 디렉터리를 의미함

             → 아래와 같이 설정하면 JVM은 기존의 방법은 모두 잊고

                  classpath에 설정된 현재 디렉터리(.)와 D:\Coding Folder\Java 에서 클래스 파일을 찾음

      ㄴ. 상대 경로로 설정하는 방법

             → 참고로 온점 두개(..)는 이전 디렉터리를 의미함

      ㄷ. 클래스패스를 고정시키는 방법 (권장하지는 않음)

             → 시스템의 '환경 변수' 설정창 > Path 선택 > classpath를 설정하면 되지만 잘 사용하지 않는 방법임

   - classpath는 해당 명령 프롬프트에서만 유효함 (창을 닫으면 사라짐)

 

 


*부가설명1

인스턴스의 정의는 "실행 중인 프로세스, 클래스의 현재 생성된 오브젝트"라고 한다.

출처: 위키백과, https://ko.wikipedia.org/wiki/%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4

 

클래스는 '틀'이라고 볼 수 있다. 붕어빵 만들 때 사용하는 '틀'(frame)처럼 말이다.

이 '틀'을 복사해서 어떤 것은 팥이 들어간 붕어빵을 만들면서 사용하고,

어떤 것은 팥반크림반 붕어빵을 만들때 사용하고,

어떤 것은 크림 붕어빵을 만들면서 사용하는 등 필요할 때 복제해서 사용한다.

즉, 틀(클래스)의 복제본을 인스턴스라고 할 수 있다.

 

강의에서는 은행 계좌를 예시로 들어준다.
계좌 '잔액'을 나타내는 변수 하나, 입금 기능을 하는 메소드 하나, 출금 기능을 하는 메소드 하나,
계좌의 잔액을 조회하는 메소드 하나가 있다고 가정하자.
int account = 0; // A씨의 계좌
public static void main(String[] args) {
	deposit(1000); // 입금
    printAccount(); // 현재 잔액 출력
    withdraw(500); // 출금
    printAccount(); // 현재 잔액 출력
}
publi static int deposit(int num) {
	//코드 생략
}
publi static int withdraw(int num) {
	//코드 생략
}
publi static int printAccount(int num) {
	//코드 생략
}

 

위의 코드를 보면, A 고객의 계좌를 만들어주고 (=변수명 account) 입금, 출금 등의 기능을 하도록 했다. 
이때 100명, 1000명의 계좌를 만들어 줘야한다면...?
이미 실행중인 프로그램(코드)에 변수를 100개, 1000개 추가로 만들어 줄 수는 없기 때문에
이런 때에 클래스와 인스턴스를 사용한다고 한다.

 

 

**부가설명2

아래 코드를 보자.

park = kim; 코드를 통해 park 참조변수는 kim 참조변수를 가리키는 것을 알 수 있다.

코드 중간에 kim = new BankAccount(); 를 통해 kim 참조변수가 새 인스턴스를 가리키도록 하였다. 

그렇다면 마지막에 kim.printAccount(); 와 park.printAccount()는 같은 결과를 출력할까?

public class Practice19 {
	public static void main(String[] args) {
		BankAccount kim = new BankAccount();
		BankAccount park;
		park = kim;
		
		kim.deposit(5000);
		kim.withdraw(4000);
		kim.printAccount();
		
		park.withdraw(500);
		
		kim.printAccount();
		park.printAccount();		
		
		kim = new BankAccount();
		
		kim.printAccount();
		park.printAccount();
	}
}

class BankAccount {
	int account = 0;
	// 입금 메소드
	public int deposit(int num) {
		account += num;
		return account;
	}
	// 출금 메소드
	public int withdraw(int num) {
		if(account < num) {
			System.out.println("잔액이 부족합니다");
			return 0;
		}
		else {
			account -= num;
			return account;
		}
	}
	// 잔액 확인 메소드
	public int printAccount() {
		System.out.println("잔액은 "+account+"원 입니다.");
		return account;
	}
}

정답 : kim.printAccount 와 park.printAccount의 출력 결과가 다르다.

 

처음에 park 참조변수가 kim 참조변수와 같은 주소를 가리키도록 해놨다.

그래서 중간에 kim 참조변수의 값을 변경하면 park 참조변수도 값이 변할 것이라고 생각했는데, 말도 안되는 소리였다.

즉, park 참조변수는 'kim 참조변수'를 저장하는게 아니라 'kim 참조변수가 가리키는 "주소"값'을 저장하는 듯 하다.

 

Comments