수정자 주입의 경우 누락된 데이터가 무엇인지 코드를 보며 일일이 찾아보며 수정해주어야한다.
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 둘다 스프링 빈으로 선언할 경우 문제가 발생한다.
@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")와 같이 오타가 발생해도 동작한다.
스프링 빈을 @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 에 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 는 여러가지 이유로 간혹 사용할 때가 있지만 많지는 않다.> 특히 최근 스프링 부트는 컴포넌트 스캔을 기본으로 제공하는데, 개인적으로는 옵션을 변경하면서 사용하기 보다는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장하고, 선호하는 편이다.
중복 등록과 충돌
두 가지 상황 존재
자동빈등록vs자동빈등록
수동빈등록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,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 인스턴스는 모두 같은 인스턴스가 공유되어 사용된다.
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() 메서드를 사용해서 찾을 수 있다.
/* 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);
}
}
//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와 연결해줌.
//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에 리포지토리를 등록
스프링 빈을 등록하는 두가지 방법
컴포넌트 스캔과 자동 의존관계 설정
컴포넌트 스캔 : @Service, @Repository,@Controller 모두 자세히 보면 @Component annotation을 포함하고 있음.
@Component annotation이 있다면 spring이 객체를 생성해 spring container에 저장(스프링 빈으로 자동 등록)
실행시키는 파일을 포함한 패키지와 동일선상 또는 하위 패키지가 아니라면 스프링빈으로 컴포넌트 스캔을 하지 않음.
자바 코드로 직접 스프링 빈 등록하기
참고: 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다(유일하게 하나만 등록해서 공유한다) 따라서 같은 스프링 빈이면 모두 같은 인스턴스다. 설정으로 싱글톤이 아니게 설정할 수 있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.
자바 코드로 직접 스프링 빈 등록하기
//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가지 방법
필드 주입
@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 를 통한 DI는 helloController , memberService 등과 같이 스프링이 관리하는 객체에서만 동작한다. 스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다. ex) MemberService memberService = new MemberService();
//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:각 테스트 실행 전에 호출된다.테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고,의존관계도 새로 맺어준다.