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

무한성장개발자·2024년 10월 19일

아이템 15. 핵심 정리 - 구현과 API를 분리하는 “정보 은닉”의 장점

아이템 15. 핵심 정리 - 클래스와 인터페이스의 접근 제한자 사용 원칙

package me.whiteship.chapter04.item15.class_and_interfaces.member;

public interface MemberService {
}
package me.whiteship.chapter04.item15.class_and_interfaces.member;

public class Member {
}
package me.whiteship.chapter04.item15.class_and_interfaces.member;

import java.util.Arrays;

class DefaultMemberService implements MemberService {

    private String name;

    private static class PrivateStaticClass {

    }

    private class PrivateClass {

    }

    public static void main(String[] args) {
        Arrays.stream(PrivateClass.class.getDeclaredFields()).forEach(System.out::println);
    }

}

public을 빼는게 좋아보인다. 내부 구현인데 다른 애들이 알 필요는 없다.
package-private으로 하는게 낫다.

한 클래스에서만 사용하는 package-private 클래스나 인터페이스는 해당 클래
스에 private static으로 중첩 시키자.

package me.whiteship.chapter04.item15.class_and_interfaces.member;

interface MemberRepository {
}
package me.whiteship.chapter04.item15.class_and_interfaces.member;

import java.util.Arrays;

class DefaultMemberService implements MemberService {

    MemberRepository memberRepository;

    public Member getMember(){
        memberRepository.findById();
        return null;
    }

//    private String name;
//
//    private static class PrivateStaticClass {
//
//    }
//
//    private class PrivateClass {
//
//    }
//
//    public static void main(String[] args) {
//        Arrays.stream(PrivateClass.class.getDeclaredFields()).forEach(System.out::println);
//    }

}

이렇게 사용하는데 여기서 밖에 안쓰는 경우

package me.whiteship.chapter04.item15.class_and_interfaces.member;

class DefaultMemberService implements MemberService {

    MemberRepository memberRepository;

    public Member getMember(){
        memberRepository.findById();
        return null;
    }

//    private String name;
//
//    private static class PrivateStaticClass {
//
//    }
//
//    private class PrivateClass {
//
//    }
//
//    public static void main(String[] args) {
//        Arrays.stream(PrivateClass.class.getDeclaredFields()).forEach(System.out::println);
//    }

    private static interface MemberRepository {
    }
}

으로 중첩시키자.

private class가 아니라 왜 private static으로 중첩하자고 했을까?

private class는 DefaultMemberService 밖으로 나갈 이유가 없다.

다른 점은
private class는 외부 인스턴스를 참조한다. 내부 클래스에서 외부 클래스의 자기를 감싸고 있는 클래스의 멤버변수의 접근이 수월하다.
private static class는 내부에 들어있지만 남남이다.

package me.whiteship.chapter04.item15.class_and_interfaces.member;

import java.util.Arrays;

class DefaultMemberService implements MemberService {

    private String name;

    private static class PrivateStaticClass {
        void doPrint(){
        	System.out.println(name);
        }
    }

    private class PrivateClass {
        void doPrint(){
            System.out.println(name);
        }
    }

//    public static void main(String[] args) {
//        Arrays.stream(PrivateClass.class.getDeclaredFields()).forEach(System.out::println);
//    }

}

PrivateStaticClass가 더 어울린다. memberRepository는 원래 별개의 클래스였다. 둘과의 관계가 더 단순해진다. DefaultMemberService가 privateStaticClass만을 사용하기 때문이다.

아이템 15. 핵심 정리 - 멤버(필드, 메서드, 중첩 클래스/인터페이스)의 접근 제한자 원칙

package me.whiteship.chapter04.item15.class_and_interfaces.item;

import me.whiteship.chapter04.item15.class_and_interfaces.member.MemberService;

public class ItemService {

    private MemberService memberService;

    boolean onSale;

    protected int saleRate;

    public ItemService(MemberService memberService) {
        //if (memberService == null) {
            //throw new IllegalArgumentException("MemberService should not be null.");
        //}

        this.memberService = memberService;
    }

    MemberService getMemberService() {
        return memberService;
    }
}

private, package private, protected, public

공개 API를 제외한 것은 모두 private,
필요하면 package private, package private이 많아진다면 컴포넌트를 나눠야 한다.

상수일 경우에만 public static final로 공개하는 것을 권장한다.
이 밖의 모든 필드는 public이 아니어야 한다.
public class의 instance field는 public이 아니어야 한다.

private -> package private으로 바꿔야 하는 경우 테스트 코드를 작성할 때,
같은 패키지 내에서 필드에 바로 접근하고 싶은 경우에 그럴 수 있다.

package me.whiteship.chapter04.item15.class_and_interfaces.item;

import me.whiteship.chapter04.item15.class_and_interfaces.member.MemberService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@ExtendWith(MockitoExtension.class)
class ItemServiceTest {

    @Mock
    MemberService memberService;

    @Test
    void itemService() {
        ItemService service = new ItemService(memberService);
        assertNotNull(service);
        assertNotNull(service.getMemberService());
    }

}

멤버 패키지 안에서 DefaultMemberService를 노출시키고 싶지 않았다.

이때 사용하는게 @Mock이다.
@ExtendWith를 사용해서 Mockito를 사용할 수 있다.

getter를 사용하는 경우 getMemberService를 통해 가져온다.

같은 패키지인 경우 아래와 같이 할 수 있다.


package me.whiteship.chapter04.item15.class_and_interfaces.item;

import me.whiteship.chapter04.item15.class_and_interfaces.member.MemberService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@ExtendWith(MockitoExtension.class)
class ItemServiceTest {

    @Mock
    MemberService memberService;

    @Test
    void itemService() {
        ItemService service = new ItemService(memberService);
        assertNotNull(service);
        assertNotNull(service.memberService);
    }

}

Mockito를 안 사용하면 Null이다.

package me.whiteship.chapter04.item15.class_and_interfaces.item;

import me.whiteship.chapter04.item15.class_and_interfaces.member.MemberService;

public class ItemService {

    private MemberService memberService;

    boolean onSale;

    protected int saleRate;

    public ItemService(MemberService memberService) {
        if (memberService == null) {
            throw new IllegalArgumentException("MemberService should not be null.");
        }

        this.memberService = memberService;
    }

    MemberService getMemberService() {
        return memberService;
    }
}

테스트를 위해 public으로 만드는 것은 안좋다.
public 클래스에는 인스턴스 변수들은 모두 private으로 해야 한다.
public static final String WHITESHIP_NAME = "keesun"; 은 해도 되는데
public static final String[] NAMES = new String[10]; 은 외부에서 변수를 참조가능하기 때문에 배열이 변경될 수 있어서 안좋다.

아이템 15. 완벽 공략 요약

아이템 15. 완벽 공략 - 자바 플랫폼 모듈 시스템 1

모듈을 이용하면 자바 런타임을 필요로 한것만 구동 시킬 수 있다.

JDK자체가 모듈로 되어있다.

JRE를 가장 기본적인 패키지만 가지고 구성가능하다.

package me.whiteship.hello;

import me.whiteship.name.NameService;

public class HelloService {

    public static void main(String[] args) {
        NameService service = new NameService();
        System.out.println("Hello " + service.getName() + ", Java Module");
    }
}
/**
 * Hello Module
 */
module me.whiteship.hello {
    requires me.whitehip.name;
}

어떤 패키지를 공개하겠다 를 알려줄 수 있다.
패키지에 module-info.java가 있어야 모듈인 것이다.

아이템 15. 완벽 공략 - 자바 플랫폼 모듈 시스템 2

package me.whiteship.hello;

//import me.whiteship.name.NameService;

public class HelloService {

    public static void main(String[] args) {
//        NameService service = new NameService();
        System.out.println("Hello ");
//        System.out.println("Hello " + service.getName() + ", Java Module");

    }
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>me.whiteship</groupId>
    <artifactId>hello-module</artifactId>
    <version>1.0.0-snapshot</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

<!--    <dependencies>-->
<!--        <dependency>-->
<!--            <groupId>me.whiteship</groupId>-->
<!--            <artifactId>name-service</artifactId>-->
<!--            <version>1.0-SNAPSHOT</version>-->
<!--        </dependency>-->
<!--    </dependencies>-->

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>me.whiteship.hello.HelloService</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

maven clean install 둘다 선택 후 run 한다.

java --module-pat
h .\hello-module\target\hello-module-1.0.0-snapshot.jar --module me.whiteship.hello/me.white
ship.hello.HelloService

name-service 모듈 추가

package me.whiteship.name;

public class NameService {

    public String getName() {
        return "whiteship";
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>me.whiteship</groupId>
    <artifactId>name-service</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.2</version>
            </plugin>
        </plugins>
    </build>

</project>

hello-module에서 name-service를 사용한다고 명시한다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>me.whiteship</groupId>
    <artifactId>hello-module</artifactId>
    <version>1.0.0-snapshot</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>me.whiteship</groupId>
            <artifactId>name-service</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>me.whiteship.hello.HelloService</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

hello-module에서 사용한다.
그러나 에러가 난다.

/**
 * name module
 */
module me.whitehip.name {
    exports me.whiteship.name;
}

me.whiteship.name에서 exports를 해야 한다.

/**
 * Hello Module
 */
module me.whiteship.hello {
    requires me.whitehip.name;
}

me.whiteship.hello에서 me.whiteship.name을 사용한다고 명시해야 한다.

자바 모듈 잘 쓰지 않는 이유

/**
 * name module
 */
module me.whitehip.name {
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>
    <modules>
        <module>hello-module</module>
    </modules>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>effective-java-part2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>effective-java-part2</name>
    <description>effective-java-part2</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>me.whiteship</groupId>
            <artifactId>name-service</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

exports를 없애고 effective java에 적으면 쓸 수 있게 된다.
모듈을 모듈 아닌 곳에서 쓰면 무의미 하다.

export가 안 되어 있더라도 모듈에 들어있는 모든 퍼블릭한 것들은 그냥 쓸 수 있게 된다.
모듈은 모듈에서 쓸 때 의미가 없다.

모듈일 때 모듈이 아닌 jar 파일을 참조하고 싶을 때는 어떻게 해야 되냐
Automatic 모듈을 사용하면 그것까지 커버가 된다.

모듈이 아닌 jar 파일들은 public인 경우 다 공개가 되서 사용이 된다.
exports 패키지를 한다고 하더라도 public인 경우 다른 모듈에서도 사용이 가능해지게 되서 무의미해진다. 해당 모듈이 아닌 곳에서 사용해도 무시가 된다.

0개의 댓글