[이펙티브 자바] 03. 클래스와 인터페이스
아이템 15. 클래스와 멤버의 접근 권한을 최소화하라
어설프게 설계된 컴포넌트와 잘 설계된 컴포넌트의 가장 큰 차이는 바로 클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼느냐다. 정보 은닉, 혹은 캡슐화라고 하는 이 개념은 소프트웨어 설계의 근간이 되는 원리다.
15.1 캡슐화의 장점
캡슐화의 장점은 정말 많은데, 그중 대부분은 시스템을 구성하는 컴포넌트들을 서로 독립시켜서 개발, 테스트, 최저과, 적용, 분석, 수정을 개별적으로 할 수 있게 해주는 것과 연관되어 있다.
- 시스템 개발 속도를 높인다. 여러 컴포넌트를 병렬로 개발할 수 있기 때문이다.
- 시스템 관리 비용을 낮춘다. 각 컴포넌트를 더 빨리 파악하여 디버깅할 수 있고, 다른 컴포넌트로 교체하는 부담도 적기 때문이다.
- 캡슐화 자체가 성능을 높여주지는 않지만, 성능 최적화에 도움을 준다. 왼성된 시스템을 프로파일링해 최적화할 컴포넌트를 정한 다음 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화 할 수 있기 때문이다.
- 소프트웨어 재사용성을 높인다. 외부에 거의 의존하지 않고 독자적으로 동작할 수 있는 컴포넌트라면 그 컴포넌트와 함께 개발되지 않은 낯선 환경에서도 유용하게 쓰일 가능성이 크기 때문이다.
- 큰 시스템을 제작하는 난이도를 낮춰준다. 시스템 전체가 아직 완성되지 않은 상태에서도 개별 컴포넌트의 동작을 검증할 수 있기 때문이다.
자바는 캡슐화를 위한 다양한 장치를 제공한다. 그중 접근 제어 메커니즘은 클래스, 인터페이스, 멤버의 접근 허용 범위를 명시한다. 이 접근 제한자를 제대로 활용하는 것이 정보 은닉의 핵심이다. 모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다.
(가장 바깥이라는 의미의) 톱레벨 클래스와 인터페이스에 부여할 수 있는 접근 수준은 package-private과 public 두 가지다. public으로 선언하며 공개 API가 되며, package-private으로 선언하면 해당 패키지 안에서만 이용할 수 있다. 패키지 외부에서 쓸 이유가 없다면 package-private으로 선언하자. 그러면 이들은 API가 아닌 내부 구현이 되어 클라이언트에 아무런 피해 없이 다음 릴리즈에서 수정, 교체, 제거를 할 수 있다. 반면 public으로 선언한다면 API가 되므로 하위 호환을 위해 영원히 관리해줘야만 한다.
클래스의 공개 API를 세심히 설계한 후, 그 외의 모든 멤버는 private으로 만들자. 그런 다음 오직 같은 패키지의 다른 클래스가 접근해야 하는 멤버에 한하여 package-private으로 풀어주자. 권한을 풀어주는 일을 자주 하게 된다면 시스템에서 컴포넌트를 더 분해야하는 것은 아닌지 고민해보자.
public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다. 필드가 가변 객체를 참조하거나, final이 아닌 인스턴스 필드를 public으로 선언하면 그 필드에 담을 수 있는 값을 제한할 수 없어 그 필드와 관련된 모든 것을 불변식으로 보장할 수 없게 된다. 심지어 필드가 final에 불변일지라도 문제는 여전히 남는다. 내부 구현을 바꾸고 싶어도 그 public 필드를 없애는 방식으로는 리팩터링 할 수 없게 된다.
기본 타입 값이나 불변 객체를 참조하는 상수는 public static final 필드로 공개해도 좋다. 그러나 가변 객체를 참조한다면 final이 아닌 필드에 적용되는 모든 불이익이 그대로 적용된다. 다른 객체를 참조하지는 못하지만 참조된 객체가 수정될 수 있다.
이를 해결하는 방법은 두가지가 있다.
가변 객체를 private으로 만들고 public 불변 리스트를 추가하는 것
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
가변 객체를 private으로 만들고 그 복사본을 반환하는 public 메서드를 추가하는 방법
private static final Thing[] PRICATE_VALUES = { ... };
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}
어느 반환타입이 편할지, 성능은 어느쪽이 나을지를 고려해 정하면 된다.
아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
이따금 인스턴스 필드들을 모아놓는 일 외에는 아무 목적도 없는 퇴보한 클래스를 작성할 때가 있다.
class Point {
public double x;
public double y;
}
이런 클래스는 데이터 필드에 직접 접근할 수 있어 캡슐화의 이점을 제공하지 못하고, API를 수정하지 않고서는 내부 표현을 바꿀 수 없고, 불변식도 보장할 수 없다. 위의 코드를 아래와 같이 캡슐화 할 수 있다.
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;}
}
public 클래스는 접근자를 제공함으로써 클래스 내부 표현 방식을 언제든 바꿀 수 있는 유연성을 얻을 수 있다.
아이템 17. 변경 가능성을 최소화하라
불변 클래스란 간단히 말해 그 인스턴스의 내부 값을 수정할 수 없는 클래스다. 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전한다.
17.1 불변 클래스를 만들기 위한 다섯 가지 규칙
- 객체의 상태를 변경하는 메서드를 제공하지 않는다.
- 클래스를 확장할 수 없도록 한다.
- 모든 필드를 final로 선언한다.
- 모든 필드를 private으로 선언한다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
17.2 불변 복소수 클래스
public final class Complex {
private final double re;
private final double im;
public Complex(double re, double im) {
this.re = re;
this.rm = im;
}
public double realPart() { return re; }
public double imaginaryPart() { return im; }
public Complex plus(Complex c) {
return new Complex(re + c.re, im + c.im);
}
public Complex minus(Complex c) {
return new Complex(re - c.re, im - c.im);
}
public Complex times(Complex c) {
return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
}
public Complex dividedBy(Complex c) {
double tmp = c.re * c.re + c.im * c.im;
return new Complex((re * c.re + im * c.im) / tmp, (im * c.re - re * c.im) / tmp));
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Complex))
return false;
Complex c = (Complex) o;
return Double.compare(c.re, re) == 0
&& Double.compare(c.im, im) == 0;
}
@Override
public int hashCode() {
return 31 * Double.hashCode(re) + Double.hashCode(im);
}
@Override
public String toString() {
return "(" + re + " + " + im + "i)";
}
}
이 클래스의 사칙연산 메서드는 인스턴스 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환한다. 이처럼 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체를 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라 한다. 이와 달리, 절차적 혹은 명령형 프로그래밍에서는 메서드에서 피연산자인 자신을 수정해 자신의 상태가 변하게 된다.
17.3 불변 클래스의 장단점
- 불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요가 없다. 불변 객체에 대해서는 그 어떤 스레드도 다른 스레드에 영향을 줄 수 없으니 불변 객체는 안심하고 공유할 수 있다. 따라서 불변 클래스라면 한번 만든 인스턴스를 최대한 재활용하기를 권한다. 가장 쉬운 재활용 방법은 자주 쓰이는 값들을 상수로 제공하는 것이다.
public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);
- 불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 인스턴스를 중복 생성하지 않게 해주는 정적 팩터리를 제공할 수 있다. 이는 여러 클라이언트가 하나의 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.
- 불변 객체를 자유롭게 공유할 수 있다는 점은 방어적 복사도 필요없다는 의미다. 그러므로 불변 클래스는 clone 메서드나 복사 생성자를 제공하지 않는게 좋다.
- 불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
- 불변 객체는 그 자체로 실패 원자성을 제공한다.
- 값이 다르면 반드시 독립된 객체로 만들어야 한다.
17.4 불변 클래스를 상속하지 못하게 하는 방법
클래스가 자신을 상속하지 못하게 하는 가장 쉬운 방법은 final 클래스로 선언하는 것이지만, 더 유연한 방법이 있다. 모든 생성자를 private 혹은 package-private으로 만들고, public 정적 팩터리를 제공하는 방법이다.
public class Complex {
private final double re;
private final double im;
private Complex(double re, double im) {
this.re = re;
this.im = im;
}
public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}
... // 나머지 코드는 생략
}
이 방식이 최선일 때가 많다. 다른 패키지에서는 이 클래스를 확장하는 게 불가능하지만 바깥에서는 볼 수 없는 package-private 구현 클래스를 원하는 만큼 만들어 활용할 수 있으니 훨씬 유연하다.
아이템 18. 상속보다는 컴포지션을 사용하라
상속은 코드를 재사용하는 강력한 수단이지만, 다른 패키지의 구체 클래스를 상속하는 일은 매우 위험하다. 이 글에서의 '상속'은 클래스가 다른 클래스를 확장하는 구현 상속을 말한다.
메서드 호출과 달리 상속은 캡슐화를 깨뜨린다. 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다. 상위 클래스의 내부 구현이 변경되면 그 여파로 변경되지 않은 하위 클래스가 오동작 할 수있다.
그러므로 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자. 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 컴포지션이라고 한다. 새로운 클래스는 기존 클래스의 내부 구현방식의 영향에서 벗어나며, 기존 클래스에 새로운 메서드가 추가되더라도 전혀 영향받지 않는다.
상속은 반드시 하위 클래스가 상위 클래스의 진짜 하위 타입인 상황에서만 쓰여야 한다. 클래스 B가 클래스 A와 is-a 관계일 때만 클래스 A를 상속해야 한다. 그렇지 않다면 컴포지션을 사용하자.
아이템 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다. 클래스를 안전하게 상속할 수 있도록 하려면 내부 구현 방식을 설명해야 한다.
효율적인 하위 클래스를 큰 어려움 없이 만들 수 있게 하려면 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅을 잘 선별하여 protected 메서드 형태로 공개해야할 수도 있다. 상속용 클래스를 설계할 때 어떤 메서드를 protected로 노출해야할지는 실제 하위 클래스를 만들어 시험해보는 것이 최선이다. protected 메서드 하나하나가 내부 구현에 해당하므로 그 수는 가능한 적어야한다.
상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 '유일'하다. 꼭 필요한 protected 멤버를 놓쳤다면 하위 클래스를 작성할 때 빈자리가 확연히 드러나고, 반대로 하위 클래스를 여러개 만들때까지 사용되지 않았다면 해당 멤버는 사실 private이었어야 할 가능성이 크다. 또한 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.
상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다. 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다. 이 때 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않을것이다.
public class Super {
// 잘못된 예 - 생성자가 재정의 가능 메서드를 호출한다.
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
public final class Sub extends Super {
// 초기화 되지 않은 final 필드. 생성자에서 초기화한다.
private final Instant instant;
Sub() {
instant = Instant.now();
}
@Override
public void oveerideMe() {
System.out.println(instant);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
19.1 상속을 금지하는 두가지 방법
- 클래스를 final로 선언하자.
- 모든 생성자를 private으로 선언하고 public 정적 팩터리를 만들어주자
19.2 꼭 클래스를 상속해야겠다면?
클래스 내부에서는 재정의 가능 메서드를 사용하지 않게 만들고, 이를 문서화 하자. 이렇게 하면 상속해도 그렇게 위험하지 않은 클래스를 만들 수 있다.
아이템 20. 추상 클래스보다는 인터페이스를 우선하라
자바가 제공하는 다중 구현 메커니즘은 인터페이스와 추상 클래스 두가지이다. 추상클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야한다. 반면 인터페이스가 선언한 메서드를 모두 정의하고 그 일반 규약을 잘 지킨 클래스라면 다른 어떤 클래스를 상속했든 같은 타입으로 취급된다.
기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있다. 반면 기존 클래스 위에 새로운 추상클래스를 끼워넣기는 어려운게 일반적이다. 두 클래스가 같은 추상클래스를 확장하길 바란다면 그 추상 클래스는 계층구조상 두 클래스의 공통 조상이어야 한다.
인터페이스는 믹스인(mixin) 정의에 안성맞춤이다. 믹스인이란 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래의 '주된 타입' 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다. 이처럼 대상 타입의 주된 기능에 선택적 기능을 '혼합'한다고 해서 믹스인이라 부른다.
인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다. 타입을 계층적으로 정의하면 수많은 개념을 구조적으로 잘 표현할 수 있지만, 현실에는 계층을 엄격히 구분하기 어려운 개념도 있다.
예를 들어 가수 타입과 작곡가 타입이 있다고 해보자. 우리 주변에는 작곡도 하는 가수도 있으므로, 타입을 인터페이스로 정의하면 가수 타입과 작곡가 타입을 모두 구현해도 문제가 되지 않는다.
아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계하라
자바 8 이후 디폴트 메서드가 소개되었지만, 생각할 수 있는 모든 상황에서 불변식을 해치치 않는 디폴트 메서드를 작성하는 것은 어렵다. 디폴트 메서드는 기존 구현체에 런타임 오류를 일으킬 수 있다.
기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 피해야 한다. 반면, 새로운 인터페이스를 만드는 경우라면 표준적인 메서드 구현을 제공하는데 아주 유용한 수단이며, 그 인터페이스를 더 쉽게 구현해 활용할 수 있게끔 해준다.
아이템 22. 인터페이스는 타입을 정의하는 용도로만 사용하라
클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트한테 이야기해주는 것이다. 인터페이스는 오직 이 용도로만 사용해야 한다.
이 지침에 맞지 않는 예로 소위 상수 인터페이스라는 것이 있다. 상수 인터페이스란 메서드 없이, 상수를 뜻하는 static final 필드로만 가득 찬 인터페이스를 말한다.
public interface PhysicalConstants {
static final double AVOGADROS_NUMBER = 6.234424;
static final double BOLTZMANN_CONSTANT = 1.234234;
}
상수 인터페이스 안티패턴은 인터페이스를 잘못 사용한 예다. 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아니라 내부 구현에 해당한다. 따라서 상수 인터페이스를 구현하는 것은 이 내부 구현을 클래스의 API로 노출하는 행위이다.
아이템 23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라
두 가지 이상의 의미를 표현할 수 있으며, 그 중 현재 표현하는 의미를 태그 값으로 알려주는 클래스를 본 적이 있을것이다. 아래는 원과 사각형을 표현할 수 있는 클래스다.
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문 등 쓸데없는 코드가 많다. 여러 구현이 한 클래스에 혼합돼 있어서 가독성도 나쁘다. 다른 의미를 위한 코드도 언제나 함께하니 메모리도 많이 사용한다. 마지막으로, 인스턴스의 타입만으로는 현재 나타내는 의미를 알 길이 전혀 없다. 한마디로, 태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다.
다행히 자바와 같은 객체 지향 언어는 타입 하나로 다양한 의미의 객체를 표현하는 훨씬 나은 수단을 제공한다. 바로 클래스 계층구조를 활용하는 서브타이핑이다.
23.1 태그 달린 클래스를 클래스 계층구조로 바꾸는 법
- 루트가 될 추상 클래스를 정의하고, 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언한다.
- 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가한다.
- 모든 하위 클래스에서 공통으로 사용하는 데이터 필드들도 전부 루트 클래스로 올린다.
- Figure 클래스에서는 태그 값에 상관없는 메서드가 하나도 없고, 모든 하위 클래스에서 사용하는 공통 데이터 필드도 없으므로 루트 클래스에는 추상 메서드인 area 하나만 남게 된다.
- 다음으로, 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의한다.
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
Circle(double radius) { this.radius = radius; }
@Override double area() { return Math.PI * (radius * radius); }
}
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; }
}
코드는 간결하고 명확해졌으며 각 의미를 독립된 클래스에 담아 관련 없던 데이터 필드를 모두 제거했다. 살아남은 필드들은 모두 final이다. 각 클래스의 생성자가 모든 필드를 남김없이 초기화하고 추상 메서드를 모두 구현했는지 컴파일러가 확인해준다.
아이템 24. 멤버 클래스는 되도록 static으로 만들라
중첩 클래스(nested class)란 다른 클래스 안에 정의된 클래스를 말한다. 중첩 클래스는 자신을 감싼 바깥 클래스안에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다. 중첩 클래스의 종류는 정적 멤버 클래스, (비정적) 멤버 클래스, 익명 클래스, 지역 클래스 이렇게 네가지로, 이 중 첫번째를 제외한 나머지는 내부 클래스(inner class)에 해당한다
24.1 정적 멤버 클래스
정적 멤버 클래스는 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 것만 제외하고는 일반 클래스와 똑같다. 정적 멤버 클래스는 다른 정적 멤버와 똑같은 접근 규칙을 적용받는다. 예컨대 private으로 선언하면 바깥 클래스에서만 접근할 수 있다. 정적 멤버 클래스는 흔히 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 쓰인다.
private 정적 멤버 클래스는 흔히 바깥 클래스가 표현하는 객체의 한 부분을 나타낼 때 쓴다.
24.2 비정적 멤버 클래스
비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다. 그래서 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다. 따라서 개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다. 비정적 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없기 때문이다.
비정적 멤버 클래스는 어댑터를 정의할 때 자주 쓰인다. 즉, 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용하는 것이다.
멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.
24.3 익명 클래스
멤버와 달리, 쓰이는 시점에서 선언과 동시에 인스턴스가 만들어진다. 코드의 어디서든 만들 수 있고 오직 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있다. 상수 표현을 위해 초기화된 final 기본 타입과 문자열 필드만 가질 수 있다.
24.4 지역 클래스
네가지 중첩 클래스 중 가장 드물게 사용된다. 지역변수를 선언할 수 있는 곳이면 실질적으로 어디서든 선언할 수 있고 유효 범위도 지역변수와 같다. 멤버 클래스처럼 이름이 있고 반복해서 사용할 수 있고, 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있으며 정적 멤버는 가질 수 없다.
아이템 25. 톱레벨 클래스는 한 파일에 하나만 담으라
소스 파일 하나에 톱레벨 클래스를 여러개 선언한다면, 그중 어느 것을 사용할 지는 어느 소스 파일을 먼저 컴파일하냐에 따라 달라진다. 그러므로 톱레벨 클래스를 서로 다른 소스 파일로 분리하자.
'스터디 > 이펙티브 자바' 카테고리의 다른 글
[이펙티브 자바] 06. 람다와 스트림 (0) | 2022.09.14 |
---|---|
[이펙티브 자바] 05. 열거 타입과 애너테이션 (0) | 2022.09.01 |
[이펙티브 자바] 04. 제네릭 (1) | 2022.08.24 |
[이펙티브 자바] 02. 모든 객체의 공통 메서드 (0) | 2022.08.10 |
[이펙티브 자바] 01. 객체 생성과 파괴 (0) | 2022.08.02 |
댓글