정의

    연관 관계가 설정된 엔티티를 조회할 때, 조회된 데이터 개수(N)만큼 연관 관계 조회 쿼리가 추가로 발생하는 현상

    → 즉, 한 번의 조회(1)로 다수의 엔티티(N)를 가져왔는데, 각 엔티티마다 연관된 데이터를 조회하기 위해 추가 쿼리 N개가 발생하는 현상


    예시 상황

    블로그 유저와 게시글이 1:N 관계일 때

    @Entity
    public class User {
        @Id @GeneratedValue
        private Long id;
    
        private String name;
    
        @OneToMany(mappedBy = "user", fetch = FetchType.EAGER) // 즉시 로딩
        private List<Post> posts = new ArrayList<>();
    }
    
    @Entity
    public class Post {
        @Id @GeneratedValue
        private Long id;
        private String title;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "user_id")
        private User user;
    }
    • findAll() 로 모든 유저(User)를 조회하면, 각 게시글(Post)을 조회하기 위한 추가 쿼리 발생

    findAll() 글로벌 패치 전략별 N + 1 문제 상황

    findAll() 동작 시 쿼리 흐름

    • 내부적으로 실행되는 JPQL: select u from User u;
    • 글로벌 패치 전략을 고려하지 않고 쿼리 실행해 단순히 User만 조회

    글로벌 패치 전략

    FetchType 쿼리 동작 N+1 발생 여부 설명
    EAGER (즉시로딩) 각 User마다 연관 엔티티(Post)를 즉시 로딩 ✅ 발생 findAll()이 모든 User를 가져온 후, 각 User마다 Post를 즉시 조회
    LAZY (지연로딩) 연관엔티티를 프록시로 로딩하고, 실제 접근 시 쿼리 실행 ⚠️ 조건부 발생 findAll() 시에는 N+1 없음,
    user.getPosts() 접근 시 N번 추가 쿼리 발생

    즉시 로딩일 때 실행되는 SQL

    -- findAll() 실행 시
    select u from User u;
    
    -- 이후 각 user의 posts를 즉시로딩
    select p from Post p where p.user_id = ?;
    select p from Post p where p.user_id = ?;
    select p from Post p where p.user_id = ?;
    ...

    → User 1번 조회해도, 연관된 POST N번 조회 ⇒ 총 N + 1 쿼리 발생


    N + 1 문제 해결 방법

    1. Fetch Join 사용

    JPQL 에서 join fetch 구문을 사용하여 한 번의 쿼리로 연관 엔티티까지 함께 로딩

      public interface UserRepository extends JpaRepository<User, Long> {
    
          @Query("select distinct u from User u left join fetch u.posts")
          List<User> findAllWithPosts();
      }
    • 실행되는 SQL 예시:select distinct u.*, p.* from user u left join post p on u.id = p.user_id;
    • → 한 번의 쿼리로 User + Post 모두 조회

    2. @EntityGraph 사용

    fetch join과 동일한 효과를 어노테이션 기반으로 제공

      public interface UserRepository extends JpaRepository<User, Long> {
    
          @EntityGraph(attributePaths = {"posts"}, type = EntityGraphType.FETCH)
          List<User> findAll();
      }

    → 내부적으로 left join fetch u.posts가 자동 추가되어 실행

    → 코드가 더 깔끔하고, 재사용성 높음

    3. Batch Size 조정 (Hibernate 전용)

    @BatchSize나 글로벌 설정으로 일정개수의 연관 엔티티 묶어서 조회하도록 설정

      @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
      @BatchSize(size = 100)
      private List<Post> posts;
      # application.yml
      spring:
        jpa:
          properties:
            hibernate.default_batch_fetch_size: 100

    N + 1 → N/100 + 1 로 성능 개선 가능 (N번의 쿼리를 100개단위로 묶어서 실행)


    요약

    해결 방법 설명 장점 단점
    Fetch Join JPQL로 한 번에 연관 엔티티 로딩 완전한 해결 복잡한 쿼리 작성 필요
    @EntityGraph 어노테이션 기반 fetch join 간결, 재사용성 높음 복잡한 조건 쿼리엔 제약
    Batch Size 조정 여러 연관 엔티티를 묶어서 조회 간단히 적용 가능 완전한 해결은 아님

    참고: maeil-mail 매일메일 - JPA의 N + 1 문제에 대해서 설명해주세요.

    정의

    JPA에서 영속성 컨텍스트(Persistence Context) 를 관리하는 핵심 인터페이스

    즉, 엔티티를 영속성 컨텍스트에 등록.조회.수정.삭제하는 창구 역할

    • 하나의 EntityManager 는 하나의 영속성 컨텍스트를 관리

    ❓영속성 컨텍스트(Persistence Context)란?

    엔티티를 영구 저장하는 환경

    • 애플리케이션과 데이터베이스 사이에서 엔티티를 1차 캐시로 관리
    • DB 접근을 최소화하고, 객체 중심의 CRUD를 가능하게 함

    주요 기능

    • 1차 캐시: 동일한 엔티티를 여러 번 조회해도 DB에 재요청하지 않고 캐시에서 반환
    • 쓰기 지연(Transactional Write-Behind): persist() 시 즉시 INSERT하지 않고, 트랜잭션 커밋 시점에 한꺼번에 SQL 실행
    • 변경 감지(Dirty Checking): 영속 상태의 엔티티 필드 변경을 감지해 자동으로 UPDATE SQL 생성
    • 지연 로딩(Lazy Loading): 연관된 엔티티를 실제 사용할 때 쿼리 실행

    엔티티 상태

    상태 설명 전이 메서드
    비영속 (Transient) 새로 생성된 객체로, 아직 영속성 컨텍스트에 저장되지 않은 상태 new Member()
    영속 (Persistent) 엔티티가 영속성 컨텍스트에 저장되어 관리되는 상태 em.persist(entity)
    준영속 (Detached) 한 번 영속되었다가 컨텍스트에서 분리된 상태 em.detach(), em.clear(), em.close()
    삭제 (Removed) 삭제 예약된 상태, 커밋 시 DB에서 삭제 em.remove(entity)

    EntityManager 주요 역할

    persist(entity): 엔티티 등록

    • 엔티티를 영속성 컨텍스트에 저장하고, DB에 INSERT 준비

    find(Entity.class, id): 엔티티 조회

    • 1차 캐시 → DB 순으로 조회

    merge(detachedEntity): 엔티티 병합

    • 준영속 상태의 엔티티를 다시 영속 상태로 변경

    remove(entity): 엔티티 삭제

    • 영속성 컨텍스트에서 제거, 트랜잭션 커밋 시 DELETE 실행

    flush(): 플러시

    • 쓰기 지연 SQL을 즉시 DB에 반영

    clear(), close(): 영속성 컨텍스트 초기화/종료

    • 관리 중인 엔티티 제거 및 컨텍스트 종료

    createQuery(), createNativeQuery(): JPQL/Native Query 실행

    • SQL 직접 실행 가능

    상태 전이 예시 코드

    @Entity
    public class Member {
        @Id @GeneratedValue
        private Long id;
        private String name;
    
        public Member(String name) {
            this.name = name;
        }
    }
    
    // 엔티티 매니저 사용 예시
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    
    tx.begin();
    
    // 1️⃣ 비영속 상태
    Member member = new Member("산초");
    
    // 2️⃣ 영속 상태로 전환
    em.persist(member); // INSERT SQL은 아직 DB에 실행되지 않음 (쓰기 지연)
    
    // 3️⃣ 변경 감지 (Dirty Checking)
    member.setName("라쉬포드"); // 트랜잭션 커밋 시 UPDATE SQL 자동 생성
    
    // 4️⃣ 준영속 상태 전환
    em.detach(member); // 이제 변경 사항은 더 이상 반영되지 않음
    
    // 5️⃣ 삭제 상태
    em.remove(member); // DELETE 예약
    
    tx.commit(); // INSERT/UPDATE/DELETE SQL이 실제 DB 반영
    em.close();
    

    Spring Data JPA에서는?

    // org.springframework.data.jpa.repository.support.SimpleJpaRepository
    
    public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID> {
    
        private final EntityManager entityManager;
        private final JpaEntityInformation<T, ?> entityInformation;
    
        public SimpleJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager em) {
            this.entityInformation = entityInformation;
            this.entityManager = em;
        }
    }
    
    ...
    
    @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);  // 병합하여 영속 상태로 전환
        }
    }
    • EntityManager를 직접 쓰지 않지만, JpaRepositorySimpleJpaRepository를 통해 내부적으로 사용

    → 즉, save(), findById() 같은 메서드들은 결국 em.persist(), em.find()로 구현되어 있음

    • 엔티티 매니저의 동작 원리를 알아야, 영속성 전이, 캐싱, flush 타이밍 등의 문제를 디버깅 할 수 있음!

    참고: maeil-mail 매일메일 - 엔티티 매니저에 대해 설명해주세요.

    JPA ddl-auto 옵션

    Spring Boot에서 Hibernate와 같은 JPA 구현체를 사용할 때, 데이터베이스 스키마 생성 및 변경 방식을 제어하는 설정

    • 설정 위치:
      • application.properties 또는 application.yml 

    ex) application.yml

    spring:
      jpa:
        hibernate:
          ddl-auto: update

    옵션별 동작 방식

    none

    • 데이터베이스 스키마와 연관된 어떠한 작업도 수행하지 않음
    • 수동으로 관리하고 싶을 때 유용
    • 운영 환경에서 주로 사용

    validate

    • 애플리케이션이 시작될 때, 엔티티 매핑이 데이터베이스 스키마와 일지하는지 검증
    • 스키마 변경은 수행하지 않음
    • 운영 환경에서 엔티티와 데이터베이스 스키마가 일치하는지 확인하고 싶을 때 사용

    update

    • 엔티티 매핑과 데이터베이스 스키마를 비교하여 필요한 경우 스키마 업데이트
    • 기존 데이터 유지되지만, 새로운 엔티티나 변경된 엔티티 필드는 스키마에 반영
    • 엔티티에 변경이 발생할 때, 자동으로 스키마 업데이트가 필요할 때 유용
    • 운영 환경에서는 주의 필요

    create

    • 애플리케이션이 시작될 때 기존 스키마를 삭제하고 새로 생성
    • 데이터가 모두 삭제되며 엔티티 매핑을 기반으로 새로운 스키마 생성
    • 개발 초기에 빈 데이터베이스 스키마를 반복적으로 생성해야할 때 유용
    • 운영 환경에서 사용하지 않음

    create-drop

    • 애플리케이션이 시작될 때 기존 스키마를 삭제하고 새로 생성한 후, 종료될 때 스키마 삭제
    • 테스트 환경에서 일시적인 데이터베이스 스키마가 필요한 경우 유용
    • 매 테스트 실행 시마다 깨끗한 데이터베이스상태 유지하고자 할 때 사용
    • 운영 환경에서 사용하지 않음

    동작 방식 정리표

    옵션 동작 설명 주요 특징 권장 환경
    none 아무런 스키마 작업도 수행하지 않음 DB 스키마를 수동으로 관리할 때 사용 운영 환경
    validate 엔티티 매핑과 실제 DB 스키마가 일치하는지 검증만 수행 불일치 시 예외 발생 / 스키마 수정 없음 운영 환경 (검증용)
    update 엔티티와 DB 스키마 비교 후, 필요한 변경 자동 반영 기존 데이터 유지 / 컬럼 추가·수정 자동 반영 개발·테스트용, 운영 시 주의
    create 기존 스키마 삭제 후 새로 생성z 모든 데이터 삭제 / 매번 초기화 개발 초기
    create-drop 애플리케이션 시작 시 스키마 생성, 종료 시 스키마 삭제 테스트 후 자동 정리 / 매번 깨끗한 DB 유지 테스트 환경

     


    운영 환경에서 스키마 변경은?

    • 스키마 자동 변경 옵션(update, create, create-drop) 절대 사용하지 않음!
    • → 자동 변경으로 인해 데이터 손실, 무결성 문제, 배포 실패 등의 위험

    안전한 방법:

    • 마이크레이션 도구 사용
      • Flyway 또는 Liquibase 등 사용하여 제어된 방식으로 스키마 관리
      • 버전별 SQL 스크립트로 변경 이력 관리 및 자동화 가능
    • 스키마 변경 시기 제어
      • 사용자 트래픽이 적은 새벽 시간대에 배포 및 스키마 변경 수행
      • CI/CD 파이프라인과 연동해 DB 스키마 변경 자동화 가능

    참고: maeil-mail 매일메일 - JPA의 ddl-auto 옵션은 각각 어떤 동작을 하고 어떤 상황에서 사용해야 할까요?

    핵심: 새로운 Entity 파악이 틀리면, 불필요한 조회가 일어나 비효율적!

    [SimpleJpaRepository#save]

    // org.springframework.data.jpa.repository.support.SimpleJpaRepository;
    
    @Transactional
    public <S extends T> S save(S entity) {
        Assert.notNull(entity, "Entity must not be null");
        if (this.entityInformation.isNew(entity)) {
            this.entityManager.persist(entity);
            return entity;
        } else {
            return this.entityManager.merge(entity);
        }
    }
    • Entity 저장 시, JpaEntityInformation의 isNew(T entity) 로 신규 여부 확인

    [JpaEntityInformation#isNew]

    1) [JpaMetamodelEntityInformation#isNew]

      // org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation
      @Override
      public boolean isNew(T entity) {
    
          // 1) @Version 필드가 없거나, primitive 타입이면 → 부모 로직으로(ID 기반)
          if (versionAttribute.isEmpty()
              || versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) {
              return super.isNew(entity); // AbstractEntityInformation#isNew
          }
    
          // 2) @Version 필드가 "래퍼 타입"이면 → null 여부로 신규 판단
          BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
    
          return versionAttribute
                  .map(it -> wrapper.getPropertyValue(it.getName()) == null)
                  .orElse(true);
      }
    • @Version 필드가 있고, 그 타입이 Wrapper Type(ex: Long) → 버전 값이 null 이면 신규
      • 그 외, AbstractEntityInformation#isNew 호출

    2) [AbstractEntityInformation#isNew]

    // org.springframework.data.repository.core.support.AbstractEntityInformation
    
    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));
    }
    • @Id 어노테이션 사용 필드 (ID 타입)로 신규 여부 판단
      • Id가 primitive 이 아니면? null 여부 확인 (id == null 이면 신규)
      • Id가 Number primitive 라면? 0인지 여부 확인 (longValue() == 0L 이면 신규)
      • 그 외 primitive? 지원 안함(예외)

    3) [JpaPersistableEntityInformation#isNew]

    // org.springframework.data.jpa.repository.support.JpaPersistableEntityInformation
    
    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();
        }
    }
    • 도메인 엔티티가 Persistable을 구현하면, 엔티티가 직접 isNew() 결정

    @GeneratedValue 어노테이션으로 키 생성 전략 사용하면?

    • 키 생성 전략(IDENTITY, SEQUENCE, AUTO 등)을 사용하면, DB 저장 시점에 ID가 자동 할당
    • ⇒ 저장 전 메모리 상태에서는 id == nullisNew() == true → 새로운 Entity → persist()

    직접 ID를 할당하는 경우에는?

    • 엔티티 생성 시 이미 ID가 존재하므로 id != nullisNew() == falsemerge()
    • ⇒ 실제로는 신규 엔티티인데도 DB 조회 + merge 복사 잡업이 수행되어 비효율
    • 엔티티에서 Persistable<T> 인터페이스를 구현해 isNew()를 직접 정의하여, JpaPersistableEntityInformation가 정확히 신규 여부를 판단하도록 해야 함

    새로운 Entity인지 판단하는 게 왜 중요할까?

    // org.springframework.data.jpa.repository.support.SimpleJpaRepository;
    
    @Transactional
    public <S extends T> S save(S entity) {
        Assert.notNull(entity, "Entity must not be null");
        if (this.entityInformation.isNew(entity)) {
            this.entityManager.persist(entity);
            return entity;
        } else {
            return this.entityManager.merge(entity);
        }
    }
    • persist()
      • INSERT 전용
      • 영속 컨텍스트에 새 엔티티 등록
      • 불필요한 SELECT
    • merge()
      • 준영속/Detached 객체를 현재 영속 컨텍스트에 복사
      • 내부적으로 DB 조회(SELECT)가 선행될 수 있음
      • 신규 엔티티에 쓰면 비효율 + 의도 혼동 → 성능/의도 모두 손해

    정리표

    상황 isNew 판단 기준 결과
    @Version 래퍼 타입(Long/Integer 등) 존재 버전 값이 null → 신규 persist
    @Version 없음 or primitive 타입 ID 기반 판단으로 위임 아래 행 참조
    ID 타입이 래퍼/참조 타입 ID == null → 신규 persist
    ID 타입이 primitive 숫자 ID == 0 → 신규 persist
    직접 ID 할당(Assigned) + @GeneratedValue 미사용 ID가 채워짐 → 신규 아님으로 판단될 위험 기본에선 merge (비효율 가능) → Persistable로 교정 권장
    Persistable 구현 엔티티의 isNew() 리턴값 리턴값에 따라 persist/merge

    참고: maeil-mail 매일메일 - Spring Data JPA에서 새로운 Entity인지 판단하는 방법은 무엇일까요?

     

    스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻한다

     

    스프링은 다음과 같은 다양한 스코프를 지원

    • 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
    • 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
    • 웹 관련 스코프
      • request웹 요청이 들어오고 나갈때 까지 유지되는 스코프
      • session웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
      • application웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

     

    싱글톤 빈 스코프

    싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환

    1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청
    2. 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환
    3. 이후에 스프링 컨테이너에 같은 요청이 와도 같은 객체 인스턴스의 스프링 빈을 반환

    프로토타입 스코프

     

    프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환

    1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청
    2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입
    3.  스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환
    4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환 

     

    프로토타입 빈의 특징 정리

    • 스프링 컨테이너에 요청할 때 마다 새로 생성
    • 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여
    • 종료 메서드가 호출되지 않음
    • 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리
    • 종료 메서드에 대한 호출도 클라이언트가 직접 해야함

    프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

    스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환하지만, 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의

     

    1. 클라이언트A는 스프링 컨테이너에 프로토타입 빈을 요청
    2. 스프링 컨테이너는 프로토타입 빈을 새로 생성해서 반환(x01), 해당 빈의 count 필드 값은 0
    3. 클라이언트는 조회한 프로토타입 빈에 `addCount()` 를 호출하면서 count 필드를 +1
    4. 결과적으로 프로토타입 빈(x01)의 count는 1
    5. 클라이언트 B도 같은 과정을 반복하여 프로토타입 빈(x02)의 count도 1

     

    싱글톤 빈에서 프로토타입 빈 사용(clientBean)

    1. clientBean 은 싱글톤이므로, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생
      1. clientBean 은 의존관계 자동 주입을 사용한다주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청
      2. 스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean 에 반환, 프로토타입 빈의 count 필드 값은 0
      3. 이제 clientBean 은 프로토타입 빈을 내부 필드에 보관 (정확히는 참조값을 보관)
    2. 클라이언트 A는 clientBean 을 스프링 컨테이너에 요청해서 받고, 싱글톤이므로 항상 같은 clientBean 이 반환
      1. 클라이언트 A는 clientBean.logic() 을 호출
      2. clientBean 은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가 ➡︎ count값이 1
    3. 클라이언트 B는 clientBean 을 스프링 컨테이너에 요청해서 받고, 싱글톤이므로 항상 같은 clientBean 이 반환 (여기서 중요한 점, clientBean 이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈, 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지사용 할 때마다 새로 생성되는 것이 아님!)
      1. 클라이언트 B는 clientBean.logic() 을 호출
      2. clientBean 은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가 ➡︎ 래 count 값이 1이었으므로 2가 됨

     

    프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결

    • 실행해보면 ac.getBean() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인
    • 의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 조회(탐색) 
    • 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워짐
    • 지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱! DL 정도의 기능만 제공하는 무언가가 있으면 됨

     

    ObjectFactory, ObjectProvider

    • 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider, 참고로 과거에 ObjectFactory 가 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider 가 만들어짐
    • 실행해보면 prototypeBeanProvider.getObject() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인
    • ObjectProvider 의 getObject() 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환 (DL)
    • 스프링이 제공하는 기능을 사용하지만기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워짐
    • 지금 딱 필요한 DL 정도의 기능만 제공
    • ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
    • ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존

     

    JSR-330 Provider

    • JSR-330 자바 표준을 사용하는 방법 - 스프링 부트 3.0은 jakarta.inject.Provider 사용
    • 실행해보면 provider.get() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인 
    • provider 의 get() 을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환 (DL)
    • 자바 표준이고기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워짐
    • Provider 는 지금 딱 필요한 DL 정도의 기능만 제공
    • get() 메서드 하나로 기능이 매우 단순
    • 별도의 라이브러리가 필요
    • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용 가능

     

    웹 스코프

    • 웹 스코프는 웹 환경에서만 동작
    • 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리, 따라서 종료 메서드가 호출

    웹 스코프 종류

    • request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리
    • session: HTTP Session과 동일한 생명주기를 가지는 스코프
    • application: 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
    • websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프

     

    빈 생명주기 콜백

    애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요

     

    • 스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료됨
    • 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 함
    • 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능 제공
    • 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 줌

     

    스프링 빈의 간단한 라이프사이클

    • 객체 생성 ➡︎ 의존관계 주입

    스프링 빈의 이벤트 라이프사이클

    • 스프링 컨테이너 생성 ➡︎ 스프링 빈 생성 ➡︎ 의존관계 주입 ➡︎ 초기화 콜백 ➡︎ 사용 ➡︎ 소멸전 콜백 ➡︎ 프링 종료
      • 초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
      • 소멸전 콜백: 빈이 소멸되기 직전에 호출

     

    ✚ 객체의 생성과 초기화를 분리

    • 생성자: 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임
    • 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행

     


    빈 생명주기 콜백의 3가지 방법

    1. 인터페이스(InitializingBean, DisposableBean)
    2. 설정 정보에 초기화 메서드, 종료 메서드 지정
    3. @PostConstruct, @PreDestroy 애노테이션 지원

     

    1. 인터페이스(InitializingBean, DisposableBean)

    • 초기화: InitializingBean - afterPropertiesSet() 메서드
    • 소멸: DisposableBean - destroy() 메서드
    public class NetworkClient implements InitializingBean, DisposableBean {
        
        ...
        중략
        ...
        
        @Override
        public void afterPropertiesSet() throws Exception {
        	connect();
            call("초기화 연결 메시지"); 
        }
        
        @Override
        public void destroy() throws Exception {
            disConnect();
        }
    }

     

    단점

    • 초기화, 소멸 메서드의 이름 변경 불가
    • 외부 라이브러리에 적용 불가
    • 초창기 방법이라 거의 사용하지 않음

     

     

    2. 빈 등록 초기화, 소멸 메서드 지정

    설정 정보에 @Bean(initMethod = "init", destroyMethod = "close") 처럼 초기화, 소멸 메서드를 지

     

    public class NetworkClient {
    	
        ...
        중략
        ...
        
        public void init() { 
            System.out.println("NetworkClient.init");
            connect();
            call("초기화 연결 메시지");
        }
    	
        public void close() {
        	System.out.println("NetworkClient.close");
            disConnect();
        }
    }
    @Configuration
    static class LifeCycleConfig {
    
        @Bean(initMethod = "init", destroyMethod = "close")
        public NetworkClient networkClient() {
        	...
        }
    }

     

    특징

    • 메서드 이름 자유로움
    • 스프링 빈이 스프링 코드에 의존하지 않음
    • 설정 정보를 사용하기에 외부 라이브러리에도 적용 가능

     

     

    3. 애노테이션 @PostConstruct, @PreDestroy 

    public class NetworkClient {
    	
        ...
        중략
        ...
        
        @PostConstruct
        public void init() { 
            System.out.println("NetworkClient.init");
            connect();
            call("초기화 연결 메시지");
        }
        
        @PreDestroy
    	public void close() {
     		System.out.println("NetworkClient.close");
     		disConnect();
        }
    }

     

    특징

    • 가장 편리하게 초기화와 종료를 실행
    • 최신 스프링 권장 방법
    • 컴포넌트 스캔과 잘 어울림
    • 외부 라이브러리에 적용 불가

     

     

    + Recent posts