본문 바로가기

SPRING/Spring

[스프링| 스프링 핵심 원리 | 기본편 | 스프링 핵심 원리 이해2 - 객체 지향 원리 적용]관심사의 분리

public class AppConfig {
    // AppConfig라는 클래스를 정의하여 애플리케이션 서비스를 구성합니다.

    public MemberService memberService() {
        // MemberService 타입의 객체를 생성하고 반환하는 메서드입니다.
        return new MemberServiceImpl(new MemoryMemberRepository());
        // MemberServiceImpl 객체를 생성하며, 이 때 MemoryMemberRepository 객체를 생성자를 통해 주입합니다.
        // 이는 의존성 주입의 한 예로, MemberServiceImpl이 MemoryMemberRepository에 의존하지만, 직접 생성하지 않습니다.
    }

    public OrderService orderService() {
        // OrderService 타입의 객체를 생성하고 반환하는 메서드입니다.
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
        // OrderServiceImpl 객체를 생성하며, 이 때 MemoryMemberRepository와 FixDiscountPolicy 객체를 생성자를 통해 주입합니다.
        // 이것도 의존성 주입의 예로, OrderServiceImpl은 MemoryMemberRepository와 FixDiscountPolicy에 의존하지만, 직접 생성하지 않습니다.
    }
}

public class MemberServiceImpl implements MemberService {
    // MemberService 인터페이스를 구현하는 MemberServiceImpl 클래스를 정의합니다. 이 클래스는 회원 관리 기능을 제공합니다.

    private final MemberRepository memberRepository;
    // MemberRepository 인터페이스에 대한 참조를 저장하는 private 필드입니다. final 키워드는 변수가 생성자에서 초기화된 이후에는 변경될 수 없음을 의미합니다.

    public MemberServiceImpl(MemberRepository memberRepository) {
        // MemberServiceImpl 클래스의 생성자입니다. MemberRepository 타입의 객체를 인자로 받아서 클래스 내의 memberRepository 필드를 초기화합니다.
        this.memberRepository = memberRepository;
        // 인자로 받은 memberRepository 객체를 클래스의 memberRepository 필드에 할당합니다. 이는 생성자를 통한 의존성 주입의 예입니다.
    }

    @Override
    public void join(Member member) {
        // Member 인터페이스의 join 메서드를 구현합니다. Member 타입의 객체 member를 인자로 받습니다.
        memberRepository.save(member);
        // memberRepository의 save 메서드를 호출하여 member 객체를 저장합니다.
    }

    @Override
    public Member findMember(Long memberId) {
        // Member 인터페이스의 findMember 메서드를 구현합니다. Long 타입의 memberId를 인자로 받습니다.
        return memberRepository.findById(memberId);
        // memberRepository의 findById 메서드를 호출하여 memberId에 해당하는 Member 객체를 반환합니다.
    }
}

생성자 주입

생성자 주입(Constructor Injection)은 의존성 주입(DI)의 한 방법으로, 객체가 생성될 때 필요한 의존성을 생성자를 통해 제공받는 방식입니다.

이 방식은 객체의 불변성을 보장하고, 의존성이 명시적으로 처리되기 때문에 코드의 명확성과 테스트 용이성을 증가시킵니다. 또한, 모든 의존성이 객체가 완전히 생성되기 전에 존재함을 보장하므로, NullPointerException을 방지할 수 있습니다.

이 클래스에서는 MemberRepository를 생성자를 통해 주입받고 있어, MemberServiceImpl 객체는 MemberRepository 없이는 인스턴스화 될 수 없습니다. 이는 결합도를 낮추고, 각 구성 요소의 테스트 및 유지 관리를 용이하게 합니다.

public class OrderServiceImpl implements OrderService {
    // OrderService 인터페이스를 구현하는 OrderServiceImpl 클래스를 정의합니다. 이 클래스는 주문 관리 기능을 제공합니다.

    private final MemberRepository memberRepository;
    // MemberRepository 인터페이스에 대한 참조를 저장하는 private 필드입니다. final 키워드는 이 변수가 생성자에서만 초기화되며 변경되지 않음을 보장합니다.

    private final DiscountPolicy discountPolicy;
    // DiscountPolicy 인터페이스에 대한 참조를 저장하는 private 필드입니다. 이 역시 final 키워드로 선언되어 변경할 수 없습니다.

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        // OrderServiceImpl 클래스의 생성자입니다. MemberRepository와 DiscountPolicy 타입의 객체를 인자로 받아서 클래스 내의 memberRepository와 discountPolicy 필드를 초기화합니다.
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
        // 인자로 받은 memberRepository와 discountPolicy 객체를 클래스의 각 필드에 할당합니다. 이는 생성자를 통한 의존성 주입의 예입니다.
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        // OrderService 인터페이스의 createOrder 메서드를 구현합니다. memberId, itemName, itemPrice를 인자로 받습니다.
        Member member = memberRepository.findById(memberId);
        // memberId를 사용하여 memberRepository에서 Member 객체를 조회합니다.
        int discountPrice = discountPolicy.discount(member, itemPrice);
        // 조회된 Member 객체와 itemPrice를 사용하여 discountPolicy의 discount 메서드를 호출하고, 할인된 가격을 계산합니다.

        return new Order(memberId, itemName, itemPrice, discountPrice);
        // 주문 정보를 포함하는 새 Order 객체를 생성하고 반환합니다.
    }
}

public class MemberApp {
    // MemberApp 클래스를 정의합니다. 이 클래스는 어플리케이션의 진입점을 포함합니다.

    public static void main(String[] args) {
        // main 메서드를 정의합니다. 이 메서드는 프로그램의 시작점입니다.

        AppConfig appConfig = new AppConfig();
        // AppConfig 클래스의 인스턴스를 생성합니다. AppConfig는 의존성 주입 컨테이너로 작동하여 애플리케이션의 설정 및 구성을 담당합니다.

        MemberService memberService = appConfig.memberService();
        // AppConfig 인스턴스를 통해 MemberService 타입의 객체를 요청합니다. 이는 의존성 주입을 통해 MemberServiceImpl 객체를 반환받습니다.

        Member member = new Member(1L, "member1", Grade.VIP);
        // Member 타입의 객체를 생성합니다. 이 객체는 ID가 1L, 이름이 "member1", 등급이 VIP인 회원을 나타냅니다.

        memberService.join(member);
        // memberService를 사용하여 member 객체를 회원으로 등록합니다. join 메서드는 내부적으로 MemberRepository의 save 메서드를 호출합니다.

        Member findMember = memberService.findMember(1L);
        // memberService를 사용하여 ID가 1L인 회원을 조회합니다. findMember 메서드는 내부적으로 MemberRepository의 findById 메서드를 호출합니다.

        System.out.println("new member = " + member.getName());
        // 콘솔에 새로 등록한 회원의 이름을 출력합니다.

        System.out.println("find Member = " + findMember.getName());
        // 콘솔에 조회한 회원의 이름을 출력합니다.
    }
}

AppConfig를 통한 의존성 주입

AppConfig 클래스는 의존성 주입을 구현하기 위한 중앙 관리 역할을 합니다. 이 클래스는 애플리케이션에서 필요한 객체들을 생성하고, 그 객체들 간의 의존 관계를 설정하는 책임을 집니다. 여기서 AppConfig는 MemberService 인터페이스의 구현체인 MemberServiceImpl 인스턴스를 생성할 때 필요한 MemberRepository 객체를 생성자를 통해 MemberServiceImpl에 주입합니다. 이 과정은 아래와 같은 장점을 갖습니다:

  1. 결합도 감소: 구성 요소 간의 의존성이 AppConfig에 의해 관리되므로, 각 클래스는 자신의 의존 객체를 직접 생성하지 않아도 됩니다. 이로 인해 결합도가 감소하고, 유지보수 및 확장성이 향상됩니다.
  2. 유연한 구성: 의존성을 외부에서 주입받기 때문에, 구성 변경이 필요할 때 AppConfig만 수정하면 됩니다. 예를 들어, 다른 MemberRepository 구현체를 사용하고 싶을 때 AppConfig의 코드 한 줄만 변경하면 됩니다.
  3. 테스트 용이성: 의존성을 주입받기 때문에, 테스트 중에는 실제 구현 대신 모의 객체(Mock Object)를 주입하여 테스트를 단순화할 수 있습니다
public class OrderApp {
    // OrderApp 클래스를 정의합니다. 이 클래스는 애플리케이션의 진입점을 포함하며 주문 관리 기능을 수행합니다.

    public static void main(String[] args) {
        // main 메서드를 정의합니다. 이 메서드는 프로그램의 시작점입니다.

        AppConfig appConfig = new AppConfig();
        // AppConfig 클래스의 인스턴스를 생성합니다. AppConfig는 의존성 주입 컨테이너로 작동하여 애플리케이션의 설정 및 구성을 담당합니다.

        MemberService memberService = appConfig.memberService();
        // AppConfig 인스턴스를 통해 MemberService 타입의 객체를 요청합니다. 이는 의존성 주입을 통해 MemberServiceImpl 객체를 반환받습니다.

        OrderService orderService = appConfig.orderService();
        // AppConfig 인스턴스를 통해 OrderService 타입의 객체를 요청합니다. 이는 의존성 주입을 통해 OrderServiceImpl 객체를 반환받습니다.

        Long memberId = 1L;
        // 회원 식별자를 위한 Long 타입의 변수 memberId를 선언하고 1L로 초기화합니다.

        Member member = new Member(memberId, "member1", Grade.VIP);
        // Member 타입의 객체를 생성합니다. 이 객체는 ID가 memberId, 이름이 "member1", 등급이 VIP인 회원을 나타냅니다.

        memberService.join(member);
        // memberService를 사용하여 member 객체를 회원으로 등록합니다. join 메서드는 내부적으로 MemberRepository의 save 메서드를 호출합니다.

        Order order = orderService.createOrder(memberId, "item1", 10000);
        // orderService를 사용하여 주문을 생성합니다. 주문은 memberId, 상품명 "item1", 가격 10000원으로 설정됩니다.

        System.out.println("order =" + order);
        // 콘솔에 생성된 주문 객체를 출력합니다. Order 클래스의 toString 메서드에 의해 형식이 정의됩니다.
    }
}

AppConfig 클래스는 의존성 주입을 구현하기 위한 중앙 관리 역할을 합니다. 이 클래스는 애플리케이션에서 필요한 서비스 객체들을 생성하고, 그 객체들 간의 의존 관계를 설정하는 책임을 집니다. 여기서 AppConfigMemberServiceOrderService 인터페이스의 구현체인 MemberServiceImplOrderServiceImpl 인스턴스를 생성할 때 필요한 MemberRepository, DiscountPolicy 객체를 생성자를 통해 각 서비스에 주입합니다.

public class MemberServiceTest {
    // MemberServiceTest 클래스를 정의합니다. 이 클래스는 MemberService의 기능을 테스트하기 위한 테스트 케이스를 포함합니다.

    MemberService memberService;
    // MemberService 인터페이스를 구현하는 객체의 참조를 저장할 필드입니다.

    @BeforeEach
    public void beforeEach() {
        // 각 테스트 메서드가 실행되기 전에 실행될 메서드를 정의합니다. 이 메서드는 테스트 환경을 초기화하는 데 사용됩니다.
        AppConfig appConfig = new AppConfig();
        // AppConfig 클래스의 인스턴스를 생성합니다. AppConfig는 의존성 주입 컨테이너로 작동하여 테스트에 필요한 서비스 구성을 제공합니다.
        memberService = appConfig.memberService();
        // AppConfig 인스턴스를 통해 MemberService 타입의 객체를 요청하고, 반환된 객체를 memberService 필드에 할당합니다.
    }

    @Test
    void join() {
        // 테스트 메서드를 정의합니다. 'join' 기능의 정상 작동을 검증합니다.

        //given
        Member member = new Member(1L, "member1", Grade.VIP);
        // 테스트에 사용될 Member 객체를 생성합니다. 이 객체는 ID가 1L, 이름이 "member1", 등급이 VIP인 회원을 나타냅니다.

        //when
        memberService.join(member);
        // memberService의 join 메서드를 호출하여 member 객체를 등록합니다.
        Member findMember = memberService.findMember(1L);
        // memberService의 findMember 메서드를 호출하여 ID가 1L인 회원을 조회합니다.

        //then
        Assertions.assertThat(member).isEqualTo(findMember);
        // Assertions 클래스의 assertThat 메서드를 사용하여, 등록된 회원과 조회된 회원이 동일한지 검증합니다.
    }
}
public class OrderServiceTest {
    // OrderServiceTest 클래스를 정의합니다. 이 클래스는 OrderService의 기능을 테스트하기 위한 테스트 케이스를 포함합니다.

    MemberService memberService;
    // MemberService 인터페이스를 구현하는 객체의 참조를 저장할 필드입니다.

    OrderService orderService;
    // OrderService 인터페이스를 구현하는 객체의 참조를 저장할 필드입니다.

    @BeforeEach
    public void beforeEach() {
        // 각 테스트 메서드가 실행되기 전에 실행될 메서드를 정의합니다. 이 메서드는 테스트 환경을 초기화하는 데 사용됩니다.
        AppConfig appConfig = new AppConfig();
        // AppConfig 클래스의 인스턴스를 생성합니다. AppConfig는 의존성 주입 컨테이너로 작동하여 테스트에 필요한 서비스 구성을 제공합니다.
        memberService = appConfig.memberService();
        // AppConfig 인스턴스를 통해 MemberService 타입의 객체를 요청하고, 반환된 객체를 memberService 필드에 할당합니다.
        orderService = appConfig.orderService();
        // AppConfig 인스턴스를 통해 OrderService 타입의 객체를 요청하고, 반환된 객체를 orderService 필드에 할당합니다.
    }

    @Test
    void createOrder() {
        // 테스트 메서드를 정의합니다. 'createOrder' 기능의 정상 작동을 검증합니다.

        Long memberId = 1L;
        // 회원 식별자를 위한 Long 타입의 변수 memberId를 선언하고 1L로 초기화합니다.

        Member member = new Member(memberId, "member1", Grade.VIP);
        // Member 타입의 객체를 생성합니다. 이 객체는 ID가 memberId, 이름이 "member1", 등급이 VIP인 회원을 나타냅니다.

        memberService.join(member);
        // memberService의 join 메서드를 호출하여 member 객체를 회원으로 등록합니다.

        Order order = orderService.createOrder(memberId, "item1", 10000);
        // orderService를 사용하여 주문을 생성합니다. 주문은 memberId, 상품명 "item1", 가격 10000원으로 설정됩니다.

        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
        // Assertions 클래스의 assertThat 메서드를 사용하여, 주문에서 적용된 할인 금액이 1000원인지 검증합니다.
    }
}

BeforeEach 어노테이션은 JUnit 테스트에서 각 테스트가 실행되기 전에 특정 작업을 수행하도록 지정합니다.

 이 경우, BeforeEach 메서드는 AppConfig 인스턴스를 생성하여 의존성 주입을 통해 MemberService 객체를 초기화합니다. AppConfig는 애플리케이션의 구성과 서비스의 의존성 관리를 담당하는 클래스로, 테스트 환경에서도 이를 통해 의존성을 주입받습니다. 이 접근 방식은 테스트 코드에서 의존성의 소스를 명확히 하여, 테스트의 독립성과 일관성을 보장합니다. 서비스의 구현이 변경되어도 테스트 코드는 AppConfig를 통해 적절히 조정된 서비스 구현체를 계속 사용할 수 있습니다.