※ 질문 출처: https://github.com/VSFe/Tech-Interview
JVM은 Java Virtual Machine의 줄임말로 자바 프로그램을 실행하기 위한 가상 실행 환경. 자바 소스코드를 컴파일하면 .class 파일(바이트코드)가 생성되는데, 이를 실행하는 것이 JVM.
자바는 JVM 위에서 실행되기 때문에 플랫폼 독립적이다. 플랫폼 독립적이지 않은 언어의 경우 다른 플랫폼에서의 실행 결과가 달라질 수 있다. 다만 성능은 다소 떨어진다.
JVM은 바이트코드 실행, 메모리 관리, 스레드 및 예외 관리를 수행하며 클래스 로더, 실행 엔진, 메모리 영역으로 구성되어 있음.
JVM은 자바 바이트코드만 있으면 실행할 수 있기 때문에 굳이 자바가 아니더라도 코틀린이나, Jython같이 다른 언어여도 실행시킬 수 있다. 반대로 자바 바이트코드를 JVM 위가 아닌 다른 환경에서 실행시키는 것은 일반적으로 어렵다.
JVM에서 우리의 코드를 실행하게 되면 OS 상에서는 JVM이라는 프로세스 1개만 있는 것으로 보이며, 우리의 프로그램은 JVM 내부의 스레드로 식별되게 됨. 만일 우리의 프로그램이 다른 스레드를 만들게 되면 이는 중첩된 구조가 아닌 여러 개의 스레드가 존재하는 것으로 식별된다.
자바에서 final 키워드는 어느 위치에 붙느냐에 따라 의미가 달라진다.
final 키워드가 붙은 변수, 메소드, 클래스는 값의 변경이나 오버라이드, 상속이 불가능하다는 점이 명확해지므로 상황에 따라 추가적인 컴파일 타임의 최적화를 기대할 수 있다.
인터페이스는 다중 상속이 가능하지만 추상 클래스는 다중 상속을 할 수 없다. 이는 클래스를 다중 상속할 때 발생할 수 있는 문제가 있기 때문이다.
A
/ \
B C
\ /
D
만일 다중 상속을 허용하고 위와 같은 구조로 다중 상속을 했다고 가정하자. D는 어떤 A 메소드를 사용해야할지 모호해진다. 따라서 이러한 모호성을 해결하기 위해 애초부터 다중 상속을 허용하지 않는 것이다.
인터페이스는 일반적으로 구현을 제공하지 않으므로 이러한 문제에서 자유롭다. Java8 이상에서는 default 메소드가 생겼기 때문에 동일한 문제가 생기지만 언어 레벨에서 반드시 명시적으로 해결하도록 안전 장치를 마련해두었다.
C++과 같은 다중 상속을 지원하는 언어의 경우 가상 상속(모호함이 발생할 경우 그냥 하나만 상속하게 처리)이라는 개념을 통해 다이아몬드 문제를 해결하고 있다.
자바의 리플렉션은 메타 프로그래밍 기술로, 런타임에 클래스, 메소드, 필드 등의 정보를 동적으로 조사하고 조작할 수 있다.
리플렉션을 이용하면 클래스의 메소드, 필드, 생성자의 정보를 얻을 수 있고 동적으로 객체를 생성하거나 메소드를 호출할 수 있다.
다만 리플렉션은 다소 오버헤드가 큰 편이고 private 필드나 메소드까지 접근할 수 있으므로 객체지향적이지 않다.
뿐만 아니라 보안적인 문제가 다소 존재하는데, 만일 악성 코드가 리플렉션을 통해 private 메소드를 이용하여 의도치 않은 동작을 일으킬 수 있다. 따라서 리플렉션을 이용하는 코드는 외부 입력을 반영하지 않고 입력 검증을 수행하는 등 보안을 고려해야할 필요가 있다.
리플렉션은 동적으로 클래스 정보를 다룰 수 있으므로 일반적으로 프레임워크 레벨에서 자주 사용된다. 예를 들어 스프링에서는 객체 생성, 의존성 주입, 메소드 호출 등에서 리플렉션을 사용하여 처리한다.
클래스 멤버에 붙어 클래스 자체에 속한다는 의미를 부여하는 키워드. 객체마다 개별적으로 생성되는 것이 아닌 클래스 전체에 공용으로 단 하나가 존재하게 됨(메모리 절약).
static 멤버의 경우 인스턴스 없이 클래스명. 으로 접근이 가능하다. 클래스에서 공유하는 멤버이므로 인스턴스 변수와 인스턴스 메소드에는 접근이 불가능하다. 반드시 같은 static 변수나 메소드에만 접근이 가능.
공통 데이터를 저장, 유틸리티 함수 생성, 중첩 클래스 구현 등에서 사용한다.
중첩 클래스 중 하나로 외부 클래스의 인스턴스와는 독립적으로 존재하는 클래스를 의미. 외부 클래스와 논리적으로는 연관되어 있으나 독립적으로 사용 가능해야할 때 사용.
중첩 클래스는 외부 클래스 인스턴스가 있어야 하지만 static 내부 클래스는 그렇지 않음
프로그램 실행 중 비정상적인 상황이 발생했을 때 처리하기 위해 사용되는 객체 기반의 오류 처리 메커니즘
예외 상황을 Exception 객체로 표현하고 이 객체를 throw하여 예외 발생을 알림.
컴파일 전 반드시 처리가 필요한 Checked Exception과 처리할 필요 없는 Unchecked Exception으로 나뉨. 만일 복구하지 못할 예외라면 Unchecked Exception으로 포장한 뒤 로그를 남기는 방식을 자주 사용.
예외 처리는 Throwable 객체를 생성하며 스택 트레이스를 추적하기 때문에 생성 비용이 높음 따라서 예외 흐름 처리 비용은 높으며 꼭 필요한 부분에만 사용해야 함
자바에서 동기화를 구현하기 위해 사용되는 모니터 락. 지정한 임계 영역에 한 번에 하나의 스레드만 실행할 수 있도록 보장
너무 잦은 synchronized는 멀티스레드의 동시성 이점을 떨어뜨림. 락을 획득하기 전의 스레드는 무한정 대기를 하게 되며 락을 획득하게 될 스레드는 무작위적이므로 기아 상태가 빠지는 스레드가 생길 수 있음.
멀티스레드 환경에서 스레드 별로 독립적인 변수를 저장하는 클래스. 스레드 자신을 Key로 하는 Map에 데이터를 보관. get이나 set 명령어를 호출하면 자기 자신을 key로 하여 ThreadLocalMap을 찾고 해당 장소에서 원하는 값을 get하고 set 하는 방식
컬렉션을 함수형 스타일로 처리할 수 있도록 해주는 API. 선언적 방식으로 데이터를 다룰 수 있게 되며 간결하고 가독성 높은 코드 작성이 가능.
데이터 소스를 추상화하여 연속적인 데이터 흐름으로 처리하며 원본 데이터는 변경하지 않음
Stream의 특징은 아래와 같다.
스트림 생성 -> 중간 연산(filter, map, sorted) -> 최종 연산(collect, forEach, reduce)의 순서로 동작
스트림은 일반적으로 전통적인 for 루프에 비해 더 느림(내부적으로 처리되는 것들이 존재하기 때문)
스트림을 사용할 때 람다식을 사용하게 되는데 람다식으로 들어오는 값은 final이거나, final처럼(정확히 말하자면 초기화 이후 값의 변경이 없음) 동작해야함. 그렇지 않으면 매 실행마다 다른 결과가 도출될 수 있으며 이 경우 문제의 소지가 있기 때문.
더 이상 사용되지 않는 객체를 자동으로 탐지해 메모리에서 해제하는 모듈.
자바에서 모든 객체는 new 키워드를 통해 힙에 할당됨. 개발자가 코드상에서 더 이상 참조하지 않는 객체가 힙에 남아있다면 메모리 누수 발생. GC가 이를 검사하여 메모리를 회수함
자바에서 GC는 Mark & Sweep 방식으로 동작함. Mark 과정을 통해 모든 객체를 스캔하고 현재 참조되고 있는 개체를 표시함. 이후 표시되지 않은 객체를 메모리에서 제거.
JVM의 힙 영역은 young과 old 영역으로 나뉘어져있음. 대부분의 객체는 금방 쓰레기가 된다는 의도로, 대다수의 GC는 young 영역에 있는 젊은 인스턴스들을 대상으로 수행하고 해당 영역에서 오래 살아남았다면 old 영역으로 이동시킴.
young 영역에서 발생하는 GC는 minor GC, old 영역에서 발생하는 GC를 major GC라고 칭함.
young 영역은 다시 한 번 3가지 영역으로 나뉨. Eden 영역은 완전 새로 할당된 객체가 위치함. minor GC가 동작하면 survivor0 혹은 survivor1로 이동. survivor0, survivor1은 최소 1번 이상 GC에서 살아남은 객체가 존재하며 반드시 둘 중 하나는 비어있게 됨
GC의 알고리즘은 아래와 같음
두 메소드는 매우 긴말한 연관 관계가 있다. 만일 두 객체가 equals로 같다고 판단될 경우 hashCode도 반드시 같아야 한다. 이를 만족시키지 못한다면 동등한 두 객체가 해시 기반 컬렉션에서는 다른 객체로 취급 된다.
다만 해시 충돌이 발생할 수 있으므로 해시 코드가 같다몬 equals가 참인 것은 아니다.
Bean은 원래 싱글톤 패턴이 적용됨. @Scope("prototype") 어노테이션을 달아주면 프로토타입 스코프가 되며 요청할 때마다 새로운 인스턴스가 생성됨
스프링 컨테이너의 관리를 받으면서 매번 새로운 객체가 필요할 때 사용
싱글톤 빈에서 프로토타입 빈을 주입받으면 프로토타입 빈이 싱글톤처럼 동작할 수 있으므로 주의 필요
AOP는 관점 지향 프로그램으로 핵심 로직과 별개로 존재하지만 여러 코드에 나타나는 횡단 관심사를 분리하여 코드의 재사용성을 높이는 방법
로깅, 트랜잭션 관리와 같은 코드는 비즈니스 로직과 무방하게 여러 코드에 동시다발적으로 나타나지만 꼭 필요하기는 한 기능들. 이러한 기능을 횡단 관심사로 식별하고 재사용함
주요 구성 요소
Spring은 런타임 시 @Aspect가 붙은 클래스를 감지하고 포인트컷 대상 객체의 프록시를 생성
JDK 다이나믹 프록시 혹은 CGLib을 이용하여 프록시를 생성하는데, JDK의 경우 인터페이스만 있어야 동작 가능하기 때문에 바이트코드를 조작하여 상속하는 CGLib을 스프링에서 사용
클라이언트에서 프록시를 호출하면 설정된 어드바이스를 참고하여 지정된 애스펙트가 적절한 시기에 실행
스프링에서는 스프링 컨테이너 내부에서 동작하여 스프링의 지원을 받을 수 있는 인터셉터의 사용을 권장, 하지만 인증, 인가의 경우 Spring 내부에 요청이 도달하기 이전에 처리할 필요가 있으므로 Filter로 처리함
정적 리소스 요청의 경우에도 매핑된 컨트롤러가 없으므로 해당 요청에 대한 처리가 필요하다면 필터 레벨에서 처리해야함
이 외에도 요청이 Spring MVC에 도달하기 이전에 처리할 필요가 있다면 필터를 사용
DisPatcherServlet은 스프링 내부에서 중앙 요청 처리자 역할을 수행. Front Controller 패턴을 구현하여 서버로 들어오는 모든 HTTP 요청을 가로채서 처리.
디스패처 서블릿은 싱글톤으로 동작하지만 Thread-safe하게 설계되어 있기 때문에 멀티스레드 요청들을 모두 처리할 수 있음
스프링 컨테이너가 실행되면 HandlerMapping이 @RequestMapping, @GetMapping 등 URL 패턴이 등록된 어노테이션을 모두 매핑 테이블로 만들어 미리 저장해둠
DispathcerServlet에 요청이 들어오면 요청 URL을 이용하여 HandlerMapping의 매핑 테이블을 이용하여 어떤 Controller로 이동해야하는지를 찾음
JPA는 자바 진영에서 사용하는 ORM 기술 표준을 의미. 일반적으로 해당 기술을 구현한 Hibernate를 사용
데이터베이스는 객체지향의 개념이 없음. 상속과 같은 개념이 없어 이를 DB에서 구현하려면 많은 코드가 필요 -> JPA는 상속과 관련된 패러다임 불일치를 개발자 대신 해결
객체는 참조를 사용하여 연관관계를 맺지만 테이블은 FK를 통해 연관 관계를 맺음. DB의 특정을 살려 단순히 엔티티에 FK를 저장할 경우 객체지향 프로그래밍의 특징이 소멸(FK은 객체가 아닌 단순한 값이니) -> JPA는 연관관계와 관련된 패러다임 불일치도 해결
즉 자바를 이용하여 객체지향적으로 개발할 수록 DB 관련 코드는 객체지향적이지 않으므로 패러다임적인 불일치로 인한 많은 시간과 코드가 할애되고 이를 해결하기 위해 JPA를 사용함
엔티티의 생명 주기를 관리하는 저장소. 엔티티 매니저가 생성될 때 내부에 자동 생성하며 식별자 값으로 구분.
트랜잭션을 커밋하는 순간 영속성 컨텍스트에 저장된 엔티티를 DB에 반영함.
영속성 컨텍스트 사용의 장점은 아래와 같음
엔티티의 생명 주기는 아래와 같음
N+1 문제는 DB에서 데이터를 가져올 때 발생하는 비효율적으로 쿼리가 N번 발생하는 현상을 의미.
어떤 엔티티 P가 있고 P는 여러 자식 엔티티 C를 가진다고 하자. 이때 우리는 P와 P의 자식 C를 모두 조회하고 싶다. 이를 위해 P를 조회하고 (쿼리 1번 실행) 이후 P의 자식 C의 개수 N번 만큼의 쿼리가 추가적으로 실행한다면 쿼리는 총 N + 1개가 실행된다 이를 N + 1 문제라고 한다.
해결 방법은 아래와 같다.
어노테이션이 작성된 영역의 모든 작업을 하나의 트랜잭션으로 묶어주는 역할을 하는 어노테이션
트랜잭션 관련된 작업은 트랜잭션을 시작한 후 메소드가 정상적으로 완료되면 커밋해야하고 예외가 발생하면 롤백해야하는데 이러한 작업을 자동으로 수행하게 한다.
Spring AOP 기반으로 동작함. @Transactional이 붙은 클래스를 빈으로 등록할 때 원본 객체 대신 프록시를 만듦
프록시가 원본 메소드의 호출을 가로채고 트랜잭션 시작 -> 작업 수행 -> 예외 처리 혹은 트랜잭션 종료를 수행
readOnly 값으로 true를 주면 해당 트랜잭션 내부는 읽기 작업만 수행한다는 의미로 내부적으로 잠금을 줄이거나 불필요한 변경 감지를 하지 않게 됨 -> 성능적인 최적화 가능
단순히 1회의 읽기 작업에 대해서는 트랜잭션을 걸 필요가 없다. 하지만 여러 개의 읽기 작업을 수행하는 경우에는 트랜잭션을 걸지 않으면 데이터의 일관성에 문제가 생길 수 있다.
만일 데이터의 일관성이 중요하다고 판단될 경우에는 읽기 전용 트랜잭션을 걸어 데이터의 일관성을 유지해주는 것이 좋다.