ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 4년차 자바 백엔드 기술 질문들
    SE General 2022. 3. 14. 22:14
    반응형
     

    내가 받았던 백엔드 인터뷰 질문들 (Java, Spring)

    실패의 쓰라림도 이후에 일이 잘 되고 나서는 거름으로 바라보게 됩니다. 받은 질문에 대해 좀 더 정확한 답변을 찾아보며, 인터뷰에 할애한 시간이 스스로에게 조금이라도 도움이 되도록 정리

    kadensungbincho.tistory.com

     

     

    최근 '예상치 않게' 백엔드 기술 인터뷰에 참여하게 되었습니다. 

     

    당황한만큼 질문에 대한 응답도 제대로 하지 못했는데요. 

     

    이번 글에서는 기억에 남는 질문들을 정리하고, 그에 대한 답을 정리하여 부족한 부분을 채워보려고 합니다:

     


    Java static 남발 시에는 어떤 문제가 발생하는가? [1]

    Java는 사용자가 '객체지향적'으로 생각하길 '원합니다'. 즉, Java의 모든 객체는 암시적 또는 명시적으로 Object.class에 기원하기에, 사용자는 프로그램을 객체(Object)의 집합으로 인식하게 됩니다. 

     

    사용자는 클래스를 생성할 때, 클래스의 인스턴스가 어떻게 행동할지 정의하게 되는데요. 프로그램은 클래스의 변수나 메소드를 new 키워드를 통해 클래스 인스턴스를 생성하기 전까지는 사용할 수 없습니다. 그러한 생성 시점에 JVM은의 메모리를 할당하고 인스턴스의 주소를 스택에 저장하며, 이후 변수와 메소드를 사용할 수 있게 됩니다.

     

    어떤 것을 static으로 표시하는 것의 의미는 해당 데이터가 어떤 특정 클래스 인스턴스에 연결되어 있지 않는다는 것을 말합니다. 일반적인 non-static 메소드는 사용하기 위해서는 하나의 클래스 인스턴스를 생성해야 합니다. Static 메소드는 호출될 인스턴스가 필요하지 않기 때문에, non-static한 메소드나 멤버에 접근할 수 없습니다. 

     

    사용자가 static 변수나 메소드를 생성하면 그것은 힙의 PermGen(Permanent Generation)에 저장됩니다. PermGen에는 클래스들에 적용된 static과 같은 non-instance 데이터들이 저장됩니다. Java 8부터 PermgGen은 Metaspace가 되었습니다만, static 변수는 기존과 같이 힙에 저장됩니다 [2](static 변수 -> 힙, 현재로 그 외의 static -> Metaspace). 차이는 Metaspace는 auto-growing하고, PermGen은 fixed size라는 점입니다. 추가로, Metaspace는 Native Memory에 속하며 JVM Memory에 속하지 않습니다. 

     

    Static 변수는 클래스가 처음 코드 상에서 reference되어, 처음 JVM에 로드될 때 한 번만 초기화됩니다.

     

    class ParentClass {
        static Car car;
        static {
            car = new Car();
        }
    }

     

    위와 같은 코드에서, 새롭게 생성된 Car() 객체는 힙에 저장되며 static 변수인 car는 생성된 객체 주소를 가지며 Metaspace에 저장됩니다. 

     

    위와 같이 static은 Metaspace의 메모리를 소비하기에, 불필요한 사용은 Java 8의 functional concepts 등을(static method 경우) 사용하여 피하는 것이 좋습니다. 

     

    Memory leak을 어떻게 방지하나? [3]

    Memory leak의 사전적 정의는 애플리케이션에서 더 이상 사용되지 않는 객체가 발생하는데 가비지 콜렉터가 워킹 메모리에서 그 객체들을 (아직 reference되고 있어서) 제거하지 못하는 상황을 말합니다. 그 결과, 애플리케이션은 점점 더 많은 리소스를 소모하고 OOM을 일으키게 됩니다. 

     

    Image from Stackify [3]

    Java Heap Leaks

    메모리 릭의 전형적인 형태는 객체가 release되지 않고 계속 생성되는 상황입니다. 그러한 상황을 쉽게 재현하기 위해서 아래와 같은 JVM 옵션을 활용할 수 있습니다:

     

    -Xms<size>
    -Xmx<size>

     

    위를 통해 초기, 최대 힙 사이즈를 제한하고 힙 사이즈를 작게 만들게 됩니다. 

     

     

    Case1 객체 레퍼런스를 가지는 static field

    첫 번째 사례는, static field가 커다란 객체를 레퍼런스 하는 경우입니다. 

     

    private Random random = new Random();
    public static final ArrayLiat<Double> list = new ArrayList<Double>(1000000);
    
    @Test
    public void givenStaticField() throws InterruptedException {
        for (int i = 0; i < 1000000; i++) {
            list.add(random.nextDouble());
        }
        
        System.gc();
        Thread.sleep(10000);
    }

     

    위의 사례에서, gc가 호출되나 memory 소비량은 줄어들지 않습니다. 

     

    이러한 상황을 방지하기 위해서는, static의 사용에 주의를 기울일 필요가 있습니다. 특히, 커다란 객체를 static으로 레퍼런스하는 것은 전체 객체 그래프 수집을 어렵게 합니다. 

     

    Case2 String.intern() on Long

    두 번째 사례는 String.intern() 관련 건입니다.

     

    @Test
    public void givenLengthString() throws IOException, InterruptedException {
        Thread.sleep(15000);
        
        Strgin str = new Scanner(new File("large.txt"), "UTF-8")
            .userDelimiter("\\A").next();
        str.intern();
        
        System.gc();
        Thread.sleep(15000);
    }

    intern API는 str String을 collect될 수 없는 JVM memory pool에 놓습니다. 그렇기에 gc는 memory를 free up 하지 못합니다. 

     

    이러한 케이스를 방지하기 위해서는 interned String이 PermGen space에 저장된다는 점을 명심해야 합니다. 

     

    또는, 커다란 interned strings을 애플리케이션이 처리하는 경우에는 -XX:MaxPermSize=<size> 로 PergGen 크기를 늘릴 수 있습니다.

    그리고 Java 8를 사용하는 경우에는 PermGen이 Metaspace로 대체되어 OOM을 일으키지 않습니다. 

     

    Case3 Unclosed Streams or Connections

    기술적으로 unclosed streams는 low-level resource leak과 memory leak를 발생시킵니다. 

     

    low-level resource leak은 파일 디스크립션, open 커넥션, 등과 같은 OS-level resource leak입니다. 

    JVM은 이러한 low-level resource를 트래킹하기 위해 메모리를 사용하기에 memory leak이 발생합니다. 

     

    이러한 케이스는 try-with-resource clause를 사용하도록 하여 최대한 방지할 수 있습니다.

     

    unclosed connections은 memory leak을 발생시킵니다. 역시 반드시 사용 후 커넥션은 close하여 방지할 수 있습니다. 

     

    Case4 HashSet에 hashCode()와 equals()가 없는 객체 추가 시

    Set에 hashCode()와 equals()이 없는 같은 객체를 반복적으로 넣으면 계속 크기가 증가합니다. 또한, 한 번 추가되면 그러한 객체를 제거할 수 없는데요. 

     

    Lombok의 @EqualsAndHashCode와 같은 어노테이션을 사용하여 hashCode() 및 equals() 미구현을 최대한 피할 수 있습니다.

     

    pinpoint의 내부는 어떤 형태로 구현되어 있을까?

    크게 구조는 아래와 같습니다:

    pinpoint architecture - Image from [6]

     

    질문이 어떻게 그렇게 자세하게 파악할 수 있는 것인가에 초점이 맞춰져 있었기에, Agent 부분을 살펴봐야 할 듯 한데요. 그 부분은 [7]의 문서를 살펴보고, [4]의 코드를 매칭시켜 보았습니다.

     

    기본적으로 Pinpoint는 단일 노드 APM에서 분산 tracing 기능을 제공하는 것까지로 진화해 왔습니다 [8]. 분산 트랜잭션 tracing을 구현하는 방법에는 크게 수동적인 방법과  자동적인 방법이 있는데요. Pinpoint는 자동적인 방법으로 Bytecode Instrumentation을 통해 구현하고 있습니다.

     

    이 Bytecode 방식은 Pinpoint를 사용하는 입장에서는 라이브러리만 넣으면 될 정도로 간단하지만, 그 만큼 라이브러리를 만드는 입장에서는 기술적으로 까다롭고 어렵다고 합니다. 하지만, 1) Pinpoint 초기 대상 사용자는(네이버 개발자) 매우 많아서 사용쪽 공수를 줄이는 것은 다수의 사용자의 시간을 절약해줄 수 있다는 점 2) 자동적으로 수행된다면 사용자가 API를 가져와 사용하지 않아도 되기에 backward-compatibility를 고려하지 않아도 된다는 점 3) 사용자가 켜고 끄는 것이 간단하다는 점 등의 장점을 고려해 자동적인 방법을 취했다고 합니다. 

     

     

    앞에서 기술한 바와 같이 bytecode instrumentation은 자바 바이트 코드를 다루는 것이기에 생산성을 높이나 개발 위험을 높일 수 있는 방식인데요. 트래킹 코드의 구조는 인터셉터로 추상화되어 있습니다. 그리고 Pinpoint는 분산 트랜잭션을 추적하기 위해 필요한 코드를 클래스 로딩 타임에 애플리케이션 코드에 삽입합니다. 이 방법은 트래킹 코드가 애플리케이션 코드에 직접 주입되기에 성능을 높인다고 합니다. 

     

     

    profiler 모듈 아래에 interceptor, instrument 와 같은 주요 패키지로 보이는 것들이 존재하는데요. 실제 소스코드를 깊게 이해하려면, Bytecode instrument에 대한 지식이 필요할 듯 합니다. addTransformer와 같은 함수들을 보니 Java Instrumentation API를 살펴보는 것이 그 시작점일 듯 합니다 [9, 10].

     

    pinpoint에서 객체 생성 시의 부하는 어떻게 최적화할 수 있는가?

    위에서의 잘못된 답변("proxy 패턴과 같은 형태로 구현하면 될 것 같다")으로 인해, 파생된 질문입니다. 

     

     

     

    transactional과 ThreadLocal 상관관계?

    먼저, 구현된 내부 [11]를 살펴봐야할 듯 합니다. 관련한 주요 설명은 아래 부분입니다:

     

    This annotation commonly works with thread-bound transactions managed by a 
    PlatformTransactionManager, exposing a transaction to all data access operations within the current execution thread. 
    Note: This does NOT propagate to newly started threads within the method.

     

    대상 주로 PlatformTransactionManager에 의해 관리되는 thread-bound 트랜잭션에 해당된다는 내용인데요. 전체적인 구조는 살필 수 있었으나, 코드 상에서 ThreadLocal의 여부를 찾기가 쉽지 않았습니다.

     

    다른 방향으로, @Transactional과 관련해 기존 Imperative Transaction Management에서 Reactive Transaction Management로 변화 시, ThreadLocal의 역할을 Reactor가 하게된다는 내용의 글 [12]을 확인했습니다. 추정과 같이, 하나의 쓰레드에 바운드한 Transactional 상태를 ThreadLocal에 저장하여 관리한다는 내용인데요. 코드 상으로 찾아보기 위해 SpringFramework를 ThreadLocal로 검색해보면, 자세한 설명과 함게 관련있어보이는 TransactionSynchronizationManager가 있습니다 [13].

     

    'Central delegate that manages resources and transaction synchronizations per thread.'라는 코멘트에 맞게, 다양한 ThreadLocal이 주렁주렁 있는 것을 볼 수 있습니다:

     

    public abstract class TransactionSynchronizationManager {
    
    	private static final ThreadLocal<Map<Object, Object>> resources =
    			new NamedThreadLocal<>("Transactional resources");
    
    	private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
    			new NamedThreadLocal<>("Transaction synchronizations");
    
    	private static final ThreadLocal<String> currentTransactionName =
    			new NamedThreadLocal<>("Current transaction name");
    
    	private static final ThreadLocal<Boolean> currentTransactionReadOnly =
    			new NamedThreadLocal<>("Current transaction read-only status");
    
    	private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
    			new NamedThreadLocal<>("Current transaction isolation level");
    
    	private static final ThreadLocal<Boolean> actualTransactionActive =
    			new NamedThreadLocal<>("Actual transaction active");
    ...

     

    Java JIT 컴파일러란 무엇인가?

    [14]로 충분할 듯 하네요.

     

     

     

    톰캣은 왜 요청을 받으면 쓰레드를 생성하나?

     

    톰캣은 쓰레드 풀을 가지고 있는데요. 각 요청마다 톰캣은 쓰레드를 쓰레드 풀에서 꺼내 할당하고 해당 쓰레드가 response를 주고 나면 쓰레드 풀로 돌아가 free 상태가 됩니다.

    Tomcat request process - Image from tomcat docs [15]

     

    concurrent hash map이란? [18]

    HashMap은 thread-safe하지 않고, Hashtable은 연산을 동기화하여 thread-safety를 제공하는데요. 

     

    Hashtable는 thread safe하지만 성능은 좋지 않습니다. High-concurrency와 High-throughtput을 원한다면 ConcurrentMap이 해답이 될 수 있습니다. 

     

    ConcurrentMap은 Map 인터페이스를 확장한 것으로 thread-safety 상황에서의 상응하는 throughput 문제를 해결하기 위해 고안되었습니다. 몇몇의 디폴트 메소드를 override해서, ConcurrentMap은 thread-safe하고 memory-consistent atomic 연산을 제공하기 위한 구현체 가이드라인을 제공해줍니다. 

     

    ConcurrentHashMap(이하 CHM)은 ConcurrentMap 구현체입니다.

     

    성능을 위해서, CHM은 해당되는 노드들로 이뤄진 테이블 버킷들로  구성되어 있으며, 주로 업데이트 시에 CAS 연산을 수행합니다. 테이블 버킷은 lazy initialize됩니다. 각 버킷은 버킷의 첫 노드를 locking하여 독립적으로 locked 됩니다. 읽기 연산을 block하지 않고, update는 최소화합니다.

     

    필요한 버킷의 수는 테이블에 접근하는 쓰레드 수에 상대적이어서 버킷 당 업데이트 진행 중인 건이 대부분 하나 이상이 되지 않도록 합니다. 

     

    그렇기에 CHM의 생성자에서 HashMap도 가진 initialCapacity, loadFactor에 추가적으로 concurrencyLevel을 설정할 수 있습니다 (그러나 Java 8부터 전자의 2개는 하위호환을 위해 남겼고 오직 초기 map 사이즈에만 적용됩니다).

     

     

    hashCode 함수는 해시맵의 성능에 어떤 영향을 미치는가?

    HashMap은 key의 hashCode(), equals() method를 사용하여 버킷 간의 값을 나눕니다. 만약 여러 hashCode()의 값이 같은 버킷에 도달한다면, hashMap을 linked list로 만들게 되어 O(1)이 O(n)이 됩니다. 

     

     

     

    코틀린은 왜 탄생했나?

    코틀린의 장점은?

    webflux는 mvc와 어떻게 다른가?

    노드도 쓰레드풀을 사용하나? [21, 22]

    노드는 Single-threaded에 기반해 설계되었습니다. 노드는 Non-blocking operations을 event-based concurrency를 통해 수행하며 concurrency를 가능하게 합니다.

     

    모던 OS는 디스크에 대한 I/O 요청을 issue할 수 있는 새로운 API를 제공하는데요. 보통 asynchronous I/O로 불립니다. 이 API는 애플리케이션이 I/O를 issue하고 I/O가 끝나기 전에, 컨트롤을 바로 호출자에게 돌려줄 수 있는 기능을 제공합니다.

     

    그러한 기능을 위한 API를 Mac 기준으로 살펴보면 아래와 같습니다:

     

    struct aiocb {
        int             aio_fildes;
        off_t           aio_offset;
        volatile void   *aio_buf;
        size_t.         aio_nbytes;
    }
    
    int aio_read(struct aiocb *aiocbp);
    
    int aio_error(const struct aiocb *aiocbp);

     

    이러한 event-based concurrency를 어렵게 하는 한 가지 이슈는 state 관리 부분인데요. 기존 쓰레드 기반의 concurrency가 stack을 활용해 쉽게 상태를 관리하는 것과 같이 event도 각각의 상태 관리가 필요합니다. 이 부분은 주로 Continuation이라는 개념에 기반해 이벤트를 구분하는 키에 연결되니 데이터 구조를 만들고 상태를 저장하고 필요할 때 다시 꺼내어 처리를 해주게 됩니다.

     

    References

    [1] https://www.linkedin.com/pulse/static-variables-methods-java-where-jvm-stores-them-kotlin-malisciuc/

    [2] https://www.linkedin.com/feed/update/urn:li:article:7549517882646272606?commentUrn=urn%3Ali%3Acomment%3A%28article%3A7549517882646272606%2C6666775262614687744%29&replyUrn=urn%3Ali%3Acomment%3A%28article%3A7549517882646272606%2C6799732294023311360%29 

    [3] https://stackify.com/memory-leaks-java/

    [4] https://github.com/pinpoint-apm/pinpoint

    [5] http://research.google.com/pubs/pub36356.html

    [6] https://pinpoint-apm.gitbook.io/pinpoint/want-a-quick-tour/overview

    [7] https://pinpoint-apm.gitbook.io/pinpoint/want-a-quick-tour/techdetail

    [8] https://github.com/pinpoint-apm/pinpoint/releases

    [9] https://www.cs.helsinki.fi/u/pohjalai/k05/okk/seminar/Aarniala-instrumenting.pdf

    [10] https://www.baeldung.com/java-instrumentation

    [11] https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html

    [12] https://spring.io/blog/2019/05/16/reactive-transactions-with-spring

    [13] https://github.com/spring-projects/spring-framework/blob/v6.0.0-M3/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java

    [14] https://d2.naver.com/helloworld/1230

    [15] https://tomcat.apache.org/tomcat-7.0-doc/architecture/requestProcess/request-process.png

    [16] https://l-webx.gitbooks.io/how_tomcat_works/content/chapter/the_serversocket_class.html

    [17] https://stackoverflow.com/a/27765746/8854614

    [18] https://www.baeldung.com/java-concurrent-map

    [19] https://www.javainuse.com/java/javaConcurrentHashMap

    [20] https://dzone.com/articles/hashmap-performance

    [21] https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

    [22] https://dev.to/arealesramirez/is-node-js-single-threaded-or-multi-threaded-and-why-ab1

     

     

     

     

     

     

     

    반응형
Kaden Sungbin Cho