[CS] 이펙티브자바 독서 #1 - 아이템15 ~ 17

2025. 10. 11. 22:16·언어공부/Java | Kotlin

원래 지난주에 BE 파트스터디를 진행하면서 이펙티브 자바를 읽었는데 그 때는 시간이 좀 없어서 블로그 정리를 못했다. 이번에는 좀 해볼생각

 

그.. 어차피 지난주에 읽을 내용도 올릴거고 이전에 읽지 않은 내용도 올릴건데 넘버링을 어떻게 해야할지 모르겠네 스타워즈식 넘버링을 해볼까


아이템15. 클래스와 멤버의 접근권한을 최소화하라

어설프게 설계된 컴포넌트라니.. 첫줄부터 뜨끔하네;;

 

암튼 잘 설계된 컴포넌트가 되려면 클래스 내부의 데이터와 내부구현정보를 외부로부터 숨겨야한다. 오직 외부 API만을 통해 다른 컴포넌트와 소통해야하며, 내부 동작 방식은 모른다. 이것이 흔히 말하는 캡슐화라고 한다.

 

이러한 캡슐화의 방법 중 하나가 바로 접근제어자다. 접근제어자를 최대한 활용하여 모든 클래스와 멤버의 접근권한을 좁혀야한다.

여기서 톱레벨클래스라는 것이 등장하게되는데, 톱레벨클래스란 소스파일에서 가장 바깥쪽에 존재하는 클래스다. 중첩클래스나 내부클래스가 아닌 클래스, 즉 일반적으로 사용하는 클래스를 톱레벨클래스라고 한다

 

톱레벨클래스와 인터페이스에 대해서는 공개API로 설정되는 public과 패키지 내부에서만 사용하는 package-private로 접근제어자를 설정할 수 있다. package-private 접근제어자는 default 접근제어자와 같다. 이렇게 굳이 외부로 드러낼 필요가 없는 클래스나 인터페이스들을 package-private로 설정하면 클라이언트에 아무 영향없이 수정, 교체가 가능하다.

 

또한 한 클래스에서만 활용하는 package-private 톱레벨클래스, 인터페이스는 private static으로 중첩시킬 수 있다. '중첩'이라는 뜻은 내부클래스로 선언하라는 뜻이다. 그니까 package-private 톱레벨클래스가 있는데 그 클래스를 호출하는 클래스가 단 하나 밖에 없을 때, 호출하는 클래스의 내부클래스로 옮긴 후 private static으로 접근제어자를 설정해 선언하라는 것이다. 이렇게되면 내부클래스의 외부클래스, 즉 유일하게 그 클래스를 호출하는 클래스에서만 접근이 가능하다.

 

물론 이렇게 설정하는 것도 중요하지만, 제일 중요한 것은 굳이 외부로 드러날 필요가 없는 클래스의 접근수준을 package-private로 설정하라는 거. 그리고 이렇게 설정을 끝낸 후에 모든 멤버의 접근제어자를 private로 설정하고, 외부클래스에서 호출해야하는 멤버에 대해서만 package-private로 설정해야한다.

 

또한 코드를 테스트할 목적으로 접근범위를 넓히는 것도 지양해야한다. private 멤버를 package-private까지 넓히는 것까지는 좋으나 그 이상은 불가. 테스트를 위해 클래스, 멤버 등을 공개해서는 안된다. 어차피 테스트코드가 같은 패키지에 있으면 package-private로 선언해도 접근이 가능하여 굳이 그럴 이유도 없다.

[CS] 자바의 정석 독서 #12 - 추상클래스와 인터페이스

다만 인터페이스는 예외로 두어야한다. 인터페이스의 모든 메서드는 public abstract로 선언되므로 인터페이스의 구현체는 반드시 public으로 선언되어야한다. 이는 하위클래스의 접근제어자는 상위클래스의 접근제어자보다 접근수준을 좁게 설정할 수 없기 때문이다. (리스코프 치환 원칙, 어길 시 컴파일에러)


public 클래스의 인스턴스 필드는 되도록 public이어서는 안된다. 불변객체를 참조하거나 final로 선언되지 않은 인스턴스 필드의 멤버를 public으로 선언하면 그 객체는 언제든 수정될 수 있다. 따라서 다중스레드 환경에서 그 변수와 코드, 기능이 올바르게 작동할지 장담할 수 없다. (스레드 안전하지 않음)

 

정적멤버도 이러한 문제를 벗어날 수 없으나, 코드 실행 시에 필수적인 상수라면 public static final로 선언할 수 있으며, 이 대의 값은 기본형 값이거나 불변 객체를 참조해야한다. 만약 가변객체를 참조하면 다른 객체를 참조할 수는 없지만 참조한 객체의 내부 상태는 수정될 수 있다.

 

배열 역시 주의해야한다. 배열도 많은 상황에서 필요하며, 외부에서 배열에 접근해야할 일도 있다. 배열은 public static final로 선언되어도 배열의 길이가 0이 아닌 이상 수정이 가능하다.

 

public class Stack {
    public static final int[] ints = new int[10];
    public static void main(String[] args) {
        for (int i = 0; i < ints.length; i++) {
            ints[i] = i;
        }

        ttt ttt = new ttt();
        ttt.medify();
    }
}

class ttt {
    Stack stack = new Stack();
    public void medify() {
        int hi = stack.ints[1];
        System.out.println("원래값 : " + hi);

        stack.ints[1] = 3;
        System.out.println("변경된값 : " + stack.ints[1]);
    }
}

이렇게 Stack 클래스의 배열 ints를 public static final로 선언했으나 ttt 클래스에서 이에 접근해 값을 수정해도 아무런 문제가 발생하지 않는다.

 

이럴수가!

 

이렇게된다면 배열은 다른 방식을 통해 선언해야한다. 기본적인 방법은 배열을 private로 선언하고 public 불변리스트를 추가해 거기에 접근할 수 있도록하는 것이다.

 

public class Stack {
    private static final Integer[] ints = new Integer[10];
    public static final List<Integer> intList = Collections.unmodifiableList(Arrays.asList(ints));
    public static void main(String[] args) {
        for (int i = 0; i < ints.length; i++) {
            ints[i] = i;
        }

        ttt ttt = new ttt();
        ttt.medify();
    }
}

class ttt {
    Stack stack = new Stack();
    public void medify() {
        int hi = stack.intList.get(1);
        System.out.println("원래값 : " + hi);

        stack.intList.add(2);
        System.out.println("변경된값 : " + stack.intList.get(1));
    }
}

불변리스트는 List<>를 사용해야하는데 거기에는 int가 들어갈 수 없어서 약간 코드를 수정했다.

암튼 이렇게 코드를 수정하면 public 리스트에는 접근이 불가능하고, 진짜 배열의 본체는 private로 수정되어 접근이 불가능하여 불변리스트에 접근하게끔 만들었다.

 

public class Stack {
    private static final Integer[] ints = new Integer[12];
    public static final List<Integer> intList = Collections.unmodifiableList(Arrays.asList(ints));
    public static void main(String[] args) {
        for (int i = 0; i < ints.length; i++) {
            ints[i] = i;
        }

        System.out.println(intList.toString());
        ints[11] = 100;
        System.out.println(intList.toString());

        ttt ttt = new ttt();
        ttt.medify();
    }
}

intList도 수정은 곧바로 이루어진다.

 

두 번째 방법은 아니면 .clone() 메서드를 통해 private 배열을 반환하는 public 메서드를 추가하여도 된다. (방어적복사)


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

아이템15에서 연장되는 내용 같은데,

 

class Test {
	public double idx;
    public double idd;
}

이렇게 인스턴스 필드를 모아놓는 일 이외에는 아무 목적도 없는 클래스를 작성할 때가 있다. '퇴보한 클래스'라는 굉장히 강한 어조를 사용하고 있는데, API를 수정하지 않고는 내부표현을 바꿀 수 없다. 또 외부에서 무단으로 접근해서 값을 수정시킬 수도 있

 

가령 예를 들어서, 개발을하다가 변수를 idx, idd로 설정하니 너무나도 직관적으로 알기 힘들어서 변수를 그냥 x, y로 바꾼다고할 때, 그럼 모든 클래스를 순회하며 변수를 수정해야할 것이다. 또 만약 매개변수로 값을 받아서 홀수면 2로 나눈 값을, 짝수면 원래값을 전해준다고할 때 그 기능도 넣기 어려울 것이고

 

class Test {
	private double idx;
    private double idd;
    
    public double getidx() { return idx; }
    public double getidd() { return idd; }
}

그래서 철저한 객체 지향프로그래밍을 위해서는 public 필드를 모두 private로 바꾸고 게터세터를 추가한다.

이런 방식은 public 클래스에서는 옳다. 패키지 바깥에서 접근할 경우 접근자를 public으로 설정하여 접근자로만 접근을 가능하게하면 된다.

 

다만 package-private 클래스나 private로 선언된 내부클래스의 경우에는 인스턴스 필드를 노출시켜도 큰 문제가 없다. 그 변수가 무엇을 표시하려는지만 잘 이해하고 알고 있기만하면 된다. 어차피 package-private로 선언된 클래스 간에서만 변수를 사용한다면 수십 (혹은 수백) 개의 패키지 외부 접근을 신경쓰지 않아도되고, 변수명이나 내부표현 방식에 수정이 생겨도 조금만 수정을하면 되기 때문이다. 

 

불변필드의 경우에는 표현방식을 변경할 수 없다와 부수작업을 수행할 수 없다는 것 이외에는 가변객체의 단점이 다소 줄어들지만 그래도 지양하는 것이 좋다.


아이템17. 변경 가능성을 최소화하라

불변클래스라는 것이 있다. 이름으로부터 알 수 있듯이 인스턴스 내부의 값이 생성된 후 클래스가 종료될 때까지 그 값을 영원히 유지하는 클래스다.

 

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {
    @Stable
    private final byte[] value;
    private final byte coder;

    private int hash; // Default to 0

    private boolean hashIsZero; // Default to false;

    @java.io.Serial
    private static final long serialVersionUID = -6849794470754667710L;

    static final boolean COMPACT_STRINGS;

    static {
        COMPACT_STRINGS = true;
    }

    @java.io.Serial
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

대표적인 불변클래스인 String 클래스

모든 인스턴스 멤버가 final 로 선언되어 있다.

불변클래스는 설계, 구현, 사용이 쉽고, 외부에서 값을 변경할 걱정도 적고 수정될 여지도 적으니 관리도 쉽다.

 

불변클래스는 다음의 원칙을 따르면 불변클래스다.

1. 객체 상태를 변경할 메서드(변경자, 대표적인 것이 세터)를 제공하지 않음

2. 클래스를 확장할 수 없다. 불변클래스더라도 하위클래스를 만들어 접근하는 것을 막는다.

java: cannot inherit from final java.lang.String

그래서 String 클래스를 확장하면 이런 오류가 터진다.

확장을 불가능하게 만드는 것은 생성자를 private 또는 package-private로 만든 후에 정적 팩토리 메서드를 제공하는 것이 최선이다. 책에서 흔히 얘기되는 BigInteger나 BigDecimal 클래스는 이러한 원칙이 잘 지켜지지 않았기 때문에, 외부 클라이언트로 이 객체를 받을 경우 방어적 복사를 진행하여 완전한 가변으로 만든 후 진행해야한다.

 

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

이것이 원칙이긴하지만 너무 빡빡하기도하여 약간의 예외를 둔다. 계산 비용이 큰 값을 객체가 생성될 때가 아니라 처음 쓰일 때 계산하여 final이 아닌 필드에 캐시해두기도 한다.

 

4. 모든 필드를 private로 선언한다. 가변객체를 클라이언트가 접근하는 것을 막는다. public final로 선언해도 부변객체가 되지만 아이템15의 마지막 내용에서 나오듯이 변경도 어렵고 부수작업도 하기 어려우므로 private로 설정하고 접근자를 두는 것이 낫다.

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

class Member {
    private String name;
    private int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Member() {
        this.name = "윤마치";
        this.age = 28;
    }

    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

class Song {
    private final String name;
    private final Member member;

    Song (Member member, String name) {
        this.member = member;
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public String getMember() {
        return member.getName() + " " + member.getAge();
    }
}
public class Stack {
    public static void main(String[] args) {
        Member member = new Member("디디디", 20);
        Song song = new Song(member, "자바");

        System.out.println(song.getMember() + " " + song.getName());

        member.setName("대한민국");
        member.setAge(30);
        System.out.println(song.getMember() + " " + song.getName());
    }
}

이렇게되면 분명히 Song 클래스는 불변클래스인듯 보이나 Member 클래스는 불변클래스가 아니게되어 값이 변하게된다.

 

    private final String name;
    private final Member member;

이렇게 클래스에 가변객체를 참조하는 변수가 하나라도 있다면 클라이언트는 그 객체의 참조를 얻을 수 없게 해야한다. 또 위의 예시처럼 절대로 클라이언트가 제공한 객체를 참조해서도 안된다. 위에서는 생성자를 통해 클라이언트가 제공한 객체를 참조했으나 생성자, 접근자 등에서 방어적 복사를 수행하여야한다.

 

또 이러한 점에서 함수형 프로그래밍도 사용된다.

public final class Complex {
	private final double re;
    private final double im;
    
    public Complex(double re, double im) {
    	this.re = re;
        this.im = im;
    }
    
    public Complex plus(Complex c) {
    	return new Complex(re + c.re, im + c.im);
    }
}

plus가 함수형프로그래밍, add가 명령형 프로그래밍이다.

이런식으로 인스턴스 값 자체를 수정하는 것이 아니라 메서드를 수행한 후 수정된 인스턴스를 가지고 있는 새 객체를 반환하는 것이 함수형 프로그래밍이다. 아마 헤퍼디에 이런 내용이 있던 것 같은데

 

이렇게되면 피연산자에 대해서 함수를 수행하지만 피연산자는 값이 그대로를 유지하게 된다. 반면에 명령형, 절차형 프로그래밍에서는 피연산자의 값이 변하게된다. 또 이러한 점을 의도하기 위하여 add 같은 동사를 사용하지 않고 plus 같은 전치사를 사용한다. 

 

왜 이런방식을 쓰는걸까? 솔직히 명령형, 절차형 프로그래밍에 비해서 번거롭고 덜 직관적으로 보인다.

우선, 이 방식으로 프로그래밍을 진행하면 코드 내에서 불변의 영역이 높아진다.

 

불변객체는 단순하다. 생성된 시점의 정보를 가비지컬렉터가 다가올 때까지 유지한다.

또한 다중스레드 환경에서 아무리 사용자가 늘어도 정보를 수정할 수 없으므로 스레드 안전하다.

그렇기 때문에 불변객체를 만들었다면 한 번 만든 인스턴스를 최대한 재활용하여 지나친 메모리 사용도 줄이고 효율성도 높이는 것이 좋다.

 

이렇게 불변클래스는 최대한 재활용해야하기 때문에 자주 사용되는 인스턴스를 캐싱하여 중복생성을 방지하는 정적 팩토리를 제공할 수 있다. 이전에 다뤘던 Integer 클래스를 비롯한 모든 박싱된 기본타입과 BigInteger 클래스가 대표적인 예시다.

 

 

불변객체는 자유롭게 공유할 수 있는데다가 불변객체끼리는 내부 데이터도 공유할 수 있다.

final int signum;
final int[] mag;

책에서 예시로 나온 BigInteger 클래스에는 signum이 부호를, mag가 값의 크기를 int 배열로 사용한다.

 

    public static final BigInteger ONE = valueOf(1);

이렇게 자주 사용되는 숫자를 미리 만들어놓은 것도 볼 수 있고

 

    public BigInteger negate() {
        return new BigInteger(this.mag, -this.signum);
    }

이렇게 negate() 메서드를 사용하면 크기가 같고 부호는 반대인 새로운 BigInteger 클래스를 생성하는데, 이 때 배열은 가변이지만 원본 인스턴스와 공유해도 된다. 새 BigInteger 인스턴스는 기존의 배열을 그대로 참조한다. 가변객체라면 외부에서 배열에 접근해 값을 수정할 수 있게되므로 이같이 설계할 수 없다. 불변객체니까 할 수 있는일

 

 

다만 불변클래스에도 단점은 있다.

값을 수정할 수 없기 때문에 값이 다르면 전부 다른 객체로 생성해야한다. 특히 객체 생성까지 자원이 많이 소요되고, 객체의 크기가 매우 크다면 성능 문제가 빚어진다.

 

이를 방지하는 방법으로는 흔히 쓰일 다단계 연산(한 번에 결과가 나오지 않고 여러 번 연산을 수행해야하는 경우)을 예측하는 것이다. 그러면 객체를 생성하지 않고도 다음 연산을 진행한다. 예를들어, Integer 클래스로 (1 + 2) * 7 을 진행하려면 먼저 1 + 2 객체를 만든 후에 7을 곱한 객체를 만들어서 연산과정에서 불필요한 객체가 생성되는데 연산과정에서 다단계연산에서의 불필요한 객체생성을 줄인다. 감사하게도 BigInteger 클래스는 다단계 연산 속도를 높이는 가변 동반 클래스를 package-private로 두고 있다. 그렇기 때문에 다단계 연산의 예측을 BigInteger가 대신해준다.

 

마지막으로, 세터 지양 역시 이러한 부분에서 나온다.

클래스는 꼭 필요한 것이 아니면 불변이여야한다. 불변객체, 불변클래스는 잠재적 성능저하 이외에는 장점이 많다. 단순한 값 객체는 불변으로 만들어야한다. 

 

불변으로 만들 수 없는 경우도 변경할 수 있는 부분을 최소한으로 해야한다. 객체가 가지는 상태의 수를 줄이고 고정된 상태가 늘어나면 관리도 쉬워지고 오류의 가능성도 줄어든다. 모든 필드는 private final로 선언하는 것이 좋다. 다른 합당한 이유가 없다면

 생성자와 정적팩토리 이외에는 어느 방법이던 public으로 제공해서는 안된다. 초기화는 불변식 설정이 모두 끝난 상태로 초기화해야한다.

 

우선 여기까지만 읽고 아이템18은 나중에 하기로한다. 내용이 좀 어렵네 이거;;

자바의 정석 읽고 시간 많을 때 한 번 다시 읽어야할 것 같기도하다

 

'언어공부 > Java | Kotlin' 카테고리의 다른 글

[CS] 스레드란 무엇일까?  (0) 2025.10.15
[CS] 이펙티브자바 독서 #2 - 아이템18  (0) 2025.10.14
[CS] 자바의 정석 독서 #13 - 내부클래스  (0) 2025.10.10
[CS] 자바의 정석 독서 #12 - 추상클래스와 인터페이스  (0) 2025.10.09
[CS] 자바의 정석 독서 #11 - 다형성  (1) 2025.10.09
'언어공부/Java | Kotlin' 카테고리의 다른 글
  • [CS] 스레드란 무엇일까?
  • [CS] 이펙티브자바 독서 #2 - 아이템18
  • [CS] 자바의 정석 독서 #13 - 내부클래스
  • [CS] 자바의 정석 독서 #12 - 추상클래스와 인터페이스
Radiata
Radiata
개발을 합니다.
  • Radiata
    DDD
    Radiata
  • 전체
    오늘
    어제
    • 분류 전체보기 (211)
      • 신년사 (3)
        • 2025년 (2)
        • 2026년 (1)
      • CS (59)
        • JVM (12)
        • 백엔드 (20)
        • 언어구현 (1)
        • 객체지향 (1)
        • 논리회로 (5)
        • 컴퓨터구조 (9)
        • 데이터베이스 (1)
        • 컴퓨터 네트워크 (10)
      • 언어공부 (64)
        • Java | Kotlin (48)
        • JavaScript | TypeScript (9)
        • C | C++ (6)
      • 개인 프로젝트 (11)
        • [2025] Happy2SendingMails (3)
        • [2026] 골든리포트! (8)
        • [2026] 순수자바로 개발하기 (0)
        • 기타 이것저것 (0)
      • 팀 프로젝트 (29)
        • [2025][GDG]홍대 맛집 아카이빙 프로젝트 (29)
      • 알고리즘 (13)
        • 백준풀이기록 (11)
      • 놀이터 (0)
      • 에러 수정일지 (2)
      • 고찰 (24)
        • CEOS 23기 회고록 (2)
  • 블로그 메뉴

    • CS
    • 언어공부
    • 개인 프로젝트
    • 팀 프로젝트
    • 알고리즘
    • 고찰
    • 신년사
    • 컬러잇 개발블로그
  • 링크

    • 컬러잇 개발블로그
  • 공지사항

  • 인기 글

  • 태그

    144
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
Radiata
[CS] 이펙티브자바 독서 #1 - 아이템15 ~ 17
상단으로

티스토리툴바