상속과 오버라이딩
상속과 오버라이딩은 객체 지향 프로그래밍(OOP)의 중요한 개념입니다. 이 두 개념을 이해하면 코드 재사용성을 높이고, 더 구조적이고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
상속(Inheritance)
상속은 기존 클래스(부모 클래스 또는 슈퍼 클래스)의 속성과 메서드를 새로운 클래스(자식 클래스 또는 서브 클래스)에서 사용할 수 있게 하는 기능입니다. 상속을 통해 코드의 재사용성을 높일 수 있으며, 클래스 간의 관계를 명확하게 정의할 수 있습니다.
- 기본 개념:
- 자식 클래스는 부모 클래스의 모든 필드와 메서드를 상속받습니다.
- 자식 클래스는 부모 클래스의 기능을 확장하거나 변경할 수 있습니다.
- 상속은 "is-a" 관계를 나타냅니다. 예를 들어, Dog 클래스가 Animal 클래스를 상속받는다면, "Dog is an Animal"이 됩니다.
- 예시:
// 부모 클래스 public class Animal { String name; public void eat() { System.out.println("이 동물은 음식을 먹습니다."); } }// 자식 클래스 public class Dog extends Animal { public void bark() { System.out.println("개가 짖습니다."); } }
- 사용:
Dog myDog = new Dog(); myDog.name = "Buddy"; myDog.eat(); // 부모 클래스의 메서드 사용
-
myDog.bark(); // 자식 클래스의 메서드 사용
오버라이딩(Overriding)
오버라이딩은 부모 클래스에서 상속받은 메서드를 자식 클래스에서 재정의하여 사용하는 것을 말합니다. 이를 통해 자식 클래스는 부모 클래스의 메서드를 자신에게 맞게 변경할 수 있습니다.
- 기본 개념:
- 메서드의 이름, 반환형, 매개변수가 부모 클래스의 메서드와 동일해야 합니다.
- 자식 클래스에서 오버라이딩한 메서드는 부모 클래스의 메서드를 덮어씁니다.
- 오버라이딩은 런타임 다형성을 가능하게 합니다.
- 예시:
// 부모 클래스 public class Animal { public void eat() { System.out.println("이 동물은 음식을 먹습니다."); } }// 자식 클래스 public class Dog extends Animal { @Override public void eat() { System.out.println("개가 사료를 먹습니다."); } public void bark() { System.out.println("개가 짖습니다."); } }
- 사용:
Animal myAnimal = new Animal(); myAnimal.eat(); // "이 동물은 음식을 먹습니다."Dog myDog = new Dog(); myDog.eat(); // "개가 사료를 먹습니다." myDog.bark(); // "개가 짖습니다."
상속과 오버라이딩의 활용
- 상속을 통해 코드 재사용성을 높이고, 새로운 기능을 추가하여 기존 코드를 확장할 수 있습니다.
- 오버라이딩을 통해 자식 클래스에서 부모 클래스의 메서드를 자신에게 맞게 재정의할 수 있으며, 이를 통해 런타임 다형성을 구현할 수 있습니다.
결론
상속과 오버라이딩은 객체 지향 프로그래밍에서 매우 중요한 개념입니다. 상속을 통해 코드 재사용성을 높이고 클래스 간의 관계를 명확히 정의할 수 있으며, 오버라이딩을 통해 자식 클래스에서 부모 클래스의 메서드를 재정의하여 다형성을 구현할 수 있습니다. 이 두 개념을 잘 이해하고 활용하면 더 구조적이고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
오버라이딩
- 상위 클래스에 있는 메소드를 하위 클래스에서 재정의 하는 것
오버로딩
- 매개변수의 개수나 타입을 다르게 하여 같은 이름의 메소드를 여러 개 정의하는 것
추상 클래스 : 클래스 내 추상 메서드가 하나 이상 포함되거나 abstract로 정의된 경우
인터페이스 : 모든 메서드가 추상 메서드로만 이루어져있는 것
공통점 | 차이점 |
- new 연산자로 인스턴스 생성 불가 - 사용하기 위해서 하위 클래스에서 확장, 구현 해야함 |
- 인터페이스는 인터페이스를 구현하는 모든 클래스에 대해 특정한 메서드가 반드시 존재하도록 강제함 - 인터페이스는 다중 상속이 가능함 - 추상 클래스는 상속 받는 클래스들의 공통적인 로직을 추상화 시키고, 기능 확장을 위해 사용 - 추상 클래스는 다중 상속이 불가능 |
추상 클래스와 인터페이스는 둘 다 자바에서 클래스의 설계를 정의하는 데 사용되지만, 목적과 사용 방법에 차이가 있습니다.
추상 클래스는 인스턴스를 생성할 수 없고, 하나 이상의 추상 메서드를 포함합니다. 구현된 메서드도 포함할 수 있으며, 상속을 통해 기능을 확장합니다. 다중 상속이 불가능합니다.
인터페이스는 모든 메서드가 추상 메서드로만 이루어져 있으며, 구현을 강제합니다. 자바 8 이후에는 디폴트 메서드와 static 메서드도 지원합니다. 다중 상속이 가능하며, 클래스가 여러 인터페이스를 구현할 수 있습니다.
주요 차이점:
구현 강제: 인터페이스는 구현을 강제하지만, 추상 클래스는 일부 구현이 가능합니다.상속: 추상 클래스는 단일 상속만 지원하며, 인터페이스는 다중 상속이 가능합니다.
스레드: 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위.
프로그램, 프로세스, 스레드의 개념
프로그램은 저장 장치에 저장된 정적인 파일 상태를 의미합니다. 이는 메모리에 올라가 있지 않으며, 실행되지 않은 코드 덩어리라고 할 수 있습니다. 예를 들어, 윈도우의 .exe 파일이나 MacOS의 .dmg 파일 등이 프로그램에 해당합니다.
프로세스는 프로그램이 실행되어 메모리에 올라간 상태를 의미합니다. 이때 프로그램은 운영체제로부터 독립적인 메모리 공간을 할당받아 동적인 상태로 전환됩니다. 따라서 프로세스는 실행 중인 컴퓨터 프로그램을 가리키며, 이는 작업 단위로 스케줄링됩니다.
프로세스와 스레드의 차이
프로세스는 운영체제로부터 독립된 메모리 공간을 할당받아 실행됩니다. 이는 Code, Data, Stack, Heap 메모리 영역으로 나뉩니다. 각각의 프로세스는 독립된 메모리 영역을 가지므로 다른 프로세스의 메모리에 접근할 수 없습니다. 따라서 하나의 프로세스가 오류로 종료되더라도 다른 프로세스에는 영향을 미치지 않습니다.
스레드는 프로세스 내에서 실행되는 더 작은 실행 단위입니다. 스레드는 프로세스의 Code, Data, Heap 메모리 영역을 공유하며, 각각의 스레드는 독립된 Stack 영역을 가집니다. 여러 스레드가 동시에 실행될 때 서로 메모리를 공유하므로 통신 부담이 적고 응답 시간이 빠릅니다. 그러나 하나의 스레드가 오류를 일으키면 동일한 프로세스 내의 다른 스레드들도 영향을 받아 종료될 수 있습니다.
면접 답변 예시
Q: 프로세스와 스레드의 차이에 대해 말해주세요.
A: 프로세스와 스레드는 실행 단위의 차이를 나타냅니다. 프로세스는 실행 중인 프로그램으로, 독립적인 메모리 공간을 할당받아 다른 프로세스와 메모리를 공유하지 않습니다. 반면, 스레드는 프로세스 내에서 실행되는 작은 단위로, 프로세스의 메모리 공간을 공유합니다. 이러한 메모리 공유로 인해 스레드 간의 통신 부담이 적고 응답 시간이 빠르지만, 하나의 스레드에서 발생한 오류가 다른 스레드에 영향을 미칠 수 있습니다. 반면 프로세스는 독립적이기 때문에 한 프로세스의 오류가 다른 프로세스에 영향을 주지 않습니다.
추가적인 설명
운영체제는 프로세스마다 Code, Data, Stack, Heap 메모리 영역을 할당합니다. 그러나 스레드는 Stack 영역을 제외한 Code, Data, Heap 메모리 영역을 공유합니다.
멀티태스킹은 여러 프로세스가 동시에 실행되는 것을 의미하며, 멀티스레딩은 하나의 프로세스 내에서 여러 스레드가 동시에 실행되는 것을 의미합니다. 멀티스레딩의 장점으로는 메모리 자원 절약과 빠른 응답 시간을 들 수 있지만, 동기화 문제와 스레드 간의 충돌 가능성도 존재합니다.
프로세스 간 정보 공유는 IPC(Inter-Process Communication) 등의 방법을 사용해야 하며, 이는 자원 소모가 큰 작업입니다. 반면 스레드는 기본적으로 메모리를 공유하므로 정보 공유가 더 효율적입니다.
결론적으로, 프로세스와 스레드는 각각의 특성과 장단점을 이해하고 적절하게 활용해야 시스템 자원을 효율적으로 사용할 수 있습니다.
- Context Switching(컨텍스트 스위칭)은 여러 프로세스를 처리해야 하는 멀티 프로세스 상황에서 인터럽트(interrupt)에 의해 현재 진행중인 Task(Process, Thread)의 상태 또는 레지스터 값(Context)을 PCB(Process Control Block)에 저장하고 CPU가 다음 프로세스를 처리할 수 있도록 새로운 프로세스의 상태 또는 레지스터 값을 교체하는 작업
- 여러개의 프로세스가 실행되고 있을때 이전에 실행되던 프로세스를 중단하고 다른 프로세스를 실행하는것. CPU에 실행할 프로세스를 교체하는 기술
컨텍스트 스위칭은 컴퓨터의 CPU가 하나의 프로세스 또는 스레드에서 다른 프로세스 또는 스레드로 작업을 전환하는 과정입니다. 이 과정은 멀티태스킹 운영체제에서 매우 중요하며, CPU의 효율적인 사용을 가능하게 합니다.
컨텍스트 스위칭의 과정
- 현재 프로세스 상태 저장:
- CPU는 현재 실행 중인 프로세스의 상태(레지스터, 프로그램 카운터, 스택 포인터 등)를 저장합니다.
- 새로운 프로세스 상태 로드:
- 저장된 상태 정보를 메모리 또는 프로세스 제어 블록(PCB)에 보관한 후, 새롭게 실행할 프로세스의 상태 정보를 로드합니다.
- 프로세스 실행:
- CPU는 새로운 프로세스의 상태 정보를 바탕으로 작업을 계속합니다.
컨텍스트 스위칭의 필요성
컨텍스트 스위칭은 다음과 같은 상황에서 필요합니다.
- 멀티태스킹: 여러 프로세스가 동시에 실행되는 환경에서 CPU가 빠르게 전환하면서 각각의 프로세스가 실행되는 것처럼 보이게 합니다.
- 입출력 대기: 프로세스가 입출력 작업을 기다리는 동안 CPU가 유휴 상태로 있지 않고 다른 작업을 수행할 수 있도록 합니다.
- 우선순위 처리: 긴급한 작업이나 우선순위가 높은 프로세스를 빠르게 실행하기 위해 필요한 전환을 합니다.
컨텍스트 스위칭의 비용
컨텍스트 스위칭에는 시간과 리소스가 필요합니다. 이를 컨텍스트 스위칭 오버헤드라고 합니다. 주요 비용 요소는 다음과 같습니다.
- CPU 사이클 소모: 프로세스 상태를 저장하고 로드하는 작업은 추가적인 CPU 사이클을 필요로 합니다.
- 캐시 무효화: 캐시 메모리는 프로세스 전환 시 무효화되어 다시 로드하는 데 시간이 걸립니다.
- 메모리 접근: 프로세스의 상태를 메모리에 저장하고 로드하는 작업은 메모리 접근 시간을 필요로 합니다.
컨텍스트 스위칭의 장점
- 효율적인 CPU 사용: CPU가 유휴 상태로 있는 시간을 줄이고, 항상 작업을 수행하도록 합니다.
- 향상된 응답 시간: 사용자 인터페이스의 반응 속도를 높여줍니다.
- 공정한 자원 분배: 모든 프로세스가 CPU 시간을 공정하게 배분받을 수 있게 합니다.
결론
컨텍스트 스위칭은 멀티태스킹 운영체제에서 중요한 역할을 하며, CPU의 효율성을 높이고 시스템의 전반적인 성능을 향상시킵니다. 하지만 이 과정에는 오버헤드가 존재하므로, 이를 최소화하기 위한 다양한 최적화 기법이 필요합니다. 효과적인 컨텍스트 스위칭은 시스템의 성능과 사용자 경험에 큰 영향을 미칩니다.
1. 등장 배경
Null 참조 문제: Null 참조는 프로그래밍 언어에서 매우 흔한 문제로, 참조 변수가 어떤 객체도 가리키지 않을 때 발생합니다. 이를 잘못 처리하면 NullPointerException이 발생해 프로그램이 비정상적으로 종료될 수 있습니다. Null 참조 문제를 효과적으로 해결하지 못하면 코드가 복잡해지고 오류가 발생하기 쉬워집니다.
안전한 참조 처리: Null을 안전하게 처리하기 위해 많은 방어 코드가 필요합니다. 예를 들어, null 체크를 하는 if 문이 많아지면서 코드가 장황해지고 가독성이 떨어지게 됩니다. 이를 해결하기 위해 Optional이 도입되었습니다.
2. Optional의 개념
Optional이란: Optional은 값이 존재할 수도 있고 존재하지 않을 수도 있는 컨테이너 객체입니다. 이는 명시적으로 값이 없을 수 있는 상황을 표현하며, Null을 대체할 수 있는 안전한 방식입니다. Java 8에서 처음 도입되었으며, Optional을 사용하면 NullPointerException을 방지하고, 더 명확하고 가독성 좋은 코드를 작성할 수 있습니다.
3. Optional의 활용
Optional 생성: Optional 객체는 주로 세 가지 방식으로 생성할 수 있습니다.
- Optional.of(T value): 값이 null이 아닌 경우 사용하며, null이면 NullPointerException을 던집니다.
- Optional.ofNullable(T value): 값이 null일 수 있는 경우 사용합니다.
- Optional.empty(): 빈 Optional 객체를 생성합니다.
Optional<String> nonEmptyOptional = Optional.of("Hello"); Optional<String> nullableOptional = Optional.ofNullable(null); Optional<String> emptyOptional = Optional.empty();
값의 존재 확인: Optional은 값이 존재하는지 확인하는 메서드를 제공합니다.
- isPresent(): 값이 존재하면 true, 아니면 false를 반환합니다.
- ifPresent(Consumer<? super T> action): 값이 존재할 때만 주어진 동작을 수행합니다.
if (nonEmptyOptional.isPresent()) { System.out.println(nonEmptyOptional.get()); } nonEmptyOptional.ifPresent(value -> System.out.println(value));
값 추출: Optional에서 값을 안전하게 추출하는 방법은 여러 가지가 있습니다.
- get(): 값을 반환하며, 값이 없으면 NoSuchElementException을 던집니다.
- orElse(T other): 값이 없으면 기본값을 반환합니다.
- orElseGet(Supplier<? extends T> other): 값이 없으면 람다 표현식이나 메서드 참조로 제공된 기본값을 반환합니다.
- orElseThrow(Supplier<? extends X> exceptionSupplier): 값이 없으면 예외를 던집니다.
String value1 = nonEmptyOptional.get(); String value2 = nullableOptional.orElse("Default Value"); String value3 = nullableOptional.orElseGet(() -> "Generated Default Value"); String value4 = nullableOptional.orElseThrow(() -> new IllegalArgumentException("No value present"));
값 변환 및 조작: Optional은 값이 있을 때 변환하거나 조작할 수 있는 메서드를 제공합니다.
- map(Function<? super T, ? extends U> mapper): 값을 변환하여 새로운 Optional을 반환합니다.
- flatMap(Function<? super T, Optional<U>> mapper): Optional을 평면화하여 중첩된 Optional을 방지합니다.
- filter(Predicate<? super T> predicate): 조건을 만족하는 경우 값을 포함하는 Optional을 반환합니다.
Optional<Integer> length = nonEmptyOptional.map(String::length); Optional<String> upperCase = nonEmptyOptional.map(String::toUpperCase); Optional<String> filteredOptional = nonEmptyOptional.filter(value -> value.length() > 5);
4. 결론
Optional을 사용하면 NullPointerException을 방지하고 코드의 명확성과 가독성을 높일 수 있습니다. 이는 특히 대규모 프로젝트나 팀 작업에서 코드의 안정성과 유지보수성을 크게 향상시킵니다. Optional을 적절히 활용하여 더 안전하고 직관적인 코드를 작성할 수 있습니다.
- ofNullable(T value)
- 설명: 주어진 값이 null일 수 있을 때 Optional 객체를 생성합니다.
- 사용 상황: 값이 null일 가능성이 있을 때 안전하게 Optional을 생성할 때 사용합니다.
- 예시: Optional<String> optional = Optional.ofNullable(someValue);
- isPresent()
- 설명: Optional에 값이 존재하면 true를, 그렇지 않으면 false를 반환합니다.
- 사용 상황: Optional에 값이 존재하는지 확인할 때 사용합니다.
- 예시: if (optional.isPresent()) { System.out.println(optional.get()); }
- ifPresent(Consumer<? super T> action)
- 설명: 값이 존재할 때 주어진 동작을 수행합니다.
- 사용 상황: 값이 존재할 때만 특정 작업을 수행하고 싶을 때 사용합니다.
- 예시: optional.ifPresent(value -> System.out.println(value));
- orElse(T other)
- 설명: 값이 존재하면 그 값을 반환하고, 그렇지 않으면 다른 값을 반환합니다.
- 사용 상황: 값이 없을 때 기본값을 제공하고 싶을 때 사용합니다.
- 예시: String value = optional.orElse("Default Value");
- orElseGet(Supplier<? extends T> other)
- 설명: 값이 존재하면 그 값을 반환하고, 그렇지 않으면 Supplier가 제공하는 값을 반환합니다.
- 사용 상황: 값이 없을 때 동적으로 기본값을 생성하고 싶을 때 사용합니다.
- 예시: String value = optional.orElseGet(() -> "Generated Default Value");
- orElseThrow(Supplier<? extends X> exceptionSupplier)
- 설명: 값이 존재하면 그 값을 반환하고, 그렇지 않으면 Supplier가 제공하는 예외를 던집니다.
- 사용 상황: 값이 없을 때 예외를 던지고 싶을 때 사용합니다.
- 예시: String value = optional.orElseThrow(() -> new IllegalArgumentException("No value present"));
- map(Function<? super T, ? extends U> mapper)
- 설명: Optional의 값을 변환하여 새로운 Optional을 반환합니다.
- 사용 상황: Optional의 값을 다른 형태로 변환하고 싶을 때 사용합니다.
- 예시: Optional<Integer> length = optional.map(String::length);
- filter(Predicate<? super T> predicate)
- 설명: 조건을 만족하는 경우에만 값을 포함하는 Optional을 반환합니다.
- 사용 상황: 특정 조건에 맞는 값만 남기고 싶을 때 사용합니다.
- 예시: Optional<String> filtered = optional.filter(value -> value.length() > 5);
결론
Optional을 사용하면 NullPointerException을 방지하고 코드의 안정성을 높일 수 있습니다. 위에 언급한 주요 메소드를 상황에 맞게 활용하면 더 안전하고 가독성 높은 코드를 작성할 수 있습니다.
Optional에서 사용하는 주요 메소드들은 다음과 같습니다:
- of(): 주어진 값이 null이 아닐 때 Optional 객체를 생성합니다. 이 메소드는 null을 허용하지 않으므로, null 값이 들어올 경우 NullPointerException이 발생합니다. 일반적으로 값이 확실히 존재할 때 사용합니다.
- ofNullable(): 주어진 값이 null일 수도 있는 경우 Optional 객체를 생성합니다. 값이 null이면 빈 Optional 객체를 반환합니다. 값의 존재 여부가 불확실할 때 유용합니다.
- get(): Optional 객체에서 값을 가져옵니다. 값이 존재하지 않을 경우 NoSuchElementException이 발생하므로, 사용 전에 반드시 isPresent() 메소드로 값의 존재 여부를 확인해야 합니다.
- isPresent(): Optional 객체에 값이 존재하는지를 확인합니다. 값이 존재하면 true, 그렇지 않으면 false를 반환합니다. 값의 존재 여부를 체크할 때 사용합니다.
- ifPresent(): Optional 객체에 값이 존재할 경우, 해당 값을 소비하는 람다식을 실행합니다. 값을 안전하게 사용할 수 있는 경우에 적합합니다.
- orElse(): Optional 객체에 값이 존재하지 않을 때 대체할 기본 값을 제공하는 메소드입니다. 값이 없을 경우 기본 값을 반환하도록 할 때 사용합니다.
- orElseGet(): orElse()와 유사하지만, 대체 값을 Supplier 인터페이스를 통해 제공받습니다. 기본 값을 계산하는 비용이 클 경우 유용합니다.
- orElseThrow(): Optional 객체에 값이 없을 경우 사용자 정의 예외를 발생시킵니다. 예외 처리를 통해 프로그램의 흐름을 제어할 수 있습니다.
이러한 메소드들은 주로 null 체크를 간소화하고, 코드의 가독성을 높이며, NullPointerException을 방지하는 데 사용됩니다. Optional을 활용함으로써 보다 안전하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
'🗂️ etc' 카테고리의 다른 글
스파르타 APM 화상면접 준비 (0) | 2024.08.20 |
---|---|
4차(마지막) 모의 면접 준비 (0) | 2024.08.17 |
기술 면접 Top 30 (0) | 2024.07.17 |
2차 모의 면접 피드백 (0) | 2024.07.03 |
2차 모의 면접 준비 (0) | 2024.07.02 |