Java에 대하여 - 2(객체지향)

SUM·2025년 1월 2일

Java

목록 보기
2/5

1. 다형성이란?

다형성은 객체지향 프로그래밍(OOP)의 중요한 개념으로, 같은 타입의 변수나 메서드가 여러 형태로 동작할 수 있는 특성을 말한다. 자바에서는 주로 상속, 인터페이스, 그리고 메서드 오버로딩/오버라이딩을 통해 다형성을 구현한다.

다형성의 장점

  • 코드의 유연성 :
    상위 타입 변수로 다양한 하위 클래스 객체를 다룰 수 있어 코드가 간결해진다.

  • 확장성 :
    새로운 하위 클래스를 추가해도 기존 코드를 수정하지 않아도 된다.

  • 재사용성 :
    상위 클래스에서 정의한 메서드를 하위 클래스가 재사용하거나 재정의하여 효율적으로 활용 가능하다.

2. 오버로딩과 오버라이딩이란?

오버로딩(Overloading)
1. 개념

  • 동일한 이름의 메서드를 매개변수의 개수, 타입, 순서가 다르게 여러 개 정의하는 것이다.
  • 컴파일 시점에 호출할 메서드를 결정하므로 정적(Static) 다형성이라고도 부른다.
  1. 특징

    • 반환 타입만 다르고 매개변수가 동일하면 오버로딩이 성립되지 않는다.
    • 예를 들어, 같은 이름의 메서드 add(int a, int b)add(double a, double b)처럼 매개변수 목록이 달라야 한다.
    • 코드 가독성과 편의성을 높일 수 있다.
    • 같은 작업을 수행하지만 입력 값이 다를 때 사용하면 좋음
  2. 예시

    class Calculator {
        int add(int a, int b) {
            return a + b;
        }
    
        double add(double a, double b) {
            return a + b;
        }
    
        int add(int a, int b, int c) {
            return a + b + c;
        }
    }
    • 이름은 모두 add지만 매개변수의 타입, 개수 등이 달라서 서로 다른 메서드로 구분된다.

오버라이딩(Overriding)
1. 개념

  • 상속 관계에서 부모 클래스(혹은 인터페이스)의 메서드를 자식 클래스가 재정의하는 것이다.
  • 런타임 시점에 어떤 메서드를 호출할지 결정하므로 동적(Dynamic) 다형성이라고도 부른다.
  1. 특징

    • 부모와 메서드 시그니처(메서드 이름, 매개변수 목록, 반환 타입)가 동일해야 한다.
    • 접근 제어자는 부모 메서드보다 더 제한적으로 설정할 수 없다(접근 범위를 축소할 수 없음).
    • @Override 애노테이션을 사용하면 컴파일 시점에 재정의 여부를 체크할 수 있다.
    • 상속받은 메서드를 현재 클래스에 맞게 동작을 변경하고 싶을 때, 부모 클래스의 기본 동작을 재정의하여 더 구체적인 기능을 구현하고 싶을 때 사용하면 좋음
  2. 예시

    class Parent {
        void greet() {
            System.out.println("Hello from Parent");
        }
    }
    
    class Child extends Parent {
        @Override
        void greet() {
            System.out.println("Hello from Child");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Parent p = new Parent();
            p.greet();  // "Hello from Parent"
    
            Parent c = new Child();
            c.greet();  // "Hello from Child" (오버라이딩 메서드 호출)
        }
    }
    • Child 클래스는 Parentgreet() 메서드를 재정의하여, 객체 타입(런타임 타입)에 따라 다른 구현이 실행된다.

정리

  • 오버로딩동일한 이름이지만 매개변수 목록이 달라 여러 메서드를 정의하는 방식이며, 컴파일 시점에 어떤 메서드가 호출될지가 결정된다.
  • 오버라이딩상속 관계에서 부모 메서드를 자식이 재정의하는 방식이며, 런타임 시점에 실제 객체 타입에 따라 호출되는 메서드가 결정된다.

3. 상속

(1) 상속이란?

  1. 정의

    • 객체 지향 프로그래밍(OOP)에서 상위(부모) 클래스의 속성과 메서드를 하위(자식) 클래스가 물려받아 재사용하는 개념이다.

    • 코드 재사용성을 높이고, 계층 구조로 클래스를 조직화할 수 있게 해 준다.

(2) 상속의 장점

  • 코드 중복 감소: 공통 기능을 상위 클래스에 작성하면, 여러 하위 클래스에서 재사용 가능하다.
  • 유지보수 용이: 상위 클래스 수정으로 하위 클래스 전체에 공통 변경 사항을 쉽게 적용할 수 있다.

(3) 상속의 단점

  1. 결합도 증가

    • 하위 클래스가 상위 클래스에 강하게 의존한다.

    • 상위 클래스 변경이 하위 클래스에 연쇄적으로 영향을 미칠 수 있다.

  2. 설계 유연성 저하

    • 상속 구조가 깊어질수록, 객체 구조(클래스 계층)가 복잡해진다.

    • 부모 클래스에 대한 종속이 커서 코드 수정 시 주의가 필요하다.

  3. 클래스 폭발(Class Explosion)

    • 기능별로 세분화하다 보면 불필요하게 많은 하위 클래스를 만들 가능성이 있다.

    • 관리가 어려워지고, 잘못된 설계로 이어질 수 있다.

(4) 상속 구현 방법

자바에서 상속extends 키워드를 사용하여 구현한다.


a. 기본 예시

// 상위(부모) 클래스
class Animal {
    String name;

    void eat() {
        System.out.println(name + " is eating.");
    }
}

// 하위(자식) 클래스
class Dog extends Animal {
    void bark() {
        System.out.println(name + " says: Woof!");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();      // Dog 객체 생성
        dog.name = "Rex";         // Animal 클래스에서 물려받은 name 필드 사용
        dog.eat();                // Animal 클래스의 eat() 메서드 사용
        dog.bark();               // Dog 클래스에서 추가로 정의한 bark() 메서드 사용
    }
}

실행 결과 예시

Rex is eating.
Rex says: Woof!
  • Dog 클래스는 Animal 클래스를 상속받는다(class Dog extends Animal).

  • Dog 객체는 상위 클래스(Animal)의 필드와 메서드(name, eat)를 그대로 사용할 수 있고, bark() 같은 고유 기능을 추가로 가질 수 있다.


b. super 키워드

상속받은 클래스(하위 클래스)에서 부모 클래스의 멤버에 접근하거나 부모 클래스 생성자를 호출할 때는 super 키워드를 사용할 수 있다.

class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    void eat() {
        System.out.println(name + " is eating.");
    }
}

class Dog extends Animal {

    // Dog 생성자가 호출될 때, 부모 클래스(Animal)의 생성자도 호출
    Dog(String name) {
        super(name); // 부모 생성자 호출
    }

    void bark() {
        System.out.println(name + " says: Woof!");
    }

    void parentEat() {
        super.eat(); // 부모 클래스의 eat() 메서드 직접 호출
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("Buddy");
        dog.eat();       // Buddy is eating.
        dog.bark();      // Buddy says: Woof!
        dog.parentEat(); // 내부적으로도 super.eat() 사용
    }
}
  • super(name);하위 클래스상위 클래스 생성자name을 전달하여 초기화하는 부분이다.

  • super.eat();자식 메서드에서 부모의 메서드를 명시적으로 호출할 때 사용한다

  • 기본 생성자가 있으면, 자바 컴파일러가 자동으로 super() 호출을 삽입해 준다.

  • 매개변수 있는 생성자만 존재한다면, 자식 쪽에서 반드시 super(…)를 써서 적절한 부모 생성자를 호출해야 한다.

  • 메서드 호출(super.method()) 부분은 필수는 아니지만, 부모 메서드의 실행 로직을 유지·확장하거나, 특정 부모 로직을 이용할 때 유용하다.


(5) 상속 사용시 주의사항

  1. 단일 상속

    • 자바는 단일 상속만 지원한다. 즉, 한 클래스가 여러 부모 클래스를 동시에 상속받을 수 없다.

    • 다중 상속이 필요한 경우, 인터페이스(interface)를 통해 구현 다형성을 확장한다.

  2. final 클래스

    • final 키워드가 붙은 클래스는 상속할 수 없다.

    • 예: public final class String은 자식 클래스를 둘 수 없다.

  3. 접근 제한자

    • private 멤버는 하위 클래스에서 직접 접근할 수 없고, protected 멤버는 같은 패키지나 하위 클래스에서 접근 가능하다.
  4. 메서드 오버라이딩

    • 하위 클래스가 상위 클래스의 메서드를 재정의하려면, 메서드 시그니처(이름, 매개변수 목록, 반환 타입)가 동일해야 하며, 접근 범위는 더 좁게 설정할 수 없다.

(6) 상속과 조합의 차이

상속과 조합의 차이점 정리

구분상속조합
개념부모 클래스의 속성과 메소드를 자식 클래스가 물려받아 사용하는 방식.객체가 다른 객체를 포함하여 기능을 확장하거나 동작을 조합하는 방식.
결합도부모 클래스와 자식 클래스 간의 강한 결합을 초래. 부모 클래스의 변경이 자식 클래스에 영향을 미침.객체 간의 관계를 느슨하게 유지하여 변경에 대한 영향을 최소화함.
재사용성코드의 재사용성을 높이고 계층 구조를 통해 논리적인 관계를 표현 가능.객체의 기능을 재사용할 수 있으며, 더 유연한 설계를 가능하게 함.
유연성부모-자식 간의 고정된 계층 구조로 인해 상대적으로 유연성이 낮음.객체를 포함하여 동작을 확장하므로 더 유연하며 설계 변경이 쉬움.
적용 상황- "is-a" 관계를 표현할 때 적합.
예: Dog is-a Animal.
- "has-a" 관계를 표현할 때 적합.
예: Car has-a Engine.
단점- 부모 클래스의 변경이 자식 클래스에 미치는 영향이 큼.
- 계층 구조가 깊어지면 관리가 어려워질 수 있음.
- 설계가 복잡할 수 있음.
- 포함된 객체와의 관계를 명확히 이해하고 설계해야 함.
장점- 코드의 중복을 줄임.
- 계층 구조를 통해 논리적인 관계를 직관적으로 표현 가능.
- 결합도를 낮추어 독립성을 높임.
- 변경 및 확장이 용이함.

예제 비교

상속

class Animal {
    void eat() {
        System.out.println("Eating...");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Barking...");
    }
}

// 사용
Dog dog = new Dog();
dog.eat(); // 부모 클래스의 메소드 사용
dog.bark();

조합

class Engine {
    void start() {
        System.out.println("Engine starting...");
    }
}

class Car {
    private Engine engine;

    Car() {
        this.engine = new Engine();
    }

    void start() {
        engine.start();
        System.out.println("Car starting...");
    }
}

// 사용
Car car = new Car();
car.start();

선택 기준

  1. "is-a" 관계를 표현하려면 상속을 선택.
  2. "has-a" 관계를 표현하거나 유연성낮은 결합도가 필요하다면 조합을 선택.
  3. 부모-자식 간 강한 결합이 문제를 일으킬 가능성이 있다면 조합이 적합.

이러한 차이를 명확히 이해하고 상황에 맞는 방식을 선택하는 것이 중요합니다.

참고 : 상속과 조합의 차이점 이해하기

4. instanceof 키워드

(2) instanceof 키워드란?

  1. 정의:

    • 자바에서 객체의 실제 타입(클래스 또는 인터페이스)을 확인하기 위한 이항 연산자이다.

    • 예) if (obj instanceof String) { ... }

    • 왼쪽 피연산자는 객체, 오른쪽 피연산자는 클래스나 인터페이스가 온다.

    • 결과는 true 또는 false를 반환한다.

  2. 용도:

    • 런타임 시 객체가 특정 클래스나 인터페이스 타입에 속하는지 확인할 때 사용한다.

    • 캐스팅((Type) obj) 전 안전 검사 용도로 많이 쓴다.

    • 다형성을 구현한 상황에서, 특정 하위 클래스만의 메서드가 필요할 때(다운캐스팅) 타입 검증용으로 사용한다.

  3. 예시:

    Object obj = "Hello";
    if (obj instanceof String) {
        String str = (String) obj; // 안전하게 캐스팅
        System.out.println(str.toUpperCase());
    }

(2) instanceof 키워드 사용 시 문제점

  1. OOP 원칙 위배 가능성

    • 객체의 실제 타입을 직접 구분하는 로직이 많아지면, 다형성을 통한 확장성·유연성을 해칠 수 있다.

    • 자식 클래스가 늘어날 때마다 if (obj instanceof SubClass) 같은 조건 분기가 증가하여 코드가 복잡해진다.

  2. 유지보수성 저하

    • 여러 곳에서 instanceof 검사 후 다운캐스팅하는 코드를 자주 쓰면, 새로운 하위 클래스를 추가할 때마다 이 검사를 모두 수정해야 할 수 있다.

    • “수정해야 할 범위”가 커져, 에러 발생 위험도 증가한다.

  3. 코드 가독성 저하

    • instanceof와 다운캐스팅이 반복되는 로직은 if-else 블록이 여러 개로 늘어나 코드 읽기가 어렵다.
    • 오버라이딩(다형성)으로 처리할 수 있는 부분을 instanceof로 분기하면, 메서드 호출만으로 해결될 상황을 복잡하게 만든다.
  4. 대안

    • 가능하다면 다형성을 적극 활용하여, 하위 클래스에서 오버라이딩 메서드를 제공하게 하고 상위 타입에서 동적 바인딩으로 호출하게 하는 것이 권장된다.

    • 그래도 instanceof가 필요한 상황(예: 특정 인터페이스 구현 여부 검사, 조건부 다운캐스팅 등)에서는 최소한으로 사용하는 것이 바람직하다.

5. interface

interface는 자바에서 추상적(추상 메서드의 모음)인 규격을 정의하는 한 형태의 참조 타입이다. 주된 목적은 메서드 시그니처(메서드 이름, 매개변수, 반환 타입)만을 선언하고, 해당 메서드들의 구현체(실제 로직)는 이를 구현하는 클래스에서 작성하도록 하는 것이다. 자바 8 이후로는 default 메서드(구현부 포함), 정적 메서드, private 메서드를 추가할 수도 있지만, 본질적으로는 “기능을 정의해 놓고 구현은 없는” 역할을 맡는다.


interface의 특징

  1. 추상 메서드의 집합

    • 보통 인터페이스 내 메서드는 구현부가 없는 추상 메서드로 구성된다(자바 8 이전).
    • 예)
      public interface MyInterface {
          void doSomething();
          int calculate(int x, int y);
      }
    • 자바 8 이후부턴 default 메서드(구현부 포함)나 static 메서드, private 메서드도 선언 가능하다.
  2. 다중 구현

    • 자바는 클래스의 단일 상속만 허용하지만, 인터페이스는 여러 개를 구현(implements) 할 수 있다.
    • 예)
      public class MyClass implements InterfaceA, InterfaceB {
          // InterfaceA, InterfaceB의 모든 추상 메서드 구현
      }
  3. 계약(Contract) 역할

    • 인터페이스는 “이 기능들을 제공하겠다”라는 계약을 의미한다.
    • 이를 구현하는 클래스는 인터페이스에 정의된 메서드들을 반드시 구현해야 한다.
    • 외부에서는 구현체를 몰라도 인터페이스만 보고 “이 메서드를 호출하면 되겠구나” 하고 사용할 수 있다.
  4. 느슨한 결합(Loose Coupling)

    • 인터페이스를 통해 실제 구현체가 바뀌어도 상위 로직(인터페이스를 사용하는 코드)을 수정할 필요가 줄어든다.

    • 예)

      public interface Logger {
          void log(String message);
      }
      
      public class FileLogger implements Logger {
          @Override
          public void log(String message) {
              // 파일에 로그 남기기
          }
      }
      
      public class ConsoleLogger implements Logger {
          @Override
          public void log(String message) {
              // 콘솔에 로그 남기기
          }
      }
      
      // 의존성 주입으로 원하는 구현체 사용 가능
      public class MyService {
          private Logger logger;
          public MyService(Logger logger) {
              this.logger = logger;
          }
          // ...
      }
    • MyServiceLogger 인터페이스를 사용하므로, 파일로 찍든 콘솔로 찍든 구현체 교체가 자유롭다.

  5. 상수 선언 가능

    • 인터페이스에 선언된 필드는 자동으로 public static final로 인식된다.
    • 보통 static final 상수를 선언할 때 사용한다.

6. abstract class

추상 클래스(abstract class)는 자바에서 공통된 특징과 일부 구체적인 구현을 함께 제공하면서, 완성되지 않은(추상적인) 메서드를 포함해 자식 클래스에서 구체화(구현)해야 하는 부분을 남겨두는 클래스다. 즉, 상속받는 클래스가 반드시 재정의해야 할 메서드이미 구현된 메서드를 함께 가질 수 있는 일종의 ‘설계도’ 역할을 한다.


(1) 추상 클래스의 특징

  1. abstract 키워드 사용

    • 클래스 선언부에 abstract 키워드를 붙여 선언한다.
    • 예:
      public abstract class Animal {
          // ...
      }
  2. 추상 메서드

    • 구현부({ }) 없이 메서드 시그니처만 선언하고, abstract 키워드를 붙인다.
    • 자식 클래스는 이 추상 메서드를 반드시 구현(오버라이딩) 해야 한다.
    • 예:
      public abstract void sound();
  3. 구체 메서드와 멤버

    • 추상 클래스는 일반 클래스처럼 일부 완성된 메서드필드(멤버 변수) 를 가질 수 있다.
    • 하위 클래스는 이 구체 메서드를 그대로 사용할 수도 있고, 필요에 따라 오버라이딩할 수도 있다.
  4. 인스턴스화 불가

    • 추상 클래스 자체로는 객체(인스턴스)를 생성할 수 없다.

    • 반드시 추상 클래스를 상속받은 자식 클래스에서 추상 메서드를 구현한 뒤, 그 자식 클래스를 통해 인스턴스를 생성한다.

    • 예:

      // 불가능
      Animal a = new Animal(); // 컴파일 에러
      
      // 자식 클래스를 통해 가능
      Animal d = new Dog(); // OK
  5. 상속과 다형성

    • 추상 클래스는 상속 구조에서 “공통된 요소와 메서드 서명”을 정의해 두고, 구체 구현을 하위 클래스에게 위임한다.
    • 하위 클래스들이 각자 구현을 달리하여 다형성을 이룰 수 있다.

(2) 예시 코드

// 추상 클래스
public abstract class Animal {
    String name;
    
    // 추상 메서드 (구현부 없음)
    public abstract void sound();

    // 구체(완성) 메서드
    public void eat() {
        System.out.println(name + " is eating.");
    }
}

// 구체 클래스(추상 클래스 Animal을 상속받아 구현)
public class Dog extends Animal {
    // 추상 메서드 구현(오버라이딩)
    @Override
    public void sound() {
        System.out.println("Bark!");
    }
}

public class Main {
    public static void main(String[] args) {
        // 불가능: 추상 클래스로는 직접 객체 생성 X
        // Animal animal = new Animal(); // 컴파일 에러
        
        // 가능: 추상 클래스를 상속받아 만든 구체 클래스의 인스턴스
        Dog dog = new Dog();
        dog.name = "Rex";
        dog.eat();     // Rex is eating.
        dog.sound();   // Bark!
    }
}
  1. Animal은 추상 클래스이며, sound() 메서드를 추상 메서드로 선언했다.
  2. Dog 클래스는 Animal을 상속받아, 반드시 sound() 메서드를 구현해야 한다.
  3. Animal 자체로는 인스턴스를 만들 수 없지만, Dog 객체를 생성해 사용할 수 있다.

7. interface VS abstract

a. 구현(메서드 몸체) 유무와 멤버 구성

  • 인터페이스: 원칙적으로 추상 메서드만 가지며, 자바 8 이후부터 default, static 메서드를 통해 일부 구현이 가능하지만 상태(필드)를 가질 수는 없다(상수 제외).
  • 추상 클래스: 구체 메서드(일반 메서드)와 필드(상태)도 함께 가질 수 있어, 일부 기본 구현을 제공할 수 있다.

b. 상속(implements / extends) 방식과 다중 상속 여부

  • 인터페이스: implements 키워드를 통해 구현하며, 다중 구현 가능 (클래스 하나가 여러 인터페이스를 동시에 구현할 수 있음).
  • 추상 클래스: extends 키워드로 상속하며, 자바에서는 단일 상속만 허용 (이미 다른 클래스를 상속 중이라면 추상 클래스 상속 불가).

언제 사용하는가?

구분인터페이스추상 클래스
주요 목적- 기능(메서드 시그니처)만 정의하고, 구현은 전적으로 클래스가 맡도록
- 서로 다른 계층/타입에도 동일한 기능(규약)을 강제하고 싶을 때
- 공통된 필드, 메서드를 일부 구현해 두고, 일부는 추상 메서드로 남겨두어 자식 클래스에서 재정의하도록
- 같은 계층(유사한 도메인)에서만 공유할 수 있는 기본 로직을 제공하고 싶을 때
상속 여부- 다중 구현 가능 (implements)
- 클래스가 여러 인터페이스를 동시에 구현할 수 있음
- 단일 상속 (extends)
- 이미 다른 클래스를 상속 중이면 추상 클래스 상속 불가능
상태(필드) 보유- 원칙적으로 불가능 (상수 public static final만 허용)
- 자바 8 이후 default 메서드로 일부 구현 가능하지만 필드는 없음
- 일반 클래스처럼 필드구현 메서드를 가질 수 있음
- abstract 메서드는 자식이 반드시 재정의
사용 예- 여러 클래스에 통일된 기능(메서드 시그니처)이 필요하지만, 내부 구현이 달라야 할 때
- 예: Comparable, Runnable
- 공통된 로직 + 필수적으로 구현해야 할 추상 메서드를 함께 제공할 때
- 예: HttpServlet(서블릿 API)

인터페이스

  • 주로 여러 클래스에 같은 기능(메서드 시그니처)을 강제하고 싶을 때 사용한다.

  • 다양한 계층의 클래스가 동일한 행위를 구현하도록 할 때, 다중 구현이 가능하므로 유연하다.

추상 클래스

  • 같은 계층(유사한 도메인) 내에서 공통된 로직이나 상태를 일부 제공하면서, 구체적인 구현은 하위 클래스에 맡길 때 사용한다.

  • 단일 상속 구조에서 일부는 완성된 메서드로, 일부는 추상 메서드로 둬서 재정의하도록 강제한다.

참고 : 추상클래스와 인터페이스를 선택하는 방법

8. final

final 키워드는 “한 번 결정된 뒤 더는 변경할 수 없다”는 의미를 나타낸다.


(1) 특징

  1. 한 번만 값 할당 가능

    • final이 붙은 변수는 한 번 값이 설정되면 더는 변경할 수 없다.
    • 예)
      final int MAX_SPEED = 100;
      // MAX_SPEED = 150; // 컴파일 에러
  2. 상수 용도로 활용

    • 보통 static과 함께 final을 사용해 상수로 선언한다(주로 대문자로 표기).
    • 예) public static final int MIN_AGE = 18;
  3. 생성자에서 초기화 가능

    • 멤버 변수로 선언된 final은 생성자를 통해 초기화가 가능하다.
    • 단, 생성자(또는 초기화 블록) 내에서 단 한 번만 값을 지정해야 한다.

(2) final 메서드

  1. 오버라이딩 불가
    • final 메서드는 자식 클래스에서 재정의(override)할 수 없다.

    • 예)

      class Parent {
          final void cannotOverride() {
              System.out.println("This is final method");
          }
      }
      
      class Child extends Parent {
          // void cannotOverride() { ... } // 컴파일 에러 (오버라이딩 불가능)
      }
  2. 의도
    • 부모 클래스에서 이미 완성된 로직이나 변경되어서는 안 되는 메서드를 자식이 임의로 변경하지 못하도록 할 때 사용한다.

(3) final 클래스

  1. 상속 불가

    • final이 붙은 클래스는 상속할 수 없다.
    • 예)
      public final class String {
          // ...
      }
    • String 클래스가 그 대표적인 예이며, 다른 개발자가 이 클래스를 상속해서 내부 구조를 바꾸지 못하도록 막는다.
  2. 의도

    • 클래스 자체를 변경 불가능한 형태로 만들고 싶을 때, 혹은 보안/무결성이 필요한 라이브러리 코드를 보호할 때 사용한다.
profile
백엔드 개발자 SUM입니다.

0개의 댓글