Object 계약과 동등성
모든 객체가 공유하는 Object 계약을 게시글 진단 도구에 적용하고 동일성·동등성·문자 표현의 차이를 실행 결과로 검증합니다.
자바 클래스는 부모를 적지 않아도 최종적으로 Object를 상속합니다.
덕분에 서로 관계없는 객체도 Object 관점으로 받을 수 있고, toString(), equals(), hashCode() 같은 공통 계약을 사용할 수 있습니다.
그러나 가장 넓은 타입은 구체 기능을 잃습니다.
이번 절은 “무엇이든 받는다”와 “무엇이든 처리한다”를 구분하는 데서 시작합니다.
Object 참조의 접근 범위
다음 코드는 Post 객체가 실제로 title()을 가지고 있어도 컴파일되지 않습니다.
변수의 정적 타입이 Object이므로 컴파일러는 Object 계약에 선언된 메서드만 허용합니다.
public final class ObjectMethodLimitFailure {
public static void main(String[] args) {
Object value = new Post("object", 40);
System.out.println(value.title());
}
private record Post(String title, int viewCount) { }
}error: cannot find symbol
System.out.println(value.title());
^실제 객체 타입을 알고 있더라도 변수 타입만으로 호출 가능성을 검사한다는 다형적 참조 규칙은 그대로입니다.
다운캐스팅하면 접근할 수 있지만, 여러 타입을 받아 공통 처리하려는 코드에서 타입별 캐스팅이 늘면 빠진 역할 인터페이스가 없는지 먼저 점검합니다.
java.lang과 Object의 역할
String, System, Math, Object는 java.lang 패키지에 있고 자동으로 import됩니다.
JDK는 컴파일러와 표준 라이브러리, 실행 환경을 함께 제공하므로 예제의 컴파일 버전과 실행 버전을 맞추는 것이 중요합니다.
프로젝트 도구가 달라도 java -version과 javac -version이 같은 주 버전을 가리키면 언어 기능 차이로 생기는 혼란을 줄일 수 있습니다.
Object가 최상위 부모인 목적은 모든 객체에 최소 공통 기능을 주고, 공통 타입을 통한 다형성의 출발점을 제공하는 것입니다.
Object 변수에는 Post, String, Integer처럼 서로 다른 객체를 담을 수 있습니다.
반면 int 같은 기본형은 객체가 아니지만 오토 박싱을 거치면 Integer가 되어 Object 자리에 들어갑니다.
public final class ObjectBoardInventory {
public static void main(String[] args) {
Object[] values = {
new Post("object", 40),
"memo",
Integer.valueOf(3)
};
System.out.println("size=" + size(values));
for (Object value : values) {
System.out.println(value.getClass().getSimpleName() + "=" + value);
}
}
private static int size(Object[] values) {
return values.length;
}
private record Post(String title, int viewCount) { }
}size=3
Post=Post[title=object, viewCount=40]
String=memo
Integer=3size(Object[])는 원소의 구체 기능을 몰라도 개수를 셀 수 있습니다.
반복문도 getClass()와 toString()처럼 Object 계약만 사용합니다.
모든 원소의 조회 수를 합치려면 Object보다 좁은 ViewCountedPost 역할이 필요합니다.
넓은 타입은 수용 범위를 넓히지만 의미 있는 연산을 자동으로 만들어 주지 않습니다.
toString 문자 표현 계약
Object의 기본 toString()은 보통 클래스 이름과 해시 기반 식별 정보를 보여 줍니다.
객체의 상태를 이해하기 어렵기 때문에 도메인 클래스가 핵심 필드를 포함하도록 재정의하면 로그와 디버깅이 쉬워집니다.
System.out.println(object)와 문자열 결합은 내부적으로 객체의 문자 표현을 요청합니다.
public final class PostToString {
public static void main(String[] args) {
Post entry = new Post("equality", 50);
print(entry);
System.out.println("direct=" + entry);
}
private static void print(Object value) {
System.out.println("audit=" + value.toString());
}
private static final class Post {
private final String title;
private final int viewCount;
Post(String title, int viewCount) {
this.title = title;
this.viewCount = viewCount;
}
@Override
public String toString() {
return "Post{title='" + title + "', viewCount=" + viewCount + "}";
}
}
}audit=Post{title='equality', viewCount=50}
direct=Post{title='equality', viewCount=50}문자 표현에 비밀번호나 토큰 같은 민감 정보를 넣지 않습니다.
또한 프로그램의 영구 저장 형식을 toString() 결과에 의존하면 디버깅 표현을 바꾸기 어려워집니다.
사람을 위한 진단 표현과 CSV·JSON 같은 교환 형식은 별도 역할로 분리합니다.
동일성과 동등성
a == b는 두 참조가 같은 인스턴스를 가리키는지 묻습니다.
a.equals(b)는 두 객체를 도메인 규칙상 같은 값으로 볼지 묻습니다.
Object의 기본 equals는 동일성 비교와 같으므로, 값 동등성이 필요하면 클래스가 계약을 재정의해야 합니다.
Post의 동등성을 제목과 조회 수로 정한다고 가정합니다.
서로 다른 시각에 생성했더라도 두 필드가 같으면 같은 게시글 값입니다.
반대로 데이터베이스 엔티티라면 고유 식별자를 기준으로 삼을 수 있습니다.
어떤 필드를 비교할지는 클래스마다 다르며, 바뀌지 않는 핵심 값을 선택해야 컬렉션에서도 안정적입니다.
import java.util.Objects;
public final class PostEquality {
public static void main(String[] args) {
Post first = new Post("object", 40);
Post second = new Post("object", 40);
Post alias = first;
System.out.println("identity=" + (first == second));
System.out.println("alias=" + (first == alias));
System.out.println("equality=" + first.equals(second));
System.out.println("hash=" + (first.hashCode() == second.hashCode()));
}
private static final class Post {
private final String title;
private final int viewCount;
Post(String title, int viewCount) {
this.title = title;
this.viewCount = viewCount;
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (other == null || getClass() != other.getClass()) return false;
Post that = (Post) other;
return viewCount == that.viewCount && Objects.equals(title, that.title);
}
@Override
public int hashCode() {
return Objects.hash(title, viewCount);
}
}
}identity=false
alias=true
equality=true
hash=trueequals를 재정의할 때는 같은 객체와 비교하면 true인 반사성, 순서를 바꿔도 같은 대칭성, 연쇄 비교가 유지되는 추이성, 상태가 같으면 결과가 반복되는 일관성, null과는 false라는 조건을 지켜야 합니다.
equals가 true인 두 객체는 반드시 같은 hashCode를 반환해야 하므로 두 메서드를 함께 구현합니다.
getClass와 instanceof 선택
getClass() 비교는 정확히 같은 런타임 클래스끼리만 동등하게 봅니다.
instanceof는 하위 타입까지 허용하지만, 부모와 자식이 비교 필드를 다르게 해석하면 대칭성이나 추이성이 깨질 수 있습니다.
값 타입을 상속하지 않게 final로 두고 정확한 클래스 비교를 쓰면 계약을 단순하게 유지할 수 있습니다.
확장 계층의 동등성이 정말 필요하면 전체 계층이 같은 기준을 공유하는지 설계 단계에서 검증합니다.
Object와 역할 타입의 선택
Object가 적합한 곳은 로깅, 범용 컨테이너의 내부 경계, 리플렉션 도구처럼 구체 의미 없이 객체 자체를 다루는 기반 코드입니다.
조회 수 합계, 공개 처리, 내보내기처럼 도메인 행동이 필요하면 BoardItem, Completable, Exporter 같은 역할을 사용합니다.
- 공통으로 필요한 행동이 Object에 있는가?
- 캐스팅이나 instanceof 분기가 계속 늘어나는가?
- 서로 다른 객체가 같은 업무 의미로 대체 가능한가?
- 동등성 기준이 객체 수명 동안 안정적인가?
- 진단 문자열과 저장 형식을 혼동하고 있지 않은가?
이 질문으로 가장 넓은 타입을 선택한 대가를 확인합니다.
Object는 모든 객체를 담는 편의보다 모든 객체가 공유하는 최소 계약을 표현하는 타입입니다.
연습 문제
폭과 높이가 같은 Rectangle 두 개가 동등하도록 equals와 hashCode를 구현하세요.
폭·높이가 양수인지 생성자에서 검증하고, 진단 결과는 Rectangle{100x20} 형식으로 만드세요.
해설 보기
import java.util.Objects;
public final class RectangleValueExercise {
public static void main(String[] args) {
Rectangle first = new Rectangle(100, 20);
Rectangle second = new Rectangle(100, 20);
Rectangle third = new Rectangle(20, 100);
System.out.println(first);
System.out.println(first.equals(second));
System.out.println(first.equals(third));
System.out.println(first.hashCode() == second.hashCode());
}
private static final class Rectangle {
private final int width;
private final int height;
Rectangle(int width, int height) {
if (width <= 0 || height <= 0) throw new IllegalArgumentException("size");
this.width = width;
this.height = height;
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (other == null || getClass() != other.getClass()) return false;
Rectangle that = (Rectangle) other;
return width == that.width && height == that.height;
}
@Override
public int hashCode() {
return Objects.hash(width, height);
}
@Override
public String toString() {
return "Rectangle{" + width + "x" + height + "}";
}
}
}Rectangle{100x20}
true
false
true면적만 비교하면 100×20과 20×100도 같아져 모양이라는 의미를 잃습니다.
동등성 필드는 값 객체가 표현하는 개념을 기준으로 정합니다.