Closed lee-ji-hoon closed 9 months ago
간만에 테스트 코드까지 작성을 해보게 됐다.
책에 나와있는 Singleton 자바 코드를 그대로 Kotlin으로 바꾸기 보다는 싱글톤을 구현한다면 지금 내 지식으로는 어떻게 구현할까? 만 중점으로 두고 구현을 했다.
그 결과 object
, lazy
dobule-check
방식 3가지로 구현을 했다.
// SingletonSample은 테코 이쁘게 짜려고 만든 interface다.
object ObjectSample : SingletonSample {
override var count = 0
}
class LazyLoadingSingletonSampleSample private constructor() : SingletonSample {
override var count = 0
companion object {
private val lazyLoadingSingletonSample by lazy { LazyLoadingSingletonSampleSample() }
fun getInstance(): LazyLoadingSingletonSampleSample {
return lazyLoadingSingletonSample
}
}
}
class DoubleCheckSingletonSample private constructor() : SingletonSample {
override var count: Int = 0
companion object {
@Volatile
private var instance: DoubleCheckSingletonSample? = null
fun getInstance() = instance ?: synchronized(this) {
instance ?: DoubleCheckSingletonSample().also { instance = it }
}
}
}
셋 다 장단점이 있는데, object는 이미 익숙할테고, double-check는 책에서 나왔으니 lazy의 내부 코드를 살짝 보면 이런 특징이 있다.
mode
를 정하지 않으면 기본적으로 synchronized 되게 double-check을 하고 있는 모습이다.
그렇기에 우린 직접 double-check를 구현할 필요가 없다는 것을 알 수 있다.
정답은 없는거 같은데 생각해보면 이런 기준을 둘거 같다.
만약 내부에서 Hilt/Coin 같은 DI를 하게 될 경우 object는 불가능하지만 by lazy를 사용해서 만든 singleton의 경우 필드 주입이 가능하기 때문에 만약 주입이 필요한데 singleton을 만들어야 한다면 by lazy를 선택할 것 같다.
하지만 책에 나와있듯이 싱글톤 패턴의 단점은 클래스 간의 의존성을 감춘다. 내용도 있어서 지양해야 할 것 같긴한다.
CacheManager에 대한 직접적인 싱글톤 접근 대신, 이를 인터페이스로 추상화하고 생성자나 setter 등을 통해 주입받을 수 있도록 변경하는게 가장 좋아보인다.
public class Demo {
private final UserRepo userRepo; // Hilt 혹은 Kotin 사용해서 주입
private final CacheManager cacheManager; // Hilt 혹은 Kotin 사용해서 주입
public boolean validateCachedUser(long userId) {
User cachedUser = cacheManager.getUser(userId);
User actualUser = userRepo.getUser(userId);
// 주요 로직 생략
}
}
클래스 로더는 런타임 중에 JVM의 메소드 영역에 동적으로 Java 클래스를 로드하는 역할을 한다
https://docs.oracle.com/javase/specs/jls/se17/html/jls-12.html#jls-12.2 참고
Java에서 클래스 및 인터페이스 로딩은 특정 이름의 클래스나 인터페이스의 바이너리 형태를 찾고, 이를 Class
객체로 구성하는 과정이다.
ClassLoader
클래스와 그 서브 클래스에 의해 구현된다.Class
객체를 구성하는 데 사용된다.Class
객체를 반환해야 한다.Class
객체를 반환해야 한다.Java의 클래스 로딩 메커니즘은 시스템의 타입 안전성을 보장하며, 클래스 로더의 일관성과 정확한 로딩 과정을 통해 Java 프로그래밍 언어의 런타임 타입 시스템이 코드에 의해 침해될 수 없도록 설계가 되어 있다.
위와 같은 경우 어떻게 로그가 찍힐지 생각해보면 2가지 경우가 있다.
클래스 로더의 일관성 및 정확한 로딩이 보장되기 위해서는 2번으로 나와야 한다.
실제로 위와 같은 결과가 나왔으며, 클래스 로더는 진짜로 일관성을 보장한다는 것을 알 수 있다.
이거는 블로그 에서 너무 잘 설명해주고 있어서 이 내용을 읽어보면 좋을거 같은데 자바 기준이니까 직접 해보자.
- kotlinc Main.kt -include-runtime -d Main.jar / / 코틀린 소스파일을 컴파일해서 Java 바이트코드로 변환
- kotlin -classpath Main.jar MainKt // Jar 파일 실행
- java -verbose:class -classpath Main.jar MainKt // 클래스 로딩 정보 출력
요소 | 클래스 로드 여부 |
---|---|
const val - static final |
로드되지 않음 |
일반 val /var - static |
로드됨 |
일반 fun - static |
로드됨 |
@JvmStatic fun - static final |
로드됨 |
이런 결과가 나오는데 정리해보자면 아래와 같은 경우에 로딩이 되는 것으로 보인다.
여기서
@JvmStatic
은 decompile 해보면 static final 임에도 불구하고 로드가 되는데,@JvmStatic
의 경우 내부적으로 싱글톤 인스턴스를 통해 접근을 하는 것으로 기억을해서 클래스 로딩을 강제로 유도하게 되는거 같다.
즉 클래스가 실제로 로드(초기화) 하는 역할을 클래스 로더가 하는 것이기 때문에 유일성의 범위는 프로세스 보다 클래스 로더 라는 말이 확실히 맞아보인다.
class MyViewModel(
private val myRepository: MyRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// ViewModel logic
// Define ViewModel factory in a companion object
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val savedStateHandle = createSavedStateHandle()
val myRepository = (this[APPLICATION_KEY] as MyApplication).myRepository
MyViewModel(
myRepository = myRepository,
savedStateHandle = savedStateHandle
)
}
}
}
}
class MyActivity : AppCompatActivity() {
private val viewModel: MyViewModel by viewModels { MyViewModel.Factory }
}
우리가 사용하는 ViewModel을 관리해주는 ViewModelProvider
가 아주 보편적인 예시로 보인다. (이름 부터 Factory라고 아예 떡하니 명시 중)
이런 팩터리 패턴을 사용하는 이유는 우선 추상화와 유연성이 뛰어나서 선택을 하게 된거 같다. ViewModel은 여러 View에서 재사용될 수도 있는데, 팩토리를 사용해서 의존성을 주입해주기도 하며, 재사용성이 뛰어나다는 장점이 있어서 사용을 하게 되는거 같다.
createBean()
함수가 재귀적으로 ref 유형의 객체를 생성하는 과정에서 순환 참조가 존재하는 경우 스택 오버플로가 발생할 가능성이 있다.
순환 의존성이란, 객체 A가 객체 B를 필요로 하고, 동시에 객체 B가 직접적이거나 간접적으로 객체 A를 필요로 하는 상황을 의미
public class A {
private boolean isRef;
private Class<?> type;
private Object arg;
// Ref 타입일 때 사용할 생성자
public A(boolean isRef, Object arg) {
if (!isRef) {
throw new IllegalArgumentException("This constructor is for ref types only.");
}
this.isRef = isRef;
this.arg = arg;
this.type = null; // Ref 타입인 경우 type은 무시
}
// Non-ref 타입일 때 사용할 생성자
public A(boolean isRef, Class<?> type, Object arg) {
if (isRef) {
throw new IllegalArgumentException("This constructor is for non-ref types only.");
}
this.isRef = isRef;
this.type = type;
this.arg = arg;
}
}
class A private constructor(
val isRef: Boolean,
val type: Class<*>?,
val arg: Any?
) {
companion object {
// Ref 타입을 위한 팩토리 메서드
fun createRef(arg: Any) = A(isRef = true, type = null, arg = arg)
// Non-ref 타입을 위한 팩토리 메서드
fun createNonRef(type: Class<*>, arg: Any) = A(isRef = false, type = type, arg = arg)
}
}
이렇게 2개로 개선이 가능할거 같다.
java 에서는 생성자를 2개로 구현 / kotlin은 기본 생성자를 private으로 막고 2개의 생성자 함수로 관리
키워드의 삭제도 지원해야 한다면 어떻게 구현을 해야 할까?
수정할 때 제거 하고 삽입을 하는데 제거 까지만의 동작을 하나의 함수로 만들어서 관리하면 되지 않을까?
ShoppingCart 클래스의 getItems() 메서드를 통해 items 컬렉션을 얻으며, 컬렉션 내의 ShoppingCartItem 개별 객체의 데이터를 수정할 수 있기 때문에 이 설계에 문제가 있는데 이 문제를 어떻게 해결할 수 있을까?
public final class ShoppingCartItem {
private final String productId;
private final double price;
public ShoppingCartItem(String productId, double price) {
this.productId = productId;
this.price = price;
}
// Getter 메서드만 제공
public String getProductId() {
return productId;
}
public double getPrice() {
return price;
}
// 변경자 메서드는 제공하지 않음
}
public class ShoppingCart {
private List<ShoppingCartItem> items = new ArrayList<>();
public List<ShoppingCartItem> getItems() {
// 깊은 복사본을 만들어서 반환
return items.stream()
.map(item -> new ShoppingCartItem(item.getProductId(), item.getPrice()))
.collect(Collectors.toList());
}
// ShoppingCartItem 객체를 업데이트하는 메서드
public void updateItem(String productId, double newPrice) {
// ...
}
}
아마 CacheManager에 대한 이야기를 하는 것 같은데, UserRepo 처럼 의존성 주입을 하면 되지 않을까? 우리에겐 Hilt가 있으니까!
클래스 로더가 클래스에 있는 변수들을 초기화하고 메모리에 올리는거니까, 생성되는 객체들은 실제로는 클래스 로더의 범위 안에 있다고 말할 수 있을 것 같다.
1) Primitive 타입의 valueOf 가 단순 팩터리 패턴으로 구현이 되어 있다. 몰랐네…
2) 해당 객체를 만들기 위해서 팩터리 클래스 객체 없이도 생성할 수 있기 때문이다. 테스트에는.. 잘 모르겠는데…
BeanDefinition
속 args
중 BeanDefinition
에 의존성을 가지고 있는 argument가 있다면 계속 재귀가 일어나 오버플로가 발생할거여 흠 의존성 방향이 단방향이 될 수 있도록 설계해야겠지
마지막에 build()
메서드로 인스턴스를 만들 때, isRef
가 false일 때 type이 설정되었는지 유효성 검사를 해서 조건에 맞는지 검사하기? 근데 애초에 이걸 같은 Class로 했어야 했는지부터 의문인 요구사항인걸
ShoppingCartItem
의 setPrice 메서드를 제거하고 가격 속성을 불변으로 수정한다.
ShoppingCart
에 카트 속 아이템을 수정하는 메서드를 만들고, 프로토타입 패턴을 적용하면 되겠지 ShoppingCart.editItem(index: Int, item: ShoppingCartItem)
머 이렇게 만들어서 list의 index 번째 아이템을 수정할 ShoppingCartItem
인스턴스로 교체해버려~// 1. Provider 를 이용해서 객체 생성
CalendarProvider provider =
LocaleProviderAdapter.getAdapter(CalendarProvider.class, aLocale)
.getCalendarProvider();
if (provider != null) {
try {
return provider.getInstance(zone, aLocale);
} catch (IllegalArgumentException iae) {
// fall back to the default instantiation
}
}
Calendar cal = null;
// 2. Extension 을 이용한 객체 생성
if (aLocale.hasExtensions()) {
String caltype = aLocale.getUnicodeLocaleType("ca");
if (caltype != null) {
cal = switch (caltype) {
case "buddhist" -> new BuddhistCalendar(zone, aLocale);
case "japanese" -> new JapaneseImperialCalendar(zone, aLocale);
case "gregory" -> new GregorianCalendar(zone, aLocale);
default -> null;
};
}
}
// 3. locale 의 language 값을 이용한 객체 생성
if (cal == null) {
// If no known calendar type is explicitly specified,
// perform the traditional way to create a Calendar:
// create a BuddhistCalendar for th_TH locale,
// a JapaneseImperialCalendar for ja_JP_JP locale, or
// a GregorianCalendar for any other locales.
// NOTE: The language, country and variant strings are interned.
if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
cal = new BuddhistCalendar(zone, aLocale);
} else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja"
&& aLocale.getCountry() == "JP") {
cal = new JapaneseImperialCalendar(zone, aLocale);
} else {
cal = new GregorianCalendar(zone, aLocale);
}
}
return cal;
private static NumberFormat getInstance(
LocaleProviderAdapter adapter,
Locale locale, Style formatStyle,
int choice
) {
NumberFormatProvider provider = adapter.getNumberFormatProvider();
return switch (choice) {
case NUMBERSTYLE -> provider.getNumberInstance(locale);
case PERCENTSTYLE -> provider.getPercentInstance(locale);
case CURRENCYSTYLE -> provider.getCurrencyInstance(locale);
case INTEGERSTYLE -> provider.getIntegerInstance(locale);
case COMPACTSTYLE -> provider.getCompactNumberInstance(locale, formatStyle);
default -> null;
};
}
true이면 애초에 생성자에 type을 입력하지 않을테니까 생성자를 두개 쓰면 되지 않을까요?
6-1. 싱글턴 패턴 (1) 생각해보기
CacheManager가 싱글턴 패턴인데, 리팩터링 시 코드의 변경을 최소화하면서 테스트 용이성을 향상 시키는 방법이 무엇이 있을까?
6-2. 싱글턴 패턴 (2) 생각해보기
Java의 경우 엄밀히 말하면 싱글턴 패턴의 유일성의 범위는 프로세스 안이 아닌 클래스 로더 안에 있다. 왜 그런지 생각해보자.
6-3 팩터리 패턴 (1) 생각해보기
6-4 팩터리 패턴 (2) 생각해보기
BeansFactroy 클래스의 createBean() 함수는 재귀 함수인데, 이 함수의 매개변수가 ref 유형이면 ref 속성이 가리키는 객체를 재귀적으로 생성한다. 설정 파일에서 객체 사이의 의존성을 잘못 설정하여 의존성이 순환하게 된 경우, BeansFactory 클래스의 createBean() 함수에 스택 오버 플로가 발생할 가능성이 있는지 생각해보고, 가능성이 있다면 해결방법이 무엇일까
6-5 빌더 패턴 생각해보기
이런 상황에서 isRef가 true이면 arg를 설정해야 하지만, type은 설정할 필요가 없고, isRef가 false이면 arg와 type 모두 설정해야 하는데 이런 요구 사항에서 위 클래스를 어떻게 개선해야 할까?
6-6 프록시 패턴 생각해보기