[Java&Spring 면접 준비] Day 2 - Java 심화 및 JPA

seri·2025년 8월 3일
0

1. Java에서 Optional 클래스가 등장한 배경과 적절한 사용법에 대해 알려주세요.

Optional은 Java 8에서 도입된 클래스이고, 가장 큰 목적은 NullPointerException을 줄이고 null 처리를 더 명시적*으로 하기 위해서입니다.
예전에는 null인지 직접 조건문으로 확인해야 했는데, Optional을 사용하면 값이 없을 수도 있다는 걸 타입 레벨에서 표현할 수 있어서 의도를 더 잘 드러낼 수 있어요.

저는 주로 메서드 반환값에 Optional를 사용하는데요, 예를 들어 DB 조회 시 값이 없을 수도 있는 경우에 Optional.empty()를 반환하면, 호출 측에서 orElse()나 ifPresent()로 안전하게 처리할 수 있어서 좋았습니다.
다만, 파라미터나 필드로 사용하는 건 코드 복잡성이 커질 수 있어서 지양하고 있습니다.


2. JPA에서 N+1 문제는 어떻게 발생하며, 이를 해결하기 위한 전략은 무엇인가요?

N+1 문제는 지연 로딩(LAZY) 설정된 연관 엔티티를 반복해서 접근할 때 발생합니다.
예를 들어 게시글 목록을 가져온 다음, 각 게시글마다 작성자 이름을 출력하려 하면, 처음에 게시글 1번만 조회된 후에 각 작성자의 정보를 가져오기 위해 추가 쿼리가 N번 실행돼서 총 N+1 쿼리가 됩니다.
저는 연관 객체를 Entity로 가져오지 않고, 필요한 필드만 JPQL 또는 QueryDSL로 직접 DTO로 매핑해서 가져와 해결했습니다. 이 방법은 성능 최적화에는 유리하지만, 쿼리 복잡도가 증가합니다.

꼬리질문:

→ DTO로 조회할 때 쿼리가 너무 많아지면 어떻게 관리하시나요?

실무에서 화면이 많아지고, 그에 따른 DTO 기반 쿼리도 많아지면 쿼리 관리 이슈가 생깁니다.

  • QueryDSL을 사용할 경우:
    Repository 레이어에서 복잡한 쿼리는 Custom Repository로 분리해서 관리했습니다.
    예: UserRepositoryCustom, UserRepositoryImpl
    이렇게 하면 repository 인터페이스와 구현을 명확히 구분할 수 있어서 유지보수가 쉬워집니다.

  • JPQL이나 네이티브 쿼리를 사용하는 경우:
    @NamedQuery 보다는 JPA의 @Query를 사용하고,
    쿼리 복잡도가 높아지면 .sql 파일로 분리하거나, 쿼리를 별도 클래스로 추출하는 방식을 고려했습니다.

추가로, 화면 단위로 DTO 쿼리를 분리하거나, 쿼리 작성 시 공통 조건은 메서드 분리로 재사용하여 중복을 줄였습니다.

→ 그럼 즉시로딩을 사용 했을때는 N+1이 발생하지 않나요?

EAGER 로딩을 사용해도 N+1 문제는 발생할 수 있습니다.
왜냐하면 EAGER는 단지 "즉시" 로딩한다는 의미이지, "어떻게(fetch 방식)" 로딩하는지는 별도로 설정되지 않으면 기본은 select 쿼리입니다.
예를 들어, EAGER 연관 필드가 있는 리스트를 조회하면, 첫 쿼리 1번 + 연관 객체 조회 N번 쿼리가 발생할 수 있습니다.

이를 해결하기 위해서는 fetch join을 사용하거나 EntityGraph를 활용해 함께 로딩하거나 DTO 직접 조회(QueryDSL)로 쿼리를 명시적으로 작성하는 방법을 사용했습니다.


3. 실무에서 FetchType.LAZY를 사용할 때 주의할 점은 무엇인가요?

LAZY는 성능 최적화를 위해 기본적으로 많이 사용하는데요, 주의할 점은 영속성 컨텍스트가 닫힌 상태에서 지연 로딩 대상에 접근하면 LazyInitializationException이 발생합니다.

실제로 저도 처음에는 컨트롤러에서 Entity를 그대로 반환해서 문제가 생긴 적이 있었습니다. 그 후에는 Service 단에서 DTO로 변환한 후에만 컨트롤러로 전달하는 구조로 바꿨습니다.
특히, 양방향 연관관계에서는 순환 참조 위험도 있어서, DTO 분리 전략을 사용해 필요한 필드만 전달하도록 구성했습니다.

꼬리질문:

→ Open Session in View 패턴에 대해서는 어떻게 생각하시나요? 이 패턴을 사용하면 컨트롤러 단에서 LazyInitializationException을 피할 수 있지만, 어떤 단점들이 있을까요?

OSIV 패턴은 컨트롤러까지 영속성 컨텍스트를 열어두기 때문에,
Lazy 로딩으로 인한 LazyInitializationException을 방지할 수 있는 장점이 있습니다.
하지만 트랜잭션 범위가 비즈니스 로직을 넘어 컨트롤러까지 확장되기 때문에, DB 커넥션 점유 시간이 길어지고, 성능 저하나 커넥션 부족 문제가 발생할 수 있습니다.

서비스 계층을 벗어난 지점에서 엔티티를 로딩하는 건 도메인 계층 설계 원칙에 어긋날 수 있습니다.
그래서 실무에서는 OSIV를 비활성화하고, 필요한 데이터는 서비스 계층에서 미리 fetch join 또는 DTO로 모두 가져온 뒤 컨트롤러로 넘기는 구조로 설계했습니다.


4. Java 스트림 API와 루프(for-each)의 성능 차이나 장단점을 설명해 주세요.

Stream API는 선언형으로 데이터를 처리할 수 있어서 코드가 간결하고 가독성이 좋다는 게 가장 큰 장점입니다.
예를 들어 필터링 → 정렬 → 매핑 같은 연산을 체이닝으로 처리할 수 있고, 병렬 스트림을 사용하면 멀티코어 환경에서도 효율적입니다.

반면 for-each 루프는 좀 더 직관적이고 디버깅이 쉬운 구조라서, 단순한 반복문에는 오히려 더 좋을 때도 있습니다.
실무에서 복잡한 데이터 가공이 필요한 경우엔 Stream을, 로직이 단순하거나 가독성이 중요한 경우엔 for-each를 쓰는 편입니다.


5. Java에서 자주 발생하는 Deadlock 문제를 발견하고 해결하기 위한 전략과 도구는 무엇이 있나요?

Deadlock은 주로 여러 스레드가 서로 자원을 점유하고, 상대방 자원을 기다리면서 락이 풀리지 않는 상황에서 발생합니다.
해결하려면 먼저 락 획득 순서를 고정하거나, tryLock() 같은 방식으로 시간 제한을 설정해서 대기하지 않게 하는 게 중요하다고 생각합니다.

문제 상황을 재현하거나 의심될 때는 jstack으로 스레드 덤프를 분석
하거나 VisualVM, jconsole 같은 도구를 써서 어느 스레드가 멈춰 있는지 확인했습니다.

꼬리질문:

→ 락 획득 순서를 고정한다는건 어떤 방식으로 구현하셨었나요?

예를 들어 두 개 이상의 자원(또는 DB row)을 동시에 락을 걸어야 하는 상황이 있으면,
항상 동일한 순서(예: ID 오름차순)로 락을 획득하도록 강제했습니다.

구현 방식은 다음과 같습니다.
1. 락을 획득할 대상들을 미리 정렬 (예: List 정렬)
2. 그 순서대로 순차적으로 락을 획득 (예: pessimistic lock 또는 메모리 락)

이렇게 하면 쓰레드 간 충돌이 발생해도 락 획득 순서가 항상 일치하므로 데드락을 방지할 수 있었습니다.

→ tryLock() 을 사용하는 것의 장단점은 무엇인가요?

  • 장점
    • 락 대기 없이 빠르게 실패할 수 있음
    • 데드락 회피에 유리함
    • 타임아웃 설정으로 유연한 처리 가능
  • 단점
    • 락을 못 잡을 경우 처리가 복잡해질 수 있음 (retry, fallback 등)
    • 락 경쟁이 심할 경우 낙오 많음
    • 병렬성이나 안정성 낮아질 수 있음

6. 스레드 풀 크기는 어떻게 정해야하고, 스레드 풀에서 데드락이나 자원 경합이 발생하면 어떻게 진단하고 해결하나요?

CPU 바운드 작업: 병렬로 실행되는 CPU 연산이 많은 작업
공식: 스레드 수 ≈ CPU 코어 수 + 1
이유: 너무 많은 스레드는 컨텍스트 스위칭 비용만 늘려 성능이 오히려 저하됩니다.

IO 바운드 작업: 네트워크/디스크 IO로 블로킹이 많은 작업.
공식: 스레드 수 ≈ CPU 코어 수 × (1 + (대기 시간 / 처리 시간))

데드락이나 자원 경합이 발생하면 먼저 스레드 수와 큐 길이를 조절해서 병목을 줄이고,
jstack, VisualVM 같은 도구로 스레드 상태나 락 점유 상태를 추적합니다.
또, 스레드 풀에 백프레셔(backpressure)나 타임아웃을 걸어 문제 상황에서도 빠르게 실패하고 회복 가능한 구조 설계하는 것도 중요합니다.

profile
꾸준히 정진하며 나아가기

0개의 댓글