반응형
반응형

 

 

"김영한 스프링 핵심 원리 기본편" 내용을 기반으로 작성한 내용입니다.

 

스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런

김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보

www.inflearn.com

📝스코프

스코프스프링 컨테이너에서 빈을 관리하는 영역을 이야기한다. 기본적으로 스프링은 싱글톤 스코프를 가지고 이걸 가장 많이 사용한다. (이번 챕터는 그렇게 핵심은 아니지만 알아둬서 나쁠 거 없는 내용이고 언젠가는 쓰이는 내용이긴 하다) 참고로 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다

 

📝프로토타입

싱글톤이 아니라 빈의 생성하고 의존관계 주입까지만 관여하고 더는 관리하지 않는 범위이다. 그렇기 때문에 직접 종료를 해줘야한다. 그렇지 않으면 엄청나게 메모리에 쌓일 것이다. 예를 들자면 다양한 클라이언트가 어떤 Bean을 요청했을 때 일반적인 싱글톤의 경우 해당 Bean을 공유해서 사용한다. (때문에 도중에 값을 수정하면 다른 사람에게도 영향이 감) 반면 프로토타입의 경우 요청마다 Bean을 새롭게 생성해서 독자적인 스코프 영역을 만든다. 

 

// prototype 사용예제1
@Scope("prototype")
@Component
public class HelloBean {}

// prototype 사용예제2
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
	return new HelloBean();
}

 

 

📝싱글톤에서 프로토타입 빈을 사용 주의점

싱글톤으로 생성된 Client Bean이라는 객체가 있고 의존관계 주입된 Bean이 프로토타입일 경우 ClientBean을 이용해  의존관계 주입된 Prototype으로 만들어진 Bean의 필드값을 바꾸는 경우를 생각해보자

 

일반적으로 Prototype이라서 서로 다른 클라이언트가 요청해도 필드값은 +1해도 각각 +1씩 가져야하지만 Client Bean이 넓은 범위 싱글톤이기 때문에 Prototype도 공유하게 된다.

 

📝 Prototype 유지하기 (Provider)

public class PrototypeProviderTest {
    @Test
    void providerTest() {
        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);
    }
    
    static class ClientBean {
        @Autowired
        private ApplicationContext ac;

        public int logic() {
            PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
            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");
        }
    }
}

위와 같은 문제를 Provider라는 걸 이용해 해결이 가능하다. 일반적인 방법으로는 ClientBean에 있는 logic을 실행시킬때마다 Prototype이 자동으로 의존관계를 주입하게 하는게 아니라 내가 직접 스프링 컨테이너에서 찾아서 넣어주게끔 한다 ac.getBean(PrototypeBean.class); 이러한 과정을 DL(Dependency Lookup)이라고 한다. 위 코드는 DL을 내가 직접해주는게 스프링에서 제공해주는 게 있다.

 

 

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
    PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}

스프링에서는 ObjectProvider는 빈을 컨테이너에서 대신 찾아주는 DL 서비스 제공해준다.

 

@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
    PrototypeBean prototypeBean = provider.get();
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}

또 다른 방법으로Java표준인 jakarta.inject.Provider을 사용하는 것이다. 얘는 따로 라이브러리 추가를 해주긴 해야한다.

 

 

📝 웹스코프

웹스코프란 웹에서 요청이 들어왔을 떄 언제까지 유지할지에 대한 범위이다.

 

  • request
    • HTTP Request와 동일한 생명주기에 요청마다 별도 빈 인스턴스가 생성되고 관리된다.
  • session
    • HTTP Session과 동일한 생명주기
  • application
    • 서블릿 컨텍스트와 동일한 생명주기
  • websocket
    • 웹 소켓과 동일한 생명주기

 

// Controller
@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.setRequestURL(requestURL);
        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

// Logging Common
@Component
@Scope(value = "request")
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);
    }
}


// Service Area
@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final MyLogger myLogger;
    
    public void logic(String id) {
    	myLogger.log("service id = " + id);
    }
}

위 예제는 여러사용자가 많이 방문하면 로깅이 많이 찍힐텐데 동시다발적인 경우 어떤 곳에서 에러가 발생했을 경우 어떤 흐름에서 끊겼는지 구분해줄 수 있다. (HTTP Request Bean을 독자적 관리하기 때문에 구분 가능) 위 코드를 사용하면 MyLogger를 의존관계 주입을 하려고했지만 Request Scope라서 Request요청이 들어와야 생성된다. 이럴 경우 MyLogger가 필요한 곳에서 넣지못해서 에러가 나게 된다.

 

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerProvider;
    
    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);
        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

...

이러한 에러는 ObjectProvider로 해결이 가능하다 ObjectProvider는 스프링이 띄워질 때가 아니라 뒤늦게 요청이 들어와야 동적으로 만들어지거나 뒤늦게 조회가 필요한 경우 지연 조회 처리가 가능하다.

 

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

더 간단한 방법이 있는데 Proxy를 이용해 가짜 빈을 넣어줘서 있는 것처럼 동작하게끔한다. 그래서 에러가 나지 않는다.

반응형
반응형

 

 

"김영한 스프링 핵심 원리 기본편" 내용을 기반으로 작성한 내용입니다.

 

스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런

김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보

www.inflearn.com

📝Bean의 라이프 사이클 관리

Bean의 라이프 사이클은 아래와 같다.

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

 

다양한 방법이 있는데 필요한 것만 소개합니다.

 

방법1 (initMethod, destroyMethod)

public class NetworkClient {
	private String url;

    public NetworkClient() {
    	System.out.println("생성자 호출, url = " + url);
    }

    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);
    }

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

// Bean 관리
@Configuration
static class LifeCycleConfig {

    @Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-spring.dev");
        return networkClient;
    }
}

Spring에서 제공하는 기능을 사용합니다. initMethod초기화 콜백을 설정할 수 있고 destoryMethod를 통해 종료 콜백을 설정할 수 있습니다. 하지만 스프링 전용이라 스프링에 의존적입니다.

 

방법2 (@PreConstruct, @PostConstruct)

public class NetworkClient {
    private String url;
    
    public NetworkClient() {
    	System.out.println("생성자 호출, url = " + url);
    }
    
    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);
    }
    
    @PostConstruct
    public void init() {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }
    
    @PreDestroy
    public void close() {
        System.out.println("NetworkClient.close");
        disConnect();
    }
}

// Bean 관리
@Configuration
static class LifeCycleConfig {

    @Bean
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-spring.dev");
        return networkClient;
    }
}

@PreDestory초기화 작업설정@PostConstruct종료 작업설정을 할 수 있습니다.

해당 어노테이션은 Java에서 제공하는 기능으로 스프링에 의존적이지 않습니다.

 

 

방법2를 많이 씁니다. 만약 외부 라이브러리에다가 적용이 필요한 경우(해당 외부라이브러리를 직접 수정할 수 없기 때문에)는 Bean을 따로 생성해서 거기에서 외부라이브러리 호출한 다음 새로운 Bean을 만들어서 사용하면 될 거 같다.

 

📝참고

객체 생성할 때 생성자 작업하고 초기화 작업은 분리시키는게 유지보수에 좋다. 생성자 작업의 경우는 필드 Injection을 하고 초기화 작업의 경우 DB Connection따위와 같은 작업이 들어간다.

반응형
반응형

 

 

"김영한 스프링 핵심 원리 기본편" 내용을 기반으로 작성한 내용입니다.

 

스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런

김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보

www.inflearn.com

 

📝의존관계 주입 (@Autowired)

의존관계 주입은 크게 4가지가 있습니다.

  • 생성자 주입
  • 수정자 주입 (setter)
  • 필드 주입
  • 일반 메서드 주입 (많이 안 쓰기 때문에 패스)

 

생성자 주입

@Component
public class OrderServiceImpl implements OrderService {

  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;

  @Autowired // 생성자 1개일 때 생략 가능
  public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
      discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
  }
}

생성자를 통해 의존관계를 주입 받습니다. 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입이 됩니다. 여기에선 빈으로 생성된 MemberRepository, DiscountPolicy가 의존관계 주입되어 들어가게 됩니다.

 

 

수정자 주입

@Component
public class OrderServiceImpl implements OrderService {

  private MemberRepository memberRepository;
  private DiscountPolicy discountPolicy;

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

  @Autowired
  public void setDiscountPolicy(DiscountPolicy discountPolicy) {
    this.discountPolicy = discountPolicy;
  }
}

setter를 통해 의존관계를 주입 받습니다. 여기에선 빈으로 생성된 MemberRepository, DiscountPolicy가 의존관계 주입되어 들어가게 됩니다.

 

 

필드 주입

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

 

필드에 바로 주입하는 방법입니다. 코드가 간별하지만 외부에서 변경이 불가능해서 테스트 하기 어렵다는 단점이 있다. 예를 들면 setDiscountPolicy, setMemberRepository같은 Setter가 존재하면 DI 컨테이너(스프링)를 따로 안 띄워도 직접 넣어서 테스트할 때 사용이 가능한데 필드만 있는 경우는 스프링을 다 띄우지 않는 이상 테스트하기 어렵다.

 

 

📝어떤 걸 사용해야할까?

생성자 주입을 사용해야합니다. 필드 주입의 경우 테스트가 어려우며 Setter의 경우 변경가능성이 있기 때문에 객체 생성시 1번만 호출되게 설계할 수 있습니다.(불변) 또한 순수 자바코드로 단위테스트도 가능합니다. 

 

final 키워드 사용시 생성자에 값이 없으면 컴파일 오류를 발생시켜 장애를 방지할 수 있습니다.

 

 

📝빈이 2개 이상일 때 해결 (@Qulifier, @Primary)

@Autowired

  1. 타입 매칭
  2. 타입 매칭의 결과가 2개이상일 때 필드명, 파라미터명으로 빈 이름 매칭
@Autowired
private DiscountPolicy rateDiscountPolicy

DiscountPolicy를 상속받은 RateDiscountPolicy, FixDiscountPolicy가 있으면 위 코드와 같이 했을 때 필드명을 보고 주입하게 된다. 즉, RateDiscountPolicy를 주입하게 된다.

 

 

@Qualifier

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

@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}

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

Qualifier의 경우 @Qualifier에 등록한 Bean이름을 찾아서 주입합니다.

 

// 어노테이션 생성
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
	public @interface MainDiscountPolicy {
}


@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}

위 같이 @Qualifier지정할 때 따로 어노테이션을 따로 만들어서 사용할 수 있는데 이렇게 사용하면 컴파일시기에 에러를 잡아낼 수 있는 장점이 있다.

 

@Primary

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {} 

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

Primary의 경우 여러개 빈이 있어도 Primary 등록한 최상위 것이 먼저 등록됩니다. Qualifier의 경우 코드가 지저분해지기 때문에 @Primray를 더 잘 사용합니다.

 

📝등록된 Bean 다 가져오기

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);
    }
    
    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);
            System.out.println("discountCode = " + discountCode);
            System.out.println("discountPolicy = " + discountPolicy);
        	return discountPolicy.discount(member, price);
        }
    }
}

DiscountPolicy 인터페이스를 상속받은 고정할인(fixDiscountPolicy)하고 퍼센티지할인(rateDiscountPolicy)이 Bean으로 만들어져있습니다. 여기에 사용자가 넘긴 정보에 따라 고정 또는 퍼센티지를 결정해야할 경우 DiscounyPolicy구현체인 Bean으로 만들어진 걸 가져와서 넘긴 파라미터에 따라 다르게 처리할 수 있습니다.

 

Map의 경우 키에 Bean의 이름이 들어가고 값에는 구현체가 들어가게 됩니다. List의 경우는 키값이 없기 때문에 구현체만 들어가게 됩니다.

 

📝 수동 등록 vs 자동 등록

수동등록

@Configuration
public class DiscountPolicyConfig {

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

Configuration을 쓰고 Bean을 그 안에서 직접 생성해서 관리합니다. 한 곳에 모여있기 때문에 보기가 편합니다.

 

 

자동등록

@Component
public class DiscountPolicy {

}

Component어노테이션 기반으로 만들어진 어노테이션을 붙이면 Component Scan범위에 있으면 찾아서 Bean 등록해줍니다

 

요즘은 자동등록을 많이 쓰지만 수동등록의 경우도 필요할 때 써야한다. 보통 업무로직(Controller, Service, Repository)의 경우 자동 등록을 사용하고 기술지원(로깅, AOP 등)의 경우 Configuration으로 수동관리하는 편이다.

 

 

 

 

반응형
반응형

 

@RequiredArgsConstructor

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class MyClass {
    private final String name;  // 생성자 매개변수로 포함됨
}

// 생성된 생성자:
// public MyClass(String name) {
//     this.name = name;
// }

 

final로 된 필드기반으로 생성자를 생성합니다.

 

 

@Getter

import lombok.Getter;

@Getter
public class MyClass {
    private String name;
    private int age;
}

// 생성된 메서드:
// public String getName() { return name; }
// public int getAge() { return age; }

필드기반으로 Getter를 생성합니다.

 

 

@Setter

import lombok.Setter;

@Setter
public class MyClass {
    private String name;
    private int age;
}

// 생성된 메서드:
// public void setName(String name) { this.name = name; }
// public void setAge(int age) { this.age = age; }

필드기반으로 Setter를 생성합니다.

 

@ToString

import lombok.ToString;

@ToString
public class MyClass {
    private String name;
    private int age;
}

// 생성된 메서드:
// public String toString() { return "MyClass(name=" + this.name + ", age=" + this.age + ")"; }

필드기반으로 toString()메서드를 자동으로 생성합니다. 로깅시 객체안에 있는 값을 편하게 볼 수 있습니다.

 

@Data

import lombok.Data;

@Data
public class MyClass {
    private String name;
    private int age;
}

// 생성되는 메서드:
// - Getter/Setter for name and age
// - toString(), equals(), hashCode()

 

반응형
반응형

https://start.spring.io/ 사이트에서 원하는 버전으로 프로젝트 설정하고 Generate하면 됩니다. Dependencies는 가장 많이 쓰는 것과 필요한 것 위주로 적어놨습니다.

 

Dpendencies

  •  

 

 

 

 

 

 

반응형
반응형

 

 

"김영한 스프링 핵심 원리 기본편" 내용을 기반으로 작성한 내용입니다.

 

스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런

김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보

www.inflearn.com

 

📝@Component 기능을 가진 어노테이션

기본적으로 아래 어노테이션들은 @Component를 가지고 있어서 Component Scan 범위에 포함된다.

 

  • @Controller
    • 스프링 MVC 컨트롤러로 인식
  • @Service
    • 특별 처리는 하지 않지만 비즈니스 계층이라는 걸 인식시킨다.
  • @Repository
    • 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
  • @Configuration
    • 스프링 설정 정보라는 표시이다

 

📝직접 어노테이션 만들기 (Custom)

등록할 컴포넌트 설정 어노테이션

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

 

제외할 컴포넌트 설정 어노테이션

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

 

어노테이션 적용시키기

// 어노테이션 (등록) 
@MyIncludeComponent
public class BeanA {
}

// 어노테이션 (제외)
@MyExcludeComponent
public class BeanB {
}

 

컴포넌트 스캔에 등록하거나 제외할 어노테이션 등록

@ComponentScan(
    includeFilters = @Filter(type = FilterType.ANNOTATION, classes =
        MyIncludeComponent.class),
    excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
        MyExcludeComponent.class)
)
반응형
반응형

 

 

"김영한 스프링 핵심 원리 기본편" 내용을 기반으로 작성한 내용입니다.

 

스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런

김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보

www.inflearn.com

 

 

📝컴포넌트 스캔

@Configuration과 @Bean을 이용해도 되지만 점점 많아지게되면 힘들고 파일 하나에 엄청난 @Bean이 생겨날 수 있다.

이걸 깔끔하게 만드는 법이 컴포넌트 스캔이다. 컴포넌트 스캔은 @Component가 붙은 정보를 Bean으로 등록하게 된다.

 

 

컴포넌트 스캔

@Configuration
@ComponentScan(
	excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
}

 

컴포넌트

@Component
public class MemoryMemberRepository implements MemberRepository {}

이렇게 되면 @Component가 붙은 MemoryMemberRepository를 Bean으로 등록할 수 있다.

 

 

📝@Bean vs @Component 

@Bean은 이제 더이상 쓸모 없는 행동일까? 어떤 상황에 @Bean을 쓰고 어떤 상황에 @Component를 쓰는지 알아보자

 

 

Bean 등록 방식 (@Bean, @Component)

// 이전 방식 사용
@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyService(); // MyService를 빈으로 등록
    }

    @Bean
    public MyComponent myComponent() {
        // MyComponent가 MyService를 필요로 한다고 명시적으로 주입
        return new MyComponent(myService());
    }
}

//이후 방식 사용
@Component
public class MyService {
    // 빈으로 등록될 클래스
}

@Component
public class MyComponent {

    private final MyService myService;

    // @Autowired를 통해 Spring이 자동으로 MyService 빈을 주입함
    @Autowired
    public MyComponent(MyService myService) {
        this.myService = myService;
    }
}

비교하기 위해 동일한 빈 등록하는 방식을 Bean과 Component로 보여줬다.

 

  • @Bean
    • @Configuration이 필요하다
    • 메소드 단위에 붙이게 된다. 해당 메소드는 클래스를 반환해야한다 (Component에서 해당 클래스가 @Component가 된다)
    • 의존관계 주입을 직접 Inject을 하기 때문에 Autowired를 통해 Injection할 필요가 없다
      • MyComponent에 myService()를 매개변수로 직접 넣어주고 있다.
  • @Component
    • 클래스 단위에 붙이게 된다. 해당 클래스는 빈으로 등록되게 된다.
    • 의존관계 주입시 @Autowired를 사용한다

 

일반적으로 @Component를 많이 사용한다. @Bean 방식하고 비교하면 코드수도 확연히 줄어들게 된다. 하지만 @Bean도 필요할 때가 있다.

 

@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource(); // 외부 라이브러리 클래스 인스턴스 생성
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        dataSource.setUsername("username");
        dataSource.setPassword("password");
        return dataSource;
    }
}

HikariDataSource의 경우 외부 라이브러리이기 때문에 내가 직접 @Component를 붙일 수가 없다. 스프링에서 관리가 필요한데 이럴 경우 위 코드와 같이 직접 HikariDataSource의 객체를 만들고 return시킨 이후에 해당 것을 빈객체로 등록하게 되면 된다. 이럴 경우 HikariDataSource를 new해서 사용하면 안 되고 내가 Bean으로 등록한 DataSourceConfig의 dataSource()를 사용해야한다.

 

 

 

반응형
반응형

 

 

"김영한 스프링 핵심 원리 기본편" 내용을 기반으로 작성한 내용입니다.

 

스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런

김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보

www.inflearn.com

 

 

스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다 그래서 초당 100의 요청이 오면 100개의 객체가 생성되고 소멸되어 메모리 낭비가 심해진다. 해결방안으로는 딱 1개만 생성되고 공유하도록 설계하면 된다.

 

📝싱글톤 패턴

1개만 생성되고 그것을 공유하도록 하는 패턴이다. 즉, 클래스의 인스턴스가 딱 1개만 생성되는 걸 보장해준다. 그래서 2개 이상 생성이 안 된다. 주의점으로 private으로 만들어서 임의로 new 키워드로 만들 수 없게 해야한다.

 

싱글톤 패턴 적용

public class SingletonService {

  //1. static 영역에 객체를 딱 1개만 생성해둔다.
  private static final SingletonService instance = new SingletonService();

  //2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다.
  public static SingletonService getInstance() {
    return instance;
  }

  //3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
  private SingletonService() {

  }

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

 

테스트 코드

@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
public void singletonServiceTest() {
    //private으로 생성자를 막아두었다. 컴파일 오류가 발생한다.
    // new SingletonService();

    //1. 조회: 호출할 때 마다 같은 객체를 반환
    SingletonService singletonService1 = SingletonService.getInstance();

    //2. 조회: 호출할 때 마다 같은 객체를 반환
    SingletonService singletonService2 = SingletonService.getInstance();

    //참조값이 같은 것을 확인
    System.out.println("singletonService1 = " + singletonService1);
    System.out.println("singletonService2 = " + singletonService2);

    // singletonService1 == singletonService2
    assertThat(singletonService1).isSameAs(singletonService2);
    singletonService1.logic();
}

하지만 이상적이지만은 않다. 싱글톤 패턴을 사용할 때 문제점이 있는데 아래와 같다.

 

 

싱글톤 패턴 문제점

  • 싱글톤 패턴은 구현 코드 자체가 많이들어간다.
  • 클라이언트가 구체 클래스를 의존해 DIP를 위반한다.
  • 구체 클래스 의존으로 인해 OCP 위반 가능성이 높다.
  • 테스트가 어렵다.
  • 내부 속성 변경이나 초기화가 어렵다.
    • 내부 속성 변경하게끔하면 공유해서 쓰기 때문에 여러곳에서 쓰일 경우 각종 문제들이 터질 가능성이 매우 높다. 즉, 무상태로 설계해야한다.
  • private 생성으로 자식 클래스 만들기가 어렵다.
  • 위와 같은 이유로 유연성이 떨어져 안티패턴으로 불린다.

 

📝싱글톤 컨테이너

우리가 직접 싱글톤을 구현해서 사용할 때는 이러한 문제점들이 있다. 그러면 스프링의 경우는 어떨까? 스프링은 싱글톤의 문제점들을 다 해결하면서 싱글톤의 장점만 취한다.

 

@Configuration
public class AppConfig {

  @Bean
  public MemberService memberService() {
    // memberRepository 호출
    return new MemberServiceImpl(memberRepository());
  }

  @Bean
  public OrderService orderService() {
    // memberRepository 호출
    return new OrderServiceImpl(
        memberRepository(),
        discountPolicy());
  }

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

memberService빈을 만드는 코드를 보면 memberRepository()를 호출한다

orderService빈을 만드는 코드를 보면 memberRepository()를 호출한다

 

결과적으로 2개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨진 것처럼 보인다. 이러한 스프링 컨테이너의 문제점을 어떻게 해결할까?

 

@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);

    System.out.println("memberService -> memberRepository = " +
            memberService.getMemberRepository());
    System.out.println("orderService -> memberRepository = " +
            orderService.getMemberRepository());
    System.out.println("memberRepository = " + memberRepository);

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

위와 같이 테스트 코드를 생성해보면 모두 같은 인스턴스라는 걸 알 수 있다. 즉, 스프링에서는 자체적으로 싱글톤을 유지시키고 있음을 알 수 있다.

 

@Bean만 사용해서 등록해서 사용할 수도 있지만 @Configuration을 사용하면 내부에서 바이트 코드를 조작해서 동일한 Bean의 경우 한번만 사용하게 즉, 싱글톤을 유지시킬 수 있게 정보를 주어서 도와준다.

 

이런식으로 원본을 두고 CGLIB를 이용해 복사된 형태의 것으로 스프링 컨테이너에서 관리해 싱글톤을 유지시킬 수 있게 도와준다.

반응형
반응형

 

 

"김영한 스프링 핵심 원리 기본편" 내용을 기반으로 작성한 내용입니다.

 

스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런

김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보

www.inflearn.com

 

 

📝BeanFactory, ApplicationContext

BeanFactory

스프링 컨테이너의 최상위 인터페이스빈을 관리하고 조회하는 역할을 담당한다.

 

 

Application Context

김영한 스프링 핵심 원리 기본편

BeanFactory의 기능을 상속받아 더 많은 기능을 제공하고 BeanFactory를 사용하는 게 아닌 Application Context를 주로 사용한다

 

  • MessageSource
    • 국제화
  • Environment
    • 로컬, 개발, 운영 구분해서 처리
  • Application Event
    • 이벤트 발행하고 구독하는 모델 편리 지원
  • ResourceLoader
    • 파일, 클래스패스, 외부 등 리소스 편리하게 조회 기능

 

 

📝 Annotation 방식 vs XML 방식

Annotation 기반 코드

Annotation을 이용해 Bean에 등록하고 관리하기 쉽게 도와준다. 지금까지 해온 방식이다.

 

 

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="hello.core.member.MemberServiceImpl">
 		<constructor-arg name="memberRepository" ref="memberRepository" />
 	</bean>
 	<bean id="memberRepository"class="hello.core.member.MemoryMemberRepository" />
 	<bean id="orderService" class="hello.core.order.OrderServiceImpl">
 		<constructor-arg name="memberRepository" ref="memberRepository" />
 		<constructor-arg name="discountPolicy" ref="discountPolicy" />
	</bean>
 	<bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy" />
</beans>

최근에는 스프링 부트를 많이 사용하며 XML 기반 설정은 잘 사용하지 않는다.

 

📝 빈 설정 메타 정보 (BeanDefinition)

기본적으로 특정 Reader가 존재해 Bean 설정 정보를 읽고 메타 정보를 만들어 사용하게 된다. BeanDefinition에는 Bean의 메타정보들이 들어있다. 직접 Reader와 Definition을 Bean의 작성방식을 내 임의대로 커스텀해서 만들 수도 있지만 거의 사용되진 않는다.

 

반응형