@ExceptionHandler 어노테이션은 무엇인가요?@ResponseBody(or ResponseEntity<T>)가 있을 때와 없을 때의 동작 방식의 차이점을 말해주세요.Filter와 Interceptor의 차이점을 말해주세요.Spring MVC의 실행 흐름에 대해 설명해주세요.@Controller 와 @RestController 의 차이점을 설명해주세요.ControllerAdvice에 대해 설명해주세요.RequestBody VS ModelAttribute의 차이점을 말해주세요.톰캣에 대해서 설명해주세요.AutoConfiguration 동작 원리를 설명해주세요.Spring과 Spring Boot의 차이를 말해주세요.JPA를 사용하는 이유를 설명해주세요.JPA, Hibernate, Spring Data JPA 의 차이가 무엇인가요?Spring Data JPA에서 새로운 Entity인지 판단하는 방법은 무엇일까요?JPA의 ddl-auto 옵션은 각각 어떤 동작을 하고 어떤 상황에서 사용해야 할까요?엔티티 매니저에 대해 설명해주세요.JPA의 N + 1 문제에 대해서 설명해주세요.자바에서 Checked Exception과 Unchecked Exception에 대해서 설명해주세요.JPA에서 ID 생성 전략에 대해 설명해주세요.private 메서드에 @Transactional 선언하면 트랜잭션이 동작할까요?
@ExceptionHandler 어노테이션은 무엇인가요?
- @ExceptionHandler란?
@ExceptionHandler 애너테이션은 Spring MVC에서 컨트롤러(@Controller)나 전역 예외 처리를 위한 @ControllerAdvice 클래스의 메서드에서 발생하는 예외를 처리하는 데 사용되는데요. 이 애너테이션은 특정 예외를 처리하는 메서드를 지정하거나 메서드의 파라미터로 처리할 예외를 설정할 수 있습니다.- 어떤 방식으로 동작하나요? 🤔
Spring MVC 애플리케이션에서 예외가 발생하면, DispatcherServlet이 적절한
HandlerExceptionResolver를 찾아 예외를 처리합니다. Spring에 기본적으로 등록된
HandlerExceptionResolver는 세 가지가 있으며, 각 리졸버는 우선순위에 따라 예외를 처리합니다. 그 중
ExceptionHandlerExceptionResolver가 가장 먼저 동작하며, 발생한 예외가
@ExceptionHandler에 등록되어 있는지 확인합니다. 만약 처리할 수 없는 예외라면 다음 리졸버로 넘어갑니다. ExceptionHandlerExceptionResolver의 특징은 예외가 WAS로 던져지지 않고 직접 처리된다는 것입니다.이렇게 함으로써 예외가 발생했을 때 적절한 방법으로 처리되어 사용자에게 친화적인 에러 메시지를 제공하거나 로깅 등의 추가 작업을 수행할 수 있습니다.
@ResponseBody(or ResponseEntity<T>)가 있을 때와 없을 때의 동작 방식의 차이점을 말해주세요.
- @ResponseBody(or ResponseEntity<T>)가 있을 때와 없을 때 차이점
@ResponseBody 혹은 ResponseEntity<T> 반환을 사용한다면, 스프링은 컨트롤러에서 반환된 값을 HTTP 응답 본문에 직접 씁니다.
이때 자바 객체를 자동으로 JSON이나 XML 등의 타입으로 직렬화합니다. 만약, 없는 경우에는 스프링은 반환값을 뷰 이름으로 해석합니다.
뷰 이름으로 해석한 이후에, 뷰 리졸버를 사용해 뷰를 찾고 응답합니다.(뷰에 전달할 모델이 있다면, 이를 뷰에 전달하고 응답합니다.)- @ResponseBody와 ResponseEntity<T> 반환 중 어떤 방식이 더욱 좋나요? 😀
@ResponseBody를 사용하는 경우, 코드를 간결하게 유지할 수 있습니다. 하지만, 상태코드와 헤더를 유연하게 변경하기는 어렵습니다. 반면, ResponseEntity<T> 반환의 경우 상태코드와 헤더를 유연하게 변경할 수 있으나 작성할 코드량이 증가한다는 단점이 있습니다. 팀 상황에 맞게 적절한 방법을 사용하는 것이 중요하다고 생각합니다.Filter와 Interceptor의 차이점을 말해주세요.
- Filter
Filter는 요청 및 응답의 전처리와 후처리를 수행하고 서블릿 컨테이너에 의해 실행되는 Java 클래스입니다. 주로 요청 로깅, 인증, 인코딩 설정, CORS 처리, 캐싱, 압축 등의 공통 기능을 구현하는 데 사용됩니다.- 특징
Filter는 서블릿 컨테이너(예: Tomcat) 수준에서 동작합니다. 모든 요청이 서블릿으로 전달되기 전에Filter를 거칩니다.
- 생명 주기: Filter는
doFilter메서드를 통해 요청 및 응답을 처리합니다. FilterChain을 통해 다음 필터 또는 최종 서블릿으로 요청을 전달합니다.
- 순서:
web.xml이나@WebFilter애노테이션을 통해 설정할 수 있으며, 필터의 순서는 설정 파일에서 정의합니다.
- Interceptor
Interceptor는 특정 핸들러 메서드 실행 전후에 공통 기능을 구현합니다. 주로 요청 로깅, 인증, 권한 검사, 세션 검사, 성능 모니터링 등을 수행하는 데 사용됩니다.
- 특징
- Interceptor는 Spring MVC의 핸들러 수준에서 동작합니다. Dispatcher Servlet이 컨트롤러를 호출하기 전에 Interceptor를 거칩니다.
- 생명 주기
preHandle메서드: 컨트롤러의 메서드가 호출되기 전에 실행됩니다.postHandle메서드: 컨트롤러의 메서드가 실행된 후, 뷰가 렌더링되기 전에 실행됩니다.afterCompletion메서드: 뷰가 렌더링된 후 실행됩니다.
- 순서:
WebMvcConfigurer를 구현한 클래스에서addInterceptors메서드를 사용하여 설정합니다. 인터셉터의 순서는 등록 순서에 따릅니다.
- (Servlet) Filter vs (Handler) Interceptor
Characteristics | (Servlet) Filter | (Handler) Interceptor |
Definition | HTTP 요청이나 응답이 수신될 때마다 서블릿 컨테이너는 Java 클래스 Filter를 실행합니다. | 인터셉터는 Spring Context에 대한 액세스를 통한 사용자 정의 사후 처리와 핸들러 실행을 금지할 가능성이 있는 사용자 정의 사전 처리만 허용합니다. |
Interface | jakarta.servlet.Filter | HandlerInterceptor |
실행 순서 | 서블릿 이전/이후, 서블릿 필터 | 컨트롤러 이전이나 이후에는 스프링 인터셉터가 필터 이후까지 작동하지 않습니다. |
Level of operation | 서블릿 수준에서 작동 | 컨트롤러 수준에서 작동 |
Method | Interceptor의 postHandle에 비해 Filter의 doFilter 기술은 훨씬 더 유연합니다. 요청이나 응답을 수정하거나, FilterChain인으로 전달하거나, 요청 처리를 중지할 수도 있습니다. | 실제 대상 "핸들러"에 액세스할 수 있으므로 HandlerInterceptor는 필터보다 더 정확한 제어를 제공합니다. 핸들러 메소드의 annotation status도 확인할 수 있습니다. |
Spring MVC의 실행 흐름에 대해 설명해주세요.
[View를 응답하는 경우]
이미지 출처 : www.egovframe.go.kr
- 클라이언트로부터 HTTP 요청이 들어옵니다. 이때 DispatcherServlet이 프론트 컨트롤러의 역할을 수행합니다.
- HandlerMapping을 통해 URL에 매핑된 핸들러를 조회합니다.
- DispatcherServlet은 찾은 핸들러를 실행하기 위해 HandlerAdapter를 사용합니다.
- HandlerAdapter가 실제로 요청을 처리하는 메서드를 호출합니다.
- 컨트롤러는 결과 데이터를 Model 객체에 담고, View 이름을 반환합니다.
- ViewResolver는 View 이름을 기반으로 적절한 뷰를 찾습니다.
- ViewResolver가 찾은 뷰를 사용해 최종적으로 HTML과 같은 응답을 생성합니다.
[Message Converter의 동작]
JSON과 문자열 등을 응답하는 경우는 View를 응답하는 경우와 유사한 흐름으로 동작합니다.
단, ViewResolver 대신 HttpMessageConverter 가 동작합니다.
MessageConverter는 HTTP 요청과 응답 두 경우 모두 사용될 수 있습니다.
이때 클라이언트의 HTTP Accept 헤더와 반환 타입 정보, Content-Type 등을 조합하여 타입에 맞는 HttpMessageConverter가 선택됩니다.
- 클라이언트로부터 HTTP 요청이 들어옵니다. 이때 DispatcherServlet이 Front Controller의 역할을 수행합니다.
- HandlerMapping을 통해 URL에 매핑된 핸들러(컨트롤러)를 조회합니다.
@RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter가 ArgumentResolver를 호출해 핸들러가 필요로 하는 파라미터의 값을 생성하고, 컨트롤러 메서드를 호출하면서 값을 넘겨줍니다. 이때 ArgumentResolver가 HttpMessageConverter를 사용해 필요한 객체를 생성합니다.
- 컨트롤러는 전달받은 파라미터를 사용하여 서비스 계층과 데이터 접근 계층을 호출해 비즈니스 로직을 수행합니다.
@ResponseBody와HttpEntity를 처리하는 ReturnValueHandler가 HTTPMessageConverter를 호출해 응답 결과를 만들어냅니다.
@Controller 와 @RestController 의 차이점을 설명해주세요.
이 두 어노테이션의 주요 차이점은 HTTP 응답의 처리 방식에 있습니다.
@Controller
주로 뷰(View)를 반환하는 컨트롤러를 정의할 때 사용됩니다.
메서드가 반환하는 값은 뷰 리졸버(View Resolver)에 의해 해석되어 JSP, Thymeleaf 등과 같은 템플릿 엔진을 통해 HTML을 생성합니다.
@RestController
주로 RESTful 웹 서비스 API를 정의할 때 사용됩니다.
메서드가 반환하는 값은 자동으로 JSON 또는 XML 형식으로 변환되어 HTTP 응답 본문에 포함됩니다. 이는
@Controller와 @ResponseBody의 결합된 형태입니다.ControllerAdvice에 대해 설명해주세요.
@ControllerAdvice는 모든 컨트롤러에 대해 전역 기능을 제공하는 애너테이션입니다.
@ControllerAdvice가 선언된 클래스에 @ExceptionHandler, @InitBinder, @ModelAttribute를 등록하면 예외 처리, 바인딩 등을 한 곳에서 처리할 수 있어, 코드의 중복을 줄이고 유지보수성을 높일 수 있습니다.
@ControllerAdvice는 내부에 @Component가 포함되어 있어 컴포넌트 스캔 과정에서 빈으로 등록됩니다. @RestControllerAdvice는 내부에 @ResponseBody를 포함하여 @ExceptionHandler와 함께 사용될 때 예외 응답을 Json 형태로 내려준다는 특징이 있습니다.RequestBody VS ModelAttribute의 차이점을 말해주세요.
이들은 클라이언트 측에서 보낸 데이터를 Java 객체로 만들어주는데
RequestBody 는
요청의 본문(Body)에 있는 값을 바인딩할 때 사용하고,
ModelAttribute 는 요청 파라미터나 multipart/form-data 형식을 바인딩할 때 사용합니다.- RequestBody
- 클라이언트가 보내는 요청의 본문을 자바 객체로 변환합니다.
- 내부적으로 HttpMessageConverter를 거치는데, 이때 ObjectMapper를 통해 JSON 값을 java 객체로 역직렬화합니다.
- 따라서 변환될 java 객체에 기본 생성자를 정의해야 하고, getter나 setter를 선언해야 합니다. 참고
- cf. record에 기본 생성자를 따로 정의하지 않았는데 역직렬화가 되는 이유
- record 는 기본생성자를 자동으로 제공하지 않는 대신, ’모든 필드를 초기화하는 생성자’를 제공합니다.
- jackson 은 일반 객체와 달리, record를 역직렬화할 때는 ’모든 필드를 초기화하는 생성자’를 사용해 역직렬화하기 때문입니다.
- ModelAttribute
- 두가지 사용법이 있습니다.
- 첫번째 사용법인 메서드 단에서의 사용법은 jsp의 Model에 하나 이상의 속성을 추가하고 싶을 때 사용합니다.
- e.g.
model.addAttribute(“속성 이름”, “속성 값”) - 두번째 사용법인 인자 단에서의 사용으로 클라이언트가 보내는 요청의 파라미터나 multipart/form-data 형식의 데이터를 자바 객체로 변환합니다.
- 내부적으로 ModelAttributeMethodProcessor를 거치는데, 이때 지정된 클래스의 생성자를 찾아 객체로 변환합니다.
톰캣에 대해서 설명해주세요.
- Tomcat
웹 서버와 웹 컨테이너의 결합한 형태입니다. 현재 가장 일반적이고 많이 사용되는 WAS입니다. 컨테이너, 웹 컨테이너,
서블릿 컨테이너라고도 부릅니다. JSP와 서블릿 처리, 서블릿의 수명 주기 관리, 요청 URL을 서블릿 코드로 매핑, HTTP
요청 수신 및 응답, 필터 체인 관리 등을 처리해줍니다.
- 서블릿이 무엇인가요? 🤔
서블릿은 자바를 이용해 웹 서비스를 만들기 위한 스펙입니다. 클라이언트가 프로그램으로 요청을 보내면 그 요청에 대한 결과를 응답해주기 위해서 사용됩니다. 서블릿은 다음과 같이 동작합니다.
- 사용자가 URL을 입력하면 사용자의 요청이 서블릿 컨테이너로 전송됩니다.
- 요청을 받은 컨테이너는 HttpServletRequest, HttpServletResponse를 생성합니다.
- 서블릿 매핑 정보를 이용해 사용자가 요청한 경로를 처리할 수 있는 서블릿을 찾습니다.
- 서블릿의 service 메서드를 호출하고 HTTP 메서드 여부에 따라서 doGet(), doPost()를 호출합니다.
- 각 메서드는 요청을 처리하고 HttpServletResponse 객체를 이용해 응답을 처리합니다.
- 서블릿의 생명주기는 어떻게 되나요? 🤓
사용자의 요청이 들어오면 서블릿 컨테이너가 서블릿이 존재하는지 확인하고 없는 경우 init() 메서드를 호출하여 생성합니다.
이후 요청은 service() 메서드를 실행합니다. 만약 서블릿에 종료 요청이 들어오는 경우에는 destroy() 메서드를
호출합니다.
AutoConfiguration 동작 원리를 설명해주세요.
AutoConfiguration의 시작은
@SpringBootApplication 어노테이션 안에 있는@EnableAutoConfiguration 이라는 애노테이션입니다.@EnableAutoConfiguration은 @Import(AutoConfigurationImportSelector.class)를 통해 자동 구성 클래스를 가져옵니다.@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; Class<?>[] exclude() default {}; String[] excludeName() default {}; }
자동 구성 클래스를 가져올 때는 AutoConfigurationImportSelector 클래스의
selectImports(AnnotationMetadata annotationMetadata) 라는 메서드를 이용하고, getAutoConfigurationEntry(AnnotationMetadata annotationMetadata); 메서드를 통해 Import할 클래스가 무엇인지 알 수 있게 됩니다.- 간단한 메서드 동작 과정 설명
- getCandidateConfigurations(annotationMetadata, attributes); - AutoConfiguration의 후보들을 가져온다.
- removeDuplicates(configurations); - 중복을 제거한다.
- getExclusions(annotationMetadata, attributes); - 자동 설정에서 제외되는 설정에 대한 정보를 가져온다.
- configurations.removeAll(exclusions); - 제외되는 설정을 제거한다.
- getConfigurationClassFilter().filter(configurations); - 필터를 적용한다.
Spring과 Spring Boot의 차이를 말해주세요.
Spring은 Spring Framework의 핵심 모듈들을 기반으로 한 프레임워크로 엔터프라이즈 애플리케이션 개발을 지원하기 위한 대규모 오픈 소스 프로젝트입니다.
Spring Framework를 사용하기 위해서는 설정 파일 작성을 통한 스프링 컨테이너 구성, 필요한 빈 객체 등록 및 의존성 설정, 데이터베이스 연결, 트랜잭션 관리 등 다양한 설정을 개발자가 직접 수동으로 구성해야 했습니다.
따라서 프로젝트 초기화 과정에서 많은 설정과 의존성을 추가하게 되며 프로젝트는 시작하는데 시간이 많이 걸렸습니다. 또한 스프링을 통해 웹 애플리케이션을 구축하기 위해서는 별도의 WAS를 설치하고 설정해야 했습니다.
Spring Boot는 Spring의 문제점을 해결해주고, 더 쉽고 빠르게 스프링 애플리케이션을 개발할 수 있도록 해주는 도구입니다.
Spring Boot를 사용하면 Spring에서 제공하는 여러 기능들을 자동으로 설정하여 개발자가 보다 쉽게 사용할 수 있도록 해줍니다.
- Spring Boot의 주요 특징
- 자동 설정(Auto Configuration)
- Spring Boot는 애플리케이션의 설정을 자동으로 구성합니다.
- @EnableAutoConfiguration, @SpringBootApplication 어노테이션을 통해 자동 설정을 활성화합니다.
- 의존성 관리 간소화
- 특정 기능을 쉽게 추가할 수 있도록 여러 개의 라이브러리와 의존성을 하나의 패키지로 묶어 제공하는 starter 의존성 통합 모듈을 제공합니다.
- 예: spring-boot-starter-web, spring-boot-starter-data-jpa, spring-boot-starter-security
- 내장 서버
- Tomcat, Jetty, Undertow와 같은 내장 웹 서버를 제공하여, 애플리케이션을 독립 실행형 JAR 파일로 배포하고, 바로 실행할 수 있게 합니다.
- 배포를 위해 War 파일을 생성해서 Tomcat에 배포할 필요 없으며, JAR 파일에는 모든 의존성 라이브러리가 포함되어 있어 외부 서버 없이도 애플리케이션을 실행할 수 있습니다.
JPA를 사용하는 이유를 설명해주세요.
데이터 액세스 기술을 사용하는 Spring 기반 애플리케이션을 더 쉽게 구축할 수 있습니다.
애플리케이션에 대한 데이터 액세스 계층을 구현하는 것은 상당히 번거로울 수 있습니다. 가장 간단한 쿼리를 실행하려면 너무
많은 상용구 코드를 작성해야 합니다. 페이지 매김, Auditing, 기타 자주 필요한 옵션을 추가하면 결국 길을 잃게 됩니다.
Spring Data JPA는 실제로 필요한 만큼의 노력으로 데이터 액세스 계층의 구현을 크게 개선하는 것을 목표로 합니다.
개발자는 다양한 기술을 사용하여 리포지토리 인터페이스를 작성하면 Spring이 자동으로 이를 연결해 줍니다. 심지어 사용자 정의
파인더를 사용하거나 예제를 통해 쿼리를 작성하면 Spring이 쿼리를 작성해줍니다.
- 더 이상 DAO 구현이 필요하지 않습니다. 인터페이스를 확장함으로써 표준 DAO에서 사용할 수 있는 표준 데이터 액세스에 가장 적합한 CRUD 방법을 얻을 수 있습니다.
- 사용자 정의 액세스 메서드 및 쿼리
- 인터페이스에서 새로운 메소드를 정의하기만 하면 됩니다.
@Query주석을 사용하여 JPQL 쿼리 제공- Spring Data의 고급 사양 및 Querydsl 지원을 사용
- JPA Named 쿼리를 통해 사용자 정의 쿼리 사용
- Automatic 사용자 정의 쿼리: 정의된 모든 메서드를 분석하고 메서드 이름에서 쿼리를 자동으로 생성 하려고 시도합니다
- Transaction Configuration: 클래스 수준에서 읽기 전용
@Transactional주석을 사용하고, 읽기 전용이 아닌 메서드에 대해 재정의됩니다.
JPA, Hibernate, Spring Data JPA 의 차이가 무엇인가요?
- JPA는 기술 명세입니다. 자바 애플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스입니다. JPA는 단순한 명세이기 때문에 인터페이스와 규약만 정의하며, 실제 구현체는 제공하지 않습니다.
- Hibernate는 JPA의 구현체 중 하나입니다.
JPA가 정의한
javax.persistence.EntityManager와 같은 인터페이스를 직접 구현한 라이브러리입니다. JPA의 구현체 중 하나일 뿐이므로, DataNucleus, EclipseLink 등 다양한 JPA 구현체로 대체할 수 있습니다.
- Spring Data JPA는 JPA를 쉽게 사용할 수 있도록 지원하는 모듈입니다. JPA를 한 단계 추상화시킨 Respository 라는 인터페이스를 제공합니다. 개발자가 Respository 인터페이스에 정해진 규칙대로 메서드를 만들어주기만 하면, 해당 메서드 이름에 적합한 쿼리를 날리는 구현체를 만들어 자동으로 Bean으로 등록해줍니다.
Spring Data JPA는 JPA를 기반으로 하며, 반복적인 코드 작성을 줄이고 데이터 접근 계층을 단순화 합니다.
이 때 JPA를 추상화 했다는 의미는, Spring Data JPA의 Repository 의 구현에서 JPA를 사용하고 있다는 것입니다.
예를 들어 Respository 인터페이스의 기본 구현체인
SimpleJpaResporitory 는 내부적으로 EntityManager 를 사용합니다.Spring Data JPA에서 새로운 Entity인지 판단하는 방법은 무엇일까요?
@Override public boolean isNew(T entity) { if(versionAttribute.isEmpty() || versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) { return super.isNew(entity); } BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity); return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true); }
새로운 Entity인지 여부는 JpaEntityInformation의
isNew(T entity)에 의해 판단됩니다. 다른 설정이 없으면 JpaEntityInformation의 구현체 중 JpaMetamodelEntityInformation 클래스가 동작합니다. @Version이 사용된 필드가 없거나 @Version이 사용된 필드가 primitive 타입이면 AbstractEntityInformation의 isNew()를 호출합니다.
@Version이 사용된 필드가 wrapper class이면 null여부를 확인합니다.public boolean isNew(T entity) { Id id = getId(entity); Class<ID> idType = getIdType(); if (!idType.isPrimitive()) { return id == null; } if (id instanceof Number) { return ((Number) id).longValue() == 0L; } throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType)); }
@Version이 사용된 필드가 없어서 AbstractEntityInformation 클래스가 동작하면
@Id 어노테이션을 사용한 필드를 확인해서 primitive 타입이 아니라면 null 여부, Number의 하위 타입이면 0인지 여부를 확인합니다.@GeneratedValue 어노테이션으로 키 생성 전략을 사용하면 데이터베이스에 저장될 때 id가 할당됩니다. 따라서 데이터베이스에 저장되기 전에 메모리에서 생성된 객체는 id가 비어있기 때문에 isNew()는 true가 되어 새로운 entity로 판단합니다.- 직접 ID를 할당하는 경우에는 어떻게 동작하나요? 🤔
키 생성 전략을 사용하지 않고 직접 ID를 할당하는 경우 새로운 entity로 간주되지 않습니다.
이 때는 엔티티에서
Persistable<T> 인터페이스를 구현해서 JpaMetamodelEntityInformation 클래스가 아닌 JpaPersistableEntityInformation의 isNew()가 동작하도록 해야 합니다.public class JpaPersistableEntityInformation<T extends Persistable<ID, ID>extends JpaMetamodelEntityInformation<T, ID> { public JpaPersistableEntityInformation(Class<T> domainClass, Metamodel metamodel, PersistenceUnitUtil persistenceUnitUtil) { super(domainClass, metamodel, persistenceUnitUtil); } @Override public boolean isNew(T entity) { return entity.isNew(); } @Nullable @Override public ID getId(T entity) { return entity.getId(); } }
- 새로운 Entity인지 판단하는게 왜 중요할까요? 🤓
@Override @Transactional public <S extends T> S save(S entity) { Assert.notNull(entity, "Entity must not be null"); if (entityInformation.isNew(entity)) { entityManager.persist(entity); return entity; } else { return entityManager.merge(entity); } }
SimpleJpaRepository의
save() 메서드에서 isNew()를
사용하여 persist를 수행할지 merge를 수행할지 결정합니다. 만약 ID를 직접 지정해주는 경우에는 신규 entity라고
판단하지 않기 때문에 merge를 수행합니다. 이때 해당 entity는 신규임에도 불구하고 DB를 조회하기 때문에 비효율적입니다.
따라서, 새로운 entity인지 판단하는 것은 중요한 부분입니다.JPA의 ddl-auto 옵션은 각각 어떤 동작을 하고 어떤 상황에서 사용해야 할까요?
ddl-auto 옵션은 스프링 부트 애플리케이션에서 Hibernate와 같은 JPA 구현체를 사용할 때 데이터베이스 스키마 관리를 제어하는 설정입니다. 이 옵션은
application.properties 또는 application.yml 파일에서 설정할 수 있으며, 다양한 값에 따라 데이터베이스 스키마에 대해 다른 동작을 수행합니다. ddl-auto 옵션에는 none, validate, update, create, create-drop 등이 존재합니다.- 각 옵션에 대한 설명을 해주시겠어요? 🤔
none으로 설정하면 데이터베이스 스키마와 관련된 어떠한 작업도 수행하지 않습니다. 데이터베이스 스키마를 수동으로 관리하고 싶을 때 유용하며, 프로덕션 환경에서 주로 사용됩니다.validate는 애플리케이션이 시작될 때, 엔티티 매핑이 데이터베이스 스키마와 일치하는지 검증하며 스키마 변경은 따로 수행하지 않습니다. 프로덕션 환경에서 엔티티와 데이터베이스 스키마가 일치하는지 확인하고 싶을 때 사용합니다.update는 엔티티 매핑과 데이터베이스 스키마를 비교하여 필요한 경우 스키마를 업데이트합니다. 기존
데이터는 유지되지만, 새로운 엔티티나 변경된 엔티티 필드는 스키마에 반영됩니다. 해당 옵션은 엔티티에 변경이 발생할 때 자동으로
스키마를 업데이트하고 싶을 때 유용합니다. 프로덕션 환경에서는 예기치 않은 스키마 변경을 방지하기 위해 주의가 필요합니다.create는 애플리케이션이 시작될 때 기존 스키마를 삭제하고 새로 생성합니다. 데이터가 모두
삭제되며 엔티티 매핑을 기반으로 새로운 스키마가 생성됩니다. 개발 초기에 빈 데이터베이스 스키마를 반복적으로 생성해야 할 때
유용합니다. 기존 데이터가 모두 삭제되므로 프로덕션 환경에서는 사용하지 않습니다.create-drop은 create와 유사하지만, 애플리케이션이 종료될 때
스키마를 삭제한다는 점이 다릅니다. 해당 옵션은 테스트 환경에서 일시적인 데이터베이스 스키마가 필요한 경우 유용하며, 매 테스트
실행 시마다 깨끗한 데이터베이스 상태를 유지하고자 할 때 사용됩니다. 프로덕션 환경에서는 사용하지 않습니다.- 프로덕션 환경에서 스키마 변경은 어떻게 해야하나요? 🤓
스키마 변경이 필요할 때는 적절한 데이터베이스 마이그레이션 도구(Flyway, Liquibase 등)를 사용하여 제어된
방식으로 스키마를 관리하거나, 사용자가 없는 새벽에 스키마 변경 작업을 수동으로 진행하는 것이 더욱 안전할 수 있습니다.
엔티티 매니저에 대해 설명해주세요.
엔티티 매니저에 대해 알기 위해선 영속성 컨텍스트에 대해 알아야 합니다. 영속성 컨텍스트는 엔티티를 영구 저장하는 환경으로 1차 캐싱, 쓰기 지연, 변경 감지를 통해
영속 로직을 효율적으로 할 수 있게 해줍니다. 이러한 효율적인 영속 로직 수행을 위해서 엔티티는 영속성 컨텍스트에 관리되어야 합니다.
이런 작업을 도와주는 것이 바로 엔티티 매니저입니다. 엔티티 매니저는 엔티티의 상태를 변경하고, 영속성 컨텍스트와 상호작용함으로써 영속 로직을 수행하는 역할을 가지고 있습니다.
- 조금 더 구체적으로 엔티티 매니저의 역할을 설명해 주실 수 있을까요? 🤔
엔티티는 영속성 컨텍스트와 관련하여 4가지 상태(비영속, 영속, 준영속, 삭제)를 가질 수 있는데요. 엔티티 매니저는
persist, merge, remove, close 메서드를 이용하여 엔티티의 상태를 변경할 수 있습니다. 또한, 엔티티
매니저는 영속성 컨텍스트의 1차 캐시로부터 엔티티를 조회할 수 있으며, 쓰기 지연 저장소에 있는 쿼리들을 flush하여 DB와
동기화시킬 수 있습니다. 또한 JPQL이나 Native Query를 이용해 직접 DB로부터 데이터를 불러올 수도 있습니다.
- 엔티티의 각 상태에 대해서 설명해주세요. 😀
Member member = new Member("산초");
비영속 상태는 엔티티 객체가 새로 생성되었지만, 아직 영속성 컨텍스트와 연관되지 않은 상태입니다. 이 상태에서는 데이터베이스와 전혀 관련이 없으며, 엔티티 객체는 메모리 상에만 존재합니다.
em.persist(member); em.merge(detagedMember); em.find(Member.class, 1L);
영속 상태는 엔티티 객체가 영속성 컨텍스트에 관리되고 있는 상태입니다. 이 상태에서는 엔티티의 변경 사항이 자동으로 데이터베이스에 반영됩니다.
em.detach(member); em.clear(); em.close();
준영속 상태는 엔티티 객체가 한 번 영속성 컨텍스트에 의해 관리되었지만, 현재는 영속성 컨텍스트와 분리된 상태입니다. 이 상태에서는 엔티티 객체의 변경 사항이 더 이상 데이터베이스에 반영되지 않습니다.
영속성 컨텍스트 종료, 트랜잭션 종료 등으로도 준영속 상태로 전환됩니다.
em.remove(member);
삭제 상태는 엔티티 객체가 영속성 컨텍스트에서 제거된 상태입니다. 이 상태에서는 엔티티 객체가 데이터베이스에서 삭제됩니다.
JPA의 N + 1 문제에 대해서 설명해주세요.
JPA N + 1 문제는 연관 관계가 설정된 엔티티를 조회할 경우에, 조회된 데이터 개수(N)만큼 연관관계의 조회 쿼리가
추가로 발생하는 현상입니다. 예를 들어, 블로그 게시글과 댓글이 있는 경우, 게시글을 조회한 후 각 게시글마다 댓글을 조회하기
위한 추가 쿼리가 발생할 수 있습니다. 이를 N + 1 문제라고 합니다.
- findAll 메서드의 글로벌 패치 전략 별 N + 1 문제 상황에 대해서 설명해주세요. 🤓
spring data jpa에서 제공하는 repository의 findAll 메서드입니다!
글로벌 패치 전략을 즉시로딩으로 설정하고 findAll()을 실행하면 N + 1 문제가 발생합니다. 이는 findAll()은
select u from User u라는
JPQL 구문을 생성해서 실행하기 때문입니다. JPQL은 글로벌 패치 전략을 고려하지 않고 쿼리를 실행합니다. 모든 User를
조회하는 쿼리 실행 후, 즉시로딩 설정을 보고 연관관계에 있는 모든 엔티티를 조회하는 쿼리를 실행합니다.글로벌 패치 전략을 지연 로딩으로 설정하고 findAll()을 실행하면 N + 1 문제가 발생하지 않습니다. 이는 연관관계에
있는 엔티티를 실제 객체 대신에 프록시 객체로 생성하여 주입하기 때문입니다. 하지만 프록시 객체를 사용할 경우에 실제 데이터가
필요하여 조회하는 쿼리가 발생하고 N + 1 문제가 발생할 수 있습니다.
- N + 1 문제는 어떻게 해결할 수 있을까요? 🤔
N + 1 문제를 해결하기 위해서는
fetch join, @EntityGraph를 사용해 볼 수 있습니다. fetch join은 연관 관계에 있는 엔티티를 한번에 즉시 로딩하는 구문입니다. @EntityGraph도 비슷한 효과를 만들어내며, 쿼리 메서드에 해당 어노테이션을 추가해 사용할 수 있습니다.select distinct u from User u left join fetch u.posts
@EntityGraph(attributePaths = {"posts"}, type = EntityGraphType.FETCH) List<User> findAll();
자바에서 Checked Exception과 Unchecked Exception에 대해서 설명해주세요.
Checked Exception은 컴파일 시점에 확인되며, 반드시 처리해야 하는 예외입니다. 자바에서는
IOException, SQLException 등이 이에 속합니다. Checked Exception을 유발하는 메서드를 호출하는 경우, 메서드 시그니처에 throws를 사용하여 호출자에게 예외를 위임하거나 메서드 내에서 try-catch를 사용하여 해당 예외를 반드시 처리해야합니다.Unchecked Exception은 런타임 시점에 발생하는 예외로, 컴파일러가 처리 여부를 강제하지 않습니다. 자바에서는
RuntimeException을 상속한 예외들이 해당됩니다. 일반적으로 프로그래머의 실수나 코드 오류로 인해 발생합니다.- 각각 언제 사용해야 할까요? 🤔
정답이 없는 영역이라고 생각해요. 자신의 주관을 만들면서 학습해봐도 좋을 것 같아요!
Checked Exception은 외부 환경과의 상호작용에서 발생할 가능성이 높은 예외에 적합합니다. 예를 들어, 파일
입출력, 네트워크 통신 등에서 발생할 수 있는 예외는 Checked Exception으로 처리하는 것이 좋습니다. 이러한 예외는
예측 가능하며, 호출하는 쪽에서 적절히 처리할 수 있는 여지가 있습니다.
Uncheked Exception은 코드 오류, 논리적 결함 등 프로그래머의 실수로 인해 발생할 수 있는 예외에 적합합니다.
예를 들어, null 참조 또는 잘못된 인덱스 접근 등은 호출자가 미리 예측하거나 처리할 수 없기 때문에 Unchecked Exception으로 두는 것이 좋습니다.
- Error와 Exception의 차이는 무엇인가요? 🤓
Error는 주로 JVM에서 발생하는 심각한 문제로,
OutOfMemoryError, StackOverflowError 등 시스템 레벨에서 발생하는 오류입니다. 이는 일반적으로 프로그램에서 처리하지 않으며, 회복이 어려운 오류에 속하며, 애플리케이션 코드에서 복구할 수 없는 심각한 문제를 나타냅니다.반면, Exception은 프로그램 실행 중 발생할 수 있는 오류 상황을 나타냅니다. 대부분의 경우 회복 가능성이 있으며, 프로그램 내에서 예외 처리를 통해 오류 상황을 제어할 수 있습니다. Exception은 다시
Checked Exception과 Unchecked Exception으로 나눌 수 있습니다.JPA에서 ID 생성 전략에 대해 설명해주세요.
JPA에서 ID를 생성하기 위해서는 직접 할당과 자동 할당을 사용할 수 있습니다. 직접 할당은
@Id어노테이션만을 사용하여 Id값을 직접 할당하는 방식입니다. 반면, 자동 할당은 @Id와 @GeneratedValue를 함께 사용해서 원하는 키 생성 전략을 선택하는 방식입니다. @GeneratedValue의 stretagy 옵션을 통해 생성 전략을 설정할 수 있는데, 여기에 올 수 있는 값인 GenerationType는 다음과 같습니다.@Target({ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface GeneratedValue { GenerationType strategy() default GenerationType.AUTO; String generator() default ""; } public enum GenerationType { AUTO, IDENTITY, SEQUENCE, TABLE }
- 자동 생성 방식을 사용할 때 각 전략에 대해서 설명해주세요. 🤔
IDENTITY 전략은 기본 키 생성을 DB에 위임하는 전략입니다. 주로 MySQL,
PostgreSQL, SQL Server, DB2에서 사용됩니다. 해당 전략을 사용하면 엔티티를 생성할 때 쓰기 지연이 적용되지
않습니다. 왜냐하면 JPA에서 엔티티를 영속하기 위해선 식별자가 필요한데, IDENTITY 전략에서는 이 식별자가 DB에
저장되어야 할당되기 때문입니다. 따라서 엔티티를 생성할 때 즉시 INSERT 쿼리가 실행되어야 합니다. 이때 하이버네이트를
사용하는 경우에는 INSERT 쿼리의 결과를 다시 조회하지 않기 위해서 내부적으로
Statement.getGeneratedKeys를 사용합니다. 추가로 IDENTITY 전략을 사용하면 배치 인서트가 불가하다는
점을 주의해야합니다.
SEQUENCE 전략은 시퀀스 키 생성 전략을 지원하는 DB에서 사용할 수 있습니다.
데이터베이스 시퀀스란, 유일한 값을 자동으로 생성하게 하는 객체입니다. auto_increment와 달리 초기 값과 한번에 증가할
크기를 설정할 수 있습니다. 해당 시퀀스를 키 생성 전략으로 갖는 DB에 대해 SEQUENCE 전략을 사용할 수 있습니다. 어떤
시퀀스를 사용할 것인지를
@SequenceGenerator 로 설정할 수 있습니다. SEQUENCE
전략은 em.persist()를 호출하는 경우 먼저 데이터베이스 시퀀스를 이용하여 식별자를 조회합니다. 이후 조회한 식별자를
엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장합니다. 트랜잭션을 커밋하여 플러시가 일어나면 엔티티를 저장한다는 점에서
IDENTITY 전략과 차이가 있습니다.TABLE 전략은 키 생성 전용 테이블을 만들어 시퀀스를 흉내내는 전략입니다. 어떤 테이블을 사용할 것인지를
@TableGenerator로
설정할 수 있습니다. TABLE 전략은 값을 조회하면서 SELECT 쿼리를 사용하며 증가를 위해 UPDATE 쿼리를 사용합니다.
SEQUENCE 전략보다 DB와 한번 더 통신한다는 점에서 성능이 안좋다는 단점이 있지만, 모든 DB에 적용할 수 있다는 장점이
있습니다.AUTO 전략은 데이터베이스 방언에 따라서 IDENTITY, SEQUENCE, TABLE 중 하나를 자동으로 선택합니다. 데이터베이스를 변경해도 코드를 수정할 필요가 없다는 장점이 있습니다.
private 메서드에 @Transactional 선언하면 트랜잭션이 동작할까요?
기본적으로
@Transactional, @Cacheable, @Async 등의 애너테이션은 런타임에 동작하는 Spring AOP를 기반으로 동작합니다. Spring AOP가 제공하는 JDK Dynamic Proxy, CGLIB 방식 모두 타깃이 구현하는 인터페이스나 구체 클래스를 대상으로 프록시를 만들어서 타깃 클래스의 메서드 수행 전후에 횡단 관심사에 대한 처리를 할 수 있습니다.Spring은 빈 생성시, 해당 빈에 AOP 애너테이션이 있는지 검사하고, 있다면 프록시 객체를 생성하여 빈을 대체합니다. AOP 적용 대상인 클래스의 경우, 즉,
@Transactional과 같은 AOP 애너테이션이 하나라도 선언된 클래스는 프록시로 감싸집니다.JDK Dynamic Proxy의 경우 타깃 클래스가 구현하는 인터페이스를 기준으로 프록시를 생성하여
public 메서드만 AOP 적용 가능합니다. CGLIB 방식의 경우 인터페이스를 구현하지 않는 클래스를 상속하여 프록시를 생성하고, private을 제외한 public, protected, package-private 메서드에 AOP 적용 가능합니다.@Slf4j @RequiredArgsConstructor @Service public class SelfInvocation { private final MemberRepository memberRepository; public void outerSaveWithPublic(Member member) { saveWithPublic(member); } @Transactional public void saveWithPublic(Member member) { log.info("call saveWithPublic"); memberRepository.save(member); throw new RuntimeException("rollback test"); } public void outerSaveWithPrivate(Member member) { saveWithPrivate(member); } @Transactional private void saveWithPrivate(Member member) { log.info("call saveWithPrivate"); memberRepository.save(member); throw new RuntimeException("rollback test"); } } public interface MemberRepository extends JpaRepository<Member, Long> { }
@SpringBootTest class SelfInvocationTest { private static final Logger log = LoggerFactory.getLogger(SelfInvocationTest.class); @Autowired private SelfInvocation selfInvocation; @Autowired private MemberRepository memberRepository; @AfterEach void tearDown() { memberRepository.deleteAllInBatch(); } @Test void aopProxyTest() { // @Transactional 애너테이션을 가지고 있으므로, 빈이 Proxy 객체로 대체되어 주입된다. assertThat(AopUtils.isAopProxy(selfInvocation)).isTrue(); // interface를 구현하지 않은 클래스이므로 CGLIB Proxy가 생성된다. assertThat(AopUtils.isCglibProxy(selfInvocation)).isTrue(); } @Test void outerSaveWithPublic() { Member member = new Member("test"); try { selfInvocation.outerSaveWithPublic(member); } catch (RuntimeException e) { log.info("catch exception"); } List<Member> members = memberRepository.findAll(); // self invocation 문제로 인해 트랜잭션이 정상 동작하지 않음. // 예외 발생으로 인한 롤백이 동작하지 않고 남아있음. assertThat(members).hasSize(1); } @Test void outerSaveWithPrivate() { try { selfInvocation.outerSaveWithPrivate(new Member("test")); } catch (RuntimeException e) { log.info("catch exception"); } List<Member> members = memberRepository.findAll(); // self invocation 문제로 인해 트랜잭션이 정상 동작하지 않음. // 예외 발생으로 인한 롤백이 동작하지 않고 남아있음. assertThat(members).hasSize(1); } @Test void saveWithPublic() { Member member = new Member("test"); try { selfInvocation.saveWithPublic(member); } catch (RuntimeException e) { log.info("catch exception"); } List<Member> members = memberRepository.findAll(); // 외부에서 프록시 객체를 통해 메서드가 호출되었기 때문에 트랜잭션 정상 동작, 롤백 성공. assertThat(members).hasSize(0); } }
Spring AOP는 외부에서 프록시 객체를 통해 메서드가 호출될 때만 AOP 어드바이스(트랜잭션 관리)를 적용합니다. 같은 클래스 내에서 메서드를 호출하면, 프록시를 거치지 않고 직접 호출되므로 트랜잭션 어드바이스가 적용되지 않습니다.
이를 해결하기 위해서는 자기 자신을 프록시로 주입 받아 프록시를 통해 메서드를 호출하거나, 별도의 클래스로 분리하거나, AspectJ를 이용하는 방법이 있습니다. AspectJ를 사용하면 동일 클래스 내에서의 메서드 호출에도 AOP 어드바이스를 적용할 수 있습니다.
- 자기 자신을 프록시로 주입 받는 방법
@Slf4j @RequiredArgsConstructor @Service public class SelfInvocation { private final MemberRepository memberRepository; private final SelfInvocation selfInvocation; public void outerSaveWithPublic(Member member) { selfInvocation.saveWithPublic(member); } @Transactional public void saveWithPublic(Member member) { log.info("call saveWithPublic"); memberRepository.save(member); throw new RuntimeException("rollback test"); } ... }
이 방법은 순환 의존성 문제를 일으킬 수 있어 권장되지 않습니다.
- 별도의 클래스로 분리하는 방법
@Slf4j @RequiredArgsConstructor @Service public class TransactionService { @Transactional public void outer() { log.info("call outer"); logCurrentTransactionName(); logActualTransactionActive(); inner(); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void inner() { log.info("call inner"); logCurrentTransactionName(); logActualTransactionActive(); } private void logActualTransactionActive() { boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive(); log.info("actualTransactionActive = {}", actualTransactionActive); } private void logCurrentTransactionName() { String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); log.info("currentTransactionName = {}", currentTransactionName); } } // 로그 // call outer // currentTransactionName = server.transaction.TransactionService.outer // actualTransactionActive = true // call inner // currentTransactionName = server.transaction.TransactionService.outer // actualTransactionActive = true
outer가 inner 메서드를 호출하는데, outer의 propagation 속성은 REQUIRED, inner는
REQUIRES_NEW로 서로 다른 트랜잭션으로 분리되어야 합니다. 하지만, 로그를 보면 동일한 outer의 트랜잭션에 속해있습니다. 이처럼 트랜잭션 전파 속성이 다른 두 메서드가 동일한 클래스 내부에서 self invocation 호출하면 의도대로 동작하지 않습니다. 이 때 outer와 inner 메서드를 각각 다른 클래스로 분리하여 호출하면 해결할 수 있습니다.
// OuterTransactionService @Slf4j @RequiredArgsConstructor @Service public class OuterTransactionService { private final InnerTransactionService innerTransactionService; @Transactional public void outer() { log.info("call outer"); logCurrentTransactionName(); logActualTransactionActive(); innerTransactionService.inner(); } private void logActualTransactionActive() { boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive(); log.info("actualTransactionActive = {}", actualTransactionActive); } private void logCurrentTransactionName() { String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); log.info("currentTransactionName = {}", currentTransactionName); } } // InnerTransactionService @Slf4j @RequiredArgsConstructor @Service public class InnerTransactionService { @Transactional(propagation = Propagation.REQUIRES_NEW) public void inner() { log.info("call inner"); logCurrentTransactionName(); logActualTransactionActive(); } private void logActualTransactionActive() { boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive(); log.info("actualTransactionActive = {}", actualTransactionActive); } private void logCurrentTransactionName() { String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); log.info("currentTransactionName = {}", currentTransactionName); } } // 로그 // call outer // currentTransactionName = server.transaction.OuterTransactionService.outer // actualTransactionActive = true // call inner // currentTransactionName = server.transaction.InnerTransactionService.inner // actualTransactionActive = true
이처럼 각각 프록시를 생성할 수 있게 두 클래스로 분리하면 AOP 어드바이스가 적용되어 의도한 대로 독립적인 트랜잭션을 시작할 수 있게 됐습니다.