프래피티 STUDY 20230917
프래피티 STUDY
- TIL
- 오버로딩과 오버라이딩
- Primitive 타입과 Reference 타입
오버로딩과 오버라이딩의 차이점
오버로딩 (Overloading)
- 정의: 오버로딩은 같은 이름의 함수나 메서드를 여러 번 정의하는 것입니다. 그러나 매개변수의 타입, 개수 또는 순서가 서로 달라야 합니다. 이를 통해 하나의 함수 또는 메서드 이름으로 여러 가지 동작을 수행할 수 있게 됩니다.
- 특징:
- 메서드 이름이 같아야 합니다.
- 매개변수의 타입, 개수 또는 순서가 달라야 합니다.
- 반환 타입은 오버로딩을 결정하는 데 영향을 주지 않습니다. 즉, 반환 타입만 다르고 매개변수가 같은 두 메서드는 오버로딩이 아닙니다.
- 오버로딩은 같은 클래스 내에서 발생합니다.
- 예시 (Java 언어 기준):위 예시에서 add 메서드는 오버로딩되어 있습니다. 세 개의 add 메서드가 있지만, 각각의 매개변수가 다릅니다. 따라서 호출 시 전달되는 인수에 따라 적절한 add 메서드가 실행됩니다.
- public class Calculator { // 두 정수를 더하는 메서드 public int add(int a, int b) { return a + b; } // 세 정수를 더하는 메서드 public int add(int a, int b, int c) { return a + b + c; } // 두 double 값을 더하는 메서드 public double add(double a, double b) { return a + b; } }
- 장점:
- 코드의 재사용성을 높일 수 있습니다.
- 메서드 이름을 기억하기 쉽게 할 수 있습니다. 같은 기능을 하는 메서드가 여러 개 있을 때, 그 기능에 따라 이름을 다르게 지으면 이름을 기억하는 것이 어려울 수 있습니다.
- 코드의 가독성을 높일 수 있습니다.
오버로딩은 프로그래머가 코드를 더 간결하게 작성할 수 있게 해주며, 함수나 메서드의 다양한 사용 방법을 제공하므로 사용자에게 편리성을 제공합니다.
오버라이딩 (Overriding)
정의: 오버라이딩은 상속 관계에 있는 부모 클래스의 메서드를 자식 클래스에서 재정의하는 것을 말합니다. 오버라이딩된 메서드는 부모 클래스의 메서드와 이름, 매개변수 타입, 반환 타입이 모두 같아야 합니다.
- 특징:
- 오버라이딩은 상속 관계에 있는 클래스 사이에서만 발생합니다.
- 오버라이딩된 메서드는 부모 클래스의 메서드와 이름, 매개변수 타입, 반환 타입이 동일해야 합니다.
- 자식 클래스에서 오버라이드된 메서드의 접근 제한자는 부모 클래스의 메서드의 접근 제한자보다 접근 범위가 좁아질 수 없습니다. 예를 들어, 부모 클래스의 메서드가 public으로 선언되었다면, 자식 클래스에서 이를 private으로 변경할 수 없습니다.
- @Override 어노테이션을 사용하여 메서드를 오버라이드할 때 해당 어노테이션을 붙여주면 컴파일러가 메서드가 올바르게 오버라이드되었는지 확인해줍니다.
- final, static, private 메서드는 오버라이드될 수 없습니다.
- 예시 (Java 언어 기준):위 예시에서 Dog 클래스는 Animal 클래스를 상속받습니다. Animal 클래스의 sound 메서드를 Dog 클래스에서 재정의(오버라이드)하였습니다. 이 경우, Dog 객체를 사용하여 sound 메서드를 호출하면 "Dog barks"가 출력됩니다.
- class Animal { void sound() { System.out.println("Animal makes a sound"); } } class Dog extends Animal { @Override void sound() { System.out.println("Dog barks"); } }
- 장점:
- 다형성의 구현: 오버라이딩을 통해 부모 클래스의 참조 변수로 자식 클래스의 객체를 참조할 때, 자식 클래스의 메서드를 호출할 수 있게 되어 다형성을 구현할 수 있습니다.
- 확장성 및 유지 보수성 향상: 부모 클래스의 기능을 그대로 사용하면서, 특정 기능만 자식 클래스에서 변경이 필요한 경우, 해당 메서드만 오버라이드하면 됩니다.
오버라이딩은 객체 지향 프로그래밍에서 중요한 개념 중 하나로, 코드의 재사용성과 유연성을 높여줍니다.
다형성
다형성(Polymorphism)은 객체 지향 프로그래밍 (OOP)의 주요 원칙 중 하나입니다. 다형성이란 "많은 형태를 가짐"을 의미하며, 프로그래밍 컨텍스트에서는 하나의 인터페이스나 클래스가 다양한 형태로 동작하는 능력을 가리킵니다. 다형성의 핵심은 코드의 재사용성과 확장성을 높이는 데 있습니다.
다형성의 특징:
- 하나의 인터페이스, 여러 구현: 다형성을 통해 같은 인터페이스나 슈퍼클래스를 상속받는 여러 클래스의 객체들이 동일한 메서드 호출에 대해 각각 다르게 반응하도록 할 수 있습니다.
- 코드의 재사용성: 공통의 로직은 슈퍼클래스에서 한 번만 작성하고, 특수한 동작은 서브클래스에서 재정의하여 사용할 수 있습니다.
- 확장성: 새로운 클래스를 추가하는 것이 쉬워집니다. 인터페이스나 슈퍼클래스의 규약을 따르는 새 클래스를 추가하면, 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있습니다.
다형성의 예시 (Java 언어 기준):
abstract class Animal {
abstract void sound();
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Cat meows");
}
}
public class TestPolymorphism {
public static void main(String[] args) {
Animal a;
a = new Dog();
a.sound();
a = new Cat();
a.sound();
}
}
위 코드에서 Animal 클래스는 sound라는 추상 메서드를 가지고 있습니다. Dog와 Cat 클래스는 이 Animal 클래스를 상속받아 sound 메서드를 오버라이드(재정의)합니다. 이렇게 같은 Animal 타입의 참조 변수 a를 사용하여 Dog와 Cat 객체를 참조하며, sound 메서드를 호출할 때 각각 다르게 동작하는 것을 볼 수 있습니다.
이런 방식으로 다형성은 코드의 유연성과 확장성을 향상시키며, 유지 보수를 쉽게 합니다.
오버로딩을 사용함으로 얻는 이점
오버로딩(Overloading)을 사용함으로써 얻을 수 있는 주요 이점은 다음과 같습니다:
- 코드의 가독성 향상: 오버로딩을 사용하면 관련된 기능을 수행하는 여러 메서드나 생성자에 동일한 이름을 사용할 수 있습니다. 이로 인해 코드의 가독성이 향상되고, 개발자가 메서드의 이름만으로 그 기능을 쉽게 파악할 수 있게 됩니다.
- 코드 재사용성: 같은 기능이지만 입력 인자의 타입이나 개수가 다른 경우에 동일한 메서드 이름을 사용하여 정의할 수 있기 때문에 코드의 재사용성이 높아집니다.
- 코드의 유연성: 같은 기능을 하는 메서드를 여러 버전으로 제공할 수 있으므로, 개발자는 상황에 따라 적절한 메서드를 선택하여 호출할 수 있습니다.
- 사용자 편의성: 개발자나 API 사용자가 다양한 매개변수 옵션을 가진 메서드를 사용할 수 있게 되므로, 사용자의 편의성이 증가합니다. 예를 들어, 벡터 또는 리스트와 같은 자료 구조에서 요소를 추가하는 메서드를 오버로드하여 다양한 데이터 타입의 요소를 추가할 수 있게 할 수 있습니다.
- 코드의 직관성: 오버로딩은 메서드의 이름을 기능에 따라 일관성 있게 사용할 수 있게 합니다. 이렇게 되면 코드를 읽는 사람이 메서드의 이름만 보고도 그 메서드가 어떤 기능을 하는지 직관적으로 이해할 수 있습니다.
- API 설계의 유연성: 라이브러리나 프레임워크를 설계할 때, 오버로딩을 사용하면 사용자에게 다양한 옵션을 제공할 수 있게 되므로, API의 유연성이 향상됩니다.
예를 들어, 자바의 PrintStream 클래스의 print 메서드는 여러 데이터 타입을 인자로 받을 수 있도록 오버로드 되어 있습니다. 이로 인해 사용자는 int, double, String 등 다양한 타입의 데이터를 동일한 print 메서드를 사용하여 출력할 수 있습니다.
오버로딩의 이점은 이런 방식으로 코드의 가독성, 재사용성, 유연성을 높여 개발 프로세스를 보다 효율적으로 만들어 줍니다.
Primitive 타입
Primitive 타입은 많은 프로그래밍 언어에서 기본적으로 제공되는 데이터 타입을 나타냅니다. Java를 예로 들면, primitive 타입은 미리 정의된 특별한 데이터 타입으로, 객체가 아닙니다. 이러한 타입은 메모리 효율성, 실행 속도 등의 이유로 사용됩니다.
Java에서는 아래와 같은 8개의 primitive 타입이 있습니다:
- byte: 8비트의 정수를 저장하며 범위는 -128부터 127까지입니다.
- short: 16비트의 정수를 저장하며 범위는 -32,768부터 32,767까지입니다.
- int: 32비트의 정수를 저장하며 범위는 약 -2^31부터 2^31-1까지입니다.
- long: 64비트의 정수를 저장하며 범위는 약 -2^63부터 2^63-1까지입니다.
- float: 32비트의 부동소수점 수를 저장합니다.
- double: 64비트의 부동소수점 수를 저장합니다.
- char: 16비트의 Unicode 문자를 저장합니다.
- boolean: 두 개의 가능한 값인 true 또는 false를 나타내는 논리값을 저장합니다.
Primitive 타입의 특징:
- 고정된 크기: 각각의 primitive 타입은 고정된 메모리 크기를 가집니다.
- 효율성: 객체를 사용하는 것에 비해 메모리 사용량이 적고 처리 속도가 빠르기 때문에 성능이 중요한 상황에서 유리합니다.
- 스택 메모리 저장: Primitive 타입의 값들은 스택 메모리에 직접 저장됩니다.
- null 값 미포함: Primitive 타입의 변수는 null 값을 가질 수 없습니다.
- Wrapper 클래스: Java는 각 primitive 타입에 대응하는 Wrapper 클래스를 제공합니다. 예를 들면, int에는 Integer, char에는 Character 등이 있습니다. 이 Wrapper 클래스들은 primitive 타입의 데이터를 객체로 취급할 필요가 있을 때 사용됩니다.
Primitive 타입은 기본적인 연산 및 데이터 처리를 위한 기본 구성 요소로서, 객체 지향 언어의 복잡한 객체 구조보다 간단하고 빠른 처리를 위해 사용됩니다.
Reference 타입
Reference 타입은 객체의 메모리 주소를 참조하는 변수 타입입니다. 대부분의 프로그래밍 언어에서, 객체를 생성하면 해당 객체는 메모리의 힙(Heap) 영역에 할당되며, 그 객체를 참조하기 위한 변수가 필요하게 됩니다. 이런 변수가 바로 Reference 타입 변수입니다.
Java에서의 Reference 타입에 대해 자세히 살펴보겠습니다:
- 객체 참조: Reference 타입 변수는 객체의 메모리 주소를 저장합니다. 이 변수를 통해 객체에 접근하고 해당 객체의 메서드를 호출하거나 속성을 변경할 수 있습니다.
- 기본적인 Reference 타입: Java에서는 배열, 클래스, 인터페이스, 열거형(enum) 등이 Reference 타입에 속합니다.
- null 값: Reference 타입 변수는 null 값을 가질 수 있습니다. 이는 해당 변수가 현재 어떤 객체도 참조하고 있지 않다는 것을 의미합니다.
- 힙 메모리: 생성된 객체는 힙 영역에 저장되며, Reference 타입 변수는 이 힙 영역의 메모리 주소를 참조합니다.
- Garbage Collection: Java에서 Reference 타입 변수가 더 이상 해당 객체를 참조하지 않게 되면, 해당 객체는 가비지 컬렉터(Garbage Collector)에 의해 메모리에서 제거될 수 있습니다.
- 동일한 객체 참조: 두 개 이상의 Reference 타입 변수가 동일한 객체를 참조할 수 있습니다. 이 경우, 한 변수를 통해 객체의 속성을 변경하면, 다른 변수를 통해서도 그 변경된 속성을 확인할 수 있습니다.
예시:
String str1 = new String("Hello, World!"); // str1은 "Hello, World!" 문자열 객체를 참조하는 Reference 변수입니다.
String str2 = str1; // str2도 같은 "Hello, World!" 문자열 객체를 참조합니다.
이러한 Reference 타입의 특성은 메모리 관리, 객체 공유, 객체의 동적 생성 및 제거 등에서 중요한 역할을 합니다.
Primitive 타입과 Reference 타입의 차이점
Primitive 타입과 Reference 타입은 여러 중요한 차이점을 가지고 있습니다. 아래에 주요 차이점을 나열해 보았습니다:
- 저장되는 값:
- Primitive 타입: 실제 값이 변수에 직접 저장됩니다.
- Reference 타입: 변수는 메모리 주소(참조)를 저장하며, 이 주소는 힙 영역에 저장된 객체를 가리킵니다.
- 메모리 영역:
- Primitive 타입: 값은 스택(Stack) 메모리 영역에 저장됩니다.
- Reference 타입: 참조(주소)는 스택에 저장되지만, 실제 객체의 데이터는 힙(Heap) 메모리 영역에 저장됩니다.
- 크기:
- Primitive 타입: 고정된 크기를 가집니다. 예를 들어, Java에서 int는 항상 32비트를 차지합니다.
- Reference 타입: 크기가 동적으로 결정됩니다, 객체의 크기나 배열의 크기에 따라 달라질 수 있습니다.
- 기본값:
- Primitive 타입: 각 타입별로 기본값이 존재합니다. 예를 들어, int의 경우는 0, boolean은 false입니다.
- Reference 타입: 기본값은 null입니다.
- 속도:
- Primitive 타입: 직접 값에 접근하기 때문에 Reference 타입에 비해 일반적으로 빠르게 동작합니다.
- Reference 타입: 메모리 주소를 통해 간접적으로 객체에 접근해야 합니다.
- 가변성:
- Primitive 타입: 불변(Immutable)입니다. 값이 변경되면 새로운 공간에 저장됩니다.
- Reference 타입: 객체 자체의 상태는 변경 가능한 경우(Mutable)가 있습니다. 예를 들어, Java의 ArrayList와 같은 컬렉션 객체들은 내부의 요소를 변경할 수 있습니다.
- 동등성 검사:
- Primitive 타입: == 연산자를 사용하여 값 자체를 비교합니다.
- Reference 타입: == 연산자는 참조(메모리 주소)를 비교합니다. 객체의 내용을 비교하기 위해서는 보통 equals() 메서드를 사용합니다.
이러한 차이점들은 프로그래밍을 할 때 데이터의 저장, 메모리 관리, 성능 최적화 등 다양한 측면에서 고려해야 할 중요한 요소들입니다.
Primitive 타입과 Reference 타입의 차이점 간결하게 정리
Primitive 타입과 Reference 타입의 주요 차이점은 다음과 같습니다:
- 저장하는 데이터:
- Primitive 타입: 실제 값 자체가 저장됩니다.
- Reference 타입: 메모리 주소(참조)가 저장됩니다.
- 메모리 영역:
- Primitive 타입: 값이 스택 영역에 저장됩니다.
- Reference 타입: 참조는 스택에 있고, 실제 객체는 힙 영역에 저장됩니다.
- 생명주기:
- Primitive 타입: 변수의 생명주기와 연결됩니다.
- Reference 타입: Java와 같은 언어에서 가비지 컬렉터에 의해 관리되며, 더 이상 참조되지 않을 때 회수됩니다.
- 크기:
- Primitive 타입: 항상 고정된 크기를 가집니다.
- Reference 타입: 객체의 크기나 구조에 따라 동적으로 변합니다.
- 기본값:
- Primitive 타입: 각 타입별로 기본값이 정해져 있습니다.
- Reference 타입: 초기값은 대개 null입니다.
- 동등성 비교:
- Primitive 타입: 값 자체를 비교합니다.
- Reference 타입: 메모리 주소(참조)를 비교합니다. 내용 비교를 위해 별도의 메서드가 필요합니다.
이러한 차이점들은 프로그래밍의 다양한 상황에서 데이터의 처리 방식이나 메모리 관리 전략 등에 큰 영향을 미치므로, 이 차이를 정확히 알고 있어야 합니다.
벨류 타입 (Value types)
벨류 타입(Value types)은 일반적으로 값 자체에 의해 정의되는 데이터 타입을 나타냅니다. 이는 참조 타입(Reference types)과 대비되는 개념입니다. 벨류 타입은 특정 프로그래밍 언어나 프레임워크에서 사용되는 용어이며, 그 의미나 동작 방식은 사용되는 맥락에 따라 다를 수 있습니다.
- 저장되는 데이터:
- 벨류 타입은 실제 값을 저장합니다. 값 타입의 변수나 필드가 다른 변수나 필드에 할당되면, 실제 값이 복사됩니다.
- 동등성:
- 일반적으로 벨류 타입은 내부의 실제 값을 기반으로 동등성이 평가됩니다.
- 생명 주기:
- 벨류 타입의 생명 주기는 그것이 저장된 스택 또는 힙(객체 내부의 필드의 경우)의 생명 주기에 바인딩됩니다.
- 변경 불가능성:
- 많은 벨류 타입은 불변(Immutable)으로 설계됩니다. 즉, 일단 생성된 후에는 내부의 값이 변경되지 않습니다.
- 메모리 위치:
- 벨류 타입은 일반적으로 스택에 할당되지만, 객체의 필드로 정의된 경우에는 해당 객체의 일부로서 힙에 저장될 수 있습니다.
- 예시:
- C#에서 struct는 벨류 타입의 한 예입니다.
- Java에서는 기본 데이터 타입(primitive types)이 벨류 타입에 가깝게 동작하지만, 벨류 타입과 같은 명확한 구분은 없습니다.
마지막으로, 언어나 프레임워크에 따라 "벨류 타입"의 정의와 동작이 약간 다를 수 있습니다. 따라서 특정 맥락에서의 정확한 정의와 성질을 항상 확인해야 합니다.
벨류 타입(Value types)과 참조 타입(Reference types)의 차이점
- 벨류 타입 (Value types):
- 데이터 저장: 벨류 타입은 실제 값을 직접 저장합니다.
- 메모리 영역: 대부분 스택 메모리에 저장됩니다.
- 복사: 변수에서 다른 변수로 값을 할당할 때 실제 값이 복사됩니다.
- 동등성 비교: 저장된 실제 값 자체를 기반으로 비교합니다.
- 변경 가능성: 일부 언어에서는 벨류 타입이 불변(Immutable)하도록 설계될 수 있습니다.
- 예시: int, float, double, char 등의 기본 데이터 타입들이 벨류 타입의 대표적인 예입니다.
- 참조 타입 (Reference types):
- 데이터 저장: 참조 타입은 메모리 주소를 저장합니다. 이 주소는 실제 데이터가 저장된 힙 메모리의 위치를 가리킵니다.
- 메모리 영역: 변수는 스택에 저장되지만, 그 변수가 참조하는 객체 자체는 힙 메모리 영역에 저장됩니다.
- 복사: 변수에서 다른 변수로 값을 할당할 때 메모리 주소가 복사됩니다. 따라서 두 변수는 동일한 객체를 참조하게 됩니다.
- 동등성 비교: 기본적으로 메모리 주소를 기반으로 비교합니다. 하지만 객체의 내용 자체를 비교하기 위한 메서드(예: equals() in Java)를 사용할 수 있습니다.
- 변경 가능성: 대부분의 참조 타입은 변경 가능(Mutable)합니다.
- 예시: 객체, 배열, 클래스 인스턴스 등이 참조 타입의 대표적인 예입니다.
이 두 타입의 주된 차이점은 "실제 값을 직접 저장하는가" 혹은 "메모리 주소를 저장하여 값을 간접적으로 참조하는가"에 있습니다. 이 차이점은 프로그래밍에서 데이터의 처리 방식과 메모리 관리 전략에 큰 영향을 미칩니다.
자바 → call by value OR call by reference?
- 자바 → call by value
Java의 호출 메커니즘에 대한 질문은 흔한 혼동을 야기하는 주제입니다. Java는 기술적으로 "call by value"입니다. 그러나 참조 타입을 다룰 때 이것이 어떻게 작동하는지 이해하는 것이 중요합니다.
- 기본 타입(Primitive types)인 경우: Java는 순수하게 call by value입니다. 메서드에 기본 타입의 변수를 전달할 때, 해당 값의 복사본이 전달됩니다. 따라서 원래 변수의 값은 메서드 내에서 변경될 수 없습니다.
- 참조 타입(Reference types)인 경우: 여기서 혼동이 시작됩니다. 객체 참조(즉, 메모리 주소)가 값으로 전달됩니다. 이 주소의 복사본이 메서드에 전달되기 때문에, 기술적으로는 여전히 call by value입니다. 그러나, 이 복사된 참조를 통해 메서드 내에서 객체의 상태를 변경할 수 있습니다. 그렇지만 전달된 참조 자체(즉, 메모리 주소)를 변경하면 원래 변수에는 영향을 주지 않습니다.
간단한 예제로 설명하겠습니다:
public void modifyReference(StringBuilder builder) {
builder = new StringBuilder("New String");
// 여기서 builder 참조는 새로운 객체를 가리키게 되지만 원래의 참조는 변경되지 않습니다.
}
public void modifyObject(StringBuilder builder) {
builder.append(" Modified");
// 여기서 원래 객체의 상태가 변경됩니다.
}
이 두 메서드를 호출하면 modifyReference는 원래의 참조에 영향을 주지 않지만, modifyObject는 원래의 객체를 변경합니다.
결론적으로, Java는 기본적으로 "call by value"이지만, 참조 타입의 변수를 전달할 때 전달되는 값은 메모리 주소의 복사본입니다. 이 주소를 통해 실제 객체의 상태를 변경할 수 있습니다. 이러한 동작 때문에, 종종 Java가 "call by reference"처럼 느껴질 수 있습니다.
- 느낌점 - 자바가 call by value라는 것을 이해하기 어려웠던 이
- 참조 타입이라도 값을 변경하는 것은 메모리에 있는 값은 변경될 수 있어도 call by value라는 의미는 내가 앞에서 말한 의미가 아니라 메모리 참조 변수에 저장된 값을 변경하는 거라 그 메모리 주소에 잇는 값은 변경되는 것이 아니라 아까 메모리 변수에 저장되어 있는 값 즉 메모리 주소를 바꾸는 것이라 call by value가 맞다
Primitive 타입과 Reference 타입 메모리 저장되는지, 데이터의 크기나 동적 할당, 동등성 비교에서의 차이
- Java의 기본 타입(Primitive types)과 참조 타입(Reference types)은 메모리에서 다르게 저장되며, 그로 인해 데이터의 크기, 동적 할당, 동등성 비교 등에서 차이점을 보입니다.
메모리 저장
- 기본 타입 (Primitive types)
- 저장 위치: 스택 메모리에 직접 저장됩니다.
- 데이터 저장: 실제 값이 직접 저장됩니다.
- 참조 타입 (Reference types)
- 저장 위치: 변수의 참조(주소)는 스택 메모리에 저장되고, 실제 데이터나 객체는 힙 메모리 영역에 저장됩니다.
- 데이터 저장: 스택에는 메모리 주소(참조)가 저장되고, 해당 주소의 힙 메모리 영역에 실제 데이터가 저장됩니다.
데이터의 크기 및 동적 할당
- 기본 타입 (Primitive types)
- 크기: 각 타입마다 정해진 크기가 있습니다 (예: int는 32비트).
- 동적 할당: 기본 타입은 동적으로 할당되지 않습니다. 선언과 동시에 스택 메모리에 고정된 크기로 할당됩니다.
- 참조 타입 (Reference types)
- 크기: 참조(주소)의 크기는 JVM 아키텍처에 따라 다르지만, 흔히 32비트 또는 64비트입니다. 실제 데이터의 크기는 객체의 내용에 따라 다릅니다.
- 동적 할당: 객체는 new 키워드를 사용해 동적으로 힙 메모리에 할당됩니다.
동등성 비교
- 기본 타입 (Primitive types)
- 값 자체를 기준으로 == 연산자로 비교합니다. 값이 동일하면 true를 반환합니다.
- 참조 타입 (Reference types)
- == 연산자는 참조(주소)를 비교합니다. 즉, 두 변수가 동일한 객체를 가리키는지 확인합니다.
- 객체의 내용 자체를 비교하려면, equals() 메서드를 사용해야 합니다 (이 메서드는 클래스에 따라 오버라이드 될 수 있음).
이러한 차이점들은 Java에서 데이터를 처리하거나 메모리 관리 전략을 구성할 때 중요한 역할을 합니다.
call by value의 문장의 의미
"Call by value"는 프로그래밍에서 인자(argument)를 메서드나 함수에 전달할 때 그 인자의 실제 값을 복사하여 전달하는 방식을 의미합니다.
다시 말하면, "call by value" 방식에서:
- 함수나 메서드를 호출할 때 사용하는 인자의 실제 값이 해당 함수나 메서드로 복사됩니다.
- 이 복사된 값은 함수나 메서드 내에서 독립적인 값으로 동작하므로, 함수나 메서드 내에서 이 값을 변경해도 원래의 인자에는 아무런 영향을 미치지 않습니다.
Java는 "call by value" 방식을 사용합니다. 그러나, Java에서 참조 타입(객체들)을 메서드에 전달할 때는 해당 객체의 참조(주소)의 복사본이 전달됩니다. 이 때문에, 메서드 내에서 이 복사된 참조를 사용하여 객체의 내용을 변경하면 원본 객체에도 그 변경이 반영됩니다. 그럼에도 불구하고, 이 복사된 참조 자체를 변경(재할당)하는 것은 원본 참조에 아무런 영향을 미치지 않습니다.
- 즉 월래 복사해서 하는 것이 call by value가 맞다. 값에 의한 호출은 지나친 의역이였다.
스택 메모리 (Stack Memory) 와 힙 메모리 (Heap Memory)
자바에서 스택(Stack) 메모리와 힙(Heap) 메모리의 주요 특징과 차이점에 대해 한국어로 설명하겠습니다.
1. 스택 메모리 (Stack Memory)
- 특징: 메서드나 함수의 호출과 관련된 로컬 변수와 실행 정보를 저장하는 메모리 영역입니다.
- 저장 내용: 기본 타입(primitive type)의 변수, 참조 타입(reference type)의 변수(객체의 참조값)
- 메모리 관리: 함수나 메서드가 종료될 때 해당 함수나 메서드에 대한 스택 프레임이 자동으로 제거됩니다.
장점:
- 자동 메모리 관리로 인해 개발자가 별도의 관리를 신경 쓸 필요가 없습니다.
- 메모리 접근 속도가 빠르므로 성능에 유리합니다.
단점:
- 고정된 크기를 가지므로 크기가 큰 데이터를 저장하기에는 부적합합니다.
2. 힙 메모리 (Heap Memory)
- 특징: 객체와 인스턴스 데이터를 저장하는 동적 메모리 영역입니다.
- 저장 내용: 참조 타입의 실제 객체 데이터
- 메모리 관리: 가비지 컬렉터에 의해 더 이상 참조되지 않는 객체가 메모리에서 제거됩니다.
장점:
- 동적으로 메모리를 할당받을 수 있어서 크기가 큰 데이터나 복잡한 데이터 구조를 저장하기에 적합합니다.
단점:
- 가비지 컬렉터의 동작으로 인해 메모리 관리에 별도의 주의가 필요합니다.
- 스택 메모리에 비해 접근 속도가 상대적으로 느릴 수 있습니다.
요약하면, 스택 메모리는 빠르고 자동으로 관리되는 영역이며 주로 로컬 변수와 실행 정보를 저장합니다. 반면, 힙 메모리는 객체와 인스턴스 데이터를 저장하는 동적 메모리 영역으로, 가비지 컬렉터에 의해 관리됩니다.
기본 타입과 참조 타입의 저장 방식이 스택과 힙 메모리를 사용하는 이유와 장단점
자바에서 기본 타입(primitive type)과 참조 타입(reference type)의 메모리 저장 방식은 서로 다릅니다. 이 두 타입이 스택과 힙 영역에 저장되는 방식과 그에 따른 장단점에 대해 설명하겠습니다.
1. 기본 타입 (Primitive Types)
- 저장 위치: 스택(Stack) 영역
- 설명: 기본 타입의 변수는 스택 메모리에 직접 값이 저장됩니다.
장점:
- 스택 메모리는 자동으로 관리되므로, 변수의 생명 주기가 끝나면 메모리에서 자동으로 제거됩니다. 따라서 메모리 누수의 위험이 적습니다.
- 접근 속도가 빠르므로 효율적인 실행을 보장합니다.
단점:
- 크기가 고정적이므로 동적으로 크기를 변경할 수 없습니다.
- 복잡한 데이터 구조를 표현할 수 없습니다.
2. 참조 타입 (Reference Types)
- 저장 위치: 힙(Heap) 영역
- 설명: 참조 타입의 변수(객체)는 힙 메모리에 생성되며, 이 객체를 가리키는 참조는 스택 메모리에 저장됩니다.
장점:
- 동적으로 메모리 크기를 할당받을 수 있습니다. (예: 배열, 리스트 등의 크기 변경)
- 복잡한 데이터 구조와 객체 지향 프로그래밍의 특징을 활용할 수 있습니다.
단점:
- 힙 영역은 가비지 컬렉터에 의해 관리되므로, 사용하지 않는 객체를 즉시 메모리에서 제거하지 않을 수 있습니다. 이로 인해 메모리 누수가 발생할 가능성이 있습니다.
- 스택 영역에 비해 접근 속도가 상대적으로 느릴 수 있습니다.
요약하면, 스택 영역은 빠르고 자동으로 관리되는 특징을 갖지만, 크기가 고정되어 있습니다. 반면, 힙 영역은 동적으로 크기를 할당받을 수 있으나, 메모리 관리에 주의가 필요합니다.
참조 타입에서 동일한 객체를 가리키는지 동등성 비교 방법
자바에서 참조 타입의 동등성을 비교하는 것은 두 가지 주요 관점이 있습니다:
- 참조 동등성 (Reference Equality)
- 구조적 동등성 (Structural Equality) 또는 내용적 동등성
1. 참조 동등성 (Reference Equality):
- 비교 방법: == 연산자 사용
- 설명: 두 참조 변수가 동일한 객체를 가리키고 있는지를 검사합니다.
String str1 = new String("hello");
String str2 = new String("hello");
if (str1 == str2) {
System.out.println("참조 동등성이 있다.");
} else {
System.out.println("참조 동등성이 없다.");
}
// 출력: 참조 동등성이 없다.
위 예시에서 str1과 str2는 동일한 문자열 값을 가지고 있지만, 두 참조 변수는 서로 다른 객체를 가리키므로 == 연산자는 false를 반환합니다.
2. 구조적 동등성 (Structural Equality) 또는 내용적 동등성:
- 비교 방법: equals() 메서드 사용
- 설명: 두 객체의 내용이 동일한지를 검사합니다.
String str1 = new String("hello");
String str2 = new String("hello");
if (str1.equals(str2)) {
System.out.println("구조적 동등성이 있다.");
} else {
System.out.println("구조적 동등성이 없다.");
}
// 출력: 구조적 동등성이 있다.
여기서는 str1과 str2가 동일한 문자열 값을 가지므로 equals() 메서드는 true를 반환합니다.
주의점:
- Object 클래스의 기본 equals() 메서드 구현은 == 연산자와 동일하게 동작합니다. 따라서 내용적 동등성을 원하는 경우에는 해당 클래스에서 equals() 메서드를 재정의(오버라이드) 해야 합니다.
- equals() 메서드를 오버라이드할 때, hashCode() 메서드도 함께 오버라이드하는 것이 좋습니다. 이는 두 객체가 동등하다면, 그들의 해시 코드도 동등해야 한다는 일반 규약 때문입니다.
요약하면, 참조 동등성은 두 참조 변수가 동일한 객체를 가리키는지 검사하고, 구조적 동등성은 두 객체의 내용이 동일한지 검사합니다.
참조 동등성과 구조적 동등성을 비교와 장단점. 그리고 스택과 힙 메모리의 자동 관리와 GC(Garbage Collection)의 동작 원리
참조 동등성은 같은 메모리 주소를 가리키는 지 여부 확인, 구조적 동등성은 참조하고 있는 메모리의 주소의 실제 데이터의 값이 같은 지 여부를 확인한다. 어는 것이 더 일반적인 경우인지는 딱히 정해지지 않았고 정해지기도 어렵다. 어느 동등성 확인 방법이 더 적합한 지 그 경우에 맞게 선택해 쓰는 것이 더 중요하다. 스택은 푸시되면 나중에 푸시(PUSH)된 것이 먼저 팝(POP)되는 경우라 개발자가 따로 수동적으로 메모리 해제를 해주지 않아도 거의 대부분 자동적으로 조절 된다. (스택의 원리상). GC의 동작 원리은 ROOT 객체를 모두 찾고(루트 객체는 메인 스택 또는 전역 변수와 같은 기본 진입점에서 직접 참조되는 객체) 그 ROOT 객체에서 참고 하고 있는 모든 객체들을 마킹한다. 이것이 Java 어플리케이션에서 사용하고 있는 메모리상에서의 객체라고 간주/인정하고 나머지는 필요없는 사용하지 않는 객체라고 보고 삭제한다 이것이 garbage collacter가 하는 일인 GC다.
GC에서 가비지 컬렉션 동작 방식
Java의 Garbage Collection (GC)는 JVM (Java Virtual Machine) 내의 메모리 관리를 위해 사용되는 자동화된 프로세스입니다. 주된 목적은 사용되지 않는 객체를 메모리에서 제거하여 메모리 누수를 방지하고, 사용 가능한 메모리를 최대화하는 것입니다.
GC의 동작 원리와 알고리즘을 간략하게 설명하겠습니다:
1. Generational Garbage Collection:
Java의 메모리 관리 전략 중 가장 중요한 것은 '세대별 수집'입니다. 힙 메모리는 크게 Young Generation, Old Generation, 그리고 Permanent Generation (Java 8 이후에는 Metaspace)으로 나뉩니다.
- Young Generation: 새로 생성된 객체들이 배치되는 영역입니다. 이 영역은 다시 Eden Space와 두 개의 Survivor Space (S0와 S1)으로 나뉩니다.
- Old Generation: Young 영역에서 살아남은 객체들이 이동하는 곳입니다.
- Permanent Generation/Metaspace: JVM의 메타데이터, 클래스, 정적 변수 등이 저장되는 영역입니다.
2. 가비지 컬렉션의 단계:
- Minor GC: Young Generation 내에서 발생하며, 대부분의 객체는 Young 영역에서 빠르게 소멸되기 때문에 비교적 자주 발생하며, 짧은 시간 동안 실행됩니다.
- Major GC (Full GC): Old Generation에서 발생합니다. 이 수집은 Young 영역의 수집보다 더 오래 걸리며, 전체 힙을 대상으로 하기 때문에 애플리케이션의 퍼포먼스에 큰 영향을 줄 수 있습니다.
3. 가비지 컬렉션 알고리즘:
- Serial Garbage Collector: 단일 스레드를 사용하여 Young Generation 영역의 메모리를 정리합니다.
- Parallel (or Throughput) Garbage Collector: 다중 스레드를 사용하여 Young Generation 영역을 병렬로 수집합니다.
- Concurrent Mark Sweep (CMS) Collector: Old Generation에 중점을 둔 GC로, 애플리케이션의 응답 시간이 중요할 때 사용됩니다.
- G1 Garbage Collector: 힙을 여러 영역으로 나누고, 각 영역을 독립적으로 수집하여, 정지 시간을 최소화하려는 목적으로 설계되었습니다.
4. 가비지 컬렉션의 동작 원리:
- Mark: GC는 'Roots'부터 시작하여 접근 가능한 객체를 탐색하고 마킹합니다.
- Sweep: 마킹되지 않은 객체, 즉 사용되지 않는 객체를 힙에서 제거합니다.
- Compact: 메모리를 재정렬하여 연속된 공간을 만들고, 메모리 단편화를 방지합니다.
최종적으로, GC는 메모리 관리를 자동화하지만, 때로는 애플리케이션의 퍼포먼스에 영향을 줄 수 있기 때문에, GC의 동작 방식과 알고리즘을 이해하고 올바르게 설정하는 것이 중요합니다.
ROOT 객체를 찾고 마킹 과정에서 어떤 기준으로 객체를 인정하고 삭제 과정과 장단점
Garbage Collection (GC)의 기본 원리는 사용되지 않는 메모리를 자동으로 회수하는 것입니다. Java의 JVM(Java Virtual Machine)에서는 주로 Generational Garbage Collection 전략을 사용합니다. 이전략에서 GC는 크게 두 단계, 즉 "마킹(Marking)"과 "삭제(Deletion)" 또는 "수거(Sweep)"로 나뉩니다.
1. ROOT 객체 찾기:
GC는 시작점으로 "ROOT 객체"를 사용합니다. ROOT 객체란 직접적으로 다른 객체에 의해 참조되지 않고도 접근 가능한 객체를 의미합니다. 예를 들면, 스택에서의 지역 변수, 메서드의 파라미터, 클래스의 정적 변수 등이 ROOT 객체에 해당합니다.
2. 마킹 (Marking) 과정:
이 단계에서는 GC가 ROOT 객체부터 시작하여 참조되는 모든 객체를 재귀적으로 탐색합니다. 이 과정에서 참조되는 모든 객체는 "살아있는(live)" 객체로 마킹됩니다.
- 참조되는 객체: ROOT 객체에서 직접적으로나 간접적으로 참조할 수 있는 객체는 모두 마킹됩니다.
- 참조되지 않는 객체: 이런 객체들은 마킹 과정에서 탐색되지 않습니다. 따라서, 이 객체들은 다음 단계에서 수거될 대상이 됩니다.
3. 삭제 (Deletion) 또는 수거 (Sweep) 과정:
마킹 단계를 거친 후에, 마킹되지 않은 객체들, 즉 참조되지 않는 객체들은 메모리에서 제거됩니다. 이렇게 해서, 사용되지 않는 객체로 인해 낭비되는 메모리를 회수하게 됩니다.
장점:
- 자동화된 메모리 관리로 개발자의 부담을 줄여줍니다.
- 메모리 누수의 가능성을 줄여줍니다.
단점:
- GC가 작동할 때, 애플리케이션의 성능이 일시적으로 저하될 수 있습니다.
- GC의 작동 시점은 예측하기 어렵습니다.
결론적으로, GC는 JVM 내에서 사용되지 않는 메모리를 효과적으로 회수하는 데 중요한 역할을 합니다. 하지만 GC의 동작 방식과 그에 따른 영향을 잘 이해하고, 필요한 경우 JVM의 GC 옵션을 조절하여 최적의 성능을 얻을 수 있어야 합니다.
GC의 다양한 알고리즘과 장단점
Java의 Garbage Collection (GC)에는 여러 알고리즘이 있습니다. 각 알고리즘은 특정 유스케이스나 환경에 잘 맞도록 설계되었으며, 각각의 장단점을 가지고 있습니다. 주요 GC 알고리즘들의 특징과 장단점을 살펴보겠습니다:
- Serial Garbage Collector:
- 작동 방식: 단일 스레드로 동작합니다. GC가 실행될 때 애플리케이션 스레드가 중단됩니다.
- 장점: 단순하고 오버헤드가 적습니다. 힙 크기가 작고 스레드 수가 적을 때 잘 작동합니다.
- 단점: 멀티 스레드 환경에서는 GC 중에 모든 스레드가 중단되기 때문에 성능에 큰 영향을 줄 수 있습니다.
- Parallel (Throughput) Garbage Collector:
- 작동 방식: 멀티 스레드를 사용하여 Young Generation 영역을 병렬로 수집합니다.
- 장점: 멀티 스레드 애플리케이션에서 잘 작동합니다.
- 단점: Old Generation의 수집 중 애플리케이션 스레드가 중단될 수 있습니다.
- Concurrent Mark-Sweep (CMS) Collector:
- 작동 방식: 애플리케이션 스레드와 동시에 수행되는 GC 스레드를 사용하여 Old Generation을 주로 수집합니다.
- 장점: 애플리케이션의 응답 시간이 중요한 경우에 적합합니다. GC 중에도 애플리케이션 스레드가 계속 실행됩니다.
- 단점: CPU 리소스를 더 많이 사용하며, 메모리 단편화 문제가 발생할 수 있습니다.
- G1 Garbage Collector:
- 작동 방식: 힙을 여러 영역으로 나누고, 각 영역을 독립적으로 수집합니다.
- 장점: 정지 시간을 예측 가능하게 최소화하려는 목적으로 설계되었습니다. 큰 힙 크기에도 잘 작동합니다.
- 단점: CMS보다 더 많은 CPU 리소스를 사용할 수 있습니다.
- *ZGC (Z Garbage Collector)**와 Shenandoah GC (Java 11 이후에 도입된 알고리즘):
- 작동 방식: 거의 중단 시간 없이 대규모 힙을 수집합니다.
- 장점: 초단위 미만의 GC 중지 시간과 대규모 힙을 지원합니다.
- 단점: 아직 초기 버전이기 때문에, 다른 GC 알고리즘에 비해 테스트와 검증이 덜 되었을 수 있습니다.
결론적으로, 가장 적합한 GC 알고리즘을 선택하려면 애플리케이션의 요구사항과 환경을 고려해야 합니다. 중요한 것은 알고리즘의 특징과 장단점을 이해하는 것입니다.
보통의 경우 JVM에서 기본으로 채택하고 있는 GC 알고리즘
JVM의 기본 GC 알고리즘은 버전 및 JVM 배포판에 따라 다를 수 있습니다. 하지만 Oracle의 HotSpot JVM을 기준으로 설명하겠습니다.
- Java 8까지: 기본적으로 Parallel (Throughput) Garbage Collector를 사용하였습니다. 이 GC는 멀티 스레드 환경에서 효과적으로 동작하며, 주로 서버 환경에서 선호되었습니다.
- Java 9 이후: G1 Garbage Collector가 기본 GC로 채택되었습니다. G1 GC는 대규모 힙 영역에서도 예측 가능한 정지 시간을 제공하는 것을 목표로 개발되었습니다.
그러나 이러한 기본값들은 JVM의 시작 옵션을 통해 쉽게 변경할 수 있습니다. 따라서 애플리케이션의 요구사항과 작업 부하에 따라 적절한 GC 알고리즘을 선택하는 것이 중요합니다.
'[F-Lab 멘토링 학습]' 카테고리의 다른 글
| API 게이트웨이 (0) | 2023.09.19 |
|---|---|
| 도커 (Docker) (1) | 2023.09.18 |
| 프래피티 STUDY - 다이나믹 배열, 해시 함수 속성 및 유형 (0) | 2023.09.17 |
| 쿠버네티스 (K8S) (0) | 2023.09.15 |
| JAVA에서 Thread 구현 방법 (0) | 2023.09.12 |