요구사항 분석

기능목록

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

도메인 모델과 테이블 설계

도메인

엔티티

테이블

엔티티 클래스 개발

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)이 가능

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

Spring 스터디3 - 1  (1) 2025.06.10
Spring 스터디2 - 10  (0) 2025.05.31
Spring 스터디2 - 9  (0) 2025.05.13
Spring 스터디2 - 8  (0) 2025.05.11
Spring 스터디2 - 7  (0) 2025.04.29

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와 동일하게 시작 시 새로 생성하고 종료 시 스키마 삭제

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

Spring 스터디3 - 2  (2) 2025.06.16
Spring 스터디2 - 10  (0) 2025.05.31
Spring 스터디2 - 9  (0) 2025.05.13
Spring 스터디2 - 8  (0) 2025.05.11
Spring 스터디2 - 7  (0) 2025.04.29

스프링 빈 스코프

스프링 빈 스코프(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 스터디3 - 2  (2) 2025.06.16
Spring 스터디3 - 1  (1) 2025.06.10
Spring 스터디2 - 9  (0) 2025.05.13
Spring 스터디2 - 8  (0) 2025.05.11
Spring 스터디2 - 7  (0) 2025.04.29

스프링 빈 생명주기

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

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

 

콜백(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 스터디3 - 1  (1) 2025.06.10
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

의존관계 주입 방법

의존관계 주입 방법은 대표적으로 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

연계 파일을 생성할 때 마지막 줄에 긴 공백이 들어가서 상대방이 적재 시 오류가 발생한다고 확인 요청이 왔다.

ByteBuffer buffer = ByteBuffer.allocate(data.length() * 3); //실제로는 숫자가 조금 다르다

 

ByteBuffer에 담을 데이터들은 data 변수에 들어가 있는데, 대충 한글이니 2바이트 등해서 주석에 왜 이렇게 계산을 하는지 잔뜩 적어놓았다. 물론 딱 맞는 숫자는 아니었고 무조건 미사용 공간이 생길 수 밖에 없는 구조다. 하지만 찾아보니 ByteBuffer를 allocate로 할당하면 그 capacity는 줄일 수가 없어서 꽉 안 채우면 미사용 공간이 공백으로 바뀌어 파일이 생성되는 것이었다.

buffer.flip();
byte[] bufferData = new byte[buffer.limit()];
buffer.get(bufferData);

 

flip()으로 읽기모드로 전환하면 position(시작)이 0으로, limit(끝)이 실제 쓴 데이터의 끝으로 변경되어, 이 상태로 복사하면 limit까지의 데이터만 복사되고, 뒤에 미사용 공간은 복사되지 않는다. 따라서 byte array를 생성할 때 길이를 buffer.limit()으로 설정하고, buffer.get()으로 버퍼에 담긴 데이터를 복사하면 된다.

 

문제가 있다면 다음에 buffer를 넘기는 메소드가 ByteBuffer 타입을 요구하는데, 위에서 생성한 bufferData는 byte array다.

//String foo = bar.foobar(buffer);
String foo = bar.foobar(ByteBuffer.wrap(bufferData);

 

이때 ByteBuffer.wrap(byte[])을 사용하면, 이미 존재하는 byte array를 ByteBuffer로 감싸기 때문에 예시로 쓴 foobar 메소드에서 타입 에러 없이 동작하게 된다. 이렇게 수정하여 연계 파일을 생성했을 때, 상대방의 연계 파일 적재 오류를 해결했다.

컴포넌트 스캔

기존에 사용하던 AppConfig를 건드리기는 애매하니 AutoAppConfig로 설정 파일을 생성한다.

package spring.basic;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
) //@Configuration 도 @Component 이기 때문에 Scan 대상에서 제외
public class AutoAppConfig {
}

 

설정 파일이라는 표시인 @Configuration 어노테이션을 달고, 컴포넌트 스캔을 위한 @ComponentScan 어노테이션을 달아준다. 컴포넌트 스캔은 이름 그대로 @Component 어노테이션을 달고 있는 클래스를 전부 빈으로 등록한다. excludeFilters로 @Configuration 어노테이션을 달고 있는 클래스는 스캔에서 제외시켰다. 왜냐하면 @Configuration 어노테이션도 안에 @Component 어노테이션을 달고 있어서 스캔 대상이 되다보니 기존에 사용하던 AppConfig와 꼬일까봐이다.

앞서 작성했던 클래스들이 스캔 대상이 되도록 @Component 어노테이션을 달아준다.

MemoryMemberRepository
...
@Component
public class MemoryMemberRepository implements MemberRepository {
...
RateDiscountPolicy
...
@Component
public class RateDiscountPolicy implements DiscountPolicy {
...
MemberServiceImpl
...
@Component
public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository;

    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
...

 

AppConfig에서는 @Bean 어노테이션과 의존관계를 포함한 모든 설정 정보를 작성했는데, AutoAppConfig에서는 설정 정보가 없으니 @Autowired 어노테이션으로 의존관계를 주입해준다. @Autowired는 컴포넌트 스캔으로 빈을 등록하면서 생성자를 만나면 해당 타입의 빈이 등록되어 있는지 찾아보고 해당 빈을 주입하는 방식이다.

AutoAppConfigTest
package spring.basic.scan;

import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import spring.basic.AutoAppConfig;
import spring.basic.member.MemberService;

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

public class AutoAppConfigTest {
    @Test
    void basicScan() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

 

AutoAppConfig를 테스트하기 위한 코드를 작성한다. AnnotationConfigApplicationContext를 사용하는 것은 동일하고, 실행 시키면 기존과 동일한 결과를 얻을 수 있다.

탐색 위치와 기본 스캔 대상

@ComponentScan 어노테이션에서 탐색 위치를 지정할 수 있다.

@ComponentScan(
        basePackages = "spring.basic" //spring.basic를 포함한 하위패키지를 모두 탐색
        basePackages = {"spring.basic.member", "spring.basic.order"} //여러개의 위치를 지정할 수 있다.

 

basePackageClasses로 지정한 클래스의 패키지를 기본 탐색 위치로 지정할 수도 있으며, 아무것도 지정하지 않으면 @ComponentScan 어노테이션이 달린 클래스의 패키지가 기본 탐색 위치가 된다. 이와 같이 프로젝트 시작 위치에 메인 설정 정보를 두는 것이 일반적이다. (스프링 부트의 경우 @SpringBootApplication 어노테이션이 달린 클래스를 프로젝트 시작 위치에 둔다.)

 

기본 스캔 대상으로는 @Component 어노테이션을 포함한 다섯가지가 존재한다.

  • @Component : 기본 컴포넌트 표시
  • @Controller : 스프링 MVC에서 웹 요청을 처리하는 컨트롤러임을 표시
  • @Service : 비즈니스 로직을 수행하는 서비스임을 표시하나 @Component와 기능상 차이는 없다
  • @Repository : 데이터 접근 계층(DAO)임을 표시
    • 데이터베이스 관련 예외를 스프링의 데이터 접근 예외로 변환하는 부가 기능이 있다
  • @Configuration : 스프링 설정 정보 클래스임을 표시

필터

컴포넌트 스캔 대상으로 추가/제외 대상을 지정할 어노테이션을 만든다.

MyIncludeComponent
package spring.basic.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
MyExcludeComponent
package spring.basic.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}

 

테스트로 사용할 클래스를 작성한다.

BeanA
package spring.basic.scan.filter;

@MyIncludeComponent
public class BeanA {
}
BeanB
package spring.basic.scan.filter;

@MyExcludeComponent
public class BeanB {
}

 

임시로 사용할 설정 정보를 담은 테스트 코드를 작성한다.

ComponentFilterAppConfigTest
package spring.basic.scan.filter;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ComponentFilterAppConfigTest {
    @Test
    void filterScan() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        //Bean 이 조회가 되어야 함
        BeanA beanA = ac.getBean("beanA", BeanA.class);
        assertThat(beanA).isNotNull();

        //Bean 이 조회가 안 되어야 함
        assertThrows(
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("beanB", BeanB.class)
        );
    }

    @Configuration
    @ComponentScan(
            includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig {

    }
}

 

아래 @ComponentScan에서 includeFilters에서 MyIncludeComponent 어노테이션을 달고있으면 스캔 대상으로 포함하고, excludeFilters에서 MyExcludeComponent 어노테이션을 달고있으면 스캔 대상으로 제외하도록 설정했다. 테스트를 돌려보면 beanA는 정상적으로 조회가 되고, beanB는 조회가 안 되면서 테스트가 성공한다. 실무에서는 include는 안 쓰고, exclude만 가끔씩 쓴다고 한다.

 

테스트에서는 FilterType으로 어노테이션을 지정했으나, 어노테이션을 포함 다섯가지가 존재한다.

  • ANNOTATION : 특정 어노테이션이 붙은 클래스만 포함/제외
  • ASSIGNABLE_TYPE : 특정 타입 또는 그 하위 타입만 포함/제외
  • ASPECTJ : AspectJ 패턴(예로 com.example..*Service+)을 사용
  • REGEX: 정규식으로 클래스 이름 필터링
  • CUSTOM : TypeFilter 인터페이스를 구현한 클래스를 사용

중복 등록과 충돌

만약 자동으로 등록된 빈끼리, 또는 수동으로 등록된 빈과 중복이 된다면 어떻게 될까? 우선 컴포넌트 스캔으로 자동으로 등록된 빈끼리 이름이 같은 경우 스프링은 ConflictingBeanDefinitionException 예외를 발생시킨다.

 

자동으로 등록된 빈과 수동으로 등록된 빈이 이름이 같으면 수동 빈이 우선순위를 가지며 자동 빈을 오버라이딩 해버린다. 하지만 개발자가 의도하였더라도 애플리케이션이 확장됨에 따라 애매하게 설정이 꼬이면서 잡기 어려운 버그가 탄생할 수 있으니 지양하는 것이 좋다. 최근 스프링 부트에서는 자동 빈과 수동 빈이 충돌할 경우 아예 예외를 발생시키도록 했다.

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

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

웹 애플리케이션과 싱글톤 패턴

스프링은 웹 애플리케이션 외에도 다양한 종류의 애플리케이션을 개발할 수 있으나, 대부분은 웹 애플리케이션이다. 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다. 만약 고객1~3이 동시에 요청을 하게 되면 3개의 인스턴스를 생성하여 응답할 것이라고 생각할 것이다. 해당 예를 스프링 없는 DI 컨테이너로 테스트할 수 있도록 테스트 코드를 작성한다.

SingletonTest
    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();

        //1. 조회 : 호출할 때마다 객체를 생성
        MemberService memberService1 = appConfig.memberService();

        //2. 조회 : 호출할 때마다 객체를 생성
        MemberService memberService2 = appConfig.memberService();

        //참조값이 다른 것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        //memberService1 != memberService2
        assertThat(memberService1).isNotSameAs(memberService2);
    }

 

ApplicationContext 없이 그냥 AppConfig로 작성하여, 1번과 2번 요청에 서로 다른 객체를 생성하여 응답한다. 이게 만약 실제 서비스였을 경우 100명이 요청하면 100개의 객체가 생성되며 메모리 낭비가 심각해지는데, 이 문제의 해결방안은 객체가 단 하나만 생성되고 공유하도록 설계한 싱글톤 패턴을 사용하면 된다.

싱글톤 패턴

스펠링은 같지만 이 싱글톤은 아니다.

 

싱글톤 패턴(Singleton Pattern)은 객체를 단 하나만 생성하고 공유하도록 설계한 디자인 패턴이다.

SingletonService
package spring.basic.singleton;

public class SingletonService {
    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance() {
        return instance;
    }

    private SingletonService() {
    }

    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}

 

싱글톤 패턴의 구현 방식은 클래스 내부에 유일한 인스턴스를 static으로 미리 생성해놓고, 생성자를 private로 설정해 외부에서 new로 인스턴스를 만들지 못하게 한다. 그리고 예시와 같이 getInstance()와 같은 메소드로만 인스턴스를 반환하도록 한다.

SingletonTest
    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonServiceTest() {
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);

        assertThat(singletonService1).isSameAs(singletonService2);
    }

 

이번 테스트에서는 같은 인스턴스를 반환한다. 이와 같이 싱글톤 패턴을 적용하면 요청 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 메모리를 효율적으로 사용할 수 있다는 장점이 있다. 하지만 단점도 존재하는데 싱글톤 패턴을 위한 코드가 많아지며(위 SingletonService 예시처럼), 클라이언트가 구현 클래스에 의존해 DIP와 OCP를 위반할 수 있고, 테스트가 어렵고 유연성이 떨어지며, 내부 상태를 변경하기 어렵다는 점이 있다.

(장점보다 단점이 많아보인다..)

싱글톤 컨테이너

스프링 프레임워크는 싱글톤 컨테이너라는 개념을 통해 싱글톤 패턴을 직접 구현하지 않아도 객체를 싱글톤으로 관리한다. 스프링 컨테이너(ApplicationContext)는 빈 객체를 기본적으로 싱글톤 스코프(다른 스코프도 존재)로 생성해 관리한다. @Configuration 어노테이션이 붙은 설정 클래스의 @Bean 메소드로 등록된 객체를 컨테이너가 단 하나만 생성해서 공유한다는 의미이며, 이 기능을 싱글톤 레지스트리라고 한다.

SingletonTest
    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);

        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        assertThat(memberService1).isSameAs(memberService2);
    }

 

싱글톤 패턴 테스트 코드와 다르게 싱글톤 컨테이너는 싱글톤 패턴을 위한 복잡한 코드가 필요 없다. 또한, 싱글톤 패턴의 단점을 대부분 극복한다. DIP와 OCP를 준수할 수 있고, 테스트가 용이하고, private 생성자에 얽매이지 않고, 유연성이 높아 다양한 스코프를 지원한다.

싱글톤의 주의점

위에서 살펴본 싱글톤 패턴이나 싱글톤 컨테이너나 여러 클라이언트가 하나의 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 Stateful하게 설계하면 안 되고, Stateless하게 설계해야 한다. 이는 특정 클라이언트에 의존적인 필드가 없어야하며, 내부 필드 대신 지역변수, 파라미터, ThreadLocal 등을 활용해야 한다.

StatefulService
package spring.basic.singleton;

public class StatefulService {
    private int price; //Stateful

    public void order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
        this.price = price; //문제 발생 지점
    }

    public int getPrice() {
        return price;
    }
}
StatefulServiceTest
package spring.basic.singleton;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

import static org.junit.jupiter.api.Assertions.*;

class StatefulServiceTest {
    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        //userA 10000원 주문
        statefulService1.order("userA", 10000);
        //userB 20000원 주문
        statefulService2.order("userB", 20000);

        //userA 주믄 금액 조회
        int price = statefulService1.getPrice();
        System.out.println("price = " + price);

        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }

    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}

 

Stateful하게 설계했을 때 발생하는 문제점을 확인할 수 있는 테스트이다. A가 10000원짜리 주문을 하고 주문금액을 조회하는 사이에, B가 20000원짜리 주문을 하면 어떤 결과가 나올까? 애플리케이션에 문제가 없다면 10000원이 나와야하지만, 싱글톤을 Stateful하게 설계하는 바람에 20000원이 나오게 된다. 실무에서도 종종 발생하는 문제라고 하는데 어마어마한 피해가 나올 수 있으니 정말 조심해야한다. 항상 Stateless로 설계할 수 있도록하자.

@Configuration과 싱글톤

AppConfig
@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService() {
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() {
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

 

기존에 작성한 AppConfig만 보면 memberService 빈을 생성할 때 memberRepository() 메소드를 호출하고, new MemoryMemberRepository()를 호출한다. 또 orderService 빈을 생성할 때도 memberRepository() 메소드를 호출하고, new MemoryMemberRepository()를 호출한다. 이렇게 보면 2개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는거 같다.

ConfigurationSingletonTest
package spring.basic.singleton;

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.AppConfig;
import spring.basic.member.MemberRepository;
import spring.basic.member.MemberServiceImpl;
import spring.basic.order.OrderServiceImpl;

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

public class ConfigurationSingletonTest {
    @Test
    void configurationTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        System.out.println("memberService -> memberRepository1 = " + memberRepository1);
        System.out.println("memberService -> memberRepository2 = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);

        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }

    @Test
    void configurationDeep() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        AppConfig bean = ac.getBean(AppConfig.class);

        System.out.println("bean = " + bean.getClass());
    }
}

 

MemberServiceImpl과 OrderServieImpl에 테스트 코드를 추가하고, ConfigurationSingletonTest 테스트를 작성한다. 위에서 예상한 바와 다르게 memberRepository 인스턴스는 모두 같은 인스턴스가 공유되어 사용되고 있음을 확인할 수 있다.

 

이는 @Configuration 어노테이션을 달고 있는 AppConfig 클래스의 영향이다. 스프링 컨테이너를 생성하기 위한 AnnotationConfigurationApplicationContext에 파라미터로 넘긴 값은 빈으로 등록되기 때문에 AppConfig도 빈이다. 빈의 클래스 정보를 출력하게 되면 spring.basic.AppConfig가 아니라 AppConfig$$SpringCGLIB$$0 같이 CGLIB을 포함하여 나온다.

 

AppConfig 빈의 클래스 정보에 CGLIB이 포함되는 이유는 스프링이 @Configuration 어노테이션을 달고 있는 AppConfig 클래스를 스프링이 직접 인스턴스를 생성하지 않고, CGLIB을 사용해 해당 클래스를 상속한 프록시 클래스를 만들어 빈으로 등록하기 때문이다.

 

그리고 위에서 memberRepository가 여러번 호출되지 않는 이유는 이 프록시 클래스가 내부적으로 AppConfig의 메소드(@Bean이 붙은 메소드) 호출을 가로채어, 싱글톤 빈을 보장하고, 컨테이너에서 이미 생성된 빈이 있으면 그것을 반환하도록 동작하기 때문이다.

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

Spring 스터디2 - 8  (0) 2025.05.11
Spring 스터디2 - 7  (0) 2025.04.29
Spring 스터디2 - 5  (1) 2025.04.19
Spring 스터디2 - 4  (1) 2025.04.17
Spring 스터디2 - 3  (0) 2025.04.13

특정 스프링 빈 조회

스프링 컨테이너에서 이름이나 타입으로 특정 빈을 조회하는 방법이다.

ApplicationContextBasicFindTest
package spring.basic.beanfind;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import spring.basic.AppConfig;
import spring.basic.member.MemberService;
import spring.basic.member.MemberServiceImpl;

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

public class ApplicationContextBasicFindTest {

    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("Bean 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
        //System.out.println("memberService = " + memberService);
        //System.out.println("memberService.getClass() = " + memberService.getClass());
    }

    @Test
    @DisplayName("Bean 이름 없이 Type으로만 조회")
    void findBeanByType() {
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("구체 Type으로 조회")
    void findBeanByName2() {
        MemberService memberService = ac.getBean(MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("Bean 이름으로 조회 실패")
    void findBeanByNameFailed() {
        //ac.getBean("hello", MemberService.class);
        assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("hello", MemberService.class));
    }
}

 

앞서 사용했던 getBean() 메소드에 스프링 빈 이름과 타입을 명시하면 특정 빈을 조회할 수 있다. 빈 이름은 중복되면 안 되니 이름으로만 검색해도 원하는 빈을 조회할 수 있지만, 타입을 명시함으로 조금 더 정확한 결과를 얻을 수 있다. 또한 타입에 .class를 붙이는 이유는 스프링이 타입을 명확하게 알 수 있어 타입에 맞는 빈을 반환하고, 개발자는 별도의 캐스팅 없이 사용할 수 있기 때문이다.

스프링 빈 조회 시 동일한 타입이 둘 이상일 때

ApplicationContextSameBeenFindTest
package spring.basic.beanfind;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.basic.member.MemberRepository;
import spring.basic.member.MemoryMemberRepository;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ApplicationContextSameBeenFindTest {
    ApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("같은 Type이 둘 이상 있으면 중복 오류 발생")
    void findBeanByTypeDuplicate() {
        //MemberRepository bean = ac.getBean(MemberRepository.class);
        assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(MemberRepository.class));
    }

    @Test
    @DisplayName("같은 Type이 둘 이상 있을땐 Bean 이름을 지정")
    void findBeanByName() {
        MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
        assertThat(memberRepository).isInstanceOf(MemberRepository.class);
    }

    @Test
    @DisplayName("같은 Type을 모두 조회")
    void findAllBeanByType() {
        Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
        System.out.println("beansOfType = " + beansOfType);
        assertThat(beansOfType.size()).isEqualTo(2);
    }

    @Configuration
    static class SameBeanConfig {
        @Bean
        public MemberRepository memberRepository1() {
            return new MemoryMemberRepository();
        }


        @Bean
        public MemberRepository memberRepository2() {
            return new MemoryMemberRepository();
        }
    }
}

우선 기존에 사용하던 AppConfig에는 중복된 타입이 없어서 테스트 코드에 @Configuration 어노테이션을 단 static 클래스로 설정 역할을 할 SameBeanConfig를 작성하고, ApplicationContext 생성 시 인자로 넘긴다.

 

해당 테스트 코드를 실행시키면 스프링 빈 팩토리에는 memberRepository1과 memberRepository2, 두 개의 빈이 저장된다. 이 상태에서 getBean() 메소드로 MemberRepository.class 타입의 빈을 조회하면 NoUniqueBeanDefinitionException이 발생하게 된다. 즉, 조회 결과가 유니크하지 않아서 발생하는 오류다.

 

같은 타입의 빈을 조회하려면 getBeansOfType() 메소드를 사용해서 조회하며, 결과는 Map<String(빈 이름), Type> 형태로 반환된다. 해당 테스트 실행 결과는 아래와 같다.

key = memberRepository1 value = spring.basic.member.MemoryMemberRepository@4879dfad
key = memberRepository2 value = spring.basic.member.MemoryMemberRepository@4758820d
beansOfType = {memberRepository1=spring.basic.member.MemoryMemberRepository@4879dfad, memberRepository2=spring.basic.member.MemoryMemberRepository@4758820d}

스프링 빈 상속 관계 조회

ApplicationContextExtendsFindTest
package spring.basic.beanfind;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.basic.discount.DiscountPolicy;
import spring.basic.discount.FixDiscountPolicy;
import spring.basic.discount.RateDiscountPolicy;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ApplicationContextExtendsFindTest {
    ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 Type으로 조회 시 자식이 둘 이상 있으면 중복 오류가 발생한다")
    void findBeanByParentTypeDuplicate() {
        //DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
        assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(DiscountPolicy.class));
    }

    @Test
    @DisplayName("부모 Type으로 조회 시 자식이 둘 이상 있으면 Bean 이름을 지정한다")
    void findBeanByParentTypeBeanName() {
        DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("특정 하위 Type으로 조회")
    void findBeanBySubType() {
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("부모 Type으로 모두 조회")
    void findAlllBeanByParentType() {
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        assertThat(beansOfType.size()).isEqualTo(2);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }

    @Test
    @DisplayName("부모 Type으로 모두 조회하기(Object)")
    void findAllBeanByObjectType() {
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }

    @Configuration
    static class TestConfig {
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }

        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }
}

 

기본적으로 부모 타입으로 조회하면 자식 타입도 함께 조회된다. 그래서 모든 자바 객체의 부모인 Object 타입으로 조회하면 모든 스프링 빈이 조회된다. 이번에도 TestConfig를 작성하여 인자로 넘겼기 때문에, getBean() 메소드로 DiscountPolicy.class 타입 조회 시NoUniqueBeanDefinitionException이 발생하게 된다. 따라서 특정 자식 타입으로 조회하거나, getBeansOfType() 메소드로 조회하면 된다.

BeanFactory와 ApplicationContext

그림과 같이 BeanFactory는 스프링 컨테이너의 최상위 인터페이스이며, ApplicationContext는 BeanFactory를 상속한 인터페이스이다. AnnotationConfigApplicationContext 등은 ApplicationContext를 상속한 구현체이다. 각각의 역할과 특징은 아래와 같다.

  1. BeanFactory의 역할과 특징
  • 스프링 컨테이너의 최상위 인터페이스
  • 빈의 생성, 조회, 의존성 주입 등 기본적인 기능 제공
    • 위에서 사용한 getBean 메소드 제공
  1. ApplicationContext의 역할과 특징
  • BeanFactory를 상속한 인터페이스
  • BeanFactory의 모든 기능을 포함하며 애플리케이션에 필요한 다양한 부가 기능 제공
    • 메시지 소스(MessageSource) : 국제화 지원
    • 이벤트 발행(ApplicationEventPublisher) : 이벤트 기반 프로그래밍 지원
    • 리소스 로딩(ResourcePatternResolver) : 다양한 경로의 리소스 로딩
    • 환경 변수 관리(EnvironmentCapable)
    • 계층적 빈 팩토리(HierarchicalBeanFactory)
    • 어노테이션 기반 DI 지원, 모든 빈 스코프 지원

XML 기반의 설정

앞서 언급했듯 스프링 컨테이너는 Java, XML, Groovy 등 다양한 형식의 설정을 읽을 수 있도록 유연하게 개발되었고, 지금까지는 대부분 어노테이션 기반의 AnnotationConfigApplicationContext를 사용해 Java로 설정 파일을 만들었다. 최근에는 XML 기반의 설정을 잘 사용하지 않지만 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점이 있다. GenericXmlApplicationContext를 사용한다.

appConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="memberService" class="spring.basic.member.MemberServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
    </bean>

    <bean id="memberRepository" class="spring.basic.member.MemoryMemberRepository" />

    <bean id="orderService" class="spring.basic.order.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
        <constructor-arg name="discountPolicy" ref="discountPolicy" />
    </bean>

    <bean id="discountPolicy" class="spring.basic.discount.RateDiscountPolicy" />
</beans>
XmlAppContext
package spring.basic.xml;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import spring.basic.member.MemberService;

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

public class XmlAppContext {
    @Test
    void xmlAppContext() {
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

테스트 코드를 보면 알 수 있듯이 appConfig.xml과 계속 사용하던 AppConfig.class는 거의 유사하다.

스프링 빈 설정 메타 정보(BeanDefinition)

스프링 컨테이너가 다양한 형식의 설정을 읽을 수 있는 이유는 BeanDefinition이라는 빈의 생성, 설정, 관리에 필요한 메타정보를 담아두는 추상화가 있기 때문이다. 스프링 컨테이너는 Java, XML, Groovy 등 다양한 형식의 설정 파일을 읽어 BeanDefinition 객체로 변환한 뒤, 이 메타 정보를 기반으로 실제 빈 객체를 생성하고 관리한다. 각 @Bean 또는 태그마다 하나의 BeanDefinition이 생성된다.

 

방금 말한 것과 같이 BeanDefinition의 동작 흐름은 1. 개발자가 Java, XML, Groovy 등 설정 파일을 통해 빈을 정의 → 2. 스프링 컨테이너가 설정 정보를 읽어(각 형식에 맞는 Reader가 이 역할을 한다) BeanDefinition 객체로 변환 → 3. BeanDefinition에 담긴 메타 정보를 참고해 빈을 생성, 의존성 주입, 라이프사이클 관리 등을 수행하게 된다.

 

BeanDefinition이 담고 있는 주요 정보는 아래와 같다.

  • beanClassName : 생성할 빈의 클래스 명
  • factoryBeanName : 빈을 생성하는 팩토리 역할의 빈 이름
  • factoryMethodName : 빈을 생성하는 팩토리 메소드 이름
  • scope : 빈의 스코프
  • lazyInit : 지연 초기화 여부(빈을 실제 사용할 때까지 생성 지연)
  • initMethodName : 빈 초기화 메소드 이름
  • destroyMethodName : 빈 소멸 전 호출할 메소드 이름
  • constructor arguments, properties : 빈 생성 시 주입할 생성자 인자와 프로퍼티 값
  • autowireCandidate, primary : 자동 주입 대상 여부, 우선순위

하지만 실무에서 BeanDefinition을 직접 다룰 일은 거의 없고 자동으로 생성, 관리되는대로 사용한다고 한다.

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

Spring 스터디2 - 7  (0) 2025.04.29
Spring 스터디2 - 6  (0) 2025.04.27
Spring 스터디2 - 4  (1) 2025.04.17
Spring 스터디2 - 3  (0) 2025.04.13
Spring 스터디2 - 2  (0) 2025.03.30

스프링 컨테이너 생성

스프링 컨테이너 생성은 이전에 작성한 MemberApp 또는 OrderApp에서 사용한 방법으로 생성한다.

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

ApplicationContext를 보통 스프링 컨테이너라고 부른다. ApplicationContext는 인터페이스인데, AnnotationConfiApplicationContext라는 ApplicationContext의 구현체를 사용하여 생성한다. AnnotaionConfig가 붙은 이름처럼 어노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 생성하게되며, 이외에 XML이나 Groovy를 통한 생성 방법도 있다.

사진처럼 ApplicationContext 인터페이스로 구현한 구현체가 AnnotaionConfigApplicationContext를 포함해서 많다.

스프링 컨테이너의 생성 과정

  1. 어노테이션 기반의 AppConfig.class를 바탕으로 스프링 컨테이너가 생성된다.
    • 스프링 빈 저장소도 같이 생성된다.
    • 스프링 빈 저장소는 빈 이름과 빈 객체의 쌍으로 구성된다.
  2. AppConfig.class에 @Bean 어노테이션을 가진 모든 메소드를 스프링 빈 저장소에 저장한다.
    • 빈 이름은 기본적으로 메소드 이름을 사용하지만, 어노테이션에 name으로 직접 부여할 수도 있다.
    • 빈 이름은 중복되면 안 된다. (덮어씌워지거나, 오류 발생)
  3. 스프링 빈 저장소에 모든 빈이 등록되면, 스프링 컨테이너는 AppConfig.class를 바탕으로 의존관계를 주입(DI)한다.

스프링 컨테이너에 등록된 모든 빈 조회

스프링 컨테이너에 실제로 스프링 빈들이 잘 등록되었는지 확인하기 위한 테스트 코드를 작성한다.

ApplicationContextInfoText
package spring.basic.beanfind;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import spring.basic.AppConfig;

public class ApplicationContextInfoTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 Bean 출력하기")
    void findAllBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName + " object = " + bean);
        }
    }

    @Test
    @DisplayName("Application Bean 출력하기")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            //ROLE_APPLICATION : 직접 등록한 Application Bean
            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " object = " + bean);
            }
        }
    }

    @Test
    @DisplayName("Infrastructure Bean 출력하기")
    void findInfrastructureBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            //ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 Bean
            if(beanDefinition.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " object = " + bean);
            }
        }
    }
}
  • getBeanDefinitionNames() 메소드로 스프링 컨테이너에 등록된 모든 빈 이름을 조회한다.
  • getBean() 메소드로 빈 이름으로 빈 객체를 조회한다.
  • getRole() 메소드로 빈 역할에 따른 빈 객체를 조회할 수 있다.

빈이 가질 수 있는 Role은 ROLE_APPLICATION(0), ROLE_SUPPORT(1), ROLE_INFRASTRUCTURE(2) 3가지다. APPLICATION은 사용자가 직접 등록한 빈이고, SUPPORT는 외부 라이브러리나 내부적으로 부요 빈을 돕는 설정/구성용 빈이고, INFRASTRUCTURE는 스프링 배우에서 사용하는 빈이다. 그리고 위 테스트 코드에서는 beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION 같이 비교하고 있는데, 괄호 안의 숫자처럼 beanDefinition.getRole() == 0으로 작성해도 비교문은 True로 통과한다.

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

Spring 스터디2 - 6  (0) 2025.04.27
Spring 스터디2 - 5  (1) 2025.04.19
Spring 스터디2 - 3  (0) 2025.04.13
Spring 스터디2 - 2  (0) 2025.03.30
Spring 스터디2 - 1  (0) 2025.03.23

+ Recent posts