자바 언어에 타입 파라미터라는 개념을 도입한 것이 제네릭스(Generics)이다.
인터페이스나 메서드에서 처리할 타입을 사용하는 쪽에서 지정할 수 있다.

가장 쉬운 예로 List 인터페이스는 제네릭스 기반으로 작성되었다.

List  
booelan add(E e);  
E get(int index);

List<Integer> list; 혹은 List<String> list; 등 사용하는 쪽에서 지정하는 것이다.

제네릭스가 없다면 여러 타입을 담으려면 결국 모든 클래스의 조상인 Object로 지정해야 했을 것이다.

List  
boolean add(Object object);  
Object get(int index);

이렇게 하면 발생하는 문제는 다음과 같다.

  1. 형변환
    매번 사용할 때마다 형변환을 해줘야 한다.

    Integer 타입을 예로 들면,

    Integer value = 4;
    add((Object) value);
    (Integer) ret = get(1);
  2. 타입 체크가 안됨
    Integer 리스트를 만들었는데 모르고 Long을 넣어도 알 수가 없다.

    Long value = 4L;
    add((Object) value);

이러한 이유로 제네릭스를 이용하는 것이 좋다.

참고자료

토비의 스프링 3.1, 이일민

개인적으로 공부하며 정리한 내용으로 정확하지 않을 수 있습니다.


Java에는 변수 타입에는 기본형(Primitive Type)과 참조형(Reference Type)이 있다.
기본형은 short, int, long, float, double, byte, char, boolean로 총 8개가 지원된다.

기본형과 참조형의 차이

기본형 변수는 값을 그대로 저장한다.
반면 참조형 변수는 객체의 레퍼런스를 저장한다.

int a = 3; // 기본형
int b[] = new int[]{1,2,3} // 참조형

기본형은 사이즈가 고정되어있기 때문에 해당 변수의 메모리 공간에 그대로 값을 할당하면 된다.
그런데 참조형에는 객체가 할당되어야 하는데, 그 사이즈가 유동적이다. 그래서 바로 값을 할당할 수 없고 힙(Heap) 영역에서 별도의 메모리공간을 차지하여 그곳에 할당한뒤 변수에는 해당 힙 영역의 주소값을 가리키게 한다.

왜 기본형과 참조형 두 개념이 있는 걸까

기본형도 참조형처럼 레퍼런스를 참조하게 되면 개발자는 기본형과 참조형의 차이를 두지 않고 개발할 수 있을거 같은데 왜 두 개념을 구분했을까 하는 생각이 들었다.

우선 기본형을 참조형처럼 레퍼런스로 참조하는 방식으로 사용하면 기본형으로 사용했을 때에 비해 참조 비용이 증가하게 된다. 변수가 몇개 안되면 성능 차이가 안느껴질 수 있지만 int[]의 사이즈가 매우 크다고 생각 했을 때 모든 요소를 참조할 때 마다 힙 영역의 메모리 주소를 또 다시 참조해야 한다고 하면 비용 차이가 커질 수 있다.

때문에 기본형으로 처리할 수 있는 것은 기본형으로 쓰는 것이 나을 것이다.

래퍼클래스(Wrapper Class)

Java는 객체지향 언어이다. 모든 것을 객체로 다룬다! 문제는 기본형은 객체가 아니라는 거다.
List에 int를 넣고 싶다고 해서 List<int> a;와 같이 선언할 수 없다.
저게 왜 안돼? 라고 생각을 할 수 있는데, 모든 것을 객체로 다루는 Java에서 기본형까지 저런식으로 사용할 수 있게 하려면 그 구현이 매우 복잡했을 것이여서 그렇지 않을 까 한다.

이럴 때 사용하는데 래퍼클래스이다. 래퍼 클래스는 기본형을 객체로 한번 감싼 클래스이다. 때문에 기본형을 객체로 다룰 수 있게 해준다.

public class Integer {
    ...
    private final int value;
    ...
}
// Integer라는 래퍼클래스를 사용하면 List에 정수형값을 담을 수 있다. 
List<Integer> a;

// 기본형은 null값을 가질 수 없지만 래퍼 클래스 변수는 null값을 가질 수 있다.
Integer b = null;

그럼 기본형 대신 항상 래퍼클래스를 쓰면 되지 않을까?

역시나 비용문제가 크다. 객체 생성 비용, 참조 비용등... 기본형에 비해 비싸다. 필요할 때만 써야한다. 

그럼 기본형의 개념을 숨기고 모든 것을 객체로만 처리하게 할 수는 없을까?

컴파일러 내부적으로 기본형의 개념을 가지되, 개발자는 기본형을 모르고 모든 것을 객체로 다룰 수 있게 할 수도 있지 않은가? 라는 생각이 들었다.
예를 들면 Groovy의 경우 int a = 3으로 선언해도 a를 래퍼 클래스(Wrapper Class)로 오토박싱(Autoboxing)하여 모든 것을 객체로 다룬다고 한다.

Java는 왜 이렇게 하지 않는 걸까? 라고 생각해서 찾아보니... 비교적 최근 언어인 Groovy등은 기존 언어의 단점등을 보완해서 새로운 언어로 정의한 것이라 가능하겠지만, Java는 기존에 사용하던 오래된 언어 속하고 이런 것들을 고치기엔 너무나 많은 곳에서 사용되고 있다. 하위호환을 생각했을 때 기본형을 없애기는 힘들 것 같다.

'개발 > Java' 카테고리의 다른 글

[Java] Generics  (0) 2020.05.23
[Java] private vs protected  (0) 2020.02.17
[Java] 불변(Immutable) 속성  (0) 2020.02.04
[Java] 자바 네티이브 인터페이스(JNI)  (0) 2020.02.01
[JVM] 런타임 데이터 영역(Run-time Data Area)  (0) 2020.02.01

private 대신 protect로 설정하는 경우 상속으로 인해 메서드가 재작성 될 수 있음을 인지하고 그에 따른 장단점을를 염두에 두고 설계되어야 한다. 오버라이딩으로 인해 예기치 못한 오류가 발생할 수 있다. 

 

상속 받는 입장에서 protected로 지정하면 상속이 되는줄도 모르고 상속을 받는다. 오버라이딩하는 줄도 모르고 오버라이딩 될 수 있으므로 명시적으로(abstract를 사용하는 등) 추상적인 인터페이스를 제공하는 것이 좋다. 

'개발 > Java' 카테고리의 다른 글

[Java] Generics  (0) 2020.05.23
[Java] Primitive Type, Wrapper Class  (0) 2020.03.07
[Java] 불변(Immutable) 속성  (0) 2020.02.04
[Java] 자바 네티이브 인터페이스(JNI)  (0) 2020.02.01
[JVM] 런타임 데이터 영역(Run-time Data Area)  (0) 2020.02.01

불변(Immutable) 개념에 대한 혼란

Java에서 프리미티브(Primitive) 타입 및 String 객체는 불변 속성이 있다.

음... String 객체는 알겠는데, 프리미티브 타입도 불변하다고 말할 수 있는건가? 하는 의문이 들었고 이것부터 짚고 넘어가고자 한다.

불변(Immutable)의 개념

최초 생성 이후로는 상태(속성)를 변경할 수 없는 성질

String 클래스를 예로 들어보자. Java에서 String은 불변하게 구현되어 있기 때문에 + 연산을 하더라도 초기 생성 이후로는 객체를 수정할 수 없다.

String str = "a";
str += "b";

초기 상태. String str = "a"
재할당. str += "b"

기존에 0xAB에 있던 String에 "b"를 추가하는게 아니라 새로운 "ab" 객체를 만들고 str이 해당 객체를 참조하도록 레퍼런스를 변경하고 있다.

Java에서 불변 속성을 이야기 할 때는 객체에 대한 것이고. 객체가 아닌 프리미티브 타입에 대해선 불변(Immutable)인가 아닌가를 이야기 하기가 어렵다.

왜 프리미티브 타입에 대해선 불변 속성을 따지기가 어려운가

프리미티브 타입은 레퍼런스로 참조되지 않고 변수에 직접 값을 가지고 있다.
int 연산을 예로 들면, 다음과 같다.

불변하다는 것은 어떤 것(Identity)의 속성을 바꿀 수 없는 것을 말하는데, int a에는 속성이랄게 없다.
객체는 명확히 속성이 있다. String 객체는 자신의 char[] 속성을 가지고 있고, 이 char[]을 변경할 수 없기에 immutable 하다고 한다.
int a는 속성이 없다. a는 그 자체로 값이다. 다시 말해 Identity가 곧 value이다. 새로운 연산이 일어날 때 마다 새로운 값(새로운 Identity)로 교체된다. Immutable은 Identity와 속성의 구분이 있어야 적용할 수 있는 개념으로 생각된다.

왜 불변 속성이 필요한 걸까?

우선 왜 Java에서 String을 불변하게 구현했는지 부터 찾아봤다.

  1. 메모리 절약
    String은 광범위하게 사용되는 객체이다. 같은 값을 갖는 경우 기존 객체를 재사용하여 힙 메모리 공간을 절약할 수 있다.

    String str1 = "hello world";
    String str2 = "hello world";

    String이 불변하지 않다면 str1, str2 각각 객체를 생성해야겠지만, 불변하기 때문에 같은 객체를 바라봐도 무방하다.

    String str1 = "a";
    while(...) {
        str1 += "a";
    }

    그러나 위 코드와 같이 String값을 변경하는 연산이 많은 경우, 10만번 연산을 한다고하면, 10만개의 객체가 생성되므로 이런 경우에는 주의해야 한다. 이 경우엔 StringBuffer를 사용하는 것이 좋다. StringBuffer는 불변이 아닌, 힙에 할당된 버퍼의 크기를 늘이거나 줄이면서 동작하기 때문에 실제 메모리에 객체가 1개만 생성된다.

  2. 성능 향상
    String은 매우 자주 사용되는 객체이고, HashMap, HashTable, HashSet 등에서 해시코드 계산도 그만큼 많을 것이다.
    String의 불변 속성으로 인해 최초 한번간 hashCode() 연산이 수행되고 이후에는 캐싱된 값을 리턴하게 되어 성능에서 이득이 있다.

  3. Thread-safe
    상태를 가지는 객체가 여러 스레드에 의해 공유될 때 문제가 생긴다. 이럴 때 사용하는 동기화 기법이 여러가지가 있는데, 예를 들면 lock을 건다던가 등이다. 불변 속성은 동기화 기법은 아니지만 객체를 Read-only로만 제공함으로써 상태를 변경하는 행위를 원천적으로 차단한다. 쓰기 기능을 포기하는 대신 Thread-safe 장점을 얻어간다고 볼 수 있다. 

여기까지가 String 클래스의 불변속성이었다. 

 

레퍼런스

https://stackoverflow.com/questions/18037082/are-java-primitives-immutable

https://www.baeldung.com/java-string-immutable

JNI

자바에서 자바가 아닌 C/C++ 작성된 메소드를 실행 할 수 있다. 실제 구현은 C/C++로 하고 자바에서는 구현체에 대한 인터페이스만 가지게 된다. 이 인터페이스를 JNI라고 한다.

왜 JNI가 필요할까?

자바는 플랫폼 독립적인 언어인데 JNI를 사용하는 순간 플랫폼에 종속되게 된다. 그럼에도 불구하고 사용해야하는 상황들은 다음과 같다.

  • 자바에서 하드웨어 제어
  • 자바에서 지원되지 않는 특정 운영체제 서비스
  • 기존 프로그램에서 자바가 제공하는 서비스를 이용

빌드 과정

  1. 'native' 키워드를 이용해 인터페이스를 포함하는 클래스를 작성한다. 몸체는 C/C++로 작성될 DLL이 대신한다.
    public class MyNative {
    public native void doSomething();
    }
  2. javah를 이용해 클래스를 컴파일한다. 컴파일 결과로 NativeClass.h라는 C 헤더파일을 얻는다.
    javah -jni MyNative
  3. MyNative.h를 프로젝트 내로 옮기고 이를 참조하는 MyNative.cpp를 작성한다. 추가로 다음 경로도 클래스패스에 추가해야한다. 컴파일하여 DLL파일을 얻는다.
    ${jdkRoot}\include\,${jdkRoot}\include\win32
  4. 3번에서 생성한 MyNative.dll을 MyNative.class 파일이 있는 곳에 복사한다.
  5. java 프로그램을 실행한다.
    java MyNative

참고자료

https://jetzt.tistory.com/365

런타임 데이터 영역

pc(program counter) 레지스터

  • cpu 내 기억장치인 레지스터를 지칭하는 것이 아니다.
  • Java는 플랫폼에 독립적으로 동작하기 위해 CPU에서 직접 instruction을 수행하지 않는다.
  • 대신 pc 레지스터라는 별도의 메모리공간를 두고 이를 이용해 instruction을 수행한다.
  • pc 레지스터에는 현재 실행중인 JVM instruction이 저장된다. 네이티브 메서드의 경우는 JVM에서 실행되지 않으므로 undefined값을 가지게 된다.
  • 스레드 마다 pc 레지스터를 가진다.

스택

  • 스레드마다 스택을 가진다. 다른 스레드의 스택영역엔 접근 할 수 없다.
  • 메서드가 호출될 때 마다 스택에 새로운 프레임들이 생성되고 메서드 종료 시 스택에서 제거된다.
  • 스택은 고정사이즈일 수도 있고 동적으로 사이즈를 확장할 수도 있다.
  • 스택의 메모리 부족과 관련된 두가지 종류의 에러가 있다.
    • StackOverflowError - 고정 사이즈의 스택에서 지정된 사이즈를 초과할 경우 발생
    • OutOfMemoryError - 다이나믹 사이즈 스택에서 메모리가 부족할 경우 발생

  • 모든 스레드가 접근할 수 있다.
  • 프리미티브 타입이 아닌 데이터들이 저장되는 영역이다. 클래스 인스턴스, 배열 등이 포함된다. 메모리 관리에 대한 스펙은 규정되어있지 않고 벤더사가 알아서 구현하도록 되어있다.
  • 힙 영역 메모리은 인접해있지 않아도 된다. 힙 영역이 부족해지만 OutOfMemoryError가 발생한다.

메서드 영역(Method Area)

  • 모든 스레드가 접근할 수 있다.
  • 코드들이 저장된다고 볼 수 있다.
  • 클래스의 런다임상수풀, 필드, 메서드 데이타, 메서드 코드, 생성자와 같은 것들을 저장한다.
  • JVM의 시작될 때 같이 생성된다. 논리적으론 힙에 속하지만 가비지 컬렉션을 한다던가 하는 건 벤더사에 맡기고 있다.
  • 고정사이드 혹은 런타임에 확장 가능한 사이즈를 갖기도 한다.
  • 이 영역의 메모리가 부족해지만 OutOfMemoryError가 발생한다.

보충자료

프레임이란?

  • 메서드가 호출될 때 마다 새로운 프레임이 생성된다. 메서드 종료 시 스택에서 제거되며 리턴값을 이전 프레임에게 전달한다.
  • 다이나믹 링킹, 메서드 리턴값, 디스패치 익셉션과 같은 데이터를 저장하기 위해 사용된다.
  • 프레임은 아래와 같이 구성된다.
    • 로컬 변수
      메서드의 지역변수를 배열 형태로 저장한다. 프리미티브 타입의 값은 스택에 같이 저장되고, 레퍼런스 타입은 실제 값은 힙영역에 있고 그 힙영역의 주소값을 스택에 저장한다.
    • 오퍼랜드(Operand) 스택
      계산을 위한 입출력 데이터를 저장하는 공간이다.
    • 런타임상수풀(Run-time constant pool) 레퍼런스
      프레임마다 런타임상수풀(Run-time constant poo) 레퍼런스를 포함하고 있다. 심볼릭 링크 형태의 메서드/변수를 참조하는 경우 런타임상수풀을 참조하여 실제 로드된 스트럭처의 오프셋 형태로 변환한다. 런타임상수풀을 사용함으로써 동적 로딩이 가능해진다.

프레임의 로컬 변수와 오퍼랜드 예제

public int add(int a, int b) {
    int c = a + b;
  return c;
}

이 코드의 바이트 코드는 다음과 같다

Code:
  stack=2, locals=4, args_size=3
    0: iload_1 # 지역변수 1번을 오퍼랜드 스택에 입력(Push)
    1: iload_2 # 지역변수 2번을 오퍼랜드 스택에 입력
    2: iadd # 오퍼랜드 스택에서 두 꺼내어(Pop)을 꺼내어 더 한뒤 그 결과를 오퍼랜드 스택에 입력
    3: istore_3 # 결과를 c에 저장
    4: iload_3 # 리턴 값 꺼내기
    5: ireturn # 리턴
  LocalVariableTable:
      Start  Length  Slot  Name   Signature
      0       6     0    this   LMyMain;
      0       6     1       a   I
      0       6     2       b   I
      4       2     3       c   I


여기서 a = 50, b = 20이라고 하면

출처 : Geeksforgeeks

마지막 ireturn을 실행하면 이전 프레임에게 결과값을 리턴하고 해당 프레임은 소멸된다.

참고자료

https://www.holaxprogramming.com/2013/07/16/java-jvm-runtime-data-area/
https://heowc.tistory.com/53
https://johngrib.github.io/wiki/jvm-stack/

추상화

일반적인 추상화란 중요한 특징을 찾아낸 후 간단하게 표현하는 것이다. 추상화는 여러가지 요소를 하나로 통합하는 방향성을 가지고 있다. 이는 문제를 여가지로 쪼개서 나눠보는 '문제 분할'과는 별개이다. 추상화를 이용하여 핵심적인 것만 남기게 되면 복잡한 내용도 한 눈에 알아볼 수 있어 이해하기 쉽다는 장점이 있다.

컴퓨터공학에서 추상화란 복잡한 자료, 모듈, 시스템에서 핵심적인 개념, 기능등을 간추려내는 것이다. 운영체제는 하드디스크는 파일, 네트워크에 대해 포트, 메모리에 대해 주소, CPU에 대해 프로세스라는 추상화된 방법을 제공한다.

객체지향에서 클래스 관련하여 추상화 요소를 찾아보자. 첫번째로 어떤 관념을 클래스를 정의하는 것 자체부터 추상화이다. 또한 여러 클래스간의 공통 속성/기능을 묶어 새로운 클래스를 정의하는 것도 추상화라고 볼 수있다. 예를 들어 개라는 관념을 Dog라는 클래스로 정의하는 것도 추상화이고, Dog 클래스와 Cat 클래스에서 공통된 기능을 묶어 Animal 클래스를 정의하는 것도 추상화이다. 즉 모든 클래스는 추상화를 통해 만들어진다. 인터페이스, 부모클래스를 정의하는 것은 클래스들을 추상화한 결과로 볼 수 있다.

캡슐화

연관 있는 것들을 하나로 묶어주는 것이 캡슐화이다. 연관있는 변수와 함수를 클래스로 묶고 외부에 감출내용은 감춘다. 잘 캡슐화(모듈화)되었다는 말은 연관있는 것들이 잘 응집되었다는 것이고 다른 모듈(객체, 클래스)와는 결합도가 낮아야 한다. 관심사의 분리, 낮은 결합도를 갖게하는 것이 목적이다.

상속

부모 클래스의 속성/기능은 자식 클래스에게 상속된다. 자식 클래스에서 따로 정의하지 않아도 부모 클래스에서 정의된 것들을 자동으로 상속받는 것이다. 상속을 통해 코드의 재사용성이 증가하여 기능을 확장하기 좋다.

다형성

서로 다른 방식을 하나의 표현으로 사용 할 수 있는 것이 다형성이다. 서로 다른 객체를 하나의 일반화된(추상화된) 클래스 혹은 메소드로 처리할 수 있는 것이다. 어떤 추상화된 객체를 사용하는 입장에서 그 객체가 실제로 무엇인지 몰라도 사용할 수 있어야 한다. 예를 들어 어떤 인터페이스를 사용한다고 할 때 그 인터페이스의 구현체가 무엇인지는 모른채로 사용할 수 있는 것이다. 객체간의 결합도를 낮추어 생산성과 유지보수성이 향상된다.

결론은 '관심사의 분리, 낮은 결합도, 코드의 재사용성'를 갖게하는 원리라고 볼 수 있다.

참고자료

https://terms.naver.com/entry.nhn?docId=3607505&cid=58598&categoryId=59316
https://ko.wikipedia.org/wiki/추상화_(컴퓨터_과학)

Java8에서 도입된 람다표현식은 추상메서드가 1개인 인터페이스를 사용할 때 익명클래스를 사용하지 않고 코드의 구현부만 넘길 수 있개 해주는 문법으로써 코드의 간결성을 높일 수 있다.

다음 상황을 가정해보자.

1. 유저 리스트를 정렬하고 싶다.

2. 나이순으로, 이름순으로 각각 한번씩!

코드로 이렇게 작성할 수 있을 거 같다.

List<User> users = ...

Collections.sort(users, new Comparator<User>() {
    @Override
    public int compare(User o1, User o2) {
        return o1.getName().compareTo(o2.getName());
    }
});

// Do something...

Collections.sort(users, new Comparator<User>() {
    @Override
    public int compare(User o1, User o2) {
        return Integer.compare(o1.getAge(), o2.getAge());
    }
});

코드에서 Comparator 인터페이스의 익명클래스 두 개를 만들어 인스턴스를 생성했다.

그런데 구현해야할 추상 메서드가 오직 하나인 인터페이스를 사용하는데에는 이 정도도 과해보인다. 정렬 기준이 추가될때 마다 저 코드가 반복된다고 생각하면 더 그렇다. 람다는 이런 번거로운 작업을 좀 더 간소화해준다. 람다를 사용하면 익명클래스를 사용하지 않고도 함수(Function)를 파라미터로 넘길 수 있다. 클래스를 사용하지 않기 때문에 메서드가 아닌 함수라는 용어를 사용한다.

람다를 사용한 코드는 다음과 같은 형태이다.

List<User> users = ...

Collections.sort(users, (o1, o2)->{
    return o1.getName().compareTo(o2.getName());
});

// Do something...

Collections.sort(users, (o1, o2)->{
    return Integer.compare(o1.getAge(), o2.getAge());
});

(파라미터) -> {구현부}의 형태를 띄고 있다.

그럼 람다로 넘길 수 있는 지 아닌지는 어디서 결정되는 걸까?

Comparator 인터페이스를 살펴보면 알 수 있다.

@FunctionalInterface
public interface Comparator<T> {
  int compare(T o1, T o2);
  // Others...
 }

구현해야 할 추상메서드가 1개인 인터페이스에 @FunctionalInterface 어노테이션을 붙여주면 람다식으로 사용할 수 있다. 추상메서드는 1개여야 하지만 default나 static 메서드에는 제한이 없다.

참고로 Comparator 인터페이스를 다시보면 헷갈릴 수 있는 부분이 있는데

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }
    // Others...
}

default, static이 붙은 걸 제외해도 equals 메서드를 추상메서드로 제공하고 있다는 점이다. 즉 2개의 추상메서드가 있다. 그렇다면 내가 넘긴 람다식이 compare에 매칭되는지 equals에 매칭되는지 어떻게 알겠는가? 여기에 대해서는 다음과 같이 말하고 있다.

Functional Interface의 정의에서 Object 클래스의 public 메서드는 제외한다.

equals는 이미 모든클래스의 조상인 Object클래스에 이미 구현되어 있으므로 이런 경우에는 추상메서드로써 남겨놔도 FunctionalInterface로 사용할 수 있게끔 해서 편의성을 가져가는 것이 아닐까 한다.

Anoymous Inner Class와의 차이

public class Main {
    public static void main(String[] args) {
        List<Integer> iList = new ArrayList<>();
        iList.sort(new Comparator<Integer>() {
            @Override
            public int compare(Integer integer, Integer t1) {
                return 0;
            }
        });
    }
}

위의 코드를 컴파일하면 Main$1.class, Main.class 두 개의 class 파일이 생성된다.

public class Main {
    public static void main(String[] args) {
        List<Integer> iList = new ArrayList<>();
        iList.sort((i1, i2) -> i2 - i1);
    }
}

람다로 작성 후 컴파일하면 Main.class만 생성된다. 별도의 클래스를 생성하지 않고 outer class의 private static 메서드로 컴파일된다.

 

어떻게 컴파일되는지 좀 더 자세하게 알아보고 싶다면 아래 글을 읽어보는 것도 좋을 것이다.
https://dzone.com/articles/how-lambdas-and-anonymous-inner-classesaic-work

 

람다에 대한 딥다이브를 원한다면 참고할 수 있는 오라클 강의도 있다.

https://www.infoq.com/presentations/lambda-invokedynamic/

 

+ Recent posts