📢 백엔드 관련 자료는 Backend Wiki에서 PR을 통해 검증과 내용 보완을 거친 후 블로그로 이관되고 있습니다 :)
변수의 종류
- 변수는 변수가 선언된 위치에 따라서 변수의 종류가 결정됩니다
- 클래스 영역에 선언된 변수는 모두 멤버 변수이고, 멤버 변수를 제외한 나머지 변수들은 모두 지역변수입니다
- 멤버 변수는 두가지로 나뉘어지는데, static이 붙어있으면 클래스 변수이고 붙지 않은것은 인스턴스 변수입니다
- 멤버변수 = 클래스 변수 + 인스턴스 변수
변수 종류 | 설명 | 생성 시기 |
클래스 변수 (멤버 변수) | static이 붙어있는 경우 | 클래스가 메모리에 로딩될 때 |
인스턴스 변수 (멤버 변수) | static이 붙어있지 않은 경우 | 인스턴스가 생성되었을 때 |
지역 변수 | 멤버 변수를 제외한 나머지 | 변수 선언문이 수행되었을 때 |
public class Study {
int iv; // 인스턴스 변수
static int cv; // 클래스 변수 (static변수, 공유변수)
void method() {
int lv = 0; // 지역 변수
}
}
인스턴스 변수
- 클래스의 인스턴스를 생성할 때 만들어집니다
- Person giibeom = new Person();
- 인스턴스는 독립적인 저장 공간을 가지므로 각자 서로 다른 값을 가질 수 있습니다
class Person {
String name;
static int eye;
}
// 참조변수 giibeom과 alex는 같은 Person 객체이지만 name은 서로 다른 값을 가진다 (다른 주소값)
Person giibeom = new Person();
giibeom.name = "Myoung gibeom";
Person alex = new Person();
alex.name = "Alex";
- 따라서 인스턴스 마다 고유한 상태를 유지해야 하는 속성(변수)의 경우 인스턴스 변수로 선언해야 합니다
- 인스턴스 변수는 직접 접근이 안되므로 인스턴스 생성 후 접근이 가능합니다!
Person myoung = new Person();
myoung.name = “gibeom”;
클래스 변수
- 클래스 변수 선언은 인스턴스 변수 앞에 static을 붙이기만 하면 됩니다
- 클래스 변수는 모든 인스턴스가 공통된 저장 공간(변수)을 공유하게 됩니다
- 따라서 한 클래스의 모든 인스턴스들이 공통적인 값을 유지하며, 공유하여 사용해야 하는 변수인 경우 클래스 변수로 선언해야 합니다
- 클래스 변수는 인스턴스를 만들지 않아도 직접 접근이 가능합니다!
- Person.eye = 2;
지역 변수
- 지역 변수는 메서드 내에 선언되며 메서드 내에서만 사용이 가능합니다
- 메서드 내에서 선언된 변수들 + 매개변수는 모두 지역변수입니다
- 따라서 메서드가 종료되면({} 밖으로 벗어난 경우) 소멸되어 사용할 수 없습니다
// add의 매개변수 x,y 와 minus의 매개변수 x,y는 서로 다른것이다!
int add(int x, int y) {
....
}
int minus(int x, int y) {
....
}
이 상황에선 클래스 변수? or 인스턴스 변수?
- 클래스 변수와 인스턴스 변수를 각각 언제 무엇으로 써야 되는지 알아봅시다
// 💡 원카드, 포커 등을 할 수 있는 게임카드를 예시로 들어보자
// 카드의 속성: 무늬, 숫자, 폭, 높이, 재질
// 카드의 폭, 높이, 재질은 "공통적인 부분"이니 서로 공유하게끔 클래스 변수로 선언합니다
// 무늬와 숫자는 "각자 고유한 값"을 가져가야 하니 인스턴스 변수로 선언합니다
class Card {
String kind; // 무늬
int number; // 숫자
static int width = 100; // 폭
static int height = 250; // 높이
static String material = "plastic"; // 재질
}
메서드
- 메서드는 특정 작업을 수행하는 일련의 문장들끼리 하나로 묶은 것입니다
- 메서드를 사용할 땐 그저 넣을 값(입력)과 반환하는 결과(출력)만 알면 됩니다
- 메서드가 내부적으로 어떻게 동작하는지 몰라도 사용하는데 아무런 지장이 없습니다
- 따라서 메서드는 내부가 보이지 않는 블랙박스 라고도 불립니다
- 이 말은 즉슨 넣을 값과 반환 결과만 동일하다면 언제든 메서드 내부 수정이 가능하다는 말입니다
- 이것이 객체지향 메서드의 장점입니다!
메서드를 사용하는 이유
메서드의 대표적인 장점 3가지에 대해서 알아봅시다
1. 높은 재사용성(reusability)
- 이미 우리는 Java API에서 제공하는 다양한 메서드들을 사용하면서 경험해본 적이 있습니다
- java.lang 패키지 (String, Integer, …)
- java.util 패키지 (Arrays, Calendar, Date, Objects, StringTokenizer, …)
- 메서드를 한번 만들어 놓으면 필요한 상황에서 몇 번이고 호출하여 사용할 수 있습니다
2. 중복된 코드의 제거
- 프로그래밍을 하다 보면 같은 내용의 문장들을 여러 곳에서 반복해서 사용하곤 합니다
- ex) 로그용 System.out.println() 등등 …
- 물론 실무에서는 log 라이브러리를 사용해야 합니다
- 메서드를 사용하면 반복되는 문장들 대신, 메서드를 호출하는 한 문장으로 대체가 가능합니다
- 오류를 수정해야할 때에도 여러곳에 퍼진 것들을 일일히 정리하기에는 위험요소가 많지만, 메서드로 구현을 한다면 메서드만 변경하면 됩니다
3. 프로그램의 구조화
- 프로그램의 전체 흐름이 한눈에 들어올 정도로 단순하게 구조화 하여 코드를 작성하여야 합니다
- 가독성이 좋아야 오류를 빠르게 찾을 수 있어 유지보수에 용이합니다
- 이때 메서드를 통해 프로그램을 구조화 시킬 수 있습니다
위의 코드를 구조화하여 작성해보겠습니다
위의 예제를 작업단위로 나누어서 메서드를 만들어보면 다음과 같습니다
// 위 사진(코드)의 구조화 적용 코드
public static void main(String[] args) {
int[] numArr = new int[10];
initArr(numArr); // 1. 배열을 초기화
printArr(numArr); // 2. 배열을 출력
sortArr(numArr); // 3. 배열을 정렬
printArr(numArr); // 4. 배열을 출력
}
메서드의 선언과 구현
- 메서드는 선언부(header, 머리)와 구현부(body, 몸통)로 이루어져 있습니다
반환타입 메서드이름 (타입 변수명, 타입 변수명, ...) --> 선언부
{
.... --> 구현부
}
메서드 선언부
- 메서드 선언부는 메서드가 작업을 수행하기 위해 어떤 값들을 필요로 하고, 작업의 결과로 어떤 타입의 값을 반환하는지에 대한 정보를 제공합니다
- 메서드의 선언부는 3가지로 구성되어 있습니다
1. 메서드의 이름
- 보통 동사로 많이 짓습니다
- 메서드의 이름만으로도 어떤 기능인지 쉽게 알 수 있도록 함축적이면서도 의미있는 이름을 짓도록 노력해야 합니다
2. 매개변수 선언 (parameter declaration)
- 작업을 수행하는데 제공 받아야 하는 필요한 입력 값들을 적습니다
- 매개변수는 여러개가 될 수 있습니다
3. 반환타입 (return type)
- 작업 수행 출력결과인 반환값의 타입을 적습니다
- 반환값은 최대 1개만 허용됩니다
- 반환타입이 void일 경우에는 아무것도 반환하지 않습니다
메서드 구현부
- 메서드 선언부 다음에 오는 괄호 {} 를 메서드의 구현부라고 합니다
- 해당 메서드 반환 타입이 void가 아닐 시 return 반환값; 을 반드시 포함해야 합니다
- 반환 타입이 void 이면 컴파일러가 메서드 마지막에 return; 을 자동적으로 추가해줍니다
- 반환 값의 타입은 메서드 선언부의 반환 타입과 일치하거나, 자동 형변환이 가능한 것이여야 합니다
// 반환값인 result가 반환타입인 double와 동일하거나 double로 자동 형변환이 되는 타입이어야 한다
// 반환 타입: double, 반환값: int(int -> double 자동 형변환 가능)
double add(int x, int y) {
int result = x + y;
return result;
}
메서드의 호출
- 메서드는 매개변수(인자)와 일치하는 ‘인수(argument)’ 를 집어넣어 호출하여야 합니다
add(7, 10) // int add(int x, int y)를 호출하고 인수 7을 x에, 인수 10을 y에 대입
print99danAll(); // void print99danAll()을 호출
- 같은 클래스 내의 메서드끼리는 참조변수 없이 서로 호출이 가능하지만, static 메서드는 같은 클래스 내의 인스턴스 메서드를 호출할 수 없습니다
- static → static : 가능
- non-static → non-static : 가능
- non-static → static : 가능
- static → non-static : 불가능
왜 static에서 non-static을 호출할 수 없을까?
- static 변수와 static 메서드는 클래스가 로딩되는 시점에 메모리에 올라갑니다
- 하지만 non-static은 인스턴스가 생성될 때만 사용이 가능합니다
- 따라서 인스턴스가 생성된 시점에서는 static은 당연히 메모리에 올라가있는 상태이겠지만, 반대로 클래스가 로딩되는 시점엔 인스턴스가 생성 되어있을거란 보장이 없기 때문에 컴파일 오류가 발생합니다
- 따라서 static에서 non-static을 사용하려면 메서드 내에서 해당 인스턴스를 생성 한 후 사용하여야 합니다
JVM의 메모리 구조
- JVM 메모리 영역은 주로 사용되는 3가지의 영역이 존재합니다
메서드 영역 (Method Area)
- 프로그램 실행 중 어떤 클래스가 사용되면 JVM은 해당 클래스파일을 읽고 분석하여 클래스에 대한 정보(클래스 데이터)를 이곳에 저장합니다 → 동적 바인딩
- 클래스 변수들이 생성되는 공간입니다
힙 (Heap)
- 프로그램이 실행 중 생성되는 모든 인스턴스는 이곳에 저장합니다
- 인스턴스 변수들이 생성되는 공간입니다
호출 스택 (Call stack)
- 메서드 작업에 필요한 메모리 공간을 제공합니다
- 메서드 작업이 끝나면 할당되었던 메모리공간은 반환되어 비어집니다
- 이 특징으로 인해 메서드 내부의 지역변수는 메서드 외부에서 사용하지 못합니다
- 지역 변수(매개변수 포함)들이 생성되는 공간입니다
프로그램 실행 시 호출 스택(Call stack) 흐름
- 호출 스택은 이름 그대로 자료구조 중 하나인 Stack 구조를 따라갑니다
- 아래 예시의 main 메서드 호출을 통해 흐름을 따라가보겠습니다
public class CallStackTest{ public static void main(String[] args) { // 시작 firstMethod(); } private static void firstMethod() { secondMethod(); } private static void secondMethod() { System.out.println("secondMethod()"); } }
- 실행 순서: main - firstMethod - secondMethod - println
- 메서드 실행 흐름대로 호출 스택의 과정을 담은 사진입니다
기본형 매개변수 vs 참조형 매개변수
매개변수 | 특징 | 이유 |
기본형 매개변수 | 변수의 값을 읽기만 할 수 있다 (read only) | 기본형 값이 복사 |
참조형 매개변수 | 변수의 값을 읽고 변경할 수 있다 (read & write) | 인스턴스(객체)의 주소가 복사 |
기본형 매개변수
// 기본형 매개변수는 값을 복사하기 때문에 변경이 불가능하다
public static void main(String[] args) {
int num = 10;
System.out.println("A: " + num); // A: 10
change100(10);
System.out.println("C: " + num); // C: 10
}
private static void change100(int num) {
num = 1000;
System.out.println("B: " + num); // B: 1000
}
참조형 매개변수
// 인스턴스나 배열같은 참조형 매개변수는 주소값을 공유한다
// 따라서 메서드 안에서 변경한 값이 메서드 밖에서도 변경된 값으로 출력된다
class Data {
int num;
}
public class Study {
public static void main(String[] args) {
Data d = new Data();
d.num = 10;
System.out.println("A: " + d.num); // A: 10
change100(d); // Data 인스턴스 자체를 넘긴다
System.out.println("C: " + d.num); // C: 1000
}
private static void change100(Data data) {
d.num = 1000;
System.out.println("B: " + d.num); // B: 1000
}
그렇다면 Java는 Call by Reference?
위의 참조형 매개변수로 값을 전달하는 경우 데이터의 값이 변경되면 원본도 변경되어 Call by Reference라고 착각할 수도 있습니다
과연 Java는 Call by Reference일까요?
Java에서는 Call by Value만 존재합니다
위의 예시처럼 change100(d)를 넣는다면 d의 주소값이 복사되어 매개변수 data에 넣어줍니다.
"d의 주소값이 복사된다" 즉, 원본 데이터가 바뀌어서 Call by Reference 처럼 동작을 하여 오해하기 쉽지만
객체의 주소값을 복사하기 때문에 Call by Value 입니다
재귀호출(recursive call)
- 메서드의 내부에서 메서드 자신을 다시 호출하는 방식입니다
- 호출된 메서드는 ‘값에 의한 호출(call by value)’을 통해 원래의 값이 아닌 복사된 값으로 작업하기 때문에 독립적인 작업수행이 가능합니다 → 호출한 메서드와 호출당한 메서드는 서로 관련이 없습니다
- 재귀호출을 탈출할 수 있는 조건문이 필수입니다 → 탈출 조건이 없다면 무한루프가 돌아서 Stack Overflow Error 발생
- 재귀호출의 대부분은 반복문으로 작성하는 것이 가능하며, 반복문 보다 몇가지 과정(매개변수 복사, 종류 후 복귀할 주소 저장 등등)이 추가로 필요하여 수행시간이 오래 걸립니다
그렇다면 재귀호출을 왜 사용할까?
- 재귀호출이 주는 논리적 간결함 때문입니다
- 몇겹의 반복문과 조건문을 재귀호출로 사용하면 단순한 구조로 바뀔 수도 있습니다
// 재귀호출 사용에 대표적인 예는 Factorial 입니다
public static void main(String[] args) {
int result = factorial(4);
System.out.println(result);
}
static int factorial (int n) {
int result = 0;
if (n == 1) {
result = 1;
} else {
result = n * factorial(n - 1); // 메서드 자신을 다시 호출한다.
}
return result;
}
하지만 재귀 호출은 for문에 비해 비효율적이므로 재귀 호출에 드는 비용보다 재귀 호출이 주는 논리적 간결함의 이득이 충분히 큰 경우에만 사용하여야 합니다
클래스 메서드 vs 인스턴스 메서드
- 변수와 동일하게 메서드 앞에 static이 붙어있으면 클래스 메서드, 붙어있지 않으면 인스턴스 메서드입니다
- 또한 동일하게 클래스 메서드는 객체(인스턴스)를 생성하지 않고도 호출이 가능합니다
- 클래스이름.메서드이름(매개변수)
- 인스턴스 메서드는 반드시 객체를 생성하여 참조변수를 통해 호출해야 합니다
클래스 참조변수 = new 클래스();
참조변수.메서드이름(매개변수);
클래스 메서드 (static 메서드)
- 인스턴스 변수나 인스턴스 메서드를 사용하지 않는 메서드인 경우 클래스 메서드로 정의합니다
- 물론 인스턴스 변수를 사용하지 않는다고 무조건 클래스 메서드(static)로 정의할 필요는 없지만 특별한 이유가 없으면 일반적으로는 클래스 메서드로 정의합니다
- 책 내용과는 별개로 실무에서는 static 메서드와 static 변수를 지양하여야 한다고 합니다
- 인스턴스 메서드가 아닐 시 캡슐화에 위배
- static은 메모리 회수 불가
- static 메서드는 오버라이딩 불가
- static 메서드는 인터페이스 구현 시 사용 불가
- 따라서 static은 모든 객체지향적 기능들의 사용을 방해하는 요소라고 합니다
- 책 내용과는 별개로 실무에서는 static 메서드와 static 변수를 지양하여야 한다고 합니다
인스턴스 메서드
- 메서드 내 인스턴스 변수를 필요로 하는 메서드일 경우 인스턴스 메서드로 정의해야 합니다
- 인스턴스 변수는 인스턴스 생성 후 사용이 가능하기 때문입니다
어느 상황에서 뭘 선언하지?
- 클래스의 멤버변수 중 모든 인스턴스에 “공통된 값을 유지” 해야 한다면 static을 붙여줍니다 (클래스 변수)
- 메서드 중 자주 사용되고, 공통적으로 사용되어야 하는 메서드에는 static을 붙여줍니다 (클래스 메서드)
- 또한 메서드 중 인스턴스 변수, 인스턴스 메서드를 사용하지 않는 메서드에는 static을 붙여줍니다 (클래스 메서드)
- ex) 매개변수로만 작업을 수행하는 경우에는 클래스 메서드(static)
- 클래스 메서드는 클래스가 메모리에 로딩될 때 이미 같이 메모리에 할당되므로 바로 사용이 가능합니다
- 하지만 인스턴스 메서드는 실행 시 호출되어야 할 메서드를 찾는 시간이 추가적으로 발생하므로 클래스 메서드가 효율적입니다
- 나머지 상황에 경우는 모두 인스턴스 메서드를 선언해야 합니다
마무리
해당 주제에서는 중요한 내용들이 많았던 것 같습니다
참조형 매개변수에서는 원본 데이터가 바뀌므로 Call by Reference로 착각을 했지만, 사실 Java에서는 Call by Value만 있더군요
또한 책 내용과는 다르게 실무에서는 객체지향적인 기능들을 방해하는 static의 사용을 지양하는 것을 새로 알아가는 것 같습니다
static의 남용은 프로그램의 상태를 추정하기 어렵게 만들며 객체지향적이지 않으므로 앞으로는 static의 사용을 자제해야 할 것 같습니다
감사합니다 :)
📢 자료가 도움이 되셨다면 Backend Wiki로 놀러 오세요!
Reference
💡 잘못된 내용이나, 보충할 내용이 있다면 언제든지 편하게 연락 주시면 감사하겠습니다 :)
🙋🏻♂️ Call me
Email : dev.gibeom@gmail.com
KakaoTalk : https://open.kakao.com/me/beomdrive
'Programming > Java' 카테고리의 다른 글
객체 지향, 그 속으로 (0) | 2022.09.06 |
---|