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이 아닌 배열은 모두 가변인 점을 주의하자!
- 관례상 이름은 알파벳 대문자와 밑줄(_)로 만든다. ex)
- 이 경우 이외에
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)이란?
- 새 클래스(전달 클래스)의 메서드(전달 메서드)들이 기존 클래스에 대응하는 메서드를 호출해 결과를 반환하는 방식
- 컴포지션과 전달방식을 사용하면 새 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어나며, 심지어 기존 클래스에 새로운 메서드가 추가되더라도 전혀 영향받지 않음
- 컴포지션과 전달 방식 예시 코드
- 전달 클래스
// 재사용할 수 있는 전달 클래스
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(); }
}
- 래퍼 클래스
// 래퍼 클래스 - 상속 대신 컴포지션을 사용
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
등
- ex)
- 단순히 골격 구현을 확장하는 것만으로 인터페이스를 구현하는 데 필요한 일이 대부분 완료
- 골격 구현 클래스 예시
// 골격 구현 클래스
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. 올바른 상수 공개 방법
- 특정 클래스나 인터페이스와 강하게 연관된 상수일 경우 : 그 클래스나 인터페이스 자체에 추가
- ex)
Integer
의MIN_VALUE
상수
- ex)
- 열거 타입으로 나타내기 적합한 경우 : 열거 타입으로 공개
- 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. 서브타이핑
→ 태그 달린 클래스는 계층구조로 바꿔서 사용하자
- 클래스 계층구조는 태그 달린 클래스의 단점을 모두 날려버림
- 태그 달린 클래스를 계층구조로 변환하는 방법
- 계층구조의 루트(root)가 될 추상 클래스 정의
- 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상메서드로 정의
- 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가
- 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의
- 태그 달린 클래스를 계층구조로 변환한 코드 예시
// 태그 달린 클래스를 클래스 계층구조로 변환
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);
}
}
javac Main.java Dessert.java
로 컴파일 할 경우- 컴파일러는 가장 먼저
Main.java
를 컴파일 main
메서드 안에서Utensil
참조를 만나면Utensil.java
파일을 살펴Utensil
과Dessert
를 모두 정의- 두 번째 명령줄 인수로 넘어온
Dessert.java
를 처리하려 할 때, 같은 클래스의 정의가 있음을 알게 되고 컴파일 오류를 뱉음(클래스 중복 정의)
- 컴파일러는 가장 먼저
javac Main.java
또는javac Main.Utensil.java
로 컴파일 할 경우- 컴파일러는 가장 먼저
Main.java
를 컴파일 main
메서드 안에서Utensil
참조를 만나면Utensil.java
파일을 살펴Utensil
과Dessert
를 모두 정의- 정상 작동 : pancake 출력
- 컴파일러는 가장 먼저
javac Dessert.java Main.java
로 컴파일 할 경우- 컴파일러는 가장 먼저
Dessert.java
을 살펴Utensil
과Dessert
를 모두 정의 - 두 번째 명령줄 인수로 넘어온
Main.java
컴파일 - 정상 작동 : 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";
}
}
'📚 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 |