티스토리 뷰
자바 신입 백엔드 모의 면접 스터디 정리
신입 자바 백엔드 개발자들이 모여 진행한 모의 면접 스터디 기록입니다. 각자 이력서 기반 질문과 CS 질문을 1~2개씩 준비해서 서로에게 면접관 역할로 질문을 던졌고, 스터디가 끝난 뒤 모범 답안을 함께 정리했습니다.
이 글은 실제 면접에서 답변하듯 말하는 흐름 그대로 정리했습니다. 질문을 읽고 스스로 답해본 다음 아래 답변과 비교해보시면 더 도움이 될 것 같습니다.
1. 운영체제
Q. 프로세스와 스레드의 차이점과, 스레드를 사용하는 이유를 설명해주세요.
프로세스는 운영체제로부터 독립된 메모리 영역, 그러니까 Code, Data, Stack, Heap을 각각 할당받는 실행 단위입니다. 그래서 프로세스끼리는 기본적으로 메모리를 공유하지 않습니다. 반면 스레드는 프로세스 내부에서 만들어지는 실행 흐름인데요, Stack 영역만 독점하고 나머지 Code, Data, Heap은 같은 프로세스 안의 다른 스레드들과 공유합니다.
그럼 왜 멀티프로세스 대신 멀티스레드를 쓰느냐. 메모리를 공유하니까 스레드 간 통신이 빠르고, 컨텍스트 스위칭이 일어나도 캐시를 초기화할 필요가 없어서 오버헤드가 적기 때문입니다. 물론 공짜는 아닙니다. 스레드 하나가 예외를 일으키면 프로세스 전체가 죽을 수 있고, 자원을 공유하는 만큼 경쟁 상태(Race Condition) 같은 동기화 문제를 개발자가 직접 챙겨야 한다는 단점이 있습니다.
꼬리 1. 컨텍스트 스위칭이 일어날 때, 프로세스 전환과 스레드 전환의 비용 차이는 구체적으로 어디서 발생하나요?
프로세스를 전환할 때는 가상 메모리 주소 매핑 정보를 담고 있는 TLB(Translation Lookaside Buffer) 캐시를 완전히 비워야 해서 캐시 미스가 크게 발생합니다. 반면 스레드는 주소 공간을 공유하니까 TLB를 비울 필요가 없고, 그래서 전환 비용이 훨씬 저렴합니다.
꼬리 2. 스레드가 메모리를 공유해서 생기는 자바의 동시성 이슈는 어떤 키워드나 클래스로 해결할 수 있나요?
세 가지 정도를 들 수 있습니다. volatile로 메모리 가시성을 확보하는 방법, synchronized나 ReentrantLock으로 상호 배제를 거는 방법, 그리고 AtomicInteger처럼 CAS 기반의 논블로킹 연산을 사용하는 방법입니다.
2. 네트워크 · HTTP
Q. TCP가 연결을 설정할 때(3-way)와 해제할 때(4-way)의 과정과, 둘의 차이를 설명해주세요.
TCP는 신뢰성 있는 연결을 지향하기 때문에, 통신을 시작하기 전에 양쪽 모두 준비가 됐는지 확인하는 핸드셰이크 과정을 거칩니다.
연결할 때는 3단계입니다. 클라이언트가 SYN을 보내면, 서버가 수락의 의미로 SYN-ACK을 보내고, 클라이언트가 최종 확인으로 ACK을 보내면서 연결 통로가 확립됩니다.
해제할 때는 4단계인데요. 한쪽이 끊겠다고 FIN을 보내면 상대방이 일단 ACK으로 확인만 해줍니다. 왜 바로 안 끊느냐면, 아직 전송 중인 데이터가 남아 있을 수 있기 때문입니다. 남은 데이터를 마저 보낸 뒤에 자신도 FIN을 보내고, 최초 요청자가 마지막 ACK을 보내면서 세션이 안전하게 종료됩니다. 해제가 한 단계 더 많은 이유가 바로 이 "남은 데이터 처리" 때문입니다.
꼬리. 연결을 해제한 뒤에도 클라이언트가 소켓을 바로 닫지 않고 TIME_WAIT 상태로 대기하는 이유는 무엇인가요?
두 가지 이유가 있습니다. 첫째, 마지막에 보낸 ACK이 유실되면 서버가 FIN을 재전송하는데, 이걸 받아서 처리해주기 위해서입니다. 둘째, 네트워크 지연으로 뒤늦게 도착한 패킷이 같은 포트로 새로 열린 소켓에 흘러들어가 데이터를 오염시키는 것을 막기 위해서입니다.
Q. 자주 쓰는 HTTP 상태 코드와 REST API 설계 원칙을 설명해주세요. (200번대 3개, 400번대 4개, 500번대 1개)
상태 코드부터 말씀드리겠습니다.
200번대는 성공 응답입니다. 200 OK는 요청 성공, 201 Created는 요청이 성공해서 새로운 자원이 생성됐다는 뜻이고, 204 No Content는 성공은 했지만 돌려줄 응답 본문이 없다는 의미입니다.
400번대는 클라이언트 쪽 오류입니다. 400 Bad Request는 문법이 잘못된 요청, 401 Unauthorized는 인증이 안 됐으니 로그인이 필요하다는 뜻, 403 Forbidden은 인증은 됐지만 접근 권한이 없다는 뜻, 404 Not Found는 요청한 자원이 없다는 뜻입니다. 여기에 409 Conflict, 요청이 서버의 현재 상태와 충돌한다는 코드도 자주 쓰입니다.
500번대는 서버 오류로, 대표적으로 500 Internal Server Error가 코드 에러나 DB 연결 실패 같은 서버 내부 문제를 나타냅니다.
REST API 설계 원칙은 REST의 6가지 제약 조건을 따르는 것인데, 실무에서 특히 중요한 네 가지만 꼽자면 이렇습니다. 첫째, URI는 동사가 아닌 명사로, 자원 중심으로 설계합니다(/users, /posts). 둘째, 행위는 URI가 아니라 GET, POST, PUT, PATCH, DELETE 같은 HTTP 메서드로 구분합니다. 셋째, 서버는 클라이언트의 상태를 저장하지 않는 무상태성을 지킵니다. 넷째, 클라이언트는 요청이 중간에 어떤 계층을 거치는지 알 필요가 없다는 계층화 원칙입니다.
참고로 여기서 자주 이어지는 후속 질문이 "PUT과 PATCH의 차이"입니다. PUT은 리소스 전체를 새 값으로 덮어쓰기 때문에 안 보낸 필드는 null이나 기본값이 되어버리고, PATCH는 보낸 필드만 부분적으로 수정합니다.
Q. 웹 API가 SOAP에서 REST로 전환된 이유와 각각의 장단점을 설명해주세요.
SOAP는 엄격한 XML 기반 프로토콜입니다. 표준화된 오류(Fault) 메커니즘 등 갖춰진 것은 많았지만, 그만큼 구조가 복잡하고 메시지 포맷이 무거워서 처리 오버헤드가 컸습니다. 웹이 폭발적으로 성장하면서 더 가벼운 통신 방식이 필요해졌고, 그 대안으로 REST가 자리 잡게 됐습니다.
REST의 장점은 이렇습니다. HTTP 프로토콜의 특성을 그대로 활용하니까 별도의 프로토콜이 필요 없고, JSON 같은 가벼운 포맷을 쓰니 처리가 쉽습니다. 캐시 같은 웹의 기존 인프라를 그대로 쓸 수 있고, 인터페이스가 단순해서 배우기도 쉽습니다.
다만 한계도 있습니다. 표준화된 에러 처리 방식이 없고, 트랜잭션이나 보안처럼 SOAP가 제공하던 엔터프라이즈급 기능을 구현하기 어렵습니다. 그리고 현실적으로 REST의 제약조건을 전부 완벽하게 만족시키는 API를 만들기도 쉽지 않습니다.
3. 데이터베이스
Q. 관계형 데이터베이스에서 정규화는 왜 하고, 반정규화는 왜 하나요?
정규화는 하나의 데이터를 여러 테이블에 중복 없이 나눠 저장하는 설계 방식입니다. 데이터가 여기저기 중복되어 있으면 수정할 때 여러 곳을 동시에 고쳐야 하고, 하나라도 빠뜨리면 데이터가 불일치하게 됩니다. 이런 이상 현상(Anomaly), 즉 삽입 이상·갱신 이상·삭제 이상을 막고 저장 공간도 절약하기 위해 정규화를 합니다.
다만 대가가 있습니다. 테이블이 여러 개로 나뉘니까 조회할 때 JOIN이 늘어나고, JOIN이 많아질수록 조회 성능은 떨어질 수 있습니다. 결국 정규화는 데이터 일관성과 조회 성능 사이의 트레이드오프입니다.
반정규화는 이 트레이드오프에서 조회 성능을 우선해야 할 때 선택합니다. 조회 빈도가 압도적으로 높고 JOIN 비용이 실제로 부담이 되는 상황에서, 의도적으로 일부 중복을 허용하는 것입니다. 통계성 데이터처럼 자주 바뀌지 않으면서 조회만 반복되는 데이터에 특히 적합합니다.
꼬리. 정규화에 여러 단계가 있는데, 1~3단계를 간단히 설명해주세요.
제1정규형은 각 컬럼이 원자값만 갖도록, 즉 한 셀에 여러 값을 넣지 않도록 분리하는 것입니다. 제2정규형은 1정규형을 만족하면서, 복합키의 일부에만 종속되는 컬럼을 별도 테이블로 분리하는 것입니다. 부분 함수 종속을 제거한다고 표현합니다. 제3정규형은 2정규형을 만족하면서, 기본키가 아닌 일반 컬럼이 또 다른 일반 컬럼에 종속되는 경우를 제거하는 것입니다. 이건 이행 함수 종속 제거라고 부릅니다.
Q. 인덱스의 역할은 무엇이고, 왜 Hash Table 대신 주로 B-Tree/B+Tree 구조를 사용할까요?
인덱스는 테이블의 조회 속도를 높이기 위해, 특정 컬럼의 데이터를 정렬해서 따로 저장해두는 자료구조입니다. 전체 테이블을 다 뒤지는 대신 인덱스로 위치를 빠르게 찾아가는 것입니다.
그럼 왜 Hash Table을 안 쓰느냐. Hash Table은 단일 값 검색에서는 O(1)이라는 압도적인 속도를 내지만, 해시 함수의 특성상 데이터가 정렬되어 있지 않습니다. 그래서 BETWEEN이나 부등호를 쓰는 범위 검색, 그리고 ORDER BY 정렬 쿼리에서는 이점을 전혀 살릴 수 없습니다. 반면 B-Tree는 데이터가 항상 정렬된 상태를 유지하면서 O(log N)의 탐색 시간을 보장하기 때문에, 범위 조회와 정렬까지 처리하기에 최적의 구조라서 주로 사용됩니다.
꼬리. 그렇다면 인덱스는 테이블에 많을수록 좋을까요?
아닙니다. 인덱스도 정렬 상태를 유지해야 하기 때문에, 쓰기 작업(INSERT/UPDATE/DELETE)이 일어날 때마다 인덱스 자료구조도 실시간으로 재정렬·재배치되어야 합니다. 인덱스가 많을수록 쓰기 성능이 급격히 떨어지고 디스크 공간도 추가로 차지합니다. 그래서 카디널리티가 높은 핵심 컬럼 위주로 선별해서 걸어야 합니다.
Q. 낙관적 락과 비관적 락의 차이를 설명해주세요.
둘 다 동시성 문제를 해결하기 위한 메커니즘인데, 핵심 차이는 데이터베이스의 실제 락을 쓰느냐 안 쓰느냐입니다. 이름 그대로 이해하면 쉽습니다.
낙관적 락은 "대부분의 트랜잭션은 충돌하지 않을 것"이라고 낙관하는 방식입니다. DB에 락을 걸지 않는 대신 엔티티에 @Version 필드를 두고, 수정하려는 시점의 버전이 처음 조회했을 때와 다르면 예외를 발생시킵니다. 충돌이 드물고 DB 커넥션을 오래 붙잡으면 안 되는 대용량 트래픽 환경에 적합합니다.
비관적 락은 반대로 "충돌이 자주 일어날 것"이라고 비관하고, DB가 제공하는 실제 Row 레벨 락을 거는 방식입니다. 락이 걸려 있는 동안 다른 트랜잭션은 풀릴 때까지 대기해야 합니다. 데이터 정합성이 무엇보다 중요하고 충돌이 빈번한 상황에 적합합니다.
4. 자바
Q. ==와 equals()의 차이점, 그리고 hashCode()와의 관계를 설명해주세요.
==는 두 객체가 가리키는 참조, 즉 메모리 주소가 같은지를 비교하는 동일성(identity) 비교입니다. 반면 equals()는 객체가 담고 있는 값이 같은지를 비교하는 동등성(equality) 비교입니다. 다만 Object의 기본 equals()는 ==와 똑같이 동작하기 때문에, 값 비교가 필요하면 재정의해야 합니다.
hashCode()는 HashMap이나 HashSet 같은 해시 기반 자료구조에서 객체를 다룰 때 쓰이는 정수 값을 반환합니다. 여기서 중요한 규약이 하나 있는데, equals()가 true인 두 객체는 반드시 같은 hashCode()를 반환해야 한다는 것입니다. 그래서 equals()를 재정의하면 hashCode()도 반드시 함께 재정의해야 합니다. 안 그러면 해시 기반 컬렉션에서 값이 같은 객체를 서로 다른 객체로 취급하는 문제가 생깁니다.
Q. String이 불변(immutable)으로 설계된 이유를 설명해주세요.
핵심은 메모리 효율, 성능, 그리고 안전성입니다.
먼저 String Pool 공유가 가능해집니다. 값이 절대 변하지 않으니까 같은 문자열 리터럴을 여러 참조가 안심하고 공유할 수 있고, 그만큼 메모리가 절약됩니다. 값이 바뀔 수 있다면 이런 공유 자체가 불가능합니다.
성능 면에서는 해시코드 캐싱이 가능합니다. 값이 안 변하니 해시코드를 처음 한 번만 계산해서 저장해두면 되고, 그래서 String이 HashMap의 키로 그렇게 많이 쓰이는 것입니다.
또 상태가 변하지 않으므로 별도의 동기화 없이 여러 스레드에서 안전하게 공유할 수 있고, 보안 관점에서도 파일 경로나 URL, 클래스명 같은 값이 전달된 뒤 몰래 바뀌는 일을 원천 차단할 수 있습니다.
5. 스프링
Q. 프레임워크와 라이브러리의 핵심적인 차이점을 설명해주세요.
핵심은 제어의 흐름에 대한 주도권이 누구에게 있느냐, 즉 제어의 역전(IoC)의 유무입니다.
라이브러리는 개발자가 주도권을 쥐고 필요한 시점에 직접 호출해서 쓰는 도구입니다. 예를 들어 코딩 테스트에서 쓰는 StringBuilder처럼, 내가 원할 때 가져다 쓰면 됩니다. 반면 프레임워크는 프로그램의 제어 흐름과 생명주기를 자기가 쥐고 있습니다. 개발자가 작성한 코드는 프레임워크에 등록되고, 적절한 시점에 프레임워크에 의해 "호출당하는" 입장이 됩니다. 그만큼 프레임워크가 정한 설계와 규칙을 따라야 하는 제약도 생깁니다.
꼬리. Spring에서 이 제어권은 구체적으로 어떻게 구현되어 운영되나요?
ApplicationContext 같은 IoC 컨테이너가 핵심 객체인 빈(Bean)들의 생성, 의존성 주입, 생명주기를 직접 관리합니다. IoC가 방법론이라면, 이를 구현하는 대표적인 수단이 DI(의존성 주입)입니다.
DI에는 생성자 주입, 필드 주입, Setter 주입 세 가지 방식이 있는데, 실무에서는 생성자 주입을 기본으로 씁니다. 필드 주입은 객체가 고정되어 테스트할 때 mock 객체를 끼워 넣을 수 없지만, 생성자 주입은 파라미터에 인터페이스를 받는 형태라 원하는 빈이든 mock이든 자유롭게 주입할 수 있기 때문입니다. 또 final로 선언할 수 있어 불변성이 보장되고 그만큼 안정성도 챙길 수 있습니다.
Q. 스프링에서 AOP가 필요한 이유와, 이를 활용한 개발 사례를 설명해주세요.
예를 들어 모든 비즈니스 메서드의 실행 시간을 측정해야 한다고 해보겠습니다. AOP 없이 하면 메서드마다 시간 측정 코드를 앞뒤로 붙여야 하는데, 그러면 세 가지 문제가 생깁니다. 같은 코드가 모든 메서드에 중복되고, 핵심 로직과 부가 기능이 뒤섞여 가독성이 떨어지고, 측정 로직을 바꾸려면 모든 메서드를 고쳐야 합니다.
AOP는 이런 공통 관심사를 핵심 로직에서 분리해 한 곳에 모듈화하는 기술입니다. @Aspect 클래스를 하나 만들고 @Around("execution(* com.example.service..*(..))")처럼 선언하면, 서비스 계층의 모든 메서드에 시간 측정 로직이 한 번에 적용됩니다. 핵심 로직과 부가 기능이 깔끔하게 분리되고, 중복이 사라지고, 수정도 한 곳에서 끝납니다.
실무에서는 주로 이런 곳에 씁니다. 메서드 실행 시간 모니터링으로 성능 병목을 찾거나, 메서드 실행 전에 사용자 권한이나 토큰 유효성을 검사하는 보안 처리, 그리고 파라미터·반환값·예외를 일관된 형식으로 남기는 로깅입니다.
Q. 스프링 MVC의 요청 처리 흐름을 @Controller와 @RestController의 차이를 중심으로 설명해주세요.
공통 흐름은 같습니다. 클라이언트 요청이 들어오면 DispatcherServlet이 가장 먼저 받고, HandlerMapping으로 요청에 맞는 컨트롤러를 찾아 위임합니다. 차이는 컨트롤러가 무엇을 반환하고, 그 반환값이 어떻게 처리되느냐입니다.
@Controller는 뷰를 반환하는 것이 기본 목적입니다. 컨트롤러가 View 이름을 반환하면 ViewResolver가 실제 View로 변환하고, View가 Model 데이터를 이용해 HTML을 생성해서 응답합니다. Content-Type은 text/html이고, 서버 사이드 렌더링으로 웹 페이지를 제공할 때 사용합니다.
@RestController는 @Controller에 @ResponseBody가 합쳐진 형태로, 데이터 자체를 반환하는 것이 목적입니다. 컨트롤러가 객체를 반환하면 HttpMessageConverter(기본적으로 Jackson)가 JSON으로 변환해서 응답합니다. Content-Type은 application/json이고, REST API 개발이나 클라이언트-서버 분리 구조, 마이크로서비스 간 통신에 사용합니다.
Q. 자바 기준으로 백엔드 웹 서버가 구동되는 과정을 간략하게 설명해주세요. (JVM, Tomcat 등)
우선 자바 코드는 javac 컴파일러를 통해 .class 바이트코드로 변환되고, JVM이 이를 실행하면서 기계어로 해석됩니다. 스프링 부트 기준으로 서버가 뜨는 과정을 순서대로 말씀드리면 이렇습니다.
먼저 java -jar 명령으로 빌드된 파일을 실행하면 JVM이 구동되고 main 메서드가 호출됩니다. 이때 @SpringBootApplication의 SpringApplication.run()이 실행되면서 본격적인 구동이 시작됩니다.
다음으로 OS 환경 변수와 application.yml의 설정 값을 읽어 Environment 객체에 담고, 로깅 시스템이 초기화됩니다. 그리고 내장 톰캣(WAS)을 생성해 구동하는데, 덕분에 외부에 톰캣을 따로 설치할 필요 없이 설정된 포트로 HTTP 요청을 받을 준비가 됩니다.
이어서 IoC 컨테이너가 만들어집니다. 컴포넌트 스캔으로 어노테이션이 붙은 클래스들을 찾아 스프링 빈으로 생성하고, 생성자 주입 등으로 의존 관계를 연결합니다. 이 과정에서 HikariCP 같은 커넥션 풀이 DB와 연결을 맺고, JPA의 EntityManagerFactory가 테이블 스키마를 검증하거나 생성합니다.
모든 초기화가 끝나면 포트 바인딩 완료 로그와 함께 톰캣의 스레드 풀이 활성화되어, 실시간 요청을 받을 수 있는 대기 상태가 됩니다.
Q. Web Server와 WAS의 차이는 무엇이고, 스프링 부트의 내장 톰캣은 둘 중 어디에 해당하나요?
웹 서버는 HTML, CSS, 이미지 같은 정적 컨텐츠 처리에 특화된 서버입니다. 대표적으로 Nginx와 Apache HTTP Server가 있습니다. WAS는 비즈니스 로직 실행과 DB 연동 같은 동적 컨텐츠 처리에 특화된 서버로, Tomcat, JBoss, WebLogic이 여기에 해당합니다.
그래서 톰캣은 WAS입니다. 예전에는 톰캣을 서버에 따로 설치하고 .war 파일을 빌드해서 올리는 방식이었는데, 스프링 부트는 톰캣을 내장하고 있어서 JAR 파일 하나만 있으면 별도 설치나 설정 없이 명령어 한 줄로 웹 서비스를 띄울 수 있습니다. 배포 편의성에서 큰 차이가 납니다.
Q. 멀티스레드 비동기 환경(@Async)에서 MDC나 SecurityContext 정보가 소실되는 원인은 무엇이고, 어떻게 해결할 수 있나요?
MDC와 SecurityContext는 내부적으로 ThreadLocal에 저장됩니다. ThreadLocal은 이름 그대로 현재 동작 중인 스레드에 한정해서 데이터를 보관하는 저장소입니다. 그런데 @Async를 쓰면 작업이 다른 스레드로 넘어가고, 새 스레드는 부모 스레드의 ThreadLocal에 접근할 수 없기 때문에 로그 추적 정보나 로그인 정보가 유실되는 것입니다.
해결하려면 스레드 풀을 설정할 때 TaskDecorator를 커스텀 구현해서, 부모 스레드가 가진 MDC와 SecurityContext 정보를 복사한 뒤 비동기 실행 스레드에 주입해줘야 합니다. SecurityContext의 경우 DelegatingSecurityContextExecutorService를 활용할 수도 있습니다.
꼬리. 그냥 자식 스레드에 데이터를 상속해주는 자바의 InheritableThreadLocal을 쓰면 안 되나요?
문제가 있습니다. 스프링이나 톰캣 같은 WAS 환경은 스레드를 매번 새로 만드는 게 아니라 스레드 풀에서 재사용합니다. 그래서 반환된 스레드에 과거의 컨텍스트 정보가 그대로 남아 메모리 누수가 생기거나, 심하면 다른 사용자의 정보가 노출되는 중대한 보안 사고로 이어질 수 있습니다.
6. JPA
Q. JPA를 사용하셨는데, 영속성 컨텍스트가 무엇인지 설명해주세요.
영속성 컨텍스트는 "엔티티를 영구 저장하는 환경"이라는 의미를 가진 논리적인 개념입니다. 애플리케이션과 데이터베이스 사이에서 객체를 관리해주는, 일종의 가상 데이터베이스 역할을 한다고 보시면 됩니다.
꼬리. 영속성 컨텍스트를 쓰면 어떤 장점이 있나요?
크게 다섯 가지를 말씀드릴 수 있습니다.
첫째, 1차 캐시입니다. 내부에 Map 형태의 캐시를 두고 있어서, 같은 트랜잭션 안에서 같은 ID로 다시 조회하면 DB를 거치지 않고 메모리에서 바로 반환합니다. 둘째, 이 1차 캐시 덕분에 같은 트랜잭션 내에서는 몇 번을 조회해도 같은 객체임이 보장되는 동일성 보장이 됩니다. 셋째, 쓰기 지연입니다. 저장이나 수정 시 쿼리를 즉시 보내지 않고 내부 저장소에 모아뒀다가 커밋 시점에 한 번에 전송해서 네트워크 오버헤드를 줄입니다. 넷째, 변경 감지(Dirty Checking)입니다. 조회 시점의 스냅샷을 보관하고 있다가 커밋 시점에 현재 상태와 비교해서, 바뀐 부분이 있으면 UPDATE 쿼리를 자동으로 만들어 실행합니다. 다섯째, 연관된 객체를 실제로 사용하는 시점에 조회하는 지연 로딩을 지원합니다.
Q. 로딩에는 지연 로딩과 즉시 로딩이 있는데, 두 가지의 차이는 무엇인가요?
지연 로딩(LAZY)은 연관된 엔티티를 실제로 사용할 때 쿼리를 날려 조회하는 방식이고, 즉시 로딩(EAGER)은 엔티티를 조회할 때 연관 엔티티까지 무조건 Join으로 한 번에 가져오는 방식입니다.
즉시 로딩은 예상치 못한 SQL Join을 발생시켜 성능을 떨어뜨리고, 개발자가 의도하지 않은 N+1 문제로 이어지기 쉽습니다. 그래서 기본은 전부 지연 로딩으로 설정해두고, 연관 엔티티가 함께 필요한 특정 로직에서만 Fetch Join이나 @EntityGraph로 그때그때 한 번에 조회하는 것이 좋습니다.
Q. JPA의 N+1 문제가 무엇인지 설명해주세요.
특정 엔티티를 조회하는 쿼리를 1번 실행했는데, 그 결과로 나온 엔티티들의 연관 객체를 조회하기 위한 추가 쿼리가 조회된 데이터 개수, 즉 N만큼 더 발생하는 성능 저하 현상입니다.
해결 방법은 크게 세 가지입니다. 첫째, JPQL에 join fetch 키워드를 사용해 연관 엔티티를 한 번의 Join 쿼리로 함께 가져오는 Fetch Join입니다. 둘째, Spring Data JPA가 제공하는 @EntityGraph를 쿼리 메서드 위에 선언하는 방법입니다. 셋째, 지정한 사이즈만큼 IN 절로 묶어서 한 번에 조회하는 Batch Size 설정입니다.
꼬리. Fetch Join과 일반 Join의 차이는 무엇인가요?
일반 Join은 주체가 되는 엔티티만 조회합니다. 연관 엔티티는 프록시 객체로 남아 있어서, 실제로 데이터에 접근하는 순간 추가 쿼리가 나갑니다. 반면 Fetch Join은 연관 엔티티의 필드까지 전부 포함해서 함께 가져오기 때문에, 프록시가 아닌 실제 엔티티가 영속성 컨텍스트의 1차 캐시에 바로 만들어지고 추가 쿼리 없이 즉시 접근할 수 있습니다. 한 줄로 요약하면, 일반 Join은 연관 엔티티를 영속화하지 않지만 Fetch Join은 연관 엔티티까지 즉시 영속화합니다.
7. 인증 · 캐시 아키텍처
Q. 세션 기반 인증과 토큰 기반 인증의 차이를 서버 확장성 관점에서 설명해주세요.
세션 기반 인증은 서버 메모리나 세션 저장소에 로그인 상태를 유지하는 Stateful 방식입니다. 서버가 상태를 들고 있기 때문에, 서버 대수가 늘어나면 서버 간 세션을 동기화하거나 Redis 같은 공통 세션 저장소를 따로 구축해야 해서 확장에 비용이 듭니다.
반면 토큰(JWT) 기반 인증은 자체 검증이 가능한 서명이 담긴 토큰을 클라이언트가 들고 다니는 Stateless 방식입니다. 서버는 상태를 저장할 필요 없이 토큰의 서명만 검증하면 되기 때문에, 어떤 서버가 요청을 받아도 인증이 가능합니다. 그래서 서버를 수평으로 확장하기에 훨씬 유리합니다.
꼬리 1. JWT의 Payload에 비밀번호나 주민번호 같은 민감 정보를 담아도 안전할까요?
안전하지 않습니다. JWT의 Payload는 암호화된 것이 아니라 Base64로 단순 인코딩된 것이라, 누구나 디코딩해서 내용을 볼 수 있습니다.
꼬리 2. Stateless 방식의 단점은 무엇이고, 탈취당한 Access Token을 즉시 무효화하려면 어떻게 해야 하나요?
토큰이 한번 발급되면 서버가 제어권을 잃는다는 것이 단점입니다. 탈취당해도 서버가 임의로 조기 만료시킬 수 없습니다. 이를 해결하려면 무효화할 토큰을 블랙리스트로 Redis 같은 별도 캐시에 등록해두고, 요청마다 블랙리스트를 확인하는 검증 절차를 추가해야 합니다.
Q. 다중 서버(분산) 환경에서 로컬 캐시와 분산 캐시의 장단점을 설명해주세요.
로컬 캐시는 각 WAS의 메모리, 즉 JVM Heap 안에 캐시 데이터를 보관하는 방식입니다. 네트워크 통신이 아예 없으니 속도가 매우 빠르고 구현도 쉽습니다. 하지만 서버가 여러 대인 환경에서는 서버마다 캐시 내용이 달라지는 정합성 문제가 발생합니다.
분산 캐시는 Redis처럼 외부에 독립된 공통 캐시 저장소를 두는 방식입니다. 네트워크 I/O 비용이 있어서 로컬 캐시보다는 느리지만, 모든 서버가 같은 최신 캐시를 바라보기 때문에 정합성이 보장되고, 캐시 메모리를 WAS와 분리해서 관리할 수 있다는 장점이 있습니다.
꼬리 1. 다중 WAS 환경에서 성능 때문에 로컬 캐시를 꼭 쓰고 싶다면, 정합성 문제를 해결할 아이디어가 있을까요?
Redis의 Pub/Sub을 활용하는 방법이 있습니다. 데이터가 변경될 때 각 서버에 로컬 캐시를 만료(Evict)시키라는 이벤트를 전파하는 방식입니다.
꼬리 2. Redis를 분산 캐시로 쓸 때, 싱글 스레드 특성 때문에 주의해야 할 점은 무엇인가요?
KEYS나 FLUSHALL처럼 오래 걸리는 명령어를 실행하면, 싱글 스레드 특성상 뒤이은 모든 요청이 대기 상태에 빠져서 서비스 전체가 마비될 수 있습니다.
마치며
직접 소리 내어 답해보니 "아는 것"과 "설명할 수 있는 것"의 차이가 확실히 드러났습니다. 특히 꼬리 질문에서 막히는 지점이 곧 이해가 얕은 지점이었고, 스터디가 끝난 뒤 모범 답안을 함께 정리하면서 그 구멍들을 메울 수 있었습니다. 다음 회차에서도 이력서 기반 질문과 CS 질문을 병행할 예정입니다.
'스터디' 카테고리의 다른 글
| 프로세스와 스레드 (0) | 2026.07.25 |
|---|---|
| HTTP 프로토콜의 발전 (0) | 2026.07.19 |
| 네트워크의 기초 (0) | 2026.07.19 |
| 디자인 패턴 (1) | 2026.07.12 |
