개발자 피터
Peter's Dev Blog
개발자 피터
전체 방문자
오늘
어제
  • 분류 전체보기 (77)
    • 🧑🏻‍💻 Develop (13)
      • Devops (3)
      • Elasticsearch (3)
      • Design Pattern (1)
      • SQL (4)
      • Architecture (1)
      • APM (1)
    • 💻 Service (7)
      • E-ROOM (3)
      • Briefing (4)
    • 💡 Problem Solving (43)
      • Baekjoon (40)
      • Programmers (2)
    • 📚 Reading (12)
      • Tech (9)
      • Self-Help (3)
    • 💬 Etc (1)
    • 📈 Retrospective (1)

블로그 메뉴

  • 🌟 깃허브
  • 🏷️ 태그 클라우드
  • 📝 방명록

공지사항

인기 글

태그

  • 문자열
  • Programmers
  • elasticsearch
  • boj
  • 백준
  • java
  • 백트래킹
  • 그리디
  • MySQL
  • SQL
  • 브루트포스
  • briefing
  • 구현
  • E-ROOM
  • 독서

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
개발자 피터

Peter's Dev Blog

📚 Reading/Tech

이펙티브 자바 - 4장. 클래스와 인터페이스

2023. 1. 24. 15:13
728x90
반응형

4장 - 클래스와 인터페이스


💡 클래스와 멤버의 접근 권한을 최소화하라

“접근 권한을 가능한 한 좁히자”

1. 정보 은닉(캡슐화)

  • 어설프게 설계된 컴포넌트와 잘 설계된 컴포넌트의 가장 큰 차이는 바로
    “클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼느냐”다.
  • 잘 설계된 컴포넌트는 모든 내부 구현을 완벽히 숨기며 오직 API를 통해서 다른 컴포넌트와 소통함. → 정보은닉(캡슐화)
  • 정보 은닉의 장점
    • 컴포넌트를 병렬로 개발할 수 있음 → 시스템 개발 속도↑
    • 컴포넌트 교체 부담↓ → 시스템 관리 비용↓
    • 외부에 거의 의존하지 않고 독자적으로 동작할 수 있는 컴포넌트 → 소프트웨어 재사용성↑
    • 시스템 전체가 완성되지 않았어도 개별 컴포넌트 동작 검증 가능 → 시스템 제작 난이도↓
  • 자바에서 정보 은닉을 위한 장치 → 접근 제어 메커니즘 : 접근 제한자 활용

 

2. 멤버에 부여할 수 있는 4가지 접근 제한자

접근 제한자 설명
private 멤버를 선언한 톱레벨 클래스에서만 접근할 수 있다.
package-private 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다. 접근 제한자를 명시하지 않았을 때 적용되는 패키지 접근수준이다(단, 인터페이스의 멤버는 기본적으로 public이 적용된다).
protected package-private의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근할 수 있다.
public 모든 곳에서 접근할 수 있다.
- protected와 public은 공개 API임  

 

3. public static final

  • 해당 클래스가 표현하는 추상 개념을 완성하는 데 꼭 필요한 상수라면 public static final로 공개해도 좋음
    • 관례상 이름은 알파벳 대문자와 밑줄(_)로 만든다. ex) TEST_VALUE
    • 이런 필드는 반드시 기본타입 값이나 불변객체를 참조해야 함.
      • 길이가 0이 아닌 배열은 모두 가변인 점을 주의하자!
  • 이 경우 이외에 public 클래스는 어떠한 public 필드도 가지면 안 됨.

 


💡 public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라

“필드들을 모두 private으로 바꾸고, 접근자(getter)를 제공하자”

1. 퇴보한 클래스

class Point {
    public double x;
    public double y;
}
  • 데이터 필드에 직접 접근 가능하므로 캡슐화의 이점 제공 불가
  • API를 수정하지 않고는 내부 표현 변경 불가
  • 외부에서 접근할 때 부수 작업 수행 불가

 

2. 캡슐화

// 접근자와 변경자(mutator) 메서드를 활용해 데이터를 캡슐화한다.
class Point {
    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() { return x; }
    public double getY() { return y; }

    public void setX(double x) { this.x = x; }
    public void setY(double y) { this.y = y; }
}
  • 클래스 내부 표현 언제든 변경 가능 → 유연성↑

 


💡 변경 가능성을 최소화하라

“클래스는 꼭 필요한 경우가 아니라면 불변이어야 함”

1. 불변 클래스

  • 인스턴스 내부 값을 수정할 수 없는 클래스, 객체가 파괴되는 순간까지 절대 달라지지 않음
  • ex) String, BigInteger, BigDecimal 등
  • 불변 클래스는 가변 클래스보다 설계/구현 및 사용이 쉬우며, 오류가 생길 여지도 적고 훨씬 안전함

 

2. 불변 클래스를 만들기 위한 5가지 규칙

1) 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.

2) 클래스를 확장할 수 없도록 한다.

- 하위 클래스에서 부주의하게 객체의 상태를 변하게 만드는 사태 방지.

3) 모든 필드를 final로 선언한다.

4) 모든 필드를 private으로 선언한다.

5) 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

- **접근자 메서드가 그 필드를 그대로 반환하게 하면 안 됨.**
- 생성자, 접근자, `readObject` 메서드 모두에서 **방어적 복사를 수행하자.**

 

3. 불변 객체의 장점

  • 단순함
    • 불변 객체는 생성된 시점의 상태를 파괴될 때까지 그대로 간직함.
    • 가변 객체는 임의의 복잡한 상태에 놓일 수 있음.
  • 근본적으로 스레드 안전하여 따로 동기화할 필요 없음
  • 불변 객체끼리 내부 데이터 공유 가능
  • 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 편리함
  • 그 자체로 실패 원자성을 제공함
    • 상태가 절대 변하지 않으니 불일치 상태에 빠질 가능성이 없음

 

4. 불변 객체의 단점

  • 값이 다르면 반드시 독립된 객체로 만들어야 함
    • 값의 가짓수가 많다면 큰 비용을 치러야 함

 

5. 변경을 최소화 하자

  • 클래스는 꼭 필요한 경우가 아니라면 불변이어야 함
  • 불변으로 만들 수 없는 클래스라도 변경가능한 부분을 최소한으로 줄이자
  • 다른 합당한 이유가 없다면 모든 필드는 private final이어야 함
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 함

 


💡 상속보다는 컴포지션을 사용하라

“컴포지션과 전달을 잘 활용하자”

1. 상속의 문제점

  • 상속은 캡슐화를 깨뜨린다.
    • 상위 클래스는 릴리스마다 내부 구현이 달라질 수 있으며, 그 여파로 코드 한 줄 건드리지 않은 하위 클래스가 오동작할 수 있음.
    • 다음 릴리스에서 상위 클래스에 새로 추가된 메서드가 하위클래스의 메서드와 시그처가 같고 반환타입이 다르다면? → 해당 클래스는 컴파일조차 되지 않음
  • 상속 잘못된 예시 코드
// 잘못된 예 - 상속을 잘못 사용했다!
public class InstrumentedHashSet<E> extends HashSet<E> {
    // 추가된 원소의 수
    private int addCount = 0;

    public InstrumentedHashSet() {
    }

    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }

    @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;
    }

    public static void main(String[] args) {
        InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
        s.addAll(List.of("틱", "탁탁", "펑"));
        System.out.println(s.getAddCount());
    }
}-

→ main 메서드가 실행되면 숫자 몇이 출력 될까?

  • 원소 3개가 추가되었으니 의도대로 3개가 출력될 것이라 기대하겠지만, 실제로는 6을 출력함
  • 이유는 HashSet의 addAll 메서드가 내부에서 add 메서드를 호출하기 때문(self-use; 자기사용).
  • 따라서 super.addAll메서드가 호출되었을 때, InstrumentedHashSet의 add 메서드가 3번 호출되기 때문에 6이 출력 됨.
  • 상속을 꼭 사용하고 싶다면?
    • 클래스 A를 상속하는 클래스 B를 작성하려 한다면 “B is a A”인지를 따져봐야 함.
    • 그러나 B is a A이더라도 A가 확장을 고려해 설계 되지 않았다면 문제가 될 수 있음.

 

2. 컴포지션과 전달

→ 컴포지션을 사용해 상속의 문제를 피하자

  • 컴포지션(composition) 설계란?
    • 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하는 방식.
  • 전달(forwarding)이란?
    • 새 클래스(전달 클래스)의 메서드(전달 메서드)들이 기존 클래스에 대응하는 메서드를 호출해 결과를 반환하는 방식
  • 컴포지션과 전달방식을 사용하면 새 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어나며, 심지어 기존 클래스에 새로운 메서드가 추가되더라도 전혀 영향받지 않음
  • 컴포지션과 전달 방식 예시 코드
  1. 전달 클래스
// 재사용할 수 있는 전달 클래스
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); }
    public boolean isEmpty()          { return s.isEmpty();   }
    public int size()                 { return s.size();      }
    public Iterator<E> iterator()     { return s.iterator();  }
    public boolean add(E e)           { return s.add(e);      }
    public boolean remove(Object o)   { return s.remove(o);   }
    public boolean containsAll(Collection<?> c)
                                   { return s.containsAll(c); }
    public boolean addAll(Collection<? extends E> c)
                                   { return s.addAll(c);      }
    public boolean removeAll(Collection<?> c)
                                   { return s.removeAll(c);   }
    public boolean retainAll(Collection<?> c)
                                   { return s.retainAll(c);   }
    public Object[] toArray()          { return s.toArray();  }
    public <T> T[] toArray(T[] a)      { return s.toArray(a); }
    @Override public boolean equals(Object o)
                                       { return s.equals(o);  }
    @Override public int hashCode()    { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }
}
  1. 래퍼 클래스
// 래퍼 클래스 - 상속 대신 컴포지션을 사용
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;
    }

    public static void main(String[] args) {
        InstrumentedSet<String> s = new InstrumentedSet<>(new HashSet<>());
        s.addAll(List.of("틱", "탁탁", "펑"));
        System.out.println(s.getAddCount());
    }
}

→ main 메서드가 실행되면 숫자 몇이 출력 될까?

  • 이번에는 의도대로 3이 출력됨
  • super.addAll 메서드가 호출되었을 때, InstrumentedSet의 add 메서드가 아닌 전달클래스에 존재하는 s인스턴스의 add 메서드가 호출되기 때문에 정상적으로 동작함.
  • 상속 예시에서 발생했던 문제가 해결됨.

 


💡 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라

“꼭 필요한 경우에만 상속가능하도록 설계, 나머지 경우엔 상속금지”

1. 상속용 클래스

  • 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야 함
  • 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있음. ex) java.util.AbstractList의 removeRange 메서드
  • 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어 보는 것이 ‘유일’함
  • 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 함
  • 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 됨
  • clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 됨

→ 클래스를 상속용으로 설계하려면 엄청난 노력이 들고 그 클래스에 안기는 제약도 상당함..

 

2. 상속 금지

→ 클래스를 확장해야 할 명확한 이유가 없다면 상속을 금지하자

  • 방법1) 클래스를 final로 선언
  • 방법2) 모든 생성자를 private이나 package-private으로 선언 후 public 정적 팩터리 제공

 


💡 추상 클래스보다는 인터페이스를 우선하라

“일반적으로 다중 구현용 타입으로 인터페이스가 가장 적합”

1. 인터페이스와 추상 클래스

  • 자바가 제공하는 두 가지 다중 구현 메커니즘은 인터페이스와 추상 클래스.
  • 인터페이스 vs 추상 클래스
  다중상속 가능 여부 계층 구조
추상 클래스 불가 상하 관계
인터페이스 가능 x

 

2. 인터페이스의 특징

  • 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있음
  • 인터페이스는 믹스인(mixin) 정의에 안성맞춤
    • 대상 타입의 주된 기능에 선택적 기능을 ‘혼합(mixed in)’
    • 추상 클래스는 다중 상속이 불가능하므로 믹스인 불가.
  • 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있음
    • 아래 코드의 구조를 클래스로 만들려면 가능한 조합 전부를 각각의 클래스로 정의한 고도비만 계층구조가 만들어짐
      • 속성이 n개라면 지원해야 할 조합의 수는 2^n개
      • 흔히 조합 폭발(combinatorial explosion)이라 부르는 현상.
public interface Singer {
        AudioClip sing(Song s);
}

public interface Songwriter {
        Song compose(int chartPosition);
}

public interface SingerSongwriter extends Singer, Songwriter {
        AudioClip strum();
        void actSensitive();
}
  • 인터페이스는 기능을 향상시키는 안전하고 강력한 수단
  • 인터페이스의 메서드 중 구현 방법이 명백한 것이 있다면, 그 구현을 디폴트 메서드로 제공할 수 있음
    • 디폴트 메서드를 제공할 때는 상속하려는 사람을 위한 설명을 @implSpec 자바독 태그를 붙여 문서화해야 함.

 

3. 템플릿 메서드 패턴

  • 정의 : 상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법. 변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브클래스에서 만들도록 한다. (토비의 스프링 3.1)
  • 인터페이스와 추상 골격 구현(skeletal implementation) 클래스를 함께 제공
    • 추상 골격 구현 클래스는 위 정의에서 슈퍼클래스에 해당
    • 관례상 Abstract_Interface_ 형식으로 이름을 지음
      • ex) AbstractSet, AbstractList 등
  • 단순히 골격 구현을 확장하는 것만으로 인터페이스를 구현하는 데 필요한 일이 대부분 완료
  • 골격 구현 클래스 예시
// 골격 구현 클래스
public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> {
    // 변경 가능한 엔트리는 이 메서드를 반드시 재정의해야 한다.
    @Override public V setValue(V value) {
        throw new UnsupportedOperationException();
    }

    // Map.Entry.equals의 일반 규약을 구현한다.
    @Override public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry) o;
        return Objects.equals(e.getKey(),   getKey())
                && Objects.equals(e.getValue(), getValue());
    }

    // Map.Entry.hashCode의 일반 규약을 구현한다.
    @Override public int hashCode() {
        return Objects.hashCode(getKey())
                ^ Objects.hashCode(getValue());
    }

    @Override public String toString() {
        return getKey() + "=" + getValue();
    }
}

 


💡 인터페이스는 구현하는 쪽을 생각해 설계하라

“심각하게 잘못된 인터페이스는 이를 포함한 API에 어떤 재앙을 몰고 올지 알 수 없다.”

1. 디폴트 메서드

  • 설명 : 디폴트 메서드를 선언하면, 그 인터페이스를 구현한 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰임
  • 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법
  • 새로 추가된 디폴트 메서드는 (컴파일에 성공하더라도) 기존 구현체에 런타임 오류를 일으킬 수 있음

→ 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니라면 피하자

 

2. 릴리스

  • 새로운 인터페이스라면 릴리스 전에 반드시 테스트를 거쳐야 함.
    • 수 많은 개발자가 해당 인터페이스를 다양한 방식으로 구현할 것이니, 최소 서로 다른 3가지 방식으로 구현한 후 테스트 해보아야 함.
    • 각 인터페이스의 인스턴스를 다양한 작업에 활용하는 클라이언트도 여러 개 만들어봐야 함
  • 인터페이스를 릴리스한 후라도 결함을 수정하는 게 가능하는 게 가능한 경우도 있겠지만, 절대 그 가능성에 기대서는 안 됨.

 


💡 인터페이스는 타입을 정의하는 용도로만 사용하라

“인터페이스는 타입 정의 용도, 상수 인터페이스 안티 패턴 지양”

1. 타입 정의

  • 인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 함.
  • 클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인터페이스로 무엇을 할 수 있는지를 클라이언트에 얘기해주는 것. 오직 이용도로만 사용해야 함.

2. 상수 인터페이스 안티 패턴

  • 상수 인터페이스 안티 패턴은 인터페이스를 잘못 활용한 예
  • 상수 인터페이스 안티 패턴 코드 (사용 금지)
// 상수 인터페이스 안티패턴 - 사용금지!
public interface PhysicalConstants {
    // 아보가드로 수 (1/몰)
    static final double AVOGADROS_NUMBER   = 6.022_140_857e23;

    // 볼츠만 상수 (J/K)
    static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;

    // 전자 질량 (kg)
    static final double ELECTRON_MASS      = 9.109_383_56e-31;
}
  • final이 아닌 클래스가 상수 인터페이스를 구현한다면 모든 하위 클래스의 이름공간이 그 인터페이스가 정의한 상수들로 오염되어 버림
  • 클라이언트 코드가 내부 구현에 해당하는 이 상수들에 종속되게 함. 그래서 다음 릴리스에서 이 상수들을 더는 쓰지 않게 되더라도 바이너리 호환성을 위해 여전히 상수 인터페이스를 구현하고 있어야 함.

3. 올바른 상수 공개 방법

  1. 특정 클래스나 인터페이스와 강하게 연관된 상수일 경우 : 그 클래스나 인터페이스 자체에 추가
    • ex) Integer의 MIN_VALUE 상수
  2. 열거 타입으로 나타내기 적합한 경우 : 열거 타입으로 공개
  3. 1, 2가 아닌 경우 : 인스턴스화할 수 없는 유틸리티 클래스에 담아서 공개
// 상수 유틸리티 클래스 
public class PhysicalConstants {
  private PhysicalConstants() { }  // 인스턴스화 방지

  // 아보가드로 수 (1/몰)
  public static final double AVOGADROS_NUMBER = 6.022_140_857e23;

  // 볼츠만 상수 (J/K)
  public static final double BOLTZMANN_CONST  = 1.380_648_52e-23;

  // 전자 질량 (kg)
  public static final double ELECTRON_MASS    = 9.109_383_56e-31;
}

// 상수 사용시
PhysicalConstants.AVOGADROS_NUMBER;

 


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

“태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류”

1. 태그 달린 클래스

  • 두 가지 이상의 의미를 표현할 수 있으며, 그중 현재 표현하는 의미를 태그 값으로 알려주는 클래스
  • 태그 달린 클래스 예시 코드
// 태그 달린 클래스 - 클래스 계층구조보다 훨씬 나쁘다!
class Figure {
    enum Shape { RECTANGLE, CIRCLE };

    // 태그 필드 - 현재 모양을 나타낸다.
    final Shape shape;

    // 다음 필드들은 모양이 사각형(RECTANGLE)일 때만 쓰인다.
    double length;
    double width;

    // 다음 필드는 모양이 원(CIRCLE)일 때만 쓰인다.
    double radius;

    // 원용 생성자
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    // 사각형용 생성자
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    double area() {
        switch(shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError(shape);
        }
    }
}
  • 태그 달린 클래스의 문제점
    • 열거 타입 선언, 태그 필드, switch문 등 쓸데 없는 코드가 많음
    • 가독성↓
    • 다른 의미를 위한 코드들 때문에 메모리 사용↑
    • 인스턴스 타입만으로는 현재 나타내는 의미를 파악하기 어려움

 

2. 서브타이핑

→ 태그 달린 클래스는 계층구조로 바꿔서 사용하자

  • 클래스 계층구조는 태그 달린 클래스의 단점을 모두 날려버림
  • 태그 달린 클래스를 계층구조로 변환하는 방법
    1. 계층구조의 루트(root)가 될 추상 클래스 정의
    2. 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상메서드로 정의
    3. 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가
    4. 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의
  • 태그 달린 클래스를 계층구조로 변환한 코드 예시
// 태그 달린 클래스를 클래스 계층구조로 변환
abstract class Figure {
    abstract double area();
}

class Rectangle extends Figure {
    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width  = width;
    }
    @Override double area() { return length * width; }
}

class Circle extends Figure {
    final double radius;

    Circle(double radius) { this.radius = radius; }

    @Override double area() { return Math.PI * (radius * radius); }
}

 


💡 멤버 클래스는 되도록 static으로 만들라

“네 가지 중첩 클래스의 쓰임 설명”

1. 정적 멤버 클래스

  • 흔히 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스 용도로 사용
  • ex) Calculator.Operation 열거 타입
  • 바깥 인스턴스와 독립적으로 존재.

 

2. 비정적 멤버 클래스

  • 어댑터를 정의할 때 자주 사용. 즉, 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용.
  • ex) 컬렉션 인터페이스 구현들의 반복자(iterator)
public class MySet<E> extends AbstractSet<E> {
        ... // 생략
        @Override public Iterator<E> iterator() {
                return new MyIterator();
        }

        private class MyIterator implements Iterator<E> {
                ...
        }
}
  • 바깥 인스턴스와 암묵적으로 연결. this를 사용해 바깥 인스턴스 참조 가능.
  • 바깥 인스턴스 없이 생성 불가

 

3. 익명 클래스

  • 정적 팩터리 메서드를 구현할 때 주로 쓰임
  • 쓰이는 시점에 선언과 동시에 인스턴스 생성
  • 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스 참조 가능.
  • 정적 문맥에서라도 상수 변수 이외의 정적 멤버는 가질 수 없음.
  • 제약이 많음
    • 선언한 지점에서만 인스턴스 생성 가능
    • instanceof 검사나 클래스의 이름이 필요한 작업 수행 불가
    • 익명 클래스를 사용하는 클라이언트는 그 익명 클래스가 상위 타입에서 상속한 멤버 외에는 호출 불가
    • 표현식 중간에 등장하므로 (10줄 이하로) 짧지 않으면 가독성↓

 

4. 지역 클래스

  • 네 가지 중첩 클래스 중 가장 드물게 사용 됨
  • 지역변수를 선언할 수 있는 곳이면 실질적으로 어디서든 선언할 수 있고, 유효범위도 지역변수와 같음
  • 비정적 문맥에서 사용될 때만 바깥 인스턴스 참조 가능.
  • 정적 멤버는 가질 수 없음.
  • 가독성을 위해 짧게 작성해야 함.

 


💡 톱레벨 클래스는 한 파일에 하나만 담으라

“소스 파일 하나에는 반드시 톱레벨 클래스(혹은 톱레벨 인터페이스)를 하나만 담자”

1. 잘못된 예시

  • Utensil.java
// 두 클래스가 한 파일(Utensil.java)에 정의되었다. - 따라 하지 말 것!
class Utensil {
    static final String NAME = "pan";
}

class Dessert {
    static final String NAME = "cake";
}
  • Dessert.java
// 두 클래스가 한 파일(Dessert.java)에 정의되었다. - 따라 하지 말 것!
class Utensil {
    static final String NAME = "pot";
}

class Dessert {
    static final String NAME = "pie";
}
  • Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }
}

 

  1. javac Main.java Dessert.java 로 컴파일 할 경우
    1. 컴파일러는 가장 먼저 Main.java를 컴파일
    2. main 메서드 안에서 Utensil 참조를 만나면 Utensil.java 파일을 살펴 Utensil과 Dessert를 모두 정의
    3. 두 번째 명령줄 인수로 넘어온 Dessert.java를 처리하려 할 때, 같은 클래스의 정의가 있음을 알게 되고 컴파일 오류를 뱉음(클래스 중복 정의)
  2. javac Main.java 또는 javac Main.Utensil.java 로 컴파일 할 경우
    1. 컴파일러는 가장 먼저 Main.java를 컴파일
    2. main 메서드 안에서 Utensil 참조를 만나면 Utensil.java 파일을 살펴 Utensil과 Dessert를 모두 정의
    3. 정상 작동 : pancake 출력
  3. javac Dessert.java Main.java 로 컴파일 할 경우
    1. 컴파일러는 가장 먼저 Dessert.java을 살펴 Utensil과 Dessert를 모두 정의
    2. 두 번째 명령줄 인수로 넘어온 Main.java 컴파일
    3. 정상 작동 : potpie 출력

→ 2, 3을 보면 컴파일 순서에 따라 출력결과가 달라짐을 확인할 수 있음.

 

2. 해결책

  • 단순히 톱레벨 클래스들(Utensil과 Dessert)을 서로 다른 소스 파일로 분리하면 됨
  • 굳이 여러 톱레벨 클래스를 한 파일에 담고 싶다면 아래 처럼 정적 멤버 클래스를 사용하는 방법을 고민해볼 수 있음.
// 톱레벨 클래스들을 정적 멤버 클래스로 바꿔본 모습
public class Test {
    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }

    private static class Utensil {
        static final String NAME = "pan";
    }

    private static class Dessert {
        static final String NAME = "cake";
    }
}
728x90
반응형
저작자표시 (새창열림)

'📚 Reading > Tech' 카테고리의 다른 글

이펙티브 자바 - 7장. 람다와 스트림  (0) 2023.01.24
이펙티브 자바 - 6장. 열거 타입과 애너테이션  (0) 2023.01.24
이펙티브 자바 - 5장. 제네릭  (0) 2023.01.24
이펙티브 자바 - 3장. 모든 객체의 공통 메서드  (0) 2023.01.24
이펙티브 자바 - 2장. 객체 생성과 파괴  (0) 2023.01.24
    '📚 Reading/Tech' 카테고리의 다른 글
    • 이펙티브 자바 - 6장. 열거 타입과 애너테이션
    • 이펙티브 자바 - 5장. 제네릭
    • 이펙티브 자바 - 3장. 모든 객체의 공통 메서드
    • 이펙티브 자바 - 2장. 객체 생성과 파괴
    개발자 피터
    개발자 피터
    Backend Engineer 🔥

    티스토리툴바