[이펙티브 자바] 객체 생성
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 : from과 of의 더 구체적인 버전
- instance 혹은 getInstance : 지정한 인스턴스를 반환하지만, 같은 인스턴스를 보장하지 않음
- create 혹은 newInstance : 매번 새로운 인스턴스를 생성해 반환함을 보장
- getType : 생성할 클래스가 아닌 다른 클래스에서 팩터리 메서드를 정의할 때 사용
- newType : newInstance와 같으나, 다른 클래스에 팩터리 메서드를 정의할 때 사용
- type : getType과 newType의 간결한 버전
아이템 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 데이터나 테스트용 패턴을 주입할 수 있어 코드가 독립적이고 테스트 친화적이다.
마무리
개발을 하다 보면 유연성, 가독성, 효율성 모두를 만족시키는 설계를 고민하게 된다.
정적 팩토리 메서드나 빌더 패턴처럼, 단순하지만 강력한 도구를 제대로 이해하고 활용하면,
불필요한 실수를 줄이고 더 유지보수하기 좋은 코드를 작성할 수 있다.
결국 중요한 건, 어떻게 하면 내가 실수를 줄이고, 팀원들이 내 코드를 쉽게 이해할 수 있을까?
여기서부터 좋은 설계가 시작된다고 느낀다.
"좋은 코드는 내가 내일 봐도 이해하기 쉬운 코드다." - 내가 코드를 작성하며 스스로에게 가장 자주 하는 말. 😊
'📚Book Archive > Effective Java' 카테고리의 다른 글
[이펙티브 자바] Generic 으로 만들어 사용하기 (0) | 2025.01.03 |
---|---|
[이펙티브 자바] Interface와 Class 설계 원칙 (0) | 2024.12.30 |
[이펙티브 자바] Class와 상속 (4) | 2024.12.20 |
[이펙티브 자바] 모든 객체의 공통 메서드 (0) | 2024.12.13 |
[이펙티브 자바] 객체 파괴 (0) | 2024.12.07 |
블로그의 정보
코드의 여백
rowing0328