## 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로 처리해야 합니다"라고 명시적으로 알려주는 역할을 합니다.
// 이 메소드는 IOException을 직접 처리하지 않고, 호출한 쪽으로 책임을 넘깁니다.
public void readFile() throws IOException {
// ... 파일 읽는 코드 ...
}
### 3. 예외 계층 구조와 Checked vs. Unchecked
Java의 모든 예외 클래스는 Throwable 클래스를 상속받으며, 크게 Error와 Exception으로 나뉩니다.
### Error
OutOfMemoryError, StackOverflowError와 같이 시스템 레벨에서 발생하는 심각한 오류입니다. 개발자가 코드 수준에서 복구할 수 없는 문제이므로, 애플리케이션 코드에서 잡으려고 시도해서는 안 됩니다.
### Exception
애플리케이션 코드에서 처리 가능한 모든 예외의 최상위 클래스이며, 다시 Checked와 Unchecked로 나뉩니다.
- 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 추상 클래스로 정의합니다. 동물은 '이름'이라는 **상태(일반 멤버 변수)**와 '먹는다'는 **공통된 행위(일반 메소드)**를 가집니다. 하지만 '우는 행위'는 동물마다 다르므로 추상 메소드로 남겨둡니다.
// 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()라는 추상 메소드를 각자의 방식대로 **구현(오버라이딩)**합니다.
// 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() 기능을 모두 사용할 수 있습니다.
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)**와 추상 메소드만을 가질 수 있습니다.
// Flyable.java
public interface Flyable {
// 자동으로 public static final이 됨
int MAX_ALTITUDE = 40000; // 최대 고도 (상수)
// 자동으로 public abstract가 됨
void fly(); // 날다 (추상 메소드)
}
### 2) 클래스 구현
서로 상속 관계가 없는 Bird와 Drone 클래스가 Flyable 인터페이스를 **구현(implements)**하여, fly()라는 기능을 각자의 방식대로 구현합니다.
// 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 객체를 모두 담을 수 있습니다.
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)이란 무엇인가요?
싱글톤 패턴은 애플리케이션 전체에서 특정 클래스의 인스턴스가 오직 하나만 생성되도록 보장하고, 이 인스턴스에 대한 전역적인 접근점을 제공하는 디자인 패턴입니다.
- 목적: 데이터베이스 커넥션 풀, 설정 객체처럼 여러 곳에서 공유되어야 하는 유일한 객체를 만들 때 사용합니다.
- 구현 방법:
- 외부에서 new 키워드로 객체를 생성할 수 없도록 생성자를 private으로 만듭니다.
- 클래스 내부에 private static으로 유일한 인스턴스를 미리 생성해 둡니다.
- 외부에서 이 유일한 인스턴스에 접근할 수 있도록 **public static 메소드(getInstance())**를 제공합니다.
Spring과 싱글톤: 스프링 컨테이너에 등록되는 모든 빈(Bean)은 기본적으로 싱글톤으로 관리됩니다. 따라서 스프링 개발자는 싱글톤 패턴을 직접 구현할 필요 없이, 컨테이너를 통해 DI를 받으면 자연스럽게 싱글톤 객체를 사용하게 됩니다.
## 싱글톤 패턴 예시 코드 (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() 메소드를 통해 동일한 인스턴스를 얻어와야 합니다.
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(); // "이것은 유일한 싱글톤 인스턴스입니다." 출력
}
}
## 핵심 포인트
- private 생성자: 외부에서 new를 통해 객체를 무분별하게 생성하는 것을 막습니다.
- private static final 인스턴스: 클래스 로딩 시 단 한 번만 생성되는 유일한 인스턴스를 클래스 내부에 보관합니다.
- 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): 이름은 같게, 매개변수는 다르게
오버로딩은 하나의 클래스 내에서 이름이 같은 메소드를 여러 개 정의하는 것입니다.
- 규칙:
- 메소드 이름이 동일해야 합니다.
- 매개변수의 개수 또는 타입이 반드시 달라야 합니다.
- 리턴 타입은 오버로딩을 구분하는 데 영향을 주지 않습니다.
- 잘못된 이해: "출력 타입이 다를 수 있다" (X)
- → 매개변수는 동일하고 리턴 타입만 다른 메소드는 오버로딩으로 인정되지 않습니다.
- 핵심: **"같은 이름의 메소드에 다양한 종류의 입력 값을 받을 수 있게 하는 기술"**입니다.
- 예: System.out.println() 메소드는 println(int a), println(String s), println(boolean b) 등 다양한 타입을 받을 수 있도록 오버로딩되어 있습니다.
## 6-2. 오버라이딩 (Overriding): 내용은 새롭게, 형식은 그대로
오버라이딩은 부모 클래스로부터 상속받은 메소드의 내용을 자식 클래스에서 새롭게 재정의하는 것입니다.
- 규칙:
- 부모 클래스의 메소드와 이름, 매개변수, 리턴 타입이 모두 동일해야 합니다.
- 메소드의 내부 구현 내용만 새롭게 작성합니다.
- 올바른 이해: "나머지는 다 그대로인데, 함수 내부 내용만 바뀌는 것이다." (O)
- 핵심: **"부모로부터 물려받은 기능을 자식의 특성에 맞게 변경하는 기술"**입니다.
- 예: Animal 클래스의 makeSound() 메소드를 상속받은 Dog 클래스는 "멍멍", Cat 클래스는 "야옹"으로 makeSound()를 각자 재정의합니다.
## 한눈에 비교하기
| 구분 | 오버로딩 (Overloading) | 오버라이딩 (Overriding) |
| 메소드 이름 | 동일 | 동일 |
| 매개변수 | 반드시 다름 (개수 또는 타입) | 반드시 같음 |
| 리턴 타입 | 상관 없음 | 반드시 같음 (Covariant return type 예외) |
| 발생 범위 | 같은 클래스 내 | 상속 관계의 자식 클래스 |
| 핵심 목적 | 이름이 같은 메소드의 기능 확장 | 부모 메소드의 기능 재정의 |
'개발 공부 > 자바' 카테고리의 다른 글
| 모든 클래스의 조상: Object 클래스 핵심 정리 (0) | 2025.12.03 |
|---|---|
| Java의 다형성 완전 정복 (0) | 2025.09.25 |
| 자바 면접 질문2 (0) | 2025.09.24 |
| 스프링 부트 개발자를 위한 Java 핵심 정리 (0) | 2025.09.24 |
| 자바빈 규약과 프로퍼티 접근 방식 (0) | 2025.09.14 |