Java, Object 그리고 OCP

이전에 Object 다형적 참조로 객체의 정보를 출력한 것을 살펴보았다.

ObjectPrint.java

public class ObjectPrint {
    public static void getInfo(Object obj) {
        String str = "(Debug) 객체 정보: " + obj.toString();
        System.out.println(str);
    }
}

Object 클래스가 없거나 toString() 메소드가 없는 경우 아무 관계가 없는 객체의 정보를 출력하는 것에는 어려움이 따를 것이다.
아무 관계가 없다는 것은 공통의 부모가 없으므로, 각각의 클래스 이름으로 메소드를 작성이 필요할 것이다.

BadPrinter.java

public class BadPrinter {
    public static void getInfo(Cat cat) {
        String str = "(Debug) 객체 정보: " + cat.toString();
        System.out.println(str);
    }

    public static void getInfo(Car car) {
        String str = "(Debug) 객체 정보: " + car.toString();
        System.out.println(str);
    }
}

이러한 코드의 문제점을 살펴보자.

구체화된 객체에 의존

BadPrinter 구체적인 타입 Car, Cat 사용하고 있다. 이후에 추가되는 클래스를 여러 개 만든 경우 지속적으로 BadPrinter 클래스의 메소드를 계속 추가해야한다.
BadPrinter 클래스는 Car, Cat 클래스으로부터 의존 되고 있는 것이다.

클래스로부터 의존 하는 것을 해소하려면 공통의 부모 클래스를 만들어 사용하는 방법이다. 부모 클래스는 다형적 참조와 메소드 오버라이딩 원리를 사용하여 문제를 해결한다. (공통 부모로 지정된 Object 클래스를 사용하는 것을 권장한다.)

추상화된 객체 의존

이전에 만든 ObjectPrint 클래스는 Car, Dog 구체적인 클래스가 아닌 추상적인 Object 클래스를 사용하였다.
즉, ObjectPrint 클래스는 Object 클래스으로부터 의존 되고 있다.

💡
추상적: 추상적이라는 뜻은 추상 클래스와 인터페이스를 뜻하는게 아니다.
예를 들어 성별이라는 개념이 있으면 남성과 여성으로 구분하고,
여기서의 성별은 실물이 없는 추상적 개념이고
남성과 여성은 하위 개념으로 구체적이게 된다.

ObjectPrint 클래스에서 사용한 Object, Cat, Car 구조는 다형성을 잘 활용한 예시이다.
다형성을 잘 활용하였다는 것은 다형적 참조와 메소드 오버라이딩을 적합하였다.

ObjectPrint 클래스 getInfo(), toString() 메소드 구조

  • 다형적 참조를 사용. Object 타입 파라미터로 사용하여 다형적 참조하였다.
    다형적 참조로 인해 Car, Car 인스턴스가 Object 타입으로 받을 수 있게 된 것이다.
  • 메소드 오버라이딩: 먼저, Object 클래스는 부모 클래스이여서 Cat,Car 클래스가 부모 클래스으로부터 toString() 메소드 오버라이딩할 수 있었다.
    자바 런타임에서 getInfo(Object obj) 는 구체적인 Car, Cat 타입에 의존하지 않고 Object 타입으로 불려오고 Object 타입은 각 인스턴스(Car, Cat)의 toString() 호출하여 정보를 출력할 수 있었다.

OCP 원칙

예제 코드를 살펴보고 OCP 원칙이 지켰는지 확인해본다.

  • Open: 새로운 클래스를 추가하여 toString() 오버라이딩하여 기능을 확장만 하였다.
  • Closed: 새로운 클래스를 추가한 결과 Object 타입과 toString() 호출 입장인 ObjectPrint 클래스는 변경하지 않았다.

다형적 참조와 메소드 오버라이딩을 사용하여 ObjectPrint 클라이언트가 Car, Cat 구체적인 타입에 의존하지 않고 추상적인 Object 타입을 의존하면서 OCP 원칙을 지켰다.
새로운 클래스를 생성하더라도 ObjectPrint 클라이언트는 코드를 변경하지 않아도 새로운 객체의 정보를 출력할 것이다.


ObjectPrint 클래스

ObjectPrint 클래스 구조를 살펴보면 익숙한 느낌이 들 것이다.
이전에 PrintStream 클래스에서 println 메소드를 살펴봤을 것이다.

System.out.println()

System.out.println() 내부 코드를 살펴보면 println(Object obj) 메소드로 오버로딩되어 접근한 적이 있다.
println(Object obj) 메소드 구조를 살펴보면 우리가 만든 ObjectPrint 클래스와 구조의 작동방식이 똑같다.

System.out.println() 메소드는 Object 매개변수를 사용하고 toString() 메소드를 호출한다. 그래서 객체의 정보를 편리하게 출력할 수 있었다.

toString() 메소드 뿐 아니라 다른 클래스의 메소드도 오버라이딩하여 사용할 수 있도록 설계되어 있으니 잘 활용하자.

정적 의존 관계, 동적 의존 관계

  • 정적 의존 관계: 클래스 간의 관계를 의미하며 컴파일 시간에 결정되어 정적 의존 관계라 한다. 프로그램을 실행하지 않고, 컴파일러들이 클래스 내에서 사용하는 타입의 의존 관계를 파악한다.
  • 동적 의존 관계: 프로그램을 구동 중이라는 뜻으로 런타임이 있다. 런타임 도중에 클래스 관계를 파악하는 것이 동적 의존 관계라 한다.
    호출부의 getInfo(Object obj) 파라미터 타입이 Object 이다. 그렇지만 들어오는 인스턴스는 Cat 타입이 올 수 있고, Car 타입이 올 수 있다.
    이렇듯 런타임에 어떤 인스턴스를 사용하는지 나타내는 것이 동적 의존 관계의 특징이다.
  • 의존 관계는 어디에 의존한다고 하면 정적 의존 관계라 한다.
    • ObjectPrint 클래스는 Object 클래스에 의존한다.
💡
정적, 동적 의존 관계가 어려울 때 메모리 구조를 떠오르도록 한다.
정적 의존 관계는 메모리 내부에서 클래스 영역끼리 의존 것이다.