Jake Seo
제이크서 개발 블로그
Jake Seo
전체 방문자
오늘
어제
  • 분류 전체보기 (717) N
    • AI 서비스 개발 일기 (2) N
    • LLM 개발 일기 (0)
    • ------레거시 (2025.08.23 이전)--.. (0)
    • 백준 문제풀이 (1)
    • 릿코드 문제풀이 (2)
    • 알고리즘 이론 (10)
      • 기본 이론 (2)
      • 배열과 문자열 (8)
    • 데이터베이스 (15)
      • Planet Scale (1)
      • MSSQL (9)
      • 디비 기본 개념 (1)
      • SQLite 직접 만들어보기 (4)
    • 보안 (7)
    • 설계 (1)
    • 네트워크 (17)
      • HTTP (9)
      • OSI Layers (5)
    • 회고 (31)
      • 연간 회고 (2)
      • 주간 회고 (29)
    • 인프라 (52)
      • 도커 (12)
      • AWS (9)
      • 용어 (21)
      • 웹 성능 (1)
      • 대규모 서비스를 지탱하는 기술 (9)
    • 깃 (7)
    • 빌드 도구 (7)
      • 메이븐 (6)
      • 그레이들 (0)
    • Java (135)
      • 이펙티브 자바 (73)
      • 자바 API (4)
      • 자바 잡지식 (30)
      • 자바 디자인 패턴 (21)
      • 톰캣 (Tomcat) (7)
    • 프레임워크 (64)
      • next.js (14)
      • 스프링 프레임워크 (28)
      • 토비의 스프링 (6)
      • 스프링 부트 (3)
      • JPA (Java Persistence API) (5)
      • Nest.js (8)
    • 프론트엔드 (48)
      • 다크모드 (1)
      • 노드 패키지 관리 매니저 (3)
      • CSS (19)
      • Web API (11)
      • tailwind-css (1)
      • React (5)
      • React 새 공식문서 요약 (1)
      • HTML (Markup Language) (5)
    • 자바스크립트 (108)
      • 모던 자바스크립트 (31)
      • 개념 (31)
      • 정규표현식 (5)
      • 코드 스니펫 (1)
      • 라이브러리 (6)
      • 인터뷰 (24)
      • 웹개발자를 위한 자바스크립트의 모든 것 (6)
      • 팁 (2)
    • Typescript (49)
    • 리눅스와 유닉스 (10)
    • Computer Science (1)
      • Compiler (1)
    • IDE (3)
      • VSCODE (1)
      • IntelliJ (2)
    • 세미나 & 컨퍼런스 (1)
    • 용어 (개발용어) (16)
      • 함수형 프로그래밍 용어들 (1)
    • ORM (2)
      • Prisma (2)
    • NODEJS (2)
    • cypress (1)
    • 리액트 네이티브 (React Native) (31)
    • 러스트 (Rust) (15)
    • 코틀린 (Kotlin) (4)
      • 자바에서 코틀린으로 (4)
    • 정규표현식 (3)
    • 구글 애널리틱스 (GA) (1)
    • SEO (2)
    • UML (2)
    • 맛탐험 (2)
    • 리팩토링 (1)
    • 서평 (2)
    • 소프트웨어 공학 (18)
      • 테스팅 (16)
      • 개발 프로세스 (1)
    • 교육학 (1)
    • 삶의 지혜, 통찰 (1)
    • Chat GPT (2)
    • 쉘스크립트 (1)
    • 컴파일 (2)
    • Dart (12)
    • 코드팩토리의 플러터 프로그래밍 (4)
    • 플러터 (17)
    • 안드로이드 스튜디오 (1)
    • 윈도우즈 (1)
    • 잡다한 백엔드 지식 (1)
    • 디자인 패턴 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 슬로우 쿼리
  • 메이븐 골
  • 메이븐 페이즈
  • prerendering
  • 자바스크립트 면접
  • 자바스크립트
  • 디자인패턴
  • 자바 디자인패턴
  • 자바
  • serverless computing
  • 외래키 제약조건
  • 스프링 검증
  • 객체복사
  • Java
  • item7
  • 러스트
  • 자바스크립트 인터뷰
  • 자바 검증
  • 이펙티브 자바
  • rust
  • next js app
  • 이펙티브 자바 item9
  • 토비의 스프링
  • 서버리스 컴퓨팅
  • Next.js
  • 알고리즘
  • 싱글턴
  • Javadoc 자바독 자바주석 주석 Comment
  • 프로그래머의 뇌
  • 이펙티브자바
  • item9
  • 도커공식문서
  • NEXT JS
  • bean Validation
  • 자료구조
  • MSSQL
  • 참조 해제
  • 팩터리 메서드 패턴
  • 메이븐 라이프사이클
  • 싱글톤
  • 작업기억공간
  • 빈 검증
  • Pre-rendering
  • pnpm
  • 싱글톤 패턴
  • try-with-resources
  • 느린 쿼리
  • 플라이웨이트패턴
  • item8
  • 추상 팩터리 패턴

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Jake Seo

제이크서 개발 블로그

Java/이펙티브 자바

이펙티브 자바, 쉽게 정리하기 - item 10. equals는 일반 규약을 지켜 재정의하라

2021. 12. 29. 00:41

equals는 일반 규약을 지켜 재정의하라

equals() 메서드의 함정

  • 기본적으로 객체의 내용이 동일한지 논리적 동치성을 확인하는 메서드이다.
  • 하지만, 직접 구현하다보면 생각치 못한 여러가지 함정이 있으므로 직접 구현하지 않는 편이 안전하다.

구현하지 않아야 할 때

  • 각 인스턴스가 본질적으로 고유할 때
    • ex) 스레드는 각각의 스레드가 고유하다.
  • 인스턴스의 논리적 동치성을 검사할 일이 없을 때
  • 상위 클래스에서 재정의한 equals()가 하위클래스에서도 문제없이 이용 가능할 때
    • ex) Map, Set은 AbstractMap, AbstractSet에서 내려받은 equals()를 그대로 사용한다.
  • 클래스가 private 혹은 package-private일 때
  • 논리적 동치성과 객체 식별성이 같은 의미를 가지게 될 때
    • ex) enum
@Override
public boolean equals(Object o) {
  throw new AssertionError(); // 호출 금지
}

equals() 메서드를 지원하지 않을 때 위와 같이 표현할 수 있다.

가끔 구현해야 할 때

  • 객체간의 논리적 동치성을 구현해야 할 때
    • 그런데, 상위 클래스에 equals()를 재사용할 수 없을 때

주로 Integer, String과 같은 값 클래스 의 경우 equals()를 구현해야 하는 상황이 있다.
이 경우엔 동치 비교는 물론, Map의 키, Set의 원소로도 활용할 수 있다.

equals() 메서드의 일반 규약

  • null-아님: null이 아닌 모든 참조 값 x에 대해 x.equals(null)은 false이다.
    • null이 아니란 전제 하에 아래 조건들이 지켜져야 한다.
  • 반사성(reflectivity): x.equals(x)는 true여야 한다.
    • 이는 일부러 어기지 않는 이상 어기기 어렵다.
  • 대칭성(symmetry): x.equals(y)가 true면, y.equals(x)도 true여야 한다.
  • 추이성(transivity): x.equals(y)가 true이고, y.equals(z)도 true면, x.equals(z)도 true여야 한다.
  • 일관성(consistency): x.equals(y)를 얼마나 반복하든, 결과는 항상 같아야 한다.

자바 API에서 equals()를 이용하는 내용은 모든 클래스가 위 규약을 지키고 있다고 가정한다.

대칭성

대칭성은 실수로 어기기 굉장히 쉽다.

public class Item10Test {
    static class CaseInsensitiveString {
        private final String s;

        public CaseInsensitiveString(String s) {
            this.s = Objects.requireNonNull(s);
        }

        @Override
        public boolean equals(Object o) {
            if(o instanceof CaseInsensitiveString) {
                return s.equalsIgnoreCase(
                        ((CaseInsensitiveString) o).s
                );
            }

            if(o instanceof String) {
                return s.equalsIgnoreCase((String) o);
            }

            return false;
        }
    }

    @Test
    @DisplayName("대칭성을 위배하는 예시")
    public void symmetryViolation() {
        CaseInsensitiveString caseInsensitiveString = new CaseInsensitiveString("abc");
        String string = "AbC";

        boolean caseInsensitiveEquals = caseInsensitiveString.equals(string);
        System.out.println("caseInsensitiveEquals = " + caseInsensitiveEquals); // true

        boolean stringEquals = string.equals(caseInsensitiveString);
        System.out.println("stringEquals = " + stringEquals); // false
    }
}
  • CaseInsensitiveString 클래스와 String 클래스를 비교할 때는 대소문자 구분없이 비교가 된다.
  • String 클래스는 CaseInsensitiveString 타입을 비교대상으로 받는 경우 무조건 false가 나타난다.

한쪽 클래스만 다른쪽 클래스와 비교할 준비가 된 상태이다. 대칭성이 위배된다.

equals() 메서드를 잘못 구현하면, 단순히 equals() 메서드만 문제가 생기는 것이 아니고, 클래스를 받아 equals()를 활용하는 모든 곳에서 문제가 생긴다. ex) List, Map, Set

추이성

  • a = b 이고, b = c일 때, a = c를 만족해야 한다는 조건이다.

대칭성을 위배하는 코드

static class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point)) {
            return false;
        }

        Point p = (Point) o;
        return p.x == x && p.y == y;
    }
}

static class ColorPoint extends Point {
    private final String color;

    public ColorPoint(int x, int y, String color) {
        super(x, y);
        this.color = color;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint)) {
            return false;
        }

        return super.equals(o) && ((ColorPoint) o).color == color;
    }
}

@Test
public void symmetryViolation3() {
    Point p = new Point(1, 0);
    ColorPoint cp = new ColorPoint(1, 0, "red");

    boolean pEqualsCp = p.equals(cp);
    System.out.println("pEqualsCp = " + pEqualsCp); // true

    boolean cpEqualsP = cp.equals(p);
    System.out.println("cpEqualsP = " + cpEqualsP); // false
}
  • 위는 대칭성을 위배하는 코드의 예이다.
    • Point는 좌표만 보기 때문에 ColorPoint와 비교가 가능하다.
      • 단, 반대는 불가능하다.

대칭성을 지키는 코드

@Override
public boolean equals(Object o) {
    if(!(o instanceof Point)) {
        return false;
    }

    if (!(o instanceof ColorPoint)) {
        Point p = (Point) o;
        return p.equals(this);
    }

    return super.equals(o) && ((ColorPoint) o).color == color;
}
  • 간단히 Point 인스턴스일 때는 색상을 비교하지 않고, 좌표만 비교하도록 코드를 구성했다.
  • 이제 대칭성을 만족한다.
  • 그러나 추이성이 지켜지지 않는다.
@Test
@DisplayName("추이성을 만족하지 않는 코드")
public void symmetryViolation3() {
    Point p = new Point(1, 0);
    ColorPoint cp = new ColorPoint(1, 0, "red");
    ColorPoint cp2 = new ColorPoint(1, 0, "blue");

    boolean pEqualsCp = p.equals(cp);
    System.out.println("pEqualsCp = " + pEqualsCp);

    boolean cpEqualsP = cp.equals(p);
    System.out.println("cpEqualsP = " + cpEqualsP);

    boolean pEqualsCp2 = p.equals(cp2);
    System.out.println("pEqualsCp2 = " + pEqualsCp2);

    boolean cp2EqualsP = cp2.equals(p);
    System.out.println("cp2EqualsP = " + cp2EqualsP);

    boolean cpEqualsCp2 = cp.equals(cp2);
    System.out.println("cpEqualsCp2 = " + cpEqualsCp2);
}
  • cp.equals(p) = true
  • cp2.equals(p) = true
  • cp.equals(cp2) = false
  • cp2.equals(cp) = false

위와 같은 결과가 나와 추이성을 만족하지 못하는 코드가 나왔다.

구체 클래스를 확장해 새로운 값을 추가하면서 equals() 규약을 만족시킬 방법은 존재하지 않는다.

리스코프 치환원칙을 위배하는 코드

@Override
public boolean equals(Object o) {
  if (o == null || o.getClass() != getClass()) {
    return false;
  }

  Point p = (Point) o;
  return p.x == x && p.y == y;
}
  • instanceof 연산을 쓰지 않고 o.getClass()로 비교하면, 오직 같은 클래스일 때만 equals()가 동작한다.
    • 하지만 이렇게 되면, Point의 하위 클래스가 어디서든 Point로 활용될 수는 없게 되는 것이다.
    • 리스코프 치환원칙 위배가 된다.
public static boolean onUnitCircle(Point p) {
    final Set<Point> unitCircle = Set.of(
            new Point(1, 0), new Point(0, 1),
            new Point(-1, 0), new Point(0, -1)
    );

    return unitCircle.contains(p);
}

public class CounterPoint extends Point {
    private static final AtomicInteger counter = new AtomicInteger();

    public CounterPoint(int x, int y) {
        super(x, y);
        counter.incrementAndGet();
    }

    public static int numberCreated() { return counter.get(); }
}

@Test
public void unitCircleTest() {
    boolean onUnitCircle = onUnitCircle(new Point(0, 1));
    System.out.println("onUnitCircle = " + onUnitCircle); // true

    boolean onUnitCircle2 = onUnitCircle(new CounterPoint(0, 1));
    System.out.println("onUnitCircle2 = " + onUnitCircle2); // false
}

위 예제에서, CounterPoint는 Point를 상속받아 x, y좌표를 나타내는 것이 동일한데, onUnitCircle() 메서드에 무조건 false만 반환하게 된다. contains() 함수가 내부적으로 Point의 equals()를 이용하기 때문일 것이다.

getClass() 메서드로 클래스 비교를 하는 것이 아닌, instanceof를 이용했다면 정상적으로 동작했을 것이다.

상속대신 컴포지션을 이용하여 Point 확장해보기

static class NewColorPoint {
    private final Point point;
    private final String color;

    public NewColorPoint(int x, int y, String color) {
        this.point = new Point(x, y);
        this.color = color;
    }

    public Point asPoint() {
        return point;
    }

    @Override
    public boolean equals(Object o) {
        if(!(o instanceof NewColorPoint)) {
            return false;
        }

        NewColorPoint cp = (NewColorPoint) o;
        return cp.equals(point) && cp.color.equals(color);
    }
}

이렇게 구성하면, 애초에 상속받은 적이 없으니 리스코프 치환원칙에서 벗어난다.

일반 규약을 지키지 않는 잘못된 자바 API들

  • java.sql.Timestamp는 java.util.Date를 확장한 후에 필드를 추가해서 일반 규약을 지킬 수 없다.
    • 그래서 컬렉션에 넣거나, 대칭성 있는 비교를 수행하려 하면 디버그하기 까다로운 현상을 만날 수도 있다.

equals()에 신뢰할 수 없는 자원이 끼어들게 하면 안된다.

  • java.net.URL은 equals()가 일반규약을 지키지 않는다.
    • 호스트 이름이 같으면, equlas()가 참이 나오는데, 사실 이건 네트워크를 거치지 않으면 알 수 없는 정보이다.

위와 같은 문제를 피하려면, 항시 메모리에 존재하는 객체만을 사용한 결정적 계산만 수행해야 한다.

null 검사

  • null 검사는 사실 instanceof 연산을 하며 쉽게 검증된다.
    • instanceof는 첫번째 피연산자가 null이면 무조건 false를 반환한다.

equals() 메서드를 구현하는 절차

  • == 연산자로 자신의 참조인지 먼저 확인한다.
    • 필드가 아주 많은 객체를 비교한다면, 이런 단순한 연산으로 매우 많은 절약을 할 수 있다.
  • instanceof 연산자로 입력이 올바른 타입인지 확인한다.
    • null 방지와 캐스팅 에러 방지를 해준다.
  • 입력을 올바른 타입으로 형변환한다.
  • 핵심 필드들이 일치하는지 하나씩 확인한다.
    • 기본 타입은 ==으로 확인하고, float, double은 Float.compare()와 같은 정적 메서드를 활용하자. (부동소수점 때문)
    • 배열의 모든 원소가 핵심 필드라면, Arrays.equals() 메서드들 중 하나를 활용하자.
    • null 값을 정상 값으로 취급하는 참조 타입 필드의 경우 Object.equals(Object, Object) 메서드로 방지할 수 있다.

필드를 비교할 때

  • 필드를 비교할 때는 성능이 싼 필드부터 비교하면 좋다.
  • 핵심 필드로부터 계산되는 파생필드가 있는 경우, 파생 필드를 비교하는 것이 더 빠르진 않은지 생각해볼 필요가 있다.

equals()를 다 구현했다면?

위에서 배운 일반 규약을 잘 지키는지 확인해보자

equals() 메서드 구현 마지막 주의사항

  • equals()를 재정의할 땐 반드시 hashCode()도 재정의하자.
  • 너무 복잡하게 해결하려 하지말자.
  • Object 외의 타입을 파라미터로 받는 equals()를 만들지 말자.
    • @Override를 항상 명시하여, 이러한 일을 방지하자.
    • 이러한 실수는 한번 하면 찾기도 힘들 수 있다.

가급적 IDE에서 제공하는 equals(), hashCode() 구현 기능을 이용하는 것이 실수를 예방하기 좋다.

핵심

  • 가급적 equals()를 재정의하지 말자.
  • 굳이 재정의 해야 할 때는 항상 규약을 명심해야 한다.
저작자표시 (새창열림)

'Java > 이펙티브 자바' 카테고리의 다른 글

이펙티브 자바, 쉽게 정리하기 - item 12. toString을 항상 재정의하라  (0) 2021.12.30
이펙티브 자바, 쉽게 정리하기 - item 11. equals를 재정의하려거든 hashCode도 재정의하라  (0) 2021.12.29
이펙티브 자바, 쉽게 정리하기 - item9. try-finally보다는 try-with-resources를 사용하라  (0) 2021.12.28
이펙티브 자바, 쉽게 정리하기 - item8. finalizer와 cleaner 사용을 피하라  (0) 2021.12.27
이펙티브 자바, 쉽게 정리하기 - item7. 다 쓴 객체 참조를 해제하라  (0) 2021.12.27
    'Java/이펙티브 자바' 카테고리의 다른 글
    • 이펙티브 자바, 쉽게 정리하기 - item 12. toString을 항상 재정의하라
    • 이펙티브 자바, 쉽게 정리하기 - item 11. equals를 재정의하려거든 hashCode도 재정의하라
    • 이펙티브 자바, 쉽게 정리하기 - item9. try-finally보다는 try-with-resources를 사용하라
    • 이펙티브 자바, 쉽게 정리하기 - item8. finalizer와 cleaner 사용을 피하라
    Jake Seo
    Jake Seo
    ✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.

    티스토리툴바