롬복의 @EqualsAndHashCode 애노테이션을 보다가 문득 equals()와 hashCode() 메서드에 대해서 잘 모른다는 사실을 알고 한번 찾아보고 정리한다.

Object 클래스에 정의된 equals(Object obj) 메서드를 보면 ‘==’ 비교결과를 반환한다.

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

그럼 ‘==’ 비교는 무엇을 뜻하는 것일까? 참조형 변수끼리 ‘==’ 비교를 한다면 ‘동일’ 객체를 참조하고 있는지 확인하는 것이다.
(논리적으로 같은 객체가 아니다.)

class Game{
    int star;

    public Game(int star) {
        this.star = star;
    }
}
Game game1 = new Game(1);
Game game2 = game1;

// '동일' 객체를 참조하므로 game1 == game2
Game game1 = new Game(1);
Game game2 = new Game(1);

// 같은 프로퍼티를 가지므로 논리적으로 동일한 객체지만 
// '동일' 객체를 참조하지 않으므로 game1 != game2

하지만 개발을 하다보면 ‘동일’한 객체인지 체크하는 것보다 논리적으로 ‘동등’한지(모든 프로퍼티가 같은 객체인지) 체크할 상황이 더 많다.
이런 경우에는 보통 클래스에 Object.equals(Object obj)를 재정의한다.

class GameWithEquals{
    int star;

    public GameWithEquals(int star) {
        this.star = star;
    }

    @Override
    public boolean equals(Object game) {
        if(!(game instanceof GameWithEquals)) return false;
        return ((GameWithEquals) game).star == this.star;
    }
}
@Test
public void 재정의한_equals_동등비교(){
    GameWithEquals game1 = new GameWithEquals(1);
    GameWithEquals game2 = new GameWithEquals(1);

    assertTrue(game1.equals(game2));
}

단순히 ‘==’ 비교를 하던 Object.equals(Object obj)를 재정의 했고, 객체간의 동등비교가 필요하면 이 함수를 활용하면 되니 모든 문제가 해결되었다고 생각할 수 있지만 equals()와 짝으로 hashcode()메서드도 재정의 되어야 한다.

왜냐하면 컬렉션들(HashSet, HashMap, HashTable …)은 객체가 동등한지 비교할 때 equals()와 hashcode()를 둘다 체크하기 때문이다. 예를들어 equals()만 재정의한 경우에는 다음 테스트를 통과하지 못한다.

@Test
public void Map은_키값비교를_equals만으로_하지않는다(){
    HashMap<GameWithEquals, String> map = new HashMap<>();
    GameWithEquals game1 = new GameWithEquals(1);
    map.put(game1, "HI");

    GameWithEquals game2 = new GameWithEquals(1);
    assertTrue(game1.equals(game2)); // True
    assertEquals(map.get(game2), "HI"); // null반환
}

컬렉션은 키값 객체를 비교할 때 equals()와 hashcode()를 모두 체크하므로 재정의 하지 않은 hashcode()에 의해 테스트는 실패하게 된다.

 

Object.hashCode()는 native로 정의되어 있으며 실제 참조하는 객체에 대한 교유값이므로 ‘==’ 비교와 동일하게 다른객체를 참조한다면 서로 다른값이 반환된다. 논리적인 비교가 되기 위해서는 equals와 마찬가지로 가진 프로퍼티를 이용하여 hashCode를 반환하도록 오버라이드 한다.

 

class GameWithEqualsAndHashCode{
    int star;

    public GameWithEqualsAndHashCode(int star) {
        this.star = star;
    }

    @Override
    public boolean equals(Object game) {
        if(!(game instanceof GameWithEqualsAndHashCode)) return false;
        return ((GameWithEqualsAndHashCode) game).star == this.star;
    }

    @Override
    public int hashCode() {
        return Objects.hash(star);
    }
}
@Test
public void Map은_키값비교를_equals와_hashCode로_한다(){
    HashMap<GameWithEqualsAndHashCode, String> map = new HashMap<>();
    GameWithEqualsAndHashCode game1 = new GameWithEqualsAndHashCode(1);
    map.put(game1, "HI");

    GameWithEqualsAndHashCode game2 = new GameWithEqualsAndHashCode(1);
    assertTrue(game1.equals(game2)); // True
    assertEquals(map.get(game2), "HI"); // HI반환, True
}

정리하면,
객체간의 논리적인 비교를 위해서는 equals()와 hashCode()를 모두 재정의 해야한다.
재정의는 개발자가 작성해도 되지만 IDE의 자동완성이나 Lombok(@EqualsAndHashCode)의 도움으로 자동작성 할 수도 있다.

 

인텔리제이에서 Run/Debug Configuration을 들어가면 Configuration에서 지정할 수 있는 옵션중에 다음 3가지가 있다.
스프링부트에서 프로파일을 지정하고 싶어서 spring.profiles.active=dev 옵션을 주고 싶은데 지정할 포인트가 3가지나 있으니 혼란스러워 정리해보고자 한다.


VM options

JVM이 어플리케이션을 구동시키면서 참고할 옵션을 지정한다.
Intellij로 스프링부트 어플리케이션을 구동시 ‘dev’ 프로파일 설정을 주고 싶다면 -Dspring.profiles.active=dev 옵션을 지정한다.
콘솔화면 상단의 실행 커맨드에서 -D로 옵션이 지정되는걸 볼 수 있다.


Program arguments

자바 메인함수에 String[] args에 바인딩 될 프로그램 파라미터이다.

Intellij에서 옵션을 지정하면 콘솔화면 상단의 실행 커맨드에서 메인클래스 뒤에 인자로 들어가는걸 볼 수 있다.

 

spring.profiles.active=dev 로 지정시 메인함수의 String[] args 안에 spring.profiles.active=dev 문자열 자체가 들어가기는 하지만 스프링부트에서 프로파일로 인식되지는 않는다.(기본 default 프로파일로 동작한다.)
스프링부트에서 외부설정으로 인식되게 하기 위해서는 --spring.profiles.active=dev 처럼 ‘-‘를 2개 붙혀서 옵션을 줘야한다. 1개도 안된다 꼭 2개로 주자.


Environment variables

어플리케이션 구동시 OS의 환경변수에 더해서 지정해 줄 수 있는 key=value 옵션이다.
여러개를 지정하고 싶으면 세미콜론(;)으로 구분하여 지정한다. 설정에서 OS의 환경변수를 모두 제외시킬 수도 있다.
스프링부트 프로파일 옵션을 주고 싶다면 spring.profiles.active=dev 을 입력하며 된다.


3가지 방법으로 모두 프로파일 지정이 가능하며, 각기 다른 프로파일을 지정해서 우선순위를 테스트 해보면 Program arguments로 지정한 프로파일로 스프링부트가 동작한다.
두번째는 VM options, 마지막은 Environment variables 이다.

  • VM options : -Dspring.profiles.active=dev
  • Program arguments : —spring.profiles.active=dev « 우선순위가 제일 높다.
  • Environment variables : spring.profiles.active=dev

 


읽어주셔서 감사합니다. 도움이 되셨다면 광고 클릭 부탁드립니다.

모두 힘내세요! : )

 

 

 

JVM

  • 컴파일된 바이트 코드(.class)파일을 OS에서 실행 가능한 코드로 변환하여 실행한다. 코드 변환을 위해서 인터프리터와 JIT 컴파일러가 동작한다.
  • JVM은 표준스팩이 존재하며 스팩에 따라 구현한 여러 구현체들이 있다.
  • OS에 종속적이기 때문에 윈도우용 JVM, 맥용 JVM 이런식으로 여러 종류가 있다.(밴더에 따라서도 구분됨)
  • JVM은 단독으로 배포되지 않고 JRE에 포함되어 배포된다.

JRE

  • Library를 포함하여 자바프로그램 실행에 필요한 것들을 모아둔 배포판이다.
  • JVM과 핵심 라이브러리, 프로퍼티, 리소스 파일 등을 가지도 있다.

JDK

  • JRE + 개발에 필요한 툴

JVM Architecture

JVM은 컴파일된 클래스파일(바이트 코드)을 OS 위에서 실행하며 실행을 위해 다음 3가지 파트로 나눠진다.

1. 클래스로더 시스템

Loading, Linking, Initialization 3가지 동작으로 클래스를 로드하고 정보를 저장한다.

  • Loading : 클래스 파일을 읽고 클래스 정보를 메소드 영역에 저장한다. 로딩이 완료된 클래스는 해당 클래스 타입의 Class 객체를 생성하여 힙 영역에 저장한다.
  • Linking : Verify, Prepare, Resolve 3가지 단계로 이루어져 있다.
  • Initialization : static 변수의 값을 할당하고 static 블록을 실행한다.

클래스파일(.class)을 로드하는 클래스로더(ClassLoader)는 계층형 구조를 가진다.

  • Bootstrap ClassLoader : $JAVA_HOME/jre/lib/rt.jar 의 클래스 파일들을 로드한다. JVM을 실행하기 위한 핵심 파일들이 로드된다.
  • Extension ClassLoader : $JAVA_HOME/jre/lib/ext 또는 java.ext.dirs 옵션에 정의된 디렉터리의 클래스 파일들을 로드한다.
  • Application ClassLoader : Classpath로 지정한 디렉터리의 클래스 파일을 로드한다. 보통 개발한 클래스 파일들이 여기서 로드된다.

로드한 클래스를 찾을 때는 Application ClassLoader를 시작으로 해서 못찾으면 상위 ClassLoader에게 찾는 클래스를 위임한다. 최상단의 Bootstrap ClassLoader에서도 클래스를 찾지 못하면 익숙한 ClassNotFoundException 이 발생한다.


2. 다양한 메모리

  • 메소드 영역

클래스 레벨의 정보들이 저장된다.(클래스 이름, 부모 클래스 이름, 메서드, 변수, static 변수 등의 정보)
JVM마다 오직 하나의 메소드 영역을 가지며 공유자원이다.
공유자원 : 다른 영역의 메모리에서도 참조가 가능하다.


  • 힙 영역

모든 객체(인스턴스)들의 정보가 힙에 저장된다.
JVM마다 하나의 힙 영역을 가지며 메소드 영역과 같이 공유자원이다. 메소드, 힙 영역은 공유자원으로 여러 쓰레드에서 접근하여 데이터를 가져갈 수 있다. (thread-safe 하지 않는다.)


  • 스택 영역

JVM은 쓰레드마다 하나의 런타임 스택을 생성해주며 이 스택들이 스택 영역에 저장된다.
쓰레드가 가진 스택은 메서드 콜들이 스택 프레임이라 부르는 블럭으로 쌓여있다.(오류가 발생하면 콘솔에 찍히는 그 형태)
공유자원이 아니므로 Thread-safe하며 Thread가 종료될 때 해당 스택은 JVM에 의해 제거된다.


  • PC 레지스터 영역

각 쓰레드마다 분리된 PC 레지스터를 가지며 현재 실행중인 스택 프레임을 가리키는 포인터가 저장된다. 쓰레드가 다음 실행 스택 프레임으로 넘어가면 PC 레지스터도 업데이트 된다.


  • 네이티브 메소드 스택 영역

각 쓰레드마다 분리된 네이티브 메소드 스택 영역을 가진다. 네이티브 메소드 정보가 저장된다.
네이티브 메소드 : 메소드 선언에 native 키워드가 붙어있고 구현은 C나 C++같이 자바가 아닌 다른 언어로 구현되어 있는 메소드


3. 실행 영역

인터프리터, JIT 컴파일러, Garbage Collector로 구성되어 있다.

  • 인터프리터 : 바이트코드는 인터프리터로 한줄씩 네이티브 코드로 바꿔가며 어플리케이션이 실행된다.
  • JIT 컴파일러 : 매번 라인을 새로 읽으며 실행하지 않고 반복되는 코드는 JIT 컴파일러가 모두 네이티브 코드로 바꿔둔다. (효율성을 위해서)
  • GC : 더이상 사용되지 않는(참조되지 않는) 객체들은 GC에 의해 정리된다.

 


읽어주셔서 감사합니다. 도움이 되셨다면 광고 클릭 부탁드립니다.

모두 힘내세요! : )

 

 

 

+ Recent posts