단순히 ORM을 ‘사용’한 경험이 아니라, ORM ↔ 트랜잭션의 동작 흐름을 이해하고 이를 바탕으로 성능 튜닝과 장애 포인트 식별/대응까지 할 수 있는지를 검증한다.”
핵심 평가 축
1. 엔티티 라이프사이클 & 영속성 컨텍스트
1차 캐시, 지연쓰기(write-behind), flush/clear 타이밍, dirty checking 이해
2. 연관관계 매핑 & 로딩 전략
LAZY/EAGER, fetch join, 그래프 엔트리 포인트, N+1 진단/해결 능력
3. 트랜잭션 경계 & 격리/전파
@Transactional 경계, readOnly, 격리수준(READ COMMITTED 등), 전파(PROPAGATION_REQUIRED/REQUIRES_NEW), 타임아웃
4. 락킹 & 동시성 제어
낙관적/비관적 락, 버전 필드, Lost Update/Phantom Read 방지 전략
5. 쿼리 최적화 & 배치
JPQL/Criteria/Native 적절한 선택, 배치 insert/update, fetch size, 인덱스 활용과 계획 확인
6. 리소스 & 커넥션 풀링
커넥션/스레드 풀 고갈 징후, 트랜잭션 장기화 원인 진단
인터뷰 질문(예시)
“flush는 언제 일어나며, 커밋과 어떤 관계가 있나요? 수동 flush가 필요한 사례는?”
“N+1이 언제 발생했었고, 어떻게 재현·진단·해결했나요? (fetch join/EntityGraph/DTO 변환/캐시 등)”
“@Transactional(readOnly=true)의 내부 동작 이점과 주의사항은?”
“낙관적 락 실패가 빈번할 때 어떤 지표를 보고 어떤 대안(재시도, 분할, 큐잉, 비관적 락)을 선택했나요?”
“대량 저장 시 Hibernate batch insert 설정과 주의할 점은?”
실전 시나리오(미션 형태)
1. N+1 재현 및 제거
조건: Post–Comment(1:N, LAZY), 최근 Post 100개 조회 화면이 느림
요구: 원인 설명 → 실행 SQL 확인 → fetch join 또는 전용 조회 DTO로 해결 → 성능 전/후 수치 보고
2. 동시성 업데이트 충돌
조건: 재고 감소 API 동시 호출
요구: Lost Update 방지 설계(낙관적 락+재시도 / 비관적 락 / 분산락) 비교 및 선택 근거
3. 배치 적재 최적화
조건: 50만 건 CSV 적재 지연
요구: jdbc.batch_size, StatelessSession/벌크, flush/clear 주기, 인덱스 일시 비활성화 전략 설명 및 측정
튜닝 레버(스프링 JPA/Hibernate 중심)
로딩: 기본 LAZY, 화면 전용 조회는 DTO 프로젝션/네이티브/조회 전용 레포 분리
쿼리: fetch join(필요한 범위만), 페이징+fetch join 주의(컬렉션 조인 금지, 서브쿼리/두 단계 조회)
배치: hibernate.jdbc.batch_size, order_inserts/updates, flush/clear 주기화
캐시: 1차 캐시 적절 활용, 2차 캐시/쿼리 캐시는 신중 도입(패턴 고정·변동성 낮을 때)
트랜잭션: readOnly, 타임아웃, 격리수준 설정; 경계 최소화(서비스 레벨)
락: @Version(낙관적), 필요 시 PESSIMISTIC_WRITE/READ + 타임아웃
풀 설정: HikariCP maxPoolSize, connectionTimeout, slow query log, 메트릭 관찰
대표 장애 포인트 & 탐지법
N+1: 호출 수 급증, APM에서 동일 쿼리 반복/카디널리티 급등
장수 트랜잭션: 커넥션 점유 증가, 락 대기, GC/스레드 덤프에 대기열
잘못된 EAGER: 불필요 조인 폭증, 직렬화 시 순환참조 이슈
잘못된 전파: REQUIRES_NEW 남용으로 커밋 순서/일관성 붕괴
락 경합/데드락: DB 대기 이벤트/Deadlock 로그, 재시도 로직 부재
대량 적재 OOM: 영속성 컨텍스트 누적(Flush/clear 미실시)
코드 스니펫(짧게)
// N+1 방지: 조회 전용 DTO
@Query("""
select new com.example.PostView(p.id, p.title, a.name)
from Post p
join p.author a
where p.createdAt >= :since
""")
List<PostView> findRecent(@Param("since") LocalDateTime since);
// 배치 저장: flush/clear 주기화
@Transactional
public void bulkSave(List<Entity> items) {
for (int i = 0; i < items.size(); i++) {
em.persist(items.get(i));
if (i % 1000 == 0) { em.flush(); em.clear(); }
}
}
// 낙관적 락: 재시도 템플릿
@Retryable(value = OptimisticLockException.class, maxAttempts = 3, backoff = @Backoff(delay = 50))
@Transactional
public void decreaseStock(Long id, int qty) { ... }
평가 루브릭(5점 척도)
5: 위 모든 축을 사례/수치와 함께 설명하고, 진단–해결–재발방지 사이클 제시
4: 주요 축을 이해하고 실무 튜닝 경험 다수 보유, 일부 미세 조정 미흡
3: 개념 이해는 충분하나 재현/수치 기반 검증 부족
2: ORM 사용 위주, 트랜잭션·락·배치 개념 단편적
1: 기본 개념 혼재, EAGER 남발·서비스 외부 트랜잭션 등 위험 패턴 다수
'Spring & Backend' 카테고리의 다른 글
| Redis와 직렬화 (0) | 2025.09.22 |
|---|---|
| 톰캣에 대해서 설명해주세요. (0) | 2025.09.22 |
| RequestBody VS ModelAttribute의 차이점을 말해주세요. (0) | 2025.09.19 |
| “모놀리식 → 멀티모듈(여전히 한 프로세스)”생기는 문제들 (3) | 2025.09.18 |
| 스택을 활용하여 브라우저의 뒤로가기/앞으로가기 기능을 구현하는 방법을 설명해주세요. (0) | 2025.09.18 |
