빈 생명주기 콜백 시작

데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.

애플리케이션 시작 시점에 connect() 를 호출해서 연결을 맺고, 애플리케이션이 종료되면 disConnect() 를 호출해서 연결을 끊는 NetworkClient 예제를 살펴보자

//Networkclient.java

package hello.core.lifecycle;

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);
    }
}
//BeanLifeCycleTest.java

package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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("<http://hello-spring.dev>");
            return networkClient;
        }
    }
}

생성자 부분을 보면 url 정보 없이 connect가 호출되는 것을 확인할 수 있다. 객체를 생성하는 단계에는 url이 없고, 객체 생성 이후에 setUrl() 이 호출되어 url이 입력 된다.

 

스프링 빈은 간단하게 다음과 같은 라이프사이클을 가진다.

객체 생성 → 의존관계 주입

그러므로 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야한다.

 

스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다. 또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 진행할 수 있다.

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

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

객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다.

 

스프링의 빈 생명주기 콜백 방법

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

 

 

인터페이스 InitializingBean, DisposableBean

//Networkclient.java

package hello.core.lifecycle;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class Networkclient implements InitializingBean , DisposableBean {

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

    @Override
    public void afterPropertiesSet() throws Exception {
        connect();
        call("초기화 연결 메시지");
    }

    @Override
    public void destroy() throws Exception {
        disconnect();
    }
}

  • InitializingBean 은 afterPropertiesSet() 메서드로 초기화를 지원한다.
  • DisposableBean 은 destroy() 메서드로 소멸을 지원한다.

초기화, 소멸 인터페이스의 단점

  • 이 인터페이스는 스프링 전용 인터페이스이다. 해당 코드가 스프링 전용 인터페이스에 의존한다.
  • 초기화,소멸 메서드의 이름을 변경할 수 없다.
  • 코드를 수정할 수 없는 외부 라이브러리에 적용할 수 없다.

→ 거의 사용하지 않는다.

 

 

빈 등록 초기화, 소멸 메서드

@Bean(initMethod = "초기화 메서드 이름", destroyMethod = "소멸 메서드 이름") 코드를 통해 초기화, 소멸 메서드를 지정할 수 있다.

//Networkclient.java

package hello.core.lifecycle;

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() {
        connect();
        call("초기화 연결 메시지");
    }

    public void close() {
        disconnect();
    }
}
//BeanLifeCycleTest.java

package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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(initMethod = "init", destroyMethod = "close")
        public Networkclient networkclient(){
            Networkclient networkClient = new Networkclient();
            networkClient.setUrl("<http://hello-spring.dev>");
            return networkClient;
        }
    }
}

특징

  • 메서드 이름을 자유롭게 줄 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않는다.
  • 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.

종료 메서드 추론

라이브러리는 대부분 ‘close’, ‘shutdown’이라는 이름의 종료 메서드를 사용한다.

@Bean의 ‘destroyedMethod’ 는 기본 값이 ‘inferred’로 등록되어 있는데, 이 추론 기능은 ‘close’, ‘shutdown’이라는 이름의 메서드를 자동으로 호출해준다.

따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 도착한다.

이 기능을 사용하기 싫을 경우 destroyedMethod=””와 같이 빈 공백을 지정하면 된다.

 

 

 

애노테이션 @PostConstruct, @PreDestroy

@PostConstruct , @PreDestroy 이 두 애노테이션을 사용하면 가장 편리하게 초기화와 종료를 실행할 수 있다.

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

특징

  • 최신 스프링에서 가장 권장하는 방법이다.
  • 애노테이션 하나만 붙이면 되므로 매우 편리하다.
  • 패키지를 잘 보면 javax.annotation.PostConstruct 이다. 스프링에 종속적인 기술이 아니라 JSR-250 라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.
  • 컴포넌트 스캔과 잘 어울린다.
  • 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용하자.

 

 

정리

@PostConstruct, @PreDestroy 애노테이션을 사용하자

코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야 하면 @Bean initMethod , destroyMethod 를 사용하자.

 

다양한 의존관계 주입 방법

  • 생성자 주입
  • 수정자 주입 ( setter 주입 )
  • 필드 주입
  • 일반 메서드 주입

생성자 주입

생성자를 통해서 의존 관계를 주입 받는 방법

지금까지 했던 방법

특징

  • 생성자 호출 시점에 딱 1번만 호출되는 것이 보장
  • 불변, 필수 의존관계에 사용
//OrderServiceImpl.jav

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

    }
}
  • MemberRepository, DiscountPolicy 변경 불가 (불변)
  • 생성자 호출할 때 MemberRepository, DiscountPolicy 필수로 넘겨주어야 함 (필수)

< 중요! > 생성자가 하나일 경우는 @Autowired를 생략해도 자동 주입 된다.

수정자 주입

setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법

특징

  • 변경,선택 가능성이 있는 의존관계에서 사용
  • 자바빈 프로퍼티 규약의 수정자 메서드를 사용하는 방법
//OrderServiceImpl.java

@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;
    }
    
}
   @Autowired(required = false)
    public void setMemberRepository(MemberRepository memberRepository) {
        
        this.memberRepository = memberRepository;
    }
  • @Autowired의 기본 동작은 주입할 대상이 없으면 오류가 발생한다.
    • (required = false)를 추가함으로써 memberRepository는 필수값이 아니게 됨 ( 선택 )

참고: 자바빈 프로퍼티: 자바에서는 과거부터 필드의 값을 직접 변경하지 않고 setXxx, getXxx 라는 메서드를 통해서 값을 읽거나 수정하는 규칙을 만들었는데, 이것이 자바빈 프로퍼티 규약이다.

  • 프로퍼티 값을 구하는 메소드는 get으로 시작한다.
  • 프로퍼티 값을 변경하는 메소드는 set으로 시작한다.
  • get과 set 뒤 첫 글자는 대문자로 바꾼다.

필드 주입

필드에 바로 주입하는 방법

특징

  • 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점이 있다.
  • DI 프레임워크가 없으면 아무것도 할 수 없다.
    • 순수한 테스트를 만들 수 없음. 값을 넣어줄 때 따로 setter를 만들어 주입을 해줘야하는데, 그렇게 setter가 만들어지면 그냥 수정자 주입과 다를 바가 없다.
@Component
public class OrderServiceImpl implements OrderService{
    @Autowired private MemberRepository memberRepository;
    @Autowired private DiscountPolicy discountPolicy;
}
  • 가급적 사용하지않는게 좋지만 사용할 수 있는 경우가 있다.
    • 테스트 코드
    • 스프링 설정을 목적으로 하는 @Configuration 같은 곳

일반 메서드 주입

일반 메서드를 통한 주입

특징

  • 한번에 여러 필드를 주입 받을 수 있다.
  • 일반적으로 잘 사용하지는 않는다.
    • 생성자 주입, 수정자 주입으로 다 해결되기 때문
//OrderServiceImpl.java

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

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

 
}

참고: 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다. 스프링 빈이 아닌 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.

 

 

옵션 처리

주입할 스프링 빈이 없어도 동작해야할 때가 있다.

자동 주입 대상을 옵션으로 처리하는 방법

  • @Autowired(required = false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력된다.
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty가 입력된다.
package hello.core.autowired;

import hello.core.member.Member;
import jakarta.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.context.ApplicationContext;

import java.util.Optional;

public class AutowiredTest {

    @Test
    void AutowiredOption(){
        ApplicationContext ac = new AnnotationConfigReactiveWebApplicationContext(TestBean.class);

    }

    static class TestBean{

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

    }
}

실행 결과

  • member는 스프링 빈이 아니다.
  • setNoBean1은 @Autowired(required = false) 이므로 호출 자체가 안된다

참고: @Nullable, Optional은 스프링 전반에 걸쳐서 지원된다. 예를 들어 생성자 자동 주입에서 특정 필드에 사용해도 된다.

생성자 주입을 선택하라

불변

  • 대부분의 의존관계 주입은 한번 일어난 후 종료시점까지 의존관계를 변경할 일이 없다.
  • 수정자 주입을 이용하면 setXxx메서드를 public으로 열어두어야하는데, 이 때문에 누군가 실수로 변경할 가능성이 생긴다.

→ 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 불변하게 설계 가능하다.

누락

프레임워크 없이 순수한 자바 코드로 단위 테스트를 하는 경우

//OrderServiceImpl.java

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

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

    }*/

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member,itemPrice);

        return new Order(memberId,itemName,itemPrice,discountPrice);
    }

    //테스트 용도
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}
//OrderServiceImplTest.java

package hello.core.order;

import org.junit.jupiter.api.Test;

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

class OrderServiceImplTest {

    @Test
    void createOrder(){
        OrderServiceImpl orderService = new OrderServiceImpl();
        orderService.createOrder(1L,"itemA",10000);

    }

}

해당 테스트 코드를 실행하면 NullPointerException에러가 뜬다

→ memberRepository, discountPolicy 모두 의존관계 주입이 누락되었기 때문

//OrderServiceImplTest.java

package hello.core.order;

import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

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

class OrderServiceImplTest {

    @Test
    void createOrder(){
        MemberRepository memberRepository = new MemoryMemberRepository();
        memberRepository.save(new Member(1L, "catsbi", Grade.VIP));

        OrderServiceImpl orderService = new OrderServiceImpl();
        orderService.setMemberRepository(memberRepository);
        orderService.setDiscountPolicy(new FixDiscountPolicy());

        Order order = orderService.createOrder(1L, "itemA", 10000);

        assertThat(order.getDiscountPrice()).isEqualTo(1000);

    }

}

위와 같이 코드를 수정해주어야 정상적으로 동작함

수정자 주입의 경우 누락된 데이터가 무엇인지 코드를 보며 일일이 찾아보며 수정해주어야한다.

final 키워드

생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다.

→ 생성자에서 값이 설정되지 않는 경우, 해당 오류를 컴파일 시점에 막아준다.

 

 

기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여할 수 있다. (생성자 주입과 수정자 주입을 동시에 사용할 수 있음)

 

롬복과 최신 트렌드

생성자 주입을 사용하면 생성자도 만들어야 하고, 주입 받은 값을 대입하는 코드도 만들어야 하는 번거로움이 생긴다.

→ 롬복을 통해 이 번거로움을 줄일 수 있다.

start.spring.io에서 초기 프로젝트를 생성할 때 롬복을 추가해줄 수 있지만 우리는 프로젝트 생성 시 롬복을 추가하지 않았으므로 build.gradle 에 아래와 같이 코드를 추가해준다.

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.0.6'
	id 'io.spring.dependency-management' version '1.1.0'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
//lombok 설정 추가 시작

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}
//lombok 설정 추가 끝
repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	//lombok 라이브러리 추가 시작
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
	//lombok 라이브러리 추가 끝
}

tasks.named('test') {
	useJUnitPlatform()
}

코드 추가 후 우측상단에 코끼리 모양을 눌러주면 새롭게 추가한 롬복 라이브러리가 적용된다.

추가적으로 IntelliJ IDEA > settings 에서 Annotation Processors를 찾아 Enable annotation processing을 체크해준다

 

롬복 적용 후 코드

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

//생성자 작성할 필요가 없어짐
    /*public OrderServiceImpl(MemberRepository memberRepository,DiscountPolicy discountPolicy) {
        this.memberRepository=memberRepository;
        this.discountPolicy = discountPolicy;

    }*/
}

@RequiredArgsConstructor : final이 붙은 필드들을 모아 생성자를 자동으로 생성해줌. 

 

 

 

 

조회 빈이 2개 이상 - 문제

@Autowired는 타입으로 조회한다.

DiscountPolicy 의 하위 타입인 FixDiscountPolicy , RateDiscountPolicy 둘다 스프링 빈으로 선언할 경우 문제가 발생한다.

실행해보면 하나의 빈을 기대했는데 ‘fixDiscountPolicy’ , ‘rateDiscountPolicy’ 2개가 발견되었다는 오류메시지가 뜬다.

 

이 문제를 해결하기 위해 매번 하위 타입을 지정하는 것은 DIP를 위배하고 유연성이 떨어진다.

어떻게 해결할 수 있을까?

 

@Autowired 필드명, @Qualifier, @Primary

앞서 언급한 문제를 해결할 수 있는 방법을 알아보자.

  • @Autowired 필드 명 매칭
  • @Qualifier @Qualifier끼리 매칭 빈 이름 매칭
  • @Primary 사용

@Autowired 필드 명 매칭

@Autowired는 타입 매칭을 시도하고, 이 때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.

//기존 코드
@Autowired
  private DiscountPolicy discountPolicy
//필드명을 빈 이름으로 수정한 코드
@Autowired
  private DiscountPolicy rateDiscountPolicy

필드 명을 ‘rateDiscountPolicy’로 지정하여 오류 없이 정상 주입 된다.

@Autowired는 먼저 타입 매칭을 시도하고, 그 결과 여러 빈이 있을 때 필드 명, 파라미터 명 으로 빈 이름을 매칭한다.

@Qualifier 사용

@Qualifier는 추가 구분자를 붙여준느 방법이다. 빈 이름을 변경하는 것이 아닌 추가적인 방법을 통한 주입이다.

빈 등록시 @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("mainDiscountPolicy") 를 못찾으면 ‘mainDiscountPolicy’라는 이름의 스프링 빈을 추가로 찾는다. 하지만 @Qualifier는 @Qualifier 를 찾는 용도로만 사용하는게 명확하고 좋다

@Qualifier는 먼저 @Qualifier끼리 매칭을 시도하고, 없을 경우 빈 이름을 매칭한다. 매칭되는 빈을 찾지 못한다면 NoSuchBeanDefinitionException 예외가 발생한다.

@Primary

@primary는 우선순위를 지정하는 방법이다.

아래와 같이 @Primary를 붙여주게 되면 rateDiscountPolicy가 fixDiscountPolicy보다 우선순위를 갖게 된다.

@Component
  @Primary
  public class RateDiscountPolicy implements DiscountPolicy {}

@Qualifier의 경우 주입 받을 때 모든 코드에 @Qualifier를 붙어주어야 한다는 단점이 있다. 반면에 @Primary는 @Qualifier처럼 모든 코드에 애노테이션을 붙이지 않아도 된다는 장점이 있다.

@Primary와 @Qualifier의 우선 순위

@Primary는 기본값 처럼 동작하는 것이고, @Qualifier는 매우 상세하게 동작한다. 스프링은 자동보다는 수동이, 넓은 범위의 선택권보다는 좁은 범위의 선택권이 우선순위가 높다. 따라서 @Qualifier가 우선순위가 더 높다.

 

 

애노테이션 직접 만들기

@Qualifier("mainDiscountPolicy") 처럼 문자를 적으면 컴파일시 타입 체크가 안된다. 즉, @Qualifier("mmainDiscountPolicy")와 같이 오타가 발생해도 동작한다.

다음과 같이 애노테이션을 만들어서 문제를 해결할 수 있다.

package hello.core.annotation;

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.*;

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

import hello.core.annotation.MainDiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
//@Qualifier("mainDiscountPolicy")
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy{
    private int discountPercent =10;
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price * discountPercent /100 ;
        }else{
            return 0;
        }

    }
}
//OrderServiceImpl.java

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

    }

애노테이션에는 상속이라는 개념이 없다.이렇게 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다. @Qualifier뿐만 아니라 다른 애노테이션들도 함께 조합해서 사용할 수 있다.

 

조회한 빈이 모두 필요할 때, List, Map

지금까지는 자동주입하기위해 스프링 빈 검색해서 2개 이상 나오는 경우 하나를 골라서 주입하는 방법에 대해 알아보았다. 이번에는 2개 이상의 스프링 빈을 모두 조회해야하는 경우를 살펴보자.

예를 들어, 소비자가 rateDiscountPolicy와 fixDiscountPolicy 중 선택할 수 있다고 가정해보자.

package hello.core.autowired;

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

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

    }
}

로직 분석

DiscountService는 Map으로 모든 DiscountPolicy 를 주입받는다. 이때 fixDiscountPolicy , rateDiscountPolicy 가 주입된다.

discount () 메서드는 discountCode로 "fixDiscountPolicy"가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행한다. 물론 “rateDiscountPolicy”가 넘어오면 rateDiscountPolicy 스프링 빈을 찾아서 실행한다.

주입 분석

Map<String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.

List<DiscountPolicy> : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다. 만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다.

 

자동, 수동의 올바른 실무 운영 기준

“편리한 자동 기능을 기본으로 사용하자”

수동 빈 등록은 언제 사용하면 좋을까?

  • 업무 로직 빈: 비즈니스 요구사항들을 개발할 때 추가되거나 변경된다. → 자동 기능 적극 사용
  • 기술 지원 빈: 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들 → 수동 빈 등록을 사용

애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱! 설정 정보에 바로 나타나게 하는 것이 유지보수 하기 좋다.

비즈니스 로직 중에서 다형성을 적극 활용하는 경우

DiscountService 가 의존관계 자동 주입으로 Map<String, DiscountPolicy> 에 주입을 받는 상황을 생각해보자. 여기에 어떤 빈들이 주입될 지, 각 빈들의 이름은 무엇일지 코드만 보고 한번에 쉽게 파악할 수 있을까?

→ 수동 빈으로 등록하거나, 자동으로 할 경우 특정 패키지에 같이 묶어 두는 것이 좋다.

컴포넌트 스캔과 의존관계 자동 주입 시작하기

  • 스프링 빈을 @Bean이나 XML의 <bean> 등을 통해서 설정 정보에 직접 등록했다. 등록해야할 스프링 빈이 수 십, 수 백개가 된다면?
    • 귀찮고, 설정 정보도 커지며 누락하는 문제도 발생
  • 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공
  • 또 의존관계도 자동으로 주입하는 @Autowired도 제공

 

//AutoAppConfig.java

package hello.core;

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

@Configuration
@ComponentScan(
        //컴포넌트 스캔을 사용하면 @Configuration 이 붙은 설정 정보도 자동으로 등록되기 때문에, AppConfig, TestConfig 등 앞서 만들어두었던 설정 정보도 함께 등록되고, 실행되어 버린다.
        // 그래서 excludeFilters 를 이용해서 설정정보는 컴포넌트 스캔 대상에서 제외
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
}
  • 컴포넌트 스캔을 사용하려면 먼저 @ComponentScan을 설정 정보에 붙여주면 된다.
  • 컴포넌트 스캔은 이름 그대로 @Component 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록 ( 참고: @Configuration 이 컴포넌트 스캔의 대상이 된 이유도 @Configuration 소스코드를 열어보면 @Component 애노테이션이 붙어있기 때문이다.)
  • MemoryMemberRepository, RateDiscountPolicy ,MemberServiceImpl, OrderServiceImpl에 @Component 추가
  • MemberServiceImpl,OrderServiceImpl의 경우 생성자에 @Autowired 추가
    • 의존 관계 주입

 

테스트

//AutoAppconfigTest.java

package hello.core.scan;

import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.*;

public class AutoAppConfigTest {

    @Test
    void basicScan() {
        ApplicationContext ac = new
                AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);

    }

}
  • 로그를 통해 잘 동작하는 것 확인

 

 

 

 

 

 

탐색 위치와 기본 스캔 대상

 

탐색할 패키지의 시작 위치 지정

모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸리기 때문에 필요한 위치부터 탐색하도록 지정할 수 있다.

@ComponentScan(
          basePackages = "hello.core",
          
           // 여러 시작 위치를 지정할 수도 있음
          basePackages = {"hello.core", "hello.service"}
          
		  // 지정한 클래스의 패키지를 탐색 위치로 지정
          basePackageClasses =AutoAppConfig.class

}

지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.

 

권장하는 방법
개인적으로 즐겨 사용하는 방법은 패키지 위치를 지정하지 않고, 
설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것이다. 
최근 스프링 부트도 이 방법을 기본으로 제공한다.

 

 

 

컴포넌트 스캔  기본 대상

컴포넌트 스캔은 @Component 뿐만 아니라 아래도 추가로 대상에 포함한다.

컴포넌트 스캔의 용도 뿐만 아니라 다음 애노테이션이 있으면 스프링은 부가 기능을 수행한다.

  • @Component : 컴포넌트 스캔에서 사용
  • @Controlller : 스프링 MVC 컨트롤러에서 사용, 부가 기능 : 스프링 MVC 컨트롤러로 인식
  • @Service : 스프링 비즈니스 로직에서 사용, 부가 기능 :사실 @Service 는 특별한 처리를 하지 않는다. 대신 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나 라고 비즈니스 계층을 인식하는데 도움이 된다
  • @Repository : 스프링 데이터 접근 계층에서 사용, 부가 기능 :스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환
  • @Configuration : 스프링 설정 정보에서 사용, 부가 기능 :스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리
참고 : useDefaultFilters - 기본으로 켜져있는데, 이 옵션을 끄면 기본 스캔 대상들이 제외된다.

 

필터

includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.

excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.

 

예제

package hello.core.scan.filter;

import java.lang.annotation.*;

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

import java.lang.annotation.*;

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

@MyIncludeComponent
public class BeanA {
}
package hello.core.scan.filter;

@MyExcludeComponent
public class BeanB {
}
package hello.core.scan.filter;

import org.junit.jupiter.api.Assertions;
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.assertThat;
import static org.springframework.context.annotation.ComponentScan.*;

public class ComponentFilterAppConfigTest {

@Test
void filterScan() {
ApplicationContext ac = new
AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
assertThat(beanA).isNotNull();
Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("beanB", BeanB.class));
}

@Configuration
@ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
static class ComponentFilterAppConfig {
}
}
  • includeFilters 에 MyIncludeComponent 애노테이션을 추가해서 BeanA가 스프링 빈에 등록된다.
  • excludeFilters 에 MyExcludeComponent 애노테이션을 추가해서 BeanB는 스프링 빈에 등록되지 않는다.

FilterType 옵션

  • ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.
    • ex) org.example.SomeAnnotation
  • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.
    • ex) org.example.SomeClass
  • ASPECTJ: AspectJ 패턴 사용
    • ex) org.example..*Service+
  • REGEX: 정규 표현식
    • ex) org\.example\.Default.*
  • CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리
    • ex) org.example.MyTypeFilter
 참고: @Component 면 충분하기 때문에, includeFilters 를 사용할 일은 거의 없다. excludeFilters 는 여러가지 이유로 간혹 사용할 때가 있지만 많지는 않다.>  특히 최근 스프링 부트는 컴포넌트 스캔을 기본으로 제공하는데, 개인적으로는 옵션을 변경하면서 사용하기 보다는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장하고, 선호하는 편이다.

 

 

 

중복 등록과 충돌

두 가지 상황 존재

  1. 자동빈등록vs자동빈등록
  2. 수동빈등록vs자동빈등록

자동 빈 등록 vs 자동 빈 등록

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 오류를 발생시킨다.

-> ConflictingBeanDefinitionException 예외 발생

 

 

수동 빈 등록 vs 자동 빈 등록

수동 빈 등록이 우선권을 가진다. -> 수동 빈이 자동 빈을 오버라이딩

수동 빈 등록시 로그가 남음

Overriding bean definition for bean 'memoryMemberRepository' with a different
  definition: replacing

 최근에는 스프링 부트에서 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
 

 

 

 

웹 애플리케이션과 싱글톤

//SingletonTest.java

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

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

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

}

결과

  • 위의 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때마다 객체를 새로 생성
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸됨 -> 메모리 낭비가 심함
  • 해결방안
    • 객체가 딱 1개만 생성되고, 공유하도록 설계 -> 싱글톤 패턴

 

 

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는디자인 패턴
  • 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 함
//SingletonService.java
//싱글톤 예제 코드

package hello.core.singleton;

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("싱글톤 객체 로직 호출");
    }
}

1. static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.
2.
이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.
3.
1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.

 

  @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를 위반한다. ( 구체클래스.getInstance() )
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다. ( DI 적용 힘듦 )
  • 안티패턴으로 불리기도 한다.

 

싱글톤 컨테이너

  • 스프링 빈이 바로 싱글톤으로 관리되는 법
  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
  • 스프링 컨테이너는 싱글톤 패턴의 문제점을 전부 해결하고 싱글톤 패턴의 장점만 가져감.
    • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
    • DIP,OCP,테스트,private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있음
    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        //1. 조회: 호출할 때 마다 같은 객체를 반환
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        //2. 조회: 호출할 때 마다 같은 객체를 반환
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);
        //참조값이 같은 것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);
        //memberService1 == memberService2
        assertThat(memberService1).isSameAs(memberService2);
    }

결과

  • 요청할 때마다 새로운 객체를 생성해서 반환하는 기능도 제공 -> 뒤에 빈 스코프에서 설명
  • 99%는 싱글톤

 

 

싱글톤 방식의 주의점

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.
  • 무상태(stateless)로 설계해야 한다!
    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    • 가급적 읽기만 가능해야 한다.
    • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다.
package hello.core.singleton;

public class StatefulService {
    private int price; //상태를 유지하는 필드
    public void order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
        this.price = price; //여기가 문제!
    }
    public int getPrice() {
        return price;
    }

}
package hello.core.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", StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
    //ThreadA: A사용자 10000원 주문
        statefulService1.order("userA", 10000);
    //ThreadB: B사용자 20000원 주문
        statefulService2.order("userB", 20000);
    //ThreadA: 사용자A 주문 금액 조회
        int price = statefulService1.getPrice();
    //ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
        System.out.println("price = " + price);
        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }
    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }



}

결과

  • 사용자A의 주문 금액은 10000원이 되어야하는데, 20000원이라는 결과가 나왔다.
  • 무상태로 설계해야함
package hello.core.singleton;

public class StatefulService {
    //private int price; //상태를 유지하는 필드
    public int order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
      //  this.price = price; //여기가 문제!
        return price;
    }
    //public int getPrice() {
       // return price;
    //}

}
package hello.core.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", StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
    //ThreadA: A사용자 10000원 주문
       int userAprice =  statefulService1.order("userA", 10000);
    //ThreadB: B사용자 20000원 주문
       int userBprice= statefulService2.order("userB", 20000);
    //ThreadA: 사용자A 주문 금액 조회
       // int price = statefulService1.getPrice();
    //ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
        System.out.println("price = " + userAprice);
       // Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }
    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }



}

 

 

 

@Configuration과 싱글톤

AppConfig를 보면

  •  memberService 빈을 만드는 코드에서 memberRepository() 호출 -> new MemoryMemberRepository() 호출
  • orderService 빈을 만드는 코드에서 memberRepository() 호출 -> new MemoryMemberRepository() 호출

결과적으로 각각 다른 2개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는 것처럼 보임

 public class MemberServiceImpl implements MemberService {
      private final MemberRepository memberRepository;
//테스트 용도
public MemberRepository getMemberRepository() {
          return memberRepository;
      }
  }
public class OrderServiceImpl implements OrderService {
      private final MemberRepository memberRepository;
//테스트 용도
public MemberRepository getMemberRepository() {
          return memberRepository;
      }
}
package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

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

        //모두 같은 인스턴스를 참고하고 있다.
        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);
    }
}

결과

  • 확인해보면 memberRepository 인스턴스는 모두 같은 인스턴스가 공유되어 사용된다.
  • AppConfig에 호출 로그를 남겨서 확인해보면 모두 1번씩만 호출된다.

 

@Configuration과 바이트코드 조작의 마법

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

public class ConfigurationSingletonTest {

    @Test
    void configurationDeep() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        //AppConfig도 스프링 빈으로 등록된다.
        AppConfig bean = ac.getBean(AppConfig.class);
        System.out.println("bean = " + bean.getClass());

    }

}

결과

  • 순수한 클래스라면 다음과 같이 출력되어야 한다.

    class hello.core.AppConfig

     

  • 예상과는 다르게 클래스명에 CGLIB가 붙어있음
    • 스프링이  CGLIB라는 바이트코드 조작 라이브러리를 사용해서 Appconfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것
    • 이게 싱글톤이 보장되도록 해줌

@Configuration을 적용하지 않고, @Bean만 적용하면 어떻게 될까?

  • AppConfig가 CGLIB 기술 없이 순수한 AppConfig로 스프링 빈에 등록된 것을 확인할 수 있다.
  • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
  • 항상 @Configuration을 사용하자!

참고 ) @Autowired ( 자동 의존관계 주입 ) 를 사용하는 방법도 있음

 

새로운 할인 정책 개발

  • 새로운 할인 정책으로 확장
    • 고정 금액 -> 10% 할인
//RateDiscountPolicy.java

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

public class RateDiscountPolicy implements DiscountPolicy{
    private int discountPercent =10;
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price * discountPercent /100 ;
        }else{
            return 0;
        }

    }
}
  • 테스트 작성
    • class 선택한 후 command + shift + T : 테스트 클래스 만들어 줌

 

//RateDiscountPolicyTest.java

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;


class RateDiscountPolicyTest {

    RateDiscountPolicy discountPolicy = new RateDiscountPolicy();

    @Test
    @DisplayName("VIP는 10% 할인이 적용되어야 한다.")
    void vip_o(){
        //given
        Member member = new Member(1L, "memberVIP", Grade.VIP);
        //when
        int discount = discountPolicy.discount(member, 10000);

        //then
        Assertions.assertThat(discount).isEqualTo(1000);
    }

    @Test
    @DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다..")
    void vip_x(){
        //given
        Member member = new Member(2L, "memberVIP", Grade.BASIC);
        //when
        int discount = discountPolicy.discount(member, 10000);

        //then
        Assertions.assertThat(discount).isEqualTo(1000);
    }
}
  • 실패 테스트도 만들어 봐야함

  • 실패 테스트 결과

//Assertions.assertThat(discount).isEqualTo(1000);
assertThat(discount).isEqualTo(1000);
  • option+ enter : 상수나 enum을 사용할 때 해당 클래스명이나 enum명을 사용하지 않고 바로 변수를 사용할 수 있게 해줌
    • 사용 이유 : 나중에 가면 갈수록 코드가 길어지면, 클래스 이름도 거추장스럽다

새로운 할인 정책 적용과 문제점

//OrderServiceImpl.java

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository=new MemoryMemberRepository();
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member,itemPrice);

        return new Order(memberId,itemName,itemPrice,discountPrice);
    }
}
  • 할인 정책을 변경하려면 클라이언트인 OrderServiceImpl에서 FixDiscountPolicy() -> RateDiscountPolicy()로 코드를 고쳐야한다.
  • 역활과 구현 충실하게 분리 O
  • 다형성 활용, 인터페이스와 구현 객체 분리 O
  • OCP,DIP 객체 지향 설계 원칙 준수 X
    • 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있음. - > DIP 위반
      • 추상 : DiscountPolicy
      • 구체 : FixDiscountPolicy, RateDiscountPolicy
    • 기능을 확장해서 변경하면 , 클라이언트 코드에 영향을 준다. -> OCP 위반

//OrderServiceImpl.java

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository=new MemoryMemberRepository();
    private DiscountPolicy discountPolicy; //이 부분 수정
    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member,itemPrice);

        return new Order(memberId,itemName,itemPrice,discountPrice);
    }
}
  • 인터페이스에만 의존하도록 코드 변경
  • 구현체가 없음 -> null pointer exception (NPE) 발생
  • 해결방안
    • 누군가가 OrderServiceImpl에 DiscountPolicy 구현 객체를 대신 생성하고 주입해주어야 함

 

관심사의 분리

로미오와 줄리엣 공연으로 비유해보자.

로미오 역할, 줄리엣 역할 : 인터페이스

레오나르도 디카프리오: 구현체

앞선 상황은 디카프리오가 줄리엣 역할을 할 여자 주인공을 직접 초빙하는 것과 같다.

디카프리오는 공연 + 초빙 이라는 다양한 책임을 갖게 됨

 -> 별도의 공연 기획자가 필요.

 

AppConfig 등장

  • 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스
//AppConfig.java

package hello.core;

import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
    }
}

AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.

AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스) 생성자를 통해서 주입(연결)해준다.

//MemberServiceImpl.java

package hello.core.member;

public class MemberServiceImpl implements MemberService {


    //private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}
  • MemberServiceImpl이 MemoryMemberRepository에 의존하지 않게 됨
  • MemberRepository라는 인터페이스에만 의존
  • MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정
  • 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중할 수 있게 됨

  • 클라이언트인 MemberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 의존관계 주입, 의존성 주입 이라고 한다.
//OrderServiceImpl.java

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;

public class OrderServiceImpl implements OrderService{

    //private final MemberRepository memberRepository=new MemoryMemberRepository();
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member,itemPrice);

        return new Order(memberId,itemName,itemPrice,discountPrice);
    }
}
  • OrderServiceImpl은 FixDiscountPolicy를 의존하지 않게됨
  • DiscountPolicy 인터페이스에만 의존
  • OrderServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정
  • 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중할 수 있게 됨

AppConfig 실행

//MemberApp.java

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;

public class MemberApp {

    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig(); //추가
        MemberService memberService = appConfig.memberService(); //추가
        //MemberService memberService = new MemberServiceImpl();

        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("new member = " +member.getName());
        System.out.println("find member = "+findMember.getName());

    }
}
//OrderApp.java

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class OrderApp {

    public static void main(String[] args) {

        //MemberService memberService = new MemberServiceImpl();
        //OrderService orderService = new OrderServiceImpl();
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();

        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId,"itemA",10000);

        System.out.println("order= " +order);

    }
}

테스트 코드도 수정

//MemberServiceTest.java

package hello.core.member;


import hello.core.AppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {
    //MemberService memberService = new MemberServiceImpl();
    MemberService memberService;
    @BeforeEach
    public void beforeEach(){
        AppConfig appConfig =new AppConfig();
        memberService = appConfig.memberService();
    }
    @Test
    void join(){
        //given
        Member member = new Member(1L,"memberA",Grade.VIP);

        //when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        //then
        Assertions.assertThat(member).isEqualTo(findMember); //join한거랑 찾은 거랑 같은가

    }
}
//OrderServiceTest.java

package hello.core.order;

import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class OrderServiceTest {

   // MemberService memberService = new MemberServiceImpl();
    MemberService memberService;
   // OrderService orderService = new OrderServiceImpl(discountPolicy);
   OrderService orderService;
   @BeforeEach
public void beforeEach(){
    AppConfig appConfig =new AppConfig();
    memberService = appConfig.memberService();
    orderService =appConfig.orderService();
}
    @Test
    void createOrder(){
        Long memberId =1L;
        Member member = new Member(memberId,"memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId,"itemA",10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);

    }
}
  • @BeforeEach는 각 테스트를 실행하기 전에 호출된다.

 

AppConfig 리팩토링

현재 AppConfig를 보면 중복이 있고 역할에 따른 구현이 잘 안보인다.

//AppConfig.java

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

    private static MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(),discountPolicy());
    }
    
    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }
}
  • new MemoryMemberRepository() 중복 제거 ( 단축키 : command + option + m )
  • 역할과 구현 클래스가 한눈에 들어옴

 

 

새로운 구조와 할인 정책 적용

정액 할인 정책 -> 정률 할인 정책

FixDiscountPolicy -> RateDiscountPolicy

 

  • AppConfig의 등장으로 애플리케이션이 사용 영역과 , 객체를 생성하고 구성하는 영역으로 분리됨

  • FixDiscountPolicy -> RateDiscountPolicy 로 변경해도 구성 영역만 영향을 받고, 사용 영역은 영향을 받지 않음
//AppConfig.java

package hello.core;

,,,
import hello.core.discount.RateDiscountPolicy;
,,,

public class AppConfig {

,,,

    public DiscountPolicy discountPolicy(){
        //return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}

 

 좋은 객체 지향 설계의 5가지 원칙의 적용

 

IoC, DI 그리고 컨테이너

제어의 역전 IoC

  • 프로그램의 제어 흐름을 프로그램에서 관리하는게 아니라 외부에서 관리하는 것
  • AppConfig가 프로그램의 제어 흐름을 가져감. 구현 객체는 자신의 로직을 실행하는 역할만 담당.

프레임워크 vs 라이브러리

  •  내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크 (JUnit)
  •  반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리

의존관계 주입 DI (Dependency Injection)

  • 인터페이스만 의존, 실제 어떤 구현 객체가 사용될지는 모른다.
  • 정적인 클래스 의존관계
    • 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다.
    • 애플리케이션을 실행하지 않아도 분석 가능
    • 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 주입 될 지 알 수 없다.
  • 동적인 객체 인스턴스 의존 관계
    • 애플리케이션 실행 시점에 실제 생성된 객 체 인스턴스의 참조가 연결된 의존 관계
  • 실행시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존 관계가 연결되는 것을 '의존관계 주입' 이라 한다.
  • 의존 관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

IoC 컨테이너 , DI 컨테이너

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것
  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다.
  • 또는 어셈블러, 오브젝트 팩토리 등으로 불리기도 한다. 

 

 

스프링으로 전환하기 

//AppConfig.java

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(),discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy(){
        //return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}
  • @Configuration : 설정 정보를 구성
  • 각 메소드에 @Bean : 스프링 빈에 등록

 

//MemberApp.java

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.context.ApplicationContext;

public class MemberApp {

    public static void main(String[] args) {
       // AppConfig appConfig = new AppConfig(); 
      //  MemberService memberService = appConfig.memberService();
        ApplicationContext applicationContext = new AnnotationConfigReactiveWebApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("new member = " +member.getName());
        System.out.println("find member = "+findMember.getName());

    }
}
  • ApplicationContext는 스프링 컨테이너 : @Bean 객체들 관리
  • AppConfig.class : AppConfig에 있는 환경설정 정보들을 관리

  • 상위 다섯개는 스프링 내부적으로 필요해서 등록하는 스프링빈
  • 아래 다섯개는 @Bean으로 AppConfig에 작성해둔 것들
//OrderApp.java

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.context.ApplicationContext;

public class OrderApp {

    public static void main(String[] args) {

        //AppConfig appConfig = new AppConfig();
        //MemberService memberService = appConfig.memberService();
        //OrderService orderService = appConfig.orderService();

        ApplicationContext applicationContext =new AnnotationConfigReactiveWebApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService",MemberService.class);
        OrderService orderService =applicationContext.getBean("orderService",OrderService.class);
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId,"itemA",10000);

        System.out.println("order= " +order);

    }
}
  • 스프링 빈은 @Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다.
    • @Bean(name ="")으로 변경할 수도 있음 : 특별한 경우가 아니면 사용 X
  • 스프링빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.
  • 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경

프로젝트 생성

 

1. 스프링 부트 스타터 사이트로 이동하여 프로젝트 생성

2. 압축해제 후 build.gradle 실행. settings의 gradle로 들어가서 build and run using과 run tests using을 모두 intelliJ로 변경

( gradle보다 실행속도가 더 빠름)

비즈니스 요구사항과 설계 / 회원 도메인 설계

❌현재 예제는 스프링 없는 순수한 자바로만 개발을 진행하는 것임을 기억

  • 도메인에 대한 큰 그림
  • 기획자들도 보는 그림

  • 개발자들이 '회원 도메인 협력 관계'를 구체화한 것
  • 결정되지 않은 인스턴스들이 포함되어 있음

  • 객체간의 참조 관계 나타낸 다이어그램 (회원 서비스 : MemberServiceImpl)
  • 클라이언트가 실제 사용하는 인스턴스끼리의 참조를 나타냄

회원 도메인 개발

파일 구조

  • 원래는 인터페이스랑 구현체랑 다른 패키지에 두는게 좋지만 복잡해지므로 해당 예제에서는 같은 패키지에 두는 것.
/* Grade.java*/

package hello.core.member;

public enum Grade {
    BASIC,
    VIP
}

 

enum

1. 클래스처럼 보이게 하는 상수

2. 서로 관련있는 상수들끼리 모아 상수들을 대표할 수 있는 이름으로 타입을 정의하는 것

3. Enum 클래스 형을 기반으로 한 클래스형 선언 

/* Member.java */
package hello.core.member;

public class Member {
    private Long id;
    private String name;
    private Grade grade;

    public Member(Long id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }

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

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }
}
  • 'command+n' 단축키로 생성자getter, setter 만들어줌
/* MemberRepository.java */

package hello.core.member;

public interface MemberRepository {

    void save(Member member);

    Member findById(Long memberId);
}
  • 회원 저장 관련 인터페이스 생성
/* MemoryMemberRepository */

package hello.core.member;

import java.util.HashMap;
import java.util.Map;

public class MemoryMemberRepository implements MemberRepository{

    private static Map<Long,Member> store = new HashMap<>();


    @Override
    public void save(Member member) {
        store.put(member.getId(), member);

    }

    @Override
    public Member findById(Long memberId) {
        return store.get(memberId);
    }
}
  • 구현체 생성
  • 실전에서는 동시성 문제 때문에 ConcurrentHashMap을 사용해야하나 예제이기 때문에 간단하게 HashMap 사용함
  • 'option + enter' 단축키로 해결할 수 있는 것들이 많음!
/* MemberService.java */

package hello.core.member;

public interface MemberService {

    void join(Member member);

    Member findMember(Long memberId);
}
  • 회원 서비스 관련 인터페이스 생성
/* MemberServiceImpl.java */

package hello.core.member;

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}
  • 구현체 생성
  • 관례상 구현체가 하나만 있을 때 파일명에 Impl을 붙임
  • 'command+shift+enter'로 자동완성하면 세미콜론까지 붙여줌
  • 구현 객체를 앞서 생성한 MemoryMemberRepository로 지정해줌 ( 오버라이딩 )
    • 인터페이스(추상화) 뿐만 아니라 구현체(구체화) 도 의존 -> DIP 위반

회원 도메인 실행과 테스트

파일 구조

/* MemberApp.java */

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;

public class MemberApp {

    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImpl();

        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("new member = " +member.getName());
        System.out.println("find member = "+findMember.getName());

    }
}
  • 앞서 작성한 코드들이 잘 동작하는지 테스트 하기 위한 코드
  • 'command + option + v' 변수 자동 생성 단축키
  • main 메소드로 테스트하는 것에는 한계가 있음 -> JUnit이라는 테스트 프레임워크 사용
    • 눈으로 확인하지 않아도 오류임을 알 수 있도록
/* MemberServiceTest.java */

package hello.core.member;


import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {
    MemberService memberService = new MemberServiceImpl();
    @Test
    void join(){
        //given
        Member member = new Member(1L,"memberA",Grade.VIP);

        //when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        //then
        Assertions.assertThat(member).isEqualTo(findMember); 
    }
}
  • given / when / then 구조로 작성

 

주문과 할인 도메인 설계

  • 역할과 구현을 분리하여 회원 저장소, 할인 정책도 유연하게 변경할 수 있게 됨

  • 역할들의 협력 관계를 그대로 재사용 할 수 있다

주문과 할인 도메인 개발

파일 구조

/* DiscountPolicy.java */

package hello.core.discount;

import hello.core.member.Member;

public interface DiscountPolicy {

    /* @return 할인 대상 금액*/
    int discount(Member member, int price);
}
  • 할인 정책 인터페이스 생성
/* FixDiscountPolicy */

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

public class FixDiscountPolicy implements DiscountPolicy{
    private int discountFixAccount = 1000; //1000원 할인
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return discountFixAccount;
        }else{
            return 0;
        }
    }
}
  • 구현체 생성 ( 정액 할인 정책 )
  • enum 타입은 비교시 == 사용
/* Order.java */

package hello.core.order;

public class Order {

    private Long memberId;
    private String itemName;
    private int itemPrice;
    private int discountPrice;

    public Order(Long memberId, String itemName, int itemPrice, int discountPrice) {
        this.memberId = memberId;
        this.itemName = itemName;
        this.itemPrice = itemPrice;
        this.discountPrice = discountPrice;
    }

    //계산 로직
    public int calculatePrice(){
        return itemPrice - discountPrice;
    }

    public Long getMemberId() {
        return memberId;
    }

    public void setMemberId(Long memberId) {
        this.memberId = memberId;
    }

    public String getItemName() {
        return itemName;
    }

    public void setItemName(String itemName) {
        this.itemName = itemName;
    }

    public int getItemPrice() {
        return itemPrice;
    }

    public void setItemPrice(int itemPrice) {
        this.itemPrice = itemPrice;
    }

    public int getDiscountPrice() {
        return discountPrice;
    }

    public void setDiscountPrice(int discountPrice) {
        this.discountPrice = discountPrice;
    }

    @Override
    public String toString() {
        return "Order{" +
                "memberId=" + memberId +
                ", itemName='" + itemName + '\'' +
                ", itemPrice=" + itemPrice +
                ", discountPrice=" + discountPrice +
                '}';
    }
}
  • toString : 추후에 order 객체 출력 시 보기 편하게 하기 위해서 정의해둔 것
/* OrderService.java */

package hello.core.order;

public interface OrderService {
    Order createOrder(Long memberId,String itemName, int itemPrice);
}
  • 주문 서비스 인터페이스 생성
/* OrderServiceImpl.java */

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository=new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member,itemPrice);

        return new Order(memberId,itemName,itemPrice,discountPrice);
    }
}
  • 주문 서비스 구현체 생성
  • 회원 찾기 + 할인 정책 두 가지 필요 
  • 할인에 대한 건 discountPolicy가 전담 -> 단일 책임 원칙이 잘 지켜짐

주문과 할인 도메인 실행과 테스트

파일 구조

 

/* OrderApp */

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class OrderApp {

    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImpl();
        OrderService orderService = new OrderServiceImpl();

        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId,"itemA",10000);

        System.out.println("order= " +order);

    }
}
  • main 메소드로 테스트하는 것은 좋지 않음 -> JUnit 사용
/* OrderServiceTest.java */

package hello.core.order;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

public class OrderServiceTest {

    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();

    @Test
    void createOrder(){
        Long memberId =1L;
        Member member = new Member(memberId,"memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId,"itemA",10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);

    }
}
  • 테스트가 추후에는 느려질 수 있음
    • 단위 테스트를 잘 만드는 것이 중요함
    • 단위 테스트 : 스프링이나 컨테이너 등의 도움없이 순수한 자바 코드로만 테스트 하는 것

스프링이란?

여러 기술의 집합체

필수

  • 스프링 프레임워크   <-핵심
  • 스프링 부트 - 스프링을 편리하게 사용할 수 있도록 지원, 기본으로 사용

선택

  • 스프링 데이터 - CRUD를 편리하게 사용하도록 도와주는 것 ex) 스프링 데이터 JPA
  • 스프링 세션 - 세션 기능을 편리하게
  • 스프링 시큐리티 - 보안 관련
  • 스프링 Rest Docs - API 문서화 편리하게
  • 스프링 배치 - 배치처리 특화
  • 스프링 클라우드 - 클라우드 기술에 특화 

등등...

 

스프링 단어?

  • 스프링 DI 컨테이너 기술 (좁은 의미)
  • 스프링 프레임워크
  • 스프링 부트, 스프링 프레임워크 등을 모두 포함한 스프링 생태계 

 

스프링은 왜 만들었을까?

핵심컨셉

  • 자바 언어 기반의 프레임워크
  • 자바언어 -> 객체 지향 언어 

->좋은 객체 지향 어플리케이션을 개발할 수 있게 도와주는 프레임워크

 

좋은 객체 지향 프로그래밍이란?

객체 지향 프로그래밍

  •  컴퓨터 프로그램을 명령어의 목록으로 보는 시각 -> 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악
  • 객체는 메시지를 주고받고, 데이터를 처리할 수 있다. ( 협력 )
    • 혼자 있는 객체는 없다.
    • 클라이언트: 요청, 서버: 응답
    • 수 많은 객체 클라이언트와 객체 서버는 서로 협력 관계를 가진다.
  • 유연하고 변경이 용이해짐
    • 블록을 조립하듯, 컴퓨터 부품을 갈아 끼우듯 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 것 ( 다형성 )

다형성

  • 역할(=인터페이스)구현(=구현 객체)분리
    • 클라이언트는 대상의 역할(인터페이스)만 알면 된다.
    • 클라이언트는 구현 대상의 내부 구조를 몰라도 된다.
    • 클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않는다.
    • 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않는다.
  • 오버라이딩
    • 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있다.
    • 다형성의 본질을 이해하려면 협력이라는 객체 사이의 관계에서 시작
    • 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.

스프링과 객체 지향

  • 다형성이 가장 중요
  • 스프링은 다형성을 극대화해서 이용할 수 있게 도와줌
  • 스프링에서 이야기하는 제어의 역전(IoC),의존관계 주입(DI)은 다형성을 활용해서 역할과 구현을 편리하게 다룰 수 있도록 지원
  • 스프링을 사용하면 마치 레고 블럭 조립하듯이 공연 무대의 배우를 선택하듯이 구현을 편리하게 변경할 수  있다.

 

SOLID

로버트 마틴이 좋은 객체 지향 설계의 5가지 원칙을 정리

SRP 단일 책임 원칙 (Single Responsibility Principle)

  • 한 클래스는 하나의 책임만 가져야 한다.
  • 하나의 책임이라는 것은 모호하다.
    • 클 수 있고, 작을 수 있다.
    • 문맥과 상황에 따라 다르다.
    • 책임의 범위를 적절하게 잘 조절해야한다.
  • 중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것 

OCP 개방-폐쇄 원칙 (Open/Closed Principle)

  • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
  • 다형성을 활용
  • 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현 (확장에 열려있음)
  • 기존 코드를 변경할 필요는 없음(변경에 닫혀있음)
  • 구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다.
    • 다형성을 사용했지만 OCP 원칙을 지킬 수 없는 문제가 생김
    • 해결책 : 객체를 생성하고, 연관관계를 맺어주는 별도의 조립, 설정자가 필요하다  <- 추후 코드를 짜면서 자세히 볼 예정

LSP 리스코프 치환 원칙 (Liskov Substitution Principle)

  • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야한다.
  • 단순 컴파일 성공을 넘어서는 이야기

ISP 인터페이스 분리 원칙 (Interface Segregation Principle)

  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다
  • 분리하면 인터페이스가 명확해지고, 대체 가능성이 높아진다.

DIP 의존관계 역전 원칙 (Dependency Inversion Principle)

  • 추상화에 의존, 구체화에 의존 X
  • 역할에 의존해야 한다. ex) 자동차의 역할에 집중해야지 기종(k3, 아반떼 ... )에 집중하면 안됨
  • 의존 한다 == 해당 코드를 안다(참조하고 있다)

정리

  • 객체 지향의 핵심은 다형성
  • 다형성만으로는 쉽게 부품을 갈아 끼우듯 개발 X
  • 다형성만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경된다. -> OCP, DIP 위반
  • 인터페이스를 먼저 설계하고 구현을 하자 -> 변경의 범위가 작아지고 유연해짐
  • 인터페이스를 도입하면 추상화라는 비용이 발생
    • 기능을 확장할 가능성이 없다면, 구체클래스(구현 클래스)를 직접 사용. 확장 가능성이 있다면, 인터페이스 구현

 

 

 

 

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

1. 회원 웹 기능 - 홈 화면 추가

//HomeController.java

package hello.hellospring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

    @GetMapping("/")
    public String home(){
        return "home";

    }
}
  • @GetMapping("/") : 첫 화면에서 하단의 함수 실행
  • template에서 return값 을 찾아 연결
//home.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <div>
        <h1>Hello Spring</h1> <p>회원 기능</p>
        <p>
            <a href="/members/new">회원 가입</a> <a href="/members">회원 목록</a>
        </p> </div>
</div> <!-- /container -->
</body>
</html>

 

  • 화면에 보여줄 home.html 
컨트롤러가 정적 파일보다 우선순위가 높다.

 

2. 회원 웹 기능 - 등록

//MemberControll.java

package hello.hellospring.controller;

import hello.hellospring.domain.Member;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class MemberController {

    private MemberService memberService;

    @Autowired
    public void setMemberService(MemberService memberService){
        this.memberService = memberService;
    }
    @GetMapping("/members/new")
    public String createForm(){
        return "members/createMemberForm";
    }
    @PostMapping("/members/new")
    public String create(MemberForm form){
        Member member = new Member();
        member.setName(form.getName());
        memberService.join(member);

        return "redirect:/";

    }

}
  •  @GetMapping : get 방식, url에 표시
//createMemberForm.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <form action="/members/new" method="post">
        <div class="form-group">
            <label for="name">이름</label>
            <input type="text" id="name" name="name" placeholder="이름을
입력하세요"> </div>
        <button type="submit">등록</button> </form>
</div> <!-- /container -->
</body>
</html>
  • <form action="/members/new" method="post"> : 등록 버튼을 누르면 "/members/new"에 post 방식으로 넘김
  • 넘겼을 때 @PostMapping("/members/new")로 넘어감
  • 등록 : post , 조회: get

 

3. 회원 웹 기능 -조회

//memberController.java

package hello.hellospring.controller;

import hello.hellospring.domain.Member;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

@Controller
public class MemberController {

    private MemberService memberService;

    @Autowired
    public void setMemberService(MemberService memberService){
        this.memberService = memberService;
    }
    @GetMapping("/members/new")
    public String createForm(){
        return "members/createMemberForm";
    }
    @PostMapping("/members/new")
    public String create(MemberForm form){
        Member member = new Member();
        member.setName(form.getName());
        memberService.join(member);

        return "redirect:/";
    }

    @GetMapping("/members")
    public String list(Model model){
        List<Member> members = memberService.findMembers();
        model.addAttribute("members",members);
        return "members/memberList";

    }

}
  • model.addAttribute("members",members) : model 안에 key를 members로 members(멤버 정보 list) 넣어둠
//memberList.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <div>
        <table>
            <thead>
            <tr>
                <th>#</th>
                <th>이름</th> </tr>
            </thead>
            <tbody>
            <tr th:each="member : ${members}">
                <td th:text="${member.id}"></td>
                <td th:text="${member.name}"></td>
            </tr>
            </tbody>
        </table>
    </div>
</div> <!-- /container -->
</body>
</html>
  • th:each="member : ${members}" : 루프를 돌며 객체를 꺼내 member에 담음
  • name, id 는  private 이기 때문에 getter setter 이용하여 값 출력됨
  • 메모리에 저장되어 있기 때문에 서버를 재시작하면 회원데이터가 사라짐 -> DB 저장 필요

 

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

컴포넌트 스캔과 자동 의존관계 설정

  • controller가 service를 의존한다 -> 서로 의존관계가 있다.

//MemberController.java

package hello.hellospring.controller;

import org.springframework.stereotype.Controller;

@Controller
public class MemberController {
	private final MemberService memberService = new MemberService();
}
  • @Controller라고 명시해주면 spring container에 MemberController 객체를 생성하여 관리 -> 스프링 빈이 관리된다.
  • 스프링이 관리를 하게되면 spring container에 등록을 하고 받아쓰는 방식으로 변환해야함
    • new를 사용하면 객체가 여러개 생성됨. 이보다는 하나만 생성해서 공유하는 것이 효율적

 

//MemberController.java

package hello.hellospring.controller;

import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService){
        this.memberService = memberService;
    }
}
  • @Autowired: 생성자가 호출될 때 spring container에 있는 memberService와 연결해줌.
  • 이 상태에서는 memberService를 spring이 알 수 있는 방법이 없음.
//MemberService.java

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service  /*추가*/
public class MemberService {

	. . .

    }
  • @Service : 서비스임을 인지하고 spring container에 서비스를 등록
//MemoryMemberRepository.java

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.stereotype.Repository;

import java.util.*;

@Repository /*추가*/
public class MemoryMemberRepository implements MemberRepository{

	. . .
}
  • @Repository:리포지토리임을 인지하고 spring container에 리포지토리를 등록

 

스프링 빈을 등록하는 두가지 방법

  1. 컴포넌트 스캔과 자동 의존관계 설정
    • 컴포넌트 스캔 : @Service, @Repository,@Controller 모두 자세히 보면 @Component annotation을 포함하고 있음.
      • @Component annotation이 있다면 spring이 객체를 생성해 spring container에 저장(스프링 빈으로 자동 등록)
      • 실행시키는 파일을 포함한 패키지와 동일선상 또는 하위 패키지가 아니라면 스프링빈으로 컴포넌트 스캔을 하지 않음.
  2. 자바 코드로 직접 스프링 빈 등록하기

 

참고: 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다(유일하게 하나만 등록해서 공유한다) 따라서 같은 스프링 빈이면 모두 같은 인스턴스다. 설정으로 싱글톤이 아니게 설정할 수 있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.

 

 

자바 코드로 직접 스프링 빈 등록하기

//SpringConfig.java

package hello.hellospring;


import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }


}
  • @Configuration : 스프링이 스프링빈에 등록하라는 것으로 인식
  • @Controller, @Autowired는 지우지말고 그대로 두기

 

 

DI의 3가지 방법

  1. 필드 주입
@Controller
public class MemberController {
    @Autowired  private MemberService memberService;
  
}
  • 추천하지 않는 방법
    • 중간에 바꿀 수 있는 방법이 없어서

2. setter 주입

@Controller
public class MemberController {

    private MemberService memberService;

    @Autowired
    public void setMemberService(MemberService memberService){
        this.memberService = memberService;
    }
}
  • 생성은 생성대로 한 후, setter를 통해 나중에 주입
  • setMemberService는 바뀔 필요가 없는 함수인데 public으로 선언되어 변경될 위험이 있음.

3. 생성자 주입

  • 생성자를 통한 주입(위에 예제)
  • 가장 추천하는 방법

 

정형화되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링빈으로 등록한다.
ex) MemoryMemberRepository를 변경해야할 일이 있을 때 다른 코드를 손댈 필요 없이 config의 코드만 수정해주면 됨 

주의: @Autowired 를 통한 DIhelloController , memberService 등과 같이 스프링이 관리하는 객체에서만 동작한다. 스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다
ex) MemberService memberService = new MemberService();

 

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

비즈니스 요구사항 정리

  • 컨트롤러: 웹 MVC의 컨트롤러 역할
  • 서비스: 핵심 비즈니스 로직 구현 예) 회원 중복 가입 방지
  • 리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
  • 도메인: 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨
폴더 구조

회원 도메인과 리포지토리 만들기

//Member.java
package hello.hellospring.domain;

public class Member {
    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;
    }
}
//MeberRepository.java

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;

public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String Name);
    List<Member> findAll();
}
  • null을 반환할 수 있음. 이 때 Optional로 감싸서 반환하는 방법 많이 사용
//MemoryMemberRepository.java

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;


public class MemoryMemberRepository implements MemberRepository{

    @Override
    public Member save(Member member) { //멤버저장
        member.setId(++sequence);
        store.put(member.getId(),member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    } // null을 반환할 수 있기 때문에 Optional.ofNullable()로 감싸줌

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream().filter(member -> member.getName().equals(name))
                .findAny();
    } //parameter로 넘어온 name과 같은게 있는지 확인, 찾으면 반환, 아니면 null

    @Override
    public List<Member> findAll() {
        return new ArrayList<>((store.values()));
    } //store에 있는 member들 반환
}
  • option + enter 단축키 -> 필요한 메소드 자동 생성
  • 실무에서 List 많이 사용

 

 

리포지토리 테스트 케이스 작성

  • 회원 리포지토리 클래스가 제대로 작동하는지 확인하기 위한 코드
  • main이나 컨트롤러를 통한 실행은 실행하는데 오래 걸리고 반복실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있음  -> JUnit이라는 프레임워크로 테스트를 실행해서 문제 해결
  • src > test>java>hello.hellospring >repository 에 파일 생성
//MemoryMemberRepositoryTest.java

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

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


class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();


    @AfterEach
    public void afterEach(){
        repository.clearStore();
    }


    @Test
    public void save(){
        Member member = new Member();
        member.setName("spring");

        repository.save(member);

       Member result = repository.findById(member.getId()).get(); //Optional에서 값을 꺼낼 때 get() 사용 좋은 방법은 아니지만 테스트코드에서는 사용 O
       // System.out.println("result =" +(result ==member));
       //Assertions.assertThat(member).isEqualTo(member); //실행했을 때 초록불 뜨면 정상 동작
        assertThat(member).isEqualTo(member);
    }

    @Test
    public void findByName(){
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        Member result = repository.findByName("spring1").get();
        assertThat(result).isEqualTo(member1);

    }

    @Test
    public void findAll() {

        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);
        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        List<Member> result = repository.findAll();

        assertThat(result.size()).isEqualTo(2);
    }

}

 

//MemoryMemberRepository.java

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository{

	...

    public void clearStore(){
        store.clear();
    }
}

 

  • 전체 테스트를 돌리려면 class를 실행
  • 모든 테스트는 순서와 상관없이 동작이 되도록 설계해야함 -> 테스트가 끝날 때마다 repository를 지워주는 코드를 넣어야함
    • @afterEach : 각 테스트가 종료될 때 마다 실행됨
  • 테스트를 먼저 만들고 구현 클래스를 만들어 개발하는 것 -> 테스트주도 개발 (TDD)

 

회원 서비스 개발

//MemberService.java

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();


    /*회원가입*/

        public Long join(Member member){
            //같은 이름 중복X
            validateDuplicateMember(member);
            return member.getId();
        }

        /*중복 회원 검증 함수*/
        private void validateDuplicateMember (Member member){
            /*Optional<Member> result = memberRepository.findByName(member.getName());
            result.ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");   Optional을 바로 반환하는 것 권장 X*/
            memberRepository.findByName(member.getName()).ifPresent(m -> {
                throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
        }
        /*전체 회원 조회*/
        public List<Member> findMembers() {
            return memberRepository.findAll();
        }

        public Optional<Member> findOne(Long memberId) {
            return memberRepository.findById(memberId);
        }

    }
  • command+option+v  단축키 : 반환값 받는 변수 자동 생성
  • 로직은 method로 뽑아내는 것이 좋음. control+t -> Extract Method를 통해 method로 분리

 

 

회원 서비스 테스트

//MemberServiceTest.java

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;


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

class MemberServiceTest {
   /* MemberService memberService = new MemberService();
    MemoryMemberRepository memberRepository = new MemoryMemberRepository(); MemberService에서 생성한 repository와 다른 repository가 생성됨*/

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach(){
        memberRepository = new MemoryMemberRepository();
        memberService=new MemberService(memberRepository);

    }



    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }

    @Test
    void 회원가입() {

//Given
        Member member = new Member();
        member.setName("hello");
//When
        Long saveId = memberService.join(member);
//Then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());

    }
    @Test
    public void 중복_회원_예외() throws Exception {
//Given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");
//When
        memberService.join(member1);
        /* 방법 1
        try{
            memberService.join(member2);
            fail();
        }catch(IllegalStateException e){
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

        }*/
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));//예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

    }


        @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}
//MemberService.java

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

		. . .

    }
  • command + shift + T 단축키 : test 더 간편하게 생성할 수 있는 단축키
  • test 코드는 한글로 적어도 괜찮음. 직관적으로 알아보기 쉬워 현업에서도 자주 사용
  • given-when-then 문법 : 무언가가 주어지고 (given)  실행 했을 때 (when)  결과가 이거여야한다(then)
    • 테스트코드가 길어졌을 때 확인하기 편해짐
  • 외부에서 repository를 넣어줌 : Defendency Injection(DI)
    • @BeforeEach : 각 테스트 실행 전에 호출된다. 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존관계도 새로 맺어준다.

+ Recent posts