개인적으로 어려웠던 스프링의 원리과 사용기술을 학습하기 위하여 작성한 글입니다. 많은 내용을 참고하여 내용을 정리하였습니다. 수정사항이 있다면 언제든 지적해주세요!!
스프링은 자바 언어 기반의 프레임워크이고, 자바는 객체 지향 언어이다.
그렇다면 좋은 객체 지향 프로그래밍이란 무엇일까?
역할 - 인터페이스
구현 - 구현클래스
이상적으로는 모든 설계에 인터페이스를 부여한다. 하지만 기능을 확장할 가능성이 없다면, 구체 클래스를 직접 사용하고 향후 꼭 필요할 때 리팩터링해서 인터페이스를 도입하는 것도 방법이다.
프로젝트는 본인의 환경에 맞추어 설정해도 무관하다. 스프링 관련 기술을 학습하기 위한 목적으로 프로젝트를 생성하였으며 불필요한 개발요소는 제외하였다.
Java 11 설치
IntelliJ 설치
프로젝트 선택
Project Metadata
Dependencies: 선택하지 않는다.
기존 자바 코드로 구현하고 관련 상황을 재현하기 위해 스프링 관련 Dependencies를 추가하지 않는다.
현재 미확정 요소가 존재하고 변경될 수 있는 요소가 존재한다. 하지만 우리는 개발일정에 따라 개발을 진행해야 한다. 우리는 인터페이스를 이용하고 구현내용은 언제든 변경이 가능하도록 설계할 것이다.
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
public enum Grade {
BASIC,
VIP
}
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);
}
}
현재 DB가 정해지지 않아 회원은 메모리 저장소를 이용하여 우선 개발을 진행한다.
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
public interface DiscountPolicy {
int discount(Member member, int price);
}
public class FixDiscountPolicy implements DiscountPolicy{
private int discountFixAmount = 1000; //1000원 할인
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
우리는
DiscountPolicy
인터페이스를 설계한 뒤FixDiscountPolicy
라는 구현체를 통하여 할인 정책을 구현한다. 나중에 할인 정책이 변경되더라도 구현체만 새로 개발하여 교체하기만 하면 된다.
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);
}
}
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);
}
}
MemoryMemberRepository
-> JdbcMemberRepository
로 변경이 필요하다면 객체를 생성하는 부분의 코드 수정이 필요하다.//MemberRepository memberRepository = new MemoryMemberRepository();
MemberRepository memberRepository = new JdbcMemberRepository();
FixDiscountPolicy
고정 할인 정책을 RateDiscountPolicy
정률 할인 정책으로 변경해보도록 하자. 우리는 인터페이스 설계에 따라 구현체만 개발하면 된다.public class RateDiscountPolicy implements DiscountPolicy{
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
OrderServiceImpl
은 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.추상(인터페이스) 의존:
DiscountPolicy
구체(구현) 클래스:FixDiscountPolicy
,RateDiscountPolicy
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private DiscountPolicy discountPolicy;
}
위 코드에서는 DiscountPolicy
인터페이스만 의존하도록 수정하였다. 하지만 지금상태에서는 구현체가 존재하지 않기때문에 null pointer exception 이 발생할 것이다.
이 문제를 해결하기 위해서는 외부에서 구현객체를 대신 생성하고 주입해주어야 한다.(중요)
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
}
MemberServiceImpl
생성자를 이용하여 의존관계 주입private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
MemberServiceImpl
의 코드에서는 MemberRepository
인터페이스에만 의존하고 있다. 구현객체는 AppConfig
에서 MemoryMemberRepository
생성하고 의존관계를 주입하고 있다.
우리는 AppConfig
를 통하여 구현객체를 주입하고 관리하도록 코드를 수정했다. MemberServiceImpl
에서 구현객체를 직접 생성하여 사용하지 않고 외부에서 의존관계를 주입받아서 사용하게 된다. 어떤 의존성이 주입되는지 모르더라도 우리는 코드 실행에만 충실하면 된다.
구현객체가 변경되더라도 AppConfig
에서 관리하는 부분만 수정하면 된다. AppConfig
의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분리되었다.
이제 새로운 할인 정책을 적용해보자
public class AppConfig {
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(),
discountPolicy());
}
public DiscountPolicy discountPolicy() {
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
AppConfig
에서 RateDiscountPolicy
구현객체를 바꾸어 주면 된다. 코드를 실행하는 영역과 구성을 책임지는 역할을 구분하였기 떄문에 애플리케이션 확장과 변경이 용이해졌다.SRP 단일 책임 원칙
AppConfig
를 통하여 구현 객체를 생성하고 연결하도록 만들었고, 다른 클라이언트 객체는 실행만을 담당하도록 만들었다. 이렇게 하나의 클래스는 하나의 책임만 가지도록 수정하였다.DIP 의존관계 역전 원칙
AppConfig
를 생성하고 의존관계를 주입하게 변경하여 DIP 문제를 해결하였다.OCP 개방-폐쇄 원칙
AppConfig
가 의존관계를 설정하여 클라이언트 코드에 주입하기 때문에 클라이언트 코드를 변경하지 않아도 된다.AppConfig
처럼 의존관계를 설정하는 역할을 스프링에서는 DI컨테이너 라고 한다. DI컨테이너가 왜 필요한지, 무슨 역할을 하는지 이해할 수 있었다. 이제 기존 자바코드에서 스프링으로 전환하며 개념을 익히고 역할에 대하여 학습하도록 할 것이다.학습에 도움이 되었던 자료
참고 - github
좋은글 잘 읽고갑니다