Skip to content

Commit

Permalink
feat: EffectiveJavaItem4 - 인스턴스화를 막으려거든 private 생성자를 사용해라 정리
Browse files Browse the repository at this point in the history
  • Loading branch information
02ggang9 committed Oct 7, 2023
1 parent 2005af7 commit 364accc
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 81 deletions.
81 changes: 0 additions & 81 deletions _posts/java/2023-10-02-EffectiveJavaItem2_1.md

This file was deleted.

95 changes: 95 additions & 0 deletions _posts/java/2023-10-06-EffectiveJavaItem3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
published: true
title: "Effective Java Item 3 - 생성자나 열거 타입으로 싱글턴임을 보증하라"
categories:
- Java
---

생성자를 private로 만들고 public static final 필드로 인스턴스를 만들어높음

오퍼레이션 자체가 외부 API 거쳐서 호출하는 것이 일수도 있고 연산이 오래 걸리는 경우 굉장히 비효율적이다. 만약 인터페이스가 있다면 가짜 대역을 만들 수 있음.
-> 근데 모킹하면 되는거 아닌가? 좀 더 찾아봐야 겠다.

간결함이 장점이다.

---
리플랙션 -> 싱글턴이 깨짐
getDeclaredConstructor -> 선언되어 있는 기본생성자라서 접근 지시지와 상관없이 private 생성자도 접근 가능함.
setAccessible(true)로 설정을 하지 않으면 private여서 생성자를 호출할 수 없게 된다.

getConstructor -> 그냥 public 한 생성자에만 접근할 수 있음

~~~java
private static boolean created;

private Elvis() {
if (created) {
throw new UnsupportedOperationException("can't be created by constructor");
}
}

~~~

static도 다시 한번 공부 ㄱㄱ

그래서 리플렉션 쓰면 싱글턴이 깨지고 private 생성자로 막아야 함. 그런데 이렇게 하면 첫번째 장점이였던 간결함이 사라짐.

두 번째 문제는 직렬화, 역직렬화 하면 여러 인스턴스가 생성된다.

직렬화 역직렬화를 쓰려면 Serializable을 구현해야 함.

그리고 직렬화 할 때 생성자가 사용이 된다? -> 이건 더 찾아봐야 할 듯.

readResolve() -> 더 찾아봐야 할듯. 오버라이딩 아닌 오버라이딩 같은 메서드??
readResolve()를 재정의 해서 기존에 있던 인스턴스를 반환하도록 만드는 것이다.

---

~~~java
public static final INSTANCE = new Elvis();
private static final INSTANCE = new Elvis();

public static Elvis getInstance() {
return INSTANCE;
}
~~~

단점은 테스트, 리플렉션, 직렬화역직렬화 똑같은 문제를 가지고 있음. 해결책은 전 단계에서 다 해결했음.

장점은 클라이언트 코드가 getInstace()를 계속 쓰면서 행위, 동작을 바꿀 수 있음.

클라이언트 코드는 바뀌지 않고 새로운 인스턴스를 만들도록 할 수 있음. 클라이언트 코드를 덜 건든다.

~~~java
public static Elvis getInstance() {
rturn new Elvis();
}
~~~

정적 팩터리를 제네릭 싱글턴 팩토리로 만들 수 있다.
인스턴스는 같지만 타입은 다르게 쓸 수 있음(원하는 타입으로 형 변환을 할 수 있다) equals를 사용하면 두 비교가 같음. 그런데 == 연산자를 사용하면 타입이 다르기 때문에 false가 뜬다.

래퍼런스가 같고 같은 객체임에도 불구하고 (해쉬코드도 같음) 그럼에도 == 비교는 할 수 없다.

Q. 여기에는 왜 T가 한번 더 들어갔을까요? -> 이걸 잘 설명해야 제네릭을 이해한거임.
비록 T 라는 타입 이름은 같지만 스콥이 다름 static 스콥.
~~~java
public static <T> MetaElvis<T> getInstance() {
return (MetaElvis<T>) INSTANCE;
}
~~~

장점 3. 메서드 참조를 공급자로 사용할 수 있다.

---

열거 타입으로 싱글턴임을 보장하라.

생성자를 private로 만들 필요도 없고 public, static, field를 정의안해도 됨.

리플랙션, 직렬화, 역직렬화에 굉장히 안정적이다.

enum은 태생, 근본적으로 new 이렇게 만들 수 없게 함. 리플렉션 API를 통해서 enum에 컨스럭터를 가져오려 해도 getConstructor 이렇게 가져오려고 해도 내부적으로 막아놓음.

---
인스턴스 메서드 vs 스테릭 메서드
73 changes: 73 additions & 0 deletions _posts/java/2023-10-07-EffectiveJavaItem4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
published: true
title: "Effective Java Item 4 - 인스턴스화를 막으려거든 private 생성자를 사용하라"
categories:
- Java
---



## Abstract를 쓰면 안되는 이유
상속받은 클래스로 인스턴스를 생성할 수 있고 잘못된 의도를 전달할 수 있습니다.

아래는 Spring이 제공하는 유틸리티 클래스입니다. 추상클래스로 만들었지만 아래처럼 상속을 받으면 인스턴스화가 가능합니다.

~~~java
// Spring Util Class
public abstract class PatternMatchUtils {
public static boolean simpleMatch(@Nullable String[] patterns, String str) {
if (patterns != null) {
for (String pattern : patterns) {
if (simpleMatch(pattern, str)) {
return true;
}
}
}
return false;
}
}

public class DefaultUtilityClass extends PatternMatchUtils {
public static void main(String[] args) {
DefaultUtilityClass utilityClass = new DefaultUtilityClass();
utilityClass.simpleMatch("test", "test");
}
}
~~~

두번째로 추상클래스로 만들면 '상속을 받으라는 의도인가?' 라는 생각이 들 수 있습니다. 따라서 위처럼 상속을 받은 후 인스턴스를 생성할 수 있는 위험이 있습니다.


## private 생성자를 사용하는 방법

위의 두가지 문제를 해결하기 위해서는 private 생성자와 AssertionError()을 사용해야 합니다. private 생성자를 사용해서 외부에서 인스턴스를 만드는 행위를 막고 AssertionError()를 사용해서 내부에서 인스턴스를 만드는 행위를 막을 수 있습니다. 주석으로 설명을 더해준다면 더욱 좋을 것 같습니다.

~~~java
public class UtilityClass {
// 인스턴스를 만들면 안됩니다.
private UtilityClass() {
throw new AssertionError();
}
}
~~~


왜하면 상속 받으면 뚫림.

abstract를 써서 상속을 해서 써야하나? 라는 착각을 만들 수 있는 여지가 있다.

그래서 private로 생성자를 막아두자!

super에 있는 생성자를 더 이상 호출할 수 없으니까 상속 자체가 막히게 된다.

throw new AssertionError(); 를 써서 막아야 함. -> 예외를 처리하기 위한게 아니라 만나면 안되는 상황임. 혹시라도 이 상황을 만난다면 무조건 에러다! 이 생성자는 쓰이면 안된다. 라는 메시지임.

단점 -> 몼쓰게 만들려고 왜 코드를 만들지?

유틸리티 클래스??

스프링에서 유틸리티 클래스를 abstract로 해놓았는데 이러면 생성자가 뚫릴 수 있음.




0 comments on commit 364accc

Please sign in to comment.