자바 질문 모음 _ 2

자바 질문

  1. Annotation(관점프로그래밍)
  2. 다형성(Polymorphism)
  3. 오버라이딩과 오버로딩의 차이
  4. 자바에서 다중상속을 막은 이유
  5. Iterator를 쓰지 않고 직접 참조 시 문제점
  6. 제네릭(Generic)
  7. Java 메모리 구조
  8. Java 동등성과 동일성

내용출처


Annotation

자바 어노테이션(Java Annotation)은 자바 소스 코드에 추가하여 사용할 수 있는 메타데이터의 일종이다. 보통 @ 기호를 앞에 붙여서 사용한다. JDK 1.5 버전 이상에서 사용 가능하다. 본질적인 목적은 소스코드에 메타데이터를 표현하는 것. 단순히 부가적인 표현뿐만이 아닌 리플렉션을 이용하면 어노테이션 지정만으로도 원하는 클래스를 주입하는 것이 가능해진다.

Annotation에는 Java에 내장되어 있는 Built-in Annotation, Annotation에 사용되는 Annotation인 Meta-Annotation, 자신의 새로 정의하는 Custom Annotation이 있다.

Built-in Annotation

Java에 내장되어 있으며 주로 컴파일러에게 정보를 제공하기 위한 목적으로 사용된다.

  • @Override : 컴파일러에게 오버라이딩을 명시적으로 알림으로서 잘못된 메소드를 오버라이딩할 시 에러를 통해 알 수 있다.
  • @Deprecated : 더 이상 사용하지 말아야할 메소드를 나타낸다.
  • @SuppressWarning : 의도적으로 경고 메세지를 무시하도록 컴파일러에게 알린다.
  • @FunctionalInterface : 함수형 인터페이스라는 것을 알림으로써 실수를 미연에 방지하도록 한다.

Meta-Annotation

Annotation의 정보를 설정하기 위한 Annotation이다.

  • @Target : Annotation의 적용대상을 지정한다.
  • @Retention : Annotation의 유지기간을 지정한다.
    • SOURCE : 소스파일에만 존재하며 컴파일 시점(클래스 파일)에서는 사라진다.
    • CLASS : 클래스 파일에 존재하고 컴파일러에 의해 사용가능하지만 런타임시에는 사라지기에 JVM에서 사용이 불가하다. Retentiondefault값이다.
    • RUNTIME : 클래스 파일에 존재하며 런타임시에도 사용 가능하다. 런타임시 Reflection을 통해 Annotation정보를 읽어 처리할 수 있다.
  • @Documented : Annotation에 대한 정보가 javadoc에 포함되도록 한다.
  • @Inherited : Annotation이 자식 클래스에도 상속된다. 자식 클래스에도 이 Annotation이 붙은 것으로 인식된다.

Custom annotation

위로


다형성(Polymorphism)

다형성은 상속, 추상화와 더불어 객체 지향 프로그래밍을 구성하는 중요한 특징 중 하나이다. 사전적 의미로는 같은 생물종이지만 모습이나 특징이 고유한 성질을 가지는 것을 말하며, 관용적인 의미로는 클래스나 메소드가 다양한 형태로 사용되는 것을 말한다. (하나의 객체가 여러가지 타입을 가질 수 있는 것을 의미) 즉, 자바에서 다형성은 같은 객체이지만 다양하게 구현되어 각자 고유한 성질을 가지는 객체로 사용되는 것을 말한다.

  • 부모 클래스 타입의 참조 변수로 자식 클래스 타입의 인스턴스를 참조

대표적으로 OverrideOverload, Interface가 있다. 이런 관점에서 보았을때 Generic을 다형성으로 표현하기 어렵다.

참고 : 9. 다형성 / 객체지향적 이해, 다형성과 클래스, 다형성과 인터페이스, 다형성의 개념

위로


오버라이딩과 오버로딩의 차이

오버라이딩과 오버로딩은 자바의 다형성을 구현하는 대표적인 방법이다.

오버라이딩하위 클래스의 성격에 맞게 부모 클래스의 함수를 재정의하는 것을 말한다. 이때 부모 클래스와의 시그니쳐가 완전히 같지만 내부 구현이 달라지는 형태이다.

오버로딩함수 이름만 같고 나머지 시그니쳐가 다른 것을 말한다. 파라미터 자료형이나, 개수 등을 다르게 정의함으로써 확장된 함수를 새로 정의하고 구현하는 것을 말한다.

위로


자바에서 다중상속을 막은 이유

다중 상속에는 여러가지 문제가 내재되어 있다. 예를 들어, 변수명 충돌이나 중복된 클래스 상속으로 인해 오버라이딩이 모호한 다이아몬드 문제가 대표적이다. 무엇보다 자바는 객체지향언어이기에 다중상속을 지원하면 객체지향이 무너질 수 있다.

객체지향의 단일책임 원칙에 의해 클래스는 오직 하나의 기능을 가지고 그 하나의 책임에 집중해야되며, 리스코브 치환 원칙에 따라 자식 클래스를 몰라도 부모 클래스의 함수를 사용할 수 있도록 대치가 가능해야 한다. 하지만 다중상속을 허용하면 클래스의 성질이 복합적으로 섞여 부모와 IS-A 관계가 모호해져 정체성이 불분명해질 수 있다. 이것은 위 객체지향 원칙에 위배되기에 이런 문제를 막고자 다중상속을 금지하였다.

하지만 단일 상속은 클래스를 경직되게 만들고 유연한 구현이 불가하다. 자바는 이런문제를 InterfaceComposition Pattern 을 사용하여 해결할 수 있다. 구현에 대한 책임을 implements 하는 클래스에 위임함으로써 다이아몬드 문제를 해결할 수 있고 상속이 아닌 mixin 개념을 사용하여 기능을 확장시킬 수 있다. mixin 은 클래스가 주 자료형 이외에 추가로 구현할 수 있는 자료형으로써 새로운 기능을 제공할 수 있다. 결국 자료형을 확장함으로써 기능을 추가하되 객체의 정체성은 유지할 수 있다.

Composition Pattern 은 구성이라는 개념을 사용한다. 여러 클래스를 멤버필드에 포함시켜 객체의 기능은 확장시키고 부모 클래스로부터 받은 성격은 유지할 수 있다.

참고 : 객체지향 개발 5대 원리:SOLID

위로


Iterator를 쓰지 않고 직접 참조 시 문제점

Iterator는 내부 구현에 대한 이해 없이 데이터를 순차적으로 탐색할 수 있도록 지원하는 인터페이스이다. 데이터의 내부 구조를 모르더라도 next()함수를 통해 일관된 순차 탐색이 가능하다. LinkedList, HashMap을 구성하는 구체적인 자료구조를 모르더라도 Iterator를 사용하면 원하는 정보를 가져올수 있다. 따라서 내부 구조를 숨길 수 있어 정보은닉 이 보장되고 Iterator 인터페이스를 각 자료구조에 맞게 오버라이딩함으로써 다형성을 보장한다.

Iterator를 쓰지 않을 경우 발생할 수 있는 문제점
  • 내부 구조가 노출되지 않아야하는 클래스로 구성된 리스트를 탐색할 때, 클래스가 탐색 기능을 지원하지 않는 한 탐색이 불가능하다. (정보은닉 문제)
  • Collection 을 구현한 자료구조를 탐색할 때, 각각의 자료구조에 맞는 탐색 기능을 구현해야한다. (다형성 문제)
  • 특히 LinekdList의 경우 데이터를 탐색할 때, 시간 복잡도가 O(n^2)이 나와 성능저하를 유발할 수 있다.

참고 : Iterator에대해서

위로


제네릭(Generic)

클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법이다. 제네릭을 사용하는 이유확장성타입 안전성때문이다. 기능을 구현할때 제네릭을 사용하면 다양한 자료형에 적용이 가능하여 확장성을 보장할 수 있다.

이러한 특징은 Object를 통해서도 보장이 가능하지만 Object는 타입에 대한 검사가 이뤄지지 않기 때문에 컴파일 타임에 문제를 인지할 수 없다. 따라서 잘못된 타입이 적용되었을 경우 에러를 잡기가 힘들다.

반면, 제네릭은 사용하고자하는 자료형을 명시적으로 표시함으로써 잘못된 자료형을 컴파일타임에 찾을 수 있다. 비록 제네릭은 생략될 수 있기 때문에 명시적인 자료형 선언 없이도 사용이 가능하여 타입 안전성이 무너질 수 있으나, 컴파일이 경고를 통해 타입 안전성이 깨진다는 메세지를 보냄으로써 문제 유발 가능성을 알 수 있다.

참고 : 생활코딩 - 제네릭

위로


Java 메모리 구조

Java는 Java코드를 컴파일러를 통해 바이트코드로 변환시킨 뒤 JVM 위에 실행시키는 구조이다. JVM은 자바 바이트코드를 실행시키는 가상머신으로서 자바가 플랫폼에 독립적으로 실행될 수 있게 한다. JVM 구조에는 Class Loader, Execution Engine, Garbage Collector, Runtime Data Areas 가 있다.

- Class Loader : 컴파일러에 의해 바이트코드로 변환된 코드를 Runtime Data Areas 에 클래스 단위로 로드시키고, Link 를 통해 적절히 배치시키는 작업을 한다. Class Loader 로 인해 동적으로 클래스를 로드할 수 있다.

- Execution Engine : Runtime Data Areas 에 배치된 바이트 코드를 실행시키는 역할을 한다. 메모리에 올라온 코드를 명령어 단위로 실행한다.

- Garbage Collector : 어플리케이션이 생성한 객체의 생존여부를 판단하여 더 이상 사용되지 않는 객체의 메모리를 반환함으로써 메모리를 자동적으로 관리하는 역할을 한다.

- Runtime Data Areas : 운영체제로부터 할당받은 메모리를 관리하는 영역이다. JVM 에서 관리하는 메모리 영역은 Method(Static or Class) Area, Runtime Constant Pool, Heap Area, Stack Area, PC Register, Native Method Stack Area 으로 나뉜다.

  • Method (Static or Class) Area : 호출한 클래스와 인터페이스에 대한 Runtime Constant Pool, 메소드와 필드, Static 변수, 메소드 바이트 코드 등을 저장한다.

  • Runtime Constant Pool : Method Area 영역에 포함되는 공간이다. 클래스와 인터페이스, 상수, 메소드와 필드에 대한 모든 참조를 저장한다.

  • Heap Area : 런타임에서 동적으로 할당하는 객체를 저장하는 공간이다. new연산을 통해 생성되는 객체와 배열을 저장하며 GC의 주 대상이 된다.

Heap Area 메모리 관리
  • Young Generation - 객체가 생성되자마자 저장되는 공간이다. 시간이 지날수록 우선순위가 낮아지며 Old 영역으로 내려가게 된다. 이곳에서 객체가 사라지면 Minor GC 가 발생한다.
  • Old Generation - 오래된 객체가 저장되는 공간이다. 이곳에서 객체가 사라지면 Major GC 가 발생한다.
  • Permanent Generation - Class Loader 에 의해 로드되는 클래스나 메소드에 대한 Meta 정보가 저장되는 영역이다. Reflection을 이용하여 동적으로 클래스를 로드하는 경우 자주 사용된다.

  • Stack Area : Stack 구조의 저장공간이다. 함수 호출시 발생하는 지역변수, 매개변수, 연산 데이터 등을 저장하는 공간이다. 함수를 호출하면 push를 통해 Stack 에 저장하고 함수 호출이 종료되면 다음 실행할 함수를 pop하여 함수를 실행한다. 스레드 별로 저장공간을 따로 생성하여 관리한다.

  • PC Register : 현재 수행 중이거나 다음에 실행할 인스트럭션 주소를 저장한다. 연산 수행 중 발생하는 데이터를 레지스터에 저장하였다가 CPU가 필요할 때 가져다 쓴다. 스레드 별로 공간을 만들어 관리한다

  • Native Method Stack Area : 자바가 접근할 수 없는 영역은 C와 같은 Low Level 언어로 작성되어 있다. 따라서 Native 코드를 실행시키면서 발생하는 데이터를 Stack 구조로 저장하기 위한 공간이다. 스레드 별로 생성된다.

참고 : JVM과 메모리 구조(성능 개선을 이한 GC의 활용)

위로


Java 동등성과 동일성

동등성(equivalent)
  • 두 객체를 비교할 때, 객체에 대한 정보가 같을 경우
  • 동등성 검사는 equals()
  • equals() 를 오버라이딩 하지 않을 경우 Object 클래스의 equals() 에 의해 해시값을 비교하기 때문에 동일성 검사가 이루어진다. 따라서 제대로된 동등성 검사를 하기 위해서는 equals()를 오버라이딩 해야한다.
동일성(identity)
  • 두 객체를 비교할 때, 완전히 같은 객체일 경우
  • 동일성 검사는 hashCode()
  • hashCode() 는 객체 별 고유의 해시키를 부여함으로써 객체를 구분한다. 동일한 객체가 아니더라도 같은 객체로 인식하고 싶다면 hashCode() 또한 오버라이딩 해야한다.
  • HashMap, HashSet, HashTable 에서는 객체를 구분할 때 hashCode()를 사용하여 구분하기에 이러한 자료구조를 사용하기 위해서는 hashCode() 를 오버라이딩하여야 제대로 사용이 가능하다.
  • Map 은 데이터를 추가하는 순간부터 객체의 해시값을 기억하므로 이후에 객체의 데이터가 변경되더라도(해시값이 변경되어도) Map 은 인지를 하지 못한다. 따라서 Map 의 키에 추가되는 데이터는 immutable 해야한다.

참고 : Java 의 equals 와 hashCode, 동등성과 동일성, StackOverFlow - Why can hashCode() return the same value for different objects in Java?

위로


Share