Language

Java

Call by value vs Call by Reference

간단하면서도 헷갈리는 개념 중 하나이다.

결론 : 자바는 Call by Value이다.

Primitive Type(원시 자료형)의 경우 Call by Value

  • int, short, long, float, double, char, boolean

Reference Type(참조 타입)의 경우 Call by Value

  • Array, 참조 타입

자바에서는 함수의 인자로 전달되는 타입이 기본형(원시 자료형)인 경우 값을 넘기게 되어있다. 이 경우 메모리에는 함수를 위한 별도의 공간이 생성된다. 이는 함수 종료시 사라진다. 따라서 함수 안에서 해당 인자의 값을 변경하더라도 원본 값은 바뀌지 않는 특징이 있다.

참조형(참조 타입)인 경우, 변수가 가지는 값이 주소 값이므로 Call by Value에 의해 주소 값이 전달된다. 따라서 함수 안에서 해당 인자의 값을 변경하게 되면 원본 값도 바뀌게 되는 특징이 있다.

String, StringBuilder, StringBuffer 차이

String

  • Immutable하기 때문에 + 등 concat 연산 시 원본을 변경하지 않고 새로운 String 객체를 생성한다. 이로 인해 메모리 공간의 낭비가 발생하고 성능이 떨어진다.

  • JDK 1.5 이후부터는 컴파일 타임에 StringBuilder로 변경한다고 한다.

  • 불변 객체이기 때문에 멀티 쓰레드 환경에서 동기화를 신경쓰지 않아도 된다.

  • 문자열 연산이 적고, 조회가 많은 상황에서 쓰기 좋다.

StringBuilder, StringBuffer

  • 공통점

    • String과 다르게 Mutable한 객체이다.

    • 따라서 문자열 연산 시 새롭게 객체를 생성하지 않고, 처음에 만든 객체를 이용해 연산하고 크기를 변경시켜 문자열을 변경한다.

    • 따라서 문자열 연산이 자주 발생하는 상황에서 성능적으로 유리하며 쓰기 좋다.

  • 차이점

    • StringBuilder : Thread-Safe 하지 않다. 멀티 쓰레드 지원하지 않음.

    • StringBuffer : Thread-Safe 하다. 멀티 쓰레드 지원함.

    • 즉, 동기화 지원의 유무.

StringBuilder는 동기화를 고려하지 않는 상황에서 사용.(Thread를 사용하지 않는 상황.) 문자열 연산이 많은 싱글 쓰레드 환경.

StringBuffer는 동기화가 필요한 멀티 쓰레드 환경에서 사용. 문자열 연산이 많은 멀티 쓰레드 환경.

객체지향 프로그래밍

객체 지향 프로그래밍은 OOP(Object Oriented Programming)이라고도 한다.

프로그래밍에서 필요한 데이터를 추상화시켜 상태와 행위를 가진 객체를 만들고 그 객체들 간의 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍 방법이다.

장점

  • 코드의 재사용성이 높다.

    • 누군가가 만든 클래스를 가져와 사용할 수 있고 상속을 통해 확장할 수도 있다.

  • 유지보수가 쉽다.

    • 수정해야 할 부분이 클래스 내부에 멤버 변수 혹은 메소드로 존재하기 때문에 해당 부분만 수정하면 된다.

  • 대형 프로젝트에 적합하다.

    • 클래스 단위로 모듈화시켜서 개발할 수 있으므로 업무 분담하기가 쉽다.

단점

  • 처리 속도가 상대적으로 느리다.

  • 객체가 많으면 용량이 커질 수 있다.

  • 설계 시 많은 노력과 시간이 필요하다.

객체 지향 프로그래밍의 특징

  • 추상화

불필요한 정보는 숨기고 필요한 정보만을 표현함으로써 공통의 속성이나 기능을 묶어 이름을 붙이는 것이다.

  • 캡슐화

속성과 기능을 정의하는 멤버 변수와 메소드를 클래스라는 캡슐에 넣는 것이다. 즉, 관련된 기능(메소드)과 속성(변수)을 한 곳에 모으고 분류하기 때문에 재활용이 원활하다.

목적 : 코드를 수정 없이 재활용하는 것

또한, 캡슐화를 통해 정보 은닉이 가능하다.

  • 상속

부모 클래스의 속성과 기능을 그대로 이어 받아 사용할 수 있게 하고 기능의 일부분을 변경해야 할 경우, 상속 받은 자식 클래스에서 해당 기능만 다시 수정(정의)하여 사용할 수 있게 하는 것이다.

상속을 통해서 클래스를 작성하면 보다 적은 양의코드로 새로운 클래스를 작성할 수 있다.

또한, 코드를 공통적으로 관리하여 코드 추가 및 변경이 용이하다.

  • 다형성

하나의 변수명, 함수명 등이 상황에 따라서 다른 의미로 해석될 수 있는 것이다.

즉, 오버라이딩, 오버로딩이 가능하다.

객체 지향 설계 원칙

이와 관련된 내용은 심도 깊으나, 여기서는 간단하게 무엇을 의미하는지만 정리한다. 추후 추가 예정.

  1. SRP(Single Responsibility Principle) : 단일 책임 원칙

    클래스는 단 하나의 책임을 가져야 하며, 클래스를 변경하는 이유는 단 하나의 이유여야 한다.

  2. OCP(Open Closed Principle) : 개방 폐쇄 원칙

    확장에는 열려있고, 변경에는 닫혀있어야 한다.

  3. LSP(Liskov Substitution Principle) : 리스코프 치환 원칙

    상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

  4. ISP(Interface Segregation Principle) : 인터페이스 분리 원칙

    인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.

  5. DIP(Dependency Inversion Principle) : 의존 역전 원칙

    고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.

접근 제어 지시자

자바에서 기본적인 부분이지만, 실제로 사용할 때 의미를 파악하지 않고 남발하는 경우가 많아 정리하려 한다.

  • public : public으로 선언된 멤버는 어떤 클래스에서라도 접근이 가능하다. public 메소드는 private 멤버와 프로그램 사이의 인터페이스 역할을 수행하기도 한다.

  • protected : protected 멤버를 포함하는 클래스가 정의되어 있는 해당 패키지 내 그리고 해당 클래스를 상속 받은 외부 패키지의 자식 클래스에서 접근이 가능하다.

  • private : private으로 선언된 멤버는 해당 멤버를 선언한 클래스에서만 접근이 가능하다. public 메소드를 이용한다면 해당 객체의 private 한 멤버에 접근이 가능하다.

  • Default(package private) : 같은 클래스의 멤버와 해당 클래스가 정의되어 있는 패키지 내에서만 접근이 가능하다.

참고

  1. private의 경우

    Private 멤버나 메소드를 가지고 있는 클래스를 A라고 하자.

    그리고 B라는 클래스가 A를 상속받는다. 이 경우, B 클래스는 private으로 선언된 멤버 혹은 메소드에 접근할 수 없다.

    따라서 상속을 받더라도 private한 멤버에는 접근이 불가능 하다.

    대신, public 메소드 통해 getter를 만들면 private 멤버를 사용할 수 있다.

  2. protected의 경우

    Protected 멤버나 메소드를 가지고 있는 클래스를 A라고 하자.

    마찬가지로 B라는 클래스가 A를 상속받는다. 이 경우, B 클래스는 protected로 선언된 멤버 혹은 메소드에 접근이 가능하다. B 라는 클래스가 다른 패키지에 선언되었을지라도 A 클래스의 멤버에 접근이 가능하다.

    하지만, 다른 패키지의 A 클래스를 상속받지 않은 클래스는 A 클래스의 멤버에 접근할 수 없다. 마치 private 처럼 말이다.

==와 equals()차이

• ==

◦ 비교를 위한 연산자.

◦ 비교하고자 하는 대상의 주소값을 비교한다.

• equals()

◦ 메소드이며, 객체끼리 내용을 비교할 수 있다.

◦ 비교하고자 하는 대상의 내용 자체를 비교한다.

Wrapper Class

기본 자료형을 객체 타입의 자료형으로 변환이 필요할 때 주로 사용한다.

✨ 사용 용도

  • 객체로 저장해야 할 경우

  • 매개변수로 객체가 요구될 경우(ex. 제네릭, Collection의 타입)

  • 객체 간의 비교가 필요할 경우

  • 제네릭이나 컬렉션에서 사용할 경우, 기본형을 쓸 수 없기 때문에 이를 Wrapping한 형태를 사용해야 한다.

✨ 특징

  • 산술 연산을 위한 클래스가 아니기 때문에 Immutable하다.(불변)

  • 불변 객체이기 때문에 값에 대한 변경은 불가능하고 새로운 값(객체)의 할당이나 참조만 가능하다.

  • Boxing : 기본 자료형 -> Wrapper Class

  • UnBoxing : Wrapper Class -> 기본 자료형

  • JDK 1.5부터 오토 박싱, 오토 언박싱을 지원한다.

  • 언박싱 시 사용되는 메소드는 다음과 같은 형태를 갖는다.

    • intValue : 객체 -> int 값으로 변환.

문자를 숫자로 바꾸거나, 숫자를 문자를 바꿀 때 두 가지 방식의 차이점이 존재한다.

✨ Code

위에서 언급했듯이, JDK 1.5부터 오토 박싱과 오토 언박싱이 지원되기 때문에 반환값이 기본형이든, wrapper class이든 차이가 없어졌다. 그래서 굳이 구별하지 않고 valueOf()를 사용해도 된다.

단, 성능을 비교하면 valueOf()가 조금 더 느리다고 한다.

기본형과 참조형의 차이점

자바에서는 Primitive Type과 Reference Type이 있다. 이는 기본형과 참조형이라고 하며, 서로 조금은 다른 특징을 가지고 있다.

✨ 기본형(Primitive Type)

  • 변수에 값 자체를 저장하며, stack 영역에 생성된다.

  • 사용하기 전에 반드시 선언되어야 하며, 초기화를 하지 않으면 자료형에 맞는 기본 값이 들어간다.

  • OS에 따라 자료의 길이가 변하지 않는다.

  • 비객체 타입이며, Null 값을 가질 수 없다.

  • 정수(byte, short, int, long), 실수(double, float), 문자(char), 논리(boolean)

✨ 참조형(Reference Type)

  • 기본형을 제외하면 참조형이라고 한다.

  • 메모리 상에서 객체가 존재하는 주소를 저장하며, heap 영역에 저장한다.

  • 클래스형, 인터페이스형, 배열형이 있다.

Primitive type & Reference type

Assembled by GimunLee (2019-11-19)

Goal

  • Primitive type에 대해 설명할 수 있다.

  • Reference type에 대해 설명할 수 있다.

Abstract

자바에는 기본형(Primitive type)과 참조형(Reference type)이 있습니다.

Primitive type (기본형 타입)

  • JAVA에서는 총 8가지의 Primitive type을 미리 정의하고 제공합니다.

  • 자바에서 기본 자료형은 반드시 사용하기 전에 선언(Declared)되어야 합니다.

  • OS에 따라 자료형의 길이가 변하지 않습니다.

  • 비객체 타입입니다. 따라서 null 값을 가질 수 없습니다. 만약 Primitive type에 Null을 넣고 싶다면 Wrapper Class를 활용합니다.

  • 스택(Stack) 메모리에 저장됩니다.

https://github.com/GimunLee/tech-refrigerator/raw/master/Language/JAVA/resources/java-type-001.png
  • boolean

    • 논리형인 boolean의 기본값은 false이며 참과 거짓을 저장하는 타입입니다. 주로 yes/no, on/off 등의 논리 구현에 주로 사용되며 두가지 값만 표현하므로 가장 크기가 작습니다.

    • boolean은 실제로 1bit면 충분하지만, 데이터를 다루는 최소 단위가 1byte이므로 메모리 크기가 1byte입니다.

  • byte

    • byte는 주로 이진데이터를 다루는데 사용되는 타입입니다.

  • short

    • short는 C언어와의 호환을 위해 사용되는 타입으로 잘 사용되지는 않는 타입입니다.

  • int

    • int 형은 자바에서 정수 연산을 하기 위한 기본 타입입니다. 즉, byte 혹은 short 의 변수가 연산을 하면 연산의 결과는 int형이 됩니다.

  • long

    • 수치가 큰 데이터를 다루는 프로그램(은행 및 우주와 관련된 프로그램)에서 주로 사용합니다.

    • long 타입의 변수를 초기화 할 떄에는 정수값 뒤에 알파벳 L을 붙여서 long 타입(즉, 8byte)의 정수 데이터임을 알려주어야 합니다. 만일 정수값이 int의 값의 저장 범위를 넘는 정수에서 L을 붙이지 않는다면 컴파일 에러가 발생합니다.

    long l = 2147483648; // 컴파일 에러 발생 long l = 2147483648L;

  • float, double

    • 실수를 가수와 지수 형식으로 저장하는 부동소수점 방식으로 저장됩니다.

    • 가수를 표현하는데 있어 double형이 float형보다 표현 가능 범위가 더 크므로 double형이 보다 정밀하게 표현할 수 있습니다.

    • 자바에서 실수의 기본 타입은 double형이므로 float형에는 알파벳 F를 붙여서 float 형임을 명시해주어야 합니다.

      float f = 1234.567; // 무조건 double 타입으로 이해하려고 하므로 컴파일 에러가 발생합니다. float f = 1234.567F; // float type이라는 것을 표시해야 합니다.

Reference type (참조형 타입)

  • JAVA에서 Primitive type을 제외한 타입들이 모두 Reference type 입니다.

  • Reference type은 JAVA에서 최상인 java.lang.Object클래스를 상속하는 모든 클래스들을 말합니다. 물론 new로 인하여 생성하는 것들은 메모리 영역인 Heap 영역에 생성을 하게되고, Garbage Collector가 돌면서 메모리를 해제합니다.

  • 클래스 타입(class type) , 인터페이스 타입(interface type) , 배열 타입(array type) , 열거 타입(enum type) 이 있습니다.

  • 빈 객체를 의미하는 Null이 존재합니다.

  • 문법상으로는 에러가 없지만 실행시켰을 때 에러가 나는 런타임 에러가 발생합니다. 예를 들어 객체나 배열을 Null 값으로 받으면 NullPointException이 발생하므로 변수 값을 넣어야 합니다.

  • Heap 메모리에 생성된 인스턴스는 메소드나 각종 인터페이스에서 접근하기 위해 JVM의 Stack 영역에 존재하는 Frame에 일종의 포인터(C의 포인터와는 다릅니다.)인 참조값을 가지고 있어 이를 통해 인스턴스를 핸들링합니다.

https://github.com/GimunLee/tech-refrigerator/raw/master/Language/JAVA/resources/java-type-002.png

String Class

클래스형에서도 String 클래스는 조금 특별합니다. 이 클래스는 참조형에 속하지만 기본적인 사용은 기본형 처럼 사용합니다. 그리고 불변(immutable)하는 객체입니다. String 클래스에는 값을 변경해주는 메소드들이 존재하지만 해당 메소드를 통해 데이터를 바꾼다 해도 새로운 String 클래스 객체를 만들어내는 것입니다. 일반적으로 기본형 비교는 == 연산자를 사용하지만 String 객체간의 비교는 .equals() 메소드를 사용해야 합니다.

오버라이딩과 오버로딩

overriding

오버라이딩과 오버로딩은 자주 나오면서도 중요한 개념이다. 하지만, 그만큼 잘 까먹기 때문에 정리하고 넘어가려 한다.

  • 오버라이딩 : 상위 클래스가 가지고 있는 메소드를 하위 클래스에서 재정의해서 사용하는 것을 의미한다.

    • 상속 시, 상위 클래스의 private 멤버를 제외한 모든 멤버를 상속받는다.

  • 오버로딩 : 같은 이름의 메소드를 여러 개 가지면서 매개변수의 타입과 개수를 다르게 하여 정의하는 것을 의미한다. 즉, 메소드의 시그니처를 다르게 하여 정의하는 것이다.

제목 없음

final 키워드

간단한 내용이지만, final 키워드가 클래스, 메소드, 변수 앞에 붙었을 때 각각의 의미에 대해서 정확히 정리하려 한다.

  • final class

    • 다른 클래스가 상속받지 못한다.

  • final method

    • 자식 클래스에서 상위 클래스의 final method를 오버라이드 하지 못한다.

  • final variable

    • 변하지 않는 상수 값이 되어 새롭게 값을 할당할 수 없는 변수가 된다.

non-static 멤버와 static 멤버 차이

  • non-static 멤버

    • 공간적 특성 : 해당 멤버는 객체마다 별도로 존재한다.

      • 인스턴스 멤버라고 부른다.

    • 시간적 특성 : 객체 생성 시에 멤버가 생성된다.

      • 객체가 생성될 때, 멤버가 생성되므로 객체 생성 후에 멤버 사용이 가능.

      • 객체가 사라지면 해당 멤버도 사라진다.

    • 공유의 특성 : 공유되지 않는다.

      • 멤버는 객체 내에 각각 독립된 공간을 유지하므로 공유되지 않는다.

  • static 멤버

    • 공간적 특성 : 해당 멤버는 클래스 당 하나만 생성된다.

      • 해당 멤버는 객체 내부가 아닌 별도의 공간에 생성된다.

      • 클래스 멤버라고 부른다.

    • 시간적 특성 : 클래스 로딩 시에 멤버가 생성된다.

      • 객체가 생성되기 전에 이미 생성되므로 객체를 생성하지 않고도 사용 가능.

      • 객체가 사라져도 해당 멤버가 사라지지 않는다.

      • 해당 멤버는 프로그램이 종료될 때, 사라진다.

    • 공유의 특성 : 동일한 클래스의 모든 객체들에 의해 공유된다. (하나의 클래스로부터 생성된 여러 객체가 공유한다.)

추상 클래스

  • 추상 클래스는 미완성된 클래스이다.

  • 미완성된 클래스는 미완성된 메소드인 추상 메소드를 포함하고 있다.

  • 추상 클래스는 혼자로는 클래스의 역할을 다 못하지만, 새로운 클래스를 작성하는 데 있어 그 바탕이 되는 부모 클래스로서의 중요한 의미를 갖는다. 왜냐하면 클래스를 작성함에 있어서 어느정도 작성된 상태에서 시작할 수 있기 때문이다.

  • 클래스 앞에 abstract 키워드를 붙인다.

abstract class Car { abstract void accelrate(); }

  • abstract 키워드가 있는 클래스라고 모두 구현해야 하는 것은 아니다. 왜냐하면 단지 공유의 목적으로 abstract class를 만드는 경우도 있기 때문이다.

추상 클래스의 목적

  • 기존의 클래스에서 공통된 부분을 추상화하여 상속하는 클래스에게 구현을 강제화한다. 메소드의 동작은 구현하는 자식 클래스에게 위임한다.

  • 공유의 목적을 갖고 있다.

추상 클래스의 특징

  • 추상 클래스는 추상 메소드가 아닌 일반 메소드, 멤버도 포함할 수 있다. 하지만 추상 메소드를 하나라도 포함하고 있다면 추상 클래스로 선언해야 한다.

  • 추상 클래스는 동작이 정의되어 있지 않은 추상 메소드를 포함하고 있으므로 인스턴스를 생성할 수 없다.

  • 추상 메소드

    • 선언부만 작성하고 구현부는 작성하지 않는 메소드이며, 앞에 abstract 키워드를 붙인다.

    • 구현부를 작성하지 않는 이유는 메소드의 내용이 상속받은 클래스에 따라 달라질 수 있기 때문이다.

    • 사용하는 목적은 추상 메소드를 포함한 클래스를 상속받는 자식 클래스가 반드시 추상 메소드를 구현하도록 강제하기 위함이다.

    • 추상 클래스를 상속받은 자식 클래스는 오버라이딩을 통해 조상인 추상 클래스의 추상 메소드를 모두 구현해야 한다.

    • 만약, 자식 클래스에서 추상 메소드를 하나라도 구현하지 않는다면 자식 클래스 역시 추상 클래스로 지정해야 한다.

인터페이스

  • 인터페이스는 인터페이스를 구현하는 모든 클래스에 대해 특정한 메소드가 반드시 존재하도록 강제한다.

  • 인터페이스의 목적은 구현 객체가 같은 동작을 한다는 것을 보장하는 것이다.

  • 일종의 추상 클래스다. 하지만 추상 클래스보다 추상화 정도가 높아서 추상 메소드 이외의 일반 메소드나 멤버 변수를 구성원으로 가질 수 없다. 오직 추상 메소드와 상수만 멤버로 가질 수 있으며, 그 외의 요소는 허용하지 않는다.

추상 클래스를 부분적으로만 완성된 미완성 설계도라고 한다면 인터페이스는 구현된 것이 아무것도 없는 기본 설계도라 할 수 있다.

  • 제약 사항

    • 모든 멤버 변수는 public static final 이어야 하며, 생략 가능.

    • 모든 메소드는 public abstract 이어야 하며, 생략 가능.

    • JDK 1.8부터 인터페이스에 static 메소드와 디폴트 메소드의 추가를 허용했다.

인터페이스의 상속

  • 인터페이스는 인터페이스로부터만 상속(extends) 받을 수 있다. 클래스와는 달리 다중 상속, 즉 여러 개의 인터페이스로부터 상속 받는 것이 가능하다.

  • 클래스에서 여러 인터페이스를 다중 구현하는 것 또한 가능하다.

인터페이스의 구현

  • 추상 클래스와 마찬가지로 자신이 직접 인스턴스를 생성할 수 없다. 따라서 인터페이스가 포함하고 있는 추상 메소드를 구현해 줄 클래스를 작성해야 한다.

  • 상속은 클래스를 확장한다는 의미의 키워드인 extends를 사용하며, 인터페이스는 구현한다는 의미의 키워드인 implements를 사용한다.

만약 모든 추상 메소드의 구현을 원하지 않는다면 위의 코드처럼 Fighter 클래스를 abstract 키워드를 붙여 추상 클래스로 선언해야 한다.

위에서는 move() 함수만을 구현했다. attack()을 구현하지 않았기 때문에 abstract Fighter인 추상 클래스로 선언을 했다.

자바에서는 상속과 구현을 동시에 할 수 있다.

클래스를 이용한 다중 상속의 문제점

  • 다중 상속이 된다고 가정했을 때, 아래의 코드를 확인해보자.

다중 상속을 허용할 경우, 발생할 수 있는 문제는 메소드 출처의 모호성이다.

주석을 달아놓은 2에서 MyPet 클래스의 인스턴스인 pet이 cry() 메소드를 호출하면 이 메소드가 Dog 클래스의 cry() 메소드인지, Cat 클래스의 cry() 메소드인지 구분할 수 없는 모호성이 생긴다.

이와 같은 이유로 자바에서 다중 상속을 지원하지 않는다.

하지만, 인터페이스를 이용해 다중 구현을 하게되면 위와 같은 메소드 호출의 모호성을 방지할 수 있다.

Cat, Dog 인터페이스를 동시에 구현한 Pet 클래스에서만 cry() 메소드를 정의하므로, 앞의 예제에서 발생한 메소드 호출의 모호성이 없다.

인터페이스를 통한 다형성

자손 클래스의 인스턴스를 조상 타입의 참조 변수로 참조하는 것이 가능하다.

인터페이스도 인터페이스를 구현한 클래스의 조상이라고 할 수 있으므로, 해당 인터페이스 타입의 참조 변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있다.

장점

  1. 대규모 프로젝트 개발 시 일관되고 정형화된 개발을 위한 표준화가 가능하다.

  2. 클래스의 작성과 인터페이스의 구현을 동시에 진행할 수 있으므로, 개발 시간을 단축할 수 있다.

  3. 클래스와 클래스 간의 관계를 인터페이스로 연결하면 클래스마다 독립적인 프로그래밍이 가능하다.

추상 클래스와 인터페이스의 차이

✨ 인터페이스

  • 클래스가 아니며, 클래스와 관련이 없다.

  • 추상 메소드와 상수만을 멤버로 가진다.

  • 한 개의 클래스가 여러 인터페이스를 구현할 수 있다. (다중 구현 가능.)

  • Java 8부터 default 메소드가 추가되었다.

    • default 키워드가 붙은 메소드는 구현할 수 있으며(일반 메소드처럼), 자식 클래스에서는 이를 오버라이딩할 수 있다.

    • 인터페이스가 변경되면 이를 구현하는 모든 클래스들이 해당 메소드를 다시 구현해야하는 번거로운 문제가 있었다. 이런 문제를 해결하기 위하여 인터페이스에 메소드를 구현할 수 있도록 변경되었다.

  • Java 8부터 static 메소드가 추가되었다.

    • 인터페이스에 static 메소드를 선언 가능하게 함으로써, 간단한 기능을 가지는 유틸리티성 인터페이스를 만들 수 있게 되었다.

  • 목적 : 구현 객체의 같은 동작을 보장하기 위해 사용한다.

✨ 추상 클래스

  • 클래스이며, 클래스와 관련이 있다. (주로 베이스 클래스로 사용)

  • 추상 메소드 및 일반 메소드와 멤버도 포함할 수 있다.

  • 한 개의 클래스가 여러 개의 클래스를 상속받을 수 없다. (다중 상속 불가능.)

  • 상속을 받아 기능을 확장시키는 데 목적이 있다.

  • 목적 : 기존의 클래스에서 공통된 부분을 추상화하여 상속하는 클래스에게 구현을 강제화한다. 메소드의 동작은 구현하는 자식 클래스로 위임한다.

  • 공유의 목적.

변수의 종류와 메모리 구조

https://user-images.githubusercontent.com/33534771/74100794-00363c80-4b76-11ea-9616-2a819e6dcb4b.png

✨ 1. 메소드 영역

• 클래스에 대한 정보와 함께 클래스 변수(static variable)가 저장되는 영역. • JVM은 자바 프로그램에서 특정 클래스가 사용되면 해당 클래스의 클래스 파일(*.class)을 읽어들여, 클래스에 대한 정보를 메소드 영역에 저장한다.

✨ 2. 힙 영역

• 모든 인스턴스 변수(멤버 변수)가 저장되는 영역. • new 키워드를 사용해 인스턴스가 생성되면, 해당 인스턴스의 정보를 힙 영역에 저장한다. • 힙 영역은 메모리의 낮은 주소 -> 높은 주소의 방향으로 할당된다.

✨ 3. 스택 영역

• 메소드가 호출될 때, 메소드의 스택 프레임이 저장되는 영역. • 메소드 호출 시, 메소드 호출과 관계되는 매개변수와 지역 변수를 스택 영역에 저장한다. • 스택 영역은 메소드의 호출과 함께 할당되며, 메소드의 호출이 완료되면 소멸한다. • 스택 영역에 저장되는 메소드의 호출 정보를 스택 프레임이라고 부른다. • 후입 선출의 구조를 갖고 있으며, 메모리의 높은 주소에서 낮은 주소의 방향으로 할당된다.

✨ 선언 위치에 따른 변수의 종류

• 클래스, 지역, 인스턴스 변수가 있으며 선언된 위치에 따라 종류가 결정된다.

`public class Test{ int iv; // 인스턴스 변수. static int cv; // 클래스 변수.

void print(){ int lv; // 지역 변수. } }` • iv, cv는 클래스 내부에 선언되어 있으므로 멤버 변수. • cv는 static으로 선언되었으니 클래스 변수이고, iv는 인스턴스 변수. • lv는 메소드 내에 선언되었으므로 지역 변수.

  1. 인스턴스 변수 • 인스턴스가 생성될 때, 생성된다. 따라서 인스턴스 변수를 사용하기 전에 먼저 객체를 생성해야 한다. • 인스턴스 변수는 독립적인 저장공간을 가지므로 인스턴스 별로 다른 값을 가질 수 있다. • 따라서 각 인스턴스마다 고유의 값을 가져야 할 때는 인스턴스 변수로 선언한다. • 힙 영역에 올라간다.

  2. 클래스 변수 • static 키워드가 붙은 변수이다. • 클래스 변수는 해당 클래스의 모든 인스턴스가 공통된 값을 공유하게 된다. • 따라서 한 클래스의 모든 인스턴스가 공통적인 값을 가져야할 때, 클래스 변수로 선언한다. • 클래스가 로딩될 때, 생성되어(그러므로 메모리에 딱 한번만 올라간다.) 종료될 때까지 유지되는 클래스 변수는 public을 붙이면 같은 프로그램 내에서 어디서든 접근 가능한 전역 변수가 된다. • 인스턴스 생성 없이 접근할 수 있으므로 클래스이름.클래스변수 를 통해 접근할 수 있다. • 메소드 영역에 올라가며, 주로 공유의 목적으로 사용한다.

  3. 지역 변수 • 메소드 내에 선언되며 메소드 내에서만 사용할 수 있는 변수이다. • 메소드가 실행될 때, 메모리를 할당받으며 메소드가 끝나면 소멸되어 사용할 수 없게 된다. • 스택 영역에 올라간다.

Reflection

  • 자바에서 이미 로딩이 완료된 클래스에서 또는 다른 클래스를 동적으로 로딩하여 구체적인 타입을 알지 못하더라도 생성자, 멤버 필드, 그리고 멤버 메소드를 사용할 수 있는 기법이다.

  • 객체를 통해서 클래스의 패키지 정보, 접근 지정자, 부모 클래스, 어노테이션 등을 얻을 수 있다.

  • 즉, 핵심은 컴파일 타임이 아니라 런타임에 동적으로 특정 클래스의 정보를 객체화하여 분석 및 추출해낼 수 있는 프로그래밍 기법이다.

사용하는 이유는?

  • 실행 시간(Runtime)에 다른 클래스를 동적으로 로딩하여 접근할 필요가 있을 때.

  • 클래스와 멤버 필드 그리고 메소드 등에 관한 정보를 얻어야할 때.

  • 리플렉션 없이도 완성도 높은 코드를 구현할 수 있지만, 사용한다면 조금 더 유연한 코드를 만들 수 있다.

주의할 점

  • 외부에 공개되지 않는 private 멤버도 Field.setAccessibile() 메소드를 통해 true로 지정하면 접근과 조작이 가능하기 때문에 주의해서 사용해야 한다.

  • Reflection에는 동적으로 해석되는 유형이 포함되므로 특정 JVM 최적화를 수행할 수 없다. 따라서 Reflection 작업이 비 Reflection 작업보다 성능이 떨어지며, 성능에 민감한 애플리케이션에서 자주 호출되는 코드엔 사용하지 않아야 한다.

Garbage Collection

Minor GC

  • 새로 생성된 대부분의 객체는 Eden 영역에 위치한다.

  • Eden 영역이 꽉 차게 되면 GC가 발생한다. GC가 발생한 이후, 살아남은 객체는 Survivor 영역 중 하나로 이동한다.

  • 이 과정을 반복하다가 계속해서 살아남은 객체는 일정 시간 참조되고 있다는 뜻이므로 Old 영역으로 이동한다.

Major GC

  • Old 영역에 있는 모든 객체들을 검사하여 참조되고 있지 않은 객체들을 한꺼번에 삭제한다.

  • 시간이 오래 걸리고 실행 중 프로세스가 정지된다. 이것은 'Stop-the-World'라고 하는데, Major GC가 발생하면 GC를 실행하는 스레드를 제외한 나머지 스레드는 모두 작업을 멈춘다. GC 작업을 완료한 이후에야 중단했던 작업을 다시 시작한다.

  • GC의 튜닝은 이 'Stop-the-World'의 시간을 줄이는 것이다.

GC는 어떤 원리로 소멸시킬 대상을 선정하는가?

  • 알고리즘에 따라 동작 방식이 매우 다양하지만 공통적인 원리가 있다.

  • Garbage Collector는 힙 내의 객체 중에서 가비지를 찾아내고 찾아낸 가비지를 처리해서 힙의 메모리를 회수한다.

  • 참조되고 있지 않은 객체를 가비지라고 하며, 객체가 가비지인지 아닌지 판단하기 위해서 Reachability라는 개념을 사용한다.

  • 어떤 힙 영역에 할당된 객체가 유효한 참조가 있으면 Reachability

  • 없다면 UnReachability로 판단한다.

  • 하나의 객체는 다른 객체를 참조하고 다른 객체는 또 다른 객체를 참조할 수 있기 때문에 참조 사슬이 형성된다. 이 참조 사슬 중 최초에 참조한 것을 Root Set이라고 한다.

  • 힙 영역에 있는 객체들을 총 4가지 경우에 대한 참조를 하게 된다.

유효한 최초의 참조가 이루어지지 않은 객체들은 Unreachable Objects로 판단하며, GC에 의해 수거된다.

인스턴스가 가비지 컬렉션의 대상이 되었다고 해서 바로 소멸이 되는 것은 아니다. 빈번한 가비지 컬렉션의 실행은 시스템에 부담이 될 수 있기 때문이다. 그래서 성능에 영향을 미치지 않도록 가비지 컬렉션 실행 타이밍은 별도의 알고리즘을 기반으로 계산이 되며, 이 계산 결과를 바탕으로 GC가 수행된다.

Java에서 Thread

먼저, 멀티 태스킹이 무엇인지 알 필요가 있다.

최근의 OS는 멀티 태스킹을 지원하지 않는게 없다.

멀티 태스킹은 두 가지 이상의 작업을 동시에 하는 것을 말한다.

예를 들어, 컴퓨터로 음악을 들으며, 웹서핑을 하는 것이다.

실제로 동시에 처리될 수 있는 프로세스의 개수는 CPU 코어의 개수와 동일한데, 이보다 많은 개수의 프로세스가 존재하기 때문에 모두 함께 동시에 처리될 수는 없다.

각 코어들은 아주 짧은 시간 동안 여러 프로세스를 번갈아가며 처리하는 방식을 통해 동시에 동작하는 것처럼 보이게 할 뿐이다.

이와 마찬가지로, 멀티 스레딩에 대해 간단히만 알아보자.

멀티 스레딩은 하나의 프로스세 안에서 여러 개의 스레드가 동시에 작업을 수행하는 것을 말한다.

  • 스레드 : 하나의 작업 단위.

스레드 구현 방법

자바에서 쓰레드를 구현하는 방법은 2가지가 있다. 두 방식 모두 run() 메소드를 오버라이딩 한다.

  1. Runnable 인터페이스 구현

  2. Thread 클래스 상속

Runnable 인터페이스 구현

  • Runnable 인터페이스를 구현하므로 다른 클래스를 상속받을 수 있다.

  • run() 메소드를 오버라이드 하면 된다.

  • 다만, start() 메소드가 없기 때문에 Runnable 인터페이스를 구현한 클래스의 객체를 만들어 Thread를 생성할 때, 생성자의 매개변수로 넘겨주고 쓰레드 객체의 start() 메소드를 수행한다.

Thread 클래스 상속

  • Thread 클래스를 상속받으면 다른 클래스를 상속받지 못한다.(다중 상속 불가능 - 자바의 특징)

  • run() 메소드를 직접 구현해야 한다.

  • 또한, Thread 클래스를 상속받으면 스레드 클래스의 메소드를 바로 사용할 수 있지만, Runnable 구현의 경우 Thread 클래스의 static 메소드인 currentThread()를 호출해 현재 스레드에 대한 참조를 얻어와야만 호출이 가능하다.

스레드의 실행

스레드의 실행은 run() 호출이 아닌 start() 호출로 해야 한다.

[왜?]

위에서 우리가 정의했던 메소드는 run()이다. 하지만, 실제로 스레드에게 작업을 시키려면 start()로 작업해야 한다고 한다. run() 메소드로 작업 지시를 하면 스레드가 일을 할까? 그렇지 않다. 두 메소드 모두 같은 작업을 한다. 하지만 run() 메소드를 사용한다면, 이건 스레드를 사용하는 것이 아니다.

Java에는 Call Stack이라는 것이 있다. 이 영역이 실질적인 명령어들을 담고 있는 메모리로, 하나씩 꺼내서 실행시키는 역할을 한다.

만약 동시에 두 가지 작업을 한다면, 두 개 이상의 콜 스택이 필요하게 된다. 스레드를 이용한다는 건, JVM이 다수의 콜 스택을 번갈아가며 일처리를 하고 사용자에게는 동시에 작업을 하는 것처럼 보여주는 것이다.

즉, run() 메소드를 이용한다는 것은 main()의 콜 스택 하나만 이용하는 것으로 스레드 활용이 아니다. (그냥 스레드 객체의 run이라는 메소드를 호출하는 것 뿐이게 된다.)

start() 메소드를 호출하면 JVM은 알아서 스레드를 위한 콜 스택을 새롭게 만들어주고 context switching을 통해 스레드답게 동작하도록 해준다.

결국, 우리는 새로운 콜 스택을 만들어 작업을 해야 스레드 일처리가 되는 것이기 때문에 start() 메소드를 써야 하는 것이다.

start()는 스레드가 작업을 실행하는 데 필요한 콜 스택을 생성한 다음 run()을 호출해서 그 스택 안에 run()을 저장할 수 있도록 해준다.

스레드의 실행 제어

스레드는 다음과 같이 5가지의 상태를 가지고 있다.

  1. NEW : 스레드가 생성되고 아직 start()가 호출되지 않은 상태

  2. RUNNABLE : 실행 중 또는 실행 가능 상태

  3. BLOCKED : 동기화 블록에 의해 일시정지된 상태(lock이 풀릴 때까지 기다림)

  4. WAITING, TIME_WAITING : 실행 가능하지 않은 일시정지 상태

  5. TERMINATED : 스레드 작업이 종료된 상태

스레드를 사용하는 것이 효율적이라 하지만, 어렵다.

이유는 위와 같이 다양한 상태를 가지고 있으며, 이를 잘 사용하기 위해선 동기화와 스케줄링이 필요하기 때문이다.

  • 스케줄링과 관련된 메소드 : sleep(), join(), yield(), interrupt()

  • start() 이후에 join()을 해주면 main 스레드가 모두 종료될 때까지 기다려주는 일도 해준다.

이와 관련된 내용들은 깊은 내용이며, 운영체제와 연관성이 높다고 판단되어 운영체제 쪽에서 다룰 예정이다.

Javad의 String

Java에서 String은 굉장히 자주 사용되며, 두 가지 생성 방식이 있다.

  1. new 연산자를 이용한 방식

  2. 리터럴을 이용한 방식

이 두 가지 방식에는 큰 차이점이 존재한다.

new를 통해 String 객체를 생성하면 Heap 영역에 존재하게 된다.

리터럴을 이용할 경우, String Constant Pool이라는 영역에 존재하게 된다.

  • == 연산의 결과는 false이다. == 연산자는 객체의 주소값을 비교하기 때문에 일반 객체처럼 Heap 영역에 생성된 String 객체와 리터럴을 이용해 String Constant Pool에 저장된 String 객체의 주소값은 다를 수 밖에 없다.

  • equals() 메소드의 수행 결과는 true이다. 이는 문자열 내용을 비교하기 때문에 같은 문자열에 대해서 true를 반환하는 것이 맞다.

왜 이런 결과가 나올까?

이를 위해서는 동작 방식에 대한 이해가 필요하다. String을 리터럴로 선언할 경우, 내부적으로 String의 intern()이라는 메소드가 호출되게 된다.

  • intern() : 주어진 문자열이 String Constant Pool에 존재하는지 검색하고 있다면 그 주소값을 반환하고 없다면 String Constant Pool에 넣고 새로운 주소값을 반환하게 된다.

기존에 new를 통해 생성된 String 객체와 리터럴로 생성된 String 객체를 == 연산하였을 경우, false를 반환했지만 new를 통해 생성된 String 객체의 intern() 메소드를 호출하여 새로운 String 객체인 intern에 대입할 경우, 리터럴로 생성된 String 객체와 == 연산시 true를 반환하게 된다.

위에서 설명했듯이 리터럴로 "loper"라는 문자열이 String Constant Pool에 저장되었고, intern() 메소드를 호출하면서 String Constant Pool에 "loper"라는 문자열을 검색하게 되고 이미 저장된 "loper" 문자열을 발견할테니 동일한 주소값을 반환하게 되므로 true가 성립되는 것이다.

String Constant Pool의 위치 변경

Java 6까지 String Constant Pool의 위치는 Perm 영역이었다. Perm 영역에 위치했던게 Java 7에서 Heap 영역으로 변경되었다. 그 이유는 OOM(Out Of Memory) 때문이다.

Perm 영역은 고정된 사이즈이며 Runtime에 사이즈가 확장되지 않는다. Perm 영역의 사이즈를 늘릴수는 있지만 어쨌거나 Runtime에 사이즈가 변경되는 것은 아니다.

그래서 Java 6까지는 String의 intern() 메소드를 호출하는 것은 OOM을 발생시킬 수 있고 그 부분을 컨트롤할 수 없었기 때문에 거의 사용하지 않는 것이 맞다.

그래서 Oracle의 엔지니어들이 Java 7에서 Perm 영역이 아닌 Heap 영역으로 String Constant Pool의 위치를 변경했다. Heap 영역으로 변경함으로써 얻는 이점은 무엇일까?

  • 바로 String Constant Pool의 모든 문자열도 GC의 대상이 될 수 있다는 점이다.

String Constant Pool의 사이즈 또한 지정할 수 있는데, -xx:StringTableSize 옵션으로 설정이 가능하다. 여기에는 1,000,000과 같은 숫자가 아닌 1,000,003과 같은 소수를 사용해야 한다. 이는 hashCode 성능과 관련된 부분이며 아티클에 자세한 내용이 나와있다.

++ 2020.09.19 추가 해당 블로그의 내용이 이해가 잘되어서 첨부하며, 추후에 추가하도록 하겠다.

문자열 클래스

Goal

  • JAVA의 3가지 문자열 클래스에 대해 간략하게 설명할 수 있다.

  • 각 문자열 클래스의 차이점에 대해 설명할 수 있다.

  • 상황에 맞게 문자열 클래스를 사용할 수 있다.

Introduction

JAVA에는 문자열 클래스로 String, StringBuffer, StringBuilder 3가지가 있습니다. 사소해보이지만 상황에라 어떤 클래스를 쓰냐에 따라, 성능차이가 발생하는데요. 어떤 차이점이 있는지 알아보도록 하겠습니다.

String vs StringBuffer vs StringBuilder

제목 없음

String과 다른 클래스(StringBuffer, StringBuilder)의 기본적인 차이는 String은 Immutable(불변), StringBuffer, StringBuilder는 Mutable(가변)에 있습니다.

String Class

String 객체는 한번 생성되면 할당된 메모리 공간이 변하지 않습니다. 즉, '+' 연산 또는 concat 메서드를 통해 기존에 생성된 String 객체에 다른 문자열을 붙여도 기존 문자열에 새로운 문자열을 붙이는 것이 아닙니다. 새로운 String 객체를 만든 후, 이 객체에 연결된 문자열을 저장하고, 그 객체를 참조하도록합니다.

  • 장점

    • String Class의 객체는 Immutable(불변)하기 때문에 단순하게 읽어가는 조회 연산에서는 타 클래스보다 빠르게 읽을 수 있습니다.

    • Immutable(불변)하기 때문에 멀티쓰레드 환경에서 동기화를 신경 쓸 필요가 없습니다. (Thread-Safe)

  • 단점

    • 문자열 연산('+', concat 등)을 많이 일어나는 경우, 더이상 참조되지 않는 기존 객체는 Garbage Collection(이하, GC)에 의해 제거되야하기 때문에 성능이 좋지 않습니다.

    • 또한, 문자열 연산이 많아질 때 연산 내부적으로 char 배열을 사용하고, 계속해서 객체를 만드는 오버헤드가 발생하므로 성능이 떨어질 수 밖에 없습니다.

StringBuffer와 StringBuilder Class

StringBuffer와 StringBuilder 클래스는 String과 다르게 mutable(변경가능)합니다. 즉, 문자열 연산에 있어서 클래스를 한번만 만들고(new), 연산이 필요할 때 크기를 변경시켜서 문자열을 변경합니다. 그러므로 문자열 연산이 자주 있을 때 사용하면 성능이 좋습니다.

StringBuffer와 StringBuilder 클래스가 제공하는 메서드는 서로 동일합니다. 그렇다면 두 클래스의 차이점을 무엇일까요? 바로 동기화 여부입니다.

StringBuffer는 각 메서드별로 Synchronized Keyword가 존재하여, Multi-Thread 환경에서도 동기화를 지원하여 Thread-Safe합니다.

반면, StringBuilder는 동기화를 보장하지 않습니다. 하지만 StringBuilder는 Single-Thread 환경에서 동기화를 고려하지 않기 때문에 StringBuffer에 비해 연산처리가 빠릅니다.

그렇기 때문에 Multi-Thread 환경이라면 값 동기화 보장을 위해 StringBuffer를 사용하고, Single-Thread 환경이라면 StringBuilder를 사용하는 것이 좋습니다.

Conclusion

String Class는 JDK 1.5버전 이전에 문자열연산('+', concat)을 할 때에는 조합된 문자열을 새로운 메모리에 할당하여 참조함으로 인해서 성능상의 이슈가 있었습니다. 그러나 JDK1.5 버전 이후에는 컴파일 단계에서 String 객체를 사용하더라도 StringBuilder로 컴파일 되도록 변경되었습니다. 그리하여 JDK 1.5 이후 버전에서는 String 클래스를 활용해도 StringBuilder와 성능상으로 차이가 없어졌습니다. 하지만 반복 루프를 사용해서 문자열을 더할 때에는 객체를 계속 추가한다는 사실에는 변함이 없습니다.

String Class를 쓰는 대신, Thread와 관련이 있으면 StringBuffer를, Thread 안전 여부와 상관이 없으면 StringBuilder를 사용하는 것을 권장합니다.

단순히 성능만 놓고 본다면 연산이 많은 경우, StringBuilder > StringBuffer >>> String 라고 합니다.

int와 short

두 가지 모두 정수형 타입이다. 그렇다면 어떤 차이가 있는지 알아보자.

  • char, short 형 : 이와 같은 정수 자료형 타입으로 표현하면 메모리 공간을 효율적으로 사용할수는 있으나 연산의 효율성은 떨어진다.

    • size : 2byte(16bits)

  • int 형 : int형보다 작은 크기의 데이터를 가지고 연산을 진행할 경우, 그 데이터를 일단 int형으로 바꿔서 연산을 진행한다. 따라서 산술 연산시, 자료형을 int형으로 선언해야 중간에 불필요한 변환 과정을 거치지 않게 되어 연산 효율이 좋다.

    • size : 4byte(32bits)

JVM

  • 스택 기반의 가상 머신.

  • JVM의 역할은 자바 애플리케이션을 클래스 로더를 통해 읽어들여 자바 API와 함께 실행하는 것이다.

  • Java와 OS 사이에서 중개자 역할을 수행하여 Java가 OS에 구애받지 않고 재사용 가능하게 해준다.

  • 메모리 관리, Garbage Collection(GC를 통해 자원을 관리)을 수행한다.

  • 자바 바이트 코드를 실행할 수 있는 주체이다.

JVM을 알야아 하는 이유는 뭘까?

  • 한정된 메모리를 효율적으로 사용하여 최고의 성능을 내기 위해서라 할 수 있다. 동일한 기능의 프로그램이더라도 메모리 관리에 따라서 성능이 좌우되기 때문에 JVM이 하는 역할을 이해하고 메모리를 효율적으로 사용하여 최고의 성능을 낼 수 있을 것이다.

자바 프로그램 실행 과정

  1. 프로그램이 실행되면 JVM은 OS로부터 이 프로그램이 필요로 하는 메모리를 할당받는다. JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.

  2. 자바 컴파일러(javac)가 자바 소스 코드를 읽어들여 자바 바이트 코드(.class)로 변환시킨다.

  3. Class Loader를 통해 class 파일들을 JVM으로 로딩한다.

  4. 로딩된 class 파일들은 Execution Engine을 통해 해석된다.

  5. 해석된 바이트 코드는 Runtime Data Area에 배치되어 실질적인 수행이 이루어지게 된다. 이러한 실행 과정 속에서 JVM은 필요에 따라 Thread Synchronization과 GC 같은 관리 작업을 수행한다.

https://user-images.githubusercontent.com/33534771/83471568-f7200100-a4bf-11ea-810f-3ea08018317f.png

각각의 역할

Class Loader(클래스 로더)

  • Runtim시에 JVM내로 클래스(.class 파일)를 로드하고 링크를 통해 배치하는 작업을 수행한다. (Runtime : 클래스를 처음으로 참조할 때.)

  • 사용하지 않는 클래스들은 메모리에서 삭제한다.

  • 동적 로드를 담당한다.

Execution Engine(실행 엔진)

  • 클래스를 실행시키는 역할이다.

  • 클래스 로더가 JVM 내의 Runtime Data Area에 바이트 코드를 배치시키고 이것은 실행 엔진에 의해서 실행된다.

  • 자바 바이트 코드는 비교적 인간이 보기 편한 형태로 기술된 것이다. 그래서 실행 엔진은 이와 같은 바이트 코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경한다.

  • 실행 엔진은 자바 바이트 코드를 명령어 단위로 읽어서 실행한다.

  • 2가지 방식이 존재한다.

    • 최초의 JVM은 인터프리터 방식이었기 때문에 속도가 느린 단점이 존재했지만, JIT 컴파일러 방식을 통해 이 점을 보완했다.

  1. Interpreter(인터프리터)

    • 자바 바이트 코드를 명령어 단위로 읽어서 실행한다.

    • 한 줄씩 실행하기 때문에 느리다.

  2. JIT(Just-In-Time) Compiler

    • 인터프리터 방식의 단점을 보완하기 위해 등장했다.

      • 인터프리터 방식으로 실행하다가 적절한 시점에 바이트 코드 전체를 컴파일하여 네이티브 코드로 변경하고 이후에는 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행하는 방식이다.

    • 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 빠르게 수행된다.

    • 한 번만 실행되는 코드라면 JIT 컴파일러가 컴파일하는 게 인터프리팅하는 것보다 오래 걸리므로 인터프리팅하는 것이 유리하다.

    • 이처럼 해당 메소드가 얼마나 자주 수행되는지 체크하고 일정 정도를 넘을 때만 컴파일을 수행하는 게 좋다.

Garbage Collector

  • GC를 수행하는 모듈이 존재한다.

Runtime Data Area

  • JVM이 프로그램을 수행하기 위해 OS로부터 할당받은 메모리 공간이다.

  • 이 공간은 용도에 따라 여러 영역으로 나누어 관리한다.

https://user-images.githubusercontent.com/33534771/83472428-2fc0da00-a4c2-11ea-90a9-dac474fada4b.png
  1. PC Register

    • Thread가 시작될 때, 각각의 Thread 별로 생성되는 공간으로 현재 수행 중인 JVM 명령어 주소를 가지게 된다.

  2. JVM 스택 영역

    • 프로그램의 실행 과정에서 임시로 할당되었다가 메소드를 빠져나가면 바로 소멸되는 특성의 데이터를 저장하기 위한 영역이다.

    • 메소드의 매개변수, 지역 변수 등 메소드의 정보를 저장한다.

  3. Natvie Method Stack

    • Java외의 언어로 작성된 네이티브 코드를 위한 영역이다.

    • 자바 프로그램이 컴파일 되어 생성되는 바이트 코드가 아닌 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역이다.

  4. Method Area(Class Area, Static Area)

    • 클래스 정보를 처음 메모리 공간에 올릴 때, 초기화 되는 대상을 저장하기 위한 메모리 공간.

    • 모든 쓰레드가 공유하는 메모리 영역이다. 클래스, 인터페이스, 메소드, 필드, Static 변수 등의 바이트 코드를 보관한다.

    • Runtime Constant Pool이라는 것이 존재하며, 이는 별도의 관리 영역으로 상수 자료형을 저장하여 참조하고 중복을 막는 역할을 수행한다. (각 클래스와 인터페이스의 상수, 메소드와 필드에 대한 모든 레퍼런스를 담고 있는 테이블이다.)

    • Java 7부터 String Constant Pool은 Heap 영역으로 변경되어 GC의 관리 대상이 되었다.

  5. Heap(힙 영역)

    • 객체를 저장하는 가상 메모리 공간이다.

    • 런타임시 동적으로 할당하여 사용하는 영역.

    • new 연산자로 생성된 객체와 배열을 저장한다.

    • 클래스 영역에 올라온 클래스들로만 객체로 생성할 수 있으며, 세 부분으로 나눌 수 있다.

    • GC의 관리 대상에 포함된다.

https://user-images.githubusercontent.com/33534771/83472881-3f8cee00-a4c3-11ea-942c-9b7aa0f4ea04.png
  • New/Young 영역

    • Eden : 객체들이 최초로 생성되는 공간.

    • Survivor 0/1 : Eden에서 참조되는 객체들이 저장되는 공간.

  • Old 영역

    • New 영역에서 일정 시간 참조되고 살아남은 객체들이 저장되는 공간이다.

    • Eden 영역에서 인스턴스가 가득차게 되면 첫 번째 GC가 발생한다. (minor GC)

    • Eden 영역에 있는 값들을 Survivor 1 영역에 복사하고, 이 영역을 제외한 나머지 영역의 객체를 삭제한다.

    • Eden 영역에서 GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역으로 이동한다. 이 과정을 반복하다가 살아남은 객체는 Old 영역으로 이동된다.

  • Permanent Generation

    • 생성된 객체들의 주소값이 저장되는 공간이다.

    • 리플렉션을 사용하여 동적으로 클래스가 로딩되는 경우 사용된다.

    • Old 영역에서 살아남은 객체가 영원히 남아있는 곳이 아니다.

    • 이 영역에서 발생하는 GC는 Major GC의 횟수에 포함된다.

JVM 2

시스템 메모리를 관리하면서, 자바 기반 애플리케이션을 위해 이식 가능한 실행 환경을 제공함

JVM은, 다른 프로그램을 실행시키는 것이 목적이다.

갖춘 기능으로는 크게 2가지로 말할 수 있다.

  1. 자바 프로그램이 어느 기기나 운영체제 상에서도 실행될 수 있도록 하는 것

  2. 프로그램 메모리를 관리하고 최적화하는 것

JVM은 코드를 실행하고, 해당 코드에 대해 런타임 환경을 제공하는 프로그램에 대한 사양임

개발자들이 말하는 JVM은 보통 어떤 기기상에서 실행되고 있는 프로세스, 특히 자바 앱에 대한 리소스를 대표하고 통제하는 서버를 지칭한다.

자바 애플리케이션을 클래스 로더를 통해 읽어들이고, 자바 API와 함께 실행하는 역할. JAVA와 OS 사이에서 중개자 역할을 수행하여 OS에 구애받지 않고 재사용을 가능하게 해준다.

JVM에서의 메모리 관리


JVM 실행에 있어서 가장 일반적인 상호작용은, 힙과 스택의 메모리 사용을 확인하는 것

실행 과정

  1. 프로그램이 실행되면, JVM은 OS로부터 이 프로그램이 필요로하는 메모리를 할당받음. JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리함

  2. 자바 컴파일러(JAVAC)가 자바 소스코드를 읽고, 자바 바이트코드(.class)로 변환시킴

  3. 변경된 class 파일들을 클래스 로더를 통해 JVM 메모리 영역으로 로딩함

  4. 로딩된 class파일들은 Execution engine을 통해 해석됨

  5. 해석된 바이트 코드는 메모리 영역에 배치되어 실질적인 수행이 이루어짐. 이러한 실행 과정 속 JVM은 필요에 따라 스레드 동기화나 가비지 컬렉션 같은 메모리 관리 작업을 수행함

자바 컴파일러

자바 소스코드(.java)를 바이트 코드(.class)로 변환시켜줌

클래스 로더

JVM은 런타임시에 처음으로 클래스를 참조할 때 해당 클래스를 로드하고 메모리 영역에 배치시킴. 이 동적 로드를 담당하는 부분이 바로 클래스 로더

Runtime Data Areas

JVM이 운영체제 위에서 실행되면서 할당받는 메모리 영역임

총 5가지 영역으로 나누어짐 : PC 레지스터, JVM 스택, 네이티브 메서드 스택, 힙, 메서드 영역

(이 중에 힙과 메서드 영역은 모든 스레드가 공유해서 사용함)

PC 레지스터 : 스레드가 어떤 명령어로 실행되어야 할지 기록하는 부분(JVM 명령의 주소를 가짐)

스택 Area : 지역변수, 매개변수, 메서드 정보, 임시 데이터 등을 저장

네이티브 메서드 스택 : 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역

: 런타임에 동적으로 할당되는 데이터가 저장되는 영역. 객체나 배열 생성이 여기에 해당함

(또한 힙에 할당된 데이터들은 가비지컬렉터의 대상이 됨. JVM 성능 이슈에서 가장 많이 언급되는 공간임)

메서드 영역 : JVM이 시작될 때 생성되고, JVM이 읽은 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드 및 메서드 코드, 정적 변수, 메서드의 바이트 코드 등을 보관함

가비지 컬렉션(Garbage Collection)

자바 이전에는 프로그래머가 모든 프로그램 메모리를 관리했음 하지만, 자바에서는 JVM이 프로그램 메모리를 관리함!

JVM은 가비지 컬렉션이라는 프로세스를 통해 메모리를 관리함. 가비지 컬렉션은 자바 프로그램에서 사용되지 않는 메모리를 지속적으로 찾아내서 제거하는 역할을 함.

실행순서 : 참조되지 않은 객체들을 탐색 후 삭제 → 삭제된 객체의 메모리 반환 → 힙 메모리 재사용

equals()메소드 동작 원리

equals()

  • 비교를 위한 메소드이다.

  • Java에서는 대상의 내용 자체를 비교한다.

  • 그렇다면 두 문자열을 비교할 때, 어떤 원리로 비교할까?

  • Ex) a = "Victory", b = "Victory"

먼저, 같은 객체인지 비교한다. 같은 객체라면 같은 값을 가지고 있기 때문에 true를 반환하며, 아래 문장은 수행되지 않는다.

  • 다음으로 인자로 들어온 Object가 String 타입인지 확인하고 조건을 만족하면 해당 객체를 String타입으로 형 변환을 한다.

  • 그리고 char[] 배열로 변환한 뒤, 문자를 앞에서부터 하나씩 비교한다. 한 개의 문자라도 다르다면 false를 반환하고 모든 문자가 동일하다면 true를 반환한다.

  • true -> 동일한 내용임을 의미. false -> 다른 내용임을 의미.

Integer vs int size 비교

Integer vs int

사실, 이 둘의 차이는 대부분 알 것이라고 생각한다.

정리하는 이유는 최근에 알게 된 사실 때문이다.

int

  • Primitive 자료형

  • 산술 연산이 가능하며, null 값을 가질 수 없다.

Integer

  • Wrapper 클래스(객체)

  • Unboxing을 하지 않으면 산술 연산이 불가능하지만, null 값을 가질 수 있다.

  • Collection, null 값이 필요한 경우 사용한다.

Integer와 int의 size 비교

  • Integer 및 int 배열을 1,000,000개 생성한다.

  • 결과

    • Integer : 19986824 byte

    • int : 3998536 byte

    • 4.99배(약 5배)

요약

  • Object : 8 byte

  • Integer : 16 byte

  • Integer를 참조하는데 4 byte

  • 따라서 Integer의 size = 20 byte

  • int의 size : 4 byte

  • 5배 차이가 난다.

Java 컴파일 과정

들어가기전

자바는 OS에 독립적인 특징을 가지고 있습니다. 그게 가능한 이유는 JVM(Java Vitual Machine) 덕분인데요. 그렇다면 JVM(Java Vitual Machine)의 어떠한 기능 때문에, OS에 독립적으로 실행시킬 수 있는지 자바 컴파일 과정을 통해 알아보도록 하겠습니다.


자바 컴파일 순서

  1. 개발자가 자바 소스코드(.java)를 작성합니다.

  2. 자바 컴파일러(Java Compiler)가 자바 소스파일을 컴파일합니다. 이때 나오는 파일은 자바 바이트 코드(.class)파일로 아직 컴퓨터가 읽을 수 없는 자바 가상 머신이 이해할 수 있는 코드입니다. 바이트 코드의 각 명령어는 1바이트 크기의 Opcode와 추가 피연산자로 이루어져 있습니다.

  3. 컴파일된 바이크 코드를 JVM의 클래스로더(Class Loader)에게 전달합니다.

  4. 클래스 로더는 동적로딩(Dynamic Loading)을 통해 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역(Runtime Data area), 즉 JVM의 메모리에 올립니다.

    • 클래스 로더 세부 동작

      1. 로드 : 클래스 파일을 가져와서 JVM의 메모리에 로드합니다.

      2. 검증 : 자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 구성되어 있는지 검사합니다.

      3. 준비 : 클래스가 필요로 하는 메모리를 할당합니다. (필드, 메서드, 인터페이스 등등)

      4. 분석 : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경합니다.

      5. 초기화 : 클래스 변수들을 적절한 값으로 초기화합니다. (static 필드)

  5. 실행엔진(Execution Engine)은 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행합니다. 이때, 실행 엔진은 두가지 방식으로 변경합니다.

    1. 인터프리터 : 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행합니다. 하나하나의 실행은 빠르나, 전체적인 실행 속도가 느리다는 단점을 가집니다.

    2. JIT 컴파일러(Just-In-Time Compiler) : 인터프리터의 단점을 보완하기 위해 도입된 방식으로 바이트 코드 전체를 컴파일하여 바이너리 코드로 변경하고 이후에는 해당 메서드를 더이상 인터프리팅 하지 않고, 바이너리 코드로 직접 실행하는 방식입니다. 하나씩 인터프리팅하여 실행하는 것이 아니라 바이트 코드 전체가 컴파일된 바이너리 코드를 실행하는 것이기 때문에 전체적인 실행속도는 인터프리팅 방식보다 빠릅니다.

Casting (업캐스팅 & 다운캐스팅)

캐스팅이란?

변수가 원하는 정보를 다 갖고 있는 것

int a = 0.1; // (1) 에러 발생 X int b = (int) true; // (2) 에러 발생 O, boolean은 int로 캐스트 불가

(1)은 0.1이 double형이지만, int로 될 정보 또한 가지고 있음

(2)는 true는 int형이 될 정보를 가지고 있지 않음

캐스팅이 필요한 이유는?

  1. 다형성 : 오버라이딩된 함수를 분리해서 활용할 수 있다.

  2. 상속 : 캐스팅을 통해 범용적인 프로그래밍이 가능하다.

형변환의 종류

  1. 묵시적 형변환 : 캐스팅이 자동으로 발생 (업캐스팅)

    Parent p = new Child(); // (Parent) new Child()할 필요가 없음

    Parent를 상속받은 Child는 Parent의 속성을 포함하고 있기 때문

  2. 명시적 형변환 : 캐스팅할 내용을 적어줘야 하는 경우 (다운캐스팅)

    Parent p = new Child(); Child c = (Child) p;

    다운캐스팅은 업캐스팅이 발생한 이후에 작용한다.

Promotion & Casting

Assembled by GimunLee (2019-11-19)

Goal

  • Promotion에 대해 설명할 수 있다.

  • Casting에 대해 설명할 수 있다.

  • 형변환할 때 발생할 수 있는 에러에 대해 설명할 수 있다.

데이터 타입 형변환 (타입변환)

Java에서 연산은 "2(byte 데이터 타입) + 3(byte 데이터 타입)" 과 같이 동일한 데이터 타입에서 가능합니다. 하지만, 프로그램을 만들다 보면 "2(byte 데이터 타입) + 3.5(double 데이터 타입)"과 같이 서로 다른 데이터 타입끼리의 연산이 필요할 때가 있습니다.

이럴경우 변수의 데이터 타입을 바꿔주는 작업이 필요한데, 이것이 데이터 타입의 형변환(타입변환)입니다. 이러한 형변환(타입변환)에는 크게 자동 형변환(Promotion)강제 형변환(Casting) 이 있습니다. 또 다른말로 자동 형변환은 묵시적 타입 변환, 강제 형변환은 명시적 타입 변환이라고도 합니다.

Promotion (자동 형변환, 묵시적 형변환)

자동 형변환(Promotion)은 프로그램 실행 도중에 자동적으로 형변환(타입변환)이 일어나는 것을 말합니다. 또한, 자동 형변환(Promotion)은 작은 메모리 크기의 데이터 타입을 큰 메모리 크기의 데이터 타입으로 변환하는 행위를 말합니다.

byte a = 10; // 정수 10을 byte 데이터 타입의 변수인 a에 저장 int b = a; // byte 데이터 타입의 변수인 a를 int 데이터 타입의 변수인 b에저장

위에 작성한 예시처럼 작은 메모리 크기의 데이터 타입(byte 데이터 타입)에서 큰 메모리 크기의 데이터 타입(int 데이터 타입)에 값을 저장하면, 별다른 문법 없이 형변환(타입변환)이 일어납니다. 이러한 형변환(타입변환)을 "자동 형변환(Promotion)"이라고 합니다

자동 형변환(Promotion)이 이루어지는 순서를 알아보겠습니다.

보시면, long 데이터 타입의 메모리 크기는 8byte이고, float 데이터 타입의 메모리 크기는 4byte인데, long 데이터 타입에서 float 데이터 타입으로 자동 형변환(Promotion)이 가능합니다. 그 이유는 표현할 수 있는 값의 범위가 float가 더 크기 때문입니다.

주의할 점은 메모리 크기가 큰 데이터 타입이라도, 타입 범위를 포함하지 못한다면 자동 형변환(Promotion) 이 불가능합니다.

  • byte 데이터 타입 -> char 데이터 타입 자동 형변환 불가

  • float 데이터 타입 -> long 데이터 타입 자동 형변환 불가

Casting (강제 형변환, 명시적 형변환)

특정 조건을 갖추지 못했지만, 형변환을 하고 싶을때 사용하는 것이 Casting (강제 형변환)입니다.

int intValue = 1; byte byteValue = intValue;

위의 경우 intValue에 저장된 1이라는 값은 byte 데이터 타입에도 저장 가능한 값입니다. 그렇지만, 위 코드를 실행하면 컴파일 에러가 발생합니다. 그 이유는 저장될 값 1에 상관없이 int 데이터 타입이 byte 데이터 타입보다 메모리 크기가 크기 때문입니다. 그림으로 보자면,

이와 같은 그림이 나옵니다. int 데이터 타입에 저장된 정수 1의 값은 실제 메모리에 저장될 때 00000000 00000000 00000000 00000001 의 값을 가집니다. 이 값을 byte 데이터 타입에 끝에 1byte(00000001) 영역만 넣자니 앞에있는 3byte (00000000 00000000 00000000) 값이 날아갑니다. 그림으로 보면 이렇습니다.

https://github.com/GimunLee/tech-refrigerator/raw/master/Language/JAVA/resources/java-promotion-casting-003.png

앞에 3byte의 공간을 삭제하는 시점에서 많은 데이터가 날아가 정상적인 값이 저장될 수 없을 것입니다. 이와 같이 메모리 크기가 큰 int 데이터 타입에서 메모리 크기가 작은 byte 데이터 타입으로 자동 형변환(Promotion)이 된다면, 정상적이지 않은 값이 나올 수 있기 때문에 Java에서 자동 형변환(Promotion)을 하지 않습니다. 하지만, 우리가 형변환 하려는 정수 값은 1 이므로 byte 데이터 타입 범위 안에 충분히 들어가는 값입니다. 우린 그걸 머릿속으로 알고 있기 때문에 byte 데이터 타입으로 변환된다 하더라도 값이 정상적일 거라고 판단할 수 있습니다. 이럴 때 강제 형변환은 아래와 같이 해주시면 됩니다.

int intValue = 1; byte byteValue = (byte) intValue;

형변환 연산

+, -, *, / 과 같은 기본적인 사칙연산은 같은 타입의 피연산자 간에만 수행되기 때문에 서로 다른 데이터 타입의 피연산자가 있을 경우 두 피연산자 중 크기가 큰 타입으로 자동 형변환(Promotion)된 후 연산이 수행됩니다. 예를 들어 int 데이터 타입의 피연산자와 double 타입의 피연산자를 덧셈하면 int 데이터 타입의 피연산자가 double 데이터 타입으로 자동 형변환(Promotion)되고 연산이 수행됩니다. 연산의 결과도 double 데이터 타입이 됩니다.

int intValue = 10; double doubleValue = 5.5; double result = intValue + doubleValue; // intValue 변수값과 doubleValue 변수값을 더해서 double 타입의 result 변수에 저장

만약 int 데이터 타입의 연산 결과를 얻고 싶다면, 강제 형변환(Casting)를 통해 아래와 같이 작성해주시면 됩니다.

int intValue = 10; double doubleValue = 5.5; int result = intValue + (int) doubleValue; // intValue 변수값과 doubleValue변수값을 더해서 int 타입의 result 변수에 저장

오토 박싱 & 오토언박싱

자바에는 기본 타입과 Wrapper 클래스가 존재한다.

  • 기본 타입 : int, long, float, double, boolean

  • Wrapper 클래스 : Integer, Long, Float, Double, Boolean

박싱과 언박싱에 대한 개념을 먼저 살펴보자

박싱 : 기본 타입 데이터에 대응하는 Wrapper 클래스로 만드는 동작

언박싱 : Wrapper 클래스에서 기본 타입으로 변환

오토 박싱 & 오토 언박싱

JDK 1.5부터는 자바 컴파일러가 박싱과 언박싱이 필요한 상황에 자동으로 처리를 해준다.

성능

편의성을 위해 오토 박싱과 언박싱이 제공되고 있지만, 내부적으로 추가 연산 작업이 거치게 된다.

따라서, 오토 박싱&언박싱이 일어나지 않도록 동일한 타입 연산이 이루어지도록 구현하자.

오토 박싱 연산

동일 타입 연산

100만건 기준으로 약 5배의 성능 차이가 난다. 따라서 서비스를 개발하면서 불필요한 오토 캐스팅이 일어나는 지 확인하는 습관을 가지자.

고유 락 (Instrinsic Lock)

Intrinsic Lock / Synchronized Block / Reentrancy

Intrinsic Lock (= monitor lock = monitor) : Java의 모든 객체는 lock을 갖고 있음.

Synchronized 블록은 Intrinsic Lock을 이용해서, Thread의 접근을 제어함.

Q) ++count 문이 atomic 연산인가?

A) read (count 값을 읽음) -> modify (count 값 수정) -> write (count 값 저장)의 과정에서, 여러 Thread가 공유 자원(count)으로 접근할 수 있으므로, 동시성 문제가 발생함.

Synchronized 블록을 사용한 Thread-safe Case

단계 3과 같이 lock 생성 없이 synchronized 블록 구현 가능

Reentrancy

재진입 : Lock을 획득한 Thread가 같은 Lock을 얻기 위해 대기할 필요가 없는 것

(Lock의 획득이 '호출 단위'가 아닌 Thread 단위로 일어나는 것)

Structured Lock vs Reentrant Lock

Structured Lock (구조적 Lock) : 고유 lock을 이용한 동기화

(Synchronized 블록 단위로 lock의 획득 / 해제가 일어나므로)

따라서,

A획득 -> B획득 -> B해제 -> A해제는 가능하지만,

A획득 -> B획득 -> A해제 -> B해제는 불가능함.

이것을 가능하게 하기 위해서는 Reentrant Lock (명시적 Lock) 을 사용해야 함.

Visibility

  • 가시성 : 여러 Thread가 동시에 작동하였을 때, 한 Thread가 쓴 값을 다른 Thread가 볼 수 있는지, 없는지 여부

  • 문제 : 하나의 Thread가 쓴 값을 다른 Thread가 볼 수 있느냐 없느냐. (볼 수 없으면 문제가 됨)

  • Lock : Structure Lock과 Reentrant Lock은 Visibility를 보장.

  • 원인 :

  1. 최적화를 위해 Compiler나 CPU에서 발생하는 코드 재배열로 인해서.

  2. CPU core의 cache 값이 Memory에 제때 쓰이지 않아 발생하는 문제.

직렬화 (Serialization)

자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 데이터 변환하는 기술

각자 PC의 OS마다 서로 다른 가상 메모리 주소 공간을 갖기 때문에, Reference Type의 데이터들은 인스턴스를 전달 할 수 없다.

따라서, 이런 문제를 해결하기 위해선 주소값이 아닌 Byte 형태로 직렬화된 객체 데이터를 전달해야 한다.

직렬화된 데이터들은 모두 Primitive Type(기본형)이 되고, 이는 파일 저장이나 네트워크 전송 시 파싱이 가능한 유의미한 데이터가 된다. 따라서, 전송 및 저장이 가능한 데이터로 만들어주는 것이 바로 **'직렬화(Serialization)'**이라고 말할 수 있다.

직렬화 조건


자바에서는 간단히 java.io.Serializable 인터페이스 구현으로 직렬화/역직렬화가 가능하다.

역직렬화는 직렬화된 데이터를 받는쪽에서 다시 객체 데이터로 변환하기 위한 작업을 말한다.

직렬화 대상 : 인터페이스 상속 받은 객체, Primitive 타입의 데이터

Primitive 타입이 아닌 Reference 타입처럼 주소값을 지닌 객체들은 바이트로 변환하기 위해 Serializable 인터페이스를 구현해야 한다.

직렬화 상황


  • JVM에 상주하는 객체 데이터를 영속화할 때 사용

  • Servlet Session

  • Cache

  • Java RMI(Remote Method Invocation)

직렬화 구현

@Entity @AllArgsConstructor @toString public class Post implements Serializable { private static final long serialVersionUID = 1L;
private String title; private String content;`
serialVersionUID를 만들어준다.
`Post post = new Post("제목", "내용"); byte[] serializedPost; try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { oos.writeObject(post);
        serializedPost = baos.toByteArray();
}
}

ObjectOutputStream으로 직렬화를 진행한다. Byte로 변환된 값을 저장하면 된다.

역직렬화 예시

try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedPost)) { try (ObjectInputStream ois = new ObjectInputStream(bais)) {
    Object objectPost = ois.readObject();
    Post post = (Post) objectPost;
}
}

ObjectInputStream로 역직렬화를 진행한다. Byte의 값을 다시 객체로 저장하는 과정이다.

직렬화 serialVersionUID

위의 코드에서 serialVersionUID를 직접 설정했었다. 사실 선언하지 않아도, 자동으로 해시값이 할당된다.

직접 설정한 이유는 기존의 클래스 멤버 변수가 변경되면 serialVersionUID가 달라지는데, 역직렬화 시 달라진 넘버로 Exception이 발생될 수 있다.

따라서 직접 serialVersionUID을 관리해야 클래스의 변수가 변경되어도 직렬화에 문제가 발생하지 않게 된다.

serialVersionUID을 관리하더라도, 멤버 변수의 타입이 다르거나, 제거 혹은 변수명을 바꾸게 되면 Exception은 발생하지 않지만 데이터가 누락될 수 있다.

요약

  • 데이터를 통신 상에서 전송 및 저장하기 위해 직렬화/역직렬화를 사용한다.

  • serialVersionUID는 개발자가 직접 관리한다.

  • 클래스 변경을 개발자가 예측할 수 없을 때는 직렬화 사용을 지양한다.

  • 개발자가 직접 컨트롤 할 수 없는 클래스(라이브러리 등)는 직렬화 사용을 지양한다.

  • 자주 변경되는 클래스는 직렬화 사용을 지양한다.

  • 역직렬화에 실패하는 상황에 대한 예외처리는 필수로 구현한다.

  • 직렬화 데이터는 타입, 클래스 메타정보를 포함하므로 사이즈가 크다. 트래픽에 따라 비용 증가 문제가 발생할 수 있기 때문에 JSON 포맷으로 변경하는 것이 좋다.

    JSON 포맷이 직렬화 데이터 포맷보다 2~10배 더 효율적

Error & Exception

Assembled by GimunLee (2019-11-19)

Goal

  • Error와 Exception의 차이점에 대해 설명할 수 있다.

  • Exception Handling을 할 수 있다.

Abstract

Error와 Exception은 같다고 생각할 수도 있지만 사실 큰 차이가 있습니다.

Error 는 컴파일 시 문법적인 오류와 런타임 시 널포인트 참조와 같은 오류로 프로세스에 심각한 문제를 야기 시켜 프로세스를 종료 시킬 수 있습니다.

Exception 은 컴퓨터 시스템의 동작 도중 예기치 않았던 이상 상태가 발생하여 수행 중인 프로그램이 영향을 받는 것우로 예를 들면, 연산 도중 넘침에 의해 발생한 끼어들기 등이 이에 해당합니다.

프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우를 프로그램 오류라 하고, 프로그램 오류에는 에러(error)와 예외(exception) 두 가지로 구분할 수 있습니다. 에러는 메모리 부족이나 스택오버플로우와 같이 발생하면 복구할 수 없는 심각한 오류이고, 예외는 발생하더라도 수습할 수 있는 비교적 덜 심각한 오류입니다. 이 예외는 프로그래머가 적절히 코드를 작성해주면 비정상적인 종류를 막을 수 있습니다.

Error의 상황을 미리 미연에 방지하기 위해서 Exception 상황을 만들 수 있으며, java에서는 try-catch문으로 Exception handling을 할 수 있습니다.

Exception Handling

잘못된 하나로 인해 전체 시스템이 무너지는 결과를 방지하기 위한 기술적인 처리입니다. JAVA에서는 예외와 에러도 객체로 처리합니다.

예외가 주로 발생하는 원인

  • 사용자의 잘못된 데이터 입력

  • 잘못된 연산

  • 개발자가 로직을 잘못 작성

  • 하드웨어, 네트워크 오작동

  • 시스템 과부하

Throwable 클래스

Throwable 클래스는 예외처리를 할 수 있는 최상위 클래스입니다. Exception과 Error는 Throwable의 상속을 받습니다.

Error (에러)

Error는 시스템 레벨에서 발생하여, 개발자가 어떻게 조치할 수 없는 수준을 의미합니다.

  • OutOfMemoryError : JVM에 설정된 메모리의 한계를 벗어난 상황일 때 발생합니다. 힙 사이즈가 부족하거나, 너무 많은 class를 로드할때, 가용가능한 swap이 없을때, 큰 메모리의 native메소드가 호출될 때 등이 있습니다. 이를 해결하기위해 dump 파일분석, jvm 옵션 수정 등이 있습니다.

Exception (예외)

예외는 개발자가 구현한 로직에서 발생하며 개발자가 다른 방식으로 처리가능한 것들로 JVM은 정상 동작합니다.

Exception의 2가지 종류

  1. Checked Exception : 예외처리가 필수이며, 처리하지 않으면 컴파일되지 않습니다. JVM 외부와 통신(네트워크, 파일시스템 등)할 때 주로 쓰입니다.

    • RuntimeException 이외에 있는 모든 예외

    • IOException, SQLException 등

  2. Unchecked Exception : 컴파일 때 체크되지 않고, Runtime에 발생하는 Exception을 말합니다.

    • RuntimeException 하위의 모든 예외

    • NullPointerException, IndexOutOfBoundException 등

대표적인 Exception Class

  • NullPointerException : Null 레퍼런스를 참조할때 발생, 뭔가 동작시킬 때 발생합니다.

  • IndexOutOfBoundsException : 배열과 유사한 자료구조(문자열, 배열, 자료구조)에서 범위를 벗어난 인덱스 번호 사용으로 발생합니다.

  • FormatException : 문자열, 숫자, 날짜 변환 시 잘못된 데이터(ex. "123A" -> 123 으로 변환 시)로 발생하며, 보통 사용자의 입력, 외부 데이터 로딩, 결과 데이터의 변환 처리에서 자주 발생합니다.

  • ArthmeticException : 정수를 0으로 나눌때 발생합니다.

  • ClassCastException : 변환할 수 없는 타입으로 객체를 변환할 때 발생합니다.

  • IllegalArgumentException : 잘못된 인자 전달 시 발생합니다.

  • IOException : 입출력 동작 실패 또는 인터럽트 시 발생합니다.

  • IllegalStateException : 객체의 상태가 매소드 호출에는 부적절한 경우에 발생합니다.

  • ConcurrentModificationException : 금지된 곳에서 객체를 동시에 수정하는것이 감지될 경우 발생합니다.

  • UnsupportedOperationException : 객체가 메소드를 지원하지 않는 경우 발생합니다.

주요 Method

  • printStackTrace() : 발생한 Exception의 출처를 메모리상에서 추적하면서 결과를 알려줍니다. 발생한 위치를 정확히 출력해줘서 제일 많이 쓰며 void를 반환합니다.

  • getMessage() : 한줄로 요약된 메세지를 String으로 반환해줍니다.

  • getStackTrace() : jdk1.4 부터 지원, printStackTrace()를 보완, StackTraceElement[] 이라는 문자열 배열로 변경해서 출력하고 저장합니다.

Exception Handling

JAVA에서 모든 예외가 발생하면 (XXX)Exception 객체를 생성합니다. 예외를 처리하는 방법에는 크게 2가지가 있습니다.

  1. 직접 try ~ catch 를 이용해서 예외에 대한 최종적인 책임을 지고 처리하는 방식

  2. throws Exception 을 이용해서 발생한 예외의 책임을 호출하는 쪽이 책임지도록 하는 방식 (주로 호출하는 쪽에 예외를 보고할 때 사용합니다.)

다른 메소드의 일부분으로 동작하는 경우엔 던지는 것을 추천합니다.

예외 잡기 (try ~ catch 구문)

로직 중에 예외가 발생할지도 모르는 부분에 try ~ catch 구문으로 보험 처리합니다.

  • try 에는 위험한 로직이 들어가고, catch 에는 예외 발생 시 수행할 로직이 들어갑니다.

  • try 중이라도 예외가 발생한 다음의 코드들은 실행되지 않으며 catch 구문으로 넘어갑니다.

  • catch 구문은 else if 처럼 여러개 쓸 수 있습니다.

  • finally 는 마지막에 실행하고 싶은 로직이 들어가며, 대표적으로 .close() 가 있습니다.

예외 던지기 (throws 구문)

예외 처리를 현재 메소드가 직접 처리하지 않고 호출한 곳에다가 예외의 발생 여부를 통보합니다. 호출한 메소드는 이걸 또 던질건지 직접 처리할 건지 정해야합니다. (return보다 강력합니다.)

public class ThrowsEx { public void call_A() throws Exception { call_B(); }
    private void call_B() throws Exception {
        call_C();
    }
    
    private void call_C() throws Exception {
        System.out.println(1 / 0);
    }
    
    public static void main(String[] args) throws Exception {
        ThrowsEx test = new ThrowsEx();
        test.call_A();
    }
}

실행 결과는 아래와 같습니다.

Exception in thread "main" java.lang.ArithmeticException: / by zero at exception.ThrowsEx.call_C(ThrowsEx.java:13) at exception.ThrowsEx.call_B(ThrowsEx.java:9) at exception.ThrowsEx.call_A(ThrowsEx.java:5) at exception.ThrowsEx.main(ThrowsEx.java:18)

JAVA Stream

Java 8버전 이상부터는 Stream API를 지원한다

자바에서도 8버전 이상부터 람다를 사용한 함수형 프로그래밍이 가능해졌다.

기존에 존재하던 Collection과 Stream은 무슨 차이가 있을까? 바로 **'데이터 계산 시점'**이다.

Collection

  • 모든 값을 메모리에 저장하는 자료구조다. 따라서 Collection에 추가하기 전에 미리 계산이 완료되어있어야 한다.

  • 외부 반복을 통해 사용자가 직접 반복 작업을 거쳐 요소를 가져올 수 있다(for-each)

Stream

  • 요청할 때만 요소를 계산한다. 내부 반복을 사용하므로, 추출 요소만 선언해주면 알아서 반복 처리를 진행한다.

  • 스트림에 요소를 따로 추가 혹은 제거하는 작업은 불가능하다.

Collection은 핸드폰에 음악 파일을 미리 저장하여 재생하는 플레이어라면, Stream은 필요할 때 검색해서 듣는 멜론과 같은 음악 어플이라고 생각하면 된다.

외부 반복 & 내부 반복

Collection은 외부 반복, Stream은 내부 반복이라고 했다. 두 차이를 알아보자.

  • 성능 면에서는 '내부 반복'이 비교적 좋다. 내부 반복은 작업을 병렬 처리하면서 최적화된 순서로 처리해준다. 하지만 외부 반복은 명시적으로 컬렉션 항목을 하나씩 가져와서 처리해야하기 때문에 최적화에 불리하다.

즉, Collection에서 병렬성을 이용하려면 직접 synchronized를 통해 관리해야만 한다.

Stream 연산

스트림은 연산 과정이 '중간'과 '최종'으로 나누어진다.

filter, map, limit 등 파이프라이닝이 가능한 연산을 중간 연산, count, collect 등 스트림을 닫는 연산을 최종 연산이라고 한다.

둘로 나누는 이유는, 중간 연산들은 스트림을 반환해야 하는데, 모두 한꺼번에 병합하여 연산을 처리한 다음 최종 연산에서 한꺼번에 처리하게 된다.

ex) Item 중에 가격이 1000 이상인 이름을 5개 선택한다.

List<String> items = item.stream() .filter(d->d.getPrices()>=1000) .map(d->d.getName()) .limit(5) .collect(tpList());

filter와 map은 다른 연산이지만, 한 과정으로 병합된다.

만약 Collection 이었다면, 우선 가격이 1000 이상인 아이템을 찾은 다음, 이름만 따로 저장한 뒤 5개를 선택해야 한다. 연산 최적화는 물론, 가독성 면에서도 Stream이 더 좋다.

Stream 중간 연산

  • filter(Predicate) : Predicate를 인자로 받아 true인 요소를 포함한 스트림 반환

  • distinct() : 중복 필터링

  • limit(n) : 주어진 사이즈 이하 크기를 갖는 스트림 반환

  • skip(n) : 처음 요소 n개 제외한 스트림 반환

  • map(Function) : 매핑 함수의 result로 구성된 스트림 반환

  • flatMap() : 스트림의 콘텐츠로 매핑함. map과 달리 평면화된 스트림 반환

중간 연산은 모두 스트림을 반환한다.

Stream 최종 연산

  • (boolean) allMatch(Predicate) : 모든 스트림 요소가 Predicate와 일치하는지 검사

  • (boolean) anyMatch(Predicate) : 하나라도 일치하는 요소가 있는지 검사

  • (boolean) noneMatch(Predicate) : 매치되는 요소가 없는지 검사

  • (Optional) findAny() : 현재 스트림에서 임의의 요소 반환

  • (Optional) findFirst() : 스트림의 첫번째 요소

  • reduce() : 모든 스트림 요소를 처리해 값을 도출. 두 개의 인자를 가짐

  • collect() : 스트림을 reduce하여 list, map, 정수 형식 컬렉션을 만듬

  • (void) forEach() : 스트림 각 요소를 소비하며 람다 적용

  • (Long) count : 스트림 요소 개수 반환

Optional 클래스

값의 존재나 여부를 표현하는 컨테이너 Class

  • null로 인한 버그를 막을 수 있는 장점이 있다.

  • isPresent() : Optional이 값을 포함할 때 True 반환

Stream 활용 예제

map()

List<String> names = Arrays.asList("Sehoon", "Songwoo", "Chan", "Youngsuk", "Dajung");
names.stream() .map(name -> name.toUpperCase()) .forEach(name -> System.out.println(name));

filter()

List<String> startsWithN = names.stream() .filter(name -> name.startsWith("S")) .collect(Collectors.toList());

reduce()

Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Optional<Integer> sum = numbers.reduce((x, y) -> x + y); sum.ifPresent(s -> System.out.println("sum: " + s));

sum : 55

collect()

System.out.println(names.stream() .map(String::toUpperCase) .collect(Collectors.joining(", ")));

Record

Java 14에서 프리뷰로 도입된 클래스 타입

순수히 데이터를 보유하기 위한 클래스

Java 14버전부터 도입되고 16부터 정식 스펙에 포함된 Record는 class처럼 타입으로 사용이 가능하다.

객체를 생성할 때 보통 아래와 같이 개발자가 만들어야한다.

보통 EntityDTO 구현에 있어서 많이 사용하는 형식이다.

이를 Record 타입의 클래스로 만들면 상당히 단순해진다.

public record Person( String name, int age ) {}

자동으로 필드를 private final 로 선언하여 만들어주고, 생성자getter까지 암묵적으로 생성된다. 또한 equals, hashCode, toString 도 자동으로 생성된다고 하니 매우 편리하다.

대신 getter 메소드의 경우 구현시 getXXX()로 명칭을 짓지만, 자동으로 만들어주는 메소드는 name(), age()와 같이 필드명으로 생성된다.

Collection

Java Collection 에는 List, Map, Set 인터페이스를 기준으로 여러 구현체가 존재한다. 이에 더해 StackQueue 인터페이스도 존재한다. 왜 이러한 Collection 을 사용하는 것일까? 그 이유는 다수의 Data 를 다루는데 표준화된 클래스들을 제공해주기 때문에 DataStructure 를 직접 구현하지 않고 편하게 사용할 수 있기 때문이다. 또한 배열과 다르게 객체를 보관하기 위한 공간을 미리 정하지 않아도 되므로, 상황에 따라 객체의 수를 동적으로 정할 수 있다. 이는 프로그램의 공간적인 효율성 또한 높여준다.

  • ListList 인터페이스를 직접 @Override를 통해 사용자가 정의하여 사용할 수도 있으며, 대표적인 구현체로는 ArrayList가 존재한다. 이는 기존에 있었던 Vector를 개선한 것이다. 이외에도 LinkedList 등의 구현체가 있다.

  • Map대표적인 구현체로 HashMap이 존재한다. (밑에서 살펴볼 멀티스레드 환경에서의 개발 부분에서 HashTable 과의 차이점에 대해 살펴본다.) key-value 의 구조로 이루어져 있으며 Map 에 대한 구체적인 내용은 DataStructure 부분의 hashtable 과 일치한다. key 를 기준으로 중복된 값을 저장하지 않으며 순서를 보장하지 않는다. key 에 대해서 순서를 보장하기 위해서는 LinkedHashMap을 사용한다.

  • Set대표적인 구현체로 HashSet이 존재한다. value에 대해서 중복된 값을 저장하지 않는다. 사실 Set 자료구조는 Map 의 key-value 구조에서 key 대신에 value 가 들어가 value 를 key 로 하는 자료구조일 뿐이다. 마찬가지로 순서를 보장하지 않으며 순서를 보장해주기 위해서는 LinkedHashSet을 사용한다.

  • Stack 과 QueueStack 객체는 직접 new 키워드로 사용할 수 있으며, Queue 인터페이스는 JDK 1.5 부터 LinkedListnew 키워드를 적용하여 사용할 수 있다. 자세한 부분은 DataStructure 부분의 설명을 참고하면 된다.

Annotation

어노테이션이란 본래 주석이란 뜻으로, 인터페이스를 기반으로 한 문법이다. 주석과는 그 역할이 다르지만 주석처럼 코드에 달아 클래스에 특별한 의미를 부여하거나 기능을 주입할 수 있다. 또 해석되는 시점을 정할 수도 있다.(Retention Policy) 어노테이션에는 크게 세 가지 종류가 존재한다. JDK 에 내장되어 있는 built-in annotation 과 어노테이션에 대한 정보를 나타내기 위한 어노테이션인 Meta annotation 그리고 개발자가 직접 만들어 내는 Custom Annotation 이 있다. built-in annotation 은 상속받아서 메소드를 오버라이드 할 때 나타나는 @Override 어노테이션이 그 대표적인 예이다. 어노테이션의 동작 대상을 결정하는 Meta-Annotation 에도 여러 가지가 존재한다.

Generic

제네릭은 자바에서 안정성을 맡고 있다고 할 수 있다. 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에서 사용하는 것으로, 컴파일 과정에서 타입체크를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안전성을 높이고 형변환의 번거로움을 줄여준다. 자연스럽게 코드도 더 간결해진다. 예를 들면, Collection 에 특정 객체만 추가될 수 있도록, 또는 특정한 클래스의 특징을 갖고 있는 경우에만 추가될 수 있도록 하는 것이 제네릭이다. 이로 인한 장점은 collection 내부에서 들어온 값이 내가 원하는 값인지 별도의 로직처리를 구현할 필요가 없어진다. 또한 api 를 설계하는데 있어서 보다 명확한 의사전달이 가능해진다.

Multi-Thread 환경에서의 개발

개발을 시작하는 입장에서 멀티 스레드를 고려한 프로그램을 작성할 일이 별로 없고 실제로 부딪히기 힘든 문제이기 때문에 많은 입문자들이 잘 모르고 있는 부분 중 하나라고 생각한다. 하지만 이 부분은 정말 중요하며 고려하지 않았을 경우 엄청난 버그를 양산할 수 있기 때문에 정말 중요하다.

Field member

필드(field)란 클래스에 변수를 정의하는 공간을 의미한다. 이곳에 변수를 만들어두면 메소드 끼리 변수를 주고 받는 데 있어서 참조하기 쉬우므로 정말 편리한 공간 중 하나이다. 하지만 객체가 여러 스레드가 접근하는 싱글톤 객체라면 field 에서 상태값을 갖고 있으면 안된다. 모든 변수를 parameter 로 넘겨받고 return 하는 방식으로 코드를 구성해야 한다.

동기화(Synchronized)

필드에 Collection 이 불가피하게 필요할 때는 어떠한 방법을 사용할까? Java 에서는 synchronized 키워드를 사용하여 스레드 간 race condition 을 통제한다. 이 키워드를 기반으로 구현된 Collection 들도 많이 존재한다. List를 대신하여 Vector를 사용할 수 있고, Map을 대신하여 HashTable을 사용할 수 있다. 하지만 이 Collection 들은 제공하는 API 가 적고 성능도 좋지 않다.

기본적으로는 Collections라는 util 클래스에서 제공되는 static 메소드를 통해 이를 해결할 수 있다. Collections.synchronizedList(), Collections.synchronizedSet(), Collections.synchronizedMap() 등이 존재한다. JDK 1.7 부터는 concurrent package를 통해 ConcurrentHashMap이라는 구현체를 제공한다. Collections util 을 사용하는 것보다 synchronized 키워드가 적용된 범위가 좁아서 보다 좋은 성능을 낼 수 있는 자료구조이다.

ThreadLocal

스레드 사이에 간섭이 없어야 하는 데이터에 사용한다. 멀티스레드 환경에서는 클래스의 필드에 멤버를 추가할 수 없고 매개변수로 넘겨받아야 하기 때문이다. 즉, 스레드 내부의 싱글톤을 사용하기 위해 사용한다. 주로 사용자 인증, 세션 정보, 트랜잭션 컨텍스트에 사용한다.

스레드 풀 환경에서 ThreadLocal 을 사용하는 경우 ThreadLocal 변수에 보관된 데이터의 사용이 끝나면 반드시 해당 데이터를 삭제해 주어야 한다. 그렇지 않을 경우 재사용되는 쓰레드가 올바르지 않은 데이터를 참조할 수 있다.

ThreadLocal 을 사용하는 방법은 간단하다.

  1. ThreadLocal 객체를 생성한다.

  2. ThreadLocal.set() 메서드를 이용해서 현재 스레드의 로컬 변수에 값을 저장한다.

  3. ThreadLocal.get() 메서드를 이용해서 현재 스레드의 로컬 변수 값을 읽어온다.

  4. ThreadLocal.remove() 메서드를 이용해서 현재 스레드의 로컬 변수 값을 삭제한다.

◼️ reference check up

Last updated