데브코스 참여 2일차로 오늘 배운 내용을 정리해보도록 하겠습니다..! 👍
📌 TIL
1) Object 클래스의 주요 메서드들
모든 Java 객체의 부모 객체인 Object 클래스는 다양한 메서드를 갖고 있습니다. 그 중 주요 메서드로 equals(), hashCode(), toString() 등이 있는데요. 오늘은 이 세가지에 대해 학습했습니다. 각 메서드에 대해 알아보기 이전 "동일성"과 "동등성"에 대해 간단하게 알아보겠습니다.
동일성 | 동등성 | |
비교 | == | equals |
의미 | 객체의 메모리 내 주소값을 비교 | 논리적으로 같은 지위를 지녔는지 즉, 같은 값을 가지는지 |
[ equals 란? ]
기존의 equals 메서드는 기본적으로 2개의 객체가 동일한지 검사하기 위해 사용됩니다. equals가 구현된 방법은 2개의 객체가 참조하는 것이 동일한지를 확인하는 것이며, 이는 동일성(Identity)을 비교하는 것입니다. 결국 2개의 객체가 동일한 메모리 주소를 참조할 경우에만 동일한 객체가 됩니다.
public boolean equals(Object obj) {
return (this == obj);
}
Q. 하지만 제가 알기로 equals() 메서드는 오버라이딩을 통해 동등성(Equality)을 비교한다고 배웠는데요 ?
A. 맞습니다. 프로그래밍을 하다보면 객체가 서로 다른 메모리에 띄워져있는 경우가 있으며, 이는 동일한(Identity)객체가 아닙니다. 그러나 프로그래밍 상으로 같은 값을 지닌다면 동등한 객체로 인식이 되어야 하는데, 이러한 동등성(Equality)을 위해 equals() 메서드를 오버라이딩하여 주소값이 아닌 다른 기준으로 비교한 결과를 반환하도록 하는 것입니다.
@Override
public boolean equals(Object o) {
if (this == o) return true; // 만일 현 객체 this와 매개변수 객체가 같을 경우 true
if (!(o instanceof Person)) return false; // 만일 매개변수 객체가 Person 타입과 호환되지 않으면 false
Person person = (Person) o; // 만일 매개변수 객체가 Person 타입과 호환된다면 다운캐스팅(down casting) 진행
return Objects.equals(this.name, person.name); // this객체 이름과 매개변수 객체 이름이 같을경우 true, 다를 경우 false
}
[ hashCode() 란? ]
hashCode() 메서드는 실행 중 객체의 유일한 integer 값을 반환합니다. 만약 중복을 허용하지 않는 Set의 자료구조를 이용한다고 했을 때, Set은 Hash Table을 사용하는 자료형이기 때문에 어떤 데이터가 존재하는지 확인하기 위해 해싱 알고리즘을 사용하게 됩니다. 해싱된 결과를 주소값으로 찾아가 그곳에 같은 자료가 있는지를 확인하는 것이죠.
그 해싱 알고리즘에 사용되는 데이터가 바로 hashCode() 입니다. 따라서, 객체의 동등성을 위해 equals() 메서드를 오버라이딩 해야하는 경우, hashCode() 메서드도 오버라이딩 해줘야 합니다. 그래야 Hash Table을 사용하는 자료구조에서도 동등성을 보장할 수 있습니다.
🤔 그렇다면 역시, hashCode()도 동등성을 위해 오버라이딩 한다는 건데, 그럼 그냥 equals()만 써도 되지 않나요 ?
상대적으로 hashCode() 메서드가 equals() 메서드에 비해 가볍기 때문에 찾으려는 데이터의 범위를 대폭 줄여주고, 최종적으로 equals() 메서드를 통해 최종 값을 콕 찝어내는 것입니다.
ex) 바구니가 1번부터 5번까지 있고 물건은 10개씩 들어있다고 가정해볼게요. 바구니에서 사과를 찾는다고 할때 우리는 몇번 바구니에 들어있는지 부터 알고 그 바구니만 찾으면 훨씬 더 빠르고 효율적으로 찾을 수 있겠죠 ?
[ toString() 란 ? ]
toString() 메서드는 객체가 가지고 있는 정보나 값들을 문자열로 만들어 리턴하는 메서드입니다. 객체 뒤 + 문자열(String)을 수행하면 결과는 문자열(String)이 되는데요. 이는 객체가 문자열로 강제변환이 되기 때문이고 그 강제변환하는 방법 역시 toString() 메서드를 호출하기 때문입니다.
toString()은 객체의 이름이나 주소값이 아닌 객체의 고유 정보를 출력하고 싶을때 오버라이딩하여 사용하면 원하는 출력 결과를 얻을 수 있습니다.
2) Optional
Optional은 자바에서 비어있는 값을 의미하는 null을 처리하기 위해 사용되는 클래스로 NullPointerException(NPE)를 방지할 수 있도록 해줍니다. 즉, 예상치 못한 NPE예외를 제공되는 메소드를 통해 회피할 수 있어 복잡한 조건문 없이도 null 값으로 인해 발생하는 예외를 처리할 수 있습니다.
Java8 이전에는 null인 객체를 다룰 때, 자바에서 NPE가 발생하기 때문에 반환하는 결과가 null인지 매번 if문으로 체크했어야 했어요.
[ 생성법 ]
optional 객체를 생성할 때에는 of() 또는 ofNullable()을 사용합니다.
// 초기화, null을 직접 사용하기 보단 (위험부담) Optional.empty()를 통해 초기화하자.
Optional<String> initNo = null;
Optional<String> initOk = Optional.empty();
String s = "zerozae";
Optional<String> s1 = Optional.of(s);
Optional<String> s2 = Optional.of("Park0Jae");
참조하는 변수의 값이 null일 가능성이 존재한다면, of() 대신 ofNullable()을 사용해야 해요. 그 이유는 of()는 매개변수 값이 null일 경우 NPE를 발생시키기 때문입니다.
🤔 NPE를 막으려고 Optional을 쓰는데, NPE를 발생시킨다구요 ?..
Optional.of() 는 레퍼런스 변수를 참조하는 시점에 NPE가 발생하는 것이 아니라, Optional 객체를 생성하는 시점에 발생해요. 따라서 개발자가 의도적으로 예외를 발생시키는 것이라고 생각하면 됩니다.
[ Optional 객체의 값 가져오기 ]
Optional 객체에 저장된 값을 가져올 때는 get()을 사용합니다. null일 경우를 대비해 orElse()나 orElseThrow(), orElseGet()을 사용하여 대체할 수 있어요.
Optional<String> ex1 = Optional.of("zerozae");
System.out.println(ex1.get()); // zerozae
Optional<String> ex2 = Optional.empty();
System.out.println(ex2.orElse("비어 있습니다."));
System.out.println(ex2.orElseGet(()-> "비어 있습니다."));
System.out.println(ex2.orElseThrow(NoSuchElementException::new));
- get() : Optional 객체에 저장되어 있는 value를 가져옵니다. (비어있으면 NoSuchElementException 발생)
- orElse(T other) : 비어 있는 Optional 객체면 other 객체를 반환합니다.
- orElseGet(Supplier other) : 비어 있는 Optional 객체에 대해 null을 대체할 값을 반환하는 supplier를 지정합니다.
- orElseThrow(Supplier exceptionSupplier) : null일경우 지정한 예외를 던집니다.
+ 이 외에도 isPresent() , ifPresent() , Stream과 같은 map(), filter() 등등이 있지만, isPresent()와 같은 조건을 통해 null을 처리하게 된다면 사실상 Optional을 사용하는 의미가 없어지기 때문에 상황에 맞게 잘 사용하도록 해요.
[ 실제 활용 ]
- 메서드 반환 타입이 Optional 인 경우 : orElseThrow 와 같은 메서드를 이용해요.
- 메서드 반환 타입이 Optional 이 아닌 경우 : ofNullable 같은 메서드로 Optional 객체를 생성하고 orElseThrow 로 처리해요.
3) 리팩토링
리팩토링이란 프로그램의 결과를 바꾸지 않고, 내부 구조를 개선하는 것을 의미해요. 따라서 리팩토링은 다음과 같이 구분합니다.
- 리팩토링 O : 소스 코드 정리
- 리팩토링 X : 버그 수정, 기능 추가 등
만약, 엄청 많은 if 문이 중첩되어 있다고 생각해볼게요. 보기만 해도 어지러울거에요..🫨 이런 중첩되어 있는 if문들은 코드의 수정이나 디버깅을 어렵게 만들기 때문에 리팩토링 할 필요성이 있습니다..
[ 가독성 측면 ]
1. early return : 말 그대로 "빨리 반환"인데요. if 문 중간중간 유효성에 대한 검사를 진행하고 유효성에 맞지 않으면 리턴하는 if 문이 존재할 수 있습니다. 이런 if 문들은 공통적으로 묶어 앞으로 빼서 빠르게 반환처리 해주도록 합시다.
2. 생성 시점에 유효성 검사 : 생성 시점에 유효성을 검사하는 거에요. 그렇다면 핵심 로직을 처리할 때는 유효성 검사를 따로 진행하지 않아도 되어 코드를 핵심 로직을 보다 쉽게 이해할 수 있습니다.
3. 메서드 처리 : 핵심 로직을 getter & setter로 처리하기 보다, 명확한 메서드명을 통해 결과값을 반환받는 방식으로 하면 훨씬 가독성이 좋아집니다.
[ 캡슐화 측면 ]
하나의 클래스에서 다른 클래스의 객체를 통한 핵심로직을 수행하는 것은 캡슐화 측면에서 좋지 않습니다. 예를 들어 "고객" 클래스가 있고 "자동차 정비소" 클래스가 있다고 하면, 고객은 사실 수리되는 로직에 대해 알 필요가 없겠죠 ? 그래서 자동차 정비소 클래스의 메서드를 통해 수리된 자동차의 결과만을 get() 메서드를 통해 반환 받으면 "고객"과 "자동차 정비소" 클래스 간 캡슐화가 잘 지켜졌다고 할 수 있습니다.
[ 결합도와 응집도 ]
캡슐화 부분에서도 말했듯이 만약 "고객"이라는 클래스가 "자동차 정비소"의 필드값을 통해 무언가를 처리하는 로직을 가지고 있다면 ? (ex. 자동차의 타입, 고장 부분 등을 가지고 수리결과를 반환하는 로직) 자동차 정비소 클래스의 필드를 하나라도 삭제하거나 수정하게 된다면, 고객 클래스 쪽에서도 분명 영향을 받을 것입니다.
이렇게 짜여진 코드를 응집도가 낮고, 결합도가 높다고 표현합니다. 즉, 연관성이 있는 것끼리 모여있는 수준이 낮고(응집도), 서로 간 영향력이 강하다(결합도)는 것이죠.
객체지향적으로 코드를 작성하려면 결합도는 낮고, 응집도는 높게 작성하도록 해야합니다.
[ setter ]
이전까지는 getter를 중심으로 리팩토링을 살펴보았는데요, 이번엔 setter 측면에서 리팩토링을 살펴볼게요. setter 같은 경우 사용을 웬만하면 하지 않는 것이 좋다고 해요. set을 통해 인스턴스의 필드를 지정하는 경우를 살펴보면 만약 하나의 필드라도 실수로 누락해서 설정해주지 않았다면 불완전한 인스턴스를 생성하는 것과 마찬가지이기 때문입니다. 객체의 일관성을 유지하기 어렵고, 지정한 값의 의도를 파악하기도 어려울 수 있습니다.
무엇보다 객체지향의 핵심인 "정보 은닉"을 해치게 됩니다!
정리
- 동일성(Identity) 비교는 == 를 통해 메모리 내 주소값이 같은지 비교합니다.
- 동등성(Equality) 비교는 equals() & hashCode()를 통해 논리적 지위가(값이) 같은지 비교합니다.
- 논리적 지위의 기준은 개발자가 오버라이딩 하여 재정의하면 됩니다.
- Optional은 Null에 대한 처리를 위해 고안되었습니다.
- 리팩토링에서 중요한 사항은 가독성, 캡슐화 , 결합도와 응집도 등이 있습니다.