🖍️ new 키워드의 단점?
우리는 객체를 생성할 때 보통 new 키워드로 직접 생성자를 호출해서 생성한다.
Calc c = new Calc();
new 키워드는 간단하지만 단점이 존재한다.
- 읽기 어려운 생성자
- 생성자의 매개 변수가 많아지면 읽기가 힘들고 유지 보수가 어렵다.
- 객체의 특징을 파악하기 어려움
- 생성자의 이름은 바꿀 수 없으므로 생성되는 객체의 목적과 의미를 구분지을 수 없다.
- 예) new Calc(int x, int y);와 new Calc(int x, int y, int z); 둘은 성격이 다르지만 모두 Calc라는 이름의 생성자를 사용
- 객체 제어 불가
- 생성자는 응집도를 높이기 위해 내부에 코드를 구현하지 않는 것이 권장된다.
- 따라서 생성자 내부에 객체 생성 로직을 두기 어려우므로 객체 생성 과정을 제어할 수 없다.
- 예) 이미 생성된 객체가 있다면 재사용하기
- 예) 분기문으로 여러 다양한 하위 타입의 객체 생성하기
new키워드의 이러한 단점을 보완하고싶다면 정적 팩토리 메서드를 쓰면 된다.
🖍️ 정적 팩토리 메서드
- 객체를 static method를 통해 간접적으로 생성
정적 팩토리 메서드 패턴은 static method를 통해 간접적으로 생성자를 호출하도록 유도한다.
class Student{
int id;
String name;
//생성자를 private으로 막아서 외부 접근 차단
private Student (int id, String name){
this.id = id;
this.name = name;
}
//정적 팩토리 메소드에서 생성자를 호출하고 만들어진 객체 리턴
public static Student of(int id, String name){
return new Student(id, name);
}
}
public static void main(String[] args){
//정적 팩토리 메소드로 객체 생성
Student student = Student.of(1, "Jane");
}
📍정적 팩토리 메소드의 장점
1. 메서드 네이밍
- 생성하려는 객체의 기능, 특징을 메서드 네이밍을 통해 명확하게 표현할 수 있다.
객체를 만들 때 값이 반드시 설정되어야 하는 필수 멤버가 있고, 선택적으로 값을 설정하는 선택 멤버가 있다. 그래서 어떤 객체를 만들 때는 모든 멤버 변수에 값이 할당되어야 하고, 어떤 객체를 만들 때는 필수 멤버 변수에만 값을 할당해야 한다. 이렇게 여러 종류의 객체를 만들기 위해서 생성자를 쓴다면, 각각 필요한 멤버의 값을 인자로 받는 생성자를 여러 개 오버라이딩해서 사용할 것이다.
이 방법의 가장 큰 문제점은, 생성하려는 객체의 특성을 제대로 표현하기 어렵다는 점이다. 생성자는 클래스명과 같아야 한다는 규칙 때문에 값을 설정한 멤버 종류가 전혀 다른 객체를 생성할 때도 같은 이름을 가진 생성자를 사용해야 한다.
따라서 정적 팩토리 메서드를 통해 생성하고자 하는 객체의 특성을 잘 유추할 수 있게 메서드 네이밍을 하는 게 좋다.
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 'of' 패턴 사용
public static Person nameAndAgeOf(String name, int age) {
return new Person(name, age);
}
}
2. 인스턴스 통제
- 자주 쓰이는 객체를 싱글톤으로 만들거나, 캐싱해서 재사용하는 등 인스턴스를 통제할 수 있다.
메소드를 거쳐서 객체를 만들기 때문에, 메서드에서 어떤 처리를 함으로써 객체 생성을 통제하고 관리할 수 있다.
대표적인 예로는 싱글톤이 있다. 정적 팩토리 메소드를 통해 객체를 한 번만 생성해서 저장해두고 같은 클래스의 객체를 요청받는 경우 생성된 객체를 반환해서 메모리를 아낀다.
class Singleton {
private static Singleton instance;
private Singleton() {}
// 정적 팩토리 메서드
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
인스턴스 캐싱(Caching)이라는 것도 있는데, 정적 팩토리 메소드 내부에 다음과 같은 로직을 두는 것이다.
생성된 객체를 캐싱해두고 같은 타입의 객체가 또 호출되면 먼저 캐시 저장소를 조회하도록 한다.
class Day {
private String day;
public Day(String day) { this.day = day; }
public String getDay() { return day; }
}
// Day 객체를 생성하고 관리하는 Flyweight 팩토리 클래스
class DayFactory {
// Day 객체를 저장하는 캐싱 저장소 역할
private static final Map<String, Day> cache = new HashMap<>();
// 자주 사용될것 같은 Day 객체 몇가지를 미리 등록한다
static {
cache.put("Monday", new Day("Monday"));
cache.put("Tuesday", new Day("Tuesday"));
cache.put("Wednesday", new Day("Wednesday"));
}
// 정적 팩토리 메서드 (인스턴스에 대해 철저한 관리)
public static Day from(String day) {
if(cache.containsKey(day)) {
// 캐시 되어있으면 그대로 가져와 반환
System.out.println("해당 요일은 캐싱되어 있습니다.");
return cache.get(day);
} else {
// 캐시 되어 있지 않으면 새로 생성하고 캐싱하고 반환
System.out.println("해당 요일은 캐싱되어 있지 않아 새로 생성하였습니다.");
Day d = new Day(day);
cache.put(day, d);
return d;
}
}
}
3. 다형성 활용
- 부모 타입을 가진 자식 객체를 생성 가능 (다형성)
- 구현체 클래스를 은닉 가능 (캡슐화)
- 다형성
- 하나의 객체가 여러 자료형 타입을 가질 수 있는 것
- 업캐스팅/다운캐스팅, 메소드 오버로딩/오버라이딩
- 캡슐화
- 자바에서 정보를 은닉하는 기법 중 하나
- 외부에서 클래스 내에 변수와 메서드가 뭐가 있는지 모르게 하는 것
아래 예제를 보면 Shape 인터페이스와 Shape의 구현체 Circle, Rectangle가 있다.
interface Shape {
void draw();
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Circle");
}
}
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Rectangle");
}
}
class ShapeFactory {
//정적 팩토리 메소드
public static Shape createShape(String type) {
if ("circle".equalsIgnoreCase(type)) {
return new Circle(); // Circle 객체 반환
} else if ("rectangle".equalsIgnoreCase(type)) {
return new Rectangle(); // Rectangle 객체 반환
}
throw new IllegalArgumentException("Unknown shape type: " + type);
}
}
public class Main {
public static void main(String[] args) {
Shape shape1 = ShapeFactory.createShape("circle"); // Circle 생성
Shape shape2 = ShapeFactory.createShape("rectangle"); // Rectangle 생성
shape1.draw(); // "Drawing a Circle" 출력
shape2.draw(); // "Drawing a Rectangle" 출력
}
}
다형성 :
createShape()은 객체 생성은 new Circle()/new Rectangle()로 하지만, 객체 반환은 Shape 타입으로 하고 있다.
즉 정적 팩토리 메서드가 부모 타입의 자식 객체를 생성하고 있다.
- 하나의 메소드로 두 종류의 객체를 생성할 수 있다는 점에서 경제적이다.
정보 은닉 :
createShape()은 구현체의 생성자 new Circle()/new Rectangle()를 대신 호출해주고 있다.
즉 정적 팩토리 메서드가 구현체 생성자를 대신 호출해준다.
- 객체를 생성하는 클라이언트는 구현체의 정보를 알 필요가 없다.
- 클라이언트가 구현체에 더 이상 의존하지 않게 되므로, 구현체가 바뀌더라도 클라이언트쪽의 코드는 변경할 필요가 없어져 유지 보수가 쉬운 코드가 완성된다.
//인스턴스 메소드는 생성자 자체가 노출됨
new Circle();
//정적 팩토리 메소드는 new Circle();가 안에 은닉되어있음
Shape.createCircle();
참고
필드가 많고 null일 수도 있는 값이 있음 -> 빌더 패턴 사용
필드가 적고 모든 값을 파라미터로 입력받아야 함 -> 정적 팩토리 메서드 패턴 사용
빌더 패턴
@Getter
@Builder
public class AuthLoginResponse {
private MemberResponse memberResponse;
private String appToken;
private String refreshToken;
}
정적 팩토리 메서드
@Data
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AuthLoginResponse {
private MemberResponse memberResponse;
private String appToken;
private String refreshToken;
public static AuthLoginResponse of(
MemberResponse memberResponse,
String appToken,
String refreshToken
) {
return new AuthLoginResponse(memberResponse, appToken, refreshToken);
}
}
* references
'CS' 카테고리의 다른 글
| [디자인 패턴] 빌더패턴 (Builder Pattern) (0) | 2025.01.10 |
|---|---|
| [네트워크] Server-Sent Event (0) | 2025.01.05 |
| [운영체제] 동시성 이슈 해결 방법 - Semaphore/Mutex (8) | 2024.12.31 |
| [운영체제] 동시성 이슈란 - Race Condition/Data Race (1) | 2024.12.23 |
| [운영체제] 프로세스 - 메모리 공간/상태(state)/문맥 교환(context-switching)/pcb/프로세스 vs 스레드 (0) | 2024.12.20 |