개발자 면접질문 정리 - 자바
💡 제네릭에 대해 설명해주시고, 왜 쓰는지 알려주세요.
제네릭은 자바에서 타입(type) 안정성을 제공하고, 캐스팅(casting) 문제를 줄이기 위해 사용하는 프로그래밍 기법입니다. 클래스, 인터페이스, 또는 메소드를 정의할 때 타입을 파라미터로 받을 수 있게 해서, 다양한 타입의 객체들을 다룰 수 있게 해줍니다. 예를 들어, List<E>
는 제네릭 인터페이스로, E 타입의 객체를 저장하는 리스트를 생성할 수 있습니다. 그리고 제네릭을 사용하면 컴파일 시점에 타입 체크를 할 수 있어서, 실행 시간에 발생할 수 있는 타입 캐스팅 오류를 방지할 수 있습니다. 또, 하나의 코드를 다양한 타입에 재사용할 수 있기 때문에, 코드의 중복을 줄이고, 유지보수가 편리해집니다.
💡 final / finally / finalize 의 차이를 설명해주세요
final
키워드는 변수, 메소드, 또는 클래스에 사용될 수 있습니다. 변수에 사용되면 그 변수는 상수가 되어 변경할 수 없습니다. 메소드에 사용되면 해당 메소드는 오버라이드될 수 없으며, 클래스에 사용되면 그 클래스는 상속될 수 없습니다.
finally
블록은 예외 처리 구조에서 try
블록 뒤에 사용되며, 예외 발생 여부와 관계없이 실행되는 코드를 포함합니다. 이는 주로 자원을 해제하거나 정리하는데 사용됩니다.
finalize()
메소드는 객체가 가비지 컬렉터에 의해 회수되기 전에 호출되는 메소드입니다. 자원 회수, 메모리 정리 등 마지막으로 수행해야 할 작업을 정의할 때 사용됩니다. 하지만 사용을 권장하지 않으며, java.lang.Object
의 finalize()
는 자바 9부터 deprecated되었습니다.
💡 직렬화(Serialize)에 대해 설명해주세요.
시스템 내부에서 사용되는 객체 또는 데이터를 외부의 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 데이터 변환하는 기술이며, 반대로 직렬화된 바이트 형태의 데이터를 다시 객체로 변환하는 과정을 ‘역직렬화’라고 합니다. 자바에서의 직렬화를 예로 들어보면, JVM의 메모리에 상주(힙 or 스택)되어 있는 객체 데이터를 바이트 형태로 변환하는 작업을 들 수 있습니다.
💡 Set과 Map의 타입이 Wrapper Class가 아닌 Object를 받을 때 중복 검사는 어떻게 할건지 설명해주세요.
hashCode() 메소드를 오버라이딩하여 리턴된 해시코드 값이 같은지를 보고 해시코드 값이 다르다면 다른 객체로 판단하고, 해시코드 값이 같으면 equals() 메소드를 오버라이딩하여 다시 비교해서 이 두 개가 모두 맞으면 중복 객체입니다.
💡 컬렉션 프레임워크에 대해 설명해주세요.
다수의 데이터를 쉽고 효과적으로 관리할 수 있는 표준화된 방법을 제공하는 클래스의 집합을 의미합니다. 자바 컬렉션에는 List, Set, Map 인터페이스를 기준으로 여러 구현체가 존재하고, 이에 더해 Stack, Queue 인터페이스도 존재합니다.
List는 순서가 있는 데이터의 집합으로, 데이터의 중복을 허용합니다. ArrayList와 LinkedList 등이 있습니다.
Set는 중복을 허용하지 않는 데이터의 집합입니다. 주요 구현체로는 HashSet, LinkedHashSet, TreeSet 등이 있습니다.
Map은 키와 값의 쌍으로 이루어진 데이터 집합입니다. 키는 중복될 수 없으며, HashMap, TreeMap, LinkedHashMap 등이 있습니다.
Queue는 FIFO(First In First Out) 또는 우선순위에 따라 요소를 처리하는데 사용됩니다. LinkedList, PriorityQueue 등이 이에 해당합니다.
Stack은 Last In First Out으로 요소를 처리하는데 사용되며 직접 new 키워드로 사용할 수 있습니다.
💡 Call by reference란 무엇이고 보통 어떻게 쓰이나요?
콜 바이 레퍼런스는 함수 호출 방식 중 하나로 값이 아닌 메모리 주소를 함수에 인자로 전달하는 방식입니다. 그렇기 때문에 전달된 변수를 변경하면 원본 값이 변경됩니다. 그래서 콜 바이 레퍼런스는 보통 함수로 여러 개의 값을 반환해야 할 때, 파라미터로 매우 큰 데이터 구조를 전달해야할 때, 그리고 전역 변수를 변경하거나 수정해야할 때 사용됩니다.
💡 Override 와 Overload 를 설명해주실 수 있을까요?
오버라이드는 상위 클래스가 가지고 있는 메서드를 하위 클래스에서 재정의하는 것을 말합니다. 오버라이드 성립 조건은 메서드 이름과 파라미터의 개수와 타입이 동일해야 한다는 점이며, 주로 상위 클래스의 동작을 하위 클래스에서 변경하기 위해 사용합니다. 반면에 오버로딩은 파라미터의 개수나 타입을 다르게 해서 같은 이름의 함수를 정의하는 것을 말합니다. 리턴값만을 다르게 갖는 오버로딩은 불가능하다는 것에 유의해야 합니다.
💡 JVM 이란 무엇이고 왜 필요한지 설명해주실 수 있을까요?
JVM, 즉 Java Virtual Machine은 자바 프로그램이 실행될 수 있는 환경을 제공하는 가상 머신입니다. 그리고 그런 환경을 제공하기 위해 자바 바이트코드(.class 파일)를 운영 체제에 특화된 코드로 변환해서 실행시키는 역할을 수행합니다. 이는 자바 프로그램이 운영 체제와 직접적인 상호작용 없이 실행될 수 있게 하여, 한 번의 코드 작성으로 어떤 플랫폼에서도 실행될 수 있는 ‘Write Once, Run Anywhere’ 원칙을 가능하게 합니다. 그래서 개발자는 동일한 애플리케이션을 다양한 운영 체제에서 재구성할 필요 없이 효율적으로 소프트웨어를 배포할 수 있습니다.
JVM의 또 다른 중요한 역할은 메모리 관리와 가비지 컬렉션입니다. JVM은 실행 중인 프로그램의 객체 생명주기를 관리하고, 사용되지 않는 객체를 감지해서 메모리에서 제거합니다. 그래서 개발자가 메모리 누수 문제를 걱정하지 않도록 도와주고, 애플리케이션의 안정성과 성능을 향상시킵니다.
또한, JVM은 Just-In-Time (JIT) 컴파일러를 통해 프로그램의 실행 속도를 최적화합니다. JIT 컴파일러는 프로그램 실행 중에 바이트코드의 ‘핫 스팟‘을 식별하고, 더 효율적인 기계어 코드로 변환하여 이를 가능하게 합니다. 이런 동적 컴파일 과정은 프로그램이 더 빠르고 효율적으로 실행되도록 돕습니다.
💡 Java가 컴파일되는 과정은 어떻게 되는지 설명해주실 수 있을까요?
컴파일 과정은 소스 코드 작성, 컴파일, 실행까지의 순서로 진행됩니다. 개발자가 자바 소스코드(.java)를 작성하면, 자바 컴파일러가 이를 바이트코드(.class)로 변환하고, 그 과정 중에 문법 검사를 합니다. 컴파일된 바이트코드는 JVM에서 기계어로 변환되서 실행되는데, 그 과정에서 JVM은 Just-In-Time 컴파일러로 실행 속도를 최적화합니다. 이 방식은 ‘Write Once, Run Anywhere’ 철학을 가능하게 합니다.
💡 객체 지향 프로그래밍이란 무엇이고 어떻게 활용할 수 있나요?
객체 지향 프로그래밍은 소프트웨어 설계 패러다임으로, 프로그램을 객체들의 상호작용으로 구성하는 방식입니다. 객체는 데이터(속성)와 이를 조작하는 함수(메서드)를 함께 묶어 놓은 독립적인 단위입니다. 주요 개념에는 클래스, 객체, 상속, 캡슐화, 다형성, 추상화가 포함됩니다.
- 클래스(Class): 객체를 정의하는 청사진으로, 객체의 속성과 메서드를 정의합니다.
- 객체(Object): 클래스의 인스턴스로, 실제 데이터와 기능을 갖고 있는 실체입니다.
- 상속(Inheritance): 기존 클래스를 기반으로 새로운 클래스를 생성하여 코드 재사용성을 높이는 기능입니다.
- 캡슐화(Encapsulation): 객체의 데이터를 외부로부터 숨기고, 객체 내부에서만 접근하도록 하는 원칙입니다.
- 다형성(Polymorphism): 동일한 인터페이스나 메서드가 다양한 형태로 동작할 수 있도록 하는 능력입니다.
- 추상화(Abstraction): 복잡한 시스템에서 중요한 부분만을 간략하게 표현하고, 세부 사항은 감추는 방식입니다.
이런 객체 지향 프로그래밍은 상속을 통한 코드 재사용성 증가, 캡슐화를 통한 유지보수성 향상, 추상화를 통한 코드 가독성 및 관리 용이성 증가, 다형성을 이용한 유연한 설계로 활용될 수 있습니다.
💡 try-with-resources에 대해 설명해주세요.
try-with-resources는 try-catch-finally의 문제점을 보완하기 위해 나온 개념입니다.
try( … ) 안에 자원 객체를 전달하면, try블록이 끝나고 자동으로 자원 해제 해주는 기능을 말합니다.
그렇기 때문에 따로 finally 구문이나 모든 catch 구문에 종료 처리를 하지 않아도 되는 장점이 있습니다.
💡 불변 객체가 무엇인지 설명하고 대표적인 Java의 예시를 설명해주세요.
불변 객체는 객체 생성 이후 내부의 상태가 변하지 않는 객체를 말합니다.
Java에서는 필드가 원시 타입인 경우 final 키워드를 사용해 불변 객체를 만들 수 있고, 참조 타입일 경우엔 추가적인 작업이 필요합니다.
참조 타입일 경우 추가적인 작업은 어떤게 있는지 설명해주세요.
참조 타입은 대표적으로 1.객체를 참조할 수도 있고, 2.배열이나 3.List 등을 참조할 수 있습니다.
참조 변수가 일반 객체인 경우 객체를 사용하는 필드의 참조 변수도 불변 객체로 변경해야 합니다.
배열일 경우 배열을 받아 copy해서 저장하고, getter를 clone으로 반환하도록 하면 됩니다. (배열을 그대로 참조하거나, 반환할 경우 외부에서 내부 값을 변경할 수 있음. 때문에 clone을 반환해 외부에서 값 변경하지 못하게 함)
리스트인 경우에도 배열과 마찬가지로 생성시 새로운 List를 만들어 값을 복사하도록 해야 합니다. 배열과 리스트는 내부를 복사하여 전달하는데, 이를 방어적 복사(defensive-copy)라고 합니다.
불변 객체나 final을 굳이 사용해야 하는 이유가 있을까요?
불변 객체나 final 키워드를 사용해 얻는 이점은 다음과 같습니다.
Thread-Safe하여 병렬 프로그래밍에 유용하며, 동기화를 고려하지 않아도 된다. (공유 자원이 불변이기 때문에 항상 동일한 값을 반환하기 때문)
실패 원자적인 메소드를 만들 수 있다. (어떠한 예외가 발생되더라도 메소드 호출 전의 상태를 유지할 수 있어 예외 발생 전과 똑같은 상태로 다음 로직 처리 가능)
부수효과를 피해 오류를 최소화 할 수 있다. ※ 부수효과 : 변수의 값이 바뀌거나 객체의 필드 값을 설정하거나 예외나 오류가 발생하여 실행이 중단되는 현상
메소드 호출 시 파라미터 값이 변하지 않는다는 것을 보장할 수 있다.
가비지 컬렉션 성능을 높일 수 있다. (가비지 컬렉터가 스캔하는 객체의 수가 줄기 때문에 Gc 수행 시 지연시간도 줄어든다.)
💡 객체지향의 설계원칙에 대해 설명해주세요.
객체지향 설계원칙은 로버트 마틴이 발표한 것으로 SOLID라는 약자로 많이 불리며 설명되어집니다.
S에 해당하는 단일 책임 원칙은 ‘하나의 객체가 하나의 책임만 져야 한다.’는 것을 의미합니다. 이 것은 애플리케이션 모듈 전반에서 높은 유지보수성과 가시성 제어 기능을 유지하는 원칙입니다.
다음 O는 개방-폐쇄 원칙으로 ‘소프트웨어 컴포넌트는 확장에 관해 열려있어야 하고 수정에 대해서는 닫혀 있어야 한다.’는 것을 의미합니다. 이 것은 다른 개발자가 작업을 수행하기 위해 반드시 수정해야 하는 제약 사항을 클래스에 포함해서는 안 된다는 사실을 의미합니다. 다른 개발자가 클래스를 확장하기만 하면 원하는 작업을 할 수 있도록 해야 합니다. 이런 특징때문에 이 원칙은 다양하고 직관적이며 유해하지 않은 방식으로 소프트웨어 확장성을 유지합니다.
다음 L은 리스코프 치환 원칙으로 ‘파생 타입은 반드시 기본 타입을 완벽하게 대체할 수 있어야 한다.’는 것을 의미합니다. 이 원칙을 따르면 타입 변환 후에 뒤 따라오는 런타임 타입 식별에 우용합니다. 예를 들어, 어떤 메서드의 파라미터 p의 타입이 T라고 하면 T의 하위 타입인 S인 q가 해당 메서드의 파라미터로 대체가능해야 합니다.
I는 인터페이스 분리 원칙으로 ‘클라이언트가 사용하지 않을 불필요한 메서드를 강제로 구현하지 않을 때까지 인터페이스를 분할해야 한다.’는 것을 의미합니다.
마지막으로 D는 의존관계 역전 원칙으로 ‘구체화가 아닌 추상화에 의존해야 한다는 사실’을 의미합니다. 이 원칙을 따름으로써 구체화된 모듈은 분리된 상태를 유지하면서 다른 모듈의 기능 또는 플러그인을 확장할 수 있습니다. 예를 들어, 클라이언트가 상속 관계로 이루어진 모듈을 가져다 사용할 때, 하위 모듈을 실제 구현체인 인스턴스를 가져다 쓰면 하위 모듈에 변화가 있을 때마다 클라이언트나 상위 모듈의 코드를 자주 수정해야 하는데, 상위의 인터페이스 타입의 객체로 통신하게 되면 하위 모듈을 바꿔야할 때 코드를 변경할 필요가 없고 또다른 하위 모듈의 확장에도 무리가 없습니다.