영속성 컨텍스트

이전에 얘기했듯 웹 애플리케이션에서는 하나의 EntityManagerFactory를 생성하고, 사용자의 요청에 따라 EntityManager를 생성하여 트랜잭션을 처리한다. 영속성 컨텍스트(Persistence Context)는 애플리케이션 내에서 객체 인스턴스를 일시적으로 보관하고 관리하는 논리적 환경이다. JPA를 이해하는데 가장 중요한 단어이다.

영속성 컨텍스트는 쉽게 말해 가상의 데이터베이스 역할을 하는 논리적인 개념으로, 눈에 보이지 않는다. EntityManager가 생성될 때 같이 생성되며, 보통 하나의 트랜잭션 범위 동안 유지된다. 객체의 생명주기는 비영속(New/Transient), 영속(Managed), 준영속(Detached), 삭제(Removed)로 나뉜다.

  • 비영속(New/Transient): 객체가 새로 생성만 되어 영속성 컨텍스트와 연관이 없는 상태
  • 영속(Managed): persist, find 등으로 영속성 컨텍스트에서 관리되는 상태
  • 준영속(Detached): 영속성 컨텍스트에서 분리된 상태
    • 영속성 컨텍스트가 제공하는 기능을 사용 못하게 된다.
    • em.detach(객체), em.clear(), em.close()로 준영속 상태로 만들 수 있다.
    • 각각 특정 객체만 전환, 전체 초기화, 종료의 동작을 한다.
  • 삭제(Removed): 영속성 컨텍스트에서 삭제된 상태

영속성 컨텍스트의 장점(또는 주요 특징)으로는 1차 캐시, 동일성(Identity) 보장, 쓰기 지연(Write-behind), 변경 감지(Dirty Checking), 지연 로딩(Lazy Loading)이 있다.

  • 1차 캐시: 컨텍스트 내부에 객체들이 캐시되어 있어, 동일한 객체를 여러번 조회해도 최초 한 번만 DB에서 읽고, 이후에는 영속성 컨텍스트에서 조회한다. 영속성 컨텍스트에 없는 객체를 find하면 DB에서 조회 후 1차 캐시에 저장한다.
  • 동일성 보장: 영속성 컨텍스트 내에서 같은 PK를 가지는 객체는 항상 같인 객체로 식별된다.
  • 쓰기 지연: persist() 나 객체 추가 작업들이 먼저 영속성 컨텍스트에 저장되고, 실제 DB에는 트랜잭션 커밋 시 일괄로 반영된다. 이때 쓰기 지연 SQL 저장소에 쿼리를 저장하고 있다가 한 번에 실행한다.
  • 변경 감지: 트랜잭션 내에서 객체의 값이 변경되면, 커밋 시점에 자동으로 변경 내용을 탐지해 DB에 반영한다.

쓰기 지연에서 '트랜잭션 커밋 시 일괄로 반영된다'고 했는데, 이때 flush가 실제로 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다. 커밋 할 때 flush가 자동(jpql 쿼리 실행 시에도)으로 호출되며, em.flush()로 직접 호출할 수도 있다. flush는 이름과 다르게 영속성 컨텍스트를 비우는 것이 아니며, 객체는 계속 영속 상태를 유지한다. flush가 발샐하면 다음 단계로 진행된다.

  1. 변경 감지로 변경된 객체를 찾는다.
  2. 쓰기 지연 SQL 저장소에 변경 SQL(INSERT, UPDATE, DELETE)를 등록한다.
  3. 등록된 쿼리를 실제 DB에 전송하여 반영한다.

'스터디 > JPA' 카테고리의 다른 글

JPA 스터디2 - 2  (2) 2025.07.14
JPA 스터디2 - 1  (1) 2025.07.13
JPA 스터디1 - 3  (0) 2025.07.01
JPA 스터디1 - 2  (3) 2025.06.16
JPA 스터디1 - 1  (1) 2025.06.10

프로젝트 생성

H2 데이터베이스는 기존에 사용하고 있었기에 신규 DB 파일만 생성하고 Java 프로젝트를 생성한다. 강의에서는 Java 8에 빌드도 Maven으로 설정하고 있으나, 깔린대로 해도 문제가 없을거 같아서 Java 21에 빌드는 Gradle로 설정했다.

plugins {
    id("java")
}

group = "jpa-basic"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    //spring-boot-starter-jpa 를 보통 많이 쓴다고 하지만,
    //강의에서는 maven 에 hibernate 와 h2 database 를 따로 추가했다.
    implementation("org.hibernate.orm:hibernate-core:7.0.6.Final") //hibernate
    implementation("com.h2database:h2:2.3.232") //h2 database

    testImplementation(platform("org.junit:junit-bom:5.10.0"))
    testImplementation("org.junit.jupiter:junit-jupiter")
}

tasks.test {
    useJUnitPlatform()
}

 

JPA 구현체인 Hibernate와 사용 할 데이터베이스인 H2 데이터베이스의 의존성을 추가해준다. 스프링 부트였다면 spring-boot-starter-jpa만 추가해도 두 개 다 포함하고 있으나, 이번 강의는 스프링을 사용하지 않기 때문에 제외한다.

JPA 설정 파일

JPA를 사용하기 위해서는 persistence.xml이라는 설정 파일이 필요하다. 기본적으로 META-INF 디렉토리 하위에 위치하고, persistence-unitname으로 이름을 지정하여 사용한다. 속성 중에 javax.persistence로 시작하는 속성은 JPA 표준 속성으로 구현체의 영향을 받지 않고, hibernate로 시작하는 속성은 Hibernate에서만 사용할 수 있는 속성이다.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="hello">
        <!-- 순수한 JPA 프로젝트에서는 Entity 클래스를 명시적으로 등록 -->
        <class>hellojpa.Member</class>
        <properties>
            <!-- 필수 속성 -->
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="javax.persistence.jdbc.user" value="sa"/>
            <property name="javax.persistence.jdbc.password" value=""/>
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/Downloads/spring/jpa-basic/jpa"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>

            <!-- 옵션 -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
            <!--<property name="hibernate.hbm2ddl.auto" value="create" />-->
        </properties>
    </persistence-unit>
</persistence>

JPA Dialect(방언)

JPA의 Dialect(방언)은 JPA 구현체가 어떤 데이터베이스에 맞는 SQL을 생성해야 하는지 알려주는 설정이다. JPA는 특정 데이터베이스에 종속되어있지 않고, SQL은 표준이 있지만 DB마다 문법·함수·데이터타입 등이 다르기 때문에 필요하다. 위의 JPA 설정 파일에서도 hibernate.dialect 속성을 H2 데이터베이스에 맞추었다. 만약 MySQL이나 Oracle을 사용한다면 사용하는 데이터베이스에 맞추면 된다.

기본 코드 생성

package hellojpa;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;

import java.util.List;

public class JpaMain {

    public static void main(String[] args) {
        //애플리케이션에 하나만 생성
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        //트랜잭션 필요 시 생성 및 close, 쓰레드 간 공유 X
        EntityManager em = emf.createEntityManager();

        //JPA 의 모든 데이터 변경은 트랜잭션 안에서 실행
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            /* Create
            Member member = new Member();
            member.setId(2L);
            member.setName("2Name");

            em.persist(member);
            */

            /* Update
            //JPA 를 통해서 객체를 가져오면 persist 할 필요가 없다.
            Member findMember = em.find(Member.class, 1L);
            findMember.setName("Name1");
            */

            List<Member> result = em.createQuery("select m from Member as m", Member.class)
                    .getResultList();

            for(Member member : result) {
                System.out.println("member.name = " + member.getName());
            }

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}

 

JpaMain.java를 생성하고 EntityManagerFactoryEntityManager(em)를 생성하고, 생성한 em에서 트랜잭션을 얻어와 그 안에서 데이터 변경을 실행한다. 트랜잭션이 종료되면 결과에 따라 commit 또는 rollback하게 되고, em을 반환하게 된다. 주석으로 작성하였 듯이 EntityManagerFactory는 애플리케이션에 하나만 생성하고, em은 트랜잭션 필요 시 생성하고 반환하면 된다.

 

=> EntityManagerFactory는 데이터베이스 연결 설정, JPA 메타데이터 파싱, EntityManager 생성 등 무거운 리소스를 관리하는 역할이다. 생성 비용이 매우 크고, 내부적으로 커넥션 풀, 캐시, 설정 정보 등을 보유한다. 또한 여러 스레드에서 동시에 접근해도 안전하게 설계(Thread-safe)되어 있지만, 여러 개를 만들면 시스템 자원을 낭비하고, 성능 저하 및 예기치 않은 동시성 문제가 발생할 수 있어 애플리케이션에 하나만 생성하고 공유하는 것이 가장 효율적이다.

 

=> EntityManager는 실제로 Entity를 관리(CRUD)하며, 영속성 컨텍스트※를 관리한다. 생성 비용이 크지 않고, 가볍다. 하지만 여러 스레드에서 동시에 접근하면 데이터 불일치, 예기치 않은 버그가 발생할 수 있다(Non-thread-safe). EntityManager는 트랜잭션 범위 내에서 Entity의 상태를 관리하는데, 트랜잭션이 끝나면 EntityManager와 함께 영속성 컨텍스트도 종료되어야 일관성이 보장된다. 다음과 같은 이유로 필요 시 생성하고, 공유하지 않는다.

  • 동시성 문제: 여러 트랜잭션이 하나의 `EntityManager`를 공유하면, 서로 다른 작업이 섞여 데이터 정합성이 깨진다.
  • 영속성 컨텍스트 오염: 이전 트랜잭션의 데이터가 남아있어 잘못된 결과를 반환할 수 있다.
  • JPA 명세: `EntityManager`는 트랜잭션 단위로 생성/사용/종료하는 것이 명확하게 권장된다.

※ 영속성 컨텍스트(Persistence Context)는 JPA에서 Entity 객체를 영구적으로 저장하고 관리하는 메모리 공간을 의미하는 핵심 개념 중 하나이다. Entity 생명주기 관리, 1차 캐시, 동일성 보장, 변경 감지(Dirty Checking), 쓰기 지연(Write-behind), 지연 로딩(Lazy Loading) 등의 주요 특징이 있다.

package hellojpa;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class Member {

    @Id
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 

@Entity 어노테이션이 있어야 JPA가 관리하는 객체가 된다. 순수한 Java 프로젝트를 생성했기 때문에 persistence.xml<class>hellojpa.Member</class>와 같이 Entity 클래스를 등록해줘야 인식한다.

JPQL

JPQL(Java Persistence Query Language)는 JPA에서 사용하는 객체지향 쿼리 언어다. 가장 큰 특징은 데이터베이스의 테이블이 아니라 Entity 객체를 대상으로 쿼리를 작성한다는 점인데, 검색도 테이블이 아니라 Entity 객체를 대상으로 검색한다. SQL을 추상화하기 때문에 특정 데이터베이스에 종속되지 않으며, JPA가 JPQL을 실제 DB에 맞는 SQL로 변환해서 실행한다.

            ...
            List<Member> result = em.createQuery("select m from Member as m", Member.class)
                    .getResultList();
            ...

 

위에서 작성된 모든 Member Entity를 검색하는 JPQL이다.


https://github.com/hongbre/jpa-basic

 

GitHub - hongbre/jpa-basic

Contribute to hongbre/jpa-basic development by creating an account on GitHub.

github.com

 

'스터디 > JPA' 카테고리의 다른 글

JPA 스터디2 - 3  (2) 2025.07.30
JPA 스터디2 - 1  (1) 2025.07.13
JPA 스터디1 - 3  (0) 2025.07.01
JPA 스터디1 - 2  (3) 2025.06.16
JPA 스터디1 - 1  (1) 2025.06.10

JPA란 무엇인가?

JPA는 Java Persistence API의 약자로 자바 객체를 관계형 데이터베이스에 저장하는 방식을 표준화한 자바 명세이다. ORM을 위한 인터페이스와 어노테이션을 정의하며, 자바 클래스를 데이터베이스 테이블에 매핑하거나 SQL 대신 객체지향 코드로 데이터베이스 CRUD 작업을 수행한다. JPA 자체는 '명세'이고, 실제로는 Hibernate, EclipseLink 등과 같은 구현체를 통해 동작한다. 이런 JPA는 데이터베이스 접근을 단순화하고, 생산성을 높이며, 객체지향 자바 코드와 관계형 데이터베이스 간의 간극을 줄여준다.

public class Member {
    private String memberId;
    private String name;
    private String tel; // 기존에 없던 필드가 추가되었을 때
    ...
}

 

JPA를 사용하지 않을 때는 tel이라는 필드가 추가되면 해당 테이블을 조회하는 모든 SQL을 찾아서 수정해야하지만, JPA를 사용하면 필드 추가 이후 SQL은 JPA가 알아서 생성해준다.

지연 로딩과 즉시 로딩

지연 로딩(Lazy Loading)은 연관된 엔티티를 실제로 사용하는 시점까지 데이터베이스에서 조회를 미루는 방식으로, Member 엔티티에서 Team 엔티티를 지연 로딩으로 설정하면, Member를 조회할 때는 Team 데이터가 실제로 필요할 때까지 쿼리가 실행되지 않는다. member.getTeam() 호출 등 실제로 Team 데이터가 필요해질 때 추가 쿼리가 실행된다.

 

=> 불필요한 데이터 조회를 방지해 성능을 향상시킬 수 있고, 메모리 사용량을 줄일 수 있다는 장점이 있다. 단점으로는 연관 객체를 사용하는 시점에 추가 쿼리가 발생하므로, 트랜잭션이 종료된 후 접근하면 LazyInitializationException이 발생할 수 있다.

 

즉시 로딩(Eager Loading)은 연관된 모든 엔티티도 함께 즉시 조회하는 방식으로, Member 엔티티에서 Team 엔티티를 즉시 로딩으로 설정하면, Member를 조회할 때 Team도 함께 조인 쿼리로 조회된다.

 

=> 연관 객체를 바로 사용할 수 있고, 트랜잭션 종료 후에도 이미 데이터가 로딩되어 있어 예외가 발생하지 않는다는 장점이 있다. 단점으로는 연관된 데이터가 많을 경우 불필요하게 많은 데이터를 한 번에 가져와 성능 저하 및 메모리 낭비가 발생할 수 있고, 복잡한 연관관계에서는 N+1 문제 등 예기치 않은 쿼리 폭증이 발생할 수 있다.

대부분에 실무에서는 지연 로딩 사용이 권장된다.

패러다임 불일치 해결

객체지향 프로그래밍과 관계형 데이터베이스 사이에는 패러다임의 불일치라는 근본적인 차이가 존재한다. 객체지향은 데이터를 객체와 메소드로 구성하며, 상속·다형성 등 계층적 구조를 지원한다. 관계형 데이터베이스는 데이터를 행과 열로 구조화하며, 상속이나 객체 참조 개념이 없다. 이러한 차이로 인해 다음과 같은 문제가 발생한다.

  • 상속 미지원: 객체지향의 상속 구조를 테이블로 직접 표현할 수 없다
  • 참조와 식별의 차이: 객체는 참조로 연결되지만, RDB는 외래키로 연결된다
  • 데이터 구조의 불일치: 객체는 중첩, 컬렉션 등 다양한 구조를 가지지만, RDB는 단순한 테이블 구조이다
  • 데이터 접근 방식의 차이: 객체는 관계를 따라 탐색하지만, RDB는 조인으로 데이터를 결합한다

JPA는 패러다임 불일치 문제를 다음과 같이 해결한다.

  1. 매핑 전략 제공
    • 상속 매핑: 객체의 상속 구조를 테이블에 저장할 수 있도록 여러 매핑 전략을 제공한다
    • 연관관계 매핑: 객체 간의 참조를 외래키로 변환하여 테이블 간 관계를 자동으로 관리한다
  2. 객체 중심 개발 지원
    • 개발자는 자바 컬렉션에 객체를 저장하듯이 엔티티를 다루면, JPA가 내부적으로 SQL을 생성해 DB에 반영한다
    • 복잡한 SQL 작성과 매핑 코드를 줄여 생산성을 높이고, 객체 모델링에 집중할 수 있다
  3. 데이터 변환 자동화
    • 객체와 ㅌ테이블 간의 데이터 변환을 자동으로 처리하여, 개발자는 객체만 신경쓰면 된다
  4. JPQL 등 객체지향 쿼리 제공
    • SQL 대신 객체를 대상으로 하는 JPQL 등 객체지향 쿼리를 지원해 객체 모델에 맞는 데이터 접근이 가능하다

따라서 상속, 다형성, 컬렉션 등 객체지향 개념을 데이터베이스에 자연스럽게 반영하므로 반복적인 SQL 및 매핑 코드가 감소되어 개발 생산성이 향상되고, 객체지향적 설계와 데이터베이스 설계의 간극이 해소된다. 또한 데이터베이스 벤더에 종속되지 않는 이식성을 확보할 수 있는 효과가 있다.

'스터디 > JPA' 카테고리의 다른 글

JPA 스터디2 - 3  (2) 2025.07.30
JPA 스터디2 - 2  (2) 2025.07.14
JPA 스터디1 - 3  (0) 2025.07.01
JPA 스터디1 - 2  (3) 2025.06.16
JPA 스터디1 - 1  (1) 2025.06.10

https://github.com/hongbre/spring-jpashop

 

GitHub - hongbre/spring-jpashop

Contribute to hongbre/spring-jpashop development by creating an account on GitHub.

github.com

 

'실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발' 강의를 쭉 들었는데, Spring 기본만 하고 JPA 기본은 안 하니 이해가 안 된다. JPA 기본 먼저 듣고 강의 다시 들으면서 정리하기로 했다.

'스터디 > JPA' 카테고리의 다른 글

JPA 스터디2 - 3  (2) 2025.07.30
JPA 스터디2 - 2  (2) 2025.07.14
JPA 스터디2 - 1  (1) 2025.07.13
JPA 스터디1 - 2  (3) 2025.06.16
JPA 스터디1 - 1  (1) 2025.06.10

요구사항 분석

기능목록

  • 회원 기능
    • 회원 등록
    • 회원 조회
  • 상품 기능
    • 상품 등록
    • 상품 수정
    • 상품 조회
  • 주문 기능
    • 상품 주문
    • 주문 내역 조회
    • 주문 취소
  • 기타 요구사항
    • 상품은 재고 관리가 필요하다.
    • 상품은 카테고리로 구분할 수 있고 도서, 음반, 영화가 있다.
    • 상품 주문 시 배송 정보를 입력할 수 있다.

도메인 모델과 테이블 설계

도메인

엔티티

테이블

엔티티 클래스 개발

https://github.com/hongbre/spring-jpashop

 

GitHub - hongbre/spring-jpashop

Contribute to hongbre/spring-jpashop development by creating an account on GitHub.

github.com

 

  1. 다중성(관계의 수) 기준
    • OneToOne(1:1)
      • 하나의 엔티티가 다른 엔티티와 오직 하나의 관계만 가질 때 사용
      • ORDERS와 DELIVERY
    • ManyToOne(N:1)
      • 여러 엔티티가 하나의 엔티티와 관계를 맺을 때 사용
      • ORDER_ITEM과 ITEM
    • OneToMany(1:N)
      • 하나의 엔티티가 여러 엔티티와 관계를 맺을 때 사용
      • ORDERS와 ORDER_ITEM
    • ManyToMany(N:N)
      • 여러 엔티티가 서로 다수와 관계를 맺을 때 사용
      • 실무에서는 사용을 지양하고, 중간 엔티티를 만들기를 권장
      • ITEM과 CATEGORY
  2. 방향 및 연관관계의 주인
    • 단방향
      • 한쪽 엔티티만 다른 엔티티를 참조
      • ORDER_ITEM과 ITEM의 관계만 단방향
    • 양방향
      • 양쪽 엔티티가 서로를 참조
    • 연관관계의 주인(Owner)
      • 외래키(FK)를 가진 엔티티가 연관관계의 주인이 된다
      • 주인 엔티티에서만 연관관계의 변경(CUD)이 가능

'스터디 > JPA' 카테고리의 다른 글

JPA 스터디2 - 3  (2) 2025.07.30
JPA 스터디2 - 2  (2) 2025.07.14
JPA 스터디2 - 1  (1) 2025.07.13
JPA 스터디1 - 3  (0) 2025.07.01
JPA 스터디1 - 1  (1) 2025.06.10

Spring initializr로 프로젝트 생성

  • Porject: Gradle-Groovy
  • Language: Java
  • Spring Boot: 3.5.0
  • Project Metadata:
    • Group: jpabook
    • Artifact: jpashop
    • Name: jpashop (기본적으로 Artifact 동일)
    • Description: Demo Project for Spring Boot (기본값)
    • Package Name: jpabook.jpashop (Group.Artifact)
    • Packaging: Jar
    • Java: 21
  • Dependencies
    • Spring Web (웹 애플리케이션 개발 필수)
    • Lombok (getter/setter 등 어노테이션으로 생성)
    • Thymeleaf (템플릿 엔진)
    • Spring Data JPA (JPA 사용을 위함)
    • H2 Database (개발용으로 많이 쓰는 데이터베이스)
    • Validation (자바 빈 객체의 값 검증)

깃헙 레포지토리 생성해서 연결

Lombok 적용 테스트

package jpabook.jpashop;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class LombokTest {

    private String data;
}

 

@Getter@Setter를 통해 data 값을 넣고 조회하는 LombokTest 클래스 생성

package jpabook.jpashop;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JpashopApplication {

    public static void main(String[] args) {

        // Lombok 적용 테스트
        LombokTest lombokTest = new LombokTest();
        lombokTest.setData("Test");
        System.out.println(lombokTest.getData());

        SpringApplication.run(JpashopApplication.class, args);
    }

}

 

콘솔에 "Test"가 출력되며 확인 완료

JPA와 DB 설정

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/Downloads\spring\springjpa1-v20250325/jpashop
    username: sa
    password:
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
#        show_sql: true
        format_sql: true
logging:
   level:
     org.hibernate.SQL: debug
     org.hibernate.orm.jdbc.bind: trace

 

H2 데이터베이스와 연결하고, JPA를 설정한다. spring.jpa.hibernate.ddl-auto의 값은 다음과 같다.

  • none: 스키마를 자동으로 생성하거나 변경하지 않음
  • validate: 엔티티와 스키마가 일치하는지 검증만 하며 변경하지 않음
  • update: 엔티티 변경사항을 반영해 스키마를 자동 갱신함(기존 데이터 유지, 컬럼 삭제 등은 미반영)
  • create: 기존 스키마를 삭제하고 엔티티 기준으로 새로 생성(기존 데이터 모두 삭제)
  • create-drop: create와 동일하게 시작 시 새로 생성하고 종료 시 스키마 삭제

'스터디 > JPA' 카테고리의 다른 글

JPA 스터디2 - 3  (2) 2025.07.30
JPA 스터디2 - 2  (2) 2025.07.14
JPA 스터디2 - 1  (1) 2025.07.13
JPA 스터디1 - 3  (0) 2025.07.01
JPA 스터디1 - 2  (3) 2025.06.16

스프링 빈 스코프

스프링 빈 스코프(Bean Scope)는 스프링 컨테이너가 관리하는 객체인 빈이 생성되고 소멸될 때까지 존재할 수 있는 범위나 생명주기이다. 주요 스코프 종류는 다음과 같다.

  • 싱글톤(Singleton) : 기본값으로 스프링 컨테이너 시작부터 종료까지 단 하나의 인스턴스만 생성, 모든 요청에 동일 객체를 반환
  • 프로토타입(Prototype) : 빈을 요청할 때마다 새로운 인스턴스 생성. 생성과 의존성 주입까지만 컨테이너가 관리하며 이후는 클라이언트가 직접 관리 필요
  • request : HTTP 요청마다 새로운 빈 생성, 요청이 끝나면 소멸한다. 주로 웹 애플리케이션에서 사용
  • session : HTTP 세션마다 새로운 빈 생성, 세션 종료 시 소멸한다. 주로 사용자별 데이터 관리에 사용
  • application : 서블릿 컨텍스트와 동일한 생명주기를 가지며, 웹 애플리케이션 전체에서 공유
  • websocket : 웹소켓 연결마다 빈 생성, 연결 종료 시 소멸한다.

프로토타입 스코프

  1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다.
  2. 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다. (3개의 클라이언트 동일)

  1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
  2. 스프링 컨테이너는 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.
  3. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
    • 이후에 빈을 관리하지 않으며, 3개의 클라이언트는 다른 빈을 받는다.
싱글톤 스코프 빈 테스트
package spring.basic.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import static org.assertj.core.api.Assertions.*;

public class SingletonTest {

    @Test
    void singletonBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);

        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class); //최초 호출 시 init 실행
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
        System.out.println("singletonBean1 = " + singletonBean1);
        System.out.println("singletonBean2 = " + singletonBean2);
        assertThat(singletonBean1).isSameAs(singletonBean2); //둘은 같은 객체이다

        ac.close(); //종료 시 destroy 실행
    }

    @Scope("singleton") //기본 Scope 는 singleton 이다
    static class SingletonBean {
        @PostConstruct
        public void init() {
            System.out.println("SingletonBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("SingletonBean.destroy");
        }
    }
}
프로토타입 스코프 빈 테스트
package spring.basic.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import static org.assertj.core.api.Assertions.assertThat;

public class PrototypeTest {

    @Test
    void prototypeBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class); //호출 시 init 실행
        System.out.println("find prototypeBean2");
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class); //호출 시 init 실행
        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);
        assertThat(prototypeBean1).isNotSameAs(prototypeBean2); //둘은 다른 객체이다

        ac.close(); //종료 시 destroy 를 실행하지 않는다
    }

    @Scope("prototype") //Scope 를 prototype 으로 변경
    static class PrototypeBean {
        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }
}

프로토타입과 싱글톤을 함께 사용할 때 문제점

일반적으로 프로토타입 스코프를 사용하는 이유는 클라이언트가 요청할 때마다 인스턴스를 생성하기 위해서일 것이다. 하지만 스프링은 싱글톤 빈의 의존성 주입을 생성할 때 한 번만 주입한다. 따라서 싱글톤 빈에 프로토타입 스코프를 가진 빈을 주입 받아서 사용할 경우 주입 시점에 프로토타입 빈이 생성되어 주입되며 이후에 같은 인스턴스를 사용하게 된다(프로토타입 빈의 의미가 사라진다). 이 문제점을 해결하기 위해서는 ObjectFactory 또는 Provider를 사용할 수 있다.

package spring.basic.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Provider;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

public class SingletonWithPrototypeTest1 {

    @Test
    void prototypeFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        prototypeBean1.addCount();
        assertThat(prototypeBean1.getCount()).isEqualTo(1);

        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        prototypeBean2.addCount();
        assertThat(prototypeBean2.getCount()).isEqualTo(1);

    }

    @Test
    void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        assertThat(count1).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(1);
    }

    @Scope("singleton")
    static class ClientBean {

        @Autowired
        private Provider<PrototypeBean> provider;

        public int logic() {
            PrototypeBean prototypeBean = provider.get();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

    @Scope("prototype")
    static class PrototypeBean {
        private int count = 0;

        public void addCount() {
            count++;
        }

        public int getCount() {
            return count;
        }

        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init " + this);
        }

        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }
}

웹 스코프

웹 스코프는 웹 환경에만 동작하며, 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 종류는 위에서 말했던 것 중 request, session, application, websocket이 있다. 이번에는 request 스코프를 통해 로그를 남기는 예제를 작성했다.

MyLogger
package spring.basic.common;


import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {

    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message) {
        System.out.println("[" + uuid + "] [" + requestURL + "] " + message);
    }

    @PostConstruct
    public void init() {
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean create:" + this);
    }

    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }
}

@Scope(value = "request")로 request 스코프로 지정하였기 때문에 이 빈은 HTTP 요청 당 하나의 인스턴스가 생성되고, HTTP 요청 종료 시 소멸된다. 빈 생성 시 @PostConstructor를 통해 UUID를 만들어서 저장하고, 빈 소멸 시 @PreDestroy를 통해 소멸 메시지를 남긴다. requestURL은 빈 생성 당시에는 알 수 없기 때문에 setter로 입력받는다.

LogDemoController
package spring.basic.web;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import spring.basic.common.MyLogger;

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        //MyLogger myLogger = myLoggerProvider.getObject();
        System.out.println("myLogger = " + myLogger.getClass());
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

MyLogger가 제대로 동작하는지 확인하기 위한 컨트롤러로, log-demo로 요청한다.

LogDemoService
package spring.basic.web;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import spring.basic.common.MyLogger;

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

    public void logic(String id) {
       // MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("service id = " + id);
    }
}

요청에 따른 로그가 정상적으로 출력된다.

MyLogger에서 @Scope의 proxyMode를 ScopedProxyMode.TARGET_CLASS를 설정해놨는데, 이는 MyLogger를 프록시 객체로 생성하고, 내부에서는 진짜 myLogger를 호출하게 된다. 이전에 나왔던 프록시 처럼 CGLIB을 통해 처리된다. Provider나 프록시나 진짜 객체 조회를 필요한 시점까지 지연처리 한다는 점이 중요하다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 9  (0) 2025.05.13
Spring 스터디2 - 8  (0) 2025.05.11
Spring 스터디2 - 7  (0) 2025.04.29
Spring 스터디2 - 6  (0) 2025.04.27
Spring 스터디2 - 5  (1) 2025.04.19

아파치를 Docker로 돌리고 싶지만 사정상 서버에 직접 설치해서 운영 중이다. 이번에 다른 목적으로 WEB 서버 6대가 추가되었는데, 6개의 콘솔을 열어서 설정파일을 수정하고 재기동하는건 말도 안 되게 귀찮기 때문에 Bash 스크립트를 작성했다.

ssh 키 쌍 생성하여 패스워드 입력 없이 서버 연결 설정

# 1.키 쌍 생성
$ ssh-keygen -t rsa
# Enter file in which to save the key : 엔터 (기본 경로 ~/.ssh 사용)
# Enter passphrase : 엔터 (패스워드 없이 사용)
# Enter same passphrase again : 엔터

# 2. 생성된 공개키를 대상 서버에 복사
$ ssh-copy -id -p 22 username@remote.server.ip

# 3. 연결 확인
$ ssh -p 22 username@remote.server.ip

ssh 연결하여 명령 실행

# 한 개의 명령 실행
$ ssh -p 22 username@remote.server.ip "touch test"

# 여러 개의 명령 실행(세미콜론으로 구분)
$ ssh -p 22 username@remote.server.ip "touch test; mv test tset"

스크립트 작성

  1. 설정 파일의 수정이 있을 때 파일명.YYYYMMDD 형태로 파일을 백업한다.
  2. conf.d의 ssl.conf, vhost.conf, workers.properties 위주로 수정한다.

스크립트 작성의 전제 조건이다.

#!/bin/bash

APACHE_CONFD_PATH=/etc/httpd/conf.d
DATE=`date +%Y%m%d`
# 오늘 날짜로 수정된 파일명
MODIFIED_CONF_LIST=`ls -1 ${APACHE_CONFD_PATH} | grep ${DATE} | sed 's/\.[^.]*$//`
# 대상 서버 목록
SERVERS="192.168.0.2 192.168.0.3 192.168.0.4 192.168.0.5 192.168.0.6"

# 수정된 파일별로 반복
for CONF in ${MODIEFIED_CONF_LIST}
do
    # 대상 서버별로 파일 전송
    for SERVER in ${SERVERS}
    do
        scp -P 22 ${APACHE_CONFD_PATH}/${CONF} apache@${SERVER}:${APACHE_CONFD_PATH}/${CONF}
        scp -P 22 ${APACHE_CONFD_PATH}/${CONF}.${DATE} apache@${SERVER}:${APACHE_CONFD_PATH}/${CONF}.${DATE}
    done
done

# 아파치 재기동
for SERVER in ${SERVERS}
do
    ssh -n -p 22 apache@${SERVER} "sudo /home/apache/apache_restart.sh"
    sleep 5
done

sudo /home/apache/apache_restart.sh

 

기존에는 apache_stop.shapache_start.sh를 따로 실행해서 재기동을 했으나, 한 번의 명령으로 재기동 하기 위해 각 서버에 apache_restart.sh를 추가 작성했다.

#!/bin/bash

# 아파치 종료
sh /home/apache/apache_stop.sh

while true
do
    # 프로세스에 httpd가 없으면(내가 실행한 grep 하나만 나오면) 탈출
    if [ `ps -ef | grep -c httpd` -eq 1 ]
    then
        break
    fi

    sleep 1
done

# 아파치 시작
sh /home/apache/apache_start.sh

스프링 빈 생명주기

스프링 빈의 생명주기는 다음과 같다.

스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 빈 사용 → 소멸 전 콜백 →스프링 종료

 

콜백(Callback)은 특정 시점에 시스템이 자동으로 호출하는 메소드다. 스프링에서는 빈을 초기화 하거나 소멸 시점에 개발자가 원하는 작업을 할 수 있도록 콜백 메소드를 제공한다. 초기화는 초기 세팅이나 DB 같은 외부 리소스 연결 등에 사용하고, 소멸은 연결 해제 등에 사용한다.

public class NetworkClient {
     private String url;

     public NetworkClient() {
         System.out.println("생성자 호출, url = " + url);
         connect();
         call("초기화 연결 메시지");
     }

     public void setUrl(String url) {
         this.url = url;
     }

     //서비스 시작시 호출
     public void connect() {
         System.out.println("connect: " + url);
     }

     public void call(String message) {
         System.out.println("call: " + url + " message = " + message);
     }

     //서비스 종료시 호출
     public void disconnect() {
     System.out.println("close: " + url);
     }
}

 

테스트 코드를 작성한다. NetworkClient는 생성되면서 connect로 연결하고, 소멸 시 disconnect로 해제한다.

public class BeanLifeCycleTest {
    @Test
     public void lifeCycleTest() {
         ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
         NetworkClient client = ac.getBean(NetworkClient.class);
         ac.close();
     }

     @Configuration
    static class LifeCycleConfig {
        @Bean
         public NetworkClient networkClient() {
             NetworkClient networkClient = new NetworkClient();
             networkClient.setUrl("https://blog.raphong.com");
             return networkClient;
         }
     }
}

 

NetworkClinet를 빈으로 등록하면서 생성자 호출 이후 setter를 통해 url을 초기화하도록 했다. 해당 코드를 실행하면 생성자에서 출력하는 3줄이 나오는데, url 변수가 전부 null로 출력된다. 이유는 생성자 호출 이후에 setter를 호출하기 때문이다.

빈 생명주기 콜백의 종류 및 구현 방법은 대표적으로 세가지가 있다.

인터페이스 구현

public class NetworkClient implements InitializingBean, DisposableBean {
    ...
     public NetworkClient() {
         System.out.println("생성자 호출, url = " + url);
     }
     ...
     @Override //다음부터 init으로 변경
     public void afterPropertiesSet() throws Exception {
         connect();
         call("초기화 연결 메시지");
     }

    @Override //다음부터 close로 변경
     public void destroy() throws Exception {
         disConnect();
     }
}
  • InitializingBean을 상속하면서 afterPropertiesSet() 메소드에 초기화 작업을 구현한다.
    • 원래 생성자에 있던 connect와 call을 해당 메소드로 옮겼다.
  • DisposableBean을 상속하면서 destroy() 메소드에 소멸 작업을 구현한다.
    • disconnect 메소드를 작성한다.

실행 시 문제 없이 생성자 출력 내용 → 초기화 출력 내용 → 소멸 출력 내용이 나오지만, 해당 방식은 스프링에 의존적이기 때문에 스프링에서만 사용할 수 있고, afterPropertiesSet() 및 destroy() 메소드의 이름을 변경할 수도 없다. 초창기에 사용하던 방식으로 지금은 거의 사용하지 않는다.

@Bean 어노테이션의 속성

public class BeanLifeCycleTest {
    ... 
     @Configuration
    static class LifeCycleConfig {
        @Bean(initMethod = "init", destroyMethod = "close")
         public NetworkClient networkClient() {
             NetworkClient networkClient = new NetworkClient();
             networkClient.setUrl("https://blog.raphong.com");
             return networkClient;
         }
     }
}

 

@Bean 어노테이션의 속성인 initMethod와 destroyMethod로 생성과 소멸에 사용할 콜백을 지정할 수 있다. 해당 방법은 외부 라이브러리에도 적용 가능하며, 메소드명을 자유롭게 지정할 수 있다. 또한 destroyMethod의 경우 기본적으로 '추론(inferred)'이 들어가게 되는데, 대부분의 라이브러리에서 사용하는 close 또는 shutdown 메소드를 자동으로 호출해준다.

어노테이션

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

     @PreDestroy
         public void close() {
         System.out.println("NetworkClient.close");
         disConnect();
     }
}
  • @PostConstruct 어노테이션을 붙인 메소드는 의존관계 주입이 끝난 후 호출된다.
  • @PreDestroy 어노테이션을 붙인 메소드는 빈 소멸 직전에 호출된다.

어노테이션을 붙이면 자동으로 처리되며, 이 방식은 자바 표준(Jakarta EE, JSR-250)으로 스프링에 종속적이지 않아 가장 권장하는 방법이다. 외부 라이브러리에는 사용할 수 없다는 단점이 있다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 10  (0) 2025.05.31
Spring 스터디2 - 8  (0) 2025.05.11
Spring 스터디2 - 7  (0) 2025.04.29
Spring 스터디2 - 6  (0) 2025.04.27
Spring 스터디2 - 5  (1) 2025.04.19

의존관계 주입 방법

의존관계 주입 방법은 대표적으로 4가지가 있다.

  1. 생성자 주입(Constructor Injection)
    • 생성자를 통해 의존 객체를 주입하는 방식으로 가장 권장하는 방법
    • 주입이 한 번만 일어나며, 주입받는 객체를 final로 선언해 불변성을 보장 가능
    • 생성자가 하나 뿐이면 @Autowired 생략 가능
  2. 수정자 주입(Setter Injection)
    • Setter 메소드를 통해 의존 객체를 주입하는 방식
    • 선택적이고 변경 가능한 의존관계에 적합
    • @Autowired(required=false)로 필수 여부 지정 가능
  3. 필드 주입(Field Injection)
    • 필드에 직접 @Autowired를 붙여 의존 객체를 주입하는 방식
    • 코드는 간결하지만, DI 컨테이너 없이 테스트가 불가하고 불변성이 보장되지 않아 미권장
  4. 일반 메소드 주입(General Method Injection)
    • 임의의 메소드에 @Autowired를 붙여 의존 객체를 주입하는 방식
    • 한 번에 여러 의존 객체를 주입할 수 있지만, 모호하므로 특별한 경우에만 사용

이렇게 의존관계를 주입하면서도, 주입할 빈이 없는 경우에도 동작해야할 때가 있다.

  • @Autowired(required=false) : 주입할 대상이 없으면 수정자 메소드 호출하지 않음
  • @Nullable : 주입할 대상이 없으면 null이 됨
  • Optional<> : 주입할 대상이 없으면 Optional.empty가 됨
    static class TestBean {
        //호출 X
        @Autowired(required = false)
        public void setNoBean1(Member noBean1) {
            System.out.println("noBean1 = " + noBean1);
        }

        @Autowired
        public void setNoBean2(@Nullable Member noBean2) {
            System.out.println("noBean2 = " + noBean2);
        }

        @Autowired
        public void setNoBean3(Optional<Member> noBean3) {
            System.out.println("noBean3 = " + noBean3);
        }
    }

 

테스트 클래스를 작성해서 실행하면 noBean2와 3은 출력되지만, 1은 호출도 하지 않기 때문에 출력이 없다.

위에서 생성자 주입을 가장 권장한다고 했는데, 권장하는 이유는 여러가지가 있다.

  • 불변성 보장 : final 선언 및 생성자 1회 호출로 불변 객체 설계 가능
  • 의존성 누락/실수 방지 : 컴파일 시 의존성 누락 바로 확인 가능
  • 순환 참조 방지 : 애플리케이션 구동 시점에 순환 참조 오류 감지
  • 테스트 용이성 : DI 컨테이너 없이도 테스트 가능
  • 코드 명확성, 유지보수성 향상 : 의존성 명확, 롬복 등과 결합 가능

롬복(Lombok)

롬복은(Lombok)은 자바에서 반복적으로 생성해야 하는 코드(getter, setter, 생성자 등)를 자동으로 생성해주는 오픈소스 라이브러리이다. 어노테이션 기반으로 동작하며, 개발자가 클래스에 특정 Lombok 어노테이션을 붙이면 컴파일 과정에서 해당 메소드들이 자동으로 생성된다.

 

롬복을 사용하기 위해서는 우선 IntelliJ 설정에서 플러그인을 설치하고 추가 설정을 해준다.

 

다음으로 gradle 설정에 의존성을 추가해주어야한다.

//추가
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

//dependencies 내에 추가
dependencies {
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
}

 

그리고 기존 코드에 롬복을 적용한다.

@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
         this.memberRepository = memberRepository;
         this.discountPolicy = discountPolicy;
     }
}

 

기본 코드이다. 생성자와 @Autowired 어노테이션이 있다.

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}

 

@RequiredArgsConstructor 어노테이션을 클래스에 추가하고, 생성자를 제거한다. 추가한 어노테이션의 이름처럼 final 필드들의 생성자를 자동으로 생성해준다.

 

코드에는 보이지 않지만 생성된걸 확인할 수 있다.

조회된 빈이 2개 이상일때 주입 방법

@Autowired는 타입으로 빈을 조회한다. 그래서 전에 배웠던 내용처럼 조회된 빈이 2개 이상이면 NoUniqueBeanDefinitionException이 발생한다. 하위 타입(구현체)를 지정해서 해결할 수 있으나 DIP 위반이므로 적절하지 않다.

Autowired 필드명 매칭

@Autowired
private DiscountPolicy discountPolicy

 

기존 코드가 이렇다면,

@Autowired
private DiscountPolicy rateDiscountPolicy

 

같이 필드명을 빈 이름으로 변경하면 fixDiscountPolicy와의 충돌 없이 rateDiscountPolicy가 주입된다. 필드명 매칭은 먼저 타입으로 매칭을 시도하고, 그 결과 빈이 2개 이상일 때 추가로 동작하는 기능으로 (1) 타입 매칭 → (2) 필드명, 파라미터명으로 빈 이름 매칭과 같이 동작한다.

@Qualifier 사용

@Qualifer 어노테이션은 추가 구분자를 붙여주는 방법으로, 빈 이름을 변경하는 것은 아니다.

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy { ... }

 

빈에 @Qualifer 어노테이션을 붙인다.

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
     this.discountPolicy = discountPolicy;
}

 

그리고 생성자에서도 @Qualifier를 명시함으로 mainDiscountPolicy라는 Qualifier 이름을 가진 빈을 찾는다. 만약 없을 경우에는 빈 이름으로 검색해서 추가로 찾기는 하지만, @Qualifier만 찾는 용도로 쓰는 것이 명확하다.

@Primary 사용

@Primary 어노테이션은 우선 순위를 주는 방법으로, @Autowired 시 빈이 2개 이상이면 @Primary 어노테이션이 붙어있는 쪽이 우선이다.

 

@Primary는 기본으로 사용할 빈을 지정하고, @Qualifier는 특정 빈을 지정하기 때문에 둘 다 있을 때는 @Qualifier가 우선이다.

@Qualifier 대용 어노테이션 만들기

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}

 

앞선 @Qualifier("mainDiscountPolicy")만 사용하면 컴파일 시 타입 체크가 안 되고, 무엇보다 조금 길게 느껴질 수 있다. 그래서 이런식으로 @MainDiscountPolicy 어노테이션을 만들면 @Qualifier("mainDiscountPolicy")를 다는 것과 동일한 역할을 한다.

둘 다 쓰기

AllBeanTest.java
package spring.basic.autowired;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import spring.basic.AutoAppConfig;
import spring.basic.discount.DiscountPolicy;
import spring.basic.member.Grade;
import spring.basic.member.Member;

import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.*;

public class AllBeanTest {
    @Test
    void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);

        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L, "userA", Grade.VIP);
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");

        assertThat(discountService).isInstanceOf(DiscountService.class);
        assertThat(discountPrice).isEqualTo(1000);

        int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
        assertThat(rateDiscountPrice).isEqualTo(2000);


    }

    static class DiscountService {
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policies;

        public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
            this.policyMap = policyMap;
            this.policies = policies;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policies = " + policies);
        }

        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
    }
}

 

DiscountService는 Map으로 모든 DiscountPolicy를 주입받고, discount 메소드는 discountCode에 따라 매칭되는 빈을 사용하는 식으로 동작한다. Map에는 키(String)에 빈 이름이, 값으로 DiscountPolicy 타입으로 조회한 모든 빈이 들어간다. List는 빈만 들어간다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 10  (0) 2025.05.31
Spring 스터디2 - 9  (0) 2025.05.13
Spring 스터디2 - 7  (0) 2025.04.29
Spring 스터디2 - 6  (0) 2025.04.27
Spring 스터디2 - 5  (1) 2025.04.19

+ Recent posts