Search
Duplicate
📒

[Java Study] 04-3. 클래스와 인터페이스

상태
완료
수업
Java Study
주제
4 more properties
참고

클래스와 인터페이스

NOTE
클래스인터페이스를 쓰기 편하고, 견고하며, 유연하게 만드는 방법을 안내한다!

상속과 컴포지션

NOTE
상속은 캡슐화를 깨트리므로, 컴포지션(구성)과 전달(delegation)을 사용하자!

상속의 문제점

상속은 강력하지만, 잘못 사용되는 경우 여러 문제를 일으킬 수 있다.
슈퍼 클래스의 내부 구현에 의존하게 되어, 슈퍼 클래스 구현이 변경되는 경우 서브클래스에 문제가 생길 수 있다.
슈퍼 - 서브 클래스간의 강한 결합이 생기므로, 유연성과 재사용성을 떨어트린다.
슈퍼 클래스가 상속을 통해 확장하기에 적합하지 않을 수 있다.

컴포지션(조합)과 전달의 사용

class Engine {} // 엔진 클래스 class Automobile {} // 차의 부모 클래스 탈 것 class Car extends Automobile { // 차는 탈것이다. 그러므로 차는 탈것 클래스를 확장한거다. private Engine engine; // 차는 엔진을 가진다. 차는 그러므로 엔진 인스턴스를 가진다. }
Java
복사
Car - Automobile: Is-A 상속관계
Automobile - Engine: Has-A 컴포지션관계
public class InstrumentedSet<E> extends ForwardingSet<E> { private int addCount = 0; public InstrumentedSet(Set<E> s) { super(s); } @Override public boolean add(E e) { addCount++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); } public int getAddCount() { return addCount; }
Java
복사
public class ForwardingSet<E> implements Set<E> { private final Set<E> s; public ForwardingSet(Set<E> s) { this.s = s; } public void clear() { s.clear(); } public boolean contains(Object o) { return s.contains(o); } // 나머지 Set 인터페이스 메서드들을 s에 전달(delegate)... }
Java
복사
InstrumentedSet은 내부적으로 사용하는 Set의 구현을 바꿈으로써 다양한 방식으로 활용될 수 있습니다.
ex) HashSet, TreeSet 등 어떤 Set 구현체를 감싸도록 변경하는것이 매우 간단하다.
ForwardingSet 클래스는 다른 Set 데코레이터 클래스에서도 재사용될 수 있으며, 이를 통해 Set 인터페이스를 구현하는 다양한 클래스에 공통적인 전달 로직을 중복 구현하지 않고 재사용할 수 있다.
새로운 기능이 필요할 때마다 상속을 통해 기존 클래스를 확장하는 대신, 적절한 인터페이스와 함께 컴포지션을 사용하여 유연하게 확장할 수 있습니다.

추상 클래스보다 인터페이스가 좋다

NOTE
추상 클래스, 인터페이스 각자의 장점이 있지만 인터페이스를 사용하는것이 좋다!
인터페이스 vs 추상클래스 비교

인터페이스 이점

인터페이스는 다중 구현이 가능하며, 이는 클래스가 여러 타입으로 동작할 수 있어 유연성을 증가시킵니다. (믹스인)
인터페이스는 구현체에서 API를 분리할 수 있게 해주며, 이는 모듈화와 코드의 재사용성을 향상시킵니다.
인터페이스를 사용하면 특정 인터페이스를 구현하는 데코레이터, 래퍼 클래스를 쉽게 만들 수 있다.

인터페이스와 추상골격 구현

인터페이스와 추상 클래스의 장점을 모두 취하는 방법이다.
인터페이스: 타입 정의 및 디폴트 메서드 제공
추상 클래스(골격): 나머지 메서드들을 모두 제공
public interface Book { void openBook(); void prepareContent(); void interpretContent(); void closeBook(); void readBook(); }
Java
복사
인터페이스
public abstract class AbstractBook implements Book { @Override public void openBook() { System.out.println("Opening the book."); } @Override public void closeBook() { System.out.println("Closing the book."); } // readBook 메소드는 템플릿 메소드로, 골격 구현에서 제공 @Override public void readBook() { openBook(); prepareContent(); interpretContent(); closeBook(); } // prepareContent와 interpretContent는 여전히 추상 메소드로 남겨져 있어, // 서브 클래스에서 구현해야 함. @Override public abstract void prepareContent(); @Override public abstract void interpretContent(); }
Java
복사
골격 구현 클래스 - 추상 클래스가 일반적이나 인터페이스도 가능함
// 픽션 책을 위한 구체 클래스 public class FictionBook extends Book { @Override public void prepareContent() { System.out.println("Preparing fiction book content."); } @Override public void interpretContent() { System.out.println("Interpreting fiction book content."); } } // 비픽션 책을 위한 구체 클래스 public class NonFictionBook extends Book { @Override public void prepareContent() { System.out.println("Preparing non-fiction book content."); } @Override public void interpretContent() { System.out.println("Interpreting non-fiction book content."); } }
Java
복사
사용 코드 - open/close 중복코드 없고, 커스텀 메서드도 만들 수 있음

인터페이스 - 구현체를 고려하고, 타입 정의에만 써라

NOTE
인터페이스를 주로 타입 정의에 쓴다는건, 클래스가 어떤 행동을 하는지에 대해서만 클라이언트에게 알려주라는 의미이다!
인터페이스는 해당 인터페이스를 구현하는 클래스가 반드시 준수해야 하는 계약을 명시합니다.
리스코프 치환 원칙(LSP)와 일맥상통하며, 하위 타입은 자신의 기본 타입으로 대체될 수 있어야 합니다.
클래스가 인터페이스를 구현하면, 다양한 구현체를 쉽게 교체하여 유연성과 유지보수성을 향상시킬 수 있습니다.
예) 테스트 중에는 Mock DB를 사용합니다.
인터페이스는 다형성을 활용하는 강력한 방법을 제공합니다. 서로 다른 클래스들이 같은 인터페이스를 구현하면, 같은 인터페이스 타입으로 처리될 수 있습니다.
인터페이스를 사용하면, 클라이언트 코드는 구현 세부 사항에서 분리됩니다.

디폴트 메서드

디폴트 메서드는 앞서 말한 타입 정의에 배치되는 것처럼 보일 수 있지만, 이를 활용하면 더 유연하고 확장성 있는 방법을 제공합니다.
디폴트 메서드를 올바르게 사용하려면 다음의 지침을 따라야 합니다.
디폴트 메서드를 남용하지 않고, 추상적인 계약을 명확하게 표현하는 데에 집중합니다.
디폴트 메서드는 간단한 확장이나 유틸리티 메서드에 적합합니다.
리스코프 치환원칙(LSP)와 일맥상통 하는데, 하위타입은 그들의 기본 타입으로 교체될 수 있어야 한다고 말한다.

태그 달린 클래스보다는 클래스 계층 구조를 활용하라

NOTE
2가지 이상의 의미를 표현하기 위한 클래스는 태그가 아닌 계층 구조를 활용하자!
public class Figure { enum Shape {RECTANGLE, CIRCLE} // 태그 private final Shape shape; // 사각형일때만 사용 private final double length; private final double width; // 원형일때만 사용 private final double radius; public Figure(Shape shape, double radius) { this.shape = shape; this.length = 0; this.width = 0; this.radius = radius; } public Figure(Shape shape, double length, double width) { this.shape = shape; this.length = length; this.width = width; this.radius = 0; } public double area() { switch (shape) { case RECTANGLE: return length * width; case CIRCLE: return Math.PI * (radius * radius); default: throw new IllegalArgumentException(); } } }
Java
복사
태그 사용 - 문제점이 많음
열거 타입 선언, 태그 필드, switch문 등 쓸데 없는 코드가 많아진다.
다른 의미를 위한 코드를 많이 사용하므로 메모리를 많이 사용한다.
// 추상 기반 클래스 정의 abstract class Shape { abstract double area(); } // Circle 클래스 class Circle extends Shape { private final double radius; public Circle(double radius) { this.radius = radius; } @Override double area() { return Math.PI * radius * radius; } } // Rectangle 클래스 class Rectangle extends Shape { private final double length; private final double width; public Rectangle(double length, double width) { this.length = length; this.width = width; } @Override double area() { return length * width; } }
Java
복사
계층화 사용 - 최적화
이전보다 코드가 훨씬 간결해졌다.

중첩 클래스는 static을 권장한다.

NOTE

중첩 클래스(Nested Class)

중첩 클래스는 자신을 둘러싼 바깥 클래스에서만 쓰이는 클래스를 의미합니다.
정적 멤버 클래스
비정적 멤버 클래스
익명 클래스
지역 클래스

비정적 멤버 클래스

비정적 멤버 클래스는 외부 클래스의 인스턴스와 자동으로 연결되며, 항상 외부 클래스에 속해있습니다.
public class OuterClass { private int outerField = 100; // 비정적 멤버 클래스 class NonStaticInnerClass { void display() { System.out.println("OuterField: " + outerField); // 외부 클래스의 필드에 접근 가능 } } }
Java
복사
비정적 멤버 클래스는 외부 클래스 인스턴스에 접근이 가능하다.
비정적 멤버 클래스는 외부 클래스의 인스턴스 없이 생성될 수 없다.

정적 멤버 클래스

정적 멤버 클래스는 외부 클래스의 인스턴스에 대한 참조를 유지하지 않으므로, 더 명확하고 단순한 의미구조를 가집니다.
public class OuterClass { private static int outerStaticField = 200; // 정적 멤버 클래스 static class StaticNestedClass { void display() { System.out.println("OuterStaticField: " + outerStaticField); // 외부 클래스의 정적 필드에만 접근 가능 } } }
Java
복사
정적 멤버 클래스는 외부 클래스의 인스턴스 멤버에 접근할 수 없지만, 정적 멤버에는 가능합니다.
정적 멤버 클래스는 외부 클래스 없이 생성될 수 있습니다.
정적 멤버 클래스가 왜 비정적 멤버 클래스보다 좋은이유는 다음과 같습니다.
성능: 비정적 멤버 클래스는 외부 클래스의 인스턴스에 대한 숨겨진 참조를 유지해 메모리누수 위험이 존재합니다.
독립성: 정적 중첩 클래스는 외부 클래스의 인스턴스와 독립적으로 존재할 수 있어 재사용성이 높습니다.