2024-10-04,   이현지

1. 생성자 패턴 (Constructor Pattern)

  • 생성자를 이용하여 클래스의 인스턴스를 생성
  • 객체 생성 시 필요한 모든 필드를 생성자 매개변수로 전달

예시

public class User {
	private String name;
    private int age; 
    
    public User(String name, int age) {
    	this.name = name; 
        this.age = age;
    }
}

장점

  • 간단하고 직관적
  • 객체 생성 시 누락 없이 모든 필드 초기화 가능

단점

  • 매개변수가 많으면 가독성이 떨어짐
  • 매개변수 값이 바뀌어서 입력될 경우, 같은 타입이면 컴파일러 오류가 발생하지 않음
// User user = new User("이름", "나이"); 일 경우
User user = new User("최강수", "41");
User user = new User("45", "강현식"); 
// ↑ 이름과 나이가 바뀌어도 컴파일 오류 발생하지 않음



2. 정적 메소드 패턴 (Static Method Pattern)

  • 정적 메소드를 사용하여 객체를 생성함

예시

public class User {
	private String name; 
    private int age;
    
    private User(String name, int age) {
    	this.name = name;
        this.age = age;
    }
    
    public static User createUser(String name, int age) {
    	return new User(name, age);
    }
}

장점

  • 객체 생성 로직을 캡슐화하여 클라이언트 코드를 단순화하고, 동일한 객체 생성 로직을 여러 곳에서 재사용 할 수 있음 (중복 감소, 일관성 유지)
  • 반환되는 인스턴스가 어떤 역할을 하는지 객체 생성 메소드명에 명시할 수 있음
  • 인터페이스나 추상클래스를 이용하여 반환타입을 제한할 수 있음

단점

생성자 패턴과 마찬가지로 매개변수 값이 잘못 바뀔 경우 타입만 맞으면 컴파일러 오류로 잡아낼 수 없음



3. 수정자 패턴 (Setter Pattern)

  • setter를 이용하여 객체의 필드를 개별적으로 설정 가능
  • 생성자로 기본값을 설정하고, 수정자를 통해 값을 변경함

예시

public class User {
	private String name;
    private int age; 
    
    public User() {}

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

장점

  • 객체를 선택적, 단계적으로 초기화할 수 있음

단점

  • setter가 많을 경우 클라이언트 코드에서 객체를 설정하는 코드가 길고 복잡해짐
  • setter를 통해 객체의 상태를 변경할 수 있으므로 객체의 불변성 및 일관성을 유지하기 어려움
    -> 멀티스레드 환경에서 문제를 일으킬 수 있음
  • 단계적으로 객체 초기화가 가능하기 때문에 필요한 모든 속성이 설정되지 않은 상태에서 객체가 사용될 우려가 있음



4. 빌더 패턴 (Builder Pattern)

  • 빌더 클래스를 사용하여 객체를 생성함
  • 복잡한 객체 생성 시 유용함
public class User {
	// 필수 필드
	private String id; 
    private String pwd;
	// 옵션 필드
	private String name; 
    private int age;
    
    /** 빌더만 인스턴스 생성 가능하도록 생성자를 private로 선언 */
    private User(UserBuilder builder) {
    	this.id = builder.id; 
        this.pwd = builder.pwd;
    	this.name = builder.name; 
        this.age = builder.age;
    }

	/** 빌더 클래스 */
	public static class UserBuilder {
        // 필수 필드
        private id; 
        private pwd;
        // 옵션 필드
        private String name;
        private int age;

       // 필수 필드는 Builder 생성자로 정의 
        public UserBuilder(String id, String pwd) {
        	this.id = id; 
            this.pwd = pwd;
            return this;
        }
        
        // 옵션 필드는 메서드로 정의
        public UserBuilder setName(String name) {
        	this.name = name;
            return this;
        }
        
        public UserBuilder setAge(int age) {
        	this.age = age;
            return this;
        }
        
        // 빌더 클래스의 build() 메서드를 통해서만 User 인스턴스 생성 가능
        public User build() {
        	return new User(this);
        }    
	}
}

인스턴스 생성

public class Sample {
	
    public void test() {
    	// 필수 필드는 빌더클래스의 생성자로 정의
    	User user = User.UserBuilder("testId", "testPwd")
        				// 옵션 필드는 빌더 클래스의 메서드로 정의
                        // .setName("Sam")
                        // .setAge(10)
                        .build();
    }

}

@Builder 어노테이션

@Builder 어노테이션을 이용하면 빌더 클래스를 직접 작성하지 않고 빌더 패턴 적용 가능

예시1) 클래스 위에 @Builder 를 붙이는 경우

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class User {
    private String name;
    private int age;
}

예시2) 생성자 위에 @Builder 를 붙이는 경우

import lombok.Builder;
import lombok.Getter;

@Getter
public class User {
    private String name;
    private int age;

    @Builder
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

예시 1과 2 모두 builder() 메소드로 객체를 생성

public class Main {
    public static void main(String[] args) {
        User user = User.builder()
                        .name("Harry")
                        .age(25)
                        .build();

        System.out.println("Name: " + user.getName());
        System.out.println("Age: " + user.getAge());
    }
}

장점

  • 클라이언트 코드의 가독성 향상
  • 필수 필드만 선택적으로 초기화 가능
  • 필수 필드에 대해선 컴파일러로 오류 체크 가능

단점

  • 추가적인 객체(builder)를 생성해야 하므로 성능에 미세한 오버헤드 발생 가능
  • 간단한 객체는 단순한 생성자나 수정자를 사용하는 것이 더 나음
  • 객체의 필수 속성을 체크하거나 제약 조건을 처리하는 코드가 빌더 내부에서 복잡해질 수 있음
  • 여러 빌더 인스턴스가 동시에 사용될 경우, 각각의 빌더가 동일한 객체상태를 유지하기 어려우므로 추가적인 설정이 필요함



이상으로 다양한 Java 객체 생성 패턴 및 장단점에 대한 포스팅을 마치겠습니다.



Reference

  • https://mangkyu.tistory.com/163
  • https://velog.io/@a01021039107/4.-%EA%B0%9D%EC%B2%B4-%EC%83%9D%EC%84%B1%EC%97%90-%EC%83%9D%EC%84%B1%EC%9E%90-vs-%EB%B9%8C%EB%8D%94-%ED%8C%A8%ED%84%B4-%EB%AC%B4%EC%97%87%EC%9D%84-%EC%8D%A8%EC%95%BC-%ED%95%A0%EA%B9%8C

업데이트: