국비지원 (스파르타)

[면접공부] 자바 - 면접질문(3) (feat.gpt)

klom 2025. 4. 14. 23:50

오늘을 끝으로 자바는 당분간 마무리짓고 DB를 진행할 예정이다. 아 학습안한건 어떻하냐고..
걔네는 나중에 정리해야지.. 근데 당분간은 못할거 같다.
다른것도 해야지... 해야지.. 아무튼 오늘은 내가 개인적으로 모의면접을 부탁해서 고것들을 위주로 학습할예정이다.
수정되있는게 있으면 반영해야지
1. callByValue vs callByReference 
2. 이펙티브 final -> 람다 캡쳐링
3. Runnable vs Thread 
4. for문 vs 람다 forRoop 
5. equals에서 hashcode의 관계

요렇게다 문제 순서는 저렇지는 않는데... 암튼 고건 중요한게 아니구
callByValue vs callByReference  요거같은 경우는 자바에서는 어떤것을 지원하는지 물어보는 질문이었다.
이게... 내가 잘못대답해서 callByReference라고 했는데 callByValue라고 하셨다..ㅜㅜ 반대로 말해버렸다..
아무튼 자바는 callByValue를 지원한다고 한다. 요거에 대해 학습해보자.

void changeName(Person p) {
     p.setName("Alice");
     p = new Person("Bob");  // 이건 외부에 영향 X
}

만약에 요러한 코드가 있다고 가정하고 callByReference를 지원한다고 한다면 C처럼 포인터를 사용하게 되어질거다.
그렇게 되면 매계변수의 Person의 주소값도 함께 넘어가지게 되어진다. 매게 변수의 p와 새롭게 할당된 new Person과 동일하게 되어질 거같다... 고렇게 되면 p의 이름은 Alice가 아니라 Bob이 된다. 하지만 자바는 callByValue이기때문에 요거 자체를 값으로 전달하게 되어진다. 그렇담 결국 매계변수로 전달한 p와 새로 할당한 p는 다른 객체라는 걸 알 수 있을거 같다. 요렇게 아는데 모의면접때.. 왜 대답을 못했지..ㅜㅜ gpt는 머라고 하는지 보자.
자바는 모든 값을 Call by Value로 전달합니다. 다만, 객체의 경우에는 참조값이 복사되어 전달되기 때문에, 마치 Call by Reference처럼 동작하는 것처럼 보일 수 있습니다. 하지만 실제로는 참조값 자체도 값으로 전달되는 거죠.

그렇다는군.. 다음은  이펙티브 final -> 람다 캡쳐링인데 이걸 설명하면 람다를 사용할때는 변수를 사용을 못하는걸로 알고 있다. 그렇기 때문에 값이 변경이 되면 안되기 때문에 final로 상수로 만들어줘야 하는데 이때 설명할 수 있는 용어가 이펙티브 final이라고 알고 있다.
gpt가 문제가 있어서 클로드로 바꿨다.
이펙티브 파이널은 자바에서 명시적으로 final 키워드를 사용하지 않았지만 실질적으로 final처럼 동작하는 변수를 의미합니다. 변수가 선언된 후 단 한 번만 초기화되고 이후에 값이 변경되지 않는 경우, 해당 변수는 '이펙티브 파이널'로 간주됩니다.
그렇담 람다 캡쳐링은 멀까??
람다 캡처(Lambda Capture)는 람다 표현식이 외부 변수를 사용할 때 그 변수의 값을 포착하는 메커니즘입니다.
람다는 final 또는 이펙티브 파이널 변수만 캡처할 수 있습니다. 이는 람다가 생성될 때 변수의 값을 스냅샷처럼 복사하기 때문입니다. 값이 변경된다면 람다가 어떤 값을 참조해야 할지 불분명해지고 동시성 문제가 발생할 수 있습니다.
이렇게 캡처된 값은 람다가 나중에 실행되더라도 생성 시점의 값을 유지합니다. 이는 람다의 실행 컨텍스트와 변수의 생명주기 관리에 중요합니다.
그러면 왜 람다는 복사본을 사용할까? 요건 생각보다 간단하다. 그저 변수관리를 외부에 의존성을 제거하기 위함이다.
이에 대한 답변이다.
람다에서 외부 변수의 값을 복사해서 사용하는 이유는 바로 그것입니다:

  1. 람다의 결과가 외부 상태에 의존하지 않도록 하기 위함입니다
  2. 람다 실행 시점의 외부 변수 상태에 따라 결과가 달라지는 것을 방지합니다
  3. 함수형 프로그래밍의 중요한 원칙인 "참조 투명성"을 유지하기 위함입니다

만약 람다가 외부 변수를 직접 참조하고 그 변수가 변경 가능하다면, 람다의 동작은 실행 시점과 외부 상태에 종속되어 예측하기 어려워집니다. 이는 함수형 프로그래밍이 추구하는 "동일 입력에 대한 동일 출력" 원칙에 위배됩니다.

따라서 자바는 람다가 참조하는 외부 변수를 final 또는 effectively final로 제한하고 값을 복사함으로써, 람다의 동작이 외부 상태 변화에 영향받지 않도록 설계했습니다. 이것이 람다의 일관성과 안정성을 보장하는 핵심 메커니즘입니다.

이게 좀 적을게 많은데 내가 알기로는 사실 람다는 변수도 사용할 수 있다. 근데 일반적인 방법은 아니고 아토믹을 이용하면 된다. 아토믹을 이용할 수 있는 이유는 간단하다. 그저.. 아토믹의 특징이 변화 감지다. 그러니까 결국 람다는 변화감지라는 걸 알고 있기 때문에 아토믹을 사용할 수 있는걸로 알고 있다. 
AtomicInteger나 AtomicReference와 같은 atomic 클래스를 사용하는 것은 다른 문제를 해결하기 위한 접근법입니다.

람다에서 외부 변수를 변경해야 하는 경우에 대한 해결책으로 atomic 클래스가 사용됩니다. 이런 경우 변수 자체는 final이지만, 그 내부 상태는 변경 가능합니다.

근데 내가 어디서 봤는데 람다도 변수(아토믹 아님)를 사용할 수 있는 방법이 있는걸로 알고 있는데 진짠가?
일반적인 변수는 불가하구 다음과 같은 걸로 해결할 수 있다고 한다.

람다에서 변수를 직접 변경하는 방법이 있다고 하셨는데, 자바의 람다 표현식에서는 기본적으로 외부 로컬 변수를 변경할 수 없습니다. 이것은 자바 언어 사양의 제약사항입니다.

하지만 몇 가지 우회 방법이 있습니다:

  1. 배열 활용하기: 단일 요소 배열을 사용하면 그 내용을 변경할 수 있습니다.
 
final int[] counter = {0};  // 배열 참조는 final
someStream.forEach(item -> counter[0]++);  // 배열 내용 변경 가능
  1. 컬렉션 사용하기: List와 같은 컬렉션도 비슷한 방식으로 활용할 수 있습니다.
 
final List<Integer> counter = new ArrayList<>(Arrays.asList(0));
someStream.forEach(item -> counter.set(0, counter.get(0) + 1));
  1. 래퍼 클래스 만들기: 간단한 커스텀 래퍼 클래스를 정의할 수도 있습니다.
 
class MutableInteger {
    int value;
    MutableInteger(int value) { this.value = value; }
}

final MutableInteger counter = new MutableInteger(0);
someStream.forEach(item -> counter.value++);

이런 방법들은 변수 자체(배열, 컬렉션, 객체 참조)는 final로 유지하면서 그 내부 상태를 변경하는 것입니다. 기술적으로는 가능하지만, 함수형 프로그래밍의 원칙에 맞지 않고 다중 스레드 환경에서 안전하지 않을 수 있어 주의해서 사용해야 합니다.

우회방법이구나.. 

거의다 끝나간다. 이제 Runnable vs Thread 요건데
내가 알기로는 Runnable과 Thread의 가장 큰 차이점은 인터페이스인가 아닌가로 나눠지는걸로 알고 있다. 그니까 함수형을 사용하고 싶다면 Runnable을 이용해야 하구 다중 상속이 가능하다는점이 가장 큰 차이점이다. 이걸 조금더 확장하면 Callable이랑 Future도 설명할 수 있을거 같은데.. 요건 어려울거 같다.

네, Runnable과 Thread는 둘 다 Java에서 멀티스레드를 구현할 때 사용되지만, 구조와 목적에 있어서 몇 가지 차이가 있습니다.

우선, Runnable은 인터페이스이고, Thread는 클래스입니다.
Thread는 직접 상속해서 run() 메서드를 오버라이드할 수 있고, Runnable은 해당 인터페이스를 구현하여 run() 메서드를 작성한 뒤 Thread 객체에 넘겨주는 방식으로 사용됩니다.

예를 들어, Runnable을 구현하면 new Thread(new MyRunnable()).start(); 이런 식으로 스레드를 실행하게 되고, Thread를 상속하면 new MyThread().start(); 형태가 됩니다.

중요한 차이는 유연성에 있습니다. Java는 단일 상속만 지원하기 때문에 Thread 클래스를 상속하면 다른 클래스를 더 이상 상속할 수 없습니다. 반면 Runnable은 인터페이스이기 때문에, 다른 클래스를 상속하면서도 멀티스레드를 구현할 수 있어서 보다 유연한 구조를 만들 수 있습니다.

그래서 실무에서는 일반적으로 Runnable을 구현하는 방식이 더 권장됩니다. 특히 스레드와 실행 코드를 명확하게 분리할 수 있기 때문에, 유지보수나 테스트 관점에서도 이점이 있습니다.

그리고 Runnable은 ThreadPool과 같은 스레드 재사용 구조와도 잘 어울려요.
예를 들어 ExecutorService 같은 곳에 넘겨줄 작업은 대부분 Runnable이나 Callable 형태로 정의되기 때문에, 확장성과 연계 측면에서도 Runnable 방식이 유리하다고 생각합니다.
이렇다고 한다.
더 쓰고 싶은데 이제 시간이 없다... 10분...정도..
빨랑 하자 for문 vs 람다 forRoop 다음은 요건데 요거 같은경우는 람다는 forRoop는 가독성 측면에서 좋은걸로 알고 있고 for문은 개인적으로 성능적인 이점이 있는걸로 알고는 있는데 요것도 한번 물어나보자. 얘는 아는데 애매한거 같다.

네, Java에서는 컬렉션을 순회할 때 전통적인 for문, 향상된 for-each문, 그리고 Java 8부터 도입된 람다식 기반의 forEach() 메서드를 사용할 수 있습니다.

전통적인 for문이나 향상된 for-each 문은 구조가 명확하고 직관적이라서 간단한 반복문에서 많이 사용됩니다. 예를 들어 리스트의 값을 출력하거나 순차적으로 처리할 때 적합합니다.

반면, 람다식의 forEach() 메서드는 함수형 스타일로 코드를 작성할 수 있어서 코드가 간결해지고, 스트림(Stream)과 함께 사용하면 더 선언적인 방식으로 데이터 처리를 할 수 있습니다.

차이점은 주로 다음과 같습니다:

  • 람다식은 코드가 간결하지만, 내부적으로 반복을 추상화하기 때문에, 반복 중 break나 continue 같은 제어문은 사용할 수 없습니다.
  • 전통적인 for문은 인덱스를 사용하거나 조건을 유연하게 제어할 수 있어서 더 세밀한 제어가 필요한 경우에 적합합니다.

따라서 상황에 따라 선택하는 것이 중요하다고 생각합니다.
간단한 출력이나 가독성이 중요한 경우엔 forEach + 람다를, 복잡한 조건 제어나 인덱스가 필요한 경우엔 전통적인 for문을 사용하는 게 좋다고 생각합니다.

또 하나 덧붙이자면, forEach()는 내부 반복 방식이고, 전통적인 for는 외부 반복 방식이라는 점도 차이가 있습니다.
스트림과 결합하면 map(), filter() 같은 연산도 함께 적용할 수 있어서, 데이터 처리 파이프라인을 구성할 때는 람다식이 훨씬 더 유리하다고 생각합니다.

  • 간단한 경우에서 for문은 성능이 좋습니다. for문은 직접적인 반복 방식이라 불필요한 추가 작업 없이 바로 값을 처리합니다.
  • 복잡한 스트림 연산과 결합된 경우는 forEach가 더 유리할 수 있지만, 단순히 순회만 하는 경우라면 기본 for문이 성능 면에서 더 유리합니다.

그러네 얘는 continue랑 break를 사용할 수 있고 간단한경우에는 조금더 우수할 수 있겠구나..

이정도면 되는 거 같구 마지막으로 equals에서 hashcode 얘는... 뭔가 알거 같았는데
내가 알기로는 동일한 해쉬값으로 되어있지 않으면 false로 나오는걸로 알고 있기만해서.. 얘도 찾아보자.

equals()와 hashCode()는 자바에서 객체 비교와 관련된 중요한 메서드입니다.
먼저, equals() 메서드는 두 객체가 동등한지 비교하는 메서드입니다. 기본적으로 Object 클래스에서 제공하는 equals()는 참조 비교를 수행하지만, 대부분의 클래스에서는 객체의 내용, 즉 필드 값을 비교하도록 오버라이드합니다. 예를 들어, Person 클래스에서 두 객체의 이름과 나이가 같다면 equals()는 true를 반환해야 합니다.

한편, hashCode() 메서드는 객체를 해시 테이블에서 빠르게 찾을 수 있도록 해시 값을 생성하는 메서드입니다. hashCode()는 컬렉션에서 효율적인 검색을 위해 사용됩니다. hashCode()는 객체의 메모리 주소를 기반으로 해시 값을 생성하며, equals() 메서드와 밀접하게 연관되어 있습니다.

equals()와 hashCode() 사이에는 중요한 계약이 있습니다.

  1. equals()가 true를 반환하는 두 객체는 반드시 동일한 hashCode() 값을 가져야 합니다.
  2. hashCode()가 다른 두 객체는 equals()가 true를 반환할 수 없습니다.
    하지만 hashCode()가 같은 두 객체가 반드시 equals()가 true를 반환해야 하는 것은 아닙니다.

예를 들어, Person 클래스에서 이름과 나이가 같다면 두 객체는 equals()로 비교할 때 true를 반환하고, 같은 hashCode() 값을 가져야 합니다. 하지만 hashCode()가 같은 두 객체라도 내용이 다르면 equals()는 false를 반환할 수 있습니다.

이 두 메서드를 올바르게 구현하는 이유는 주로 해시 기반 컬렉션에서 객체를 정확하게 처리하고 효율적인 검색을 위해서입니다. 예를 들어, HashMap이나 HashSet에서는 hashCode()를 사용해 객체를 빠르게 찾고, 동일한 객체가 저장되지 않도록 equals()로 비교합니다. 이 과정에서 equals()와 hashCode()가 제대로 구현되지 않으면 컬렉션에서 잘못된 동작을 할 수 있습니다.

따라서, equals()와 hashCode()는 자바의 해시 기반 컬렉션에서 매우 중요한 역할을 하며, 둘 다 적절히 오버라이드되어야 합니다.  

이제 다 끝났다... 오늘은 대부분아는건데 뭔가 오히려 허를 찌르는듯한 기분이 들었다. 안다고 방심하지말자.