
타입스크립트를 오랫동안 사용해오면서 클래스 기반 프로그래밍에 익숙해졌다고 생각했지만, 최근 Java를 배우기 시작하면서 기존에 알던 개념들을 더 깊이 있게 돌아보게 되었다. Java는 타입스크립트와 비교했을 때 문법적으로는 유사한 부분이 많지만, 객체지향 프로그래밍 언어로서의 철학과 제약에서 차이를 보인다. 이 글에서는 Java의 클래스, 인터페이스, 추상 클래스에 대해 정리하고, 타입스크립트 개발자의 관점에서 차이점이나 느낀 점들을 함께 서술해보았다.
클래스(Class) – 객체의 설계도
Java에서 클래스는 현실 세계의 사물이나 개념을 프로그래밍 세계에 옮겨오기 위한 청사진 역할을 한다. 필드(field), 생성자(constructor), 메서드(method)를 통해 객체의 상태와 행동을 정의할 수 있으며, 이러한 구조는 타입스크립트의 클래스와 유사하다.
자바에서는 모든 클래스가 명시적으로 접근 제어자를 가진다. 일반적으로 하나의 public 클래스를 하나의 파일로 정의하며, 이 파일의 이름은 반드시 클래스 이름과 동일해야 한다. 예를 들어 Person이라는 클래스를 정의하려면 파일명은 Person.java가 되어야 한다.
public class Person {
// 필드
private String name;
private int age;
// 생성자
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 메서드
public void greet() {
System.out.println("Hello, my name is " + name);
}
// Getter
public String getName() {
return name;
}
// Setter
public void setName(String name) {
this.name = name;
}
}
자바의 클래스는 public, private, protected, static과 같은 접근 제어자를 통해 멤버의 가시성을 세밀하게 제어하며, 이는 캡슐화를 보다 강력하게 실현할 수 있게 한다. 또한 클래스는 단일 상속만 지원하므로, 여러 기능을 조합할 땐 인터페이스와 조합하는 것이 일반적이다.
생성자 오버로딩
Java에서는 하나의 클래스 내에 여러 개의 생성자를 정의할 수 있다. 이를 생성자 오버로딩(Constructor Overloading)이라고 하며, 서로 다른 매개변수 목록(parameter list)을 가진 생성자들을 선언함으로써 다양한 초기화 방식을 제공할 수 있다.
public class User {
private String name;
private int age;
// 기본 생성자
public User() {
this("Unknown", 0); // 다른 생성자 호출
}
// 이름만 받는 생성자
public User(String name) {
this(name, 0); // 다른 생성자 호출
}
// 나이만 받는 생성자
public User(int age) {
this("Unknown", age); // 다른 생성자 호출
}
// 이름과 나이를 받는 생성자
public User(String name, int age) {
this.name = name;
this.age = age;
}
}이제 User 객체는 다음과 같이 다양한 방식으로 생성할 수 있다:
User u1 = new User(); // name: Unknown, age: 0
User u2 = new User("Alice"); // name: Alice, age: 0
User u2 = new User(25); // name: Unknown, age: 25
User u3 = new User("Bob", 25); // name: Bob, age: 25
타입스크립트에서는 선택적 매개변수(optional parameter), 매개변수 기본값(default value), 또는 조건문을 활용한 내부 로직 분기로 유사한 기능을 제공하기는 한다. 하지만 이름만 받거나 나이만 받는 등 매개변수 조합에 따라 완전히 다른 초기화 방식을 제공하려면, 타입스크립트에서는 매개변수 타입을 유니언으로 정의하고 런타임에서 타입을 분기하는 수고가 필요하다. 예를 들어 constructor(name: string | number, age?: number)와 같이 선언한 뒤, 생성자 내부에서 typeof name === 'string' 같은 조건문으로 분기하여 각각의 초기화 로직을 작성해야 한다.
반면, Java는 시그니처가 다른 여러 생성자를 별도로 선언할 수 있어 이런 분기를 정적 타입 시스템 차원에서 자연스럽게 해결할 수 있다는 점이 큰 차이다.
추상 클래스(Abstract Class) – 공통 로직과 구조의 틀
추상 클래스는 공통 로직을 구현하면서도 하위 클래스에서 반드시 구현해야 하는 메서드를 정의할 수 있는 클래스다. abstract 키워드를 사용하며, 인스턴스로 생성할 수는 없다.
public abstract class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public void breathe() {
System.out.println(name + " is breathing");
}
public abstract void makeSound();
}public class Dog extends Animal {
public Dog(String name) {
super(name);
}
public void makeSound() {
System.out.println("Bark!");
}
}인터페이스와 달리 필드와 생성자를 가질 수 있고, 일부 메서드는 구현할 수 있다는 점이 특징이다. 완전히 추상적인 인터페이스와 구체적인 클래스 사이에서 공통된 기능을 제공하면서 유연한 구조 설계를 도울 수 있다.
인터페이스(Interface) – 역할을 정의하는 계약
Java에서 인터페이스는 클래스가 "무엇을 할 수 있어야 하는가"를 정의하는 일종의 계약(contract)이다. 타입스크립트의 인터페이스처럼 특정 메서드 시그니처를 강제할 수 있으며, 자바에서는 다중 구현이 가능하다는 점에서 중요한 구조적 장치로 사용된다.
public interface Drivable {
void drive();
}이 인터페이스를 구현하는 클래스는 drive() 메서드를 반드시 구현해야 한다:
public class Car implements Drivable {
@Override
public void drive() {
System.out.println("The car is driving");
}
}이처럼 자바의 인터페이스는 객체가 가져야 할 행위의 형태를 정의하며, 클래스는 이를 implements 키워드로 구현한다. 자바는 클래스는 하나만 상속할 수 있지만, 인터페이스는 여러 개 구현할 수 있으므로 역할 기반 설계에 적합하다.
default 메서드
Java 8부터 인터페이스에 default 키워드를 사용해 기본 메서드 구현을 포함할 수 있게 되었다. 이는 기존 인터페이스에 기능을 추가하면서도 기존 구현 클래스에 영향을 주지 않기 위한 장치다.
public interface Greeter {
void greet(String name);
default void sayHello() {
System.out.println("Hello!");
}
}위 인터페이스를 구현하는 클래스는 greet()는 필수로 구현해야 하지만, sayHello()는 생략 가능하다. 기본 구현이 있기 때문이다.
public class KoreanGreeter implements Greeter {
@Override
public void greet(String name) {
System.out.println("안녕하세요, " + name + "님!");
}
}Greeter greeter = new KoreanGreeter();
greeter.greet("철수"); // 출력: 안녕하세요, 철수님!
greeter.sayHello(); // 출력: Hello! (기본 구현)필요하다면 sayHello()도 오버라이드할 수 있다:
public class FriendlyGreeter implements Greeter {
@Override
public void greet(String name) {
System.out.println("Hi, " + name + "!");
}
@Override
public void sayHello() {
System.out.println("Hey there!");
}
}
static 메서드
인터페이스에 static 메서드를 정의하면, 인스턴스 없이도 인터페이스 이름으로 직접 호출할 수 있는 도우미 메서드를 만들 수 있다:
public interface MathUtils {
static int square(int x) {
return x * x;
}
}int result = MathUtils.square(4); // 16
다중 인터페이스 충돌 시 해결 방법
여러 인터페이스에서 동일한 시그니처의 default 메서드가 정의되어 충돌할 경우, 구현 클래스에서 명시적으로 어떤 인터페이스의 메서드를 사용할지 지정해줘야 한다.
public interface A {
default void greet() {
System.out.println("Hello from A");
}
}
public interface B {
default void greet() {
System.out.println("Hello from B");
}
}
public class C implements A, B {
@Override
public void greet() {
A.super.greet(); // 또는 B.super.greet();
}
}
인터페이스 vs 추상클래스
추상 클래스와 인터페이스는 모두 설계의 틀을 제공한다는 공통점이 있지만, 중요한 차이점이 존재한다. 추상 클래스는 필드(멤버 변수)와 일부 구현된 메서드를 포함할 수 있으며, 생성자도 가질 수 있다. 반면, 인터페이스는 기본적으로 상수(static final 변수)만 가질 수 있고, 메서드는 구현하지 않는 추상 메서드로만 구성되었다.
하지만 Java 8부터 인터페이스에도 default 메서드와 static 메서드가 추가되면서, 기본 구현을 포함할 수 있게 되었다. 이는 기존 인터페이스에 새로운 메서드를 추가할 때, 기존 구현 클래스를 깨뜨리지 않으면서도 기능을 확장할 수 있도록 설계된 변화다.
따라서, 추상 클래스는 공통 필드와 구현을 공유하는 데 초점을 두고, 인터페이스는 다중 상속이 가능하도록 역할(behavior) 중심의 계약을 정의하는 데 더욱 적합하다.
// 추상 클래스 예시
public abstract class Vehicle {
protected String brand;
public Vehicle(String brand) {
this.brand = brand;
}
// 일반 메서드 (구현 있음)
public void showBrand() {
System.out.println("브랜드: " + brand);
}
// 추상 메서드 (구현 없음, 하위 클래스가 반드시 구현)
public abstract void drive();
}
// 인터페이스 예시
public interface Flyable {
// 상수 (public static final 생략 가능)
int MAX_ALTITUDE = 10000;
// 추상 메서드 (구현 없음)
void fly();
// default 메서드 (기본 구현 포함)
default void checkAltitude() {
System.out.println("최대 비행 고도는 " + MAX_ALTITUDE + "미터입니다.");
}
// static 메서드 (인스턴스 없이 호출 가능)
static void info() {
System.out.println("Flyable 인터페이스는 비행 가능한 객체를 위한 계약입니다.");
}
}
// 구현 클래스 예시
public class Airplane extends Vehicle implements Flyable {
public Airplane(String brand) {
super(brand);
}
@Override
public void drive() {
System.out.println("비행기를 조종합니다.");
}
@Override
public void fly() {
System.out.println("비행 중입니다.");
}
}
// 사용 예시
public class Main {
public static void main(String[] args) {
Airplane airplane = new Airplane("Boeing");
airplane.showBrand(); // 추상 클래스의 일반 메서드
airplane.drive(); // 추상 클래스의 추상 메서드 구현
airplane.fly(); // 인터페이스 추상 메서드 구현
airplane.checkAltitude(); // 인터페이스 default 메서드 호출
Flyable.info(); // 인터페이스 static 메서드 호출
}
}위 예시는 추상 클래스와 인터페이스의 주요 차이점을 명확하게 보여준다. 먼저, 추상 클래스는 필드와 생성자를 가질 수 있으며, 구현된 메서드와 추상 메서드를 함께 포함할 수 있다. 이를 통해 공통된 상태와 행동을 하위 클래스에 물려주면서, 반드시 구현해야 하는 메서드를 강제할 수 있다.
반면, 인터페이스는 기본적으로 상수와 추상 메서드만을 정의했으나, Java 8부터는 default 메서드와 static 메서드를 포함할 수 있게 되었다. default 메서드는 인터페이스 내에서 기본 구현을 제공하여, 구현 클래스에서 선택적으로 오버라이드할 수 있다. 그리고 static 메서드는 인터페이스 이름으로 직접 호출할 수 있는 메서드로, 유틸리티 성격의 기능을 제공하는 데 사용된다.
이처럼 추상 클래스와 인터페이스는 각각의 목적과 사용법에 차이가 있으며, 상황에 맞게 적절히 선택하여 활용하는 것이 중요하다.
익명 클래스(Anonymous Class) – 일회성 구현체의 강력한 도구
자바의 익명 클래스는 이름이 없는 일회성 클래스로, 주로 인터페이스나 추상 클래스의 구현이 간단할 때 사용된다. 타입스크립트에서도 비슷하게 익명 객체나 함수로 일회성 동작을 구현할 수 있지만, 자바의 익명 클래스는 클래스 기반 언어의 특성을 살려, 즉석에서 새로운 하위 클래스를 정의하고 인스턴스를 생성할 수 있다는 점이 특징이다.
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("작업 실행 중...");
}
};
new Thread(task).start();여기서 new Runnable() { ... } 부분이 익명 클래스다. 이름 없이 Runnable 인터페이스를 구현한 클래스를 즉석에서 정의하고 인스턴스를 만든 것이다.
더 읽어보기
2023.12.13
자바에서의 함수형 프로그래밍
자바스크립트로 프로그래밍을 처음 배웠을 때, 함수는 단순한 실행 단위가 아니라 변수에 담고, 다른 함수에 인자로 넘기고, 반환할 수도 있는 1급 객체였다. 이러한 개념은 자연스럽게 받아들여졌고, 다양한 고차 함수를 조합해 로직을 구성하는 방식에 익숙해졌다.반면 자바는 객체지향 언어로서…
2023.11.20
JDK
JDK는 자바 개발자용 도구 모음이다. 자바 소스 코드를 작성하고, 컴파일하고, 디버깅하며, 실행할 수 있는 모든 필수 도구가 포함되어 있다. 즉, 자바 프로그래밍을 시작하려면 반드시 JDK가 필요하다. 단순히 자바 프로그램을 실행하는 것과 개발하는 것은 다르다. 개발을 위해서는 소스…
2023.10.17
컬렉션 프레임워크
자바 컬렉션 프레임워크(Collection Framework)는 자바 프로그래밍에서 데이터를 효율적으로 저장하고 관리하기 위한 표준화된 자료구조와 알고리즘의 집합이다. 배열과 달리 크기 제한 없이 동적으로 데이터를 다룰 수 있고, 검색, 정렬, 삽입, 삭제와 같은 다양한 기능을 내장하고…
2026.04.11
Trie 자료구조
문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다.Trie는 무엇…
2026.03.19
Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까
웹 페이지에서 이미지를 로드할 때 흥미로운 장면을 종종 볼 수 있다. 이미지가 한 번에 완전히 나타나는 것이 아니라, 위에서 아래로 조금씩 채워지면서 나타나는 경우가 있기 때문이다. 특히 네트워크가 느리거나 이미지가 큰 경우 이런 현상이 더 분명하게 보인다. 마치 이미지가 위쪽부터 스캔…
2026.03.19
Streams API 부록 1. HTTP 다운로드 진행률은 어떻게 계산될까
파일을 다운로드할 때 가끔 몇 퍼센트 진행되었는지 혹은 진행 막대(progress bar)가 조금씩 채워지는 모습을 볼 수 있다. 그런데 모든 다운로드가 이런 식으로 진행률을 보여 주는 것은 아니다. 어떤 다운로드는 퍼센트가 표시되지만, 어떤 경우에는 진행 막대 없이 로딩 스피너만 계속…
2026.03.13
Streams API 4. 왜 모든 언어에는 Stream API가 존재할까
Streams API를 공부하다 보면 묘한 기시감을 느끼게 된다. JavaScript에서 ReadableStream, WritableStream, TransformStream을 살펴보고 있는데, 어딘가 낯설지 않다. Java를 써 본 사람이라면 InputStream, OutputStre…
댓글
댓글을 불러오는 중...