출처 : 인프런 > 스프링 핵심 원리 - 기본편 강의를 듣고 작성한 글입니다.
강의 링크 : 스프링 핵심 원리 - 기본편


src\main\java\hello\core\discount\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; // 10% 할인
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP){
return price * discountPercent / 100;
} else{
return 0;
}
}
}

만들고자 하는 클래스에 Ctrl+Shift+T 단축키를 누르면 테스트를 쉽게 만들 수 있다.
src\test\java\hello\core\discount\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;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
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
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
// assertThat(discount).isEqualTo(1000);
assertThat(discount).isEqualTo(0);
}
}
성공 테스트(vip_o)를 만들면 실패 테스트(vip_x)도 꼭 함께 만들어야 한다.

// assertThat(discount).isEqualTo(1000);로 실행해버리면 discount의 기대했던 값은 1000원인데 실제값은 0원이다. 회원은 VIP가 아니고 BASIC이기 때문이다. 따라서 assertThat(discount).isEqualTo(0);으로 고쳐야 한다.

그럼 잘 동작한다.








discountPolicy에 값이 없기 때문에 NullPointerException 에러가 발생한다.

구현체가 절대로 다른 구현체는 선택하는 일은 없어야 하고 어떤 구현체를 선택할지는 별도의 클라이언트가 정하는 것이다. 예를 들어 역할극의 배우가 절대로 다른 배우를 선택하는 책임을 가져서는 안되고 배우는 자신의 역할(인터페이스)에만 집중을 하고 그에 맞는 배우를 선택하는 것은 공연 기획자가 해야할 일이다.
AppConfig란? 애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가진 별도의 클래스이다.
src\main\java\hello\core\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());
}
}

src\main\java\hello\core\member\MemberServiceImpl.java
package hello.core.member;
public class MemberServiceImpl implements MemberService {
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);
}
}



AppConfig 객체가 대신 리포지토리를 생성해주고 그 참조값을 생성자로 전달해주고 있다. -> DI(Dependency Injection) = 의존 관계 주입
src\main\java\hello\core\order\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;
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);
// 그 다음 할인 정책에 member 넘기기
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}

src\main\java\hello\core\MemberApp.javapackage 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) {
// 기존의 new MemberServiceImpl를 통한 구현 -> AppConfig를 이용한 구현
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
// MemberService memberService = new MemberServiceImpl(); ->
Member member = new Member(1L, "memberA", Grade.VIP); // id는 long type이라 1L 그냥 1이라 쓰면 error
memberService.join(member);
// 위에서 join한 Member와 findMember가 같은 지 확인
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find Member = " + findMember.getName());
}
}
src\main\java\hello\core\OrderApp.javapackage 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) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService(); // <- MemberService memberService = new MemberServiceImpl();
OrderService orderService = appConfig.orderService(); // <- 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);
System.out.println("order.calculatePrice = " + order.calculatePrice());
}
}
src\test\java\hello\core\member\MemberServiceTest.javapackage hello.core.member;
import hello.core.AppConfig;
import org.assertj.core.api.Assertions;
import org.hamcrest.core.IsEqual;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MemberServiceTest {
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);
}
}
src\test\java\hello\core\order\OrderServiceTest.javapackage 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;
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);
}
}

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

src\main\java\hello\core\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());
}
}
중복을 제거하고, 역할에 따른 구현이 보이도록 리팩터링 하자
src\main\java\hello\core\AppConfig.java
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());
}
private static MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
private static DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}




src\main\java\hello\core\AppConfig.java
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
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;
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(memberRepository()); // 생성자 주입
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
private static MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
private static DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}









지금까지 순수한 자바 코드만으로 DI를 적용했다. 이제 스프링을 사용해보자.
지금은 코드만 작성하고 설명은 마지막에 하겠다.
src\main\java\hello\core\AppConfig.java
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
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;
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository()); // 생성자 주입
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public static MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public static DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}

ㄴ
src\main\java\hello\core\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.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MemberApp {
public static void main(String[] args) {
// AppConfig 실행
// 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); // id는 long type이라 1L 그냥 1이라 쓰면 error
memberService.join(member);
// 위에서 join한 Member와 findMember가 같은 지 확인
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find Member = " + findMember.getName());
}
}
applicationContext.getBean("memberService", MemberService.class);
기본적으로 method 이름으로 등록이 되기 때문에 첫번째 인자에 method name인 memberService를 적고 두번째 인자에는 반환 type인 MemberService.class를 적어준다.

appConfig 위 5개는 내부적으로 필요해서 등록한 스프링 빈이고 appConfig, memberService, memberRepository, orderService, discountPolicy 5개는 @Bean으로 만든 스프링 빈이다.
src\main\java\hello\core\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.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(); // <- MemberService memberService = new MemberServiceImpl();
// OrderService orderService = appConfig.orderService(); // <- OrderService orderService = new OrderServiceImpl();
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);
System.out.println("order.calculatePrice = " + order.calculatePrice());
}
}

두 코드를 실행하면 스프링 관련 로그가 몇줄 실행되면서 기존과 동일한 결과가 출력된다.

스프링 컨테이너를 사용하면 어떤 장점이 있을지 다음 강의에서 알아보자!