Java

Java의 주요 특징

재심 2024. 10. 3. 19:15

캡슐화 (Encapsulation)

객체지향의 중요 원칙 중 하나로 데이터(필드)와 그 데이터에 접근하는 방법(메서드)을 객체 안에 묶어 관리하고 외부의 직접적인 접근을 제한하는 것.

이를 통해 객체 내부의 상태를 보호하고 외부와의 상호작용은 제한된 방식으로만 가능하도록 함.

 

캡슐화의 핵심은 접근 제어자를 사용하는 것.

  • private: 클래스 내부에서만 접근 가능.
  • public: 누구나 접근 가능
  • protected: 같은 패키지 또는 상속받은 클래스에서 접근 가능
  • default (아무것도 명시하지 않았을 때): 같은 패키지 내에서만 접근 가능

객체 내부의 속성에 직접 접근하거나 수정하는 것을 방지하고 객체 내부에서 제공하는 메서드를 통해 안전하게 데이터를 다룰 수 있게 유도하기 위함. (getter, setter를 통해 데이터에 접근하고 수정)

 

public class Person {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 

추상화 (Abstraction)

복잡한 시스템이나 객체에서 핵심적인 개념만 드러내고 불필요한 세부사항은 숨김.

즉, 세부 구현보다는 "무엇을 할 수 있는가"에 집중하는 것.

 

 

  • 필요한 정보만 제공: 객체의 구체적인 동작이나 내부 구조는 숨기고, 외부에서 그 객체를 사용할 때 필요한 중요한 기능만을 제공. 예를 들어, 자동차를 운전할 때, 운전자는 엔진이 어떻게 작동하는지 몰라도 핸들, 페달, 기어와 같은 인터페이스만 알면 운전할 수 있어. 이처럼 복잡한 내부 동작은 추상화되었어.
  • 인터페이스와 구현 분리: 추상화는 주로 **인터페이스(interface)**나 **추상 클래스(abstract class)**를 통해 구현되는데, 이들은 구현 세부 사항을 숨기고, 어떤 기능을 제공할지에 대한 명세만을 정의. 인터페이스나 추상클래스를 다양한 방식으로 구현 가능하게 됨.
  • 유연성 제공: 추상화는 코드의 유연성을 높여주는데, 구체적인 구현에 종속되지 않고, 인터페이스나 추상 클래스만 알면 구체화된 클래스가 변경되도라도 그대로 사용 가능.

대표적인 예로 자동차가 있음.

자동차의 경우 운전, 후진, 브레이크와 같은 인터페이스가 정의되어 있으면 어떤 형태의 자동차이더라도 인터페이스의 구현을 하면 됨.

 

인터페이스 예시

아래는 인터페이스의 예시. Animal이 추상화의 역할을 하고, Dog과 Cat이 그 추상화를 구체화한 것.

Animal의 메서드로 sound()가 있는데 각 동물마다 소리내는 방식을 구현해서 사용하게 됨.

 

interface Animal {
    void sound();
}

class Dog implements Animal {
    public void sound() {
        System.out.println("Bark");
    }
}

class Cat implements Animal {
    public void sound() {
        System.out.println("Meow");
    }
}

 

추상 클래스 예시

일반 클래스처럼 필드와 메서드를 가질 수 있지만 일부 메서드는 선언만하고 구체적인 구현은 상속받은 구현체에 맡김

abstract class Shape {
    abstract void draw();  // 추상 메서드
}

class Circle extends Shape {
    void draw() {
        System.out.println("Drawing a circle");
    }
}

class Rectangle extends Shape {
    void draw() {
        System.out.println("Drawing a rectangle");
    }
}

 

상속 (Inheritance)

한 객체를 또 다른 객체가 이어받는 것. 부모 코드를 자식 코드가 재사용할 수 있음.

자동차의 예를 들면 A라는 자동차를 만들었는데 더 좋은 엔진과 더 좋은 브레이크를 만들었다고 가정하자. 이 때 B라는 자동차를 만들면서 A자동차를 상속받으면 A의 좋은 엔진과 브레이크를 그대로 사용할 수 있다.

 

class Animal {
    String name;

    void eat() {
        System.out.println("This animal is eating.");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("The dog is barking.");
    }

    // 부모 클래스의 메서드를 오버라이딩
    @Override
    void eat() {
        System.out.println("The dog is eating.");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        myDog.name = "Buddy";   // Animal 클래스의 속성 사용
        myDog.eat();            // 오버라이딩된 메서드 호출
        myDog.bark();           // Dog 클래스의 메서드 호출
    }
}

 

상속의 장점

  • 코드 재사용: 부모 클래스에서 공통적으로 사용할 수 있는 기능을 정의하면 자식 클래스들이 그대로 사용가능
  • 유지보수성 향상: 공통 기능을 부모 클래스에 정의하면 한 번의 수정으로 모든 자식 클래스에 반영 가능
  • 유연성과 확장성: 자식 클래스는 부모 클래스의 기능을 그대로 상속받으면서 자신만의 고유한 기능을 추가하거나 부모의 기능을 수정해서 사용할 수도 있음

상속의 주의점

  • 자바는 다중 상속을 허용하지 않음. 즉, 한 클래스는 하나의 부모 클래스만을 상속받을 수 있음. 하지만 여러 인터페이스를 구현할 수는 있음
  • 상속을 너무 많이 사용하면 클래스 간 결합도를 높여 유지보수를 어렵게 만들 수도 있음

다형성 (Polymorphism)

같은 이름의 메서드나 객체가 상황에 따른 다른 방식으로 동작하는 것. 다형성은 코드의 유연성과 확장성을 높여주며 상속과 함께 사용.

 

다형성의 구분

  • 컴파일 시간 다형성 (정적 다형성)
  • 런타임 다형성 (동적 다형성)

컴파일 시간 다형성 (정적 다형성)

메서드 오버로딩이 여기에 해당된다. 같은 이름의 메서드를 매개변수 타입이나 개수에 따라 여러가지로 정의하는 것.

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;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println(calc.add(5, 3));         // int add 호출
        System.out.println(calc.add(2.5, 3.5));     // double add 호출
        System.out.println(calc.add(1, 2, 3));      // int add(3개 인자) 호출
    }
}

 

런타임 다형성 (동적 다형성)

오버라이딩과 업캐스팅이 해당됨.

class Animal {
    void sound() {
        System.out.println("Some animal sound");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Bark");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("Meow");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Dog();  // 업캐스팅
        myAnimal.sound();             // Dog의 sound() 호출 -> "Bark"
        
        myAnimal = new Cat();         // 업캐스팅
        myAnimal.sound();             // Cat의 sound() 호출 -> "Meow"
    }
}

 

위 코드에서 Animal 클래스는 부모 클래스이고, Dog과 Cat은 자식클래스이다.

sound() 메서드는 부모에서 정의되었지만 각각 오버라이딩 되어있음.

 

실제로 사용할 때 Animal 타입의 변수로 선언하고 자식 클래스인 Dog과 Cat을 참조하고 있는데 실제 호출되는 메서드는 참조하는 객체의 타입에 따라 결정되는 것. 그래서 실행시점에 어떤 메서드가 실행될지 결정되기 때문에 그게 바로 런타임 다형성임.

 

다형성의 장점

  • 코드 유연성: 부모 클래스 타입을 사용하면 자식 클래스가 추가되도 코드 수정 없이 새로운 객체들을 처리할 수 있음
  • 코드 일관성: 동일한 인터페이스나 부모 클래스를 사용하면 여러 객체를 동일한 방식으로 처리가능
void makeAnimalSound(Animal animal) {
    animal.sound();
}

makeAnimalSound(new Dog());  // "Bark"
makeAnimalSound(new Cat());  // "Meow"

 

위 코드는 makeAnimalSound 메서드에서 부모 클래스인 Animal을 파라미터로 받는다. 실제로는 구현체들을 넘겨주기만 하면 똑같이 동작한다.