2025-11-16

오늘 배운 것

자바

  • 불변 객체(Immutable)
    • 정의
      • 한 번 생성되면 내부 상태가 절대 변하지 않는 객체
      • 값이 바뀌는 순간 “새 객체”를 만들어야 한다
      • 예 : String, Integer, Long, LocalDate, LocalDateTime
    • 중요한 이유
      • Thread-safe
        • 복사 없이 여러 스레드가 공유해도 절대 문제 없음
        • 멀티 스레드 환경에서 가장 안전한 방식
      • 예측 가능(버그 감소)
        • 객체가 어디서 값이 바뀌는지 추적할 필요 없음
      • equals, hashCode 안정
        • HashMap, Set에 key로 안전하게 사용 가능
        • 값이 변하면 해시가 깨져 Key를 잃어버리는 문제 발생
      • 부작용 없음
        • 특정 메서드에서 외부 객체 값을 몰래 변경하는 오류 방지
      • 함수형 프로그래밍 기반
        • Java 8 Stream, Lambda, Optional 등에서 불변 객체 사용이 핵심 철학
      • String
        • 내부 char 배열(value[])도 final
        • concat, replace 호출해도 기존 객체 수정이 아닌 새객체 생성
      • Wrapper 클래스(Integer, Long, Double 등)
        • 내부 값이 final
      • Java Time API (LocalDate, LocalDateTime 등)
        • 날짜 변경 메서드는 항상 새로운 객체 반환
    • 불변 객체 만들 때 지켜야 할 규칙
      • 필드 모두 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
        • 개발자가 처리해야 하는 예외
    • Checked vs Unchecked
      • Checked Exception
        • 컴파일 시점에 반드시 처리해야 함
        • 예 : IOException, SQLException, ClassNotFoundException
        • 특징
          • try-catch 또는 throws 필수
          • 파일, DB, 네트워크 처럼 실패 가능성이 높은 동작
      • Unchecked Exception (RuntimeException)
        • 컴파일러가 강제하지 않음
        • 예 : NullPointerException, IllegalArgumentException, IndexOutOfBounds
        • 특징
          • 프로그래밍 실수(버그)에서 발생
    • 예외 처리 방법
      • 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 사용 가능
    • 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)
    • Java 21 (LTS)
      • Virtual Thread
        • 수천 ~ 수백만 스레드 만드는 것도 부담 없음
        • 스프링 부트 3 + WebFlux에서 각광
      • Pattern Matching for Switch
      • Sequenced Collections

스프링

  • 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 MVC 흐름(DispatcherServlet 중심)
    • Spring MVC란?
      • 웹 요청을 처리하기 위한 스프링의 아키텍처 패턴
      • 요청 → 컨트롤러 → 비즈니스 로직 → 응답 흐름을 표준화
      • 구조적으로 Front Controller 패턴을 사용
        • DispatcherServlet = FrontController
          • HTTP 프로토콜로 들어오는 모든 요청을 먼저 받아서 적합한 컨트롤러에 위임
      • DispatcherServlet이 모든 요청의 입구 역할
    • Spring MVC 전체 요청 흐름
      1. 클라이언트 요청
      2. DispatcherServlet이 요청 수신
      3. HandlerMapping으로 어떤 컨트롤러를 호출할지 탐색
      4. HandlerAdapter가 컨트롤러 실행 방식에 맞는 어댑터 선택
      5. Controller 메서드 호출
      6. 비즈니스 로직 수행 (Service → Repository)
      7. Controller가 ModelAndView 또는 ResponsneBody 반환
      8. ViewResolver가 어떤 View를 렌더링할지 선택
      9. DispatcherServlet이 최종 Response 반환
    • 컴포넌트 별 역할
      • DisptacherServlet
        • Spring MVC의 중심
        • 요청을 받아 어떤 컨트롤러에게 전달할지, 어떤 뷰를 반환할지 전 과정 조율
      • HandlerMapping
        • 요청 URL에 매칭되는 컨트롤러 탐색
      • HandlerAdapter
        • 컨트롤러 호출 방식을 표준화
        • @Controller, @RestController, RequestMappingHandlerAdapter 등 방식 통합
      • Controller
        • 요청 → 비즈니스 로직 → 응답 데이터 생성
      • ViewResolver
        • 뷰 템플릿 경로 매핑
        • Thymeleaaf, JSP, JSON 변환 등
    • Spring에서 JSON 응답이 자동으로 되는 이유
      • Spring Boot의 경우 HttpMessageConverters + Jackson 라이브러리가 자동 등록되기 때문
      • @RestController 또는 @ResponseBody가 있으면 객체 → JSON 변환 자동 수행
  • 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 컨테이너가 생성하고 관리하는 객체
      • 스프링이 직접 관리하는 인스턴스
      • 등록 방식
        1. 컴포넌트 스캔(@Component 계열)
          • @Component, @Service, @Repository, @Controller
        2. 수동 Bean 등록 (@Bean + @Configuration)
          • 동적으로 객체를 생성해야 하는 경우
            • 운영/개발 환경에 따라 다른 구현체 사용해야 될 때
            • 특정 설정값에 따라 빈 결정해야 될 때
            • 전략 패턴으로 여러 구현체 중 하나 선택해야 하는 경우(카드결제, 계좌 결제 등등)
          • 외부 라이브러리 Bean 등록할 때
            • JWT 라이브러리, ObjectMapper, RestTemplate, RedisClient 등
          • 파라미터를 넣어서 객체를 생성해야 할 때
            • @Component 방식은 파라미터 있는 생성자를 마음대로 사용 불가
              • 직접 빈 생성해야 함
          • 테스팅을 위해 대체 구현체를 넣어야 할 때
    • ApplicationContext (스프링 IoC 컨테이너)
      • DI를 실제로 수행하고 Bean을 관리하는 핵심 컨테이너
      • 역할
        • Bean 생성
        • Bean 의존성 주입
        • Bean 생명주기 관리
        • Bean 검색
      • 스프링 부트에서는 실행하며 바로 ApplicationContext가 뜸
  • Bean 생성 과정과 생명주기
    • Spring Bean 생명 주기 전체 흐름
      • 스프링 컨테이너는 Bean을 등록하고, 의존성을 주입하고, 초기화 콜백을 수행한 뒤 사용하다가 종료 시점에 소멸 콜백을 호출합니다
    • Bean 생성과정
      1. Bean 정의(BeanDefinition) 스캔
        • @ComponentScan, @Bean 등을 읽어 Bean 메타 정보를 등록
      2. Bean 인스턴스 생성
        • new 키워드로 객체 생성(생성자 호출)
      3. 의존성 주입 수행
        • 생성자, Setter 등을 통해 필요한 객체 넣어줌
      4. BeanNameAware
        • 자신의 Bean 이름을 알 수 있게 함
      5. BeanFactoryAware / ApplicationContextAware
        • 컨테이너 객체를 주입받음
      6. 초기화 콜백(init)
        • @PostConstruct, IntializingBean.afterPropertiesSet(), @Bean(initMethod=””) 중 하나 선택하여 이 단계에서 진짜 초기화 로직 수행
        • 예 : DB 커넥션 열기
      7. Bean 사용
      8. 소멸 콜백 (Destroy)
        • @PreDestory, DisposableBean.destroy(), @Bean(destroyMethod=””) 로 리소스 해제 (DB 커넥션 닫기 등)
    • 초기화 / 소멸 콜백 방식
      • @PostConstruct, @PreDestroy

          @Component
          public class ConnectionPool {
                    
              @PostConstruct
              public void init() {
                  // 초기화 작업
              }
                    
              @PreDestroy
              public void close() {
                  // 종료 작업
              }
          }
        
      • 이외에도 여러 방식이 있는데, 이유는 스프링 외부 라이브러리는 @PostConstruct 사용불가하여 initMethod가 필요

  • 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 (싱글톤과 비슷하지만 웹 컨텍스트 기준)
  • 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를 선호하는 경향이 강함
    • 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;
          }
        
  • @Transactional 동작 원리
    • AOP + 프록시로 동작
    • 역할
      • 메서드 실행 전 트랜잭션 시작
      • 메서드가 정상 종료되면 commit
      • 예외 발생시 rollback
    • 동작 구조
      • @Transactional = Proxy를 만들어 트랜잭션 로직을 감쌈
      • 서비스 메서드를 호출할 때 프록시가 먼저 가로채서 트랜잭션 제어

          Client
             
          [Proxy]   트랜잭션 시작/커밋/롤백
             
          [Real Service]   실제 비즈니스 로직
        
    • @Transactional 흐름
      1. 스프링은 @Transational이 붙은 Bean을 감싸는 Proxy 생성
      2. 클라이언트가 메서드를 호출하면 Proxy가 먼저 실행
      3. Proxy가 트랜잭션 시작
      4. 실제 서비스 메서드 실행
      5. 정상 종료 시 commit
      6. 예외 발생 시 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
        • 트랜잭션이 너무 오래 걸릴 때 자동 종료
  • JPA 기본 개념
    • 정의
      • Java Persistence API
      • 자바 ORM(Object Relational Mapping) 표준 인터페이스
      • 자바 객체와 데이터베이스 테이블을 자동으로 매핑해주는 기술
      • JPA는 인터페이스
      • 실제 구현체는 Hibernate, EclipseLink(대부분 Hibernate 사용)
    • 사용 이유
      • 기존 JDBC 문제
        • SQL 반복 작성
        • 데이터 매핑 반복
        • 변경 시 모든 SQL 수정
        • CRUD 중복 코드 많음
        • 객체와 테이블 간 불일치
      • JPA 장점 ⇒ 생산성과 유지보수 크게 증가
        • 반복 코드 제거 (CRUD 자동)
        • 객체 중심 모델 기반 개발
        • 캐싱, 더티 체킹 등 성능 최적화
        • 변경된 필드 자동 반영(UPDATE SQL 자동 생성)
        • 지연 로딩으로 최적화 가능
    • 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 시점에 모아서 실행
    • 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로 동기화 하는 작업일 뿐, 영속성 컨텍스트는 유지됨
    • 엔티티 상태

      상태 설명
      비영속(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 을 생성하는 기능
      • 동작 원리
        1. 영속성 컨테스트에 엔티티 저장 시 스냅샷 저장
        2. 엔티티 값 변경
        3. flush 시점에 스냅샷과 현재 상태 비교
          1. 값이 다르면 → 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 두 개 한 번에 실행
          
  • 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, 배치 사이즈 설정 등이 무력화됨
  • 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(컬렉션 연관)
        • 순회 시 연관된 데이터마다 추가 쿼리 발생
    • 해결 방법(중요도 순)
      • 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();
          

results matching ""

    No results matching ""