equals() and hashCode()

Object 클래스는 모든 클래스의 조상이다.
특징으로는 java 에 존재하는 모든 클래스는 Object 클래스를 상속하고있다.

오늘은 Object의 메소드 중에 equals()hashCode() 를 다루겠다 🫡


동일성 vs 동등성 🤔

이 둘의 차이를 간단히 말하자면 동등성은 == 연산을 했을때 같은 것이고, 동일성은 우리가 같은 값이라고 정의한 것, 즉 equals() 메소드를 오버라이딩해서 비교하고 같은 값을 말한다


hashCode 란 해싱 알고리즘에 의해 반환된 정수 값, HashSet과 같은 자료 구조에서 사용되는 Hash Table에서의 hashing을 가능하게 하는 것을 목적

  • hashCode() 는 오버라이딩 하지 않았을 때, 임의의 정수값을 반환한다.
  • HashSet같은 자료구조에서 같은 값인지 판별할 때는 hashCode() 의 결과값을 capacity(용량) 으로 % 모듈러 연산한 값을 비교한 후 equals() 를 통해서 비교한다.
    -> hashCode()로 먼저 비교하는 이유는 hashCode()equals()보다 가벼우며 이를 통해서 비교할 값의 후보군을 확 줄일 수 있기 때문이다 👍

아래 코드로 HashSet의 예를 보겠다 👀

@AllArgsConstructor
public class Good {

	int a;
	String b;

	@Override
	public boolean equals(Object obj) {
		if (this == obj) return true;
		if (obj == null || getClass() != obj.getClass()) return false;
		Good good = (Good)obj;
		return a == good.a && Objects.equals(b, good.b);
	}

	@Override
	public int hashCode() {
		return Objects.hash(a, b);
	}
}

-> 여기서 오버라이딩한 equals()는 a, b 값이 같다면 동등함을 나타내고 hashCode() 는 같은 a, b 가 같다면 같은 해시코드를 리턴한다.


위 코드를 아래 코드로 적용해보자 👊

public class Hello {

	public static void main(String[] args) {
		Good good1 = new Good(1, "a");
		Good good2 = new Good(1, "a");

		Set<Good> set = new HashSet<>();
		set.add(good1);
		set.add(good2);

		System.out.println("set size : " + set.size());
	}
}

결과는 아래와 같다

equals(), hashCode()에 의해 두 객체가 같다고 판별되어 사이즈가 1인걸 볼 수 있다.


하지만 여기서 hashCode 메소드를 주석처리하면!

equals() 가 동작하더라도 hashCode()에서 다른 값이라고 판별되기 때문에 1의 사이즈가 나오는 것을 볼 수 있다.


객체지향적인 설계

강한 응집력, 약한 결합력 그리고 확실한 역할

이번에는 객체지향적인 설계에 있어서 강한 응집력, 약한 결합력 그리고 객체의 확실한 역할에 대해서 예시를 통해 알아보겠다.

지난 #1 포스팅의 Enum 활용과 이어지는 예다.

//Command Class
@Getter
@AllArgsConstructor
public class AppendCommand {

	private AppendType appendType;

	private String target;

	private String source;

	@RequiredArgsConstructor
	public enum AppendType {
		FRONT((a, b) -> b.concat(a)),
		END(String::concat),
		;

		private final BiFunction<String, String, String> appender;

		public String append(String str1, String str2) {
			return this.appender.apply(str1, str2);
		}
	}
}

위 코드는 두 target, source 을 통해서 apendTypeFRONT 면 source 앞에 target 을 붙이고 END 면 source 뒤에 target 을 붙이는 간단한 예시다.

해당 클래스를 사용하려면 클래스를 사용하는 Client는 아래와 같은 코드를 가져야한다.

public class Client {

	public String executeCommand(AppendCommand command) {
		AppendType appendType = command.getAppendType();
		String source = command.getSource();
		String target = command.getTarget();

		return appendType.append(source, target);
	}
}

위 코드의 문제점은 무엇일까? 🤔

나는 아래 두 가지 문제점을 대표적으로 생각했다.

  1. AppendCommand 의 역할이 명확하지 않다
  2. ClientAppendCommand 에 강하게 결합되어있다.

1번 문제에 대해 설명하자면 AppendCommand 라는 클래스를 설계 할 때는 분명 문자열을 append 해주는 역할을 수행하는 클래스로 설계했을 것이다.

하지만 실제로 AmmendCommandappend 를 해주는 역할을 명확하게 수행하지 않는다. 직접적으로 해당 역할을 수행하지 않고 append 하는데 필요한 재료들만 들고있는 셈이다.


다음으로 2번 문제에 대해 설명하자면 ClientAppendCommand 라는 객체만 들고있을 뿐이며, 그저 들고있는 AppendCommand 를 통해서 append 를 하고싶을 뿐이지 AppendCommand 가 들고있는 속성들에 대해서는 궁금하지 않다.

하지만 위 코드는

AppendType appendType = command.getAppendType();
String source = command.getSource();
String target = command.getTarget();

이런식으로 AppendCommand 의 속성을 가져오며 부수적인 속성들과 결합이 추가된다.

이렇게되면 AppendCommand 의 속성 혹은 호출 방법, 기타 등등 변화가 생긴다면 Client 도 코드를 수정해야하는 문제가 생긴다.


해결방법 💡

해결방법은 간단하다! 😀
불필요한 Getter 를 제거하고 AppendCommand 의 역할을 확실히 수행할 메소드를 만들어주면 된다.


해결방법을 적용하면

//Before
@Getter
@AllArgsConstructor
public class AppendCommand {

	private AppendType appendType;

	private String target;

	private String source;
}

//After
@AllArgsConstructor
public class AppendCommand {

	private AppendType appendType;

	private String target;

	private String source;

	public String execute() {
		return appendType.append(target, source);
	}
}

Getter 를 제거하고 execute 라는 append 를 수행하는 메소드를 정의했다.


이로 인해 Client 에서는

//Before
public class Client {

	public String executeCommand(AppendCommand command) {
		AppendType appendType = command.getAppendType();
		String source = command.getSource();
		String target = command.getTarget();

		return appendType.append(source, target);
	}
}

//After
public class Client {

	public String executeCommand(AppendCommand command) {
		return command.execute();
	}
}

AppendCommand 의 속성을 사용해 append 를 호출하는 코드 제거

이렇게 변경되면 ClientAppendCommand 와의 결합력이 약해지며 AppendCommand응집도는 올라가고 역할 또한 명확해진다!! 😄

결과적으로 코드 가독성 또한 좋아지고 유지보수 또한 유리해지며 객체지향적 코드에 가까워질 수 있다 👍

profile
서버 개발하는 사람입니다

0개의 댓글

Powered by GraphCDN, the GraphQL CDN