본문 바로가기

잡다한 개발잡담

Java의 개념 원리를 파악하자

1. 자바는 왜 플랫폼 독립적인 언어라고 불릴까?

자바는 JVM 위에서 동작하기 때문이다.

위의 답을 알기 위해 우리는 자바의 실행 원리를 이해할 필요가 있다.

우리는 자바 언어를 활용해서 코딩을 하게 되는데 이런 프로그래밍 언어는 컴퓨터가 이해하지 못한다. 그래서 이러한 프로그래밍 언어를 javac라는 명령어를 통해 컴파일 하는 것이 필요하다.

컴파일을 하게 되면 .class라는 바이트코드가 생성된다. 이러한 .class 파일은 java라는 명령어를 통해 실행할 수 있다. 실행하게 되면 Java virtual Machine(JVM)이 클래스 파일을 읽어 프로그램을 실행하게 된다.

이 때, JVM의 특징은 os에 종속적이라는 것이다. 즉 윈도우의 jvm과 리눅스의 jvm은 다르므로 각각 os에 알맞은 jvm을 설치해야 한다.

2. JVM과 JRE, JDK의 차이는 무엇인가?

JVM은 자바 클래스 파일을 실행하는 역할이며, JRE는 JVM이 실행될 수 있는 환경을 만들어 주고, JDK는 개발 할 수 있는 여러 툴들을 제공

JVM은 컴파일 된 클래스 파일을 구동하는 역할을 한다.(실제 자바 프로그램을 구동하는 실행자의 역할)

JRE는 Java Runtime Environment로 자바 프로그램이 실행될 수 있는 환경을 말한다.

  • Class Loader - 컴파일 된 클래스 파일을 메모리에 로딩
  • Bytecode Verifier - 로딩된 클래스 파일의 정보가 플랫폼에서 실행되는지 문제가 있는지 없는지 검증
  • Javar Virtual Machine - 검증된 클래스 파일을 플랫폼에서 실행 시켜 준다.

JVM은 JRE의 한 부분으로 JRE는 클래스 파일이 주어지는 동안 자바가 실행될 수 있는 환경을 제공한다.

그렇다면 JDK는? Java Development Kit

프로그램 개발을 위한 것을 제공한다.

컴파일러 - javac , 개발을 위한 데이터베이스, 개발에 필요한 여러가지 툴 셋

출처 : https://www.javacodemonk.com/

 

JavaCodeMonk

Articles on Machine Learning, Microservices Architecture, SDET, Java, Spring and Python technologies.

www.javacodemonk.com

3. 자바의 오토박싱과 언박싱은 무엇인가?

자바에는 크게 두가지의 데이터 타입이 있다. Primitive 데이터 타입과  Object 데이터 타입이다.

Primitive 데이터 타입은 가벼운 데이터로 스택 메모리에 머문다.(boolean, char, byte ... int...)

Object 데이터 타입은 상대적으로 무거운 데이터로 실제 데이터는 힙 영역에 존재하고 이 데이터를 포인팅하는 레퍼런스만 스택 영역에 존재한다.

이 두개의 데이터 타입 영역은 서로 다른 성격을 띄우므로 호환이 되기 어렵다. 그래서 Wrapper 클래스라는 것을 만들어 primitive 데이터를 object 데이터화 시켰다.

그래서 이러한 Wraaper class를 만들어 둘 간의 호환을 가능하게 만들었다. Wrapper 클래스에서 primitive로 변환하는 것을 Unboxing 그 반대는 AutoBoxing이라 한다.

즉 10이라는 primitive 데이터가 있다면 Autoboxing을 통해 Integer 데이터로 만들 수 있다.

4. 자바의 메모리 영역 스택과 힙을 알아보자

자바의 메모리는 가벼운 데이터(primitive, reference) -> 스택, 무거운 데이터가 존재하는 힙(Object) 메모리로 나눌 수 있다.

물론 생성된 객체를 사용하기 위해서는 곧바로 힙 메모리에 접근하는 것이 아닌 스택 메모리에 있는 레퍼런스에 접근해 주소값에 따라 객체에 접근한다.

이케아를 떠올리면 앞의 가벼운 물건들은 바로 집어갈 수 있게 해놓고 무거운 물품들은 어디에 위치해있다고 표시된 태그가 있다. 가벼운 물건(primitivie), 태그(reference)는 스택에 , 태그 위치한 창고는 힙, 창고 안에 있는 무거운 물품은 객체로 생각하자.

만약 힙 메모리가 가득차면 메모리 부족으로 Out of Memory가 발생할 수 있다.

자바에서는 레퍼런스가 끊긴 객체를 수시로 자동청소해주는 것이 가비지 컬렉터다~

5. String과 StringBuffer의 차이점을 알아보자

자바에서는 String 클래스에 한해서 + 연산자를 지원한다. 그럼에도 불구하고 StringBuffer 클래스를 왜 사용할까?

메모리의 관리 이점에 그 정답이 있다.

String의 특징은 무엇인가? 힙 메모리에서 한번 정해진 값으로 이뮤터블하다는 것이 큰 특징이다.

즉 + 연산자를 통한다면 이뮤터블한 String 클래스의 성격 상 새로운 객체를 생성하게 된다. 이것은 힙 메모리의 낭비로 이루어질 수 있다. 물론 기존의 레퍼런스를 제거하므로 가비지 컬렉터가 해결해주겠지만 말이다.

그러나 StringBuffer의 경우 이뮤터블한 객체가 아닌 가변적으로 변경될 수 있다. 즉 StringBuffer의 append 매서드를 통해 새로운 문자열을 만들 때, 힙 메모리에 새로운 객체를 생성하는 것이 아닌 기존의 StringBuffer 객체에 더해서 힙 메모리 영역의 낭비를 줄일 수 있다.

두 클래스의 차이는 String은 Immutable, StringBuffer는 mutable 

결론적으로 문자열의 변경이 자주 일어나는 상황에서는 String보다는 String Buffer를 사용하는 것이 메모리 이점에 더 큰 이점을 가질 수 있다.

6. StringBuilder와 StringBuffer의 차이점을 알아보자

StringBuilder 클래스 역시 StringBuffer와 동일한 방식으로 사용할 수 있으며 mutable한데, 이 둘의 차이점은 무엇일까?

결론부터 말하자면 동기화를 지원하지 않는 StringBuilder가 동기화를 지원하는 StringBuffer 보다 더 빠른 속도를 지원한다.

StringBuffer는 동기화를 지원하며 StringBuilder는 동기화를 지원하지 않는다. 아무래도 동기화를 지원한다는 것은 동기화를 하는 과정을 거치므로 퍼포먼스에서 성능 저하가 일어날 수 있다.

만약 멀티 스레드를 고려하지 않는 단순한 문자열 연산에서는 StringBuilder를 사용하는 것이 더욱 좋지만, 만약 멀티 스레드 환경이라면 StringBuffer를 사용하는 것이 옳다.

7. 오버라이딩과 오버로딩의 차이를 알아보자

오버라이딩은 상속 관계에서 부모로부터 상속받은 매서드를 자식 클래스에서 재정의하는 것을 말한다.

상속받은 매서드는 abstract일 수도 있고, 구현이 완료된 매서드 일 수도 있다. 그러나 매서드의 파라미터와 리턴 타입은 부모와 자식은 반드시 동일해야 한다.(리턴타입의 경우에는 서브 클래스도 가능하다.)

오버로딩은 동일한 매서드의 이름이 같은 클래스 내에 존재하지만 서로 다른 파라미터를 가지고 있다. 그러나 리턴 타입은 같을수도 있다. 사용하는 이유는 가독성을 위한 것이라고 생각하면 될 것 같다.

ex) System.out.println(int i), System.out.println(String s)

오버라이딩은 파라미터 리스트가 반드시 같아야 하며, 오버로딩은 파라미터 리스트가 반드시 달라야 한다. 

8. String new()와 literal은 무슨차이가 있을까

String은 직접 문자열 생성이 가능한 유일한 클래스이다. 

ex) String s1 = new String("a") , String s2 = "Java"

String literal을 이용해 생성할 경우 String Pool의 영역에 저장한다. String Pool은 jdk 1.7이후부터 heap memory 내부에 String pool이 존재하므로 heap memory 영역의 특정 공간이라고 생각하면 된다. 그리고 중요한것은 이 영역은 기본적인 Pooling방식을 따르므로 기존에 같은 내용의 문자열이 존재하는 경우 새로운 객체를 생성하지 않고, 생성된 객체를 재사용한다.

그러나 만약 literal이 아닌 new keyword를 이용할 경우 string pool에 생성되지 않고 같은 내용이더라도 힙 메모리 영역에 따로 생성된다.

9. 'static'은 무엇을 의미하는가?

자바는 객체를 필요할 때마다 메모리에 올려서 실행해야 한다. 그러나 static은 프로그램이란 객체 생성 이전에 jvm에 의해서 클래스가 로딩되는 처음 순간에 자동으로 메모리가 올라오게 된다. 즉 인스턴스와는 상관없다. 또한, 객체 생성이전에 로딩된 메모리이므로 객체의 영역으로는 접근하지 못한다.

변수중 static 이름이 붙은 변수는 객체 생성이전에 클래스 이름으로 접근하는 것이 가능하다. 즉 어느 곳에서 접근해도 같은 값을 가지고 있다. 그래서 흔히 말하는 전역 변수가 필요할 때 사용한다.

static method는 클래스 레벨에서 호출이 가능한 메소드이다. 객체 생성을 한 이후에 레퍼런스를 이용해서 호출하지 않는다. 스태틱 매서드는 크게 2가지의 경우가 있다. 프로그램이 로딩되는 시간에 실행되어야 하는 때 또는 자주 실행되는 매서드일 때 static 으로 생성한다. Math 클래스를 예로 들 수 있다.

10. 추상 클래스와 인터페이스의 차이는?

슈퍼 클래스의 관점에서 보자면 슈퍼 클래스가 어느정도 구현이 되어 있는 상태에서 서브 클래스를 디자인해야 될 경우가 있다. 즉, 슈퍼 클래스가 어느정도 구현되어 있지만 완성된 형태를 서브 클래스에 요구하는 경우 자바에서는 추상 클래스로 디자인한다. 그러므로 서브 클래스로 상속이 될 수 있는 멤버 변수나 매서드들이 있을 수도 있고, abstract 매서드가 있을 수도 있다. 그리고 abstract는 단일 상속만 지원한다.

인터페이스는 실질적인 구현이 전혀 없는 abstract method로 가득차 있다. 구현은 서브클래스에 온전히 맡기고 이러이러한 매서드를 받게 될 것이다라는 규약과 같은 것이다. 인터페이스 내에 변수를 선언하게 되면 자동으로 public static final이 붙게 되어서 변수가 아닌 상수가 된다. 또한 인터페이스는 상속이 아닌 구현의 개념으로 다중 구현이 가능하다.

정리하자면 추상 클래스는 단일 상속만 가능하지만 인터페이스는 다중 구현이 가능하다. 또한 추상 클래스는 모든 접근제어가 가능하다. 그러나 인터페이스는 오직 public만 지원한다. 추상 클래스는 변수와 상수를 모두 사용가능하지만 인터페이스는 오직 상수만이 사용가능다. 또한 추상 클래스는 일반 매서드와 abstarct 매서드 모두 가능하지만 인터페이스는 오직 abstract 매서드만 가능하다.

11. 자바 exception 이란?

실행 중 예기치 않은 상황을 자바에서는 exception이라 한다. 이런 exception이 발생해도 미리 처리해놓는다면 프로그램을 계속 실행이 유지되는데 이런 것을 exception handling이라 한다. 예기치 않은 상황에서 발생하며, 처리가 가능하며, 컴파일 타임에서 발생할 수 있는 checked 와 런타임에서 발생할 수 있는 unchecked로 나눌 수 있다.

반면에 에러는 프로그램에서 핸들링이 불가능하여 회복이 불가능한 경우를 말한다. 가장 대표적인 경우는 out of memory(OOM)으로 프로그램을 재 실행시켜야 한다.

익셉션 핸들링 방법은 크게 2가지가 있다. 현장에서 바로 처리하는 try ~ catch가 있으며 콜러 매서드에 throws 키워드를 이용해서 던지는 방법이다. 후자의 경우에는 콜러 매서드에서 try ~ catch 로 잡거나 또는 그 위의 콜러 매서드로 throws를 던질 수도 있다.

12. Error와 checked Exception, unchecked Exception의 차이

exception의 경우 런타임 이전에 체크하는 exception을 checked Exception 또는 compile time exception이라고 한다. 이런 예외는 ide에서 개발 할 때, 컴파일 타임에서 체크해준다. unchecked Exception은 런타임 시에 발생할 수 있는 것으로 널 포인터 익셉션, 어레이 인덱스 아웃 오브 익셉션, 0으로 나눌 때 발생하는 에리스메틱스 익셉션 등이 있다.

error라는 개념은 심각한 것으로 익셉션 핸들링이 불가능한 것으로 힙 메모리 사이즈의 부족으로 발생할 수 있는 oom이 있다.

13. throws와 throw의 차이를 말해보세요

throws는 익셉션 핸들링의 한가지 방식이다. throw는 예외 상황을 발생시킬 때 사용, 만약 서버에 리퀘스트를 던지고 너무 오랜시간 리스폰스를 받지 못한다면 개발자는 더 이상 기다리지 않고 throw를 통해 예외 상황을 일으킬 수 있다.

throw는 익셉션 객체를 생성해서 던지지만, throws는 발생할 수 있는 익셉션 클래스 이름을 매서드 선언부에 써준다. 즉 throw는 오브젝트를 사용하고 throws는 클래스를 사용한다. 또한 전자는 매서드 내부 후자는 매서드 선언부에 사용되며 전자는 한개만 사용가능하며 후자는 여러 exception을 다중으로 선언 가능하다.

14. byte stream과 character의 차이는?

자바에서는 1byte로 구성된 데이터가 다닐 수 있는 byte stream과 2byte가 기본인 character stream이 있다. 자바에서의 기본 캐릭터인 유니코드를 담기 위해 character stream이 생겼다. 텍스트와 같은 캐릭터 데이터를 핸들링 할 경우에는 character stream을 사용한다.

15. try ~ catch finally에서 system.exit()을 호출 할 경우에는?

system.exit()을 호출했다면 시스템이 종료되므로 finally 블록은 당연히 실행되지 않을 것이다.

그러나 return의 경우 해당 블록을 빠져나가는 것인데 finally 블록이 실행되고 전체적인 블록을 빠져나가게 되므로 finally 블록은 실행될 것이다.

16. Linked List vs Array List

vector와 array List 부터 먼저 비교해보자. vector와 array List의 동작 방식은 비슷하다. vector는 사이즈를 넓혀야 할 경우 2배씩 크기를 넓히는 동작 구조를 가지고 있다. Ex) 10 -> 20, 그리고 벡터는 싱크로나이즈드를 지원한다.

그러나 어레이리스트의 경우 사이즈가 10일 때 50프로씩 즉 15로 늘어난다. 그리고 싱크로나이즈드를 지원하지 않으므로 벡터보다 성능이 좋다.

ArrayList와 LinkedList의 차이를 알아보자. 

ArrayList는 List interface를 구현했으므로 엘리먼트들에 인덱스를 부여하여 빠른 접근이 가능하다. 그러나 추가나 삭제의 경우 전체적인 인덱스를 재배치해야하므로 속도가 느리다.

반면에 LinkedList는 노드라는 개념으로 앞뒤 노드들이 연결되어 있는 구조이다. 따라서 중간에 추가나 삭제할 경우 위치에 관계 없이 빠르게 수행이 가능하다. head와 tail만 영향을 받는다. 그러나 search의 경우 Index의 기능을 사용하지 않으므로 순차적으로 탐색해야 된다. 그래서 어레이리스트보다 많은 시간이 걸린다.

list계열의 컬렉션은 상황에 따라 사용을 달리할 수 있는데 먼저 벡터는 싱크로나이즈드 지원, 사이즈 확장시 2배가 된다. arrayList의 경우 랜덤 액세스가 빈번하게 필요할 때 적절하다. 링크드리스트는 엘리먼트들에 대한 랜덤액세스가 적지만 add나 remove가 빈번하게 일어날 경우 선택하면 좋은 선택이 될 수 있다.

17.  Enumeration과 Iterator의 차이는?

Enumertaion은 vector와 hastable과 같은 초기의 collection에서만 지원하며 스냅 샷을 활용하기 때문에 사용 중 다른 곳에서 collection에 접근하여 추가 또는 삭제를 할 경우 올바른 답을 얻지 못할 수 있다. 그러나 이와 반대로 iterator은 모든 collection 객체를 지원하고 스냅 샷이 아닌 직접 객체에 접근하기 때문에 만약 다른 매서드에서 추가 삭제가 일어날 경우 에러가 떠서 이런 상황을 방지할 수 있는데 이것을 fail-fast라고 한다. 또한 스냅샷이 아닌 직접 접근이므로 remove method를 지원한다.

18. 스레드의 생성 방법의 차이점은?

기본적으로 스레드는 생성하면 자동으로 실행되는 것이 아닌 start()라는 매서드를 반드시 실행해줘야 한다. 그리고 다중 스레드를 지원하기 때문에 빠른 처리가 가능하다. 멀티 쓰레딩의 문제점은 공유 자원의 동기화나 데드락이 발생할 때이다. 스레드의 생성 방법은 크게 2가지이다. Thread 클래스를 상속받는 것이 첫 번째 방법이다. 스레드를 상속받고, run method를 오버라이딩한다. 두번 째 방법은 runnable이라는 인터페이스를 임플리먼트 하는 방법이다. 런 매서드를 오버라이딩 한 뒤에 그 객체를 스레드 컨스트럭트에 넣은 후에 스타트 매서드를 통해 시작할 수 있다. 선호되는 방식은 두번째인데 그 이유는 자바는 하나의 상속만 지원하기 때문에 인터페이스를 사용하는 것이 더 선호된다.

19. 데드락이란?

스레드들이 진행 안하고 서로 기다리고 있는 상황이다. 데드락을 방지하는 방법은 락을 선점하는 순서를 동일하게 하면 해결할 수 있다.
4가지 정도가 있는데 가장 큰 쉬운 방법은 하나이 상의 멀티플 락을 선점하지 않는 방법이 있다. 만약 어쩔수 없이 멀티플 락이 필요한 경우 싱크로나이즈드 내에 싱크로나이즈드 가 있는 중복 락을 피하고, 락의 순서를 동일하게 해야한다. 또는 퍼포먼스가 중요하지 않다면 싱글 스레드를 사용해서 피해보자

20. 메인 메소드에서 static을 제거하면?

실행 안된다. static은 객체 생성 이전에 메모리에 처음 로딩이 되는 영역으로 메인 매서드가 psvm에 실행 할 수 있게 되는 것이다. 제거하게 되면 메모리에 처음 로딩 되는 영역이 없어지므로 실행 되지 않을 것이다.