객체 지향을 알고 있다면, 다형성과 상속에 대해 들어보았을 것이다. 다형성과 상속에 대해 알아보자.
다형성
다형성은 어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질로, 객체 지향에서는 어떤 객체의 속성이나 기능이 그 맥락에 따라 다른 역할을 수행할 수 있는 객체 지향의 특성이다.
- 장점 :
- 코드 재사용성이 높아진다. 동일한 코드가 다른 클래스에서 재사용될 수 있으므로, 코드의 중복을 줄이고 생산성을 향상시킬 수 있다.
- 유연성이 높아진다. 객체의 형식을 추상화하고 이를 다른 객체에서 동일한 인터페이스를 사용할 수 있어 객체 간의 결합도를 낮출 수 있다.
- 코드 가독성이 좋아진다. 동일한 이름을 가진 메서드나 연산자가 상황에 맞게 서로 다른 동작을 수행하기 때문에, 코드의 가독성이 높아진다. (예를 들어, Circle의 draw는 원을 그리고, Rectangle의 draw는 직사각형을 그린다.)
- 단점 :
- 복잡성이 증가한다. 다른 클래스에서 동일한 이름의 메서드나 연산자를 사용하므로 코드의 동작을 이해하는 데 어려움이 있을 수 있다.
- 오버 헤드가 발생한다. 런타임 시에 메서드나 연산자의 동작을 결정하기 위해 추가적인 연산이 필요할 수 있다.
- 디버깅이 어려울 수 있다. 코드의 실행경로가 다양해 디버깅이 어려울 수 있다.
다형성을 구현하는 법
다형성을 구현하는 법에는 크게 2가지가 있다.
오버로딩(Overloading)
같은 이름의 메서드를 다양한 매개변수 타입과 개수로 오버로딩하여 사용하는 것
오버로딩은 메서드 이름을 동일하게 유지하면서 다양한 상황에서 유연하게 대응할 수 있는 방법을 제공한다.
오버라이딩(Overriding)
부모 클래스의 메서드를 자식 클래스에서 재정의하여 사용하는 것
자식 클래스는 부모 클래스의 메서드를 재활용하면서, 독자적인 기능을 추가할 수 있다.
오버로딩 | 오버라이딩 | |
메서드 이름 | 동일 | 동일 |
매개변수 타입 | 다름 | 동일 |
리턴타입 | 상관 없음 | 동일 |
상속
상속은 기존의 클래스를 재활용하여 새로운 클래스를 작성하는 자바의 문법 요소이다.
클래스 간 공유될 수 있는 속성과 기능들을 상위 클래스로 추상화시켜 해당 상위 클래스로부터 확장된 여러 개의 하위 클래스들이 모두 상위 클래스의 속성과 기능들을 간편하게 사용할 수 있도록 한다.
따라서, 반복적인 코드를 최소화하고 공유하는 속성과 기능에 간편하게 접근하여 사용할 수 있도록 한다.
상속은 코드 재사용만을 위한 기법이 아니다. 일반적인 클래스가 이미 구현되어 있는 상태에서 그보다 좀 더 구체적인 클래스를 구현하기 위해 사용되는 기법이다.
상속은 좋은 객체 지향 기술로 보여지지만, 실제로 상속은 정말 개념적으로 연관관계가 있을 때만 하는 상당히 제한적으로 다뤄진다.
instance Of 알아보기 (캡슐화와 SOLID를 곁들인..)
객체가 어떤 클래스인지, 어떤 클래스를 상속받았는지 확인하는데 사용하는 연산자 이다. 즉, 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기위해 해당 연산자를 사용한다. (참고)
instanceof의 사용의 단점은 다음과 같다.
가장 큰 단점은 instanceof를 사용함으로써 각 객체가 무엇인지, 어떤 값을 반환해야 하는지에 대한 정보가 불필요하게 외부 객체에 노출될 수 있다는 점이다. 이는 객체 지향의 캡슐화를 깨뜨리는 결과를 초래한다. 각 객체가 가진 책임과 역할을 분리함으로써 유지 보수와 확장성을 높이기 위해 객체 지향 프로그래밍을 사용하는데, 캡슐화가 깨지면 객체 지향 프로그래밍의 본래 의미가 상실된다.
캡슐화
객체의 데이터(필드), 동작(메소드)을 하나로 묶고, 실제 구현 내용을 외부에 감추는 것
외부 객체는 객체 내부의 구조를 알지 못하며, 객체가 노출해서 제공하는 필드와 메소드만 이용할 수 있다.
캡슐화를 하는 이유는 외부의 잘못된 사용으로 인해 객체가 손상되지 않도록 하기 위함이다.
자바에서는 접근제한자를 통해 구현한다.
(참고)
그 외에도
매번 새로운 타입이 추가될 때마다 instanceof에 해당 조건을 추가해야 하므로, 객체의 확장이 어려워진다는 문제점이 있다. 따라서, 객체의 확장에는 열려있고, 변화에는 닫혀있도록 해야하는 Open Closed Principle에 위반되고,
instanceof를 사용하여 객체의 타입을 확인하고 그에 맞는 행동을 수행하는 메서드가 있다고 가정할 때,
메서드는 여러 타입에 대한 로직을 모두 알아야 하며, 따라서 여러 가지 책임이 가중되어 Single Responsibility Principle에 위반되며,
instanceof의 경우 알맞은 타입을 찾을 때까지 컴파일 시에 모든 타입을 돌며 검사하는 성능의 이슈가 발생하는
이유가 있다.
다음의 글을 참고하였습니다.
상속의 단점
결합도가 높아진다.
결합도는 하나의 모듈이 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지 나타내는 의존 정도를 뜻한다.
객체 지향에서는 결합도는 낮을 수록, 응집도는 높을 수록 좋다.
상속을 하게 되면 부모 클래스와 자식 클래스 관계가 컴파일 시점에 결정되어 결합도가 높아진다.
컴파일 시점에 결정되는 관계는 유연성을 상당히 떨어뜨리고, 실행 시점에 객체의 종류를 변경하는 것이 불가능하여 다형성 및 객체 지향 기술을 사용할 수 없다.
불필요한 기능 상속
부모 클래스에 메소드를 추가했을 때, 자식 클래스에는 적합하지 않는 메소드가 상속되는 문제이다.
부모 클래스의 결함이 넘어온다.
부모 클래스의 결함이 그대로 자식 클래스에 넘어온다. 이는 자식 클래스가 잘 설계되었더라도 부모 클래스에 의해 문제가 발생할 수 있다.
부모 클래스를 변경하면 자식 클래스도 변경해야 한다.
부모 클래스와 자식 클래스 사이의 개념적인 결합으로 인해, 부모 클래스를 변경할 때 자식 클래스도 함께 변경해야 할 수 있다.
클래스 폭발
상속을 남용하게 될 때, 새롭게 만든 클래스에 하나의 기존 기능을 연결하기 위해 상속을 하게 될 것이고, 또 다시 새롭게 만든 클래스에 기능을 연결하기 위해 상속을 하고,... 이렇게 반복되면서 필요 이상으로 많은 수의 클래스를 추가해야 하는 경우를 가리켜 클래스 폭발이라고 한다.
따라서, 위와 같은 상속의 단점 때문에 상속은 되도록이면 사용하지 않는다.
만약, 추상화가 필요하면 인터페이스로 implements 하거나 객체 지향 설계를 할 때는 합성을 사용한다.
그렇다면, 합성이 무엇일까?
합성
합성은 중복되는 로직들을 갖는 객체를 구현하고, 이 객체를 주입받아 중복 로직을 호출함으로써 퍼블릭 인터페이스를 재사용하는 방법이다.
따라서, 합성은 구현에 의존하지 않고, 인터페이스에 의존하기 때문에 결합도가 낮다.
또한, 합성은 실행 시점에 동적으로 변경할 수 있다. (런타임)
합성은 부모 클래스에 의존하는 상속의 단점들을 대부분 해결해준다.
인터페이스
위에서 합성은 인터페이스에 대해 의존한다고 하였다.
그렇다면, 인터페이스가 무엇일까?
인터페이스는 추상화와 상속과 더불어 다형성이라는 객체 지향의 특징을 구현하는 핵심이다.
객체의 인스턴스 메소드를 이용하는 사용자 입장에서 그 객체의 내부 구현이 어떻든 깊이 학습할 필요없이 원하는 메소드만 호출하고 결과 값을 제대로 받게 해주는 간편한 상호작용 기능이다.
- 인터페이스를 작성하는 것은 추상 클래스를 작성하는 것과 같다. (추상 메서드의 집합)
- 인터페이스도 필드를 선언할 수 있지만, 변수가 아닌 상수(final)로서만 정의할 수 있다.
- public static final과 public abstract 제어자는 생략 가능하다. (인터페이스에 정의된 모든 멤버에게 적용되는 사항이기 때문)
final ?
final 키워드는 어떤 곳에 사용되냐에 따라 다른 의미를 가진다. 하지만 final 키워드를 붙이면 무언가를 제한한다는 의미를 가지는 것은 공통적이다.
변수 : final을 붙이면 이 변수는 수정할 수 없다는 의미 (단, 수정 할 수 없다는 범위는 그 변수의 값에 한정한다. 만약, 다른 객체를 참조하거나 할 때 참조하는 객체의 내부의 값은 변경할 수 있다라는 의미이다.)
메서드 : final을 붙이면 override를 제한한다. (상속 받은 클래스에서 해당 메서드를 수정해서 사용하지 못하도록 할 수 있는것이 메서드에 final을 붙이는 것)
클래스 : final 키워드를 클래스에 붙이면 상속 불가능 클래스
참고
인터페이스를 구현하고 싶다면 어떻게 해야할까?
- 인터페이스는 그 자체로는 인스턴스를 생성할 수 없다. 구현부를 만들어주는 클래스에 상속되어야 한다.
- 해당 클래스에 인터페이스를 구현하고 싶다면, implements 키워드를 쓴 후에 인터페이스를 나열한다.
- 인터페이스를 상속 받았으면, 자식 클래스에서 인터페이스가 포함하고 있는 추상 메소드를 구체적으로 구현해준다.
- 인터페이스의 가장 큰 특징은 다중 상속이 가능하다는 것이다. (인터페이스 끼리의 상속은 다중 상속이 가능하다. (메소드 구현부가 없으니 충돌 가능성이 없음))
- 자식 클래스에 extends와 implements는 동시에 가능하다.
- 인터페이스에 클래스를 상속하는 행위는 불가능 (인터페이스는 클래스와는 달리 Object 클래스가 최고 조상이 아니기 때문)
- 인터페이스의 extends는 상속이 아니다 (인터페이스는 하나의 타입이나 규격일 뿐이지 그 자체가 하나의 객체가 되는 것이 아니고, 규격이나 스펙 자체 혹은 기능 자체의 선언을 물려받은 것)
만약 클래스가 구현하는 인터페이스의 메서드 중 일부만 구현한다면, abstract를 붙여서 추상 클래스로 선언해야 한다.
추상 클래스는 인터페이스와 다른 점이 있다.
- 추상 클래스는 하위 클래스들의 공통점을 모아 추상화하여 만든 클래스이다.
- 다중 상속이 불가능하여 단일 상속만 허용한다.
- 추상 메소드 외에 일반 클래스처럼 일반적인 필드, 메서드, 생성자를 가질 수 있다.
- 추상 클래스는 추상화를 하면서 중복되는 클래스 멤버들을 통합 및 확장할 수 있다.
- 추상 클래스는 인터페이스와 달리 클래스간의 연관관계를 구축하는 것에 초점을 둔다.
언제 인터페이스를 사용하고 언제 추상 클래스를 사용할까?
이 둘은 대표적으로 '다중 상속' 기능 여부의 차이가 있지만, 이것이 포인트가 아니라 이에 따른 사용 목적이 다르다는 것에 포인트를 맞춰야 한다.
- 인터페이스 : implements 라는 키워드처럼 인터페이스에 정의된 메서드를 각 클래스의 목적에 맞게 기능을 구현하는 느낌
- 어플리케이션의 기능을 정의해야하지만 그 구현 방식이나 대상에 대해 추상화 할 때
- 서로 관련성이 없는 클래스들을 묶어주고 싶을 때 (형제 관계)
- 다중 상속(구현)을 통한 추상화 설계를 해야 할 때
- 특정 데이터 타입의 행동을 명시하고 싶은데, 어디서 그 행동이 구현되는지는 신경쓰지 않는 경우
- 클래스와 별도로 구현 객체가 같은 동작을 한다는 것을 보장하기 위해 사용
- 추상 클래스 : extends 키워드를 사용해서 자신의 기능들을 하위 클래스로 확장 시키는 느낌
- 상속 받을 클래스들이 공통으로 가지는 메소드와 필드가 많아 중복 멤버 통합을 할 때
- 멤버에 public 이외의 접근자(protected, private) 선언이 필요한 경우
- non-static, non-final 필드 선언이 필요한 경우 (각 인스턴스에서 상태 변경을 위한 메소드가 필요한 경우)
- 요구 사항과 함께 구현 세부 정보의 일부 기능만 지정했을 때
- 하위 클래스가 오버라이드하여 재정의하는 기능들을 공유하기 위한 상속 개념을 사용할 때
- 추상 클래스는 이를 상속할 각 객체들의 공통점을 찾아 추상화시켜 놓은 것으로, 상속 관계를 타고 올라갔을 때 같은 부모 클래스를 상속하여 부모 클래스가 가진 기능들을 구현해야할 경우 사용한다.
참고 1
참고 2
참고 3
'개발 > Java' 카테고리의 다른 글
자바 기본 - 예외 (2) | 2024.11.06 |
---|---|
자바 기본 - 문자열 (0) | 2024.11.05 |
자바 기본과 객체 지향 (4) (0) | 2024.10.31 |
자바 기본과 객체 지향 (3) - 동일성과 동등성 (0) | 2024.10.31 |
자바 기본과 객체 지향 (2) - JDK, JRE, JVM (0) | 2024.10.31 |