Java

Q0. 자바의 장단점

자바의 가장 큰 장점은 JVM을 이용한 플랫폼 독립적이라는 점입니다. 자바에서 코드를 컴파일하면 바이트 코드 형태로 출력됩니다. 이 바이트 코드는 JVM에서 런타임에 완벽한 기계코드로 변경되어 실행됩니다. 하나의 바이트코드를 가지고 서로 다른 기계마다 해당 JVM만 설치되어있으면 다시 컴파일 할 필요 없이 나머지는 JVM에서 해당 기계에 맞도록 실행해줍니다. 그 밖에 C언어 계열에서 지원하는 포인터 연산을 과감하게 배제하여 안정성있는 코드 작성에 집중할 수 있도록 했습니다. 또한 객체지향 프로그래밍을 완벽하게 지원합니다.

자바의 단점은 JVM을 사용하는데서 발생합니다. 자바 프로그램이 실행되기 위해서는 먼저 JVM이 실행되어야 하는데 이에 메모리가 소모되며 실행 속도가 느려지게 됩니다. 또한 바이트코드를 완전한 기계코드로 변환하는 과정에서도 일반적인 컴파일 언어로 작성된 프로그램보다 속도가 느립니다.

Q1. 기본 자료형(primitive data type)과 참조 자료형(reference data type)에 대해 설명하시오

기본 자료형은 byte, short, int, long, float, double, char, boolean이 존재합니다. 기본 자료형으로 생성한 변수에는 값 자체가 저장됩니다. 사용전에 반드시 선언되어야하며 OS에 따라 자료형의 길이가 변하지 않고 null값을 가질 수 없는 특징이 있습니다.

참조 자료형은 class, interface, array, enum이 존재합니다. 참조 자료형으로 생성한 변수에는 객체의 주소값이 저장됩니다. 생성자를 이용하여 새로운 객체의 주소를 가리키거나 null값을 이용하여 해제할 수 있습니다.

Q2.객체지향 프로그래밍(object-oriented programming)에 대해 설명하시오

실세계의 특정 사물을 추상화하여 그에 필요한 멤버 변수와 메소드를 정의하는데서 출발합니다. 캡슐화(관련있는 모든 메소드와 변수를 하나의 클래스로 묶는 것)를 통해 이러한 멤버 변수와 메소드의 이용가능범위를 적절하게 제한하여 안전한 프로그래밍을 지원합니다. 상속의 개념을 이용하여 부모 클래스의 기능을 자식 클래스에서 물려받거나 재정의를 통해 부모와 자식의 다른 기능을 구현하는 다형성까지 포함합니다.

객체지향 프로그래밍을 이용하면 코드의 재사용성을 극대화시켜 개발 및 유지보수 과정에서 효율적입니다. 또한 캡슐화를 통해 클래스 외부에서 불필요한 데이터의 접근을 제한하여 올바른 값을 유지하도록 보호할 수 있습니다.

Q3. 다형성(polymerphism)에 대해 설명하시오

객체지향 프로그래밍에서 하나의 메소드나 클래스가 다양한 방법으로 동작하게 하는 요소를 말합니다. 대표적으로 메소드 오버라이딩, 오버로딩이 있습니다.

메소드 오버라이딩은 자식 클래스에서 부모 클래스의 메소드를 새롭게 재정의하는 것을 말합니다. 메소드의 선언부는 같은 형태지만 몸체에서 다른 역할을 수행하도록 합니다. 같은 이름과 형식의 메소드라도 클래스에 따라 다른 기능을 부여하는 기능을 갖습니다.

메소드 오버로딩은 하나의 클래스에서 같은 이름을 가졌지만 인자의 종류나 갯수가 다른 메소드를 구현하는 것을 말합니다. 같은 기능을 수행하지만 다른 인자를 수행하는 메소드를 정의할 경우 이름을 동일하게 묶어주는 기능을 수행합니다.

Q4. 접근 제어자(access modifier)에 대해 설명하시오

접근제어자는 객체지향 프로그래밍에서 클래스의 멤버 변수 또는 메소드에 설정하는 키워드로 접근 영역을 제한하는데 사용합니다. public, default, protected, private이 존재합니다. 접근제어자를 사용하지 않았을 경우에는 기본적으로 default를 갖습니다. public은 접근 제한을 하지 않음을 의미합니다. default는 같은 패키지 내에서만 접근이 가능함을 의미합니다. protected는 같은 패키지 내 혹은 다른 패키지의 자식 클래스에서 접근이 가능함을 의미합니다. private은 같은 클래스 내에서만 접근이 가능함을 의미합니다.

Q5. 추상 클래스(abstract class)와 인터페이스(interface)에 대해 설명하시오

추상 클래스는 abstract 키워드를 이용하여 미완성 메소드, 즉 형태만 정의해놓고 몸체는 없는 상태를 포함하고 있는 클래스를 의미합니다. 추상 클래스는 상속에 큰 의미를 두고 있습니다. 클래스이기 때문에 extends 키워드를 이용하여 상속을 진행하며 이런 추상 클래스를 상속받은 자식 클래스에서는 반드시 미완성 메소드를 재정의해야합니다. 즉 추상클래스는 상속받아서 기능을 이용하고 확장하는데 목적이 있습니다.

인터페이스는 추상 클래스보다 추상화 정도가 높은 상태를 정의할 떄 사용합니다. 인터페이스는 기능의 구현에 큰 의미를 두고 있습니다. 멤버 변수와 일반 메소드를 가질 수 없으며 오직 상수와 추상 메소드만을 선언할 수 있습니다. implements 키워드를 이용하여 인터페이스와 클래스간의 상속을 진행하며 자식 클래스에서 반드시 메소드를 구현해야합니다. 그 밖에 인터페이스 사이에는 extends를 사용하여 상속이 가능하며 클래스에서 지원하지 않는 다중상속이 가능합니다. 왜냐하면 인터페이스의 모든 메소드는 추상메소드이기에 인터페이스를 implements하는 클래스는 모든 메소드를 구현해 주어야 하기에 비록 이름이 같더라도 어던 메소드를 사용해야 할 지 고민할 필요가 없기 때문입니다. (프린터기, 흑백프린터기, 컬러프린터기 예 생각. 클래스 이름만 알면 될 뿐 내부적으로 구현이 어떻게 이뤄졌는지 알 필요가 없을때)

Q6. final 키워드의 다양한 쓰임새를 설명하시오

변수에 final 키워드를 사용하면 해당 변수를 상수화 할 수 있습니다. final 키워드가 붙어 있는 변수는 초기화만 가능하며 이후 새로운 값으로 변경이나 재할당이 불가능합니다.

다음으로 클래스에 final 키워드를 사용하면 해당 클래스를 상속할 수 없습니다. 이 경우 abstract 키워드와 기능적 면에서 충돌이 있기 때문에 함께 사용할 수 없습니다.

마지막으로 클래스의 메소드에 final 키워드를 사용하면 해당 메소드를 오버라이딩 할 수 없습니다.

Q7. static 키워드의 쓰임새를 설명하시오

클래스 내부의 메소드나 멤버 변수에 static키워드를 사용하면 하나의 인스턴스에 속하지 않고 해당 클래스로부터 생성된 모든 인스턴스가 공통으로 공유하는 메소드와 변수로 변경됩니다.

Q8. 컬렉션(collection) 클래스에서 제네릭을 사용하는 이유를 설명하시오

컬렉션 클래스에서 제네릭을 사용하면 컴파일러는 특정 타입만 포함될 수 있도록 컬렉션을 제한합니다. 컬렉션 클래스에 저장하는 인스턴스 타입을 제한하여 런타임에 발생할 수 있는 잠재적인 모든 예외를 컴파일 타임에 잡아낼 수 있도록 도와줍니다.

Q9. 컬렉션(collection) 클래스의 대표 인터페이스를 설명하시오

컬렉션 클래스는 크게 List, Set, Map이 있습니다.

List는 순차적인 데이터를 저장하며 중복을 허용하는 자료구조입니다. ArrayList, LinkedList, Stack이 하위 클래스로 존재합니다.

Set은 순서를 유지하지 않고 데이터의 중복을 허용하지 않는 자료구조입니다. HashSet, TreeSet이 하위 클래스로 존재합니다.

Map은 키와 값으로 이루어진 데이터를 순서를 유지하지 않고 키의 중복을 허용하지 않는 자료구조입니다. HashMap, TreeMap, HashTable이 하위 클래스로 존재합니다.

Q10. 객체의 직렬화(serialization)에 대해 설명하시오

객체에 저장되어 있는 데이터를 스트림(파일로 저장 혹은 네트워크를 이용하여 전송)에 바로 쓰기 위해 연속적인 데이터로 변환하는 것을 말합니다. 반대로 스트림으로부터 데이터를 읽어 객체로 변환하는 과정은 역직려로하라고 합니다. io패키지 내에 구현되어 있는 Serializable 인터페이스를 상속받으면 직렬화가 가능한 클래스로 변경할 수 있습니다. 직렬화를 시키고자 하는 클래스에 보안정보나 전송하고 싶지 않은, 직렬화에서 제외하고 싶은 객체가 포함되어 있을 경우 transient키워드를 이용하여 해당 객체를 직렬화 대상에서 제외할 수 있습니다.

Q11. 래퍼 클래스(wrapper class)에 대해 설명하시오

기본 자료형으로 표현된 데이터를 참조 자료형으로 만들어야 할 경우 래퍼 클래스를 사용합니다. 보통 특정 메소드에서 참조 자료형을 인자로 받거나, 기본 자료형이 아닌 객체 자료형으로 저장해야 할 경우, 객체간 비교가 필요할 경우에 사용합니다. (int -> Integer, char -> Character 등등)

Q12. 자바에서 쓰레드를 구현하기 위한 2가지 방법을 간단하게 설명하시오

lang 패키지 내에 구현되어 있는 Thread 클래스를 상속받거나 Runnable 인터페이스를 상속받아 run메소드를 재정의하여 구현합니다.

Q13. 자바에서 쓰레드의 동기화(synchronized)와 데드락(deadlock)을 설명하시오

2개 이상의 쓰레드가 하나의 공유 자원에 접근하여 값을 변경하려 할 떄, 동기화를 적용하지 않으면 값이 올바르지 못하게 변경될 가능성이 있습니다. 따라서 공유 변수에 synchronized 키워드를 사용하거나 블록을 설정하여 하나의 쓰레드가 공유자원을 점유하고 있을 경우 다른 쓰레드가 잠시 대기상태에 머무르도록 할 수 있습니다.

데드락은 다음과 같은 예를 들어 설명할 수 있습니다. a쓰레드가 foo라는 공유 변수에 락을 걸어둔 상태로 작업을 진행하고 있고, b쓰레드가 bar라는 공유 변수에 락을 걸어둔 상태로 작업을 진행하고 있다고 가정합니다. 여기서 a쓰레드에서 bar공유 변수가 필요한 코드를 만났지만 bar는 이미 b쓰레드가 선점하고 있으므로 a쓰레드는 대기 상태로 변경됩니다. 동시에 b쓰레드에서도 foo공유 변수가 필요한 코드를 만났다고 가정하면 foo는 이미 a쓰레드가 선점하고 있으므로 b쓰레드도 역시 대기 상태로 변경됩니다. 이렇게 두 쓰레드가 모두 대기 상태에 머무르게 되는 현상을 말합니다.

Q14. String, StringBuffer, StringBuilder에 대해서 설명하시오

String은 문자열을 처리하는 자바의 대표적인 클래스입니다. String클래스는 한번 생성되면 변경이 불가능한 immutable한 성격을 가지고 있습니다. String클래스가 immutable한 이유는 변경이 적고 참조만 많은 경우, 혹은 여러개의 쓰레드에서 공유하는 문자열일 경우 별 다른 동기화를 구현하지 않고 안전하게 공유될 수 있다는 장점이 있기 때문입니다. 하지만 문자열을 변형하는 경우가 많은 경우(자르거나 이어붙이기) 매번 새로운 String 객체가 생성되기 때문에 메모리와 속도 측면에서 비효율적입니다. 따라서 String 클래스는 변경이 적고 단순 참조만 많은 경우에 사용합니다.

StringBuffer클래스와 StringBuilder클래스는 새로운 객체를 생성하지 않고 기존 문자열을 변경합니다. 단, StringBuilder의 경우 쓰레드의 동기화를 지원하지 않기 떄문에 쓰레드에서 사용하기 위해서는 StringBuffer클래스를 이용해야한다. 하지만 속도는 동기화를 처리하지 않는 StringBuilder클래스가 동기화를 처리하는 StringBuffer클래스보다 빠르기 때문에 쓰레드를 사용하지 않는 환경에서는 StringBuilder클래스를 사용하는 편이 유리합니다.

Q15. JVM(java virtual machine)의 메모리 구조와 가비지 컬렉션(garbage collection)을 설명하시오

JVM의 메모리 구조는 크게 class, stack, heap, native 메소드, PC 레지스터로 나뉩니다.

class 영역에는 static 변수, 전역 변수, 클래스에 대한 정보가 저장됩니다.

stack 영역에는 메소드 호출에 따른 메소드를 위한 공간인 프레임(frame)이 생성되어 메소드 안에서 필요한 각종 값이 임시로 저장됩니다. 메소드의 수행이 끝나면 프레임별로 삭제가 진행됩니다.

heap 영역에는 new 연산자로 생성된 객체와 배열이 저장되는 공간입니다. 크게 permanent generation, new, old 영역으로 나뉩니다. permanet generation 영역에 생성된 객체들의 주소값이 저장됩니다. new 영역은 다시 eden과 servivor영역으로 나뉘어있습니다. eden에는 객체들이 최초로 생성되는 영역이며 survivor은 eden 영역에서 참조되는 객체들이 저장되는 영역입니다. 마지막으로 old 영역은 new 영역에서 일정 시간 참조되고 있는 객체들이 저장되는 공간입니다.

navtive 메소드 영역은 자바 이외의 다른 언어에서 제공되는 메소드가 저장되는 영역입니다.

PC 레지스터 영역은 쓰레드가 생성될 때마다 생성되는 영역으로 쓰레드가 어떤 명령(현재 실행되는 부분의 명령과 주소)을 실행할지 저장합니다.

가비지 컬렉션은 크게 minor와 major로 나뉩니다. minor에서는 new영역을 대상으로 실행됩니다. 첫째로 new영역 안의 eden영역이 가득 차면 survivor1 영역으로 이동시킨 후 나머지 영역의 객체를 삭제합니다. 둘째로 eden 영역과 survivor1 영역이 기준치 이상으로 찼을 경우 참조가 실제로 되고 있는지 검사 후 참조되는 객체만 survivor2 영역에 복사 후 나머지 영역의 객체를 삭제합니다. 마지막으로 일정 시간 참조되고 있는 객체들을 old 영역으로 이동시킵니다.

major에서는 old 영역을 대상으로 실행됩니다. minor에 비해 시간이 오래걸리며 old영역이 가득 차 프로세스가 정지될 가능성이 있는 경우 실행됩니다. old영역에 있는 모든 객체를 검사하여 참조되지 않는 객체들을 한꺼번에 삭제합니다.

Q16. NIO(new input-output)을 설명하시오

기존의 자바 IO(input-output)의 단점을 보완한 새로운 IO 패키지를 의미하며 non-blocking IO를 지원한다는 특징이 있습니다. blocking이란 IO작업에서 주로 사용되는 메소드의 경우 실행 속도가 느려 잠시 멈춰있는 상태를 말합니다. 이 문제를 해결하기 위해 보통 쓰레드를 구현했습니다. 하지만 이런 작업이 수만개가 동시에 진행될 가능성이 있는 서버 프로그램일 경우 쓰레드가 너무 많아져 생기는 문제(CPU context switching, memory allocation)가 발생합니다. 실제로 이런 수많은 쓰레드가 모두 작업을 하지 않고 대부분은 자원만 낭비하는 상태로 유지되는 경우가 많기 때문입니다.

따라서 모든 IO에 대해 쓰레드를 생성하는 방식이 아닌 채널관리자(selector)를 사용하여 실제 IO가 발생한 채널만 쓰레드를 생성하여 관리하는 방식입니다. 하지만 채널을 이용한 프로그래밍은 기존 다중쓰레드를 이용한 방식보다 구현하기 어려운 단점이 있습니다.

Q17. 자바의 call-by-value, call-by-reference에 대해 예를 들어 설명하시오

자바의 기본 자료형은 call-by-value, 참조 자료형은 call-by-reference에 의해 메소드의 인자 값을 전달합니다. 예를 들어 아래와 같은 코드에서 swap 함수 내부에서는 a와 b의 값이 변경되나, 메인 메소드에서는 적용되지 않습니다. 메소드의 인자로 a와 b가 call-by-value에 의해 복사된 값이 넘어가기 때문입니다.

public class Main {
  public static void main(String args[]) {
    int a = 1;
    int b = 2;
    System.out.println(a + " " + b);  // 결과 : 1 2
    
    swap(a, b);
    System.out.println(a + " " + b);  // 결과 : 1 2
  }
  
  private static void swap(int a, int b) {
    int tmp = a;
    a = b;
    b = tmp;
  }
}

참조 자료형의 경우에도 메소드의 인자로 넘어갈 때 레퍼런스의 복사 값이 넘어가기 때문에 객체간의 교환은 불가능합니다. 복사된 레퍼런스 값끼리만 복사가 되기 때문입니다. 예를 들어 아래와 같은 경우가 있습니다.

public class Main {
	public static void main(String args[]) {
		Person p1 = new Person("이순신");
		Person p2 = new Person("홍길동");
		System.out.println(p1.name + " " + p2.name); // 결과 : 이순신 홍길동

		swap(p1, p2);
		System.out.println(p1.name + " " + p2.name); // 결과 : 이순신 홍길동
	}

	private static void swap(Person p1, Person p2) {
		Person tmp = p1;
		p1 = p2;
		p2 = tmp;
	}

	private static class Person {
		public String name;

		public Person(String name) {
			this.name = name;
		}
	}
}

따라서 name 멤버 변수를 변경하기 위해서는 다음과 같이 변경해줍니다. 이 경우 레퍼런스이 복사 값이 메소드의 인자로 넘어왔지만 결국 가리키는 객체는 동일하기 때문에 name 멤버 변수 값을 성공적으로 교환할 수 있습니다.

public class Main {
	public static void main(String args[]) {
		Person p1 = new Person("이순신");
		Person p2 = new Person("홍길동");
		System.out.println(p1.name + " " + p2.name); // 결과 : 이순신 홍길동

		swap(p1, p2);
		System.out.println(p1.name + " " + p2.name); // 결과 : 홍길동 이순신
	}

	private static void swap(Person p1, Person p2) {
		String tmp = p1.name;
		p1.name = p2.name;
		p2.name = tmp;
	}

	private static class Person {
		public String name;

		public Person(String name) {
			this.name = name;
		}
	}
}

Q18. 자바의 형변환 규칙은 어떻게 되는지 예를 들어 설명하시오

형변환에는 크게 2가지가 존재합니다. 첫번째는 묵시적 형변환으로 자바에서는 작은 단위를 큰 단위로 바꾸는 경우 묵시적 형변환을 지원합니다. 예를 들어 상속관계에서 하위 객체를 상위 객체로 변환하거나 int형 변수를 double형 변수로 변환할 수 있습니다. 두번쨰는 명시적 형변환으로 큰 단위에서 작은 단위로 바꾸는 경우 명시적 형변환을 프로그래머가 정의해주어야 합니다. 이 경우 데이터의 일부가 유실될 수 있습니다. 예를 들어 long 변수에 저장되어 있는 값을 int 변수로 변환하는 경우 (int)키워드를 사용하는 방식입니다.

Q19. String 클래스의 intern 메소드와 상수 풀(constant pool)에 대해 설명하시오

자바에서 쓰이는 모든 String 객체는 상수 풀에서 관리합니다. 상수 풀은 객체가 생성되는 영역인 heap의 permanent generation영역에 생서되어 자바 프로세스가 종료될 때까지 함께합니다. 상수 풀을 이용하여 String을 관리하는 이유는 중복 문자열에 대한 효율적인 메모리 관리 때문입니다. 같은 문자열이 이미 존재하는데 다시 동일한 문자열이 상수풀에 삽입되려는 경우 삽입을 위해 heap에 생성되었던 문자열을 해제하고 상수 풀에서 관리하는 레퍼런스로 반환해줍니다. 이렇게 함으로서 동일한 문자열로 인해 메모리가 낭비되는 현상을 해결합니다.

String 클래스의 intern 메소드는 heap 영역에 있는 문자열 객체를 상수 풀로 이전시키는 메소드입니다. intern 메소드는 실행 후 해당 문자열과 동일한 문자열이 없다면 해당 객체를 상수 풀에 등록하고 heap 영역에서 해제한 후 레퍼런스 값을 반환합니다. 만약 해당 문자열과 동일한 문자열이 있다면 해당 객체를 heap 영역에서 해제 후 상수 풀에 있는 해당 문자열의 레퍼런스 값을 반환해 줍니다.

Q20. 자바8에서 소개된 람다식과 메소드 레퍼런스를 설명하라

람다식이란 식별자 없이 실행가능한 함수로 함수를 따로 만들지 않고 코드 한줄에 함수를 써서 그것을 호출하는 방식입니다. 람다식은 코드를 간결하게 만들 수 있고 가독성이 향상되며 함수를 만드는 과정없이 한번에 처리할 수 있기에 코딩하는 시간이 줄어들지만, 람다를 사용하면서 만드는 무명함수는 재사용이 불가능하고 재귀와 같이 다소 부적합한 부분도 있습니다.

메소드 레퍼런스는 이미 정의되어 있는 메소드가 있다면 이 메소드의 정의가 람다식을 대신할 수 있다는 개념입니다. 이를 통해 람다식으로 줄어든 코드의 양을 조금 더 줄일 수 있어 가독성을 개선할 수 있습니다.

Q21. JIT(just in time) 컴파일러에 대해서 설명하시오.

Java는 컴파일하면 ByteCode로 변환되고 이 bytecode가 실제 실행될 때 인터프리터에 의해 native code로 해석됩니다.

여기서 bytecode에서 native code로 해석될 때 JIT 컴파일러가 사용됩니다. 이는 같은 코드를 매번 해석하지 않고 실행할 떄 컴파일을 하면서 해당 코드를 캐싱해버리빈다. 이후엔 바뀐 부분만 컴파일하고 나머지는 캐싱된 코드를 사용함으로서 인터프리터의 속도를 개선하는데 도움을 줍니다.

Q21. 자바의 클래스 멤버 변수 초기화 순서에 대해 설명하시오

static 변수 선언부는 클래스가 로드 될 때 제일 먼저 초기화됩니다. 필드 변수 선언부는 객체가 생성 될 때 생성자 블록보다 앞서 초기화되고 생성자 블록은 객체가 생성될 떄 초기화됩니다.

클래스 변수는 클래스가 처음 로딩될 때 단 한번 초기화되고 인스턴스 변수는 인스턴스가 생성될 때마다 각 인스턴스별로 초기화가 이루어집니다.

태그:

카테고리:

업데이트: