Jake Seo
제이크서 개발 블로그
Jake Seo
전체 방문자
오늘
어제
  • 분류 전체보기 (719)
    • AI 서비스 개발 일기 (3)
    • LLM 개발 일기 (1)
    • ------레거시 (2025.08.23 이전)--.. (0)
    • 백준 문제풀이 (1)
    • 릿코드 문제풀이 (2)
    • 알고리즘 이론 (10)
      • 기본 이론 (2)
      • 배열과 문자열 (8)
    • 데이터베이스 (15)
      • Planet Scale (1)
      • MSSQL (9)
      • 디비 기본 개념 (1)
      • SQLite 직접 만들어보기 (4)
    • 보안 (7)
    • 설계 (1)
    • 네트워크 (17)
      • HTTP (9)
      • OSI Layers (5)
    • 회고 (31)
      • 연간 회고 (2)
      • 주간 회고 (29)
    • 인프라 (52)
      • 도커 (12)
      • AWS (9)
      • 용어 (21)
      • 웹 성능 (1)
      • 대규모 서비스를 지탱하는 기술 (9)
    • 깃 (7)
    • 빌드 도구 (7)
      • 메이븐 (6)
      • 그레이들 (0)
    • Java (135)
      • 이펙티브 자바 (73)
      • 자바 API (4)
      • 자바 잡지식 (30)
      • 자바 디자인 패턴 (21)
      • 톰캣 (Tomcat) (7)
    • 프레임워크 (64)
      • next.js (14)
      • 스프링 프레임워크 (28)
      • 토비의 스프링 (6)
      • 스프링 부트 (3)
      • JPA (Java Persistence API) (5)
      • Nest.js (8)
    • 프론트엔드 (48)
      • 다크모드 (1)
      • 노드 패키지 관리 매니저 (3)
      • CSS (19)
      • Web API (11)
      • tailwind-css (1)
      • React (5)
      • React 새 공식문서 요약 (1)
      • HTML (Markup Language) (5)
    • 자바스크립트 (108)
      • 모던 자바스크립트 (31)
      • 개념 (31)
      • 정규표현식 (5)
      • 코드 스니펫 (1)
      • 라이브러리 (6)
      • 인터뷰 (24)
      • 웹개발자를 위한 자바스크립트의 모든 것 (6)
      • 팁 (2)
    • Typescript (49)
    • 리눅스와 유닉스 (10)
    • Computer Science (1)
      • Compiler (1)
    • IDE (3)
      • VSCODE (1)
      • IntelliJ (2)
    • 세미나 & 컨퍼런스 (1)
    • 용어 (개발용어) (16)
      • 함수형 프로그래밍 용어들 (1)
    • ORM (2)
      • Prisma (2)
    • NODEJS (2)
    • cypress (1)
    • 리액트 네이티브 (React Native) (31)
    • 러스트 (Rust) (15)
    • 코틀린 (Kotlin) (4)
      • 자바에서 코틀린으로 (4)
    • 정규표현식 (3)
    • 구글 애널리틱스 (GA) (1)
    • SEO (2)
    • UML (2)
    • 맛탐험 (2)
    • 리팩토링 (1)
    • 서평 (2)
    • 소프트웨어 공학 (18)
      • 테스팅 (16)
      • 개발 프로세스 (1)
    • 교육학 (1)
    • 삶의 지혜, 통찰 (1)
    • Chat GPT (2)
    • 쉘스크립트 (1)
    • 컴파일 (2)
    • Dart (12)
    • 코드팩토리의 플러터 프로그래밍 (4)
    • 플러터 (17)
    • 안드로이드 스튜디오 (1)
    • 윈도우즈 (1)
    • 잡다한 백엔드 지식 (1)
    • 디자인 패턴 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 알고리즘
  • 슬로우 쿼리
  • 추상 팩터리 패턴
  • Pre-rendering
  • Java
  • 외래키 제약조건
  • prerendering
  • 싱글턴
  • NEXT JS
  • item7
  • 토비의 스프링
  • 느린 쿼리
  • 자바스크립트 인터뷰
  • Javadoc 자바독 자바주석 주석 Comment
  • 싱글톤 패턴
  • 스프링 검증
  • MSSQL
  • 싱글톤
  • next js app
  • item9
  • 서버리스 컴퓨팅
  • 이펙티브 자바 item9
  • 팩터리 메서드 패턴
  • 자바 검증
  • 작업기억공간
  • pnpm
  • 객체복사
  • rust
  • 자바 디자인패턴
  • 자바스크립트
  • 플라이웨이트패턴
  • 이펙티브자바
  • 러스트
  • 메이븐 골
  • 도커공식문서
  • try-with-resources
  • 참조 해제
  • 빈 검증
  • 프로그래머의 뇌
  • item8
  • 자바스크립트 면접
  • 메이븐 라이프사이클
  • 메이븐 페이즈
  • serverless computing
  • Next.js
  • 이펙티브 자바
  • 디자인패턴
  • 자료구조
  • 자바
  • bean Validation

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Jake Seo

제이크서 개발 블로그

Java/이펙티브 자바

이펙티브 자바, 쉽게 정리하기 - item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라.

2022. 1. 4. 20:21

이펙티브 자바, 쉽게 정리하기 - item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라.

상속용 클래스가 지켜야 할 것들

  • 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
    • 어떤 순서로 호출하는지, 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 한다.
    • 재정의 가능 메서드란 public, protected 중 final이 아닌 모든 메서드를 말한다.
    • 재정의 가능한 메서드를 호출할 수 있는 모든 상황을 문서로 남기는 것이 좋다.
      • 백그라운드 스레드나 정적 초기화 과정에서 호출될 수도 있으므로 유의하자.

Implentation Requirements와 @implSpec 태그 - remove() 의 예

API 문서 메서드 설명 끝에서 발견할 수 있는 문구 중 Implementation Requirements가 있다. 이 절은 메서드 주석에 @implSpec 태그를 붙이면 자바독 도구가 생성해준다.

/**
* {@inheritDoc}
*
* @implSpec
* This implementation iterates over the collection looking for the
* specified element.  If it finds the element, it removes the element
* from the collection using the iterator's remove method.
*
* <p>Note that this implementation throws an
* {@code UnsupportedOperationException} if the iterator returned by this
* collection's iterator method does not implement the {@code remove}
* method and this collection contains the specified object.
*
* @throws UnsupportedOperationException {@inheritDoc}
* @throws ClassCastException            {@inheritDoc}
* @throws NullPointerException          {@inheritDoc}
*/
public boolean remove(Object o) {
  Iterator<E> it = iterator();
  if (o==null) {
      while (it.hasNext()) {
          if (it.next()==null) {
              it.remove();
              return true;
          }
      }
  } else {
      while (it.hasNext()) {
          if (o.equals(it.next())) {
              it.remove();
              return true;
          }
      }
  }
  return false;
}
  • 주석 위쪽 @implSpec 태그를 볼 수 있다.
    • 엘리먼트를 찾기 위해 컬렉션을 순회하고, iterator의 remove()를 통해 원소를 제거한다고 적혀있다.
    • iterator에 remove()가 구현되어 있지 않다면, UnsupportedOperationException을 던진다고 상세히 설명하고 있다.
  • @implSpec은 이 클래스를 상속하여 메서드를 재정의했을 때 나타날 효과를 상세히 설명하고 있다.
    • 이 주의점을 통해 우리는 어떤 메서드를 어떤 방식으로 상속해야 할지 알 수 있다.
    • 상속용 클래스가 지켜야 할 좋은 문서화의 예이다.

훅(hook) 메서드 공개하기 - removeRange() 의 예

/**
 * Removes from this list all of the elements whose index is between
 * {@code fromIndex}, inclusive, and {@code toIndex}, exclusive.
 * Shifts any succeeding elements to the left (reduces their index).
 * This call shortens the list by {@code (toIndex - fromIndex)} elements.
 * (If {@code toIndex==fromIndex}, this operation has no effect.)
 *
 * <p>This method is called by the {@code clear} operation on this list
 * and its subLists.  Overriding this method to take advantage of
 * the internals of the list implementation can <i>substantially</i>
 * improve the performance of the {@code clear} operation on this list
 * and its subLists.
 *
 * @implSpec
 * This implementation gets a list iterator positioned before
 * {@code fromIndex}, and repeatedly calls {@code ListIterator.next}
 * followed by {@code ListIterator.remove} until the entire range has
 * been removed.  <b>Note: if {@code ListIterator.remove} requires linear
 * time, this implementation requires quadratic time.</b>
 *
 * @param fromIndex index of first element to be removed
 * @param toIndex index after last element to be removed
 */
protected void removeRange(int fromIndex, int toIndex) {
    ListIterator<E> it = listIterator(fromIndex);
    for (int i=0, n=toIndex-fromIndex; i<n; i++) {
        it.next();
        it.remove();
    }
}
  • 주석에서 이 메서드가 clear()에 의해 호출됨을 알리고 있다.
  • clear()를 고성능으로 만들기 쉽게 하기 위해 이 메서드를 외부로 공개하고 있다.
  • 이렇게 특정한 이유로 protected 접근 제어자로 메서드를 노출해야 할 필요가 있는 경우도 있다.

상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 유일하다. 직접 시험하며 어떤 메서드를 공개할지 선택하면 된다. 만일 하위 클래스를 여러개 만드는 동안 한번도 쓰이지 않는 protected 멤버가 존재한다면, private이었어야 할 가능성이 크다. 널리 쓰일 클래스를 상속용으로 설계한다면 설계의 결정요소와 문서화의 책임이 더욱 크다. 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증하자.

재정의 가능 메서드를 생성자에서 사용하면 안된다.

  • 상속용 클래스의 생성자는 직접적이든, 간접적이든, 재정의 가능 메서드를 호출하면 안된다.
    • 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되기 때문이다.
public class Super {
  public Super() {
    overrideMe();
  }

  public void overrideMe() {
  }
}
public class Item19Test {
    static class Super {
        public Super() {
            overrideMe();
        }

        public void overrideMe() {
            System.out.println("super's override me");
        }
    }

    static class Sub extends Super {
        private final Instant instant;

        Sub() {
            // 상속받은 클래스는 자동으로 부모 클래스의 생성자를 호출한다.
            instant = Instant.now();
        }

        @Override
        public void overrideMe() {
            System.out.println("instant = " + instant);
        }
    }

    @Test
    public void constructorTest() {
        Sub sub = new Sub();
        sub.overrideMe();
    }
}

출력 결과

instant = null
instant = 2022-01-01T11:06:14.830557Z
  • 상위 클래스는 하위 클래스가 인스턴스 필드를 초기화하기도 전에 overrideMe()를 호출한다.
  • final 필드의 상태가 두가지다. (정상이 아니다.)
  • print는 null을 받아들이기 때문에 정상적으로 실행됐지만, 다른 경우 null 값을 사용했다면, NullPointerException의 위험이 존재한다.

Cloneable, Serializable

  • 직렬화, 객체 복사에 사용되는 clone(), readObject()와 같은 경우도 생성자와 비슷한 효과를 가지고 있으므로 직접적이든 간접적이든 재정의 가능한 메서드를 호출해선 안 된다.
    • readObject()는 역직렬화가 끝나기 전에 재정의한 메서드부터 호출하게 된다.
    • clone()는 하위 클래스의 clone() 메서드가 복제본의 상태를 올바른 상태로 수정하기 전에 재정의한 메서드를 호출한다.
      • clone()이 잘못되면 원본 객체에도 피해를 줄 수 있다.

상속용 클래스와 그 제약

  • 클래스를 상속용으로 설계하려면 엄청난 노력이 들고 그 클래스 안에 제약도 상당하다.
  • 상속용으로 설계하지 않은 클래스는 상속을 금지하는 편이 버그를 줄일 수 있다.
    • 클래스를 final로 만들어 상속을 금지한다.
    • 모든 생성자를 private 혹은 package-private으로 선언하고 public 정적 팩터리를 만든다.
  • 혹여나 일반 클래스에서 상속을 허용하고 싶다면, 재정의 가능 메서드는 절대 사용하지 않도록 문서에 표기하자.

핵심 정리

  • 상속용 메서드를 만들 때는 클래스 내부에서 스스로를 어떻게 사용하는지 문서로 남기자.
  • 문서화한 것은 그 클래스가 쓰이는 한 반드시 지키자.
    • 그렇지 않을 경우 하위 클래스의 오동작을 만들 수 있다.
  • 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하자.
    • 클래스를 final로 만들거나 생성자를 모두 외부에서 접근 불가능하게 바꾸면 된다.
저작자표시 (새창열림)

'Java > 이펙티브 자바' 카테고리의 다른 글

이펙티브 자바, 쉽게 정리하기 - item 21. 인터페이스는 구현하는 쪽을 생각해 설계하라  (0) 2022.01.04
이펙티브 자바, 쉽게 정리하기 - item 20. 추상 클래스보다는 인터페이스를 우선하라  (0) 2022.01.04
이펙티브 자바, 쉽게 정리하기 - item 18. 상속보다는 컴포지션을 사용하라  (2) 2022.01.04
이펙티브 자바, 쉽게 정리하기 - item 17. 변경 가능성을 최소화하라  (0) 2022.01.03
이펙티브 자바, 쉽게 정리하기 - item 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라  (0) 2022.01.01
    'Java/이펙티브 자바' 카테고리의 다른 글
    • 이펙티브 자바, 쉽게 정리하기 - item 21. 인터페이스는 구현하는 쪽을 생각해 설계하라
    • 이펙티브 자바, 쉽게 정리하기 - item 20. 추상 클래스보다는 인터페이스를 우선하라
    • 이펙티브 자바, 쉽게 정리하기 - item 18. 상속보다는 컴포지션을 사용하라
    • 이펙티브 자바, 쉽게 정리하기 - item 17. 변경 가능성을 최소화하라
    Jake Seo
    Jake Seo
    ✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.

    티스토리툴바