2025-11-16
오늘 배운 것
자바
- 불변 객체(Immutable)
- 정의
- 한 번 생성되면 내부 상태가 절대 변하지 않는 객체
- 값이 바뀌는 순간 “새 객체”를 만들어야 한다
- 예 : String, Integer, Long, LocalDate, LocalDateTime
- 중요한 이유
- Thread-safe
- 복사 없이 여러 스레드가 공유해도 절대 문제 없음
- 멀티 스레드 환경에서 가장 안전한 방식
- 예측 가능(버그 감소)
- 객체가 어디서 값이 바뀌는지 추적할 필요 없음
- equals, hashCode 안정
- HashMap, Set에 key로 안전하게 사용 가능
- 값이 변하면 해시가 깨져 Key를 잃어버리는 문제 발생
- 부작용 없음
- 특정 메서드에서 외부 객체 값을 몰래 변경하는 오류 방지
- 함수형 프로그래밍 기반
- Java 8 Stream, Lambda, Optional 등에서 불변 객체 사용이 핵심 철학
- Thread-safe
- 예
- String
- 내부 char 배열(value[])도 final
- concat, replace 호출해도 기존 객체 수정이 아닌 새객체 생성
- Wrapper 클래스(Integer, Long, Double 등)
- 내부 값이 final
- Java Time API (LocalDate, LocalDateTime 등)
- 날짜 변경 메서드는 항상 새로운 객체 반환
- String
- 불변 객체 만들 때 지켜야 할 규칙
- 필드 모두 private + final
- setter 금지
- 생성 시 모든 값 초기화 후 절대 변경 불가
-
변경 메서드는 ‘새 객체’ 반환
public final class User { private final String name; private final int age; public User(String name, int age) { this.name = name; this.age = age; } public User withAge(int newAge) { return new User(this.name, newAge); // 기존 객체 불변 유지 } public String getName() { return name; } public int getAge() { return age; } } -
컬렉션을 필드로 가진다면 defensive copy 필요
public final class Group { private final List<String> members; public Group(List<String> members) { this.members = List.copyOf(members); // 외부 리스트 복사 } public List<String> getMembers() { return List.copyOf(members); // 내부도 복사본으로 반환 } } // 외부에서 필드의 참조를 얻으면 변경할 수 있어서 불변성이 깨지므로 복사형태로 세팅 및 반환
- 불변 객체 단점
- 객체 생성 비용 증가
- 변경 시마다 새 객체 생성 → 메모리 사용 증가
- 대규모 반복 작업에서 성능 손해 가능
- 정의
- 예외 처리
- 정의
- 프로그램 실행 중 발생하는 비정상 상황
- Java는 예외 처리를 통해 프로그램이 갑자기 종료되지 않도록 한다
-
예외 계층 구조
Throwable ├── Error (프로그램이 복구 불가) └── Exception ├── Checked Exception └── Unchecked Exception (RuntimeException)- Error
- OutOfMemoryError, StackOverflowError 등
- 복구 불가 → catch 하면 안됨
- JVM 레벨 문제
- Exception
- 개발자가 처리해야 하는 예외
- Error
- Checked vs Unchecked
- Checked Exception
- 컴파일 시점에 반드시 처리해야 함
- 예 : IOException, SQLException, ClassNotFoundException
- 특징
- try-catch 또는 throws 필수
- 파일, DB, 네트워크 처럼 실패 가능성이 높은 동작
- Unchecked Exception (RuntimeException)
- 컴파일러가 강제하지 않음
- 예 : NullPointerException, IllegalArgumentException, IndexOutOfBounds
- 특징
- 프로그래밍 실수(버그)에서 발생
- Checked Exception
- 예외 처리 방법
-
try-catch-finally
try { riskyCode(); } catch (IOException e) { handle(e); } finally { cleanUp(); } // try-catch-resources // Java 7+ 자동 자원 해제 기능 try (FileInputStream fis = new FileInputStream("a.txt")) { ... } // close() 자동 호출 // I/O, DB 커넥션, 소켓에서 필수로 사용해야 함 -
throws : 메서드 호출자에게 예외를 떠넘김
void read() throws IOException
-
- 예외를 던질 때 주의점
- 예외에는 상황 정보를 담아야 함
- 의미없는 catch 후 무시 금지 → 디버깅이 불가능 해짐
- 예외를 삼키지 말고 로그 남기기
- 커스텀 예외
- 런타임 예외를 확장하여 생성을 권장
- 서비스 로직 흐름이 단순
- checked exception은 throws 전파로 코드 오염 가능
- 런타임 예외를 확장하여 생성을 권장
- 예외 처리 원칙
- 비즈니스 규칙 위반 → RuntimeException 기반 커스텀 예외
- 외부 자원 실패(IO/DB/소켓) → Checked Exception
- IOException → RuntimeException으로 변환해 전파하는 패턴 흔함
- 메서드 안에서 모든 예외 처리하지 않기
- 책임이 분산되고 흐름이 꼬임
- 예외 메시지는 매우 구체적으로 작성
- 예외 전파 흐름
- 메서드에서 예외 발생
- JVM은 스택을 타고 위로 던짐
- 어디에서도 잡히지 않으면 프로그램 종료
- 어디에서 예외를 잡을지 책임 분리 중요
- 정의
- 직렬화
- 정의
- 객체를 연속적인 바이트형태로 변환하는 과정
- 필요성
- 자바 객체는 JVM 안에서만 존재 가능하여 객체를 외부로 보내려면 byte 형태 필요
- 사용처
- 네트워크 전송 (API, 소켓 통신)
- 파일 저장 (직렬화된 객체 저장)
- 캐시 저장 (Redis, Memcached 등)
- 분산 시스템 통신
- Java RMI / 메시지 큐 전송
- 방법
-
Serializable 인터페이스 구현
public class User implements Serializable { private String name; private int age; }- 아무 메서드도 없는 마커 인터페이스
- JVM이 이 객체는 직렬화 가능하다고 판단하는 기준
-
- transient 키워드
-
직렬화 시 제외할 필드에 사용
class User implements Serializable { private String name; private transient String password; // 저장되면 위험 } -
사용 이유
- 보안 정보(password, token 등) 제외
- 직렬화 불가능한 타입 제외
- 임시 필드 제외
-
- serialVersionUID
-
직렬화된 바이트를 역직렬화할 때 이 객체 클래스 버전이 같은지 판단하는 값
private static final long serialVersionUID = 1L; -
필요 이유
- 클래스 구조(필드 등)가 바뀌었는데, 이전 버전의 직렬화 파일을 역질력화 하면 InvalidClassException 발생
- 개발자가 명시적으로 버전 관리
-
- 동작 방식
- 객체의 필드값 순회
- transient가 아닌 것만 직렬화
- 부모 클래스도 Serializable이면 함께 직렬화
- static 필드는 직렬화
- 역직렬화 시
- 생성자를 호출하지 않고 메모리를 직접 할당해 객체 생성
- 문제점
- 느림
- Reflection 기반으로 동장
- JSON, ProtoBuf 등 다른 포맷보다 훨씬 느림
- 안전하지 않음
- 직렬화된 데이터를 조작하면 임의 객체 생성 취약점 발생 가능
- 클래스 버전 변경 시 취약
- 필드 추가/삭제 되면 역직렬화 실패 → 유지보수 어려움
- 느림
- JSON/Protobuf 사용 권장
- JSON(ObjectMapper)
- XML
- Protobuf
- Avro
- MessagePack
- 등을 사용하는 것을 권장
- 스프링/마이크로서비/클라우드 환경에서 거의 직렬화 사용 X
- 그래도 직렬화 사용하는 경우
- 레거시 시스템
- Java 기반 캐시 (예전 Ehcache)
- Java RMI
- WAS 간 세션 클러스터링 (예전 방식)
- Lombok @Data + Redis 저장 구조 일부에서 사용
- 정의
-
Java 버전 별 특징
| 버전 | 핵심 변화 | | — | — | | Java 8 | 람다, 스트림, Optional, 날짜 API, CompletableFuture | | Java 11 | HTTP Client, String 개선, var in lambda | | Java 17 | Records, Sealed Class, instanceof 개선, GC 향상 | | Java 21 | Virtual Thread |
- Java 8
- Lambda
- Stream API
- Optional
- Functional Interface
- Default / Static 메서드 in 인터페이스
- LocalDate / LocalTime / LocalDateTime
- CompletableFuture
- Java 11 (LTS)
- String API 강화
- strip, isBlank, lines
-
HTTP Client 정식 추가
HttpClient client = HttpClient.newHttpClient(); - var in lambda
- 람다 파라미터 타입에 var 사용 가능
- String API 강화
- Java 16-17(17 LTS)
- Records
public record User(String name, int age) {}- 불변
- final 필드
- equals, hashCode 자동
- Sealed Class
public sealed class Shape permits Circle, Square {}- 상속 제한
-
Pattern Matching for instanceof
if (obj instanceof User u) { System.out.println(u.name()); } - GC 성능 향상 (ZGC, Shenandoah)
- Records
- Java 21 (LTS)
- Virtual Thread
- 수천 ~ 수백만 스레드 만드는 것도 부담 없음
- 스프링 부트 3 + WebFlux에서 각광
- Pattern Matching for Switch
- Sequenced Collections
- Virtual Thread
- Java 8
스프링
-
Spring & Spring Boot 기본 개념
| 항목 | Spring Framework | Spring Boot | | — | — | — | | 설정 방식 | XML/JavaConfig로 직접 설정 많음 | 자동 설정으로 설정 부담 ↓ | | 서버 | 외부 WAS 필요 | 내장 Tomcat 기본 제공 | | 설정 파일 | 복잡, 설정 직접 관리 |
application.yml만 관리 | | 스타터 패키지 | 없음 | starter 패키지 제공 | | 목적 | 유연성 높은 프레임워크 | 빠르고 효율적인 개발 |- Spring Framework
- 정의 : 자바 기반 엔터프라이즈 애플리케이션 개발을 돕는 프레임워크
- 목표 : 느슨한 결합 + 효율적인 개발 구조 제공
- 핵심 기능
- IoC / DI 컨테이너 : 객체 생성/관리 책임을 개발자가 아닌 컨테이너가 담당
- AOP 지원 : 공통 관심사(로깅, 트랜잭션)를 깔끔하게 모듈화
- 스프링 MVC 웹 프레임워크 : Controller-Service-Repository 구조 기반의 웹 애플리케이션 구성
- 장점
- 객체 간 강한 결합도를 낮춤
- 트랜잭션/보안/AOP 등 공통기능 제공
- 테스트 용이성 증가
- 모듈성 증가로 유지보수 용이
- Spring Boot
- 정의 : Spring을 더 빠르고 편하게 쓰도록 만든 확장 프레임워크
- 특징
- 자동 설정 : 개발자가 설정을 최소화해도 필요한 Bean들을 자동으로 구성
- 내장 서버 제공(Tomcat, Jetty) : 실행만 하면 바로 웹 서버 동작 → WAR 배포 X
- Optionated Defaults : 대부분 프로젝트에서 “좋은 기본값”을 미리 설정해 둠
- starter 의존성 제공 : 관련된 라이브러리를 묶어서 제공(예: spring-boot-starter-web)
- Spring Framework
- Spring MVC 흐름(DispatcherServlet 중심)
- Spring MVC란?
- 웹 요청을 처리하기 위한 스프링의 아키텍처 패턴
- 요청 → 컨트롤러 → 비즈니스 로직 → 응답 흐름을 표준화
- 구조적으로 Front Controller 패턴을 사용
- DispatcherServlet = FrontController
- HTTP 프로토콜로 들어오는 모든 요청을 먼저 받아서 적합한 컨트롤러에 위임
- DispatcherServlet = FrontController
- DispatcherServlet이 모든 요청의 입구 역할
- Spring MVC 전체 요청 흐름
- 클라이언트 요청
- DispatcherServlet이 요청 수신
- HandlerMapping으로 어떤 컨트롤러를 호출할지 탐색
- HandlerAdapter가 컨트롤러 실행 방식에 맞는 어댑터 선택
- Controller 메서드 호출
- 비즈니스 로직 수행 (Service → Repository)
- Controller가 ModelAndView 또는 ResponsneBody 반환
- ViewResolver가 어떤 View를 렌더링할지 선택
- DispatcherServlet이 최종 Response 반환
- 컴포넌트 별 역할
- DisptacherServlet
- Spring MVC의 중심
- 요청을 받아 어떤 컨트롤러에게 전달할지, 어떤 뷰를 반환할지 전 과정 조율
- HandlerMapping
- 요청 URL에 매칭되는 컨트롤러 탐색
- HandlerAdapter
- 컨트롤러 호출 방식을 표준화
- @Controller, @RestController, RequestMappingHandlerAdapter 등 방식 통합
- Controller
- 요청 → 비즈니스 로직 → 응답 데이터 생성
- ViewResolver
- 뷰 템플릿 경로 매핑
- Thymeleaaf, JSP, JSON 변환 등
- DisptacherServlet
- Spring에서 JSON 응답이 자동으로 되는 이유
- Spring Boot의 경우 HttpMessageConverters + Jackson 라이브러리가 자동 등록되기 때문
- @RestController 또는 @ResponseBody가 있으면 객체 → JSON 변환 자동 수행
- Spring MVC란?
- IoC / DI / Bean 개념
- IoC(Inversion of Control, 제어의 역전)
- 객체의 생성/초기화/소멸 같은 제어권을 개발자가 아닌 스프링 컨테이너가 관리하는 거서
- 개발자가 직접 new로 객체를 생성하는 것이 아닌, 컨테이너가 대신 객체를 생성하고 주입
- 장점
- 객체 간 결합도 감소
- 테스트 용이
- 유지보수 용이
- 의존성 구조가 명확
- DI(Dependency Injection, 의존성 주입)
- IoC의 한 형태로, 필요한 객체를 직접 만들지 않고 외부(컨테이너)에서 넣어주는 것
- 방식
- 생성자 주입
- 의존성을 불변으로 만들 수 있음
- Mock 객체 주입이 가능하여 테스트 용이
-
순환참조 조기발견 가능
@Service public class UserService { private final UserRepository userRepository; // 불변 public UserService(UserRepository userRepository) { this.userRepository = userRepository; } }
- Setter 주입
-
선택적 의존성 사용
@Service public class UserService { private UserRepository userRepository; @Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } }
-
- 필드 주입
-
사용은 쉬우나 테스트 및 유지보수 최악
@Service public class UserService { @Autowired private UserRepository userRepository; }
-
- 생성자 주입
- Bean(빈)
- 스프링 IoC 컨테이너가 생성하고 관리하는 객체
- 스프링이 직접 관리하는 인스턴스
- 등록 방식
- 컴포넌트 스캔(@Component 계열)
- @Component, @Service, @Repository, @Controller
- 수동 Bean 등록 (@Bean + @Configuration)
- 동적으로 객체를 생성해야 하는 경우
- 운영/개발 환경에 따라 다른 구현체 사용해야 될 때
- 특정 설정값에 따라 빈 결정해야 될 때
- 전략 패턴으로 여러 구현체 중 하나 선택해야 하는 경우(카드결제, 계좌 결제 등등)
- 외부 라이브러리 Bean 등록할 때
- JWT 라이브러리, ObjectMapper, RestTemplate, RedisClient 등
- 파라미터를 넣어서 객체를 생성해야 할 때
- @Component 방식은 파라미터 있는 생성자를 마음대로 사용 불가
- 직접 빈 생성해야 함
- @Component 방식은 파라미터 있는 생성자를 마음대로 사용 불가
- 테스팅을 위해 대체 구현체를 넣어야 할 때
- 동적으로 객체를 생성해야 하는 경우
- 컴포넌트 스캔(@Component 계열)
- ApplicationContext (스프링 IoC 컨테이너)
- DI를 실제로 수행하고 Bean을 관리하는 핵심 컨테이너
- 역할
- Bean 생성
- Bean 의존성 주입
- Bean 생명주기 관리
- Bean 검색
- 스프링 부트에서는 실행하며 바로 ApplicationContext가 뜸
- IoC(Inversion of Control, 제어의 역전)
- Bean 생성 과정과 생명주기
- Spring Bean 생명 주기 전체 흐름
- 스프링 컨테이너는 Bean을 등록하고, 의존성을 주입하고, 초기화 콜백을 수행한 뒤 사용하다가 종료 시점에 소멸 콜백을 호출합니다
- Bean 생성과정
- Bean 정의(BeanDefinition) 스캔
- @ComponentScan, @Bean 등을 읽어 Bean 메타 정보를 등록
- Bean 인스턴스 생성
- new 키워드로 객체 생성(생성자 호출)
- 의존성 주입 수행
- 생성자, Setter 등을 통해 필요한 객체 넣어줌
- BeanNameAware
- 자신의 Bean 이름을 알 수 있게 함
- BeanFactoryAware / ApplicationContextAware
- 컨테이너 객체를 주입받음
- 초기화 콜백(init)
- @PostConstruct, IntializingBean.afterPropertiesSet(), @Bean(initMethod=””) 중 하나 선택하여 이 단계에서 진짜 초기화 로직 수행
- 예 : DB 커넥션 열기
- Bean 사용
- 소멸 콜백 (Destroy)
- @PreDestory, DisposableBean.destroy(), @Bean(destroyMethod=””) 로 리소스 해제 (DB 커넥션 닫기 등)
- Bean 정의(BeanDefinition) 스캔
- 초기화 / 소멸 콜백 방식
-
@PostConstruct, @PreDestroy
@Component public class ConnectionPool { @PostConstruct public void init() { // 초기화 작업 } @PreDestroy public void close() { // 종료 작업 } } -
이외에도 여러 방식이 있는데, 이유는 스프링 외부 라이브러리는 @PostConstruct 사용불가하여 initMethod가 필요
-
- Spring Bean 생명 주기 전체 흐름
-
Bean Scope
| Scope | 설명 | 실제 사용 상황 | | — | — | — | | singleton (기본값) | 컨테이너당 단 하나의 객체 | 거의 모든 서비스/리포/컨트롤러 (상태 없음) | | prototype | 요청할 때마다 새로 생성 | 요청마다 다른 객체가 필요한 경우, 상태 필요 객체 | | request | HTTP 요청마다 생성 | 요청 단위 info 저장 (requestId, locale 등) | | session | 세션마다 생성 | 로그인 사용자 정보 유지(세션 로그인 방식) | | application | 서블릿 컨텍스트 생존 기간 | 전체 웹 앱에서 공유해야 하는 전역 정보 |
- 정의
- 스프링 컨테이너가 빈은 언제 생성하고 얼마나 오래 유지할지 결정하는 범위
- 종류
- singleton (기본값)
- 컨테이너당 하나 생성
- 애플리케이션 시작 시 만들어지 종료까지 유지
- 대부분 서비스, 리포지토리가 singleton
- 장점
- 메모리 효율 좋음
- 객체 재사용 → 빠름
- 스프링 DI 구조와 가장 잘 맞음
- 주의
- 상태를 가지면 안됨 → 모든 요청이 같은 인스턴스를 공유하기 때문에
- prototype
- 요청할 때마다 새로 생성
- 스프링은 생성만하고 이후 생명주기는 관리하지 않음
- 사용처
- 매번 다른 객체가 필요할 때
- 상태가 매 요청마다 달라야 할 때
- 주의
- 소멸이 자동으로 호출되지 않으므로 직접 정리해야 함
- request
- HTTP 요청마다 1개의 빈 생성
- 웹 요청에서만 사용 가능
- 예 : 로그인 세션에서 Request 값 바인딩 등
- session
- HTTP 세션마다 1개의 빈 생성
- 로그인 사용자별로 독립적인 객체가 필요할 때
- application
- 서블릿 컨텍스트 생명주기와 동일
- 거의 사용 x (싱글톤과 비슷하지만 웹 컨텍스트 기준)
- singleton (기본값)
- 정의
- AOP 원리(JDK Proxy / CGLIB)
- 정의
- Aspect Oriented Programming(관점 지향 프로그래밍)
- 핵심 로직(비즈니스 로직)과 공통 로직(로깅, 트랜잭션, 인증 등)을 분리하는 기법
- 장점
- 중복 코드 제거
- 모듈화
- 유지보수용이
- 동작 방식 : 프록시 기반
- 프록시 객체를 만들어 실제 객체 앞에 세우는 방식
- 실제 객체를 감싸는 가짜 객체(프록시)가 중간에서 Advice(로깅, 트랜잭션 등)를 수행하고 이후 실제 객체를 호출하는 구조
- 필요 이유
- 개발자는 컨트롤러/서비스의 코드를 건드리지 않고 공통로직(트랜잭션, 로깅 등)으르 넣을 수 있음
- 유지보수가 쉬움
- 관심사 분리 (SoC: Seperation of Concern) → 소프트웨어 개발 원칙
- 소프트웨어 아키텍처에서 특정 기능만 담당하는 모듈을 설계하여 효율성을 높임
- 종류
- JDK Dynamic Proxy(인터페이스 기반)
- 스프링이 우선적으로 사용
- 특징
- 인터페이스를 기반으로 프록시 생성
- 인터페이스를 구현한 프록시 객체 생성
- 인터페이스가 필수적으로 존재해야함
- CGLIB(클래스 상속 기반 프록시)
- 인터페이스가 없거나, proxtTargetClas = true 설정한 경우
- 특징
- 실제 클래스를 상속해서 프록시 생성
- final 클래스 or final 메서드 사용 시 프록시 불가
- 스프링 부트 2.0+에서는 starter의 기본 설정 때문에 CGLIB를 선호하는 경향이 강함
- JDK Dynamic Proxy(인터페이스 기반)
-
AOP 핵심 용어
용어 설명 Aspect 횡단 관심사의 모듈(예: LoggingAspect) Advice 언제 실행할지(before, after, around) Pointcut AOP를 적용할 메서드 지정 JoinPoint 실제 AOP가 적용되는 지점 Target 실제 호출되는 객체 Proxy Target 앞에서 대신 동작하는 객체 - AOP 제한점
- 프록시 방식이기 때문에 메서드 내부 호출은 적용되지 않음
- @Transactional이 내부 호출에서 안먹히는 이유와 동일
- final 클래스/메서드는 CGLIB 적용 불가
- 프록시 방식이기 때문에 메서드 내부 호출은 적용되지 않음
- 정의
-
Filter / Interceptor / AOP / DispatcherServlet 비교
[요청] ↓ (Filter) ← 가장 앞 ↓ (DispatcherServlet) ↓ (Interceptor preHandle) ↓ Controller ↓ (Interceptor postHandle) ↓ (Interceptor afterCompletion) ↓ (AOP: 메서드 호출 시점) ↓ 비즈니스 로직(Service, Repository) ↓ [응답]- Filter (Servlet Filter)
- 작동위치
- DispatcherServlet 이전
- 가장 앞단에서 HTTP 요청/응답 가로챔
- 누가 제공
- Servlet 스펙(자바 EE)
- 스프링과 무관하게 동작 가능
- 사용 예시
- 인증/인가(기초적인 토큰 검사)
- XSS(Cross-Site Scrripting) 필터링
- 공격자가 웹사이트에 악성 스크립트를 삽이비하여 사용자 브라우저에서 실행되게 하는 보안 공격
- CORS 처리
- Encoding(Filter)
- 요청/응답 공통 처리
- 특징
- 가장 낮은 레벨에서 HTTP 요청을 직접 다룸
-
예시
@WebFilter(urlPatterns = "/*") public class MyFilter implements Filter { public void doFilter(...) { // 요청 전 처리 chain.doFilter(request, response); // 응답 후 처리 } }
- 작동위치
- DispatcherServlet
- 역할
- 스프링 MVC의 Front Controller
- 모든 요청을 중앙에서 받아서 처리 흐름을 결정
- 하는 일
- 어떤 Controller 호출할지 결정 (HandlerMapping)
- 적절한 어댑터 선택(HandlerAdapter)
- Controller 실행
- ViewResolve를 통해 뷰 렌더링
- 예외 처리
- 특징
- Spring MVC 요청 처리의 중심, 전체 흐름 조율자
- 역할
- Interceptor
- 작동 위치
- DispatcherServlet 이후
- Controller 호출 직전/이후
- 누가 제공
- Spring MVC 기능
- 사용 예시
- 로그인 여부 체크 (세션 기반)
- 사용자 권한 체크
- Request/Response 로깅
- 실행 시간 측정
- API Key 검증
- 특징
- 스프링 MVC 핸들러(Controller)에 특화된 처리
-
코드
public class LoginInterceptor implements HandlerInterceptor { public boolean preHandle(...) {} // Controller 호출 전 public void postHandle(...) {} // Controller 호출 후 public void afterCompletion(...) {} // 요청 전체 완료 후 }
- 작동 위치
- AOP
- 작동 위치
- 메서드 호출 순간(Controller/Service/Repository 메서드 단위)
- 누가제공
- Spring AOP
- 사용 예시
- 트랜잭션(@Transactional)
- 메서드 실행 시간 츠그정
- 서비스/레포지토리 공통 로깅
- 권한 체크
- 캐싱
- 특징
- 비즈니스 로직에 직접 관여
- 메서드 단위 정교한 제어 가능
-
Pointcut으로 메서드를 지정해 동작
@Around("execution(* com.example.service.*.*(..))") public Object log(ProceedingJoinPoint joinPoint) throws Throwable { // Before Object result = joinPoint.proceed(); // After return result; }
- 작동 위치
- Filter (Servlet Filter)
- @Transactional 동작 원리
- AOP + 프록시로 동작
- 역할
- 메서드 실행 전 트랜잭션 시작
- 메서드가 정상 종료되면 commit
- 예외 발생시 rollback
- 동작 구조
- @Transactional = Proxy를 만들어 트랜잭션 로직을 감쌈
-
서비스 메서드를 호출할 때 프록시가 먼저 가로채서 트랜잭션 제어
Client ↓ [Proxy] ← 트랜잭션 시작/커밋/롤백 ↓ [Real Service] ← 실제 비즈니스 로직
- @Transactional 흐름
- 스프링은 @Transational이 붙은 Bean을 감싸는 Proxy 생성
- 클라이언트가 메서드를 호출하면 Proxy가 먼저 실행
- Proxy가 트랜잭션 시작
- 실제 서비스 메서드 실행
- 정상 종료 시 commit
- 예외 발생 시 rollback
- JDK Proxy / CGLIB 필요
- @Transational은 AOP 기반이고 스프링 AOP는 프록시 기반이므로 빈을 감싸는 프록시 객체를 만들어야 함
- 인터페이스 있으면 JDK Proxy, 없으면 CGLIB Proxy
- 이러한 구조 덕분에 스프링은 메서드 실행 전/후에 트랜잭션 로직 삽입 가능
- 내부 호출이 실패하는 이유
- 내부 호출은 프록시를 거치지 않기 때문
- 외부에서 호출 → 프록시가 인터셉트 → 트랜잭션 적용
- 내부에서 호출 → 그냥 자기 자신 호출 → 프록시 패스
- 내부 호출은 프록시를 거치지 않기 때문
- 롤백 규칙
- 기본 규칙
- Unchecked Exception(Runtime Exception) → rollback
- Checked Exception → rollback X
- 따라서 필요하면 rollbackFor 옵션으로 커스터마이징
- 기본 규칙
- 옵션
- propagation
- REQUIRED (기본)
- REQUIRES_NEW
- NESTED
- readOnnly = true
- 변경 감지(Dirty Checking) 스킵 → 성능 개선
- 데이터 변경하면 안됨
- DB에 따라 힌트 제공(예: MySQL은 큰 영향 없음)
- timeout
- 트랜잭션이 너무 오래 걸릴 때 자동 종료
- propagation
- JPA 기본 개념
- 정의
- Java Persistence API
- 자바 ORM(Object Relational Mapping) 표준 인터페이스
- 자바 객체와 데이터베이스 테이블을 자동으로 매핑해주는 기술
- JPA는 인터페이스
- 실제 구현체는 Hibernate, EclipseLink(대부분 Hibernate 사용)
- 사용 이유
- 기존 JDBC 문제
- SQL 반복 작성
- 데이터 매핑 반복
- 변경 시 모든 SQL 수정
- CRUD 중복 코드 많음
- 객체와 테이블 간 불일치
- JPA 장점 ⇒ 생산성과 유지보수 크게 증가
- 반복 코드 제거 (CRUD 자동)
- 객체 중심 모델 기반 개발
- 캐싱, 더티 체킹 등 성능 최적화
- 변경된 필드 자동 반영(UPDATE SQL 자동 생성)
- 지연 로딩으로 최적화 가능
- 기존 JDBC 문제
- JPA → Hibernate 구조
- JPA는 표준 스펙, Hibernate는 구현체
- JPA(인터페이스) → Hibernate(구현체) → JDBC(실제 DB 연결)
- 핵심 기능
- 매핑
- @Entity, @Id, @Column등을 이용해 클래스 ↔ 테이블, 필드 ↔ 컬럼 매핑
- 영속성 컨텍스트(Persistence Context)
- 엔티티의 1차 캐시 관리
- 변경 감지(Dirty Checking)
- 엔티티 필드 변경 → 자동 UPDATE SQL 생성
- 지연 로딩(Lazy Loading)
- 연관된 엔티티는 필요할 때 로딩 → N+1 문제 발생 가능
- 매핑
- JPA 기본 CRUD
-
저장
User user = new User(); userRepository.save(user); -
조회
User user = userRepository.findById(id).get(); -
수정
user.setName("수아"); // UPDATE는 save 필요 없음 -> Dirty Checking -
삭제
userRepository.delete(user);
-
- JPA가 SQL을 자동으로 만들 때의 규칙
- Hibernate 내부에서 엔티티의 변경사항 감지 후 필요한 SQL만 자동 생성
- flush 시점에 자동 UPDATE 발생
- 정의
- 영속성 컨텍스트
- 정의
- 엔티티를 저장(관리)하는 1차 캐시 공간
- JPA가 엔티티를 관리하기 위해 사용하는 메모리 저장소
- EntityManager 내부에 존재
- JPA가 많은 기능을 자동으로 처리할 수 있게 함
- 엔티티가 영속성 컨텍스트에 저장되는 시점
em.persist(entity);- Spring Data JPA에서는 save() 호출 시점
- 핵심 기능
- 1차 캐시
- 영속 상태 엔티티는 메모리에 캐싱
- 성능 최적화
-
같은 트랜잭션 안에서는 기본적으로 DB HIT 최소화
User user1 = em.find(User.class, 1L); // DB 조회 User user2 = em.find(User.class, 1L); // DB 조회 안 함 (캐시!)
- 동일성 보장
- 같은 트랜잭션에서 동일한 엔티티는 == 비교가 true
- Hibernate의 핵심 특징
- 트랜잭션 내에서 데이터 일관성 보장
- Dirty Checking
- 엔티티 필드 값이 바뀌면 flush 시점에 자동으로 UPDATE SQL 생성
- 쓰기 지연
- INSERT/UPDATE느느 바로 실행되지 않고 flush 시점에 모아서 실행
- 1차 캐시
- Flush 발생 시점
- 자동 Flush
- 트랜잭션 커밋
-
JPQL 실행 : 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리 언어
String jpql = "select m From Member m where m.name like ‘%hello%'"; List<Member> result = em.createQuery(jpql, Member.class).getResultList(); for (Member member : result) { System.out.println("member = " + member); } - 강제 호출 :
em.flush()
- flush는 영속성 컨텍스트 → DB로 동기화 하는 작업일 뿐, 영속성 컨텍스트는 유지됨
- 자동 Flush
-
엔티티 상태
상태 설명 비영속(new) 아직 영속성 컨텍스트에 관리되지 않는 상태 영속(managed) persist() 후 1차 캐시에 저장된 상태 준영속(detached) clear(), close(), detach()로 관리가 끊어진 상태 삭제(removed) remove() 호출된 상태 - 영속성 컨텍스트 때문에 JPA가 자동으로 제공하는 기능
- 1차 캐시
- 동일성 보장
- Dirty Checking(setter → 자동 UPDATE)
- Write-behind(SQL 모아서 실행)
- 지연 로딩(Lazy Loading) 프록시 유지
- 엔티티 상태 관리
- 정의
- Flush / Dirty Checking
- Dirty Checking
- 정의
- 영속 상태 엔티티의 변경을 감지하고 자동으로 UPDATE SQL 을 생성하는 기능
- 동작 원리
- 영속성 컨테스트에 엔티티 저장 시 스냅샷 저장
- 엔티티 값 변경
- flush 시점에 스냅샷과 현재 상태 비교
- 값이 다르면 → UPDATE SQL 자동 생성
- 일어나지 않는 상황
- 엔티티가 영속 상태가 아닐 때(비영속,준영속)
- readOnly 트랜잭션일 때 (스냅샷 생성 생략)
- flush가 발생하지 않을 때
- 정의
- flush
- 정의
- 영속성 컨택스트에 있는 변경사항을 DB에 반영하는 작업
- 트랜잭션 커밋은 아님
- flush 후에도 영속성 컨텍스트는 유지
- 발생 시점
- 트랜잭션 커밋 시
- commit → flush → commit 완료
- JPQL 쿼리 실행시
- JPQL 실행 전에 flush 강제 발생
- 이유
- JPQL은 DB에 직접 쿼리를 날리므로 영속성 컨테스트와 DB 상태가 다르면 안됨
- 명시적 호출 :
em.flush()
- 트랜잭션 커밋 시
- 동작 방식
- Dirty Checking → 변경된 엔티티 탐색
- SQL 생성 (INSERT/UPDATE/DELETE)
- 쓰기 지연 SQL 저장소에 쌓인 쿼리 실행
- DB에 반영
-
그 후에도 영속성 컨텍스트는 그대로 유지
@Transactional public void updateUser() { User u = userRepository.findById(1L).get(); // 영속 상태 u.setAge(20); // Dirty Checking 대상 // flush 시점 } // 메서드 끝 → commit 발생 → flush 자동 → UPDATE SQL 실행
- 쓰기 지연 (Write Behind)
-
flush 전에 SQL이 즉시 실행되지 않고 쓰기 지연 SQL 저장소에 쌓였다가 한 번에 DB에 반영되는 최적화 기법
User a = new User(); User b = new User(); save(a); // INSERT 저장 (즉시 실행 X) save(b); // INSERT 저장 (즉시 실행 X) flush() // INSERT 두 개 한 번에 실행
-
- 정의
- Dirty Checking
- LAZY / EAGER
- 정의
- 연관관계 매핑된 엔티티를 언제(DB에서) 가져올지를 결정하느느 옵션
- LAZY → 필요한 시점에 가져오기(지연 로딩)
- EAGER → 즉시 전부 가져오기 (즉시 로딩)
-
기본 설정
관계 기본 fetch 타입 @ManyToOne EAGER @OneToOne EAGER @OneToMany LAZY @ManyToMany LAZY
- LAZY (지연로딩)
- 엔티티를 조회할 때 연관 엔티티를 함께 조회하지 않고 실제 사용되는 순간 쿼리가 발생하는 방식
-
사용하기 전까지 프록시 객체만 존재
Order order = orderRepository.findById(1L); // Member 로딩 안 함 order.getMember().getName(); // 이 시점에 Member 쿼리 실행 - 동작 방식
- LAZY 연관 엔티티는 “가짜 객체(프록시)”로 로딩
-
프록시 객체가 초기화 되는 순간 쿼리가 나감
order.getMember(); // Proxy order.getMember().getName(); // 여기서 DB 조회
- Fetch join으로 LAZY 최적화 가능
-
지연 로딩으로 인해 필요한 엔티티를 미리 가져오고 싶다면
@Query("SELECT o FROM Order o JOIN FETCH o.member") List<Order> findAllWithMember(); // 쿼리 1번으로 Order + Member 같이 가져옴
-
- EAGER(즉시 로딩)
- 엔티티를 조회할 때 연관된 엔티티를 전부 즉시 가져오는 방식
- 문제
- 필요없는 연관 엔티티까지 다 끌고 오기 때문에 예상치 못한 성능 이슈 발생
Order order = orderRepository.findById(1L); // 바로 Member join 조회 발생
- 실무에서는 무조건 LAZY
- 이유
- EAGER는 예측 불가능하나 쿼리 발생
- 개발자가 의도하지 않은 시점에 JOIN 발생
- 객체 하나 조회하려 했더니 쿼리가 10개 발생
- N+1 문제의 대표 원인
- EAGER는 특히 여러 엔티티 조회시 JOIN, 쿼리 폭발을 유발
- 성능 최적화 불가
- EAGER는 쿼리 최적화, fetch join, 배치 사이즈 설정 등이 무력화됨
- EAGER는 예측 불가능하나 쿼리 발생
- 이유
- 정의
- N+1 문제와 해결
- 정의
- 한번의 조회(1)로 N개의 엔티티를 가져왔는데, 그 N개의 엔티티 각각에 대해 추가 쿼리(N)가 발생하는 문제
- 의도는 1번 조회이지만 실제로는 총 N+1번 쿼리가 실행
- 대표적인 JPA 성능 문제
- 원인
-
LAZY 로딩 + 연관 컬렉션 순회
@Entity class Order { @ManyToOne(fetch = FetchType.LAZY) private Member member; }List<Order> orders = orderRepository.findAll(); // 1번 (order 목록 조회) for (Order order : orders) { order.getMember().getName(); // N개의 member 조회 } /* Order 10개면 Order 조회 1번 + Member 조회 10번 총 11번 쿼리 -> N+1 */
-
- 발생 상황
- ManyToOne, OneToOne(단일 엔티티 연관)
- Getter 호출시 추가 쿼리 발생
- OneToMany, ManyToMany(컬렉션 연관)
- 순회 시 연관된 데이터마다 추가 쿼리 발생
- ManyToOne, OneToOne(단일 엔티티 연관)
- 해결 방법(중요도 순)
-
Fetch Join (가장 강력한 해결책)
@Query("SELECT o FROM Order o JOIN FETCH o.member") List<Order> findAllWithMember(); // Order + Member 한 번에 가져옴 // 쿼리 딱 1번 // 가장 추천되고 실무에서도 가장 많이 사용 - @EntityGraph
- JPQL 없이도 fetch join 효과 주는 방식
- Spring Data JPA에서 지원
-
JOIN FETCH 와 거의 동일한 효과
@EntityGraph(attributePaths = {"member"}) List<Order> findAll();
- BatchSize(하나의 쿼리로 여러 개 조회)
- 컬렉션이나 ToOne 관계에서 IN 쿼리로 묶어서 가져오게 하는 방식
-
설정
@BatchSize(size = 100) @OneToMany(mappedBy = "order") private List<OrderItem> items; // 또는 전역 설정 spring: jpa: properties: hibernate.default_batch_fetch_size: 100 - 효과
- 원래 Order 100개면 OrderItem 쿼리 100번
- 배치 사이즈가 100개면 IN 100개로 1~2번 쿼리 실행
- 컬렉션 (N:1) 최적화에 매우 좋음
- DTO로 직접 조회(JPQL)
- 필요한 데이터만 조회 가능
- 근본적인 해결 가능
-
단점 : 엔티티가 아니므로 영속성 컨텍스트 관리 불가
@Query("SELECT new com.dto.OrderMemberDto(o.id, m.name) FROM Order o JOIN o.member m") List<OrderMemberDto> findOrders();
-
- 정의