일급 컬렉션이 무엇인가요?
일급 컬렉션(First-Class Collection)은 하나의 컬렉션을 감싸는 클래스를 만들고, 해당 클래스에서 컬렉션과
관련된 비즈니스 로직을 관리하는 패턴을 말합니다. 아래 코드 중에서 Order의 List 자료구조를 감싼 Orders가 일급 컬렉션의 예시입니다.
// 일급 컬렉션 public class Orders { private final List<Order> orders; public Orders(List<Order> orders) { validate(orders); // 검증 수행 ... } public void add(Order order) { if (order == null) { throw new IllegalArgumentException("Order cannot be null"); } orders.add(order); } public List<Order> getAll() { return Collections.unmodifiableList(orders); } public double getTotalAmount() { return orders.stream() .mapToDouble(Order::getAmount) .sum(); } }
public class OrderService { private final Orders orders = new Orders(); public void addOrder(Order order) { orders.add(order); } public Orders getOrders() { return orders; } // 추가 비즈니스 로직... }
- 일급 컬렉션을 사용해야하는 이유는 무엇인가요? 😀
일급 컬렉션 클래스에 로직을 포함하거나 비즈니스에 특화된 명확한 이름을 부여할 수 있습니다. 또한, 불필요한 컬렉션 API를 외부로 노출하지 않도록 할 수 있으며, 컬렉션을 변경할 수 없도록 만든다면 예기치 않은 변경으로부터 데이터를 보호할 수 있습니다.
equals와 hashCode는 왜 함께 재정의해야 할까요?
equals와 hashCode 메서드는
객체의 동등성 비교와 해시값 생성을 위해서 사용할 수 있습니다. 하지만, 함께 재정의하지 않는다면 예상치 못한 결과를 만들 수
있습니다. 가령, 해시값을 사용하는 자료구조(HashSet, HashMap..)을 사용할 때 문제가 발생할 수 있습니다.
class EqualsHashCodeTest { @Test @DisplayName("equals만 정의하면 HashSet이 제대로 동작하지 않는다.") void test() { // 아래 2개는 같은 구독자 Subscribe subscribe1 = new Subscribe("[email protected]", "backend"); Subscribe subscribe2 = new Subscribe("[email protected]", "backend"); HashSet<Subscribe> subscribes = new HashSet<>(List.of(subscribe1, subscribe2)); // 결과는 1개여야하는데..? 2개가 나온다. System.out.println(subscribes.size()); } class Subscribe { private final String email; private final String category; public Subscribe(String email, String category) { this.email = email; this.category = category; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Subscribe subscribe = (Subscribe) o; return Objects.equals(email, subscribe.email) && Objects.equals(category, subscribe.category); } } }
- 왜 이런 현상이 발생하나요? 🤔
해시값을 사용하는 자료구조는 hashCode 메서드의 반환값을 사용하는데요. hashCode 메서드의 반환 값이 일치한 이후
equals 메서드의 반환값 참일 때만 논리적으로 같은 객체라고 판단합니다. 위 예제에서 Subscribe 클래스는
hashCode 메서드를 재정의하지 않았기 때문에 Object 클래스의 기본 hashCode 메서드를 사용합니다. Object
클래스의 기본 hashCode 메서드는 객체의 고유한 주소를 사용하기 때문에 객체마다 다른 값을 반환합니다. 따라서 2개의
Subscribe 객체는 다른 객체로 판단되었고 HashSet에서 중복 처리가 되지 않았습니다.
동일성과 동등성에 대해서 설명해주세요.
동일성과 동등성은 객체를 비교할 때 중요한 개념입니다. 자바에서는 이 두 개념을 equals() 메서드와 == 연산자를 통해 구분할 수 있습니다.
equals()와==의 차이는 무엇인가요?
equals()는 객체의 내용을 비교하는 반면, ==는 객체의 참조(레퍼런스)를 비교합니다. 따라서 두 객체의 내용이 같더라도 서로 다른 객체라면 equals()는 true를 반환할 수 있지만, ==는 false를 반환합니다.
- 동등성(Equality)은 뭔가요?
동등성은 논리적으로 객체의 내용이 같은지를 비교하는 개념입니다. 자바에서는
equals() 메서드를
사용하여 객체의 동등성을 비교합니다. Apple 클래스를 예시로 보면, Object.equals 메서드를 오버라이딩하여 객체의
실제 데이터를 비교하도록 했습니다. 그래서 apple과 anotherApple은 다른 객체이지만, 무게가 같기 때문에 동등성 비교
결과 true가 반환됩니다.public class Apple { private final int weight; public Apple(int weight) { this.weight = weight; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Apple apple = (Apple) o; return weight == apple.weight; } @Override public int hashCode() { return Objects.hashCode(weight); } public static void main(String[] args) { Apple apple = new Apple(100); Apple anotherApple = new Apple(100); System.out.println(apple.equals(anotherApple)); // true } }
- 왜 equals() 메서드를 오버라이딩 했나요?
public class Object { ... public boolean equals(Object obj) { return (this == obj); } ... }
Object 클래스의 equals() 메서드는 == 연산자를 사용하여 동일성을 비교합니다. 그리고 모든 클래스는 Object
클래스를 상속하여 동일성 비교를 기본으로 동작하기 때문에, 동등성 비교가 필요한 클래스에서 필요에 맞게 equals &
hashCode 메서드를 오버라이딩해야 합니다.
- 동일성(Identity)은 뭔가요?
동일성은 두 객체가 메모리 상에서 같은 객체인지 비교하는 개념입니다. 자바에서는
== 연산자를 사용하여 객체의 동일성을 비교합니다. == 연산자는 객체의 레퍼런스(참조)를 비교하므로, 두 변수가 동일한 객체를 가리키고 있는지를 확인합니다.public static void main(String[] args) { Apple apple1 = new Apple(100); Apple apple2 = new Apple(100); Apple apple3 = apple1; System.out.println(apple1 == apple2); // false System.out.println(apple1 == apple3); // true }
apple1과 apple2는 참조가 다르기 때문에 == 연산 결과 false가 반환되지만, apple1의 참조를 가지는 apple3은 == 연산 결과 true를 반환합니다.
- String은 객체인데 == 비교해도 되던데 어떻게 된건가요?
문자열 리터럴은 문자열 상수 풀(String Constant Pool) 에 저장되기 때문에, 동일한 문자열 리터럴을 참조하면 == 연산자가 true를 반환할 수 있습니다. 하지만
new 키워드를 사용하여 문자열을 생성하면 새로운 객체가 생성되므로 == 연산자가 false를 반환할 수 있습니다. 따라서 문자열 비교 시 항상 equals() 메서드를 사용한 동등성 비교를 하는 것이 좋습니다.public class StringComparison { public static void main(String[] args) { String str1 = "안녕하세요"; String str2 = "안녕하세요"; String str3 = new String("안녕하세요"); // 동일성 비교 System.out.println(str1 == str2); // true System.out.println(str1 == str3); // false // 동등성 비교 System.out.println(str1.equals(str2)); // true System.out.println(str1.equals(str3)); // true } } // String.class equals 오버라이딩 되어있음. public boolean equals(Object anObject) { if (this == anObject) { return true; } return (anObject instanceof String aString) && (!COMPACT_STRINGS || this.coder == aString.coder) && StringLatin1.equals(value, aString.value); }
- Integer 같은 래퍼 클래스는 어떻게 비교하나요?
래퍼 클래스도 객체이기 때문에 == 연산자는 참조를 비교합니다. 값 비교를 원할 경우 equals() 메서드를 사용해야
합니다. 다만, 자바는 특정 범위의 래퍼 객체를 캐싱하므로 같은 값의 Integer 객체가 같은 참조를 가질 수 있습니다(-128 ~ 127). 하지만 일반적으로 equals()를 사용하는 것이 안전합니다.