객체를 생성하는 방법
public 생성자
public 클래스 이름 () {
}
정적 팩터리 메서드 (Boolean에서 사용하는 정적 펙터리 메서드)
- 디자인 패턴에서의 팩터리 메서드와 다르다.
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
생성자 말고 정적 팩터리 메서드를 썻을 때 장점과 단점을 알아보자
장점
1. 이름을 가질 수 있다.
- 이름을 통해서 반환될 객체의 특성을 쉽게 묘사할 수 있다.
생성자, 정적 팩터리 메서드 중에서 “값이 소수인 BigInteger를 반환한다.”라는 뜻을 명확하게 표현하는지 생각해보면 이름의 장점을 얻을 수 있다.
public BigInteger(int bitLength, int certainty, Random rnd)
public static BigInteger probablePrime(int bitLength, Random rnd)
- 우리가 메모리 주소를 사용하지 않고 변수를 통해서 메모리 주소에 접근하는 것을 구분할 수 있는 것처럼 이름은 엄청난 가독성을 제공한다.
생성자는 오버로딩의 한계가 있지만 정적 팩터리 메서드는 한계가 없다.
메서드 시그니처 (return 타입을 제외하고 메서드 이름과 파라미터의 타입)
생성자의 문제
- 이름과 금액이 필요한 조건
- 주소(보통 값타입으로 만들어서 사용하지만 여기서 String으로)와 금액이 필요한 조건
public class User {
private String name;
private String address;
private int money;
public User(String name, int money) { //1 (3)번이 추가되면 같이 컴파일 에러
this.name = name;
this.money = money;
}
public User(int money, String address) { //2
this.address = address;
this.money = money;
}
public User(String address, int money) { //3 메서드 시그니처 중복으로 컴파일 에러
this.address = address;
this.money = money;
}
}
- 매개변수들의 순서를 다르게 한 생성자를 새로 추가하는 식으로 사용하다 보면 API를 사용하는 개발자 입장에서 어떤 역활을 하는지 정확하게 기억하기 힘들다.
한 클래스에 시그니처가 같은 생성자가 여러 개 필요하면 생성자를 정적 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지어주자
- 메서드 이름이 다르기 때문에 오버로딩이 문제가 되지 않는다.
- 생성자를 private으로 처리해주지 않으면 협업시 생성자로도 객체를 생성할 수 있는거니까 정적 팩터리 메서드를 사용하겠다 라는 내용 전달하기 힘들다고 생각한다.
public class User {
private String name;
private String address;
private int money;
private User() {
}
public static User withNameAndMoney(String name, int money) {
User user = new User();
user.name = name;
user.money = money;
return user;
}
public static User withAddressAndMoney(String address, int money) {
User user = new User();
user.address = address;
user.money = money;
return user;
}
}
2. 호출될 때마다 인스턴스를 새로 생성하지 않을 수 있다.
Spring이 ApplicationContext에 Bean을 등록할 때를 생각한 간단한 예시
public class MyContext {
private static Map<String, Object> MY_CONTEXT_CACHE = new HashMap<>();
static {
MY_CONTEXT_CACHE.put("myService", new MyService());
MY_CONTEXT_CACHE.put("myRepository", new MyRepository());
}
public static Object getContext(String name) {
return MY_CONTEXT_CACHE.get(name);
}
public static void main(String[] args) {
MyService myService1 = (MyService) MyContext.getContext("myService");
MyService myService2 = (MyService) MyContext.getContext("myService");
System.out.println(myService1.equals(myService2)); //true
}
}
class MyService {
public MyService() {
System.out.println("hihi"); // MY_CONTEXT_CACHE가 초기화 될 때 한번만 호출
}
}
class MyRepository{}
- 불변 클래스는 인스턴스를 미리 만들어서 생성한 인스턴스를 캐싱하여 재사용할 수 있다.
- 생성 비용이 큰 객체가 자주 요청되는 상황이라면 성능을 올려줄 수 있다.
비슷한 item
- 싱글톤으로 불필요한 객체 생성을 피할 수 있다. (item3)
- 인스턴스화 불가능하게 만들 수 있다. (item4)
- 불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다. (item17)
- 열거 타입은 인스턴스가 하나만 만들어진다. (item34)
- 플라이웨이트 패턴과 비슷하다.
인스턴스 통제가 가능하다.
- 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철철하게 통제할 수 있다.
- 한번 생성되어진 인스턴스를 사용하도록 강제할 수 있다.
동시성 문제가 있겠지만 예시로 가져와 봤다.
- 인스턴스를 자기가 원하는 시점에는 생성하고 예외를 던지고 할 수 있을거 같다.
public class InstanceControl {
private static int count; // 기본값 0
private static MyInstance MY_INSTANCE = new MyInstance();
public static MyInstance getInstance() {
if (count++ % 5 != 0) {
return MY_INSTANCE;
}
// throw new IllegalArgumentException(); // 예외를 던질 수 있음
return null;
}
public static void main(String[] args) {
IntStream.rangeClosed(1, 8)
.forEach(i -> System.out.println(InstanceControl.getInstance()));
// null
// com.example.test.MyInstance@14dad5dc
// com.example.test.MyInstance@14dad5dc
// com.example.test.MyInstance@14dad5dc
// com.example.test.MyInstance@14dad5dc
// null
// com.example.test.MyInstance@14dad5dc
// com.example.test.MyInstance@14dad5dc
}
}
class MyInstance { }
3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
- 반환할 객체의 클래스를 자유롭게 선택할 수 있는 유연성을 제공한다.
interface Car {
}
class SuperCar implements Car {
public static Car newInstance() {
return new SuperCar();
}
}
인터페이스를 통해서 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.
java.util.Collection에서 정적 팩터리 메서드를 통해 45개의 유틸리티 구현체를 제공한다.
- 인터페이스로 45개의 구현 클래스를 공개하지 않기 때문에 API를 사용하는 프로그래머가 구현 클래스를 알아야하는 개수와 난이도가 줄어든다.
- 명시한 인터페이스대로 동작하는 객체를 얻을 것임을 알기 때문에 별도 문서를 찾아가며 구현클래스를 확인하지 않아도 된다.
4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
public interface MessageService {
/**
*
* @return 언어에 맞는 메시지를 반환한다.
*/
String getMessage();
}
class KoreanMessageService implements MessageService{
@Override
public String getMessage() {
return "안녕";
}
}
class EnglishMessageService implements MessageService{
@Override
public String getMessage() {
return "hi";
}
}
입력 매개변수 location 정보에 맞춰서 구현 클래스 객체를 반환
- 매개변수에 따라 다른 클래스 객체를 반환할 수 있다.
class MessageFactory {
public static MessageService from(String location) {
if ("ko".equals(location)) {
return new KoreanMessageService();
}
return new EnglishMessageService();
}
}
자바 8부터는 public static 메서드를 인터페이스 추가 가능하다.
public interface MessageService {
/**
* @return 언어에 맞는 메시지를 반환한다.
*/
public static String getMessage(String location) {
if ("ko".equals(location)) {
return "안녕";
}
return "hi";
};
}
- 굳이 Collecions라는 클래스를 만들지 않고도 인터페이스에 구현 가능하다.
- java 8 - 인터페이스는 public정적 멤버만 가능하다.
- java 9 - private 정적 메서드는 가능하지만 정적 멤버 클래스는 public만 가능하다.
- java.util.Collection 같은 유틸성 클래스들은 필요 없어진다.
EnumSet 클래스 (아이템36)가 생성자 없이 public static 메서드로 allOf(), of() 등을 제공하는데, 리턴하는 객체의 타입이 enum 타입의 개수(64개)에 따라 RegularEnumSet 또는 JumboEnumSet으로 달라진다.
- 클라이언트 입장에서 RegularEnumSet 또는 JumboEnumSet 의 존재를 모르기 때문에 RegularEnumSet 을 사용할 이점이 없어져 다음 릴리즈 때 이 클래스를 삭제하더라도 아무 문제가 없다.
- 클라이언트는 팩터리가 건네주는 객체가 어느 클래스의 인스턴스인지 알 수도 없고 알 필요도 없기 때문이다. (EnumSet 의 역활을 대신 수행해줄 수 있는 하위 클래스이기만 하면 된다.)
5. 정적 팩터리 메서드를 작성하는 시점에는 변환할 객체의 클래스가 존재하지 않아도 된다.
장점 3, 4와 비슷한 개념으로 인터페이스나 클래스가 만들어지는 시점에서 하위 타입의 클래스가 존재하지 않아도 나중에 만들 클래스가 기존의 인터페이스나 클래스를 상속 받으면 언제든지 의존성을 주입 받아서 사용가능하다.
- 핵심은 반환값이 인터페이스가 되며 정적 팩터리 메서드이 변경없이 구현체를 바꿔 끼울 수 있다.
public class RaceCar extends Car {
...
}
자바 6부터는 java.util.ServiceLoader라는 범용 서비스 제공자 프레임워크를 통해서 jar 파일을 바꿔끼는 것만으로 내가 원하는 적절한 구현체를 사용하도록 바꿀 수 있다.
public class Car {
// 상황에 따라 유연하게 반환될 인스턴스가 결정됨
public static Car getCar() {
Car car = new Car();
// 특정 텍스트 파일에서 Car 구현체의 FQCN(Full Qualified Class Name)을 가져온다
// FQCN에 해당하는 인스턴스 생성
// car가 FQCN에 해당하는 인스턴스를 가리키도록 한다
return car;
}
}
- getCar는 실행시점에 위 코드에 뭐가 적혀있냐에 따라서 다르게 작동한다.
ServiceLoader 사용법
서비스 제공자 프레임워크
서비스 제공자 프레임워크 또는 서비스 제공자 인터페이스 패턴이라 불리는 것은 개념적인 이야기다.
- 다양한 구현 방법과 변형이 존재할 수 있다.
- 목적이 중요한 것 이지 구현 형태가 중요한게 아니다.
- 목적은 확장 가능한 애플리케이션 을 만드는 방법을 제공하는 것이다.
- 확장이 가능하다는 건 코드는 그대로 유지되면서 외적인 요인을 변경했을 때 애플리케이션의 동작을 다르게 동작할 수 있게 만들 수 있는 것 을 말한다.
서비스 제공자 프레임워크의 핵심 컴포넌트 3개
- 서비스 인터페이스(service interface): 구현체의 동작을 정의한다.
- 제공자 등록 API(provider registration API): 제공자가 구현체를 등록할 때 사용한다.
- 서비스 접근 API(service access API): 클라이언트가 서비스의 인스턴스를 얻을 때 사용한다.
- 원하는 구현체의 조건을 명시할 수 있다. (조건을 명시하지 않으면 기본 구현체를 반환하거나 지원하는 구현체들을 하나씩 돌아가며 반환한다.) - 유연한 정적 팩터리의 실체
종종 4번 째 컴포넌트가 쓰이기도 한다.
- 서비스 제공자 인터페이스(service provider interface): 서비스 인터페이스의 인스턴스를 생성하는 팩터리 객체를 설명해준다.
Spring Boot 느낌으로 예제를 만들어 보았다.
- 서비스 제공자 프레임워크의 관점에서본 각각의 역할
서비스 제공자 인터페이스 (SPI)와 서비스 제공자 (서비스 구현체)
- 어떤 서비스를 확장 가능하게 만들 것이냐 를 서비스 제공자 인터페이스라고 부른다
public interface ServiceInterface {
}
class Service1 implements ServiceInterface {
}
class Service2 implements ServiceInterface {
public static String ofLocation(String location) {
if ("ko".equals(location)) {
return "안녕하세요";
}
return "hello";
}
}
서비스 제공자 등록 API (서비스 인터페이스의 구현체를 등록하는 방법)
- 서비스 구현체를 등록하는 방법을 제공한다.
public class ProviderRegistrationConfig {
public List<ServiceInterface> getServiceInstance() {
return List.of(
new Service1(),
new Service2()
);
}
}
서비스 접근 API (서비스의 클라이언트가 서비스 인터페이스의 인스턴스를 가져올 때 사용하는 API)
- 서비스 접근 API는 등록된 서비스를 가져오는 방법 이다.
public class Main {
public static void main(String[] args) throws Exception {
MyApplicationContext context = new MyApplicationContext(ProviderRegistration.class);
Service2 service2 = (Service2) context.getServiceBean(Service2.class);
System.out.println(service2.ofLocation("ko"));
}
}
SpringBoot를 실제로 사용하지는 않고 예제가 돌아가게 간단하게 만들어서 사용했다.
public class MyApplicationContext {
private ProviderRegistration registration;
public MyApplicationContext(Class<?> clazz) throws InstantiationException, IllegalAccessException {
this.registration = (ProviderRegistration) clazz.newInstance();
}
public ServiceInterface getServiceBean(Class<?> clazz) throws ClassNotFoundException {
List<ServiceInterface> serviceInstance = registration.getServiceInstance();
return serviceInstance.stream()
.filter(clazz::isInstance)
.findFirst()
.orElseThrow(ClassNotFoundException::new);
}
}
대표적인 서비스 제공자 프레임워크 중 하나인 JDBC(Java Database Connectivity)
JDBC는 자바 6 전에 등장한 개념이라 ServiceLoader를 사용하지 않는다.
- 서비스 인터페이스 역할: Connection
- 제공자 등록 API 역할: DriverManager.registerDriver
- 서비스 접근 API 역할: DriverManager.getConnection
- 서비스 제공자 인터페이스 역할: Driver
단점
1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
- java.util.Collections는 상속할 수 없다.
우회해서 사용하는 방법
- collections에게 위임하면서 확장할 수 있다.
정적 팩토리를 제공하면서 생성자를 제공
- Arrays.asList()를 통해서 ArrayList를 생성할 수 있고 생성자를 통해서 넣어줄 수 있다.
상속보다 컴포지션을 사용(아이템18) 도록 유도하고 불변 타입(아이템17)로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점이 될 수도 있기는 하다.
2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
생성자처럼 API 설명에 명확히 드러나지 않으니 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다.
- 문서 정리에 좀 더 신경을 써서 사용자가 한번에 찾아볼 수 있도록 작성해야한다.
- 헷갈리지 않도록 일종의 명명규칙을 사용하도록 하자.
컨벤션
- from: 매개 변수 하나를 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
Date d = Date.from(instant);
- of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
Set faceCards = EnumSet.of(JACK, QUEEN, KING);
- valueOf: from 과 of의 더 자세한 버전
BigInteger prime = BinInteger.valueOf(Integer.MAX_VALUE);
- instance 혹은 getInstance: (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
StackWalker luke = StackWalker.getInstance(options);
- create 혹은 newInstance: instance 혹은 getInstance 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
Object newArray = Array.newInstance(classObject, arrayLen);
- getType: getInstance 와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 떄 쓴다. "Type" 은 팩터리 메서드가 반환할 객체의 타입이다.
FileStore fs = Files.getFileStore(path);
- newType: newInstance 와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 떄 쓴다. "Type" 은 팩터리 메서드가 반환할 객체의 타입이다.
BufferedReader br = Files.newBufferedReader(path);
- type: getType과 newType의 간결한 버전
List litany = Collections.list(legacyLitany)
'책 > 이펙티브 자바' 카테고리의 다른 글
item6 - 불필요한 객체 생성을 피하라 (0) | 2024.02.25 |
---|---|
item3 - private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2024.02.18 |
item2-생성자에 매개변수가 많다면 빌더를 고려하자 (0) | 2024.02.16 |