개발 공부/자바

자바 면접 질문1

baby-t 2025. 9. 24. 09:14

## 1. Java의 주요 특징은 무엇인가요?

Java는 다음과 같은 핵심적인 특징을 가진 프로그래밍 언어입니다.

  • 객체 지향 프로그래밍 (Object-Oriented Programming): Java는 모든 것을 객체로 표현하며, 캡슐화, 상속, 다형성, 추상화라는 OOP의 4대 원칙을 충실히 따릅니다. 이는 코드의 재사용성을 높이고 유지보수를 용이하게 만듭니다.
  • 플랫폼 독립성 (Platform Independence): Java는 JVM(자바 가상 머신) 위에서 동작합니다. 따라서 어떤 운영체제든 JVM만 설치되어 있다면, 한 번 작성한 코드(.class 바이트코드)를 수정 없이 어디서든 실행할 수 있습니다 (Write Once, Run Anywhere).
  • 자동 메모리 관리 (Automatic Memory Management): **가비지 컬렉터(Garbage Collector)**가 더 이상 사용되지 않는 객체를 자동으로 메모리에서 해제해 줍니다. 이를 통해 개발자는 메모리 관리에 대한 부담을 덜고 비즈니스 로직에 더 집중할 수 있습니다.
  • 멀티스레딩 지원 (Multi-threading Support): 언어 자체에서 멀티스레드 프로그래밍을 지원하여, 여러 작업을 동시에 처리하는 고성능 애플리케이션을 쉽게 개발할 수 있도록 돕습니다.
  • 강력한 타입 체크 (Strongly & Statically Typed): Java는 변수를 선언할 때 반드시 타입을 명시해야 하는 정적 타입 언어이며, 동시에 타입 간의 연산을 엄격하게 검사하는 강타입 언어입니다. 컴파일 시점에 타입 오류를 잡아낼 수 있어 프로그램의 안정성을 높여줍니다.
    ### 2x2로 보는 타입 시스템
      정적 타입 (Static) (컴파일 시 타입 결정) 동적 타입 (Dynamic) (실행 시 타입 결정)
    강타입 (Strong) (엄격한 타입 규칙) Java, C#, Scala Python, Ruby
    약타입 (Weak) (느슨한 타입 규칙) C, C++ JavaScript, PHP

     


 

## 2. Java의 예외 처리(Exception Handling)에 대해 설명해주세요.

 

### Java의 예외 처리(Exception Handling) 상세

Java의 예외 처리는 프로그램 실행 중 발생할 수 있는 오류(예외)에 대비하여, 프로그램이 비정상적으로 종료되지 않고 오류를 '처리'하고 정상적인 실행 흐름을 유지하도록 만드는 체계적인 메커니즘입니다. 핵심은 오류 처리 코드를 비즈니스 로직 코드와 분리하여 코드의 가독성과 안정성을 높이는 데 있습니다.

### 1. try-catch-finally의 동작 원리

### try: 예외 감시 구역

try 블록은 예외가 발생할 가능성이 있는 코드를 감싸는 '감시 구역'입니다. 이 블록 안의 코드가 실행되는 동안 예외가 발생하면, JVM은 즉시 해당 예외 객체를 생성하고 코드 실행을 멈춘 뒤, 이 예외를 처리할 수 있는 catch 블록을 찾기 시작합니다.

### catch: 예외 처리 구역

catch 블록은 특정 타입의 예외가 발생했을 때 실행되는 '처리 구역'입니다.

  • 다중 catch 블록: 하나의 try 블록에 여러 종류의 예외를 처리하기 위해 여러 개의 catch 블록을 연결할 수 있습니다. 이때, 더 구체적인 예외(자식 클래스)를 먼저 잡고, 더 일반적인 예외(부모 클래스)를 나중에 잡아야 합니다.
     
    Java
    try {
        // 파일 읽기 및 네트워크 작업
    } catch (FileNotFoundException e) {
        // 파일이 없을 때의 처리
    } catch (IOException e) {
        // 파일 입출력 중 다른 문제가 발생했을 때의 처리
    } catch (Exception e) {
        // 위에서 잡지 못한 모든 나머지 예외 처리
    }
    

### finally: 항상 실행되는 최종 구역

finally 블록은 try-catch 구문의 **'무조건 실행'**을 보장하는 최종 구역입니다.

  • try 블록이 정상적으로 실행을 마쳤을 때
  • catch 블록이 예외를 처리했을 때
  • try나 catch 블록 안에서 return 문을 통해 메소드가 종료될 때

위 모든 상황에서 finally 블록은 반드시 실행됩니다. 이 특성 때문에, 파일 스트림이나 데이터베이스 커넥션과 같이 **반드시 닫아주어야 하는 자원을 해제(resource cleanup)**하는 코드는 finally 블록에 위치시키는 것이 표준적인 사용법입니다.

### 2. 예외 전파와 throws

메소드 내에서 발생한 예외를 try-catch로 직접 처리하지 않으면, 예외는 해당 메소드를 호출한 곳(caller)으로 전파됩니다. 이 책임 전가가 계속해서 **콜 스택(Call Stack)**의 역순으로 일어나다가, 최상위인 main 메소드에서도 처리되지 않으면 프로그램은 비정상적으로 종료됩니다.

throws 키워드는 메소드 선언부에 사용하여, "이 메소드는 이러이러한 Checked Exception을 발생시킬 수 있으니, 나를 호출하는 쪽에서 책임지고 try-catch로 처리해야 합니다"라고 명시적으로 알려주는 역할을 합니다.

Java
// 이 메소드는 IOException을 직접 처리하지 않고, 호출한 쪽으로 책임을 넘깁니다.
public void readFile() throws IOException {
    // ... 파일 읽는 코드 ...
}

 

### 3. 예외 계층 구조와 Checked vs. Unchecked

Java의 모든 예외 클래스는 Throwable 클래스를 상속받으며, 크게 Error와 Exception으로 나뉩니다.

### Error

OutOfMemoryError, StackOverflowError와 같이 시스템 레벨에서 발생하는 심각한 오류입니다. 개발자가 코드 수준에서 복구할 수 없는 문제이므로, 애플리케이션 코드에서 잡으려고 시도해서는 안 됩니다.

### Exception

애플리케이션 코드에서 처리 가능한 모든 예외의 최상위 클래스이며, 다시 CheckedUnchecked로 나뉩니다.

  • Checked Exception
    • RuntimeException을 상속하지 않는 모든 Exception의 자식 클래스입니다.
    • 철학: API를 설계한 사람이 "이 기능을 사용한다면 이런 문제는 충분히 발생할 수 있으니, 반드시 대비해야 한다"고 알려주는 강제적인 예외입니다. IOException(파일이 없거나 읽기 실패), SQLException(DB 접근 실패) 등이 대표적입니다.
    • 처리: 컴파일러가 try-catch나 throws를 사용했는지 검사하므로, 반드시 코드로 처리해야 합니다.
  • Unchecked Exception (Runtime Exception)
    • RuntimeException을 상속하는 모든 클래스입니다.
    • 철학: 주로 개발자의 논리적인 실수나 부주의로 인해 발생하는 예외입니다. (예: NullPointerException, ArrayIndexOutOfBoundsException). "이런 예외는 잡아서 처리할 생각을 하기 전에, 애초에 발생하지 않도록 코드를 제대로 짜야 한다"는 의미를 내포합니다.
    • 처리: 컴파일러가 예외 처리를 강제하지 않습니다.

### 4. 예외 클래스의 종류

#### 1. Checked Exception: 반드시 처리해야 하는 예외

컴파일러가 try-catch나 throws를 사용했는지 직접 확인하는 예외들입니다. 주로 외부 자원과의 통신에서 발생합니다.

  • IOException
    • 원인: 파일을 읽고 쓰는 등 입출력(I/O) 작업 중 문제가 발생했을 때.
    • 예시: 네트워크 연결이 끊기거나, 파일 시스템에 문제가 생겼을 때.
  • FileNotFoundException
    • 원인: 존재하지 않는 파일을 읽으려고 시도할 때 발생합니다. (IOException의 자식 클래스)
    • 예시: new FileInputStream("없는파일.txt");
  • SQLException
    • 원인: 데이터베이스에 접근하여 SQL 쿼리를 실행하는 과정에서 오류가 발생했을 때.
    • 예시: 잘못된 SQL 문법을 사용하거나, DB 연결이 끊겼을 때.
  • ClassNotFoundException
    • 원인: 런타임에 클래스를 찾으려고 했지만, 해당 클래스가 클래스패스(classpath)에 존재하지 않을 때.

#### 2. Unchecked Exception (RuntimeException): 주로 개발자의 실수

컴파일러가 확인하지 않으며, 주로 개발자의 논리적인 실수로 인해 발생하는 예외들입니다.

  • NullPointerException (NPE)
    • 원인: null 값을 가진 참조 변수로 객체의 멤버(필드나 메소드)에 접근하려고 할 때. 가장 흔하게 마주치는 예외입니다.
    • 예시: String name = null; if (name.equals("jemini")) { ... }
  • ArrayIndexOutOfBoundsException
    • 원인: 배열의 범위를 벗어난 인덱스로 접근하려고 할 때.
    • 예시: int[] arr = new int[5]; arr[5] = 10; (인덱스는 0~4까지만 유효)
  • IllegalArgumentException
    • 원인: 메소드에 잘못된 또는 부적절한 인자 값을 전달했을 때.
    • 예시: 나이를 인자로 받는 메소드에 음수 값을 전달했을 때, 개발자가 의도적으로 발생시킬 수 있습니다.
  • IllegalStateException
    • 원인: 메소드를 호출하기에 객체의 상태가 적절하지 않을 때.
    • 예시: 이미 닫힌 데이터베이스 커넥션을 다시 사용하려고 할 때. (회원 관리 예제의 중복 회원 가입 시에도 이 예외를 사용했습니다.)
  • NumberFormatException
    • 원인: 숫자로 변환할 수 없는 문자열을 숫자로 변환하려고 할 때.
    • 예시: Integer.parseInt("abc");
  • ClassCastException
    • 원인: 허용되지 않는 타입으로 객체를 강제 형 변환하려고 할 때.
    • 예시: Object dog = new Dog(); Cat cat = (Cat) dog;

 


 

## 3. 추상 클래스(Abstract Class)와 인터페이스(Interface)의 차이는 무엇인가요?

두 개념 모두 객체 지향의 추상화를 구현하는 핵심적인 방법이지만, 목적과 사용 방식에서 차이가 있습니다.

구분 추상 클래스 (Abstract Class) 인터페이스 (Interface)
핵심 목적 "is-a" 관계: 연관성 높은 클래스들의 공통된 기능과 상태를 추출하여 코드 재사용 "can-do" 관계: 클래스가 수행해야 할 동작(메소드)을 명세하는 역할
다중 상속 불가능 (단일 상속만 허용) 가능 (여러 인터페이스 구현 가능)
멤버 변수 일반 멤버 변수(상태) 소유 가능 public static final 상수만 소유 가능
메소드 추상 메소드 + 일반 메소드 모두 가능 (Java 8+) default, static 메소드를 제외하면 추상 메소드만 가능
키워드 extends (확장) implements (구현)
 

언제 무엇을 쓸까?: "부모-자식" 관계처럼 관련성 높은 클래스들 간에 공통된 코드나 상태를 공유하고 싶다면 추상 클래스를, 서로 관련 없는 클래스들에게 **"이 기능은 반드시 구현해야 해"**라고 기능적 계약을 맺고 싶다면 인터페이스를 사용하는 것이 좋습니다.

### 1. 추상 클래스 예시: "is-a" 관계 (동물)

추상 클래스는 관련성 높은 클래스들의 공통된 특징(상태와 행위)을 추출하여 부모 역할을 하도록 만들 때 사용합니다. '개(Dog)'와 '고양이(Cat)'는 모두 '동물(Animal)'이라는 공통된 분류에 속합니다.

### 1) 추상 클래스 정의

'동물'이라는 추상적인 개념을 Animal 추상 클래스로 정의합니다. 동물은 '이름'이라는 **상태(일반 멤버 변수)**와 '먹는다'는 **공통된 행위(일반 메소드)**를 가집니다. 하지만 '우는 행위'는 동물마다 다르므로 추상 메소드로 남겨둡니다.

Java
 
// Animal.java
public abstract class Animal {
    // 공통된 상태(멤버 변수)를 가질 수 있음
    protected String name;

    // 공통된 기능(일반 메소드)을 가질 수 있음
    public void eat() {
        System.out.println(name + "이(가) 밥을 먹습니다.");
    }

    // 자식 클래스가 반드시 구현해야 하는 기능(추상 메소드)
    public abstract void makeSound();
}

### 2) 클래스 상속 및 구현

Dog와 Cat 클래스가 Animal을 **상속(extends)**받아, makeSound()라는 추상 메소드를 각자의 방식대로 **구현(오버라이딩)**합니다.

Java
 
// Dog.java
public class Dog extends Animal {
    public Dog(String name) { this.name = name; }

    @Override
    public void makeSound() {
        System.out.println(name + "이(가) 멍멍!");
    }
}

// Cat.java
public class Cat extends Animal {
    public Cat(String name) { this.name = name; }

    @Override
    public void makeSound() {
        System.out.println(name + "이(가) 야옹~");
    }
}

### 3) 사용 예시 (다형성)

Animal 타입의 변수에 Dog나 Cat 객체를 담을 수 있습니다. 이때 eat()처럼 공통된 기능과, 각자 다르게 구현한 makeSound() 기능을 모두 사용할 수 있습니다.

Java
 
public class Main {
    public static void main(String[] args) {
        // Animal animal = new Animal(); // 에러! 추상 클래스는 직접 인스턴스화 불가

        Animal myDog = new Dog("뽀삐");
        Animal myCat = new Cat("나비");

        myDog.eat();        // 부모의 공통 메소드 호출 -> "뽀삐이(가) 밥을 먹습니다."
        myDog.makeSound();  // 자식이 오버라이딩한 메소드 호출 -> "뽀삐이(가) 멍멍!"

        myCat.eat();        // 부모의 공통 메소드 호출 -> "나비이(가) 밥을 먹습니다."
        myCat.makeSound();  // 자식이 오버라이딩한 메소드 호출 -> "나비이(가) 야옹~"
    }
}

## 2. 인터페이스 예시: "can-do" 관계 (비행)

인터페이스는 서로 관련 없는 객체들에게 공통된 '기능'을 부여하고 싶을 때 사용합니다. '새(Bird)'와 '드론(Drone)'은 종류가 전혀 다르지만, '날 수 있다(Flyable)'는 공통된 기능을 가질 수 있습니다.

### 1) 인터페이스 정의

'날 수 있는'이라는 기능적 계약을 Flyable 인터페이스로 정의합니다. 인터페이스는 **상수(static final)**와 추상 메소드만을 가질 수 있습니다.

Java
 
// Flyable.java
public interface Flyable {
    // 자동으로 public static final이 됨
    int MAX_ALTITUDE = 40000; // 최대 고도 (상수)

    // 자동으로 public abstract가 됨
    void fly(); // 날다 (추상 메소드)
}

### 2) 클래스 구현

서로 상속 관계가 없는 Bird와 Drone 클래스가 Flyable 인터페이스를 **구현(implements)**하여, fly()라는 기능을 각자의 방식대로 구현합니다.

Java
 
// Bird.java
public class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("새가 날개로 하늘을 납니다.");
    }
}

// Drone.java
public class Drone implements Flyable {
    @Override
    public void fly() {
        System.out.println("드론이 프로펠러를 돌려 비행합니다. 최대 고도: " + MAX_ALTITUDE + "ft");
    }
}

### 3) 사용 예시 (다형성)

Flyable이라는 인터페이스 타입의 변수에, 이 인터페이스를 구현한 Bird나 Drone 객체를 모두 담을 수 있습니다.

Java
 
public class Main {
    public static void main(String[] args) {
        Flyable flyingObject1 = new Bird();
        Flyable flyingObject2 = new Drone();

        flyingObject1.fly(); // -> "새가 날개로 하늘을 납니다."
        flyingObject2.fly(); // -> "드론이 프로펠러를 돌려 비행합니다. 최대 고도: 40000ft"
    }
}

이처럼 Bird와 Drone은 전혀 다른 객체지만, Flyable이라는 **동일한 규격(기능)**을 따르기 때문에 다형적으로 사용할 수 있습니다.


 

## 4. 싱글톤 패턴(Singleton Pattern)이란 무엇인가요?

싱글톤 패턴은 애플리케이션 전체에서 특정 클래스의 인스턴스가 오직 하나만 생성되도록 보장하고, 이 인스턴스에 대한 전역적인 접근점을 제공하는 디자인 패턴입니다.

  • 목적: 데이터베이스 커넥션 풀, 설정 객체처럼 여러 곳에서 공유되어야 하는 유일한 객체를 만들 때 사용합니다.
  • 구현 방법:
    1. 외부에서 new 키워드로 객체를 생성할 수 없도록 생성자를 private으로 만듭니다.
    2. 클래스 내부에 private static으로 유일한 인스턴스를 미리 생성해 둡니다.
    3. 외부에서 이 유일한 인스턴스에 접근할 수 있도록 **public static 메소드(getInstance())**를 제공합니다.

Spring과 싱글톤: 스프링 컨테이너에 등록되는 모든 빈(Bean)은 기본적으로 싱글톤으로 관리됩니다. 따라서 스프링 개발자는 싱글톤 패턴을 직접 구현할 필요 없이, 컨테이너를 통해 DI를 받으면 자연스럽게 싱글톤 객체를 사용하게 됩니다.

## 싱글톤 패턴 예시 코드 (Java)

Java
 
public class SingletonService {

    // 1. private static final로 클래스 내부에 유일한 인스턴스를 미리 생성합니다.
    private static final SingletonService instance = new SingletonService();

    // 2. public static 메소드를 통해서만 이 유일한 인스턴스에 접근하도록 허용합니다.
    public static SingletonService getInstance() {
        return instance;
    }

    // 3. private 생성자를 만들어, 외부에서 new 키워드로 객체를 생성하는 것을 막습니다.
    private SingletonService() {
    }

    // 싱글톤 객체가 수행할 메소드
    public void showMessage() {
        System.out.println("이것은 유일한 싱글톤 인스턴스입니다.");
    }
}

## 사용 방법

new 키워드로 생성할 수 없으며, 항상 getInstance() 메소드를 통해 동일한 인스턴스를 얻어와야 합니다.

Java
 
public class Main {
    public static void main(String[] args) {
        // SingletonService service = new SingletonService(); // 컴파일 에러! private 생성자

        SingletonService service1 = SingletonService.getInstance();
        SingletonService service2 = SingletonService.getInstance();

        // service1과 service2는 동일한 객체 참조 값을 가집니다.
        if (service1 == service2) {
            System.out.println("service1과 service2는 같은 싱글톤 객체입니다.");
        }

        service1.showMessage(); // "이것은 유일한 싱글톤 인스턴스입니다." 출력
    }
}

## 핵심 포인트

  1. private 생성자: 외부에서 new를 통해 객체를 무분별하게 생성하는 것을 막습니다.
  2. private static final 인스턴스: 클래스 로딩 시 단 한 번만 생성되는 유일한 인스턴스를 클래스 내부에 보관합니다.
  3. public static getInstance(): 이 유일한 인스턴스에 접근할 수 있는 유일한 통로를 제공합니다.

 

## 5. Java의 컬렉션(Collection) 프레임워크 종류는 무엇인가요?

Java 컬렉션 프레임워크는 데이터 그룹을 저장하고 관리하기 위한 표준화된 아키텍처입니다. 핵심 인터페이스는 크게 세 가지입니다.

  • List: 순서가 있는 데이터의 집합이며, 중복을 허용합니다.
    • ArrayList: 내부적으로 배열로 구현되어 있어, 인덱스를 통한 조회 속도가 매우 빠릅니다.
    • LinkedList: 노드 간의 연결(링크)로 구현되어 있어, 데이터의 추가/삭제가 빈번할 때 유리합니다.
  • Set: 순서가 없고, 중복을 허용하지 않는 데이터의 집합입니다.
    • HashSet: 해시(Hash)를 기반으로 하여, 가장 빠른 검색/입력 성능을 보입니다.
    • TreeSet: 데이터를 정렬된 상태로 유지합니다.
  • Map: Key-Value 쌍으로 데이터를 저장하며, Key는 중복될 수 없습니다.
    • HashMap: 해시(Hash)를 기반으로 하여, Key를 통해 Value를 매우 빠르게 검색할 수 있습니다.
    • TreeMap: Key를 기준으로 정렬된 상태를 유지합니다.

 

## 6-1. 오버로딩 (Overloading): 이름은 같게, 매개변수는 다르게

오버로딩은 하나의 클래스 내에서 이름이 같은 메소드를 여러 개 정의하는 것입니다.

  • 규칙:
    1. 메소드 이름이 동일해야 합니다.
    2. 매개변수의 개수 또는 타입이 반드시 달라야 합니다.
    3. 리턴 타입은 오버로딩을 구분하는 데 영향을 주지 않습니다.
  • 잘못된 이해: "출력 타입이 다를 수 있다" (X)
    • → 매개변수는 동일하고 리턴 타입만 다른 메소드는 오버로딩으로 인정되지 않습니다.
  • 핵심: **"같은 이름의 메소드에 다양한 종류의 입력 값을 받을 수 있게 하는 기술"**입니다.
    • 예: System.out.println() 메소드는 println(int a), println(String s), println(boolean b) 등 다양한 타입을 받을 수 있도록 오버로딩되어 있습니다.

## 6-2. 오버라이딩 (Overriding): 내용은 새롭게, 형식은 그대로

오버라이딩은 부모 클래스로부터 상속받은 메소드의 내용을 자식 클래스에서 새롭게 재정의하는 것입니다.

  • 규칙:
    1. 부모 클래스의 메소드와 이름, 매개변수, 리턴 타입이 모두 동일해야 합니다.
    2. 메소드의 내부 구현 내용만 새롭게 작성합니다.
  • 올바른 이해: "나머지는 다 그대로인데, 함수 내부 내용만 바뀌는 것이다." (O)
  • 핵심: **"부모로부터 물려받은 기능을 자식의 특성에 맞게 변경하는 기술"**입니다.
    • 예: Animal 클래스의 makeSound() 메소드를 상속받은 Dog 클래스는 "멍멍", Cat 클래스는 "야옹"으로 makeSound()를 각자 재정의합니다.

## 한눈에 비교하기

구분 오버로딩 (Overloading) 오버라이딩 (Overriding)
메소드 이름 동일 동일
매개변수 반드시 다름 (개수 또는 타입) 반드시 같음
리턴 타입 상관 없음 반드시 같음 (Covariant return type 예외)
발생 범위 같은 클래스 내 상속 관계의 자식 클래스
핵심 목적 이름이 같은 메소드의 기능 확장 부모 메소드의 기능 재정의