본문 바로가기
김영한 스프링/김영한 스프링 기본편

자바로 만들기

by hoshi03 2023. 8. 31.

단축키 - 설정 keymap에서 볼수 있음 

alt + insert로 generate 해서 생성자,get,set 등 쉽게 만들 수 있음

ctrl + alt + v - 타입?을 알아서 만들어줌

psvm - public static void main메인 함수 만들때 psvm 치고 엔터누르면 바로 생성

ctrl + shift + t <- 원래 메스드 이름 뒤에 test를 붙인 상태로 테스트를 생성해줌

 

어노테이션

@Test - 테스트 생성 @DisplayName - 나오는 테스트 이름 변경 @BeforeEach - 테스트 실행전에 실행되는 코드

 

자바 팁

단위테스트 할때 사용하는 Assertions는 static을 붙여서 사용하는게 편하다

Assertions.assertThat().isEqualTo 형태로 쓰던걸 alt + enter로 아래처럼 변경해주면 편하게 쓸 수 있음

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

 

회원 도메인 

 

grade - 사용자 등급을 정의해둔 enum

Member - 사용자 클래스 id, name, grade를 가짐

MemberRepository - save와 findById로 멤버를 검색할 함수를 정의해둔 인터페이스

MemoryMemberRepository - MemberReository를 구현한 클래스, HashMap으로 save, findById

MemberService - join과 findMember 함수를 정의해둔 인터페이스

MemberServiceImpl - MemberService 구현체, MemoryMemberRepository 저장소를 가져와서 save와 findById 작업 수행

 

테스트를 하기 위해서 src - test에 member 패키지와 memberServiceTest 클래스를 생성하고 @Test 어노테이션 붙이기

테스트는 given, when, then 세 단계로 나눠서 테스트

 

아래와 같은 방법으로 예상값이실제값과 일치하는지 비교 가능

Assertions.assertThat(member).isEqualTo(findMember);

단위테스트 하는 방법을 잘 알아두자

 

주문 도메인

 

DiscountPolicy - 할인정책 역할

Order - 주문(주문한 회원 id, 상품 이름, 상품가격, 할인율) 클래스 

OrderSerivce - 주문 서비스 역할

OrderServiceImpl - 할인을 적용해서 주문서비스를 구현해 주문을 생성

FixDiscountPolicy - 정액할인 구현

RateDiscountPolicy - 비율 할인을 적용해 주문서비스를 구현

 

할인 정책 문제점

위에 FixDiscountPolicy와 RateDiscountPolicy를 교체하려면 클라이언트인 OrderServiceImpl의 코드를 변경해줘야 한다

내 생각에는 그냥 코드한줄 바꾼거 아닌가? 였지만

 

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

 

이렇게 변경하는 건 추상적인 인터페이스 뿐 만 아니라 구현 클래스에도 의존한다 -> dip 위반

이렇게 기능을 확장해서 변경하면 클라이언트 코드에도 영향을 준다 -> ocp 위반

 

문제 해결하기

 

dip 위반하는걸 추상(인터페이스)에만 의존하게 의존관계를 변경하면 된다 

 private DiscountPolicy discountPolicy; <- 인터페이스만

그러나 이렇게 하면 구현부가 없기에 널포인트 에러 발생.. 누군가가 구현 객체를 대신 생성하고 주입해줘야한다

 

역할을 분리해서 구현 객체를 생성하고 연결하는 별도의 설정 클래스 만들어주기

 

설정 클래스인 AppConfig를 만들어준다

 

MemberServiceImpl을 변경해서 MemberServiceImpl 에서 생성자를 주입하는 생성자 코드를 작성한다

 

private final MemberRepository memberRepository;

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

위에 방법으로 MemberServiceImpl은 추상화된 인터페이스로만 이루어졌고 어떤 방법으로 구현할지는 아래의 AppConfig 클래스에서 만들어준다

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

할인 정책도 수정해서 인터페이스에서는 생성자를 만들어두고 appconfig에서 결정하게 변경한다

public class OrderSerivceImpl implements OrderSerivce {

    //dip를 위반하는 코드
    //    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    //        private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

    //dip에 맞게 인터페이스에만 의존하게 코드 수정후 appconfig에서 구현 클래스 선택
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

 

AppConfig 클래스에서 어떤 할인정책을 쓸지 정해주게 AppConfig 클래스를 변경했다

 

public class AppConfig {
    //AppConfig에서 구체적으로 어떤 구현객체를 생성하고 연결할지 정해준다
    public MemberService memberService(){
        return  new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderSerivce orderSerivce(){
        return new OrderSerivceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }
}

만들어둔 AppConfig를 이용하기 위해 MemberApp 클래스와 OrderApp 클래스를 수정해준다

        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();

테스트 케이스도 수정해준다, 테스트 케이스는 @BeforeEach 어노테이션을 써서 사용할 MemberService와 OrderSerivice를 수정해준다!

 

MemberService memberService;
    OrderSerivce orderSerivce;
    @BeforeEach
    public void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
        orderSerivce = appConfig.orderSerivce();
    }

 

AppConfig 리팩토링

 

위의 AppConfig 함수에서는 new MemoryMemberRepository를 중복해서 호출했기에 함수를 리팩토링해서 

저장소를 고르는 부분과 할인정책을 선택하는 부분을 함수로 분리시켜서 리팩토링했다,

역할과 구현을 분리해서 명확하게 들어온다

public class AppConfig {
    //AppConfig에서 구체적으로 어떤 구현객체를 생성하고 연결할지 정해준다
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

    //저장소 선택하는 함수
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderSerivce orderSerivce(){
        return new OrderSerivceImpl(memberRepository(), discountPolicy());
    }

    //할인정책 선택하는 함수
    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }
}

 

지금까지의 전체 흐름 - 객체 지향 원리 적용 ppt 20페이지, 21페이지

 

제어의 역전, IoC

IoC <- 제어의 역전, 프레임워크가 호출하는 방식으로 구현

구현 객체가 프로그램의 흐름을 조종, AppConfig를 이용한 후에는 OrderServiceImpl등 구현체는 직접 구현객체를 생성하지 않고 AppConfig가 구현 객체를 호출하게 됨

OrderSerivceImpl 같은 구현체를 여러개 만들어도

AppConfig가 구현 객체를 만들게 된다.

이렇게 프로그램이 제어 흐름을 직접 제어하는게 아니라 외부에서 관리하는 걸 IoC라고 한다

 

프레임워크와 라이브러리의 차이

Junit이 내가 작성한 코드를 제어하고 실행 - 프레임워크

내가 작성한 코드가 제어의 흐름을 담당하면 (내가 짠 코드로 java 코드를 xml이나 json 등으로 변환) - 라이브러리

 

의존관계 주입 DI

OrderServiceImpl 구현체는 DiscountPolicy 인터페이스에 의존하고 어떤 구현 객체가 사용할지는 모름, 클래스가 사용하는 import 코드만 보고 어떤 것을 쓸지 분석이 가능한 정적인(실행하지 않고도 판단 가능한) 상태

정적인 클래스 의존관계와, 실행 시간에 결정되는 동적인 객체 의존관계를 분리해서 생각

!의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않아도 동적인 객체 인스턴스를 변경할 수 있다

 

IoC 컨테이너, DI 컨테이너

AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결하는 것

 

스프링으로 전환하기

AppConfig 자바코드 상단에 @Configuration 어노테이션을 붙이고 메서드에 @Bean을 붙여준다

MemberApp 코드를 수정해서 AppConfig를 객체로 생성해서 불러오던걸 환경설정을 가져와서 쓰는 걸로 변경했다

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

        //이렇게 하면 AppConfig의 환경설정 정보를 기반으로 스프링이 관리
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        //이름과 클래스로 설정에서 가져오기
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);

OrderApp 코드도 수정해보자

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

ApplicationContext를 스프링 컨테이너라 한다.
기존에는 개발자가 AppConfig 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를
통해서 사용한다.
스프링 컨테이너는 `@Configuration` 이 붙은 `AppConfig` 를 설정(구성) 정보로 사용한다. 여기서 `@Bean` 이
라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된
객체를 스프링 빈이라 한다.
스프링 빈은 `@Bean` 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다

 

아직은 이렇게 바꾸는게 더 복잡하고 비ㅣ효율적인것 같지만 쓰는 이유가 있지 않을까..?

'김영한 스프링 > 김영한 스프링 기본편' 카테고리의 다른 글

의존관계 자동 주입  (1) 2023.10.03
컴포넌트 스캔  (0) 2023.09.20
싱글톤 컨테이너  (0) 2023.09.18
스프링 빈과 스프링 컨테이너  (0) 2023.09.13