본문 바로가기

(7)

JDK 21, Virtual Thread

Virtual Thread란…? JDK 21에 새롭게 들어온 개념 (2023.09.19 에 LTS 출시) gradle 8.4v 부터 지원 kotlin v1.9.20 부터 21 바이트 코드 지원 Spring 6.1, Spring boot 3.2 부터 지원 Jetbrain Intellij 2023.3 JDK 21(LTS)에 추가된 경량 스레드, OS 스레드를 그대로 사용하지 않고 JVM 내부 스케줄링을 통해서 수십만 ~ 수백만개의 스레드를 동시에 사용할 수 있게한다. 전통적인 Java의 Thread Java의 Thread는 OS Thread를 랩핑한 것 (Platform Thread) Java 애플리케이션에서 Thread를 사용하면 실제로 OS Thread를 사용한 것 OS Thread는 생성 갯수가 제한적..

Java - Comparable vs Comparator

Stream 성애자인 나는 오늘도 코딩 테스트 문제나 비즈니스 로직을 짜면서 Stream을 쓰고있다. 하지만 코딩 테스트를 풀면서 생긴 의문점이 있다. 바로 정렬을 해주는 sorted() 라는 함수인데 이 안의 파라미터로 Comparator 타입이 들어간다는 갓텔리제이의 TMI한 설명이 있다. 중요한건 이 문제를 이렇게도 풀 수 있지만, 아래와 같이 Music 클래스가 Comparable 인터페이스를 구현하고, compareTo 메서드를 재정의 함으로써도 해결할 수 있다. sorted 함수의 파라미터를 Music의 comparTo 메서드로 변경하였다. (조금 거슬리는 부분은 compareTo 메서드를 임의로 역순으로 정렬되게 설정하였다는 것이다) 여전히 파란불이 떳다! 둘다 돼네~ 하고 넘어가자고 하니,..

일급 컬렉션(First Class Collection)의 소개, 써야할 이유

최근에 List 타입이나 Map의 반환타입을 가진 메서드나 혹은 필드를 일급컬렉션으로 대체해보라는 코멘트를 받았다. 일급 컬렉션.... 일급컬렉션 ... 분명 들어본...것 같은데? 뭐였지... 뭐였더라? 이런 생소한(?), 혹은 알듯 말듯한 용어가 나올때마다 당황스럽지만 우리에게는 구몬 아니 구글 선생님이 계신다! 오픈카톡방에서 조졸두님이라 불리우는 동욱님의 블로그에 '일급 컬렉션'에서 원하는 내용이 있었다. 그것도 자세히! 간단하게 정리해보면서 내 것으로 만들어 보려고 한다. (오늘도 동욱님의 공유지식에 감사함을 느끼며.... 정리를 시작해보자) 일급 컬렉션은 나뿐만 아니라 대부분의 개발자들에게 쉽지 않은 개념이였던 것 같다. 일급 컬렉션은 객체지향적으로, 리팩토링하기 쉬운 코드를 만들기 위해 필요하..

J2SE,J2EE의 차이점

매일 사용하고 있음에도 가장 무지한 부분이 아닌가 생각한다. 어쩌면 가장 기초적인 내용임에도 불구하고 잘 모르고 썻던 것 같다. 개발 서적에 자주 언급되는 J2EE, J2SE(백엔드 공부를 하다보니 J2SE를 더 자주 접했던것 같기는 하다)가 어떤 것인지 간략히 정리해본다. J2EE : 위키백과 링크 자바 플랫폼, 엔터프라이즈 에디션 - 위키백과, 우리 모두의 백과사전 위키백과, 우리 모두의 백과사전. 자바 플랫폼, 엔터프라이즈 에디션(Java Platform, Enterprise Edition; Java EE)은 자바를 이용한 서버측 개발을 위한 플랫폼이다. Java EE 플랫폼은 PC에서 동작하는 표준 플랫폼인 Java SE에 부가하여, 웹 애플리케이션 서버에서 동작하는 장애복구 및 분산 멀티티어를 ..

Java - extends, implements, abstract 차이

상속이라는 개념을 이론적으로 분명 학습했는데, 잘 안쓰다보니(직접 만들어 쓴적은 없다는 얘기... JpaRepository 인터페이스 같은 것은 자주 사용...) 그 개념을 자꾸 잊어먹어서 implements, extends 심지어 추상 클래스 ,메서드인(명칭만 기억하지 기능은 기억도 안나는 것 같다.. 반성한다) abstract 등 한번 기회 잡아서 다시 정리해야지하며 미뤘는데, 어쩌다보니 스터디원분이 상속 얘기를 꺼내면서, 매우 진땀을 흘렸다. 민망해서 밤에 이불킥 몇 번 하다가 내 선생님인 '구글'을 찾아봤다. 마침 간단하게 잘 요약해둔 블로그가있어서, 옮겨적어본다. 참고: https://velog.io/@hkoo9329/%EC%9E%90%EB%B0%94-extends-implements-%EC%B..

Java - Equals, Hashcode 메소드

Equals와Hashcode, toString 등.. 기존의 정의되어있는 메소드를 오버라이딩을 하여 사용하는 경우가 많다. Equals와 toString은 어떤 용도로 사용하는지 알고있었지만, Hashcode의 경우 IntelliJ IDE의 자동완성 기능에서 종종 보았지만, 의미와 용도를 잘 모르고 지나치는 경우가 많았다. (부끄럽지만 알아볼 생각도 안했다) Equals와 Hashcode 메소드에 대해 알아보자 ! 아래의 코드와 같이 member1객체와 member2객체를 선언하고 객체를 비교하였을 때, 결과는 어떻게 될까? 답은 당연히 false일 것이다. 그 이유는 둘은 동일 객체가 아니기 때문이다. public class Test { public static void main(String[] arg..

프로그래밍 공부/Java

JDK 21, Virtual Thread

728x90

Virtual Thread란…?

  • JDK 21에 새롭게 들어온 개념 (2023.09.19 에 LTS 출시)
    • gradle 8.4v 부터 지원
    • kotlin v1.9.20 부터 21 바이트 코드 지원
    • Spring 6.1, Spring boot 3.2 부터 지원
    • Jetbrain Intellij 2023.3

JDK 21(LTS)에 추가된 경량 스레드, OS 스레드를 그대로 사용하지 않고 JVM 내부 스케줄링을 통해서 수십만 ~ 수백만개의 스레드를 동시에 사용할 수 있게한다.

전통적인 Java의 Thread

  • Java의 Thread는 OS Thread를 랩핑한 것 (Platform Thread)
  • Java 애플리케이션에서 Thread를 사용하면 실제로 OS Thread를 사용한 것
  • OS Thread는 생성 갯수가 제한적이고 생성, 유지하는 비용이 비쌈
  • 이 때문에 애플리케이션에서는 플랫폼 스레드를 효율적으로 사용하기 위해 Thread Pool을 사용함
 

위와 같은 동작 매커니즘에의한 Throughput(처리량) 의 한계

  • 기본적인 Web Request 처리 방식은 Thread Per Request(하나의 요청/하나의 스레드)
  • 처리량을 높이려면 스레드 증가 필요, But 스레드는 한정적이다. (OS 스레드 제약)

특히나 Blocking I/O 쪽에서 많은 문제가 발생함.

  • Thread 에서 I/O 작업을 처리할 때, Blocking이 일어난다.
  • 작업을 처리하는 시간보다 대기하는 시간이 길다.
 

그래서 우리는 Reactive Programming을 사용했었음.

  • Webflux 스레드를 대기하지 않고 다른 작업 처리 가능 (장점)
  • 코드를 작성하고 이해하는 비용이 높다 (단점)
  • Reactive하게 동작하는 라이브러리 지원을 필요로 한다 (단점)
  • JPA를 사용할 수 없고 R2DBC라는 래퍼런스가 상대적으로 적은 라이브러리를 사용해야 함 (단점)
 

그렇다면 왜 이렇게 구성이 되어있을까…?

Java Design

  • 자바의 디자인은 ‘스레드 중심’으로 구성되어있다.
  • Exception Stack trace, Debugger, Profiling 모두 스레드 기반
  • Reactive할 때, 작업이 여러 스레드를 거쳐 처리되는데, 컨택스트 확인이 어려워 디버깅이 어려움.

Virtual Thread가 해결하고자하는 문제

  • 애플리케이션의 높은 처리량 확보
    • Blocking 발생시 내부 스케줄링을 통해 다른 작업을 처리 (= 기존 자바의 한계)
  • 자바 플랫폼의 디자인과 조화를 이루는 코드 생성
    • 기존 스레드 구조 그대로 사용(= Webflux가 못한부분)
 

결론은 Virtual Thread는 Reacitve와 MVC의 장점만 차용한 케이스!

 
 

Virtual Thread가 앞에 따로 존재함.
뒤에 Fork/Join Pool이 Carrier Thread(Platform Thread와 거의 동일한 형태)
Carrier Thread는 OS Thread와 1:1 매핑되는 구조이긴 하지만 실제 Application에서는 Platform Thread가 아니라 Virtual Thread만 사용하게된다.

 
  • Virtual Thread가 Blocking되면 Virtual Thread와 Carrier Thread에서 Unmount 된다.
  • 그리고 다른 Virtual Thread가 해당 Carrier Thread와 Mount 된다.
  • OS Thread가 갯수에 제한이 있는 것에 비해서 Virtual Thread는 엄청난게 많이 생성할 수 있다.
 

사용법 예시

// Virtual Thread 방법 1
Thread.startVirtualThread(() -> {
    System.out.println("Hello Virtual Thread");
});

// Virtual Thread 방법 2
Runnable runnable = () -> System.out.println("Hi Virtual Thread");
Thread virtualThread1 = Thread.ofVirutal().start(runnable);

// Virtual Thread 이름 지정 
Thread.Builder builder = Thread.ofVirtual().name("JVM-Thread");
Thread virtualThread2 = builder.start(runnable);

// 스레드가 Virtual Thread인지 확인하여 출력
System.out.println("Thread is Virtual? " + virtualThread2.isVirtual());

// ExecutorService 사용 
try(final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 3; i++) {
        executorService.submit(runnable);
    }
}

Spring Boot(MVC) 적용법 (3.2 이상)

# application.yaml

spring:
  threads:
    virtual:
      enabled: true

Spring Boot(MVC) 적용법 (3.x)

// Web Request를 처리하는 Tomcat이 Virtual Thread를 사용하여 유입된 요청을 처리하도록 한다. 
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
    return protocolHandler -> {
        protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
    }
}

// Async Task에 Virtual Thread 사용
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
public AsyncTaskExecutor asyncTaskExecutor() {
    return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}

유의사항

유의사항 1

  • Platform Thread => Virtual Thread (X)
    • 전통적으로 사용하던 이 개념으로 Virtual Thread를 쓰게되면 성능 향상을 체감할 수 없을 것이다.
  • Task => Virtual Thread (O)

리소스라고 생각하기보다는 Task별로 Virtual Thread 할당

유의사항 2

Thread Local 사용시 주의

  • Platform Thread Pool을 사용할 때, 공유를 위해 ThreadLocal을 사용하던 관습
  • Virtual Thread는 Heap을 사용하기 때문에 이를 남발하면 메모리 사용이 늘어남.
    • Platform Thread와는 달리 Virtual Thread는 수십 수백만개까지 생성될 수 있기 떄문에 메모리 점유량이 확 늘어날 수 있어 메모리 이슈 발생 가능성이 있음.

유의사항 3

synchronized 사용시 주의

synchronized 사용시 Virtual Thread에 연결된 Carrier Thread가 Blocking 될 수 있으니 주의
(이런 경우를 pinning 이라고 함)

// synchronized 사용 (pinning 발생)
// 순차적 접근을 보장한다. 
public synchronized String accessResource() {
    return access();
}

// ReentrantLock 사용 (pinning 발생하지 않음)
private static final ReentrantLock LOCK = new ReentrantLock();

public String accessResource() {
    // 순차적 접근을 보장한다.
    LOCK.lock();
    try {
        return access();
    } finally {
        LOCK.unlock();
    }
}

I/O와 관련된 부분에 있어서는 Virtual Thread를 적용했을 때, 상당히 큰 효과를 누릴 수 있다.

적합한 사용처

  • I/O Blocking이 발생하는 경우 Virtual Thread가 적합
  • CPU Intensive 작업에는 적합하지 않음
    • ex) 이미지 프로세싱을해서 썸네일을 만든다. 동영상 인코딩을 한다거나. (I/O Intensive 작업이 아니라 CPU Intensive 작업이기 때문에 Virutal Thread를 도입한다고해서 크게 달라질게 없다)
  • Spring MVC 기반 Web API 제공시 편리하게 사용할 수 있다.
    • 높은 Throughput을 위해서 Webflux를 고려중이라면 대안이 될 수 있다.
    • Webflux 도입에도 (1)Stream 형태의 서비스를 제공해야하는 경우 vs (2)단순히 많은 처리를 하고 싶어서 가 있을 수 있는데, 이 중 (2)의 케이스는 Virtual Thread라는 새로운 대안으로 고민해볼 수 있다.

Virtual Thread에 대한 오해

  • Virtual Thread는 기존(Platform) Thread를 대체하는 것이 목적이 아니다.
  • Virtual Thread는 기다림에 대한 개선, 그리고 플랫폼 디자인과의 조화
  • 도입한다고 무조건 처리량이 높아지지 않는다.
  • Virtual Thread는 그 자체로 Java의 동시성을 완전히 개선했다고 보기는 어렵다.

Virtual Thread의 제약

  • Thread Pool에 적합하지 않다. Task 별로 Virtual Thread를 할당해야 함
  • Thread Local 사용시 메모리 사용이 늘어날 수 있음
  • synchronized 사용시 주의가 필요함. (carrier thread가 blocking 될 수 있음)
    • ReentrantLock을 사용
  • 제한된 리소스의 경우 semaphore를 사용

참고 자료

  • https://www.youtube.com/watch?v=vQP6Rs-ywlQ
  • https://techblog.woowahan.com/15398/
728x90
프로그래밍 공부/Java

Java - Comparable vs Comparator

728x90

Stream 성애자인 나는 오늘도 코딩 테스트 문제나 비즈니스 로직을 짜면서 Stream을 쓰고있다. 하지만 코딩 테스트를 풀면서 생긴 의문점이 있다. 

바로 정렬을 해주는 sorted() 라는 함수인데 이 안의 파라미터로 Comparator 타입이 들어간다는 갓텔리제이의 TMI한 설명이 있다. 중요한건 이 문제를 이렇게도 풀 수 있지만, 아래와 같이 Music 클래스가 Comparable 인터페이스를 구현하고, compareTo 메서드를 재정의 함으로써도 해결할 수 있다. 

sorted 함수의 파라미터를 Music의 comparTo 메서드로 변경하였다. 
(조금 거슬리는 부분은 compareTo 메서드를 임의로 역순으로 정렬되게 설정하였다는 것이다)

여전히 파란불이 떳다! 

둘다 돼네~ 하고 넘어가자고 하니, 참된 개발자가 아닌 것 같다는 생각과 함께 자괴감에 빠질 것 같아. 이참에 가려운 곳을 긁어보기로 했다. 

1. Interface Comparable

정의 :

정렬 수행 시 기본적으로 적용되는 정렬 기준이 되는 메서드를 정의하는 인터페이스이다. java.lang.Comparable package에 위치한다. 
java에서 제공되는 정렬이 가능한 클래스들은 모두 Comparable 인터페이스를 구현하고 있으며, 정렬 시에 이에 맞게 정렬이 수행된다. 
(ex. 숫자형 (오름차순 정렬): Integer, Doble, Long ...., 문자열 (사전순 정렬) : String )

구현 방법 : 

정렬할 객체에 Comparable interfacefmf implements하고, compareTo() 메서드를 오버라이드 한다! 

더보기

💡 CompareTo() 메서드 작성법
∙ 현재 객체 < 파라미터로 넘어온 객체 : 음수 리턴
∙ 현재 객체 == 파라미터로 넘어온 객체 : 0 리턴 
∙ 현재 객체 > 파라미터로 넘어온 객체 : 양수 리턴 
∙ 음수 또는 0 이면 객체의 자리가 그대로 유지되며, 양수인 경우에는 두 객체의 자리가 바뀐다.

아래는 Integer의 compareTo() 메서드다 (Integer class는 Comparble을 

 

사용 방법 : 

∙Arrays.sort(array)
∙Collections.sort(this) 

더보기

참고 : Arrays.sort()와 Collections.sort()의 차이

1. Arrays.sort() 
배열을 정렬할 때 사용
∙ex) byte[], char[], double[], int[], Object[], T[] etc... 등 Object Array에서는 TimSort(병합 정렬 + 삽입 정렬)를 사용
∙Object Array: 새로 정의한 클래스에 대한 배열 * Primitive Array에서는 Dual Pivot QuickSort(퀵 정렬 + 삽입 정렬)를 사용 
∙Primitive Array: 기본 자료형에 대한 배열

2. Collectrions.sort()
List Collection을 정렬할 때 사용
∙Ex) ArrayList, LinkedList, Vector etc.. * 내부적으로 Arrays.sort()를 사용

 

2. Interface Comparator 

정의 :

  • 정렬 가능한 클래스(Comparable 인터페스를 구현한 클래스)들의 기본 정렬 기준과 다르게 정렬하고 싶을 때 사용하는 인터페이스
  • java.util.Comparator package에 위치한다.
  • 주로 익명 클래스로 사용된다. 
  • 기본적인 정렬 방법인 오름차순 정렬을 내림차순으로 정렬할 때 많이 사용한다.

구현 방법 :

  • Comparator interface를 implements gn compare() 메서드를 오버라이드 한 myComparator class를 작성한다. 
  • compare() 메서드 작성 법 (오름 차순의 경우 Comparable의 compareTo() 메서드 구현과 동일 하고, 내림 차순의 경우 두 대소 비교를 반전시키면 된다) 

사용 방법 : 

  • Arrays.sort(array, Comparator);
  • Collections.sort(list, Comparator);
  • Arrays.sort(), Collections.sort() 메서드는 두 번째 인자로 Comparator interface를 받을 수 있다. 

Comparator interface를 이용한 Java 객체 정렬 방식은 두 가지가 있다. 

먼저 첫 번째로는 class T가 Comparator<T>를 구현하게 만들고 compare() 메서드를 오버라이드 하면 된다. 이는 위의 Comparable을 구현하는 것가 거의 유사하기 때문에 생략한다. 

두 번째는 처음에도 언급했듯이, 익명 함수를 이용하는 것이다.

new Comparator<Music> 부분이 비활성화(?) 되어있다. 그 이유는 익명 클래스를 intelligent(= 총명한, 똑똑한)한 갓텔리제이는 람다식으로도 바로 바꿔줄 수 있기 때문이다. (사실, 처음부터 람다식으로 코드를 짯지만, IntelliJ의 quick function 기능으로 lambda 식을 abstract class로 변환하여 익명 클래스로 변경하였다. 보는 것 처럼 익명 클래스는 가독성이 매우 떨어진다. 가독성 적인 측면에서 익명 클래스보다는 람다식을 활용하자!) 

이번 포스팅을 하면서 Comparable과 Comparator의 개념에 대해서 알아보았다. 정리하면서 이제 좀 명확해지는 것 같다. 
간단하게 말하자면 Comparable은 기본적인 정렬할 때 사용, Comparator은 기본 정렬 기준과 다른 정렬을 정의할 때 사용!

참조: https://gmlwjd9405.github.io/2018/09/06/java-comparable-and-comparator.html

 

[Java] Comparable와 Comparator의 차이와 사용법 - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

 

728x90
프로그래밍 공부/Java

일급 컬렉션(First Class Collection)의 소개, 써야할 이유

728x90

최근에 List 타입이나 Map의 반환타입을 가진 메서드나 혹은 필드를 일급컬렉션으로 대체해보라는 코멘트를 받았다. 

일급 컬렉션.... 일급컬렉션 ... 분명 들어본...것 같은데? 

뭐였지... 뭐였더라? 

다소 미화된 필자의 모습

이런 생소한(?), 혹은 알듯 말듯한 용어가 나올때마다 당황스럽지만 우리에게는 구몬 아니 구글 선생님이 계신다!

오픈카톡방에서 조졸두님이라 불리우는 동욱님의 블로그에 '일급 컬렉션'에서 원하는 내용이 있었다. 그것도 자세히! 

간단하게 정리해보면서 내 것으로 만들어 보려고 한다. (오늘도 동욱님의 공유지식에 감사함을 느끼며.... 정리를 시작해보자)


일급 컬렉션은 나뿐만 아니라 대부분의 개발자들에게 쉽지 않은 개념이였던 것 같다. 

일급 컬렉션은 객체지향적으로, 리팩토링하기 쉬운 코드를 만들기 위해 필요하다. 먼저 이 일급 컬렉션의 목적을 상기시키고 시작해보자! 

일급 컬렉션이라는 용어는 '소트웍스 앤솔로지'라는 서적의 객체지향 생활체조 파트에서 언급되었다.

규칙 8: 일급 콜렉션 사용 
이 규칙의 적용은 간단하다. 
콜렉션을 포함한 클래스는 반드시 다른 멤버 변수가 없어야 한다. 
각 콜렉션은 그 자체로 포장돼 있으므로 이제 콜렉션과 관련된 동작은 근거지가 마련된셈이다.
필터가 이 새 클래스의 일부가 됨을 알 수 있다.
필터는 또한 스스로 함수 객체가 될 수 있다.
또한 새 클래스는 두 그룹을 같이 묶는다든가 그룹의 각 원소에 규칙을 적용하는 등의 동작을 처리할 수 있다.
이는 인스턴스 변수에 대한 규칙의 확실한 확장이지만 그 자체를 위해서도 중요하다.
콜렉션은 실로 매우 유용한 원시 타입이다.
많은 동작이 있지만 후임 프로그래머나 유지보수 담당자에 의미적 의도나 단초는 거의 없다.

간단하게 설하면 아래의 코드를

Map<String, String> map = new HashMap<>();
map.put("1", "A");
map.put("2", "B");
map.put("3", "C");

아래와 같이 Wrapping 하는 것을 얘기한다.

public class GameRanking {
    private Map<String, String> ranks;
    
    public GameRank(Map<String, String> ranks) {
        this.ranks = ranks;
    }
}

Collection을 Wrapping하면서, 그 외 다른 변수가 없는 상태일급 컬렉션이라 한다. 
Wrapping 함으로써 다음과 같은 이점을 가지게 된다. 

  1. 비즈니스에 종속적인 자료구조
  2. Collection의 불변성을 보장
  3. 상태와 행위를 한 곳에서 관리
  4. 이름이 있는 컬렉션

이제부터 하나 하나 소개를 해나간다.

1. 비즈니스에 종속적인 자료구조 

예를 들어 다음과 같은 조건으로 로또 복권 게임을 만든다고 가정하자.

로또 복권은 아래의 조건을 가진다.

  • 6개의 번호가 존재 (보너스 번호는 이번 예제에서 제외하겠다)
  • 6개의 번호는 서로 중복되지 않아야 함

일반적으로 이런 일은 서비스 메서드에서 진행한다.
그래서 구현을 해보면 아래처럼 된다. 

public class LottoService {
    private static final int LOTTO_NUMBERS_SIZE = 6;
    
    public void createLottoNumber() {
        List<Long> lottoNumbers = createNonDuplicateNumbers();
        validateSize(lottoNumbers);
        validateDuplicate(lottoNumbers);
        
        //이후 로직 실행
    }
    
    private void validateSize(List<Long> lottoNumbers) {
        if (lottoNumbers.size() != LOTTO_NUMBERS_SIZE) {
            throw new IllegalArgumentException("로또 번호는 6개만 가능합니다");
        }
    }
    
    private void validateDuplicate(List<Long> lottoNumbers) {
        Set<Long> nonDuplicateNumbers = new HashSet<>(lottoNumbers);
        if(nonDuplicateNumbers.size() != LOTTO_NUMBERS_SIZE) {
            throw new IllegalArgumentException("로또 번호들은 중복될 수 없습니다");
        }
    }
    ....
    

서비스 메서드(= createLottoNumber 메서드)에서 비즈니스 로직을 처리했다. 
이럴 경우 큰 문제가 있는데.

로또 번호가 필요한 모든 장소에선 검증로직이 들어가야만 한다. 

  • List<Long> 으로 된 데이터는 모두 검증 로직이 필요할까?
  • 신규 입사자분들은 어떻게 이 검증로직이 필요한지 알 수 있을까?

등등 모든 코드와 도메인을 알고 있지 않다면 언제든 문제가 발생할 여지가 있다. 

그렇다면 이 문제를 어떻게 깔끔하게 해결할 수 있을까? 

  • 6개의 숫자로만 이루어져야만 하고
  • 6개의 숫자는 서로 중복되지 않아야만 하는

이런 자료구조가 없을까? 
없으면 우리가 직접 만들면 된다.

아래와 같이 해당 조건으로만 생성할 수 있는 자료구조를 만들면 위에서 언급한 문제들이 모두 해결된다. 

그리고 이런 클래스를 우린 일급 컬렉션이라 부른다.

public class LottoTicket {
    private static final int LOTTO_NUMBERS_SIZE = 6;
    
    private final List<Long> lottoNumbers;
    
    public LottoTicket(List<Long> lottoNumbers) {
        validateSize(lottoNumbers);
        validateDuplicate(lottoNumbers);
        this.lottoNumbers = lottoNumbers;
    }
    
    private void validateSize(List<Long> lottoNumbers) {
        if (lottoNumbers.size() != LOTTO_NUMBERS_SIZE) {
            throw new IllegalArgumentException("로또 번호는 6개만 가능합니다");
        }
    }
    
    private void validateDuplicate(List<Long> lottoNumbers) {
        Set<Long> nonDuplicateNumbers = new HashSet<>(lottoNumbers);
        if(nonDuplicateNumbers.size() != LOTTO_NUMBERS_SIZE) {
            throw new IllegalArgumentException("로또 번호들은 중복될 수 없습니다.");
        }
    }
}

이제 로또 번호가 필요한 모든 로직은 이 일급 컬렉션만 있으면 된다. 

public class LottoService2 {
    public void createLottoNumber() {
        LottoTicket lottoTicket = new LottoTicket(createNonDuplicateNumbers());
        // 이후 로직 쭉쭉 실행 
    }

 

2. 불변

일급 컬렉션은 컬렉션의 불변을 보장한다. 

여기서  final 을 사용하면 안되는가? 라는 질문을 많이 한다. 
하지만 똑바로 알아야 하는 것이 있는데, Java의  final 은 정확히는 불변을 만들어주는 것은 아니며, 재할당만 금지한다.

아래 테스트 코드를 참고하자.

@Test
public void final도_값변경이_가능하다() {
    //given
    final Map<String, Boolean> collection = new HashMap<>();
    
    //when
    collection.put("1",true);
    collection.put("2",true);
    collection.put("3",true);
    collection.put("4",true);
    
    //then
    assertThat(collection.size()).isEqualTo(4);
}
    

이를 실행하면 

위의 결과와 같이 값이 추가되는 것을 확인할 수 있다.
이미   collection   은 비어있는 HashMap으로 선언되었음에도 값이 변경될 수 있다는 것이다. 

예로들어 위의 코드의 //when 부분만 아래의 코드와 같이 변경된다면

//when
collection = new HashMap<>();

컴파일 에러가 발생한다. 

final로 할당된 코드에 재할당 할 수는 없기 때문이다.

지금까지 본 것처럼Java의 final은 재할당만 금지한다. 
이외에도 member.setAge(10) 과 같은 코드 역시ㅣ 작동해버리기 반쪽짜리라 할 수 있다. 

요즘과 같이 소프트웨어 규모가 커지고 있는 상황에서 불변 객체는 아주 중요하다.
각각의 객체들이 절대 값이 바뀔일이 없다는게 보장되면 그만큼 코드를 이해하고 수정하는데 사이드 이펙트가 최소화되기 때문이다. 

Java에서는 final로 그 문제를 해결할 수 없기 때문에 일급 컬렉션(First Class Collection)과 래퍼 클래스 (Wrapper Class)등의 방법으로 해결해야만 한다. 

그래서 아래와 같이 컬렉션의 값을 변경할 수 있는 메서드가 없는 컬렉션을 만들면 불변 컬렉션이 된다. 

public class Orders {
    private final List<Order> orders;
    
    public Orders(List<Order> orders) {
        this.orders = orders;
    }
    
    public long getAmountSum() {
        return orders.stream()
                     .mapToLong(Order::getAmount)
                     .sum();
    }
}

이 클래스는 생성자와 getAmountSum() 외에 다른 메서드가 없다.
즉, 이 클래스의 사용법은 새로 만들거나 값을 가져오는 것뿐이다.
List라는 컬렉션에 접근할 수 있는 방법이 없기 때문에 값을 변경/추가 할 수 없다.

이렇게 일급 컬렉션을 사용하면, 불변 컬렉션을 만들 수 있다. 

(불변성은 항상 보장되는 것이 아니라, 선택적인 요소라는 점은 조심하자👀)

3. 상태와 행위를 한 곳에서 관리 

일급 컬렉션의 세번째 장점은 값과 로직이 함께 존재한다는 것이다. (이 부분은 Enum의 장점과도 같다)

예를 들어 여러 Pay들이 모여있고, 이 중 NaverPay 금액의 합이 필요하다고 가정해보자.
일반적으로 아래와 같이 작성한다. 

@Test
public void 로직이_밖에_있는_상태() {
    //given
    List<Pay> pays = Arrays.asList(   //값이 있는 곳
        new Pay(NAVER_PAY, 1000L),
        new Pay(NAVER_PAY, 1500L),
        new Pay(KAKAO_PAY, 2000L),
        new Pay(TOSS, 3000L));
        
     //when
     Long naverPaySum = pays.stream()   // 계산은 여기서
             .filter(pay -> pay.getPayType().equals(NAVER_PAY))
             .mapToLong(Pay::getAmount)
             .sum();
             
     //then
     assertThat(naverSum).isEqualtTo(2500L);
}
  • List에 데이터를 담고
  • Service 혹은 Util 클래스에서 필요한 로직 수행 

이 상황에서는 문제가 있다.
결국   pays   라는 컬렉션과 계산 로직은 서로 관계가 있는데, 이를 코드로 표현이 안된다.

Pay타입의 상태에 따라 지정된 메서드에서만 계산되길 원하는데, 현재 상태로는 강제할 수 있는 수단이 없다. 
지금은 Pay타입의 List라면 사용될 수 있기 때문에 히스토리를 모르는 분들이라면 실수할 여지가 많다.

  • 똑같은 기능을 하는 메서드를 중복 생성할 수 있다.
    히스토리가 관리 안 된 상태에서 신규화면이 추가되어야 할 경우 계산 메서드가 있다는 것을 몰라 다시 만드는 경우가 빈번하다.

    만약 기존 화면의 계산 로직이 변경 될 경우, 신규 인력은 2개의 메서드의 로직을 다 변경해야하는지, 해당 화면만 변경해야하는지 알 수 없다. 

    관리 포인트가 증가할 확률이 매우 높다.
  • 계산 메서드를 누락할 수 있다. 
    리턴 받고자 하는 것이 Long 타입의 값이기 때문에 이 계산식을 써야한다고 강제할 수 없다.

결국에 네이버페이 총 금액을 뽑을려면 이렇게 해야한다는 계산식을 컬렉션과 함께 두어야 한다.
만약 네이버페이 외에 카카오 페이의 총 금액도 필요하다면 더더욱 코드가 흩어질 확률이 높다.

그래서 이 문제 역시 일급 컬렉션으로 해결한다.

public class PayGroups {
    private List<Pay> pays;
    
    public PayGroups(List<Pay> pays) {
        this.pays = pays;
    }
    
    public Long getNaverPaySum() {
        return pays.stream()
                   .filter(pay -> PayType.isNaverPay(pay.getPayType()))
                   .mapToLong(Pay::getAmount)
                   .sum();
    }
}

만약 다른 결제 수단들의 합이 필요하다면 아래와 같이 람다식으로 리팩토링 가능하다. 

public class PayGroups {
    private List<Pay> pays;
    
    public PayGroups(List<Pay> pays) {
        this.pays = pays;
    }
    
    public Long getNaverPaySum() {
        return getFilteredPays(pay -> payType.isNaverPay(pay.getPayType()));
    }
    
    public Long getKakaoPaySum() {
        return getFilteredPays(pay -> payType.isKakaoPay(pay.getPayType()));
    }
    
    private Long getFilteredPays(Predicate<Pay> predicate) {
        return pays.stream()
                .filter(predicate)
                .mapToLong(Pay::getAmount)
                .sum();
    }
}

이렇게 PayGroups라는 일급 컬렉션이 생김으로서 상태와 로직이 한 곳에서 관리 된다. 

4. 이름이 있는 컬렉션

마지막 장점은 컬렉션에 이름을 붙일 수 있다는 것이다.
같은 Pay들의 모임이지만 네이버페이의 List와 카카오페이의 List는 다르다. 
그렇다면 이 둘을 구분하려면 어떻게 해야할까? 
가장 흔한 방법은 변수명을 다르게 하는 것이다. 

@Test
public void 컬렉션을_변수명으로() {
    //given
    List<Pay> naverPays = createNaverPays();
    List<Pay> kakaoPays = createKakaoPays();
    
    //when
    
    //then
    ...
}

위 코드의 단점이 뭘까? 

  • 검색이 어렵다

    네이버페이 그룹이 어떻게 사용되는지 검색 시 변수명으로만 검색할 수 있다.

    이 상황에서 검색은 거의 불가능하다.

    네이버페이의 그룹이라는 뜻은 개발자마다 다르게 지을 수 있기 때문이다. 
  • 명확한 표현이 불가능하다

    변수명에 불과하기 때문에 의미 부여가 어렵다.


    이는 개발팀 / 운영팀간에 의사소통시 보편적인 언어로 사용하기가 어려움을 의미한다.

    중요한 값임에도 이를 표현할 명확한 단어가 없는 것이다. 

위 문제 역시 일급 컬렉션으로 쉽게 해결할 수 있다. 

네이버페이 그룹과 카카오페이 그룹 각각의 일급 컬렉션을 만들어 이 컬렉션 기반으로 용어사용과 검색을 하면 된다. 

@Test
public void 일급컬렉션의_이름으로() {
    //given
    NaverPays naverPays = new NaverPays(createNaverPays());
    
    KakaoPays kakaoPays = new KakaoPays(createKakaoPays());
    
    //when
    
    //then
}

개발팀 / 운영팀 내에서 사용될 표현은 이제 이 컬렉션에 맞추면 된다. 
검색 역시 이 컬렉션 클래스를 검색하면 모든 사용 코드를 찾아낼 수 있다. 

 


마무리하며.....

보통 다른 블로그에서 글을 읽어도 나만의 방식으로 많이 바꿔서 적는 편인데(원래 그러는게 맞는건가?
동욱님 블로그 설명이 워낙 훌륭해서 그런것도 있고, 내가 일급 컬렉션에 대해 아는게 전혀 없었기 때문에 예를 들거나하는 부분에 더 창의성을 발휘 하기 어려웠던게 아닌가 싶다. 

나의 글이 읽기 힘들다면.... "기억보단 기록을" 블로그에서 일급 컬렉션에 대해 정리해놓은 글을 보라! 

 

 

 

 

 

728x90

'프로그래밍 공부 > Java' 카테고리의 다른 글

JDK 21, Virtual Thread  (1) 2024.03.17
Java - Comparable vs Comparator  (0) 2020.08.03
J2SE,J2EE의 차이점  (0) 2020.03.14
Java - extends, implements, abstract 차이  (0) 2020.01.13
Java - Equals, Hashcode 메소드  (3) 2019.12.15
프로그래밍 공부/Java

J2SE,J2EE의 차이점

728x90

매일 사용하고 있음에도 가장 무지한 부분이 아닌가 생각한다. 어쩌면 가장 기초적인 내용임에도 불구하고 잘 모르고 썻던 것 같다. 개발 서적에 자주 언급되는 J2EE, J2SE(백엔드 공부를 하다보니 J2SE를 더 자주 접했던것 같기는 하다)가 어떤 것인지 간략히 정리해본다. 

J2EE : 위키백과 링크

 

자바 플랫폼, 엔터프라이즈 에디션 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 자바 플랫폼, 엔터프라이즈 에디션(Java Platform, Enterprise Edition; Java EE)은 자바를 이용한 서버측 개발을 위한 플랫폼이다. Java EE 플랫폼은 PC에서 동작하는 표준 플랫폼인 Java SE에 부가하여, 웹 애플리케이션 서버에서 동작하는 장애복구 및 분산 멀티티어를 제공하는 자바 소프트웨어의 기능을 추가한 서버를 위한 플랫폼이다. 이전에는 J2EE라 불리었으나 버전 5.0 이후로 J

ko.wikipedia.org

J2SE : 위키백과 링크

 

자바 플랫폼, 스탠더드 에디션 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 자바 플랫폼 스탠더드 에디션(Java Platform, Standard Edition, 약자 Java SE)는 데스크톱 및 서버, 최근의 고사양 임베디드 시스템을 위한 표준 자바 플랫폼으로 표준적인 컴퓨팅 환경을 지원하기 위한 자바 가상 머신 규격 및 API 집합을 포함한다. 따라서 자바 EE, 자바 ME 등 다른 플랫폼은 구체적인 목적에 따라 자바 SE를 기반으로 API를 추가하거나 자바 가상 머신 규격 및 API의 일

ko.wikipedia.org

 

Java SE(Standard Edition)

자바 스탠다드 에디션은 가장 보편적으로 쓰이는 자바 API집합체다. 예전에는 J2SE로 불렸으나 버전 6.0이후에 Java SE로 변경되었다. 이전에는 썬마이크로시스템즈에서 관리했으나 지금은 JCP주도하에 개발되고 있다. 일반 자바 프로그램 개발을 위한 용도로 사용되며 스윙이나 AWT와 같은 GUI방식의 기본 기능이 포함된다. 

Java EE(Enterprise Edition)

자바 엔터프라이즈 에디션은 자바를 이용한 서버측 개발을 위한 플랫폼이다. Java EE는 표준 플랫폼인 Java SE를 사용하는 서버를 위한 플랫폼이다. 전사적 차원(대규모의 동시 접속과 유지가 가능한 다양한 시스템의 연동 네트워크 기반 총칭)에서 필요로 하는 도구로 EJB, JSP, Servlet, JNDI 같은 기능을 지원하며 WAS(Web Application Server)를 이용한 프로그램 개발 시 사용된다. 

 

 

728x90
프로그래밍 공부/Java

Java - extends, implements, abstract 차이

728x90

상속이라는 개념을 이론적으로 분명 학습했는데, 잘 안쓰다보니(직접 만들어 쓴적은 없다는 얘기... JpaRepository 인터페이스 같은 것은 자주 사용...) 그 개념을 자꾸 잊어먹어서 implements, extends 심지어 추상 클래스 ,메서드인(명칭만 기억하지 기능은 기억도 안나는 것 같다.. 반성한다) abstract 등 한번 기회 잡아서 다시 정리해야지하며 미뤘는데, 어쩌다보니 스터디원분이 상속 얘기를 꺼내면서, 매우 진땀을 흘렸다. 민망해서 밤에 이불킥 몇 번 하다가 내 선생님인 '구글'을 찾아봤다. 마침 간단하게 잘 요약해둔 블로그가있어서, 옮겨적어본다.  

참고: https://velog.io/@hkoo9329/%EC%9E%90%EB%B0%94-extends-implements-%EC%B0%A8%EC%9D%B4

 

자바 extends, implements 차이

상속이란 (Inheritance) 상속을 말하기 전에 먼저 OOP가 무엇인지 알면 좋을거 같다.OOP(Object-Oriented Programming, 객체 지향 프로그래밍) 이란? OOP의 특징으로 1. 상속과 인터페이스 (계층성) 2. 다형성, 사용편의성 (모듈화) 3. 캡슐화, 정보은닉 4. 자료 추상화 (모델링) 5. 동적 바인딩 위에 링크에는...

velog.io

 

상속(Inheritance)이란 

상속을 말하기전에 먼저 객체 지향 프로그래밍(OOP)의 특징을 알면 좋다.

OOP의 특징은 아래의 5가지이다.

  1. 상속과 인터페이스(계층성)

  2. 다형성, 사용편의성(모듈화)

  3. 캡슐화, 정보은닉

  4. 자료 추상화(모델링)

  5. 동적 바인딩

상속은 아래의 그림을 통해서 이해를 도울 수 있다.

각 상자는 모두 객체(Object)이고, 자바 용어로는 클래스이다.
이러한 계층을 표현하기 위해서 만들어진 것이 상속이다.
하위 객체는 상위 객체(부모)의 특징을 물려받는다.

또 다른 예로 변수또한 객체에 선언되어 있다면 물려받게 된다.
이 메소드 또는 변수를 '구현'하는가 그대로 '사용'하는가에 따라서 상속의 형태가 갈리게 된다.

  1. extends 
    사실상 extends가 상속의 대표적인 형태다.
    부모에서 선언 / 정의를 모두하여 자식은 메소드 / 변수를 그대로 사용할 수 있다.
    '다중상속'을 지원하지 않는다.

  2. Implements (interface 구현)
    부모 객체는 선언만 하며 정의(내용)은 자식에서 오버라이딩(재정의)해서 사용한다.
    (extends가 못하는) 다중상속을 대신해준다.

  3. abstract
    extends와 implements의 혼합, extends하되 몇 개는 추상 메서드로 구현되어 있다.

정리

  1. extends는 일반 클래스와 abstract 클래스 상속에 사용되고, implements는 interface 상속에 사용된다.
  2. class to class, interface to interface 상속은 extends를 사용한다.
  3. class to interface 를 사용할 땐 implements를 써야하고
  4. interface to class 를 사용할 땐 implements를 사용할 수 없다.
  5. extends는 1 개의 상속만 받을 수 있다.
  6. extends 자식 클래스는 부모 클래스의 기능을 사용한다. 
  7. implements는 여러개 사용 가능하다.
  8. implements는 설계 목적으로 구현 가능하다.
  9. implements한 클래스는 implements의 내용을 다 사용해야한다.

 

 

728x90
프로그래밍 공부/Java

Java - Equals, Hashcode 메소드

728x90

Equals와Hashcode, toString 등.. 기존의 정의되어있는 메소드를 오버라이딩을 하여 사용하는 경우가 많다.
Equals와 toString은 어떤 용도로 사용하는지 알고있었지만, Hashcode의 경우 IntelliJ IDE의 자동완성 기능에서 종종 보았지만, 의미와 용도를 잘 모르고 지나치는 경우가 많았다. (부끄럽지만 알아볼 생각도 안했다

Equals와 Hashcode 메소드에 대해 알아보자 !

아래의 코드와 같이 member1객체와 member2객체를 선언하고 객체를 비교하였을 때, 결과는 어떻게 될까? 답은 당연히 false일 것이다. 그 이유는 둘은 동일 객체가 아니기 때문이다.

public class Test {

    public static void main(String[] args) {

        final Member member1 = new Member("1");
        final Member member2 = new Member("1");

        System.out.println(member1 == member2); //false
        System.out.println(member1.equals(member2)); //false
    }

    public static class Member {

        private String id;

        public Member(final String id) {
            this.id = id;
        }
    }
}

 

동일 객체란 서로 같은 참조를 바라보는 것을 말한다. 예를 들어 아래와 같을 수 있다.

public class Test {

    public static void main(String[] args) {

        final Member member1 = new Member("1");
        final Member member2 = member1;

        System.out.println(member1 == member2); //true
        System.out.println(member1.equals(member2)); //true
    }

    public static class Member {

        private String id;

        public Member(final String id) {
            this.id = id;
        }
    }
}

 

 

위의 예제의 경우 동일 객체이기 때문에, 둘 다 true 값이 나온다.  유저의 id가 같은데 왜 다른 유저가 되는지에 대해 의문을 품을 수 있다. 보통 자바에서 이것을 동등 객체라고 한다. 동등 객체를 비교하기 위해서는 equals 함수를 사용해야 한다. 하지만 이전 예제에서 equals 함수의 결과값도 false가 나왔다. 이는 equals 함수를 재정의 하지 않았기 때문이다. 

객체 비교(equals( ))

다음은 Object의 equals() 메소드이다. 

public boolean equals(Object obj) {...}

Object 클래스의 equals()는 아래와 같은 기능으로써 이 메소드는 비교 연산자인 ==과 동일한 결과를 리턴한다. 오로지 참조값이 같은지(= 동일 객체인지) 확인하는 기능이다.

public boolean equalse(Object obj) {
    return this == obj;
}

자바에서는 두 객체를 동등 비교할 때 equals() 메소드를 흔히 사용한다. equals() 메소드는 두 객체를 비교해서 논리적으로 동등하면 true를 리턴하고 그렇지 않으면 false를 리턴한다. 논리적으로 동등하다는것은 둘의 참조값과 관계없이 객체 내부의 값이 같다는 것을 의미한다. 이 equals 함수를 재정의한 대표적인 예가 String 클래스이다. String 클래스의 equals 함수는 번지수 비교가 아닌, 문자열을 비교한다. 아래는 String Class equals 메소드의 코드이다.

//String class
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

위 소스를 보면 Object로 객체를 받아 하나하나 문자열을 비교하는 것을 볼 수 있다. char은 참조값이 아닌 기본값이기 때무에 == 로 비교가 가능하다. 

팁: String을 이용해 문자열 리터럴방식으로 문자열을 정의하면 동일 객체를 생성하게 되어있다.

예를 들어 Member라는 클래스가 있는데 여기에 Id값이 같은경우에 동등객체로 취급한다면 equals() 메소드를 재정의해서 id필드값이 같음을 비교하면 된다. 아래의 코드를 참조하자.

public class Member {
    public String id;
    public Member(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Member) {
            Member member = (Member) obj;
            if (obj.equals(id)) {
                return true;
            }
        }
        return false;
    }
}

위와 같이 equals 메소드를 재정의하게 되면 Member 객체를 통해 객체를 비교할 때 id가 같다면 두 객체는 동등할 것이다.

  • 동일성 비교== 이다. 객체 인스턴스 의 주소 값을 비교한다.
  • 동등성 비교equals() 메소드를 사용해서 객체 내부의 값을 비교한다.

Java HashCode란?

객체 해시코드란 객체를 식별한 하나의 정수값을 말한다. Object의 hashCode() 메소드는 객체의 메모리 번지를 이용해서 해시코드를 만들어 리턴하기 때문에 객체 마다 다른 값을 가지고있다. 객체의 값을 동등성 비교시 hashCode()를 오버라이딩 할 필요성이 있는데, 컬렉션 프레임워크에서 HashSet, HashMap, HashTable은 다음과 같은 방법으로 두 객체가 동등한지 비교한다.

우선 hashCode() 메소드를 실행해서 리턴된 해시코드 값이 같은지를 본다. 해시 코드값이 같으면 equals() 메소드로 다시 비교한다. 이 두개가 모두 맞아야 동등 객체로 판단한다. 

아래의 예제를 보면 Key 클래스의 equals 메소드를 재정의해서 number 필드값이 같으면 true를 리턴하도록했다. 그러나 hashCode 메소드를 재정의 하지 않았기 때문에 Object hashCode() 메소드가 사용된다.

public class Key {
    public int number;

    public Key(int number) {
        this.number = number;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Key) {
            Key compareKey = (Key) obj;
            if (this.number == compareKey.number) {
                return true;
            }
        }
        return false;
    }
}

이런 경우 두개의 동등한 객체를 HashMap의 식별키로 Key 객체를 사용하면 저장된 값을 찾아 오지 못한다. 왜냐하면 number 필드 값이 같더라도 hashCode() 메소드에서 리턴하는 해시코드가 다르기 때문에 다른 식별키로 인식하기 때문이다. 아래의 예제를 실행해보자!

public class KeyExample{
  public static void main(String[] args){

    //Key 객체를 식별키로 사용해서 String 값을 저장하는 HashMap 객체생성.
    HashMap<Key, String> hashMap = new HashMap<Key, String>();

    //식별키 "new key(1)"로 "홍길동"을 저장함
    hashMap.put(new Key(1), "홍길동");

    //식별키 "new key(1)"로 "홍길동"을 읽어옴
    String value = hashMap.get(new Key(1));
    System.out.println(value);
  }
}

위에 내용에서 Hash Code를 정의하지 않았기 때문에 값은 null이 조회될 것이다. 만약 의도한 대로 홍길동을 읽으려면 아래와 같이 hashCode를 재정의 해야 한다.

public class Key{
...
  @override
  public int hashCode(Object obj){
    return number;
  }
}

저장할 때의 new Key(1)과 읽을 때의 new Key(1)은 다른 참조값의 다른 객체이지만 HashMap은 hashCode()의 리턴값이 같고, equals() 리턴값이 true가 나오기 때문에 두 객체는 동등 객체로 평가하여 하나의 키처럼 사용하게 된다. (즉 같은 식별키로 인식한다는 뜻이다) 이러한 이유로 객체의 동등성 비교를 위해서는 Object의 equals() 메소드만 재정의하지 말고, hashCode() 메소드도 재정의해서 논리적 동등 객체일경우 동일한 해시코드가 리턴되도록 해야한다.

다시한번 강조하지만 키를 기반으로 찾는 것이지 객체의 참조 값으로 찾는 것은 아니다. 고로 hash code값이 같다고해서 객체의 참조 값이 같은 것은 아니다.

해시코드 생성(hash(), hashCode())

object 중에 해쉬코드를 생성해주는 역할이 있다. 이 Object.hash(Object ...values) 메소드는 매개값으로 주어진 값들을 이용해서 해시 값들을 이용해서 해시 코드를 생성하는 역할을 하는데, 주어진 매개값들로 배열을 생성하고 Arrays.hashCode() 재정의할 때 리턴값을 생성하기 위해 사용하면 좋다. 클래스 하나가 여러가지 필드를 가지고 있을 때 이 필드들로부터 해시코드를 생성하게 되면 동일한 필드값을 가지는 객체는 동일한 해시코드를 가질 수 있다. 좀 더 정밀한 동등 객체를 이용한다면 이 방법을 사용할 수 있다.

IDE 또는 lombok을 이용해 더 쉽게 equals, hashCode를 재정의 할 수 있다.

@Override
public int hashCode() {
  return Objects.hash(field1, field2, filed3);
}

 

참고: https://minwan1.github.io/2018/07/03/2018-07-03-equals,hashcode/

 

Wan Blog

WanBlog | 개발블로그

minwan1.github.io

 

728x90