자바에 관한 질문들

자바에 관한 질문들

  • 객체지향의 5대 원칙에 대해 있는대로 설명해 보시오.

5대 원칙이라고 말씀하시면 SOLID 말씀이실 것이다. 메탈기어 솔리드가 참 재밌었는데….

S : SRP, Single Responsibility Principle로써 객체와 메소드가 각각 하나씩의 책임을 갖게 하는 원칙이다. 하나의 책임을 수행하도록 객체와 메소드의 분리를 수행해야 하고 필요하다면 접근 제어자를 통해 책임을 강제시키는 것도 필요하다.

  • 꼬리질문: 접근 제어자를 통해 책임을 강제시킨다는 말씀은 무슨 소린가?
    • 객체 내부의 정보에 다른 클래스의 메소드에서 맘대로 접근해 정보를 수정할 수 없게 하고 객체가 스스로 자신의 값을 바꾸는 등의 작업을 수행케 하는 것이다. 이는 객체지향의 다른 원칙들 중 하나인 캡슐화(encapsulation)에도 해당하는 사항이다.
    • 예를 들어 몸무게 값을 갖고 있는 필드를 public으로 선언한다면 해당 필드에는 int 범위 내의 어떠한 숫자라도 위치할 수 있을 것이다. 이런 상황은 개발자가 의도하지 않은 상황으로, 이런 필드는 마땅히 수정자(setter) 메소드를 통해 접근해 값을 바꾸게 하고 데이터에 대한 유효성 검사를 시행하는 것이 옳다.
    • public 필드는 일단 API가 공개되면 개발자의 의도대로 값을 전혀 통제할 수 없기 때문에 위험하다. 이는 나중에 또 설명…

O: OCP, Open Close Principle로써 객체는 확장에는 열려있고(open) 변경/수정에는 닫혀있어야(close) 한다. 쉽게 말해 값이나 성질 등을 하드코딩하기보다는 다형성(polymorphism)과 상속을 활용해 새로운 클래스로 확장시켜나가는 작업이 필요하다.

L: Liskov Substitution Principle 즉 리스코프 치환 법칙이다. 변수의 타입이 선언됐을 때 하위 타입은 상위 타입의 기능을 문제없이 모두 수행할 수 있어야 한다. 하위 타입에만 존재하는 값이나 성질을 가지고 상위 타입의 메소드를 오버라이드하는 경우 이 원칙이 깨지는 경우를 많이 본다.

I: 인터페이스

인터페이스 분리의 원칙이다. 타입에서 제공하는 메소드를 모두 사용할 필요가 없다면 더 작은 타입으로 분리해 내서 인터페이스를 다시 구현해야 한다.

D: Dependency Inversion Principle 의존성 역전의 원칙이다. 객체는 자신이 의존하고 있는 다른 객체의 세부 구현을 몰라도 된다는 원칙이다. 즉 “타입” 에 의존하고 “클래스” 에는 의존 관계를 갖지 말라는 얘기다.

저의 개인적인 생각입니다만 SRP와 OCP를 지키다보면 DIP는 자동으로 지켜지는 경우가 많았다. 객체가 하나씩 책임을 갖고 서로 의존관계를 갖다 보면 DIP는 자동으로 만족된다. 하나의 책임을 가진 객체들이 모여서 큰 기능을 이루게 되면 클래스나 메소드 내부의 하드 코딩된 값들이나 동작들이 모두 소거되고 확장성있고 유지보수하기 쉬운 코드를 작성하게 되는 것 같다.

더 좋은 답변: 더 안정적인 클래스/타입에 의존해야 한다는 원칙이다.

  • 추상클래스와 인터페이스의 차이는? 뭘 써야 옳은가?

결론부터 말씀드리면 이펙티브 자바에서는 “인터페이스를 사용하라” 고 말한다. 제 생각도 같다.

인터페이스에도 디폴드 메소드가 도입됨에 따라 추상 클래스를 사용할 이유는 점점 없어지고 있다. 다만 추상 클래스를 사용하면 클래스가 사용하는 자료구조 등을 통일하도록 “권장” 할 수 있다는 점이 그나마의 이점이지만 어차피 서브클래스를 작성할 때 필드를 새로 정의하면 깨질 장점이므로 솔직히 말씀드리면 제 눈에 보이는 추상클래스의 장점이 별로 없다.

뿐만 아니라 추상클래스가 인터페이스에 비해 뒤떨어지는 가장 치명적인 단점은 다중 상속이 안 된다 는 점이다. 한번에 하나의 추상클래스만 상속받을 수 있으므로 여러 기능을 가지려면 여러 기능을 가진 슈퍼클래스를 갖던지 아니면 타입을 분리해내야 한다.

인터페이스는 다중 구현이 가능하므로 이런 단점을 극복할 수 있다. “믹스인(mix-in) 클래스를 만들기 쉽다” 고도 말한다. 예를 들어 Serializable도 갖고 Comparable도 가지면서 원래 구현하고자 하는 타입도 가지고 하는 식의 구현이 가능하다.

  • JCF에 대해 있는대로 설명해보세요.

JCF는 크게 세 가지 계열로 나눠볼 수 있을 것 같은데 List, Map, Set이 그것이다. 먼저 각 계열별 특징을 간략히 설명드리면 List는 순차 저장이 보장되는 배열같은 자료구조이고 값의 중복이 허용된다. Set은 값의 중복이 허용되지 않으며 순차 저장이 보장되지 않는다. Map은 Key-Value 쌍으로 이뤄진 자료구조이다.

List

List에서 흔히 사용하는 것을 ArrayList를 들 수 있다. ArrayList는 말 그대로 array를 사용한 리스트로 내부적으로 고정된 크기의 배열을 갖고 있다가 값이 가득 차면 두 배 늘어난 크기의 배열을 새로 만들고 거기에 데이터를 옮겨쓰는 작업을 반복 한다.

배열에 담긴 모든 데이터를 복사하고 옮기고 또 원 데이터를 말소시키는 작업은 오버헤드가 발생할 수 있기 때문에 처음부터 리스트가 꽤 많은 요소를 담을 것 같다 싶으면 애초에 좀 큰 배열을 사용하도록 명시적으로 선언해주는 것도 방법일 것이다. java.util.ArrayList의 생성자에는 int값으로 내부 배열의 초기 크기를 명시해줄 수 있다. 참고로 빈 생성자로 어레이리스트 객체를 선언했다면 내부에 만들어지는 배열의 크기는 16이다.

자바에서 가장 흔히 사용되는 선형 자료구조이고 연속된 배열을 사용하기때문에 인덱스를 아는 상태에서 값을 조회하는 작업은 O(1), 데이터의 삽입 / 삭제는 O(n)의 시간 복잡도가 발생한다.

자바에서도 Linkedlist를 사용할 수 있다. 똑같은 List 인터페이스의 구현체이기 때문에 사용법은 어레이리스트와 같고 단순히 앞 원소가 다음 원소의 주소값을 참조로 갖는 Single linkedlist로 구현되어 있다. 이 경우 데이터의 삽입 삭제도 O(1)로 수행할 수 있게 된다.

thread-safe가 보장돼야 하는 경우 synchronized 메소드와 자료구조가 적용된 java.util.Vector를 사용할 수 있지만 성능이 떨어지게 되기 때문에 필요한 상황에만 제한적으로 사용해야 하고 concurrent 패키지에 있는 CopyOnWriteArrayList의 사용을 고려해보는 것이 좋다.

Map

맵은 키 - 밸류 저장소 방식의 자료구조이다. 키는 중복될 수 없다.

Map 인터페이스의 잘 알려진 구현체로는 HashMap과 TreeMap이 존재한다.

우선 HashMap부터 살펴보면 이 자료구조는 해시테이블을 통해 키를 관리하고 값을 저장할 수 있는 자료구조이다. 해시테이블 구조이므로 키는 중복될 수 없다. Key는 또한 원시형 데이터 타입을 사용할 수 없다. java.lang.Object의 서브클래스만이 해시맵의 키가 될 수 있으며 hashCode()와 equals() 가 오버라이드되어 있는 객체여야 한다는 강제조건은 없지만 구현하는 것이 필수적이다.

  • 꼬리질문: 두 메소드가 오버라이드되어야 하는 이유는? 해시테이블의 동작 원리는?
    • 해시테이블의 작동 원리부터 살펴드리면 해시테이블은 객체를 해시 함수에 넣고 한번 돌린 후 산출된 해시값을 인덱스로 삼아 값을 저장하는 방식이다.
    • java.lang.Object의 hashCode() 메소드는 객체를 해시코드로 바꿔주는 메소드로써 아무런 오버라이드가 되어있지 않다면 native 메소드가 호출되어 사용중인 JVM의 전략에 따라 해시코드를 만들어주게 된다.
    • 참고로 말씀드리면 OpenJDK의 경우 해시 코드 생성 전략이 6개가 있는데 기본 전략은 2번, 메모리 주소 기반 생성법이다.
    • 여튼 해시함수는 정의역이 치역보다 크기 때문에 생성된 해시코드의 중복은 필연적으로 발생하기 마련이다. 중복이 발생한 상황을 hash collision이라고 하며 이를 resolve하기 위해 open addressing, chaining 등의 방법이 사용되는데 java.util.HashMap은 chaining 법을 사용하고 있다. 이는 동일한 hashcode를 갖는 key에 엮인 값들은 몽땅 한 테이블에 링크드리스트의 형태로 몰아넣은 것이다.
    • 이 때 중복이 발생한 인덱스까지 찾아가는 데에는 .hashCode()가, 일단 찾아간 후 실제 개발자가 원하는 값을 찾아 링크드리스트를 탐색하는 데에는 .equals() 를 통한 객체의 동등성 비교가 사용되기 때문에 두 메소드가 반드시 동시에 구현돼야 하는 것이다.

HashMap의 성능을 최적화하기 위해서는 좋은 전략으로 구현된 .hashCode()가 중요하고, 해시테이블의 적합한 사이즈도 중요하다. 비둘기집의 원리라는 것이 있다. 잠시 설명드리면 n개의 새장에 n + 1 마리의 새를 집어넣으면 적어도 하나의 새장에는 반드시 두 마리 이상의 새가 들어가게 되는 간단한 정리를 말하는 것이다.

아무리 고품질의 hash function도 충돌 현상을 피하기 어려운데 저장해야 할 객체의 숫자에 맞는 해시테이블의 크기마저 갖지 못했다면 해시 테이블의 성능은 떨어질 수밖에 없다. HashMap도 이를 충분히 고려한 후 구현되었기 때문에 0.75라는 load factor 상수값을 갖고 전체 사이즈의 75% 이상 객체가 차게 되면 자동으로 크기를 두 배 늘린다.

TreeMap은 Red-Black Tree 기반의 자료구조이다. 사용법은 해시맵과 같다. 키의 값을 기준으로 정렬이 필요할 때 더 우수한 성능을 보여준다.

thread-safe함을 보장하기 위해서 HashTable을 사용할 수 있는데 성능을 위해서라면 concurrent 패키지에 있는 ConcurrentHashMap을 사용하는 것이 더 좋겠다.

Set

Set은 값이 중복될 수 없고 순차적 저장이 보장되지 않는 자료구조이다. 자바책들에서는 보따리로 많이 비유하곤 한다.

HashSet은 HashMap을 기반으로 한 Set 인터페이스의 구현체이다. 내부적으로 HashMap의 키 값에 입력받은 객체를 다 넣고 구현한다. 이때 value 칸에 넣을 더미 오브젝트를 객체가 생성될 시점에 하나 만들어서 들고있는 것이 재밌는 점이다.

이외의 동작원리는 해시맵과 아예 같다.

TreeSet은 TreeMap을 기반으로 했다.

  • 접근 제어자에 대해 설명해보세요.

네 종류가 있다.

  • public: 암데서나 접근 가능함.
  • private: 아무데서도 접근이 불가능함. 자기 자신만 접근할 수 있음
  • (default): 아무런 접근 제어자를 달지 않으면 디폴트 상태. 같은 패키지 내에서만 접근가능
  • protected: 같은 패키지 내와 자신을 상속한 서브클래스 에서 접근가능

interface에서는 모든 메소드가 퍼블릭이다.

  • Overload / Override 차이가 뭐에요?

오버로드는 저그의 인구수를 늘려주는데 하이브에서 연구하면 수송선으로도 쓸 수 있다. 디텍터이기도 하다.

같은 이름을 갖는 메소드의 파라메터만 달리 해서 다른 기능을 수행시키는 것이다. 생성자 오버로딩이 즐겨쓰인다. 너무 남용하면 안되고 명확한 문서화를 통해 구현하는 것이 좋겠다.

무작정 생성자 오버로딩만 할 게 아니라 생성자 파라메터의 숫자가 많다면 builder 패턴의 도입을 고려해야 한다.

오버라이드는 상위 타입 / 클래스에 정의되어 있는 메소드를 자식 클래스에서 재정의하는 것이다. 대표적으로 java.lang.Object 에 구현되어 있는 메소드들을 오버라이드해서 쓰는 경우가 많다.

  • 꼬리질문: java.lang.Object에 있는 메소드들에 대해서 설명해보세요.

    • hashCode(): 객체를 해시코드로 바꿔주는 메소드.
    • equals(): 객체의 동등성(equality)을 판별하는 메소드.
    • toString(): 객체를 문자열로 바꿔주는 메소드.
    • finalize(): 객체룰 소멸시키는 메소드…. 인데 이걸 부른다고 해서 바로 gc가 도는 건 아닙니다.
    • clone(): 객체를 복사하는 메소드. 딥 카피가 필요한지 섈로우 카피가 필요한지 오버라이드 당시에 결단해야 합니다.
  • static에 대해 설명해보세요.

static은 정적 변수, 혹은 메소드로 주로 상수값을 저장하거나 유틸 클래스 등을 작성할때 즐겨 쓴다.

기술적인 설명을 해 보자면 이 static 변수는 클래스로더가 돌 때 객체가 만들어지지 않아도 무조건 heap에 변수가 저장되고 class area에 그 참조가 올라가게 된다. 따라서 new를 통해 객체를 만들지 않아도 원래의 기능을 다 수행할 수 있다.

  • String에 대해 아는대로 설명해보세요.

String은 재밌는 클래스인데 가장 흔히 쓰이지만 가장 오해도 많이 받기도 하고 JVM 내부적인 최적화도 많이 돼있는 클래스이다.

우선 String 객체를 만들때부터의 과정부터 설명드리고 싶다. 우리는 String a = "a"; 이런 식의 구현을 많이하고 new String("a"); 와 같은 구현은 잘 하지 않는다.

문자열을 스트링 변수에 바로 할당하면 컴파일 타임에 최적화가 발생하는데, 이 최적화 과정 중에는 interning이라는 과정이 포함되어있다. 같은 값을 갖는 스트링 객체를 String pool에 보관하고 다음번에도 같은 문자열이 선언되면 새 객체를 만들지 않고 풀에서 갖다 쓰는 방식의 구조를 가지게 된다.

String pool이 어디에 위치하냐면…. JDK 1.6까지는 PermGen에 있었으나 1.7부터 PermGen을 deprecate시키기 위한 설계 변경을 하기 시작하면서 Tenured로 이동했다. 그 말인즉슨 gc의 대상이 된다는 말이다.

그래서 String은 객체이지만 동일성 비교 가 먹히는 경우도 있다. 풀에서 반환된 값의 경우 메모리 번지수까지 같을 가능성이 왕왕 있기 때문이다. 하지만 객체의 동일성으로 그 내용을 비교하는 것은 정석이 아니다. 어떠한 경우에도 String 클래스가 제공하는 .equalsIgnoreCase() 와 같은 메소드를 통해 객체의 동등성을 비교해야 할 것이다.

이유는 String에 관련한 유틸 클래스 중 interning 과정을 거치지 않고 문자열 객체를 만드는 경우가 종종 있기 때문이다. 예를 들어 .substring() 같은 메소드는 스트링을 몇 번째 자리부터 몇 번째 자리까지 잘라 새 객체를 만들어주는데 이 때는 인터닝 안하고 바로 new String()으로 객체를 만들어서 갖다준다. 이런 경우 어떠한 상황에도 동일성 비교가 먹히지 않는다.

또 하나 우리가 기억해야 할 점은 String은 불변 객체 라는 점이다. 한번 선언되면 그 내용을 바꿀 수 없다. 그렇기 때문에 interning과 같은 최적화가 가능한 것이다. 문제는 문자열의 내용을 바꿔야 할 일이 있을때 내용을 복제하고, 내용을 바꾼 다음 새로운 객체를 생성해 그 내용을 덮어써야 한다는 점 이다. 적지않은 오버헤드가 발생할 여지가 있다.

문자열의 덧셈 연산을 할 수 있다. "a" + "b" = "ab" 가 된다. 이런 덧셈 연산을 할 경우 .concat() 메소드도 있지만…. 이 메소드는 앞서 설명드린 내용을 그대로 코드로 바꾼 것에 불과하다.

문자열의 덧셈연산을 할 경우 JDK1.8 까지는 StringBuilder를 이용한 형태로 최적화된다. JDK1.9 부터는 StringConcatFactory 라는 객체의 도움을 받는 것으로 최적화 전략이 변경되었다.

  • 꼬리질문: 그러면 개발자가 StringBuilder를 선언해서 쓸 필요는 없겠다.

    • 그렇지 않다. 덧셈 연산 한 번당 빌더 객체 하나가 새로 만들어진다는 점에 주목하자.
    • 같은 문자열 덧셈 연산을 100번, 1000번 수행했다면 글자 하나 더하고 객체를 버리고, 새로 만들고 하는 작업을 반복하게 된다.
    • 이런 경우 처음부터 빌더를 만들어서 .append()로 최적화하는 방향이 맞다.
  • 꼬리질문: StringBuilder와 StringBuffer의 차이점에 대해 논하시오.

    • 둘 다 슈퍼클래스는 AbstractStringBuilder로써 구현한 abstract method는 같다.
    • 그러나 StringBuffer는 메소드와 변수에 synchronized 예약어를 붙여놓았다. thread-safe를 보장해야 할 상황에만 제한적으로 사용해야 퍼포먼스를 보장할 수 있다.
    • 개인적인 실험을 해 본 기억이 난다. StringBuilder와 StringBuffer의 .append() 작업을 각각 1억 번 수행했는데 3초(3000ms) 정도의 실행시간 차이가 발생했다. 적지 않은 차이라고 생각한다.

디자인 패턴 정리

Spring Framework

스프링에서 사용되는 디자인 패턴을 나열하고 설명해 보시오.

  • Builder 패턴: Security에서의 config method, 개발자가 정의한 모델 클래스의 생성에도 많이 쓰인다.

  • Adapter 패턴: Controller의 RequestMapping에 사용됨.

  • Factory 패턴: 특정 타입의 서브클래스들의 인스턴스를 만들어주는 패턴. JDBC 연동 등에 사용됨.

  • Decorator 패턴: 특정 타입의 구현체가 실제 구현체를 갖고 있으면서, 자신은 부가기능만을 구현하고 전달받은 실제 구현체에게 실제 기능을 위임하는 방식.
    인터페이스의 수정 없이 부가기능을 구현할 수 있다.

JDK Dynamic Proxy가 이 방식을 따른다.

  • React.js 에서의 활용: 자바스크립트에서 decorator 패턴을 구현하는 좋은 사례는 고계함수(high-order functions)이다. 리액트에서도 고계 함수가 사용된다. 고계함수를 통해 routing, state management등의 부가 기능이 구현되며 고계함수에서는 부가기능만 구현하고 render()등 원래 기능의 구현은 고계함수의 생성자로 전달된 원래 컴포넌트에서 수행되는 것이다.