반응형

1. 빈 스코프

: 빈이 존재할 수 있는 범위

 

-  스코프의 종류

싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
프로토타입: 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프

웹 관련 스코프
    request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
    session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
    application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

 

- 프로토타입 스코프의 특징

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

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

스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다. 그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제이다. 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할 때 마다 새로 생성해서 사용하는 것이 목적이다.

 

해결방법1 : ObjectFactory, ObjectProvider

ObjectProvider 의 getObject() 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)

과거에 ObjectFactory가 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider 가 만들어짐.

예제)

package hello.core.scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class SingletonWithPrototypeTest1 {
	@Test
	void singletonClientUsePrototype() {
		AnnotationConfigApplicationContext ac = 
				new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
		
		ClientBean clientBean1 = ac.getBean(ClientBean.class);
		int count1 = clientBean1.logic();
		Assertions.assertThat(count1).isEqualTo(1);
		
		ClientBean clientBean2 = ac.getBean(ClientBean.class);
		int count2 = clientBean2.logic();
		Assertions.assertThat(count2).isEqualTo(1);
	}
	
	@Scope("singleton")
	static class ClientBean{
		// DL ObjectProvider 적용
		@Autowired
		private ObjectProvider<PrototypeBean> prototypeBeanProvider;
		
		public int logic() {
			PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
			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");
		}		
	}
}

※ 특징 

ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존

 

해결방법 2 : JSR-330 Provider

javax.inject:javax.inject:1 라이브러리를 gradle에 추가.

예제)

package hello.core.scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Provider;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class SingletonWithPrototypeTest1 {	
	@Test
	void singletonClientUsePrototype() {
		AnnotationConfigApplicationContext ac = 
				new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
		
		ClientBean clientBean1 = ac.getBean(ClientBean.class);
		int count1 = clientBean1.logic();
		Assertions.assertThat(count1).isEqualTo(1);
		
		ClientBean clientBean2 = ac.getBean(ClientBean.class);
		int count2 = clientBean2.logic();
		Assertions.assertThat(count2).isEqualTo(1);
	}
	
	@Scope("singleton")
	static class ClientBean{
		// DL Provider적용
		@Autowired
		private Provider<PrototypeBean> prototypeBeanProvider;
		
		public int logic() {
			PrototypeBean prototypeBean = prototypeBeanProvider.get();
			prototypeBean.addCount();
			int count = prototypeBean.getCount();
			return count;
		}
	}
	
	@Scope("prototype")
	static class PrototypeBean{
		private int count = 0;
		
		public void addCount() {
			count++;
		}
		
		public int getCount() {
			return count;
		}
		
		@PostConstruct
		public void init() {
			System.out.println("PrototypeBean.init " + this);
		}
		
		@PreDestroy
		public void destroy() {
			System.out.println("PrototypeBean.destroy");
		}		
	}
}

※ 특징

get() 메서드 하나로 기능이 매우 단순하다.
별도의 라이브러리가 필요하다.
자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

반응형
반응형

1. 빈 생명 주기

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

 

※ 초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
※ 소멸전 콜백: 빈이 소멸되기 직전에 호출

 

- 스프링 빈은 간단하게 다음과 같은 라이프사이클을 가짐.
객체 생성  의존관계 주입

※ 스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료된다. 따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다.

※ 생성자 주입은 객체 생성과 동시에 의존 관계가 주입됨

 

왜 콜백함수가 필요할까?

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

 

생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다.

반면에 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는등 무거운 동작을 수행한다.
따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다. 

 

이때, 콜백 함수를 통해 초기화 됐을때의 실행 될 로직과 소멸전 실행될 로직을 구현 할 수 있다.

 

 

2. 빈 생명주기 콜백

- 인터페이스(InitializingBean, DisposableBean) 구현

예제)

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);
	}
    
	// InitializingBean 인터페이스 구현
	@Override
	public void afterPropertiesSet() throws Exception {
		connect();
		call("초기화 연결 메시지");
	}
    
	// DisposableBean 인터페이스 구현
	@Override
	public void destroy() throws Exception {
		disConnect();
	}
}

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

거의 사용하지 않음

 


- 설정 정보에 초기화 메서드, 종료 메서드 지정

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

예제)

초기화 메소드, 소멸 메소드 작성 (init 메소드, close 메소드)

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() {
		System.out.println("NetworkClient.init");
		connect();
		call("초기화 연결 메시지");
	}
    
	public void close() {
		System.out.println("NetworkClient.close");
		disConnect();
	}
}

설정 정보에 초기화 소멸 메서드 지정

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 이라는 이름의 종료 메서드를 사용하는게 일반적이라, close , shutdown 라는 이름의 메서드를 자동으로 호출한다. 따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작함.

 

 

- @PostConstruct, @PreDestroy 애노테이션 지원

예제)

package hello.core.lifecycle;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

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

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.err.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() throws Exception {
		System.out.println("NetworkClient.init");
		connect();
		call("초기화 연결 메세지");
	}

	@PreDestroy
	public void close() throws Exception {
		System.out.println("NetworkClient.close");
		disconnect();
	}
	
}

※ @PostConstruct, @PreDestroy 애노테이션 특징
최신 스프링에서 가장 권장하는 방법으로 애노테이션 하나만 붙이면 되므로 매우 편리하다.
스프링에 종속적인 기술이 아니라 JSR-250 라는 자바 표준, 스프링이 아닌 다른 컨테이너에서도 동작.
외부 라이브러리에는 적용하지 못함.

외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용.

반응형
반응형

 

1. 의존관계 주입 방법 4가지
생성자 주입

- 생성자를 통해서 의존 관계를 주입 받음.

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

- 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입

 

수정자 주입(setter 주입)

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

- 선택, 변경 가능성이 있는 의존관계에 사용
- 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법

※ @Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생.

주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false) 로 지정하면 된다.

 

필드 주입

- 코드가 간결 하지만 외부에서 변경이 불가능해서 테스트 하기 힘듦
- DI 프레임워크가 없으면 아무것도 할 수 없음.

- 사용 안하는것을 추천


일반 메서드 주입

- 일반 메서드를 통해서 주입 받음.
- 한번에 여러 필드를 주입 받을 수 있음, 일반적으로 잘 사용하지 않는다.

 

※ 주입할 스프링 빈이 없어도 동작해야 할 때 옵션 사용법

package hello.core.autowired;

import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.lang.Nullable;

import hello.core.member.Member;

public class AuotowiredTest {
	
	@Test
	void AutowiredOption() {
		ApplicationContext ac =  new AnnotationConfigApplicationContext(TestBean.class);
		
	}
	
	static class TestBean{		
/*		// 오류남
		@Autowired
		public void setNoBean0(Member noBean0) {
			System.out.println("noBean0 = "+ noBean0);
		}
 */
		
		// 호출 자체가 안됨
		@Autowired(required = false)
		public void setNoBean1(Member noBean1) {
			System.out.println("noBean1 = "+ noBean1);
		}
		
		// 호출은 되지만 null
		@Autowired
		public void setNoBean2(@Nullable Member noBean2) {
			System.out.println("noBean2 = "+ noBean2);			
		}
		
		// 호출 되고 Optional.empty로 들어옴
		@Autowired
		public void setNoBean3(Optional<Member> noBean3) {
			System.out.println("noBean3 = "+ noBean3);			
		}
	}

}

 

생성자 주입을 사용해야 함

- 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안됨.(불변)

- 수정자 주입을 하게 되면 누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것 자체가 안좋은 설계.

- 생성자 주입을 사용하면 필드에 final 키워드를 사용가능, 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아줌.

- 생성자 주입이 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법

 

※ Lombok

@RequiredArgsConstructor어노테이션을 붙이면 final이 붙은 필드에 자동으로 생성자 주입을 시켜줌.

예시)

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

 

 

2. 의존관계 주입 시 조회한 빈이 2개 이상일 경우

@Autowired 는 타입(Type)으로 조회 하는데 조회된 타입이 2개일 경우 NoUniqueBeanDefinitionException 오류가 발생.

 

해결 방법 

1) @Autowired 필드 명 매칭 

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

예시)

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

 


2) @Qualifier끼리 매칭 빈 이름 매칭

- @Qualifier 는 추가 구분자를 붙여주는 방법. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것이 아님.

예시)

조회된 첫번째 빈에 @Qualifier 적용

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

 

조회된 두번째 빈에 @Qualifier 적용

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

 

@Qualifier 사용

private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;

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

 

 

3) @Primary 사용

- @Primary 는 우선순위를 정하는 방법. @Autowired 시에 여러 빈이 매칭되면 @Primary 가 우선권을 가짐.

예시)

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy { ... }



@Component
public class FixDiscountPolicy implements DiscountPolicy { ... }

 

※ 우선순위
@Primary 는 기본값 처럼 동작하는 것이고, @Qualifier 는 매우 상세하게 동작한다. 

스프링은 자동보다는 수동이, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선 순위가 높다. 여기서도 @Qualifier 가 우선권이 높다.

 

 

3. 조회한 빈이 모두 필요할때 (List, Map)

예제)

package hello.core.autowired;

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

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

import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;

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");
	 
		Assertions.assertThat(discountService).isInstanceOf(DiscountService.class);
		Assertions.assertThat(discountPrice).isEqualTo(1000);
		
		int rateDiscountPrice =  discountService.discount(member, 20000, "rateDiscountPolicy");
		Assertions.assertThat(rateDiscountPrice).isEqualTo(2000);
	}
	
	
	static class DiscountService{
		// DiscountPolicy의 2개의 구현체 자동 주입
		private final Map<String, DiscountPolicy> policyMap;
		private final List<DiscountPolicy> policies;
		
		@Autowired
		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) {
			// discountCode로 주입된 구현체 선택
			DiscountPolicy discountPolicy = policyMap.get(discountCode);
			return discountPolicy.discount(member, price);
		}
	}
}

 

 

4. 수동빈과 자동빈의 선택

- 편리한 자동 기능을 기본으로 사용.
- 직접 등록하는 기술 지원 객체는 수동 등록
- 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민

반응형
반응형

1. 의존 관계 자동 주입

- 자동 빈 등록

이전 예제에서 스프링 빈을 등록할 때 자바 코드의 @Bean이나 XML의 등을 통해서 설정 정보에 직접 등록할 스프링 빈을 수동으로 등록했지만, 스프링에선 자동으로 스프링 빈을 등록하는 @ComponentScan이라는 기능을 제공한다.

 

자동 구성을 해주는 새로운 기획자

package hello.core;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import static org.springframework.context.annotation.ComponentScan.*;

@Configuration
@ComponentScan
public class AutoAppConfig {
 
}

@ComponentScan 어노테이션을 붙여주고, @Bean 어노테이션과 내용은 하나도 없다.

Bean으로 등록하고 싶은 클래스에 직접 @Component어노테이션을 붙여주면 자동으로 Bean에 등록된다.

MemberRepository에 의존관계를 주입하고자 하는 MemoryMemberRepository만 Bean에 등록하기 위해 @Component어노테이션을 붙인다.

DiscountPolicy도 마찬가지로 RateDiscountPolicy에만 @Component어노테이션을 붙인다.

 

예시)

@Component
public class MemoryMemberRepository implements MemberRepository {
	...
}
@Component
public class RateDiscountPolicy implements DiscountPolicy {
	...
}

 

컴포넌트 스캔 기본 대상

- @Component : 컴포넌트 스캔에서 사용

- @Controlller : 스프링 MVC 컨트롤러에서 사용

- @Service : 스프링 비즈니스 로직에서 사용

- @Repository : 스프링 데이터 접근 계층에서 사용

- @Configuration : 스프링 설정 정보에서 사용

 

 

스프링 컨테이너를 인터스턴스화 할때 해당 config파일을 인자로 넘겨주면 된다.

 

- 자동 의존 관계 주입 등록

@Autowire어노테이션을 통해 자동 의존 관계를 주입.

 

예시)

@Component
public class MemberServiceImpl implements MemberService {
	private final MemberRepository memberRepository;
	
    @Autowired
	public MemberServiceImpl(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}
}
@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;
	}
}

@Autowire의 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다. getBean(MemberRepository.class), getBean(DiscountPolicy.class)와 동일.

 

2. Bean 탐색 위치와 기본 스캔 대상

모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.

@ComponentScan( basePackages = "hello.core" )

 

basePackages : 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다. basePackages = {"hello.core", "hello.service"} 이렇게 여러 시작 위치를 지정할 수도 있다.

basePackageClasses : 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.

 

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

 

※ 권장하는 방법

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

 

3. 컴포넌트 스캔 대상 필터

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.Filter;

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

 

FilterType의 5가지 옵션

ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.

ASSIGNABLE_TYPE: 지정한 클래스의 타입과 자식 클래스 타입을 인식해서 동작한다. 

ASPECTJ: AspectJ 패턴 사용

REGEX: 정규 표현식

CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리

 

 

4. 중복 등록과 충돌

- 자동 Bean 등록 vs 자동 Bean  등록

컴포넌트 스캔에 의해 자동으로 스프링 Bean이 등록될때 이름이 같은 경우 스프링은 ConflictingBeanDefinitionException 예외를 발생시킨다. 

 

- 수동 Bean  등록 vs 자동 Bean  등록

수동 Bean 등록이 우선권을 가지며, 오버라이드 됐다는 로그를 출력 

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

 

반응형
반응형

1. 순수 DI 컨테이너

맨 처음 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다.
고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸되어 메모리 낭비가 심하다.
해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다. 싱글톤 패턴 적용

package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
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());
	}
	
	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(),discountPolicy());
	}
    
	public MemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}

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

 

2. 싱글톤 패턴

클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.

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

- 싱글톤 패턴의 문제점

싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
의존관계상 클라이언트가 구체 클래스에 의존한다. (DIP 위반)
클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
테스트하기 어렵다.
내부 속성을 변경하거나 초기화 하기 어렵다.
private 생성자로 자식 클래스를 만들기 어렵다.
결론적으로 유연성이 떨어진다.
안티패턴으로 불리기도 한다.

 

 

3. 싱글톤 컨테이너
스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리
싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 함
스프링 컨테이너는 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있음
 : 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
 : DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다.

 

싱글톤 패턴 적용 시 주의점

여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안되고 무상태(stateless)로 설계해야 한다.

 : 특정 클라이언트에 의존적인 필드가 있으면 안된다.
 : 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
 : 가급적 읽기만 가능해야 한다.
 : 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

※ 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다

 

 

4. @Configuration어노테이션

스프링은 싱글톤을 보장하기 위해 클래스의 바이트코드를 조작하는 라이브러리를 사용한다. 

클래스 명에 xxxCGLIB가 붙으면서 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용하여 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록함.

@Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.

 

 

 

 

 

반응형
반응형

1. 스프링으로 전환하기

- AppConfig 파일 

package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
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;

// 어노테이션 기반의 스프링컨테이너
// AppConfig에 설정을 구성한다는 뜻의 @Configuration 을 붙여준다.
@Configuration
public class AppConfig {
	// 각 메서드에 @Bean 을 붙여준다. 이렇게 하면 스프링 컨테이너에 스프링 빈으로 등록한다.
	@Bean
	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}

	@Bean
	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(),discountPolicy());
	}
    
	@Bean
	public MemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}
    
	@Bean
	public DiscountPolicy discountPolicy() {
		return new RateDiscountPolicy();
	}
}

 

- 회원 서비스 클라이언트 수정

package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MemberApp {
	public static void main(String[] args) {
		// AppConfig appConfig = new AppConfig();
		// MemberService memberService = appConfig.memberService();
		ApplicationContext applicationContext = new AnnotationConfigApplicationContext(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());
	}
}

 

- 주문 서비스 클라이언트 수정

package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class OrderApp {
	public static void main(String[] args) {
		// AppConfig appConfig = new AppConfig();
		// MemberService memberService = appConfig.memberService();
		// OrderService orderService = appConfig.orderService();
		ApplicationContext applicationContext = new AnnotationConfigApplicationContext(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);
	}
}

ApplicationContext 를 스프링 컨테이너라 한다.
- 기존에는 개발자가 AppConfig 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.
- 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성) 정보로 사용한다. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
- 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. ( memberService , orderService )
- 이전에는 개발자가 필요한 객체를 AppConfig 를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다. 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.
- 기존에는 개발자가 직접 자바코드로 모든 것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다.

 

 

2. 스프링 컨테이너

ApplicationContext = 인터페이스

 

스프링 컨테이너는 XML 기반으로 만들 수도 있고 어노테이션 기반으로 만들수도 있음

위의 예제는 어노테이션 기반으로 만듦

// AnnotationConfigApplicationContext는 ApplicationContext의 구현체 (어노테이션 기반)

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class) 

 

// GenericXmlApplicationContext ApplicationContext의 구현체 (xml기반)

ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");

※ BeanFactory
스프링 컨테이너의 최상위 인터페이스, 스프링 빈을 관리하고 조회하는 역할을 담당, getBean() 제공

※ ApplicationContext

BeanFactory 기능을 모두 상속받아서 제공, 애플리케이션을 개발할 때는 빈을 관리하고 조회하는 기능은 물론이고, 수 많은 부가기능이 필요하다.(메시지 소스를 활용한 국제화 기능, 환경변수, 애플리케이션 이벤트, 편리한 리소스 조회)

 

 

3. 스프링 Bean 설정 메타 정보 (BeanDefinition)

- 다양한 설정 형식을 지원 가능하게 해주는 BeanDefinition 추상화

역할과 구현을 개념적으로 나누어 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 BeanDefinition만 알면 된다.
@Bean , <bean> 당 각각 하나씩 메타 정보가 생성되고, 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성.

 

반응형
반응형

1. 할인 정책의 변경으로 인한 소스 변경

public class OrderServiceImpl implements OrderService {
	// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
	private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
	...
}

 

문제점
- DIP 위반: 

주문 서비스 클라이언트 OrderServiceImpl는 DiscountPolicy 인터페이스 뿐만 아니라 구현 클래스에도 의존함.
      인터페이스 : DiscountPolicy
      구현 클래스 : FixDiscountPolicy , RateDiscountPolicy
- OCP 위반 :

기능을 확장해서 변경하면, 클라이언트 코드에 영향을 줌.

해결 방안 : 

누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 함.


- 관심사의 분리
애플리케이션을 하나의 공연이라 생각해보자. 각각의 인터페이스를 배역(배우 역할)이라 생각하자.

실제 배역 맞는 배우를 선택하는 것은 마치 로미오 역할(인터페이스)을 하는 레오나르도 디카프리오(구현체, 배우)가

줄리엣 역할(인터페이스)을 하는 여자 주인공(구현체, 배우)을 직접 초빙하는 것과 같다.

디카프리오는 공연도 해야하고 동시에 여자 주인공도 공연에 직접 초빙해야 하는 다양한 책임을 가지고 있다.
배우는 본인의 역할인 배역을 수행하는 것에만 집중해야 한다.
디카프리오는 어떤 여자 주인공이 선택되더라도 똑같이 공연을 할 수 있어야 한다.
공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 나올시점이다.
공연 기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 분리하자.

 

 

2. 기획자

애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스 생성

package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
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());
	}
	
	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(),discountPolicy());
	}
    
	public MemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}

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

 

 

문제가 있었던 회원 서비스 구현체 수정

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;
	}
    
	public void join(Member member) {
		memberRepository.save(member);
	}
    
	public Member findMember(Long memberId) {
		return memberRepository.findById(memberId);
	}
}

 

문제가 있었던 회원 서비스 클라이언트

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

public class MemberApp {
	public static void main(String[] args) {
		// MemberService memberService = new MemberServiceImpl();
		// 기획자 인스턴스 생성
		AppConfig appConfig = new AppConfig();
		// 실행 시 기획자의 인스턴스로 동적 회원 서비스 생성
		MemberService memberService = appConfig.memberService();
		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());
	}
}

 

 

문제가 있었던 주문 서비스 구현체 수정

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 DiscountPolicy discountPolicy = new FixDiscountPolicy();
	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);
	}
}

 

문제가 있었던 주문 서비스 클라이언트 

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

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

정리

AppConfig를 통해서 관심사를 확실하게 분리했다.
AppConfig는 공연 기획자다.
AppConfig는 구체 클래스를 선택하여 애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임진다.
OrderServiceImpl, MemberServiceImpl 은 기능을 실행하는 책임만 지면 된다.

 

3. IoC, DI, 그리고 컨테이너
제어의 역전 IoC(Inversion of Control)
- 프로그램의 제어 흐름은 AppConfig가 가져간다. 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. OrderServiceImpl 은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모른다.

- 프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다. 심지어 OrderServiceImpl도 AppConfig가 생성한다. 그리고 AppConfig는 OrderServiceImpl 이 아닌 OrderService 인터페이스의 다른 구현 객체를 생성하고 실행할 수 도 있다. 그런 사실도 모른체 OrderServiceImpl 은 묵묵히 자신의 로직을 실행할 뿐이다. 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.

 

의존관계 주입 DI(Dependency Injection)

- OrderServiceImpl 은 DiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.

- 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.
- 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다.
- 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.

 

 

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

 

 

반응형
반응형

Eclipse(STS)로 세팅 하는 방법

 

1. 사전 준비

- JAVA11 설치 (https://www.oracle.com/kr/java/technologies/javase/jdk11-archive-downloads.html)

- STS설치 (https://spring.io/tools)

자바 11 설치

 

STS 설치

 

2. 스프링 부트 스타터 사이트에서 스프링 프로젝트 생성 (https://start.spring.io)

 

3. STS 설정

- 2.에서 GENERATE한 스프링 프로젝트 원하는 경로에 core.zip 압축 해제

- STS 다운로드 후 압축 해제 → contents.zip 압축 해제 → sts-4.17.2.RELEASE 폴더의 SpringToolSuite4.exe 실행 → 위의 core.zip 압축해제 한 곳으로 Workspace 지정

 

 

4. 순수 자바로 가상의 요구사항 개발

 

4-1) 회원 도메인 설계

회원 저장소에 대한 요구 사항이 명확하지 않아 인터페이스와 구현체를 분리

회원 서비스에 대한 역할도 인터페이스와 구현체를 분리

클라이언트에서 MemberServiceImpl로 실행 로직 구현

 

※ 생각해보기

회원 서비스 구현체

package hello.core.member;

public class MemberServiceImpl implements MemberService {
	// 생각해볼 포인트
	private final MemberRepository memberRepository = new MemoryMemberRepository();
	
	public void join(Member member) {
		memberRepository.save(member);
	}
	public Member findMember(Long memberId) {
		return memberRepository.findById(memberId);
	}
}

 

클라이언트

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

문제점
다른 저장소로 변경할 때 OCP 원칙을 잘 준수하지 못했음.
DIP를 잘 지키고 있지 못함.
의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있음

 

 

4-2) 주문 도메인 설계

회원 저장소에 대한 요구 사항이 명확하지 않아 인터페이스와 구현체를 분리

할인 정책에 대한 요구 사항이 명확하지 않아 인터페이스와 구현체를 분리

회원 서비스에 대한 역할도 인터페이스와 구현체를 분리

클라이언트에서 MemberServiceImpl 와 OrderServiceImpl로 실행 로직 구현

 

※ 생각해보기

주문 서비스 구현체

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

 

클라이언트

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

회원 도메인에서와 마찬가지로 같은 문제점 발생

반응형
반응형

1. 객체지향의 특징 4가지
추상화 : 객체들의 공통적인 특징(기능, 속성)을 도출하는것
캡슐화 : 실제 구현되는 부분을 외부에 드러나지않도록 은닉
상속성 : 하나의 클래스가 가진 특징(함수,데이터)을 다른 클래스가 그대로 물려받는것
다형성 : 다른 방법으로 동작하는 함수를 같은 이름으로 호출하는것, 객체지향의 꽃, 오버로딩 / 오버라이딩

- 다형성
역할(인터페이스)과 구현(인터페이스를 구현한 객체)으로 세상을 구분

예시1)
자동차 역할을 K3, 아반떼, 테슬라로 구현하더라도 운전자는 운전자 역할이 가능하다.
운전자를 클라이언트라고 했을 때, 자동차 역할을 그대로 사용할 수 있다.
자동차의 역할과 구현을 구분한 이유는 운전자(클라이언트)에 영향을 주지 않고 새로운 자동차를 무한히 출시 할 수 있음.


예시2)
로미오 역할을 구현한 장동건, 원빈 / 줄리엣 역할을 구현한 김태희, 송혜교 
배역만 만들어두고, 배우는 언제든지 유연하게 변경할 수 있다.
로미오 역할, 줄리엣 역할을 누가 하든 서로에게 영향을 끼치지 않음.

※ 역할과 구현을 구분하면 세상이 단순해지고, 유연해지며 변경도 편리해짐.
: 클라이언트는 대상의 역할(인터페이스)만 알면됨.
: 클라이언트는 구현 대상의 내부구조를 몰라도됨.
: 클라이언트는 구현 대상의 내부구조가 변경되어도 영향을 받지 않음.
: 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않음.


2. Java의 다형성
역할 = 인터페이스, 구현 = 인터페이스를 구현한 클래스, 구현 객체
객체를 설계 할 때 역할과 구현을 명확히 분리
객체 설계시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체 만들기
(상속도 다형성이 가능하지만 인터페이스로 만드는것이 나음)

 

예시)
다형성으로 인터페이스를 구현한 객체를 실행 시점에서 유연하게 변경이 가능하다.

public class MemberService{
	// 인터페이스를 참조 변수로 선언
	// 인터페이스를 오버라딩하여 구현한 구현 객체 중 원하는 구현 객체로 생성자 함수를 클라이언트가 호출
	private MemberRepository memberRepository = new MemoryMemberRepository(); // 기존 코드
	private MemberRepository memberRepository = new JdbcMemberRepository(); // 변경 코드
}

 

※ 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있음.

 

- 다형성의 본질을 이해하려면 ?

객체는 객체끼리 협력 관계이다.
클라이언트 : 요청, 서버 : 응답
혼자있는 객체는 없음. 수 많은 객체 클라이언트와 객체 서버는 서로 협력관계를 가짐
클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경 할 수 있다.



3. 좋은 객체 지향 설계의 5가지 원칙(SOLID)
- SRP 단일 책임 원칙 : 한 클래스는 하나의 책임만 가져야한다.
변경이 있을때 파급효과가 적으면 단일 책임 원칙을 잘 따른것 (객체의 생성과 사용을 분리)
※ 책임의 범위를 잘 조절해야함


- OCP 개방 폐쇄 원칙 : 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야한다. (다형성)
역할과 구현을 분리, 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현하는것.

※ 생각해보기

public class MemberService{
	private MemberRepository memberRepository = new MemoryMemberRepository(); // 기존 코드
	private MemberRepository memberRepository = new JdbcMemberRepository(); // 변경 코드
}

클라이언트가 구현 클래스를 직접 선택하고있으니 OCP를 지키지 못함.
구현 객체를 변경하려면 클라이언트 코드를 변경해야함.
다형성으로 서비스를 구현했지만 OCP를 지킬 수 없음.
※ 해결방법 : 객체를 생성하고 연관관계를 맺어주는 별도의 조립, 설정자가 필요함(스프링 컨테이너가 해줌 DI, IoC 컨테이너)


- LSP 리스코프 치환 원칙 : 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야한다.
다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 함. 컴파일은 잘 되더라도 규약을 위배하면 LSP 위반.
자동차 인터페이스의 엑셀은 앞으로가라는 기능, 뒤로가게 구현하게되면 LSP 위반, 느리더라도 앞으로가야함.


- ISP 인터페이스 분리 원칙 : 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.
자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스로 분리
사용자 클라이언트 -> 운전자 클라이언트, 정비사 클라이언트로 분리
정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않음
인터페이스가 명확해지고, 대체 가능성이 높아짐


- DIP 의존 관계 역전 원칙 : 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.
스프링의 의존성 주입은 이 원칙을 따르는 방법중에 하나.
클라이언트 코드가 구현 클래스를 바라보지 말고 인터페이스를 바라보라는 뜻.
역할에 의존해야지 구현에 의존하지 말라는것.
의존 = 내가 그 코드를 안다.

MemberService 클라이언트는 구현 객체를 알고있음(MemoryMemberRepository)

public class MemberService{
	private MemberRepository memberRepository = new MemoryMemberRepository();
}


클라이언트가 구현객체를 알고있는 것도 DIP 위반



4. 스프링과 객체 지향
스프링은 다형성을 극대화하여 이용할 수 있게 도와줌.
IoC, DI는 다형성을 활용해서 역할과 구현을 편리하게 다룰수 있도록 지원.
스프링을 사용하면 레고 블럭을 조립하듯, 공연무대의 배우를 선택하듯 구현을 편리하게 변경 할 수 있음.

 

- 스프링은 다음 기술로 다형성 + OCP, DIP를 가능하게 지원
DI(Dependency Injection) : 의존관계, 의존성 주입
DI 컨테이너 제공 : 자바 객체들을 컨테이너 안에 넣어두고 의존관계를 서로 연결해주고 주입해주는 기능을 제공
-> 클라이언트 코드의 변경없이 기능 확장 
-> 쉽게 부품 교체 하듯 개발 가능

※ 공연을 설계하듯이 배역만 만들어두고, 배우는 언제든지 유연하게 변경할 수 있도록 만드는것이 좋은 객체 지향 설계이다.

반응형
반응형

1. 왜 GIT을 써야하는가

2. GIT의 구조 및 명령어

3. GIT 사용의 예시

4. GIT 실습

 

4. GIT 실습

4.1 Main - Develop - Feature 브랜치 전략

 Main branch

- 배포 브랜치로 서비스 1.0.0이 출시되기 전까지 건들일 없는 브랜치

 -직접적인 PUSH 절대 불가

 Develop branch

- Feature Branch의 병합 브랜치로 기능이 개발된 브랜치를 병합하여 배포하기 위한 브랜치

 - 팀원들이 만든 기능은 이브랜치로만 머지

 Feature Branch

- 기능 개발 브랜치로 개발자 로컬 저장소에서 관리, 새로운 기능을 구현하는 브랜치

- 브랜치명은 Feat/기능 이름 으로 작업 이후 원격저장소로 Push하여 PR함

- Develop으로 머지

- PR된 Remote Feature 브랜치는 삭제

 

 

4.2 기본 세팅 (PL)

① github에서 새 래포지토리 생성

② 로컬에서 develop branch 생성

 - git branch develop

 - git push --set-upstream origin develop

③ github에서 기본 베이스 변경

 - github > settings > branches > main으로 되어있는것을 develop으로 업데이트

④ Access > Collaborators에서 팀원들 추가

 

 

4.3 PR까지 작업 (팀원)

① github에서 이슈 오픈

 -  github > Issue > New Issue > 내용 작성(Assignees: 작업자, Label : 이슈 분리를 위한 태그) > Submit new Issue

② 로컬에서 원격 develop과 로컬 develop 일치 여부 확인

 - git fetch

 - origin/develop(원격 레포지토리)과 develop(로컬)이 일치 하지 않을 경우

 - git pull origin develop

 - 원격의 최신과 로컬의 최신이 맞게됨

③ 작업할 브랜치 생성

 - git branch Feat/xxxx

④ 작업할 브랜치로 이동

 - git switch Feat/xxxx

⑤ 개발

⑥ 커밋

 - git add .

 - git commit -m "xxxx 작업 완료"

⑦ github에 push

 - git push --set-upstream origin Feat/xxxx

github에서 PR 날리기

 -  github > Compare & Pull request 버튼 클릭 > compare : Feat/xxxx에서 base : develop 설정 >

내용작성(closes # 입력 시 만들어둔 이슈가 보임, reviewer : 리뷰 해줄사람,  Label : 이슈 분리를 위한 태그) >

Create pull reqeust

 

4.4 피드백 하기 (PL)

① github에서 피드백

 -  github > Files changed > 코드 라인 별로 피드백 가능 > Finished your review 버튼 > Approve 가능

 

4.5 branch 합치기(팀원)

① github에서 merge(PR을 날린 본인이 confirm함)

 -  github > Merge pull request > confirm merge

② PR 성공 확인 및 원격 레포지토리 branch 삭제

 - Pull reqeust successfully merged and closed 확인

 - Delete branch 버튼 클릭(원격 레포지토리에서 내가 생성한 브랜치 삭제)

③ 로컬 브랜치 삭제

 - git switch develop

 - git branch -D Feat/xxxx

※ pull은 항상 develop에서만 함

 

 

※ 실무 실습

https://www.youtube.com/watch?v=qJOfzcMG_hs&list=LL&index=1

 

※ 간략 실습

생활코딩 : 지옥에서 온 Git

반응형

+ Recent posts