안녕하세요. 린내입니다~!
저번 게시글에선 주로 인터페이스와 추상클래스에 대해 알아보았습니다.
이번 게시글은 정적 클래스에 대해서 중점적으로 알아볼까요?
사실 자바에서는 정적 클래스가 따로 존재하진 않습니다.
자바에서 이너(혹은 중첩) 클래스가 아닌 일반 클래스는 static 키워드가 허용되지 않기 때문입니다.
통상적으로 정적 멤버(정적 필드, 정적 메소드)만 갖고 있는 클래스를 정적 클래스로 지칭하고 있습니다.
// static class StaticExample { 일반 클래스에 static 키워드 사용 불가
class StaticExample {
// 정적 멤버만 선언
}
정적 클래스의 멤버는 클래스 초기화 시 메서드 영역(클래스 영역)에 저장되어 프로그램이 종료될 때까지 유지됩니다.
따라서 프로그램 내에서 1. 전역적으로 사용할 수 있으며 2. 유일성을 보장받을 수 있다는 특징이 있습니다.
듣기만 해선 장점이 많은 클래스인 거 같은데요, 왜 정적 클래스를 악마라고 표현하는 주장까지 존재하는 걸까요?
결론적으로 말해서 정적 클래스는 객체지향사상과 거리가 멀기 때문입니다.
객체지향이 아니기 때문에 객체지향의 장점인 캡슐화, 추상화, 상속, 다형성을 활용할 수가 없습니다.
또한 캡슐화를 할 수 없기 때문에 상태가 없고 절차만 있다고 볼 수 있습니다.
- 포인트 1. 객체지향의 기술적 기능을 사용할 수 없다.
- 객체지향의 장점인 캡슐화, 추상화, 상속, 다형성을 활용할 수가 없다.
- 포인트 2. 상태가 없고 절차만 있다.
- 캡슐화가 없으므로 상태가 없다.
- 입력 → 처리 → 출력의 절차만 있다
- 데이터가 왔고, 데이터를 처리했고, 데이터를 출력했다는 절차만 존재할 뿐이다..
- 따라서, 인간의 관점으로는 이해가 어렵고 수정이 어려운 코드가 늘어나기 쉽다..
그렇다면 정적 클래스는 무엇이라고 이해해볼 수 있을까요?
인간이 인식하기에는 어렵지만 어쨌든 실세계에 존재하고 있고,
세계 어디에서든 존재하며 유일하게 존재하는 것이 무엇이 있을까요?
제 개인적인 생각이지만
정적 클래스를 중력으로 표현해볼 수 있지 않을까요?
public final class Gravity { //중력을 무엇으로 상속할까... 추상화, 상속, 다형성 불가.. !
// 중력 상수 - 중력의 성질을 나타내지만, 어떤 대상의 순간적인 상태 나타내는 것이 아님!
static final double GRAVITY_CONSTANT = 9.8; // 상수만 있을 뿐, 상태 없음!
// 대상의 무게 구하기
static double getWeight(Object object){
return object.mess * GRAVITY_CONSTANT;
}
}
우리가 살아가는 세계라고 볼 수 있는 지구에 한해서 생각을 해보면
중력은 어디서든 존재하며 유일하게 존재합니다.
중력 상수라는 final 값을 갖고 있으며 중력을 통해 대상의 무게를 구할 수도 있습니다.
위의 중력 정적 클래스를 사용하여 사과와 바나나 중 어떤 것이 더 무거운지 알아보는 코드를 작성해볼까요?
public class WeightService { // 서비스 구현
public void getHeavierObjectByGravity(){ // 중력을 계산하여 더 무거운 물체 가져오기
Object apple = new Apple();
Object banana = new Banana();
Object heavierObject;
// 사과와 바나나의 무게가 같지 않을 때
// 절차지향의 세계
// 입력
double weight1 = Gravity.getWeight(apple); // 정적 클래스 사용
double weight2 = Gravity.getWeight(banana); // 정적 클래스 사용
double heavierWeight;
// 처리
if(weight1 > weight2){
heavierWeight = weight1;
}else{
heavierWeight = weight2;
}
// 출력
if(Gravity.getWeight(apple) == heavierWeight){
heavierObject = apple;
}else{
heavierObject = banana;
}
System.out.println(heavierObject + "가 더 무거움");
}
}
- 정적 클래스를 사용하여 더 무거운 물체를 가져오는 로직을 구현해보았습니다.
- 객체지향이 아니므로 캡슐화, 추상화, 상속, 다형성을 활용할 수가 없었습니다.
- 캡슐화된 필드가 없으므로 상태 또한 갖고 있을 수 없었습니다.
- 입력 → 처리 → 출력의 절차만 있었습니다.
- 위처럼 절차와 데이터만을 다루는 세상을 인간은 쉽게 인식할 수 없습니다. (유연한 개발이 힘들어집니다)
- 아직까지는 정적 클래스를 되도록 지양해야하는지에 대한 이유가 확실히 와닿지 않습니다..
- 객체지향 클래스를 이용하여 무게를 구하는 코드를 구현해본 후 비교해보면 어떨까요?
- 객체지향적으로 무게를 구하기 위해서 무게를 구하는 객체인 “저울“이라는 클래스를 생성해보겠습니다.
- 먼저 저울이라는 인터페이스를 선언하고 양팔저울이라는 클래스가 이를 구현하는 방식으로 설계해보겠습니다.
- 객체지향의 장점을 더욱 살릴 수 있는 예시를 작성하기 위해 스프링 프레임워크를 사용한 예시코드를 보여드리겠습니다.
// 저울 인터페이스
public interface Scale {
void putObjectsOnScale(Object left, Object right);
Object getObjectOnScaleDown();
}
// 양팔저울 클래스
public class BalanceScale implements Scale{ //추상화, 상속, 다형성 있음.
Object left; // 캡슐화 -> 상태 있음
Object right; // 특정 대상의 가변적인 순간적인 상태를 갖고 있을 수 있다..
public void putObjectsOnScale(Object left, Object right){
this.left = left;
this.right = right;
}
public Object getObjectOnScaleDown(){
//..
}
}
@Configuration
public class BeanConfig {
@Bean
public Scale scale(){
return new BalanceScale(); //Scale 주입 시 BalanceScale을 주입!
}
}
@Service
@RequiredArgsConstructor
public class WeightService { // 서비스 구현
public final Scale scale; //의존성주입
public void getHeavierObjectByScale(){
Object apple = new Apple();
Object banana = new Banana();
// 객체지향의 세계
scale.putObjectsOnScale(apple, banana); // 저울에 물체를 올려둔다. (상태)
Object heavierObject = scale.getObjectOnScaleDown(); // 더 무거워서 내려간 쪽의 물체 가져오기 (행위)
System.out.println(heavierObject + "가 더 무거움");
}
}
- 저울이라는 객체를 통하여 무게를 구하는 코드를 구현해보았습니다.
- 정적 클래스를 통한 무게 구하기랑 차이점이 느껴지시나요?
- 객체지향의 세계에선 입력-처리-출력의 절차 대신,
- 객체의 상태와 객체의 행위가 존재합니다.
- 사람이 실세게에서 물체의 무게를 중력 계산식을 통해서 구하는 일은 드뭅니다.
- 저울에 올려두고 내려간 물체가 더 무겁다고 인식하는 실세계의 방식(일상의 방식)을 코드로 구현한 것입니다.
- 아직까진 객체지향의 장점을 잘 모르시겠다고요? 그렇게 느끼실 수도 있습니다.
- 아직 단순한 예시코드에 불과하니까요.
- 그렇다면 프로그램에 대한 요구조건이 갑자기 바뀌었다고 가정하면 어떻게 될까요 ??
“지구에서 무게를 재지 말고 화성에서의 무게를 재어주세요~“
- 이런,, 우리는 서비스를 불변의 법칙이 통한다고 생각하고 정적 클래스를 활용하여 구현하였습니다.
- 정적 클래스를 활용한 소스코드를 변경하려면 소스코드 자체에 대한 수정이 필요합니다.
public class WeightService { // 서비스 구현
public void getHeavierObjectByGravity(){ // 중력을 계산하여 더 무거운 물체 가져오기
Object apple = new Apple();
Object banana = new Banana();
Object heavierObject;
// 사과와 바나나의 무게가 같지 않을 때
// 절차지향의 세계
// 입력
//double weight1 = Gravity.getWeight(apple); // 지워진 코드
double weight1 = GravityOnMars.getWeight(apple); // 새로운(수정된) 정적 클래스 사용
//double weight2 = Gravity.getWeight(banana); // 지워진 코드
double weight2 = GravityOnMars.getWeight(apple); // 새로운(수정된) 정적 클래스 사용
double heavierWeight;
// 처리
if(weight1 > weight2){ // 현 상황은 아니지만 추가적인 비즈니스 로직이 연쇄적으로 변경되어야 한다면..?
heavierWeight = weight1;
}else{
heavierWeight = weight2;
}
// 출력
//if(Gravity.getWeight(apple) == heavierWeight){ // 지워진 코드
if(GravityOnMars.getWeight(apple) == heavierWeight){ // 새로운(수정된) 정적 클래스 사용
heavierObject = apple;
}else{
heavierObject = banana;
}
System.out.println(heavierObject + "가 더 무거움");
}
}
- 정적 클래스를 사용한 서비스 로직은 결합도가 너무 강해 소스코드에 대한 대대적인 수정이 필요합니다.
- 예시코드처럼 간단한 예시가 아닌 애초에 복잡한 로직에대 중간 처리과정까지 수정이 필요한 경우 엄청난 수고가 필요하겠죠..?
- 그럼 객체지향을 사용한 예시코드에 화성의 무게를 재어달라는 요구사항을 적용해볼까요?
@Configuration
public class BeanConfig {
@Bean
public Scale scale(){
//return new BalanceScale(); // 지워진 코드
return new BalanceScaleOnMars(); //Scale 주입 시 BalanceScaleOnMars을 주입!
}
}
- 짠 ! 소스코드 한 줄로 수정이 완료되었습니다 !!
- 이럴수가.. 너무 간단하게 요구사항이 해결되었는데요?
- 심지어 핵심 비즈니스 로직인 WeightService의 소스코드는 한 줄도 변경하지 않았습니다.
- 이렇게 할 수 있었던 이유는 무엇일까요?
- 객체지향적으로 설계를 해두었기 때문에 캡슐화, 추상화, 상속, 다형성을 활용할 수 있었기 때문입니다!!
@Service
@RequiredArgsConstructor
public class WeightService { // 서비스 구현
public final Scale scale; //의존성주입 -> 다형성을 이용하여 scale에 BalanceScaleOnMars가 주입된다!
public void getHeavierObjectByScale(){
Object apple = new Apple();
Object banana = new Banana();
// 객체지향의 세계
scale.putObjectsOnScale(apple, banana); // 저울에 물체를 올려둔다. (상태)
Object heavierObject = scale.getObjectOnScaleDown(); // 더 무거워서 내려간 쪽의 물체 가져오기 (행위)
System.out.println(heavierObject + "가 더 무거움");
}
}
- 인터페이스를 이용한 객체지향.. 너무 선녀.. 아니 천사 같지 않나요..??
위의 예시들을 통해 정적 클래스의 단점과 객체지향 클래스의 장점을 확인해보았습니다.
- 그렇다면 정적 클래스는 사용하지 말아야할까요?
- 그건 어려울 것 같습니다. 왜냐하면 실세계엔 여전히 “중력“이라는 개념이 존재하기 때문이죠.
- 어떠한 상황에선 위에서 사용한 것처럼 “저울“객체를 사용하지 못하고 “중력“을 사용할 수밖에 없는 경우가 존재할 수 있습니다.
- 예를 들어, 우리는 수학적 계산을 할 때 여전히 java.lang.Math 클래스를 사용하는 것이 불가피할 수도 있습니다.
- 수학의 개념은 세계 어디에서든 존재하며 유일하게 존재하며 심지어 불변하기 때문입니다.
package java.lang;
public final class Math {
/**
* Don't let anyone instantiate this class.
*/
private Math() {}
//..
}
- 위에서 Math 정적 클래스는 final 로 선언되었으며 생성자는 private 제한자를 두고 있습니다.
- 이에 대한 자세한 이유는 이펙티브 자바 아이템 4 인스턴스화를 막으려거든 private 참고해주세요.
- “정적 유틸리티 클래스는 객체지향적으로 사고하지 않은 이들이 종종 남용하는 방식이다“
- 라고 지적하긴 했지만 나름의 쓰임새는 있다고 언급하고 계십니다.
- 생각해보면 당연합니다. 중력이든 수학이든 우리는 불변의 성질을 정의할순 있어도 존재화, 객체화 시킬 순 없기 때문입니다.
- 이에 대한 자세한 이유는 이펙티브 자바 아이템 4 인스턴스화를 막으려거든 private 참고해주세요.
- 또한 많이 볼 수 있는 정적 유틸리티 클래스의 예시를 보실까요?
package org.apache.commons.lang3;
public class StringUtils {
//..
/**
* {@link StringUtils} instances should NOT be constructed in
* standard programming. Instead, the class should be used as
* {@code StringUtils.trim(" foo ");}.
*
* <p>This constructor is public to permit tools that require a JavaBean
* instance to operate.</p>
*/
public StringUtils() {
}
}
- StringUtils 클래스는 정적 유틸리티 클래스임에도 불구하고 final 선언과 private 생성자 접근제한을 두지 않았습니다.
- 왜 일까요?
- 이유는 주석으로 이해해볼 수 있습니다.
- 이 생성자는 JavaBean 인스턴스가 작동해야 하는 도구를 허용하기 위해 공개됩니다.
- 비록 정적 유틸리티 클래스이지만 확장성에 대한 여지를 남기기 위해 public 생성자를 선언한 것을 알 수 있습니다.
- 그럼 우리는 맨 처음으로 돌아와서 ! 추상클래스에 대해 다시 생각해봅시다.
- 앞에서 본 여러 예시를 통해 이러한 주장은 의미가 없다는 걸 알 수 있습니다.
“정적 유틸리티 클래스를 추상클래스로 선언한 이유는 인스턴스화를 막기 위해서이다.”
인스턴스를 막기 위해선 더욱 확실한 방법을 사용하면 그만입니다 .
위의 Math의 예시에서 보았듯이 final 선언과 private 생성자를 사용하는게 최선입니다.
심지어 인스턴스화를 막기위해 추상클래스를 사용한다 하더라도,
생성자를 private으로 제한하지 않으면 상속을 통해서 인스턴스화를 시킬 수도 있습니다.
public abstract class AbsUtils {
//..
}
public class AbsUitilsChild extends AbsUtils{
//..
}
AbsUtils child = new AbsUitilsChild(); // 정적클래스의 무의미한 인스턴스화가 가능함...
우리는 그렇다면 추상클래스의 목적은 여전히 상속에 있다는 것을 알 수 있습니다.
- 추상클래스 → 상속을 위한 것
- 정적 (유틸리티) 클래스는 확장성이 부족해 단점이 많다.
- 따라서, 추상클래스를 정적 (유틸리티) 클래스로 사용하기
- → 추후 상속을 통해 정적 클래스에 대한 부족한 확장성을 보충할 여지를 남길 수 있음!
스프링 라이브러리 진영에서 최근 추상클래스로 정적 유틸리티 클래스를 선언하고 있는 이유는 위의 이유라고 생각해 볼 수 있겠습니다.
- https://stackoverflow.com/questions/70443894/why-are-almost-all-utility-classes-in-spring-declared-as-abstract-rather-than-fi
- 제가 가진 궁금증에 대해 같은 질문을 하신 분이 계시네요. 활발하게 논의되진 않았지만 답변을 하신 분도 확장성을 고려한 결과로 추측하고 계셨습니다
개인적인 생각으로는 "유틸리티 클래스를 사용하면 안된다" 혹은 "유틸클래스는 악마이다"
라는 주장까지는 좀 극단적인 표현이 아닐까 싶습니다.
정적 유틸리티 클래스를 주의하며 지양해야하는 이유는 아마도
위에서도 언급했던 이펙티브 자바의 저자 블로크 슈아 의 말처럼
"임시방편으로 남용되기 쉬운 것이 정적 유틸리티 클래스"이기 때문인 것 같습니다.
(가비지 컬렉터가 되기 쉽다는 말이겠죠..)
그럼,,
별다른 고민없이 사용하고 있었던 정적 유틸리티 클래스에 경종을 울리며...
아래와 같은 유의사항을 정리하는 것으로
이만 이번 포스팅을 마무리하겠습니다..!
- 현재 사용하는 기능이 정말로 객체지향으로 표현할 수 없는가를 고민한다
- 무심결에 사용하고 있는 기능들이 절차지향(혹은 데이터 지향)이지 않는지 의심하면서 개발한다.
- 객체지향적으로 작성할 수 있음에도 불구하고 관성적으로 유틸리티 클래스로 생성 혹은 사용되는 것들을 주의한다.
- 출처
https://youtu.be/85B3FV_sB60
https://medium.com/@sahoosunilkumar/utils-class-good-and-or-bad-3f0d867ffc39
https://javabypatel.blogspot.com/2017/07/real-time-example-of-abstract-class-and-interface-in-java.html
https://limdingdong.tistory.com/19
https://hamryt.tistory.com/2
https://mangkyu.tistory.com/227
https://da-nyee.github.io/posts/woowacourse-is-the-singleton-pattern-object-oriented/
https://be-study-record.tistory.com/24
https://stackoverflow.com/questions/25223553/how-can-i-create-an-utility-class
https://stackoverflow.com/questions/32375149/is-it-mandatory-utility-class-should-be-final-and-private-constructor?rq=4
https://stackoverflow.com/questions/9618583/java-final-abstract-class?rq=4
https://www.youtube.com/watch?v=5oUdqn7WeP0
https://dev-junhee.tistory.com/16
'백엔드 > Java' 카테고리의 다른 글
동시성 이슈 처리 및 테스트 방안 - 2(CountDownLatch 활용) (6) | 2024.03.25 |
---|---|
동시성 이슈 처리 및 테스트 방안 - 1 (6) | 2024.02.26 |
천사와 악마 사이 추상클래스... - 인터페이스 및 정적 유틸리티 클래스 비교 - 1 (0) | 2024.02.01 |
이넘아 놀자 4 - 인터페이스 사용하여 Enum 공통화하기 (0) | 2023.09.03 |
이넘아 놀자 - 3. 코드값으로 되어있는 Enum의 경우 (0) | 2023.08.28 |