클린코드

6. 객체와 자료구조

자료/객체 비대칭

객체는 동작을 공개하고 자료를 숨긴다. 그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기는 쉽다. 반면 기존 객체에 새 동작을 추가하기는 어렵다.

//다형적인 도형 (Polymorphic Shape)
public class Square implements Shape {
  private Point topLeft;
  private double side;

  public double area() {
    return side * side;
  }
}

public class Rectangle implements Shape {
  private Point topLeft;
  private double height;
  private double width;

  public double area() {
    return height * width;
  }
}

public class Circle implements Shape {
  private Point center;
  private double radius;
  public final double PI = 3.141592653589793;

  public double area() {
    return PI * radius * radius;
  }
}

반대로 객체를 사용하지 않고, 자료에 해당하는 DTO등을 사용한다면 절차지향적인 코드 방식으로 새 객체는 추가하지 어렵지만, 기존 객체에 새 동작을 추가하기는 쉬운 코드가 된다.

//절차적인 도형 (Procedural Shape)
public class Square {
  public Point topLeft;
  public double side;
}

public class Rectangle {
  public Point topLeft;
  public double height;
  public double width;
}

public class Circle {
  public Point center;
  public double radius;
}

public class Geometry {
  public final double PI = 3.141592653589793;

  public double area(Object shape) throws NoSuchShapeException {
    if (shape instanceof Square) {
      Square s = (Square)shape;
      return s.side * s.side;
    } else if (shape instanceof Rectangle) {
      Rectangle r = (Rectangle)shape;
      return r.height * r.width;
    } else if (shape instanceof Circle) {
      Circle c = (Circle)shape;
      return PI * c.radius * c.radius;
    }
    throw new NoSuchShapeException();
  }
}

디미터 법칙

디미터 법칙은 잘 알려진 휴리스틱heuristic(경험에 기반하여 문제를 해결하거나 학습하거나 발견해 내는 방법)으로, 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙 이다.

즉, 기차충동을 피하라!

// 기차 충돌
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

위의 코드는 임시파일을 생성하기위해 경로값을 얻는 코드이다. 객체에게서 경로값을 얻는 대신 임시파일을 만들라는 메서드를 추가한다.

이렇게 ctxt의 내부구조를 드러내지 않고, 여러 객체를 탐색하지 않도록(즉, 디미터 법칙을 만족하도록)하는게 좋다.

Abstract vs Interface

추상클래스 추상클래스를 설명하자면 없거나 하나 이상의 추상메소드를 가지고 있는 것이 추상클래스다.

오브젝트 중

역할의 개념을 적용하면 Movie가 구체적인 클래스는 알지 못한 채 오직 역할에 대해서만 결합되도록 의존성을 제한할 수 있다.

역할을 사용하면 객체의 구체적인 타입을 추상화할 수 있다.

자바에서는 일반적으로 역할을 구현하기 위해 추상 클래스나 인터페이스를 사용한다. 역할을 대체할 클래스들 사이에서 구현을 공유해야 할 필요가 있다면 추상클래스를 사용하면 된다. 구현을 공유할 필요 없이 역할을 대체하는 객체들의 책임만 정의하고 싶다면 인터페이스를 사용하면 된다.

default 추가이후, 메서드 유형과, 최종 변수라는 특징은 약해졌다.

따라서 그 후 구분되는 차이는 인터페이스를 구현한다는 표현과, 추상 클래스를 확장 한다는 표현의 차이이다.

또한 다중 구현의 가능여부이다.

Default method

그렇다면 의문.

추상클래스가 왜필요하지 ?

Default메서드 쓰면 되는데

-> 여기에 대해 왜 java8에 default가 추가되었는지 알아보자.

https://blog.powerumc.kr/473

원래 객체지향 언어에서 Interface는 그 시그너처와 선언이 변하지 않는다는 것을 전제로 하여 다형성(polymophism)을 정의하는 객체간의 규약이다. 그러나 Java8 Interface는 Interface를 업그레이드하는 개념을 도입하였다.

Now users of your code can choose to continue to use the old interface or to upgrade to the new interface. Alternatively, you can define your new methods as default methods

-> 이제 구현코드의 포함으로 인해 interface를 “규약 또는 약속”으로 정의하기가 모호해졌다.

-> 다시 다중 상속으로 인한 모호성 문제 (c++의 해결책과 유사)

즉, 에러가 난다. 구현시 명시적으로 재정의 해야함

public interface OperateCar {
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}
public interface FlyCar {
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}
 
public class FlyingCar implements OperateCar, FlyCar {
    public int startEngine(EncryptedKey key) {
        FlyCar.super.startEngine(key);
        OperateCar.super.startEngine(key);
    }
}

디폴트 메소드 vs 추상클래스 -> 디폴트 메소드가 생기면서 추상클래스와 인터페이스의 다른점이 모냐라고 궁금증을 가질 수 있다. 차이점 인터페이스는 private 값을 가지지 못한다.(오직 public, abstract, default, static 상태만 가질 수 있다.) 인터페이스는 생성자를 가질 수 없지만 추상클래스는 생성자를 가질 수 있다.

즉 추상클래스는 상속받아 기능을 확장하는것, 인터페이스는 구현한 객체의 같은 동작을 보장하기 위함이라는 목적은 유지가 된다고 볼 수 있을 것 같다.

Geeks for Geeks에 따르면

Abstraction: Hiding the internal implementation of the feature and only showing the functionality to the users. i.e. what it works (showing), how it works (hiding). Both abstract class and interface are used for abstraction.

추상화 : 내부 구현을 숨기고, 유저에게 기능만 보여준다. 추상 클래스와 인터페이스가 이를 담당한다.

다음중 하나라도 해당할 경우 추상 클래스 사용

  1. 반드시 필요한 일부 코드를 공유해야 하는 경우
  2. Static 또는 final이 아닌 필드 선언할 필요가 있고, 메서드를 통해 이 필드를 수정하거나 접근 할 필요가 있을때.
  3. 즉 공통 메서드나 공통 필드를 가져야 할때, 혹은 publc이외의 접근제어자가 필요할때 추상 클래스를 사용한다.

다음 중 하나라도 해당할 경우 인터페이스를 사용

  1. 완전한 추상화. 즉 이 인터페이스를 구현하는 클래스는 반드시 인터페이스에 선언된 메서드 모드를 구현해야한다.
  2. 여러 인터페이스를 상속해야 할때
  3. 특정 데이터 타입의 행위는 구체화 하지만, 누가 이 행위를 하는지는 상관이 없을 때.

추상클래스의 궁극적인 목적은 상속하기 위함이다.

추상클래스는 객체의 추상적인 상위 개념으로 공통된 개념을 표현할 때 쓴다. 위에 예를 들어, 동물에는 공통적으로 이름과 색깔이 있고, 달린다는 공통된 행동이 있다. 하지만, 각기 요구하는 객체구현 방식마다 객체가 달라지겠지만, 공통된 속성을 동물이라는 추상클래스에 추상메소드로 구현하여 하위 개념인 말이라는 객체에 상속시켜 구현하면 객체의 재사용성과 객체를 표현하는 것이 더 명확해진다고 생각한다. 그리고 동물마다 차이가 생기는 태울 수(탈 수) 있는가, 없는가를 나누기 위해 RideInterface를 구분하여 동물 중에서 태울 수 있는 동물들에게 Implements 시키면 객체를 좀더 잘 표현할 수 있을거라고 생각한다.

특히, 인터페이스는 구성하는 요소들이 자주 바뀔때 쓰면 유용하다. 그리고, 메소드 형태만 서로 공유해서 구현(재정의)되는 상황일 때 사용하는 것이 적합하다. 즉, 공통된 메소드(객체행위)로 만들어 사용하고 싶을때 인터페이스를 이용하면 효과적이다.

-> 즉 행동만 필요하고, 메부의 구현을 전혀 신경 쓸 필요가 없을때.


https://brunch.co.kr/@kd4/6

인터페이스와 추상 클래스는 존재 목적이 다르다. 추상 클래스는 그 추상 클래스를 상속받아서 기능을 이용하고, 확장 시키는 데 있다. 반면 인터페이스는 함수의 껍데기만 있는데, 그 이유는 그 함수의 구현을 강제하기 위해서 입니다. 구현을 강제함 으로써 구현 객체들의 같은 동작을 보장 할 수 있다.

상속 : 슈퍼 클래스의 기능을 이용하거나 확장하기 위해 사용. 다중 상속의 모호성때문에 하나만 상속받을 수 있다.

인터페이스 : 해당 인터페이스를 구현한 객체들에 대해 동일한 동작을 약속하기 위해 존재한다.

  • 자바는 다중 상속의 모호성때문에 다중 상속을 못하게 했고, 이때문에 interface에 abstract class와 상호 보환적인 점들이 추가되게 됨 다중상속의 모호성 : 상속하는 클래스들이 같은 메서드를 가질때. 문제발생

다형성

다형성이란 동일한 조작방법으로 동작시키지만 동작방법은 다른 것을 의미한다.

메서드의 다형성 vs 클래스의 다형성

객체지향개념에서 다형성이란 ‘여러 가지 형태를 가질 수 있는 능력’을 의미하며 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함(= 하나의 클래스가 다양한 동작방법을 가지고 있는 것)으로써 다형성을 프로그램적으로 구현하였다.

“상위 클래스의 좀 더 포괄적인 개념을 이용해서 하위 클래스들에 동일한 방식으로 접근하되, 그 하위 클래스의 특화된 메서드를 실행하는 기법”

Java에서 다형성은 상속과 인터페이스를 통해 이루어진다.

Java에서 다형성은 오버라이딩과 오버로딩을 통해 이루어진다.

같은 행위를 하지만 용도와 목적에 부합하여 다양한 기능수행과 처리, 결과를 낳을 수 있는 것이다.

참고로 overloading이 다형성인지 아닌지에 대해서는 이견이 존재하는 것으로 보인다. By 생활코딩

호출할 수 있는 메소드 역시 타입에 따라 달라진다는 것이다. 상속의 오버라이딩을 설명하면서 오버라이딩을 하게 되면 컴파일러는 실제 객체의 메소드를 바라보는 것이 아니라. 변수 선언 타입의 메소드를 본다.

다형성이 왜 필요한가?

  1. 코드 재사용성, 유지보수성
  2. 확장성(메서드를 재정의) 유연성(하나의 인터페이스를 일관성 있게 사용자 중심적으로 제공 = 편리성)
  3. 객체지향 설계원칙에 부합함

  4. 의존성 약화

다형성의 특징

동적 바인딩 : 런타임시 호출 클래스 및 메소드 결정

재사용성 , 추상화, 상속성 개념 포함

메세지