면접 준비 (3)

1. Spring

1.1 Spring 핵심

Spring Framework는 어떤 문제를 해결하기 위해 등장했나요?
  • 객체를 직접 생성하고 연결하면 클래스끼리 강하게 결합되어, 구현체를 바꾸거나 테스트하기 어려웠습니다.
  • Spring은 객체의 생성과 생명주기를 컨테이너가 대신 관리해 결합도를 낮춥니다.
  • 트랜잭션이나 로깅처럼 여러 곳에 반복되는 관심사는 AOP로 분리하고, 특정 기술에 종속되지 않는 순수한 자바 객체(POJO)로 비즈니스 로직을 작성할 수 있게 해줍니다.
IoC(제어의 역전)란 무엇인가요?
  • 객체의 생성과 호출 흐름에 대한 제어권이 개발자 코드에서 프레임워크(컨테이너)로 넘어가는 것을 말합니다.
  • 기존에는 필요한 객체를 직접 new로 만들었다면, Spring에서는 컨테이너가 객체를 만들어 필요한 곳에 넣어줍니다.
DI(의존성 주입)란 무엇이며 왜 필요한가요?
  • 객체가 필요한 의존 객체를 스스로 만들지 않고 외부에서 주입받는 방식입니다.
  • 구체 클래스가 아닌 인터페이스에 의존하게 되어, 구현체를 교체해도 사용하는 쪽 코드를 고치지 않아도 됩니다.
  • 테스트할 때 실제 구현 대신 가짜 객체를 주입할 수 있어 단위 테스트가 쉬워집니다.
BeanFactory와 ApplicationContext의 차이는 무엇인가요?
  • BeanFactory: 빈의 등록과 조회를 담당하는 가장 기본적인 컨테이너로, 빈을 실제로 요청할 때 생성합니다.
  • ApplicationContext: BeanFactory를 상속하면서 국제화, 이벤트 발행, 환경 변수 처리, AOP 연동 같은 부가 기능을 함께 제공합니다.
  • 또한 싱글턴 빈을 애플리케이션 기동 시점에 미리 생성하므로, 설정 오류를 실행 초기에 발견할 수 있습니다. 실무에서는 ApplicationContext를 사용합니다.
Bean을 등록하는 방식에는 어떤 것들이 있나요?
  • @Component 계열 애노테이션을 붙이고 Component Scan으로 자동 등록하는 방식입니다. 직접 작성하는 클래스에 주로 사용합니다.
  • @Configuration 클래스 안에서 @Bean 메서드로 직접 등록하는 방식입니다. 외부 라이브러리 클래스처럼 애노테이션을 붙일 수 없을 때 사용합니다.
  • XML 설정 방식도 있지만 최근에는 거의 쓰이지 않습니다.
Component Scan은 어떻게 동작하나요?
  • 지정한 패키지와 그 하위 패키지를 훑으면서 @Component가 붙은 클래스를 찾아 빈으로 등록합니다.
  • @Service, @Repository, @Controller는 내부적으로 @Component를 포함하고 있어 함께 스캔 대상이 됩니다.
  • Spring Boot에서는 @SpringBootApplication이 붙은 클래스의 패키지를 기준으로 자동 스캔하므로, 빈이 등록되지 않는다면 패키지 위치를 먼저 확인해야 합니다.

1.2 Bean

Bean Scope에는 어떤 것들이 있나요?
  • singleton: 기본값으로, 컨테이너에 하나의 인스턴스만 존재하며 모두가 공유합니다.
  • prototype: 요청할 때마다 새로운 인스턴스를 생성합니다.
  • request, session: 웹 환경에서 각각 HTTP 요청, 세션 단위로 생성됩니다.
싱글턴 Bean을 사용할 때 주의할 점은 무엇인가요?
  • 모든 요청이 같은 인스턴스를 공유하므로, 특정 요청이나 사용자에 종속된 값을 필드에 저장하면 안 됩니다.
  • 여러 스레드가 동시에 접근하기 때문에 가변 상태를 두면 값이 뒤섞이는 동시성 문제가 발생합니다.
  • 따라서 빈은 상태를 갖지 않도록(무상태) 설계하고, 요청별 데이터는 메서드의 파라미터나 지역 변수로 다뤄야 합니다.
Bean의 생명주기는 어떻게 되나요?
  • 객체 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸 전 콜백 순으로 진행됩니다.
  • 초기화 작업은 @PostConstruct, 정리 작업은 @PreDestroy를 사용합니다.
  • 의존관계 주입이 끝난 뒤에 초기화 콜백이 호출되므로, 주입받은 객체를 사용하는 초기화 로직은 생성자가 아니라 초기화 콜백에 두어야 합니다.
  • 참고로 prototype 빈은 생성과 주입까지만 관여하고 소멸은 관리하지 않습니다.
Prototype Bean을 Singleton Bean에 주입하면 어떤 문제가 있나요?
  • 싱글턴 빈이 생성될 때 의존성 주입이 한 번만 일어나므로, 프로토타입 빈도 그때 한 번만 주입됩니다.
  • 결과적으로 매번 새 인스턴스를 기대했는데 계속 같은 인스턴스를 사용하게 됩니다.
  • 사용할 때마다 새 인스턴스가 필요하다면 ObjectProvider를 주입받아 필요한 시점에 조회하는 방식으로 해결합니다.
생성자 주입, 필드 주입, Setter 주입의 차이는 무엇이며 무엇을 권장하나요?
  • 생성자 주입: 객체 생성 시점에 의존성이 모두 주입됩니다. 필드를 final로 선언해 불변으로 만들 수 있고, 의존성이 누락되면 애플리케이션 기동 시점에 바로 실패해 문제를 빨리 발견할 수 있습니다.
  • 필드 주입: 코드가 간결하지만 스프링 컨테이너 없이는 객체를 만들 수 없어 순수 단위 테스트가 어렵고, 의존성이 얼마나 많은지 드러나지 않아 설계가 나빠져도 알아채기 힘듭니다.
  • Setter 주입: 선택적이거나 나중에 변경될 수 있는 의존성에 사용합니다.
  • 일반적으로 생성자 주입이 권장되며, 의존성이 하나뿐이면 @Autowired도 생략할 수 있습니다.
순환 참조는 왜 발생하며 어떻게 해결하나요?
  • A가 B를 의존하고 B가 다시 A를 의존해서 컨테이너가 어느 쪽을 먼저 만들지 결정할 수 없는 상태입니다.
  • 생성자 주입을 사용하면 애플리케이션 기동 시점에 바로 오류가 발생해 문제를 조기에 발견할 수 있습니다. Spring Boot 2.6부터는 순환 참조가 기본적으로 금지되어 있습니다.
  • 임시로 @Lazy를 붙이거나 설정으로 허용할 수는 있지만, 근본적으로는 두 클래스가 공통으로 쓰는 로직을 별도 클래스로 분리하는 등 설계를 고치는 것이 맞습니다.

1.3 Spring Boot

Spring과 Spring Boot의 차이는 무엇인가요?
  • Spring은 설정을 직접 해야 할 것이 많아 프로젝트를 시작하는 데 시간이 오래 걸렸습니다.
  • Spring Boot는 관례에 따른 기본 설정을 자동으로 적용해 주고, 필요한 의존성을 묶은 Starter를 제공하며, 서버를 내장해 별도 WAS 없이 실행할 수 있게 해줍니다.
  • 즉 Spring을 대체하는 것이 아니라, Spring을 더 빠르고 쉽게 쓰도록 도와주는 도구입니다.
Auto Configuration은 어떻게 동작하나요?
  • Spring Boot가 미리 정의해 둔 자동 설정 클래스 목록을 읽어, 현재 프로젝트 상황에 맞는 것만 골라 적용합니다.
  • 판단 기준은 조건 애노테이션입니다. 특정 클래스가 클래스패스에 있는지, 사용자가 직접 등록한 빈이 이미 있는지 등을 확인합니다.
  • 특히 @ConditionalOnMissingBean 덕분에, 개발자가 같은 역할의 빈을 직접 정의하면 자동 설정이 물러나고 사용자 설정이 우선 적용됩니다.
Starter는 무엇인가요?
  • 특정 기능을 쓰는 데 필요한 의존성들을 하나로 묶어둔 패키지입니다.
  • 예를 들어 spring-boot-starter-web을 추가하면 Spring MVC, 내장 톰캣, JSON 처리 라이브러리가 함께 딸려옵니다.
  • 라이브러리 간 버전 호환성도 함께 관리해 주기 때문에 버전 충돌 문제를 크게 줄여줍니다.
Embedded Server를 사용하면 어떤 이점이 있나요?
  • 톰캣 같은 서버가 애플리케이션 안에 포함되어 있어, 별도로 WAS를 설치하고 WAR를 배포할 필요가 없습니다.
  • 실행 가능한 jar 하나로 배포와 실행이 끝나므로 배포 과정이 단순해집니다.
  • 애플리케이션마다 독립된 서버를 갖게 되어, 컨테이너 기반 환경이나 마이크로서비스 구조에 잘 맞습니다.
Profile은 어떻게 활용하나요?
  • 개발, 스테이징, 운영처럼 환경마다 다른 설정을 분리해 관리하는 기능입니다.
  • application-dev.yml처럼 환경별 설정 파일을 두고, spring.profiles.active로 어떤 환경인지 지정합니다.
  • @Profile을 빈에 붙이면 특정 환경에서만 해당 빈이 등록되게 할 수도 있습니다.
Conditional Annotation은 무엇인가요?
  • 특정 조건을 만족할 때만 빈이나 설정을 등록하도록 하는 애노테이션입니다.
  • @ConditionalOnClass(해당 클래스가 있을 때), @ConditionalOnMissingBean(같은 빈이 없을 때), @ConditionalOnProperty(설정값이 특정 값일 때) 등이 있습니다.
  • Auto Configuration이 상황에 맞게 동작할 수 있는 핵심 장치입니다.

1.4 Spring MVC

Spring MVC의 요청 처리 과정을 설명해 주세요.
  • 클라이언트 요청이 들어오면 DispatcherServlet이 가장 먼저 받습니다.
  • HandlerMapping으로 요청 URL에 맞는 컨트롤러를 찾습니다.
  • HandlerAdapter가 해당 컨트롤러를 실행하며, 이 과정에서 Argument Resolver가 요청 데이터를 메서드 파라미터로 변환합니다.
  • 반환값이 @ResponseBody@RestController라면 MessageConverter가 JSON으로 변환해 바로 응답하고, 뷰 이름이라면 ViewResolver가 뷰를 찾아 렌더링합니다.
DispatcherServlet은 어떤 역할을 하나요?
  • 모든 요청을 가장 앞에서 받아 처리 흐름을 조율하는 프론트 컨트롤러입니다.
  • 요청마다 반복되는 공통 처리(핸들러 탐색, 예외 처리, 응답 변환)를 한곳에 모아, 개발자는 각 컨트롤러의 비즈니스 로직에만 집중할 수 있게 해줍니다.
HandlerMapping과 HandlerAdapter는 각각 무엇인가요?
  • HandlerMapping: 요청 URL과 HTTP 메서드를 보고 어떤 핸들러(컨트롤러 메서드)가 처리할지 찾아줍니다.
  • HandlerAdapter: 찾은 핸들러를 실제로 호출하는 역할을 합니다.
  • 핸들러의 형태가 여러 가지일 수 있기 때문에, 어댑터를 통해 DispatcherServlet이 형태에 상관없이 동일한 방식으로 호출할 수 있게 만든 구조입니다.
@RestController에서 객체를 반환하면 어떻게 JSON이 되나요?
  • HttpMessageConverter가 반환된 객체를 응답 본문으로 변환합니다.
  • 클래스패스에 Jackson이 있으면 Jackson 기반 컨버터가 등록되어 객체를 JSON으로 직렬화합니다.
  • 요청의 Accept 헤더와 반환 타입을 보고 적절한 컨버터가 선택되며, 요청 본문을 객체로 바꾸는 @RequestBody도 같은 컨버터가 처리합니다.
Argument Resolver는 무엇인가요?
  • 컨트롤러 메서드의 파라미터를 어떤 값으로 채울지 결정하는 컴포넌트입니다.
  • @RequestParam, @PathVariable, @RequestBody 같은 기본 동작이 모두 각각의 Resolver로 구현되어 있습니다.
  • 직접 구현해서 등록하면, 예를 들어 인증된 사용자 정보를 컨트롤러 파라미터로 바로 주입받는 식으로 반복 코드를 줄일 수 있습니다.

1.5 Filter와 Interceptor

Filter, Interceptor, AOP의 실행 위치는 어떻게 다른가요?
  • Filter: 서블릿 컨테이너 레벨에서 동작하며 DispatcherServlet보다 앞에 있습니다. 요청이 Spring에 들어오기 전과 나간 후를 다룹니다.
  • Interceptor: Spring MVC 레벨에서 DispatcherServlet과 컨트롤러 사이에 위치합니다. 어떤 핸들러가 실행될지 알 수 있습니다.
  • AOP: 스프링 빈의 메서드 호출 시점에 프록시를 통해 동작하며, 메서드의 파라미터와 반환값에 접근할 수 있습니다.
Filter와 Interceptor는 각각 언제 사용하나요?
  • Filter: 요청 자체를 감싸거나 교체해야 하는 경우, 인코딩 설정, 전역 로깅처럼 Spring과 무관한 처리에 적합합니다. Spring Security도 필터 기반으로 동작합니다.
  • Interceptor: 어떤 컨트롤러가 실행되는지 알아야 하거나, 특정 URL 패턴에만 적용하고 싶을 때 적합합니다. 인증·인가 확인, 공통 파라미터 검증 등에 사용합니다.
  • 예외 발생 시에도 후처리가 필요하면 Interceptor의 afterCompletion을 활용할 수 있습니다.

1.6 AOP와 Proxy

AOP는 어떤 문제를 해결하나요?
  • 로깅, 트랜잭션, 성능 측정, 권한 검사처럼 여러 클래스에 반복해서 등장하는 부가 기능(횡단 관심사)을 한곳으로 모읍니다.
  • 비즈니스 로직에서 이런 코드를 걷어내 핵심 로직만 남길 수 있고, 정책이 바뀌어도 한 곳만 수정하면 됩니다.
Join Point, Pointcut, Advice는 각각 무엇인가요?
  • Join Point: 부가 기능을 적용할 수 있는 지점입니다. Spring AOP에서는 메서드 실행 시점을 의미합니다.
  • Pointcut: 그 많은 지점 중 실제로 적용할 대상을 선별하는 표현식입니다.
  • Advice: 선별된 지점에 실제로 적용할 부가 기능 코드입니다. 실행 전후를 모두 감쌀 수 있는 Around가 가장 많이 쓰입니다.
  • Pointcut과 Advice를 묶은 모듈을 Aspect라고 합니다.
JDK Dynamic Proxy와 CGLIB Proxy의 차이는 무엇인가요?
  • JDK Dynamic Proxy: 인터페이스를 구현한 프록시를 만듭니다. 대상 클래스가 반드시 인터페이스를 가지고 있어야 합니다.
  • CGLIB Proxy: 대상 클래스를 상속한 자식 클래스를 만들어 프록시로 사용합니다. 인터페이스가 없어도 되지만, 상속이 불가능한 final 클래스나 final 메서드에는 적용할 수 없습니다.
  • Spring Boot 2.0부터는 기본적으로 CGLIB을 사용합니다.
Self Invocation 문제는 무엇인가요?
  • AOP는 프록시 객체를 통해 호출될 때만 동작합니다.
  • 같은 클래스 안에서 this.메서드() 형태로 자기 자신의 메서드를 호출하면 프록시를 거치지 않고 실제 객체의 메서드가 바로 실행됩니다.
  • 그래서 @Transactional이나 @Cacheable 같은 애노테이션이 무시되는 현상이 발생합니다.
  • 가장 권장되는 해결책은 해당 메서드를 별도의 빈으로 분리해 프록시를 거쳐 호출되도록 만드는 것입니다.

1.7 트랜잭션

@Transactional은 어떻게 동작하나요?
  • Spring이 해당 빈의 프록시를 만들어, 메서드 호출을 가로챕니다.
  • 프록시는 메서드 실행 전에 트랜잭션을 시작하고, 정상적으로 끝나면 커밋, 지정된 예외가 발생하면 롤백합니다.
  • 실제 커밋과 롤백은 PlatformTransactionManager가 수행하며, 트랜잭션이 사용하는 커넥션은 ThreadLocal에 보관되어 같은 스레드 안에서 공유됩니다.
트랜잭션 전파 수준에는 어떤 것들이 있나요?
  • REQUIRED: 기본값으로, 진행 중인 트랜잭션이 있으면 참여하고 없으면 새로 시작합니다.
  • REQUIRES_NEW: 기존 트랜잭션을 잠시 미뤄두고 항상 새 트랜잭션을 시작합니다. 로그 기록처럼 바깥 트랜잭션이 롤백되어도 남겨야 하는 작업에 사용합니다.
  • SUPPORTS: 트랜잭션이 있으면 참여하고 없으면 트랜잭션 없이 실행합니다.
  • MANDATORY: 반드시 기존 트랜잭션이 있어야 하며 없으면 예외를 던집니다.
  • NESTED: 세이브포인트를 만들어 부분 롤백이 가능하게 합니다.
@Transactional의 롤백 조건은 무엇인가요?
  • 기본적으로 RuntimeExceptionError가 발생했을 때만 롤백합니다.
  • IOException 같은 Checked Exception은 기본 설정에서 롤백되지 않고 그대로 커밋됩니다.
  • Checked Exception에도 롤백이 필요하면 rollbackFor 속성으로 명시해야 합니다.
  • 또한 메서드 안에서 예외를 try-catch로 잡아버리면 프록시까지 전달되지 않아 롤백되지 않는다는 점도 주의해야 합니다.
readOnly = true는 어떤 효과가 있나요?
  • JPA를 사용하는 경우 flush를 하지 않도록 설정되어, 변경 감지를 위한 스냅샷 비교 작업이 생략되므로 성능과 메모리에 이점이 있습니다.
  • JDBC 드라이버와 DB에 읽기 전용이라는 힌트를 전달해 최적화 여지를 줍니다.
  • 읽기 전용이라는 의도가 코드에 드러나고, 복제 구성에서 읽기 전용 DB로 라우팅하는 기준으로도 활용됩니다.
@Transactional이 동작하지 않는 경우는 언제인가요?
  • 같은 클래스 내부에서 자기 자신의 메서드를 호출한 경우(Self Invocation)입니다.
  • private 메서드나 final 메서드에 붙인 경우 프록시가 가로챌 수 없습니다.
  • 스프링 빈이 아닌 객체를 직접 new로 만들어 호출한 경우입니다.
  • 예외를 잡아서 처리해 버려 프록시까지 예외가 전달되지 않은 경우입니다.
비동기 메서드에서 트랜잭션은 어떻게 동작하나요?
  • 트랜잭션 정보는 ThreadLocal에 저장되는데, @Async 메서드는 다른 스레드에서 실행되므로 호출한 쪽의 트랜잭션이 전파되지 않습니다.
  • 따라서 비동기 메서드는 별도의 독립적인 트랜잭션으로 동작하며, 바깥 트랜잭션이 롤백되어도 이미 커밋된 비동기 작업은 되돌아가지 않습니다.
  • 트랜잭션 커밋 이후에 비동기 작업을 실행하고 싶다면 @TransactionalEventListener를 활용하는 방법이 있습니다.

1.8 JPA

JPA와 Hibernate의 관계는 무엇인가요?
  • JPA는 자바에서 ORM을 어떻게 사용할지 정의한 표준 명세(인터페이스)입니다.
  • Hibernate는 그 명세를 실제로 구현한 구현체 중 하나이며 가장 널리 쓰입니다.
  • 표준에 맞춰 개발하면 구현체를 교체할 수 있지만, 실무에서는 Hibernate 고유 기능도 함께 사용하는 경우가 많습니다.
JPA와 MyBatis의 차이는 무엇인가요?
  • JPA: 객체와 테이블을 매핑하는 ORM입니다. 객체를 다루면 프레임워크가 SQL을 만들어 실행하므로, 개발자는 SQL이 아니라 도메인 객체 중심으로 코드를 작성합니다.
  • MyBatis: SQL을 직접 작성하고 그 결과를 객체에 매핑해 주는 SQL Mapper입니다. 쿼리를 개발자가 완전히 통제합니다.
  • 가장 큰 차이는 SQL을 누가 작성하느냐이며, 여기서 나머지 특성이 갈립니다.
JPA와 MyBatis는 각각 어떤 장단점이 있나요?
  • JPA의 장점: 반복적인 CRUD 쿼리를 직접 작성하지 않아 생산성이 높고, 영속성 컨텍스트의 1차 캐시와 변경 감지 같은 기능을 활용할 수 있습니다. DB 방언을 통해 특정 DB에 대한 종속성도 줄어듭니다.
  • JPA의 단점: 실행되는 SQL이 코드에 드러나지 않아 예측이 어렵고, N+1 문제처럼 의도치 않은 쿼리가 발생할 수 있습니다. 통계나 복잡한 집계 쿼리는 표현하기 까다롭습니다.
  • MyBatis의 장점: 쿼리를 직접 쓰기 때문에 복잡한 조인이나 튜닝이 필요한 쿼리를 자유롭게 다룰 수 있고, 실행될 SQL이 그대로 보여 예측 가능합니다. 조건에 따라 쿼리를 조립하는 동적 SQL도 강력합니다.
  • MyBatis의 단점: 단순한 CRUD도 매번 SQL을 작성해야 해 반복 작업이 많고, SQL이 특정 DB 문법에 의존하기 쉽습니다. 테이블이 바뀌면 관련 쿼리를 모두 찾아 수정해야 합니다.
JPA와 MyBatis 중 무엇을 선택해야 하나요?
  • 도메인 모델이 명확하고 등록·수정·조회 같은 일반적인 CRUD가 중심이라면 JPA가 생산성 면에서 유리합니다.
  • 복잡한 통계나 리포팅처럼 조회 쿼리의 비중이 크고 튜닝이 중요한 시스템이라면 MyBatis가 적합합니다.
  • 실무에서는 둘 중 하나만 고르기보다, 기본 CRUD와 도메인 로직은 JPA로 처리하고 복잡한 조회만 QueryDSL이나 네이티브 쿼리, 혹은 MyBatis로 함께 쓰는 방식을 많이 사용합니다.
영속성 컨텍스트란 무엇이며 어떤 이점이 있나요?
  • 엔티티를 담아 관리하는 논리적인 저장 공간으로, 트랜잭션 범위에서 동작합니다.
  • 1차 캐시: 같은 트랜잭션 안에서 같은 식별자로 조회하면 DB에 다시 가지 않고 캐시에서 반환하며, 항상 동일한 객체임이 보장됩니다.
  • 쓰기 지연: 변경 사항을 모아두었다가 커밋 시점에 한 번에 반영합니다.
  • 변경 감지지연 로딩도 영속성 컨텍스트가 있기에 가능합니다.
Entity의 생명주기(상태)는 어떻게 되나요?
  • 비영속: 객체를 막 생성해 영속성 컨텍스트와 아무 관계가 없는 상태입니다.
  • 영속: persist()하거나 조회해서 영속성 컨텍스트가 관리하는 상태로, 변경 감지가 동작합니다.
  • 준영속: detach(), clear(), close() 등으로 관리 대상에서 분리된 상태이며 변경 감지가 동작하지 않습니다.
  • 삭제: remove()로 삭제를 예약한 상태입니다.
Dirty Checking은 어떻게 동작하나요?
  • 영속성 컨텍스트는 엔티티를 처음 가져올 때의 상태를 스냅샷으로 보관합니다.
  • flush 시점에 현재 값과 스냅샷을 비교해 달라진 필드가 있으면 UPDATE 쿼리를 자동으로 만들어 실행합니다.
  • 그래서 별도의 save() 호출 없이 값만 바꿔도 반영됩니다. 단, 준영속 상태의 엔티티는 대상이 아닙니다.
Flush는 언제 발생하나요?
  • 트랜잭션이 커밋될 때 자동으로 발생합니다.
  • JPQL 쿼리를 실행하기 직전에도 발생합니다. 아직 반영되지 않은 변경 사항이 쿼리 결과에 빠지는 것을 막기 위해서입니다.
  • flush()를 직접 호출할 수도 있습니다.
  • flush는 변경 내용을 DB에 동기화하는 것이지 영속성 컨텍스트를 비우는 것이 아니라는 점을 구분해야 합니다.
지연 로딩과 즉시 로딩의 차이는 무엇인가요?
  • 지연 로딩(LAZY): 연관된 엔티티를 프록시로 두었다가 실제로 사용할 때 조회합니다.
  • 즉시 로딩(EAGER): 엔티티를 조회할 때 연관 엔티티까지 함께 가져옵니다.
  • @ManyToOne@OneToOne은 기본값이 EAGER인데, 예상치 못한 조인과 N+1 문제를 유발하므로 모두 LAZY로 설정하고 필요한 곳에서 fetch join으로 함께 조회하는 방식이 권장됩니다.
N+1 문제란 무엇이며 어떻게 해결하나요?
  • 목록을 조회하는 쿼리 1번을 실행한 뒤, 각 엔티티의 연관 데이터를 가져오기 위해 추가 쿼리가 N번 실행되는 문제입니다.
  • fetch join으로 연관 엔티티를 한 번에 조회하거나, @EntityGraph로 같은 효과를 낼 수 있습니다.
  • 컬렉션의 경우 @BatchSizedefault_batch_fetch_size 설정으로 여러 건을 IN 절로 묶어 조회하면 쿼리 수를 크게 줄일 수 있습니다.
Fetch Join의 한계는 무엇인가요?
  • 컬렉션을 fetch join하면 조인 결과로 행이 늘어나기 때문에 페이징을 정상적으로 적용할 수 없습니다. 이 경우 하이버네이트가 전체를 메모리로 읽어 페이징하므로 데이터가 많으면 위험합니다.
  • 둘 이상의 컬렉션을 동시에 fetch join하면 결과가 곱해져 예외가 발생합니다.
  • 그래서 컬렉션 페이징이 필요할 때는 fetch join 대신 배치 사이즈 설정을 사용하는 것이 일반적입니다.
Cascade와 Orphan Removal의 차이는 무엇인가요?
  • Cascade: 부모 엔티티에 수행한 작업을 자식에게 전파합니다. 부모를 저장하면 자식도 함께 저장되는 식입니다.
  • Orphan Removal: 부모와의 연관관계가 끊어진 자식을 자동으로 삭제합니다. 컬렉션에서 자식을 제거하면 DELETE 쿼리가 실행됩니다.
  • 둘 다 자식의 생명주기를 부모가 온전히 관리하는 경우에만 사용해야 하며, 자식이 다른 곳에서도 참조된다면 의도치 않은 삭제가 발생할 수 있습니다.
연관관계의 주인은 무엇인가요?
  • 양방향 연관관계에서 실제로 외래 키를 관리하는 쪽을 말하며, 외래 키를 가진 테이블에 매핑된 엔티티가 주인이 됩니다.
  • 주인이 아닌 쪽은 mappedBy로 지정하며 조회만 가능하고 외래 키에 영향을 주지 않습니다.
  • 그래서 주인 쪽에 값을 설정하지 않으면 DB에 반영되지 않습니다. 객체 상태와 DB를 일치시키기 위해 양쪽 모두 값을 설정하는 편의 메서드를 두는 것이 좋습니다.

1.9 Spring Security

Spring Security의 Filter Chain은 어떻게 동작하나요?
  • 서블릿 필터로 등록된 진입점이 Spring이 관리하는 필터 체인으로 요청을 위임합니다.
  • 인증 처리, 인가 검사, CSRF 확인 등을 담당하는 여러 필터가 정해진 순서대로 실행됩니다.
  • 각 필터는 조건에 맞지 않으면 그냥 통과시키므로, 요청 하나가 여러 필터를 거쳐 최종적으로 컨트롤러에 도달합니다.
  • 커스텀 인증을 구현할 때는 이 체인의 적절한 위치에 필터를 추가하는 방식으로 확장합니다.
Authentication과 Authorization은 어떻게 처리되나요?
  • Authentication(인증): 사용자가 누구인지 확인하는 과정으로, 인증에 성공하면 사용자 정보와 권한을 담은 Authentication 객체가 만들어집니다.
  • Authorization(인가): 인증된 사용자가 해당 자원에 접근할 권한이 있는지 확인하는 과정으로, 저장된 권한 정보를 요청 경로나 메서드의 요구 권한과 비교합니다.
SecurityContext는 어떻게 관리되나요?
  • 인증 정보는 SecurityContext에 담기고, SecurityContextHolder가 이를 ThreadLocal에 보관합니다.
  • 덕분에 컨트롤러나 서비스 어디서든 파라미터로 넘기지 않고도 현재 사용자 정보를 꺼내 쓸 수 있습니다.
  • 요청 처리가 끝나면 정리되며, 스레드 기반이라 @Async나 별도 스레드로 넘어가면 기본적으로 전파되지 않는다는 점을 주의해야 합니다.
AuthenticationManager와 AuthenticationProvider의 역할은 무엇인가요?
  • AuthenticationManager: 인증 요청을 받아 처리를 총괄하는 진입점으로, 실제 작업은 여러 Provider에게 위임합니다.
  • AuthenticationProvider: 특정 인증 방식을 실제로 수행합니다. 자신이 처리할 수 있는 인증 타입인지 확인한 뒤 검증합니다.
  • 일반적인 아이디·비밀번호 방식이라면 UserDetailsService로 사용자 정보를 조회하고 PasswordEncoder로 비밀번호를 대조합니다.
JWT 기반 인증은 어떻게 구현하나요?
  • 로그인 요청을 검증한 뒤 사용자 정보를 담은 토큰을 발급해 클라이언트에 전달합니다.
  • 이후 클라이언트는 요청마다 Authorization 헤더에 토큰을 담아 보냅니다.
  • 커스텀 필터가 토큰의 서명과 만료를 검증하고, 유효하면 Authentication 객체를 만들어 SecurityContext에 저장합니다.
  • 서버가 세션을 유지하지 않으므로 세션 정책을 STATELESS로 설정합니다.
CSRF는 무엇이며 언제 비활성화할 수 있나요?
  • 사용자가 로그인한 상태를 악용해, 공격자가 만든 페이지에서 의도하지 않은 요청을 보내게 하는 공격입니다.
  • 브라우저가 쿠키를 자동으로 함께 보내기 때문에 가능하며, 서버는 예측 불가능한 토큰을 함께 요구해 이를 막습니다.
  • 세션 쿠키를 쓰지 않고 헤더에 토큰을 담아 보내는 완전한 Stateless API라면 쿠키 자동 전송이 없으므로 비활성화할 수 있습니다.
  • 다만 인증 토큰을 쿠키에 저장한다면 여전히 CSRF 대비가 필요합니다.
Method Security는 무엇인가요?
  • URL 패턴이 아니라 메서드 단위로 권한을 검사하는 기능입니다.
  • @PreAuthorize로 메서드 실행 전에 조건을 검사하거나, @PostAuthorize로 반환값까지 확인해 검사할 수 있습니다.
  • URL만으로 표현하기 어려운 세밀한 권한 정책(예: 본인이 작성한 글만 수정 가능)을 표현할 때 유용합니다.

1.10 Spring 동시성

Spring MVC의 스레드 모델은 어떻게 되나요?
  • 요청 하나당 스레드 하나를 할당하는 방식입니다.
  • 처리 과정에서 DB 조회나 외부 API 호출 같은 블로킹 I/O가 발생하면, 그동안 스레드는 아무 일도 하지 않으면서 계속 점유됩니다.
  • 따라서 동시에 처리할 수 있는 요청 수가 스레드 풀 크기에 직접적으로 제한됩니다.
Tomcat Thread Pool 크기는 어떻게 정해야 하나요?
  • 기본값은 최대 200개 정도이며, 무작정 늘리면 메모리 사용량과 컨텍스트 스위칭 비용이 커집니다.
  • I/O 대기 시간이 길수록 스레드를 늘리는 것이 처리량에 도움이 되고, CPU 연산 위주라면 코어 수를 크게 넘겨도 효과가 없습니다.
  • 중요한 점은 스레드 수만 늘리면 DB 커넥션 풀이 병목이 된다는 것입니다. 스레드 수, 커넥션 풀 크기, DB의 최대 연결 수를 함께 고려해야 합니다.
@Async는 어떻게 동작하나요?
  • 프록시가 메서드 호출을 가로채, 실제 실행을 별도의 스레드 풀에 넘기고 호출한 쪽에는 즉시 제어를 돌려줍니다.
  • 반환값이 필요하면 CompletableFuture로 받습니다.
  • 주의할 점은 기본 스레드 풀 설정이 무제한에 가까울 수 있으므로 직접 Executor를 등록해 크기를 제한해야 하고, 앞서 말한 대로 트랜잭션과 SecurityContext가 전파되지 않는다는 것입니다.
WebFlux는 무엇이며 MVC와 어떻게 다른가요?
  • 논블로킹 방식으로 동작하는 리액티브 웹 프레임워크입니다.
  • MVC가 요청마다 스레드를 점유하는 것과 달리, WebFlux는 적은 수의 이벤트 루프 스레드로 많은 동시 연결을 처리합니다.
  • I/O 대기 중에는 스레드를 반납하므로, 외부 API 호출이 많거나 동시 접속이 매우 많은 서비스에서 적은 자원으로 높은 처리량을 낼 수 있습니다.
Mono와 Flux의 차이는 무엇인가요?
  • 둘 다 Reactor가 제공하는 데이터 흐름을 표현하는 타입입니다.
  • Mono: 데이터가 0개 또는 1개인 경우를 표현합니다. 단건 조회나 저장 결과에 사용합니다.
  • Flux: 데이터가 0개 이상 여러 개인 경우를 표현합니다. 목록 조회나 스트리밍에 사용합니다.
  • 둘 다 선언만으로는 아무 일도 일어나지 않고, 구독(subscribe)하는 시점에 실제로 실행됩니다.
WebFlux를 선택할 때 주의할 점은 무엇인가요?
  • 처리 과정 어디에라도 블로킹 코드가 있으면 이벤트 루프가 막혀 전체 성능이 오히려 크게 나빠집니다.
  • 따라서 DB 드라이버까지 논블로킹(R2DBC 등)이어야 효과를 볼 수 있는데, 지원 범위가 JDBC보다 좁습니다.
  • 코드가 선언적으로 바뀌어 학습 곡선이 있고 스택 트레이스 추적도 어려워 디버깅 난이도가 높습니다.
  • 최근에는 Java 21의 Virtual Thread를 사용해 기존 MVC 스타일을 유지하면서 확장성을 얻는 방법도 대안으로 고려됩니다.

2. FastAPI

2.1 FastAPI 구조

FastAPI의 주요 특징은 무엇인가요?
  • 파이썬 타입 힌트를 기반으로 요청 데이터를 자동 검증하고 변환합니다.
  • 그 타입 정보를 이용해 OpenAPI 문서와 Swagger UI를 자동으로 생성합니다.
  • ASGI 기반이라 async/await를 이용한 비동기 처리를 자연스럽게 지원합니다.
  • 코드에 선언한 타입이 곧 문서이자 검증 규칙이 되어, 별도 문서화와 검증 코드를 따로 작성할 필요가 줄어듭니다.
FastAPI는 어떤 라이브러리 위에 만들어졌나요?
  • Starlette: 라우팅, 미들웨어, 요청·응답 처리 같은 ASGI 웹 프레임워크의 핵심 기능을 담당합니다.
  • Pydantic: 타입 힌트 기반의 데이터 검증과 직렬화를 담당합니다.
  • FastAPI는 이 둘을 결합하고 의존성 주입과 자동 문서화를 얹은 형태입니다.
  • Uvicorn은 FastAPI의 일부가 아니라, 애플리케이션을 실제로 구동하는 별도의 ASGI 서버입니다.
FastAPI의 요청 처리 과정은 어떻게 되나요?
  • Uvicorn이 HTTP 요청을 받아 ASGI 형식으로 애플리케이션에 전달합니다.
  • 등록된 미들웨어를 순서대로 거칩니다.
  • 라우터가 경로와 메서드에 맞는 엔드포인트를 찾습니다.
  • Depends로 선언된 의존성들을 해결하고, Pydantic이 요청 본문과 파라미터를 검증합니다.
  • 엔드포인트를 실행하는데, async def면 이벤트 루프에서 직접 실행하고 일반 def면 스레드 풀에서 실행합니다.
  • 반환값을 response_model에 맞게 직렬화해 응답합니다.

2.2 WSGI와 ASGI

WSGI와 ASGI의 차이는 무엇인가요?
  • WSGI: 파이썬 웹 서버와 애플리케이션을 연결하는 전통적인 동기 인터페이스입니다. 요청 하나에 응답 하나가 대응하는 구조입니다.
  • ASGI: WSGI의 비동기 버전으로, 하나의 연결에서 여러 번 데이터를 주고받는 것을 표현할 수 있습니다.
  • 그래서 ASGI는 WebSocket이나 SSE처럼 오래 유지되는 양방향 통신을 지원할 수 있습니다.
ASGI가 필요한 이유는 무엇인가요?
  • WSGI는 동기 방식이라 I/O를 기다리는 동안 워커가 다른 요청을 처리할 수 없습니다.
  • 외부 API 호출처럼 대기 시간이 긴 작업이 많으면 워커가 대기에만 묶여 처리량이 떨어집니다.
  • ASGI는 대기 중에 다른 요청을 처리할 수 있어 같은 자원으로 훨씬 많은 동시 연결을 감당할 수 있습니다.
  • 또한 WSGI 구조로는 표현할 수 없던 WebSocket 같은 프로토콜을 지원하기 위해서도 필요했습니다.
Uvicorn과 Gunicorn의 역할은 각각 무엇인가요?
  • Uvicorn: 실제로 ASGI 애플리케이션을 실행하는 서버로, HTTP 요청을 받아 이벤트 루프에서 처리합니다.
  • Gunicorn: 프로세스 매니저 역할로, 여러 워커 프로세스를 띄우고 죽은 워커를 재시작하는 등 생명주기를 관리합니다.
  • 전통적으로는 Gunicorn이 Uvicorn 워커를 실행하는 조합을 많이 썼고, 최근에는 Uvicorn 자체의 멀티 워커 옵션이나 컨테이너 오케스트레이터가 그 역할을 대신하기도 합니다.

2.3 동기와 비동기

defasync def 엔드포인트는 어떻게 다르게 실행되나요?
  • async def: 이벤트 루프에서 직접 실행됩니다. 내부에서 await로 대기하는 동안 다른 요청이 처리될 수 있습니다.
  • def: FastAPI가 별도의 스레드 풀로 넘겨 실행합니다. 동기 코드가 이벤트 루프를 막지 않도록 하기 위한 배려입니다.
  • 따라서 동기 라이브러리만 사용한다면 일반 def로 두는 것이 오히려 안전합니다.
async def 안에 블로킹 코드가 있으면 어떤 문제가 생기나요?
  • 이벤트 루프는 단일 스레드로 동작하므로, 블로킹 코드가 실행되는 동안 루프 전체가 멈춥니다.
  • 그 결과 해당 워커가 처리 중인 모든 요청이 함께 지연되어, 비동기로 만들었는데 오히려 성능이 나빠집니다.
  • time.sleep, 동기 DB 드라이버, requests 라이브러리 등이 대표적인 원인입니다.
  • 부득이하게 동기 코드를 써야 한다면 스레드 풀에서 실행하도록 넘겨야 합니다.
CPU-bound 작업은 어떻게 처리해야 하나요?
  • CPU 연산은 스레드 풀에 넘겨도 GIL 때문에 효과가 제한적입니다.
  • 별도 프로세스로 넘기거나, Celery 같은 작업 큐로 분리해 API 서버와 분리하는 것이 좋습니다.
  • 요청-응답 안에서 무거운 연산을 처리하면 워커가 오래 묶여 전체 처리량이 급격히 떨어집니다.

2.4 Pydantic

Pydantic BaseModel은 어떤 역할을 하나요?
  • 타입 힌트로 데이터의 구조를 선언하면, 입력값을 검증하고 선언한 타입으로 변환해 줍니다.
  • 검증에 실패하면 어떤 필드가 왜 잘못되었는지 상세한 오류를 만들어 줍니다.
  • FastAPI는 이를 이용해 요청 본문 검증과 자동 문서화를 동시에 처리합니다.
Request Validation과 Response Model은 각각 무엇인가요?
  • Request Validation: 요청 본문이나 파라미터를 선언한 모델에 맞춰 검증합니다. 조건에 맞지 않으면 엔드포인트 코드가 실행되기 전에 422 응답이 반환됩니다.
  • Response Model: 응답의 형태를 선언하는 것으로, 선언한 필드만 걸러서 내보냅니다.
  • 특히 응답 모델은 비밀번호 해시처럼 내부에서만 쓰는 필드가 실수로 노출되는 것을 막아주는 안전장치 역할을 합니다.
Custom Validator는 어떻게 사용하나요?
  • 타입만으로 표현할 수 없는 검증 규칙을 직접 정의할 때 사용합니다.
  • 필드 단위 검증은 해당 필드 값만 검사하고, 모델 단위 검증은 여러 필드를 함께 비교할 수 있습니다.
  • 예를 들어 시작일이 종료일보다 앞서야 한다는 규칙은 두 필드를 함께 봐야 하므로 모델 단위 검증으로 구현합니다.
Pydantic v1과 v2의 차이는 무엇인가요?
  • v2는 핵심 검증 로직이 Rust로 다시 작성되어 성능이 크게 향상되었습니다.
  • 메서드 이름이 정리되어 .dict().model_dump()로, .parse_obj().model_validate()로 바뀌었습니다.
  • 검증 데코레이터도 @validator에서 @field_validator로, @root_validator에서 @model_validator로 변경되었습니다.
  • 설정 방식도 내부 Config 클래스 대신 model_config를 사용합니다. 호환되지 않는 부분이 많아 마이그레이션 시 주의가 필요합니다.

2.5 Dependency Injection

Depends는 어떻게 동작하나요?
  • 엔드포인트 파라미터에 Depends(함수)를 선언하면, FastAPI가 요청 처리 전에 그 함수를 실행하고 반환값을 파라미터로 넣어줍니다.
  • 의존성 함수가 또 다른 의존성을 가질 수 있어 계층적으로 구성할 수 있습니다.
  • 인증 확인, DB 세션 준비, 공통 파라미터 처리처럼 여러 엔드포인트에서 반복되는 준비 작업을 재사용할 수 있습니다.
의존성으로 DB 세션을 관리하는 방법은 무엇인가요?
  • yield를 사용하는 의존성으로 만듭니다. yield 앞에서 세션을 생성해 넘겨주고, 뒤에서 정리 작업을 수행합니다.
  • 요청 처리가 끝나면 yield 이후 코드가 실행되어 세션이 자동으로 닫히므로, 커넥션 반납을 빠뜨릴 위험이 없습니다.
  • 요청마다 독립된 세션을 사용하게 되어, 세션을 전역으로 공유할 때 생기는 문제를 피할 수 있습니다.
Dependency Cache는 무엇인가요?
  • 하나의 요청 안에서 같은 의존성이 여러 번 사용되어도 실제로는 한 번만 실행하고 결과를 재사용하는 기능입니다.
  • 예를 들어 여러 의존성이 공통으로 현재 사용자 조회에 의존한다면, DB 조회가 한 번만 일어납니다.
  • 매번 새로 실행해야 한다면 use_cache=False로 끌 수 있습니다.

2.6 Middleware

Middleware는 어떻게 동작하나요?
  • 모든 요청과 응답을 감싸는 계층으로, 요청이 들어올 때와 응답이 나갈 때 양쪽에서 처리할 수 있습니다.
  • 여러 개를 등록하면 양파 껍질처럼 겹겹이 감싸는 구조가 되어, 요청은 바깥에서 안으로, 응답은 안에서 바깥으로 흐릅니다.
  • 요청 처리 시간 측정, 요청 ID 부여, 공통 로깅, CORS 헤더 추가 등에 사용합니다.
Middleware와 Dependency의 차이는 무엇인가요?
  • Middleware: 모든 경로에 일괄 적용되며 응답까지 가공할 수 있습니다. 다만 어떤 엔드포인트가 실행될지, 검증된 파라미터가 무엇인지 알기 어렵습니다.
  • Dependency: 특정 엔드포인트나 라우터 단위로 적용할 수 있고, 반환값을 엔드포인트에서 바로 사용할 수 있으며 다른 의존성과 조합할 수 있습니다.
  • 그래서 인증처럼 대상별로 다르게 적용하고 결과값(사용자 정보)을 써야 하는 경우는 Dependency가, CORS나 전역 로깅처럼 모든 요청에 동일하게 적용되는 것은 Middleware가 적합합니다.

2.7 데이터베이스 연동

SQLAlchemy Session은 무엇인가요?
  • DB 작업의 작업 단위를 관리하는 객체로, 조회한 객체를 추적하고 변경 사항을 모아 커밋 시점에 반영합니다.
  • 같은 세션 안에서 같은 기본키로 조회하면 동일한 객체를 반환하는 식별 맵을 유지합니다.
  • JPA의 영속성 컨텍스트와 비슷한 역할을 한다고 이해하면 됩니다.
AsyncSession은 언제 사용하나요?
  • 엔드포인트를 async def로 작성하고 이벤트 루프를 막지 않으면서 DB 작업을 하려면 AsyncSession을 사용합니다.
  • 이때 드라이버도 asyncpg처럼 비동기를 지원하는 것을 써야 합니다.
  • 동기 드라이버를 비동기 세션과 함께 쓰면 실제로는 블로킹이 발생해 의미가 없으므로, 애플리케이션 전체를 동기와 비동기 중 한쪽으로 일관되게 맞추는 것이 중요합니다.
Session 생명주기는 어떻게 관리해야 하나요?
  • 요청 하나당 세션 하나를 원칙으로 하고, 요청이 끝나면 반드시 닫아야 합니다.
  • 의존성 주입에서 yield로 생성과 정리를 함께 관리하는 방식이 표준적입니다.
  • 세션은 스레드나 태스크 간에 안전하지 않으므로 전역 변수로 공유해서는 안 됩니다.
  • 세션이 사용하는 커넥션은 커넥션 풀에서 빌려오는 것이므로, 오래 붙잡고 있으면 풀이 고갈됩니다.

2.8 배포

Worker 수는 어떻게 정하나요?
  • CPU 연산 위주라면 코어 수를 기준으로 잡습니다.
  • 비동기로 I/O를 처리한다면 워커 하나가 이미 많은 동시 요청을 감당하므로 워커를 무리하게 늘릴 필요가 없습니다.
  • 반드시 함께 고려해야 할 것은 DB 연결 수입니다. 워커마다 독립된 커넥션 풀을 갖기 때문에, 워커 수와 풀 크기를 곱한 값이 DB의 최대 연결 수를 넘지 않아야 합니다.
Reverse Proxy는 왜 두나요?
  • TLS 처리를 앞단에서 담당해 애플리케이션 서버의 부담을 덜어줍니다.
  • 정적 파일 제공, 요청 크기 제한, 타임아웃 설정 같은 처리를 애플리케이션 밖에서 할 수 있습니다.
  • 여러 인스턴스로 트래픽을 분산하고, 애플리케이션 서버를 외부에 직접 노출하지 않아 보안에도 유리합니다.
Graceful Shutdown은 왜 필요한가요?
  • 배포나 스케일 조정으로 서버를 내릴 때, 처리 중이던 요청이 강제로 끊기면 사용자 입장에서는 오류로 보입니다.
  • 종료 신호를 받으면 새 요청은 더 이상 받지 않되, 진행 중인 요청은 끝까지 처리한 뒤 종료하는 방식이 Graceful Shutdown입니다.
  • 이 과정에서 DB 커넥션 풀 정리 같은 자원 반납도 함께 수행하며, 오케스트레이터가 강제 종료하기 전에 끝나도록 종료 대기 시간을 맞춰 설정해야 합니다.

3. LangGraph

3.1 LangGraph 기본

LangChain과 LangGraph의 차이는 무엇인가요?
  • LangChain: LLM, 프롬프트, 도구 같은 구성 요소를 조합하는 라이브러리로, 주로 앞에서 뒤로 흐르는 파이프라인 형태를 다룹니다.
  • LangGraph: 실행 흐름을 노드와 엣지로 이루어진 그래프로 표현하며, 상태를 명시적으로 관리하고 순환 구조를 지원합니다.
  • LangChain이 "무엇을 조합할지"에 가깝다면, LangGraph는 "어떤 순서로, 어떤 상태를 들고 실행할지"를 다룬다고 볼 수 있습니다.
LangGraph가 순환 구조를 지원하는 이유는 무엇인가요?
  • 에이전트의 동작은 본질적으로 반복입니다. 모델이 판단하고, 도구를 실행하고, 그 결과를 다시 모델에게 주는 과정을 목표를 달성할 때까지 되풀이합니다.
  • 이런 흐름은 몇 번 반복될지 미리 알 수 없기 때문에 방향이 한쪽으로만 흐르는 DAG로는 표현할 수 없습니다.
  • LangGraph는 노드에서 이전 노드로 되돌아가는 엣지를 허용해 이 반복 구조를 자연스럽게 표현합니다.
Workflow와 Agent는 어떻게 다른가요?
  • Workflow: 실행할 단계와 순서를 사람이 미리 정해둔 방식입니다. 동작이 예측 가능하고 비용과 지연 시간을 가늠하기 쉬우며 디버깅이 수월합니다.
  • Agent: 다음에 무엇을 할지 모델이 스스로 판단합니다. 상황에 유연하게 대응하지만 실행 경로가 매번 달라져 예측과 검증이 어렵습니다.
  • 처리해야 할 단계가 정형화되어 있다면 Workflow가 낫고, 입력에 따라 필요한 작업이 크게 달라진다면 Agent가 적합합니다.

3.2 StateGraph

StateGraph의 State, Node, Edge는 각각 무엇인가요?
  • State: 그래프 전체가 공유하는 데이터로, 어떤 필드를 갖는지 스키마로 미리 정의합니다.
  • Node: 실제 작업을 수행하는 함수입니다. 현재 상태를 입력으로 받고, 변경할 필드만 담은 딕셔너리를 반환합니다.
  • Edge: 한 노드가 끝난 뒤 어디로 갈지 연결하는 경로입니다.
  • 노드가 상태 전체를 반환할 필요 없이 바뀐 부분만 반환하면 되므로, 각 노드를 독립적으로 작성하고 테스트하기 쉽습니다.
Conditional Edge는 무엇인가요?
  • 다음에 실행할 노드를 고정하지 않고, 현재 상태를 보고 판단하는 함수로 결정하는 엣지입니다.
  • 그 함수는 상태를 받아 다음 노드의 이름을 반환하고, 그래프는 그 결과에 따라 분기합니다.
  • 예를 들어 모델의 응답에 도구 호출이 포함되어 있으면 도구 노드로, 아니면 종료로 보내는 라우팅에 사용합니다.
START와 END는 무엇인가요?
  • 그래프의 시작점과 끝점을 나타내는 특수한 지점입니다.
  • START에서 나가는 엣지가 어느 노드부터 실행할지 정하고, END로 향하는 엣지에 도달하면 실행이 종료됩니다.
  • 진입점과 종료 조건을 그래프 구조 안에 명시적으로 드러내기 위한 장치입니다.
Command는 무엇인가요?
  • 노드가 상태 갱신과 다음에 이동할 노드를 한 번에 반환할 수 있게 해주는 객체입니다.
  • 보통은 상태 갱신은 노드가, 라우팅은 조건부 엣지가 담당하는데, 두 결정이 밀접하게 얽혀 있다면 노드 안에서 함께 처리하는 것이 더 자연스러울 수 있습니다.
  • 다만 라우팅 로직이 노드 안으로 숨으면 그래프 구조만 보고 흐름을 파악하기 어려워지므로 남용하지 않는 것이 좋습니다.

3.3 State 관리

State Schema는 어떻게 정의하나요?
  • TypedDict를 사용하는 방식이 가장 일반적입니다. 가볍고 오버헤드가 없으며 타입 힌트로 구조를 표현할 수 있습니다.
  • 런타임 검증이 필요하거나 기본값과 제약 조건을 두고 싶다면 Pydantic 모델을 사용할 수도 있습니다.
  • 다만 Pydantic은 상태가 갱신될 때마다 검증 비용이 발생하므로, 검증이 꼭 필요한 경우에 선택하는 것이 좋습니다.
Reducer는 무엇이며 왜 필요한가요?
  • 노드가 반환한 값을 기존 상태에 어떻게 반영할지 정의하는 함수입니다.
  • 기본 동작은 덮어쓰기인데, 대화 메시지처럼 기존 값에 이어 붙여야 하는 필드는 덮어쓰면 이전 내용이 사라집니다.
  • 또한 여러 노드가 병렬로 실행되어 같은 필드를 동시에 갱신하려 하면, 무엇을 남길지 정할 방법이 없어 충돌 오류가 발생합니다. Reducer는 이때 병합 규칙을 제공합니다.
Annotated는 어떻게 사용하나요?
  • 상태 스키마의 필드에 타입과 함께 Reducer를 지정할 때 사용합니다.
  • 예를 들어 메시지 목록 필드에 메시지 누적용 Reducer를 붙이면, 노드가 새 메시지만 반환해도 기존 목록 뒤에 이어 붙습니다.
  • 즉 "이 필드는 덮어쓰지 말고 이렇게 합쳐라"라는 규칙을 타입 선언에 함께 담는 방식입니다.

3.4 실행 흐름

Graph를 실행하려면 어떤 과정이 필요한가요?
  • 노드와 엣지를 모두 등록한 뒤 compile()을 호출하면 실행 가능한 객체가 만들어집니다.
  • 이 단계에서 그래프 구조가 유효한지 검증되고, 체크포인터 같은 실행 옵션이 함께 설정됩니다.
  • 컴파일된 객체를 invokestream으로 실행합니다.
invoke와 stream의 차이는 무엇인가요?
  • invoke: 그래프를 끝까지 실행하고 최종 상태만 반환합니다.
  • stream: 실행 도중의 중간 결과를 순차적으로 받아볼 수 있습니다.
  • 스트리밍 모드에 따라 각 단계가 끝난 뒤의 전체 상태, 노드가 반환한 변경분, 모델이 생성하는 토큰 단위 출력 등을 선택할 수 있습니다.
  • LLM 응답은 생성에 시간이 걸리므로, 사용자에게 진행 상황을 보여주려면 스트리밍이 사실상 필수입니다.
병렬 Node는 어떻게 동작하나요?
  • 하나의 노드에서 여러 엣지가 뻗어 나가면 그 노드들이 같은 단계에서 함께 실행됩니다.
  • 서로 독립적인 여러 자료를 동시에 조회하는 것처럼, 순서가 상관없는 작업을 병렬로 처리해 전체 시간을 줄일 수 있습니다.
  • 이때 여러 노드가 같은 상태 필드를 갱신한다면 반드시 Reducer로 병합 규칙을 정의해야 합니다.
Recursion Limit은 무엇인가요?
  • 그래프가 실행할 수 있는 최대 단계 수를 제한하는 설정입니다.
  • 순환 구조를 지원하기 때문에, 조건이 잘못되면 같은 노드를 무한히 반복하며 계속 모델을 호출할 수 있습니다.
  • 한도를 넘으면 예외가 발생해 무한 루프와 불필요한 비용을 막아줍니다. 이 예외가 자주 발생한다면 한도를 늘리기보다 종료 조건이 올바른지 먼저 점검해야 합니다.

3.5 Persistence

Checkpointer는 무엇인가요?
  • 그래프가 한 단계를 실행할 때마다 그 시점의 상태를 저장해 주는 구성 요소입니다.
  • 덕분에 실행이 중단되어도 이어서 재개할 수 있고, 이전 대화 내용을 유지할 수 있으며, 과거 시점의 상태를 조회하거나 그 지점부터 다시 실행할 수도 있습니다.
  • 인간 개입이나 오류 복구 같은 기능이 모두 이 저장 기능 위에서 동작합니다.
Thread ID는 어떤 역할을 하나요?
  • 하나의 대화나 실행 세션을 구분하는 식별자입니다.
  • 실행할 때 설정으로 전달하면, 체크포인터가 그 ID에 해당하는 이전 상태를 불러와 이어서 진행합니다.
  • 사용자별로 다른 ID를 쓰면 대화가 서로 섞이지 않고, 같은 ID로 다시 호출하면 이전 맥락이 유지됩니다.
어떤 Checkpointer를 사용해야 하나요?
  • 메모리 기반 체크포인터는 설정이 간단하지만 프로세스가 종료되면 사라지므로 개발과 테스트용입니다.
  • 운영 환경에서는 PostgreSQL이나 Redis 같은 외부 저장소 기반 체크포인터를 사용해야 합니다.
  • 서버를 여러 대로 운영하면 메모리 저장소는 인스턴스 간에 상태가 공유되지 않아, 요청이 다른 서버로 가면 맥락이 끊어집니다.

3.6 Human-in-the-loop

Interrupt는 어떻게 동작하나요?
  • 노드 실행 도중 중단 지점을 만나면 그래프 실행이 멈추고, 그때까지의 상태가 체크포인터에 저장됩니다.
  • 호출한 쪽에는 사람의 입력이 필요하다는 정보가 전달됩니다.
  • 이 기능은 상태를 저장할 수 있어야 성립하므로 체크포인터 설정이 반드시 필요합니다.
중단된 실행은 어떻게 재개하나요?
  • 같은 Thread ID로 다시 호출하면서 사람이 입력한 값을 함께 전달하면, 저장된 상태를 복원해 중단 지점부터 이어서 실행합니다.
  • 이때 주의할 점은 중단이 일어났던 노드가 처음부터 다시 실행된다는 것입니다.
  • 따라서 중단 지점 앞에 외부 시스템을 호출하는 코드가 있으면 중복 실행될 수 있으므로, 그런 작업은 별도 노드로 분리하는 것이 안전합니다.
Human-in-the-loop는 언제 필요한가요?
  • 결제, 데이터 삭제, 외부 발송처럼 되돌리기 어려운 작업을 실행하기 전에 승인을 받아야 할 때 사용합니다.
  • 모델이 만든 결과를 사람이 검토하고 수정한 뒤 이어서 진행하고 싶을 때도 활용합니다.
  • 며칠에 걸쳐 진행되는 승인 절차처럼, 사람의 응답을 기다리는 동안 서버가 계속 대기할 수 없는 장기 실행 워크플로에도 필요합니다.

3.7 Memory

단기 기억과 장기 기억의 차이는 무엇인가요?
  • 단기 기억: 하나의 대화 세션 안에서 유지되는 맥락으로, 주고받은 메시지와 현재 작업 상태가 여기에 해당하며 체크포인터가 담당합니다.
  • 장기 기억: 세션이 끝나도 계속 남아야 하는 정보로, 사용자의 선호나 과거에 학습한 사실이 해당합니다.
  • 단기 기억은 대화가 끝나면 의미가 없지만, 장기 기억은 다음에 다시 만났을 때 활용되어야 한다는 점이 다릅니다.
Checkpoint와 Memory의 차이는 무엇인가요?
  • Checkpoint: 특정 실행의 상태를 그대로 저장한 스냅샷으로, 목적이 "중단된 지점부터 다시 이어가는 것"입니다.
  • Memory: 여러 실행에 걸쳐 재사용할 지식으로, 목적이 "다음에 참고할 정보를 남기는 것"입니다.
  • 체크포인트는 실행 단위로 쌓이고 특정 스레드에 묶이지만, 장기 기억은 스레드와 무관하게 조회됩니다.
Store는 무엇인가요?
  • 스레드를 넘어 유지되는 장기 기억을 저장하는 공간입니다.
  • 네임스페이스와 키로 데이터를 구분해 저장하므로, 사용자별로 기억을 분리해 관리할 수 있습니다.
  • 임베딩 기반 검색을 지원하도록 구성하면, 현재 대화와 의미적으로 관련 있는 기억만 골라 불러올 수 있습니다.
  • 사실이나 지식을 담는 형태와 과거 사례를 담는 형태로 나누어 관리하기도 합니다.

3.8 Subgraph

Subgraph는 왜 사용하나요?
  • 그래프가 커지면 노드와 엣지가 많아져 흐름을 파악하기 어려워집니다. 관련 있는 부분을 하나의 서브그래프로 묶으면 구조가 훨씬 명확해집니다.
  • 자주 쓰이는 처리 흐름을 서브그래프로 만들어 두면 여러 그래프에서 재사용할 수 있습니다.
  • 멀티 에이전트 구조에서 각 에이전트를 서브그래프로 만들고, 상위 그래프가 이들을 조율하는 형태로 설계하기도 합니다.
Subgraph에서 State를 공유하는 방식은 무엇인가요?
  • 부모 그래프와 서브그래프의 상태 스키마에 공통 필드가 있으면, 서브그래프를 노드처럼 바로 추가할 수 있습니다. 공통 필드를 통해 값이 오갑니다.
  • 스키마가 완전히 다르다면 중간에 변환하는 함수를 노드로 두고, 그 안에서 부모 상태를 서브그래프 입력으로 바꿔 호출한 뒤 결과를 다시 부모 상태로 되돌립니다.
  • 후자의 방식은 서브그래프를 독립적으로 유지할 수 있어, 재사용성을 중시할 때 유리합니다.

3.9 Tool Calling

Tool Node는 무엇인가요?
  • 모델이 요청한 도구 호출을 실제로 실행해 주는 노드입니다.
  • 모델의 응답에 담긴 도구 이름과 인자를 읽어 해당 함수를 실행하고, 결과를 도구 응답 메시지 형태로 상태에 추가합니다.
  • 여러 도구를 한 번에 요청한 경우도 함께 처리합니다.
Tool 실행 흐름은 어떻게 되나요?
  • 모델 노드가 실행되어 응답을 생성합니다.
  • 조건부 엣지가 그 응답에 도구 호출이 포함되어 있는지 확인합니다.
  • 포함되어 있으면 도구 노드로 이동해 실행하고, 결과를 메시지에 추가한 뒤 다시 모델 노드로 돌아갑니다.
  • 모델이 도구 결과를 보고 최종 답변을 생성하면 도구 호출이 없으므로 종료로 향합니다. 이 반복이 에이전트의 기본 루프입니다.
Tool 오류는 어떻게 처리하나요?
  • 도구 실행 중 발생한 예외를 그대로 던져 그래프를 중단시키는 대신, 오류 내용을 도구 결과 메시지로 만들어 모델에게 돌려주는 방식이 기본입니다.
  • 그러면 모델이 잘못된 인자를 스스로 수정해 다시 시도할 수 있습니다.
  • 다만 모델이 고칠 수 없는 오류(외부 서비스 장애 등)라면 무한히 재시도할 수 있으므로, 재시도 횟수를 제한하는 장치가 필요합니다.
  • 위험한 도구는 실행 전에 중단을 걸어 사람의 승인을 받도록 구성합니다.

3.10 오류와 복구

Node에서 예외가 발생하면 어떻게 되나요?
  • 해당 지점에서 그래프 실행이 중단되고 예외가 호출한 쪽으로 전달됩니다.
  • 체크포인터를 사용하고 있다면 마지막으로 성공한 단계까지의 상태는 저장되어 있습니다.
  • 따라서 원인을 해결한 뒤 같은 Thread ID로 다시 실행하면, 처음부터가 아니라 실패한 지점부터 이어서 진행할 수 있습니다.
Retry Policy는 무엇인가요?
  • 노드 실행이 실패했을 때 자동으로 다시 시도하도록 설정하는 정책입니다.
  • 재시도 횟수와 간격을 지정할 수 있으며, 일시적인 네트워크 오류나 외부 API의 속도 제한처럼 다시 시도하면 성공할 가능성이 있는 경우에 유용합니다.
  • 반대로 잘못된 입력처럼 재시도해도 결과가 같은 오류에는 적용해도 의미가 없습니다.
왜 Idempotency가 중요한가요?
  • 재시도나 재개가 일어나면 노드가 처음부터 다시 실행됩니다.
  • 그 노드가 결제나 메일 발송처럼 외부에 영향을 주는 작업을 포함하고 있으면 같은 작업이 두 번 수행될 수 있습니다.
  • 이를 막으려면 요청마다 고유한 키를 부여해 이미 처리한 요청인지 확인하거나, 외부 호출을 별도 노드로 분리해 재실행 범위를 최소화해야 합니다.
  • 그래프를 설계할 때 "이 노드가 두 번 실행되어도 괜찮은가"를 기준으로 노드 경계를 나누는 것이 좋습니다.

3.11 설계 판단

일반 Python 코드 대신 LangGraph를 사용하는 이유는 무엇인가요?
  • 상태 관리, 중간 저장과 재개, 스트리밍, 병렬 실행, 인간 개입 같은 기능을 직접 구현하면 상당한 비용이 듭니다. LangGraph는 이를 기본으로 제공합니다.
  • 실행 흐름이 그래프 구조로 드러나 팀원이 로직을 파악하기 쉽고, 각 노드의 입출력을 추적하기도 좋습니다.
  • 반대로 단순히 순서대로 처리하는 파이프라인이라면 일반 코드가 더 간단하고 명확합니다. 반복이나 분기, 중단·재개가 필요할 때 가치가 커집니다.
DAG Workflow와 순환 Graph는 어떻게 다른가요?
  • DAG: 흐름이 한 방향으로만 진행되어 실행 경로와 비용을 미리 예측할 수 있고 디버깅이 쉽습니다.
  • 순환 Graph: 조건에 따라 이전 단계로 돌아갈 수 있어, 결과를 검토하고 다시 시도하는 형태를 표현할 수 있습니다.
  • 대신 몇 번 반복될지 예측하기 어려워 비용과 응답 시간이 유동적이므로, 반복 횟수 제한 같은 안전장치가 필요합니다.
Agent와 고정 Workflow는 어떤 기준으로 선택하나요?
  • 처리 단계가 정해져 있고 입력이 달라져도 흐름이 크게 바뀌지 않는다면 고정 워크플로가 낫습니다. 예측 가능하고 비용도 적습니다.
  • 어떤 도구를 몇 번 써야 할지 미리 알 수 없고 상황에 따라 달라진다면 에이전트가 적합합니다.
  • 실무에서는 전체 골격은 워크플로로 고정하고, 판단이 필요한 일부 구간만 에이전트에게 맡기는 절충안을 많이 사용합니다.
State에는 어떤 데이터를 담아야 하나요?
  • 노드 사이에 실제로 전달되어야 하는 최소한의 데이터만 담는 것이 좋습니다.
  • 상태는 단계마다 직렬화되어 체크포인트로 저장되므로, 큰 데이터를 그대로 담으면 저장 비용과 지연이 커집니다.
  • 대용량 문서나 파일은 식별자나 경로만 상태에 두고 실제 내용은 외부 저장소에서 조회하는 방식이 좋습니다.
  • 특정 노드 안에서만 쓰이고 끝나는 임시 값은 상태에 올릴 필요가 없습니다.
Graph가 지나치게 복잡해지는 문제는 어떻게 관리하나요?
  • 노드 하나가 하나의 책임만 갖도록 나누되, 지나치게 잘게 쪼개면 오히려 흐름을 읽기 어려워지므로 균형이 필요합니다.
  • 관련 있는 구간은 서브그래프로 묶어 상위 그래프를 단순하게 유지합니다.
  • 분기 조건을 노드 내부에 숨기지 말고 조건부 엣지로 드러내면 그래프 구조만 봐도 흐름을 파악할 수 있습니다.
  • 상태 필드가 계속 늘어난다면 책임이 뒤섞이고 있다는 신호이므로 구조를 다시 점검하는 것이 좋습니다.
디버깅과 관찰 가능성은 어떻게 확보하나요?
  • LangSmith 같은 추적 도구를 연동하면 각 노드의 입력과 출력, 모델 호출 내용, 토큰 사용량과 소요 시간을 단계별로 확인할 수 있습니다.
  • LLM은 같은 입력에도 다른 결과를 낼 수 있어 재현이 어렵기 때문에, 실행 기록을 남겨두는 것이 특히 중요합니다.
  • 체크포인터에 저장된 상태 이력을 조회하면 어느 단계에서 값이 잘못되었는지 되짚어 볼 수 있습니다.
  • 문제가 된 지점의 상태를 수정해 그 시점부터 다시 실행하면, 전체를 처음부터 돌리지 않고도 원인을 좁혀갈 수 있습니다.