코드의 여백

[이펙티브 자바] 객체 생성

by rowing0328

※ 책 내용을 바탕으로 제 관점에서 풀어쓴 글입니다. 일부 내용이 다를 수 있습니다.

 

아이템 1 - 생성자 대신 정적 팩토리 메서드를 고려하라

// 생성자를 사용하는 경우
new Member("hyoseung", MemberType.ADMIN); // true가 뭘 의미하는지 모호하다.

// 정적 팩터리 메서드를 사용하는 경우
User user = User.createAdminUser(); // 관리자 생성임을 바로 알 수 있다.
  • 이름을 붙일 수 있다.
    정적 팩토리 메서드는 이름을 통해 의도를 명확히 드러낼 수 있다.
    반면, 생성자는 이름을 붙일 수 없어 "이게 뭐 하는 생성자인지" 헷갈릴 수 있다.
  • 판단 기준이 명확하다.
    생성자에서 boolean 같은 값으로 구분하면 의미가 불분명하고 실수할 가능성이 높다.
    정적 팩토리 메서드는 명확한 이름으로 로직을 전달하니 더 직관적이다.
  • 사용성 향상 및 오류 방지
    이름 덕분에 코드가 더 읽기 쉽고 유지보수가 편하다.
    잘못된 값을 전달할 위험도 줄어든다.

 

// Singleton pattern - Single Object
class ConnectionManager {

    // 생성자를 private으로 선언해 외부에서 인스턴스를 생성하지 못하게 한다.
    private ConnectionManager() {
    }

    // 내부클래스에서 단 한번만 인스턴스를 생성한다.
    private static class ConnectionManagerHolder { 
        private static final ConnectionManager instance = new ConnectionManager();
    }

    // 정적 팩토리 메서드를 통해 인스턴스를 제공한다.
    public static ConnectionManager getInstance() { 
        return ConnectionManagerHolder.instance;
    }

}

// Flyweight pattern = Collection Object
class Icon {

    private final String filePath;

    // 캐싱을 위한 저장소
    private static final Map<String, Icon> cache = new HashMap<>();

    private Icon(String filePath) {
        this.filePath = filePath;
    }

    // 정적 팩터리 메서드를 통해 인스턴스를 제공한다.
    public static Icon getIcon(String filePath) {
        return cache.computeIfAbsent(filePath, Icon::new); // 이미 존재하면 반환, 없으면 생성
    }

}
  • 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
    객체 생성 과정을 개발자가 직접 제어할 수 있다.
    동일한 객체를 반환해 메모리 사용을 효율화할 수 있다.
    싱글톤 패턴을 통해 애플리케이션 내에서 단일 인스턴스를 보장할 수 있다.
    Flyweight 패턴처럼 중복 생성을 방지하고 객체를 재사용할 수 있다.

 

// 인터페이스 정의
interface MemberRepository {

    void save(String memberName);

}

// 구현체 A
class InMemoryMemberRepository implements MemberRepository {

    public void save(String memberName) {
        System.out.println(memberName + " saved in memory.");
    }

}

// 구현체 B
class DatabaseMemberRepository implements MemberRepository {

    public void save(String memberName) {
        System.out.println(memberName + " saved in database.");
    }

}

// 정적 팩터리 메서드를 제공하는 클래스
class MemberRepositoryFactory {

    public static MemberRepository getRepository(String type) {
        if ("memory".equalsIgnoreCase(type)) {
            return new InMemoryMemberRepository();
        } else if ("database".equalsIgnoreCase(type)) {
            return new DatabaseMemberRepository();
        }
        throw new IllegalArgumentException("Unknown repository type: " + type);
    }

}

public class Main {

    public static void main(String[] args) {
        // 메모리 기반 구현체 반환
        MemberRepository memoryRepo = MemberRepositoryFactory.getRepository("memory");
        memoryRepo.save("Alice");

        // 데이터베이스 기반 구현체 반환
        MemberRepository databaseRepo = MemberRepositoryFactory.getRepository("database");
        databaseRepo.save("Bob");
    }

}
    • 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
      정적 팩토리 메서드는 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
      인터페이스나 상위 타입을 반환하여 클라이언트는 구현 세부 사항에 의존하지 않아도 된다.
      구현체를 숨길 수 있어, 반환 객체를 바꿔도 클라이언트 코드는 수정할 필요가 없다.
      다형성을 활용하면 새로운 구현체 추가 시 기존 코드를 수정하지 않아도 된다.
      스프링의 의존성 주입(DI)처럼 객체 생성과 사용을 깔끔하게 분리할 수 있다.

 

@Bean
public PasswordEncoder passwordEncoder() {
    // 실제 구현체는 런타임 시점에 결정됨
    return new BCryptPasswordEncoder();
}
  • 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

 

정적 팩토리 메서드 네이밍 규칙

// from
Date d = Date.from(instant);

// of
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);

// valueOf
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);

// instance or getInstance
StackWalker luke = StackWalker.getInstance(options);

// create or newInstance
Object newArray = Array.newInstance(classObject, arrayLen);

// getType
FileStore fs = Files.getFileStore(path);

// newType
BufferedReader br = Files.newBufferedReader(path);

// type
List<Complaint> litany = Collections.list(legacyLitany);
  • from : 매개변수를 하나 받아 해당 타입의 인스턴스를 반환하는 메서드
  • of : 여러 매개변수를 받아 적합한 인스턴스를 반환하는 메서드
  • valueOf : fromof의 더 구체적인 버전
  • instance 혹은 getInstance : 지정한 인스턴스를 반환하지만, 같은 인스턴스를 보장하지 않음
  • create 혹은 newInstance : 매번 새로운 인스턴스를 생성해 반환함을 보장
  • getType : 생성할 클래스가 아닌 다른 클래스에서 팩터리 메서드를 정의할 때 사용
  • newType : newInstance와 같으나, 다른 클래스에 팩터리 메서드를 정의할 때 사용
  • type : getTypenewType의 간결한 버전

 

 

아이템 2 - 생성자에 매개변수가 많다면 빌더를 고려하라.

// 모든 값이 필수인 경우 (생성자 사용)
User user = new User("hyoseung", 25); // 간단하고 명확

// 필수 값이 없는 경우 (롬복 빌더 사용)
User user = User.builder()
    .name("hyoseung")
    .age(25)
    .build();

// 필수 값이 일부인 경우 (생성자 + 빌더 조합 사용)
User user = new User.Builder("hyoseung") // 필수값은 생성자로
    .age(25) // 선택값은 빌더 메서드로
    .build();
  • 모든 값이 필수인 경우
    생성자는 필수값을 강제하고, 코드가 간결하며 직관적이다.
  • 필수 값이 없는 경우
    롬복의 @Builder는 빌더 코드를 자동 생성하여 작성 시간을 단축하고 가독성을 높인다.
  • 필수 값이 일부인 경우
    생성자와 빌더를 조합해 필수값은 생성자로 강제하고, 선택값은 빌더 메서드로 처리해 유연성을 확보한다.

 

 

아이템 3 - private 생성자나 열거 타입으로 싱글턴임을 보증하라

public class Speaker {

    private static volatile Speaker instance;

    private Speaker() {}

    public static Speaker getInstance() {
        if (instance == null) {
            synchronized (Speaker.class) {
                if (instance == null) {
                    instance = new Speaker();
                }
            }
        }
        return instance;
    }

}
  • 지연 초기화(Lazy Initialization)
    인스턴스를 필요할 때 처음으로 생성한다.
    if (instance == null) 조건으로 인스턴스가 없을 때만 생성해 메모리를 효율적으로 관리한다.
  • synchronized
    getInstance() 메서드에 synchronized를 사용해 멀티스레드 환경에서도 안전하게 동작한다.
    단 하나의 인스턴스만 생성되도록 보장한다.

 

 

아이템 4 - 인스턴스화를 막으려거든 private 생성자를 사용하라

public class BikeUtils {

    private BikeUtils() {
        throw new AssertionError();
    }

    // static method (유틸성)
    public static <T> T convertObject(..) {...}

}
  • Human error 방지
    누군가(심지어 자신도) 실수로 인스턴스를 생성하지 않도록 private constructor를 사용한다.
  • 간단한 노력으로 안정성 확보
    private constructor를 선언하는 것은 많은 노력이 필요하지 않으며, 실수를 방지하는 확실한 방법이다.
  • Util 클래스의 경우 생략 가능
    관용적으로 인스턴스를 생성하지 않는 클래스(예: 유틸리티 클래스)라면, 경우에 따라 생략하기도 한다.

 

 

아이템 5 - 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

@Configuration
public class PhonePatternChecker {

    private final String pattern = "\\d{10}"; // 고정된 패턴

    public boolean isValid(String phone) {
        return phone.matches(pattern);
    }
}

@Configuration
public class PhonePatternChecker {

    private final String pattern;

    public PhonePatternChecker(String pattern) { // 생성자 패턴
        this.pattern = pattern;
    }

    public boolean isValid(String phone){ ... }

}
  • 유연성
    외부에서 값을 주입받아 코드 수정 없이 다양한 상황에서 재사용할 수 있다.
  • 환경별 대응
    application.yml 설정을 통해 환경(live, dev, test)에 따라 다른 값을 쉽게 적용할 수 있다.
  • 테스트 편의성
    테스트 작성 시 Mock 데이터나 테스트용 패턴을 주입할 수 있어 코드가 독립적이고 테스트 친화적이다.

 

 

마무리

개발을 하다 보면 유연성, 가독성, 효율성 모두를 만족시키는 설계를 고민하게 된다.

정적 팩토리 메서드나 빌더 패턴처럼, 단순하지만 강력한 도구를 제대로 이해하고 활용하면,

불필요한 실수를 줄이고 더 유지보수하기 좋은 코드를 작성할 수 있다.

 

결국 중요한 건, 어떻게 하면 내가 실수를 줄이고, 팀원들이 내 코드를 쉽게 이해할 수 있을까?

여기서부터 좋은 설계가 시작된다고 느낀다.

 

"좋은 코드는 내가 내일 봐도 이해하기 쉬운 코드다." - 내가 코드를 작성하며 스스로에게 가장 자주 하는 말. 😊

블로그의 정보

코드의 여백

rowing0328

활동하기