티스토리 뷰

반응형

static factory method
정적 팩토리 메서드 (Static Factory Method)

요새 고민인 부분이 있습니다. 한 번 정적 팩토리 메서드에 사용법에 대해 알고 나서부터는 만들고자 하는 대부분의 객체의 의존성을 배제하고 직접적인 Setter를 금지하여 객체를 만들고자 하는 욕심이 생겼습니다. 그래서 오늘은 지금까지 공부한 방법에 대해 연구하고 객체를 만드는 과정을 알아보고자 합니다.

 

0. `Builder` 패턴을 잘 사용하기, 그리고 생성자는 private로 하여 불변 및 상속 불능으로 만들기

정적 팩토리 메소드를 사용하는 이유 중에 하나는 인스턴스의 프로퍼티 변조를 막고 파라미터를 통해 클래스 인스턴스를 만들기 위함입니다. 그래서 생성자는 private로 접근 제어자를 설정하여 클래스 내부에서만 조립이 가능하도록 막습니다. 

또, private 으로 선언이 된 이상 상속이 불가능합니다. 왜냐면 상속에는 protected 이상의 접근제어자가 필요하기 때문입니다.

그래서 코드를 짠다면 아래처럼 짜줍니다. Lombok을 사용하면 좀 더 쉽습니다.

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
public class DetailRes {

	
}

 


 

1. 시작부터 정리

사실 꼭 이런 방법으로 사용하라는 가이드는 없지만 개발자들끼리 상호작용하기에는 괜찮은 규칙입니다. 아래의 내용이 꽤나 길어 필요하신 분들이 보고 가실 수 있도록 정리로 시작해 보았습니다.

* of: 집계용으로 여러 개의 매개변수를 받아서 사용

Details.of(A a, B b, C c); 
Details.of(Integer integer, String... strings);

* from : 단일 매개변수 받아서 사용

Details details = Details.from(Entity e);

* getType: 다른 타입(Type)의 인스턴스 생성(=newType과 비슷)

OutputStream os = OutputStreamParser.getOutputStream(is);
OutputStream os = OutputStreamUtils.newOutputStream();

* create: 파라미터를 여러 개 받아 새 인스턴스 생성(=newInstance와 비슷)

Entity entity = Entity.create(String name, Integer age);
Entity entity = Entity.newInstance();

 

 

2. `of` 메서드는 적절히 남발하기

보통 매개변수를 받아서 원하는 타입의 인스턴스를 만드는데 자주 사용하는 메서드의 포맷입니다. 기본적인 of 메서드는 아래처럼 표현할 수 있습니다. 여러 매개변수를 받아도 되지만 하나를 받아도 사용하는 데는 문제없습니다. 

public class DetailRes {

    private Long id;
    private String name;
    private String text;
    private String font;

    public static DetailRes of(DetailEntity entity) {
        return DetailRes.builder()
                        .id(entity.getId())
                        .name(entity.getName())
                        .text(entity.getTextColumn())
                        .font(entity.getFontSize())
                        .build();
    }
    

	public static DetailRes of(DetailEntity entity, DetailChildEntity child) {
    	return DetailRes.builder()
                        .id(entity.getId())
                        .name(entity.getName())
                        .text(child.getTextColumn())
                        .font(child.getFontSize())
                        .build();
    }
}

a. valueOf

String 또는 Integer와 같은 Wrapper 클래스를 사용하시다 보면 많이 보셨을 문법입니다. 보통은 해당 인스턴스를 그대로 반환하는 데 사용합니다. 

@Getter
public class NullSafe {
    private String string;
    private Integer integer;
    public static NullSafe valueOf(String string) {
        return new Detail(string);
    }
    public static Detail valueOf(Integer integer) {
        return new NullSafe(integer);
    }
    
    private NullSafe(String string) {
        this.string = isBlank(string) ? "" : string;
    }
    
    private NullSafe(Integer integer) {
        this.integer = integer == null ? 0 : integer;
    }
    
    private boolean isBlank(String string) {
        if(string == null) return true;
        if(string.length() == 0) return true;
        if(string.trim().equals("")) return true;
        return false;
    }
}

위 클래스는 nullsafe를 체크하는 클래스입니다. 아래처럼 사용하면 되겠습니다.

NullSafe ns = NullSafe.valueOf("abcd");
ns.getString(); // "abcd"

NullSafe nsNumber = NullSafe.valueOf(null);
nsNumber.getInt(); // 0

b. instanceOf

위의 of 메서드와 크게 다르지 않은 메서드입니다. Java 내에서는 `instanceof` 로 타입을 비교하는 메서드가 있어서 종종 헷갈리지만 of 메소드가 많을 때 사용하면 유용합니다. 

 


 

3. `from`은 객체의 원형이 나올 때 사용하기

of를 사용하여 인스턴스를 만들어도 되지만, 제가 사용하는 from 객체는 매개변수가 하나이지만 그 객체가 가지고 있는 인스턴스변수가 모두 setting 되었을 때 주로 사용합니다. 예를 들어, 아래처럼 인스턴스 변수가 있을 때 이 클래스는 모든 프로퍼티를 가지게 됩니다.

public class NullSafe {
    private Long longValue;
    private Integer intValue;
    private String stringValue;
    
    public static NullSafe from(String string) {
        return NullSafe.builder()
                       .longValue(getNum(string))
                       .intValue(getNum(string).intValue())
                       .stringValue(string)
                       .build();
    }
    
    private static Long getNum(String str) {
        try { 
            return Long.parseLong(str);
        } catch(NumberFormatException e) {
            return 0L;
        }
    }
}

 


 

4. `toInterface` 메소드 활용하기

to로 시작하는 정적 팩토리 메서드는 보통 타입을 변환할 때 많이 사용합니다. 주로 List 또는 Collection 형태를 반환하는데 많이 사용합니다. 

public class Detail {
	public static List<Detail> toList(Entity e) {
         return e.getChildren().stream()
                               .filter(Detail::isMyChild)
                               .map(Detail::of)
                               .toList();
    }
}

public List<Detail> getListByEntity(Long id) {
    Entity e = entityRepository.findById(id);
    List<Detail> list = Detail.toList(e);
    return list;
}

 


 

5. 싱글톤 객체는`getInstance()`, 새 객체는 `create()` 활용하기 

보통 싱글톤 객체라고 하면 기존에 생성된 인스턴스를 재사용하기 위함입니다. 그래서 static 영역에 보관해 두고 계속 꺼내서 사용하여도 그 값은 고유하기 때문에 여러 클래스에서 상수값처럼 사용이 가능합니다. 

public class Detail {
    private List<String> somethings;
    private Set<Integer> uniqueIds;
    private String key;
    private static Detail detail = null;
    private Detail() {
    	this.somethings = List.of("aaa", "hello", "abbo");
        this.uniqueIds = new HashSet<>();
        this.key = "abbo-tistory";
    }
    
    public static Detail getInstance() {
        if(detail == null) {
            return new Detail();
        }
        return detail;
    }
    
    
}

create는 객체를 파라미터를 받아 생성할 때 주로 사용하며 newInstance와도 유사합니다. getInstance와 차이가 있다면 매개변수가 있고 없고의 차이입니다.

a. create는 엔티티 클래스에, newInstance는 DTO 클래스에

create는 생성의 의미가 있기 때문에 없는 데이터를 만들기 위해 JPA Entity 클래스에 많이 사용합니다.

@Entity
@Getter
public class Entity {
     @Id @GeneratedValue(strategy = GenerateType.IDENTITY)
     private Long id;
     
     private String name;
     private String type;
     
     public static Entity create(String name, String type) {
         return Entity.builder()
                      .name(name)
                      .type(type)
                      .build();
     }
     
     @Builder
     private Entity(String name, String type) {
         this.name = name;
         this.type = type;
     }
}

반면에 newInstanceDTO 클래스에 사용하는 편입니다.

@Getter
public class DetailRes {
    ...
    public static DetailRes newInstance(Entity e) {
        return DetailRes.builder()
                        .entity(e)
                        .build();
    }
    
    @Builder
    private DetailRes(Entity e) {
        this.name = e.getName();
        this.type = e.getType();
    }
}

 


 

5. `get()` 메서드로 각기 다른 타입 또는 클래스 리턴하기

get으로 시작하는 메서드는 get 뒤에 오는 타입의 인스턴스를 얻기 위함입니다. 마치 클래스 변수에 getter를 붙인 것과 타입이 유사하지만, 파라미터를 받아서 변환하기 위함으로 사용하기도 합니다.

public class Parser {
    private File file;
    private Path path;
    
    public static File getFile(String filePath) {
        return new File(filePath);
    }
    
    public static Path getPath(String filePath) {
        return Paths.get(filePath);
    }
}


String path = "/Users/abbo/tistory.txt";
File file = Parser.getFile(path);
Path path = Parser.getPath(path);

 

6. 그 외 괜찮은 네이밍에 대하여

a. isVerb

참인지 거짓인지를 비교하기 위해 is로 시작하는 메서드입니다. 보통 파라미터를 1개받고 이게 옳고 그른지 동사형을 is 뒤에 붙입니다. 

b. 동사(has, contains, anyMatch 사용하기)

Collection인 경우 자주 사용하는 동사형 메서드 네이밍 규칙입니다.

public class Details {
    private List<Details> list;
    
    public static Details toList(Entity e) {
       return e.getChildren().stream().map(Details::of).toList();
    }
    
    public static boolean containsFilter(Entity e, String filter) {
        return toList(e).stream().anyMatch(detail -> detail.contains(filter));
    }
}

 

c. 간결하게 type만 쓰기

getType을 간결하게 type만 사용할 수도 있습니다. 

 

 

반응형
댓글
공지사항