스프링/핵심원리-기본

스프링 예제

eunkyung 2022. 9. 6. 03:17

스프링 프로젝트 생성

 

JAVA 11 설치, IDE:IntelliJ 설치

 

 https://start.spring.io -사이트로 이동해 스프링 프로젝트 생성.  스프링 부트는 프로젝트 환경 설정 편하게 하기 위해 사용.

설정-> Progect: Gradle / Language: Java / Spring Boot: snapshot이나 M버전은 정식 출시된게 아니므로 이를 제외한 가장 최신 버전으로 선택 / Packaging: Jar / Java:11(설치 version) / Dependencies: 아무것도 선택하지 않으면 스프링 부트가 코어쪽 라이브러리만 가지고 간단히 구성 = 별도의 의존관계 설정 x.

 

이후 생성 누르면 압축파일로 다운로드 됨 -> study 폴더에 압축 해제 후 import. 

File -> settings ->gradle  검색 -> gradle로 설정된 것 intellij로 바꿔줘야 더 빠름. 

 

+BASIC, VIP는 상수 취급.


비즈니스 요구사항과 설계

 

미확정인 것은 나중에 바뀔 가능성 O -> 하지만 역할과 구현을 분리하면 개발에는 아무 문제가 없다!

현재는 순수한 자바 코드로 개발 진행.

 

*회원 도메인 설계

-회원 도메인 협력관계 //기획자들도 볼 수 있는 그림.

클라이언트는 회원 서비스를 호출,

회원 저장소 별도로 생성(회원 데이터에 접근하는 계층을 따로 만듦. 아직 저장소가 미확정이기에) -> 회원 저장소 인터페이스로 생성 + 구현한 여러 회원 저장소(메모리, DB, 외부 시스템 연동 회원 저장소).

일단 저장소를 이용해야 하기에 간단히 memory회원 저장소 생성 후 개발 진핸, 테스트나 로컬 개발 시 사용. memory는 서부재부팅 시 데이터가 사라지기에 개발용으로만 사용됨. -> 이후 저장소 확정되며 그 부분만 개발해 교체하면 됨(갈아끼우면 됨!)

-회원 클래스 다이어그램  //회원 도메인 협력 관계를 바탕으로 개발자가 구체화해 그린 것.서버 실행하지 않고도 클래스 분석해서 볼 수 있는 그림.

멤버 서비스 역할을 인터페이스로. 이것의 구현체로 멤버서비스Impl 생성. 회원저장소 인터페이스와 메모리/DB 구현 객체 있음. 

MemberService를 인터페이스로 설정한 이유는 다형성을 위해, 변경을 용이하게 하기 위해.

HashMap이 아닌 ConCurrentHashMap을 사용한 이유는 동시성 이슈 때문. 

-회원 객체 다이어그램 // 서버가 뜬 뒤 동적으로 결정되는 것을 얘를 통해 알 수 있음. 

클라이언트-------> 회원서비스 Impl(클라이어트가 실제 바라보는 것은 Impl임) ------->메모리 회원 저장소(교체 가능)

 

 

 

 


테스트

 

회원 객체 다이어그램에 따라 생성. 

클래스다이어그램은 정적인 것, 회원 객체 다이어그램은 동적인 것. 

---------------

회원 도메인의 설계상 문제점은 무엇일까?

다른 저장소 변경 시 OCP 원칙을 잘 지키지 X. DIP 잘 지켜지지 X.

=>얘가 의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점O.

 

-------------------------------------------------------------------------------------------------------------------------------

>주문과 할인 도메인 설계

클라이언트는 주문 생성 -> 주문 서비스는 주문 생성의 역할. 회원 등급이 필요하기 때문에 ->회원 저장소로 가서 회원 조회 -> 회원 등급을 가지고 할인 정책 역할에 물어보고 -> 주문서비스는 할인까지 반영된 주문 결과 클라이언트에 반환.

 

1-클라이언트(main같은 코드나 컨트롤러)가 주문에 필요한 데이터를 가지고 주문 서비스에 요청.

2-회원 아이디로 등급 정보 찾아옴.

3-할인여부 요청

 

역할이 먼저 만들어지고 이후 구현을 만듦.

=> 역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계했다. 덕분에 회원 저장소는 물론이고, 할인 정책도 유연하게 변경할 수 있다

정액 할인->정률 할인으로 쉽게 갈아끼울수 있다는 얘기.

 

실제 구현

OrderService 인터페이스 / OrderService 인터페이스 구현에인 OrderServiceImpl(구현체 하나면 뒤에 Impl붙임.)

+여러 구현체

 

*객체 다이어그램

실제 new로 생성해서 애플리케이션에 띄워 동적으로 객체들의 연관 관계 발생.

역할들의 협력관계를 그대로 재사용 할 수 있다 = DB나 할인 구현체 바뀌어도 주문 서비스 변경할 필요없다.

 

+enum 타입은 ==쓰는게 맞음.

 

 

-------------------------------------

새로운 할인 정책 적용과 문제점

 

-할인 정책 변경 시 클라이언트인 OrderServiceImpl코드를 고쳐야 함.

-문제점

우리는 역할과 구현을 충실하게 분리했다. OK

다형성도 활용하고, 인터페이스와 구현 객체를 분리했다. OK

OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했다?-> 그렇게 보이지만 사실은 아니다.

DIP: 주문서비스 클라이언트( OrderServiceImpl )는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같은데? -> 클래스 의존관계를 분석해 보자. 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다. 추상(인터페이스) 의존: DiscountPolicy 구체(구현) 클래스: FixDiscountPolicy , RateDiscountPolicy

OCP: 변경하지 않고 확장할 수 있다고 했는데! 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다! 따라서 OCP를 위반한다

 

-왜 클라이언트 코드를 변경해야 할까?

지금까지는 단순히 DiscountPolicy(추상 클래스)만 의존한다고 생각. 하지만 잘보면 FixDiscountPolicy(구체적인 클래스)도 의존하고 있음. 실제 코드를 보면 의존하고 있음. 

private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

=>DIP위반.

그래서 FixDiscountPolicy 를 RateDiscountPolicy 로 변경하는 순간 OrderServiceImpl 의 소스 코드도 함께 변경해야 한다! =>OCP 위반

 

-이 문제를 어떻게 해결할까.

DiP에 위반하지 않게 인터페이스에만 의존하도록 의존관계 변경해야 함.

private DiscountPolicy discountPolicy; //final이면 안됌. final은 반드시 값을 할당해야 하기에 final도 없애주기

DIP는 지킴. 하지만 아무 값도 할당되지 않았기에 nullPointException 발생.

 

해결방안 -> 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다.

 

>관심사의 분리 

애플리케이션을 하나의 공연에 비유해보면,

로미오 역할을 하는 배우, 줄리엣 역할을 누가 할지는 특정 배우가 정하지 않아야 한다. 기존 코드는 인터페이스와 구현체 모두에 의존했으므로 로미오 역할을 하는 배우가 직접 줄리엣 역할을 하는 배우를 선택하는 것과 같은데, 그렇게 되면 로미오 역할의 배우는 연기와 캐스팅 같은 다양한 책임을 가지게 된다. 

=>"관심사를 분리"가  필요

구현체인 배우는 역할인 배역을 수행하는 것에 집중하고, 상대역으로 어떤 배우가 되든지 영향을 받지 않아야 한다.

공배우를 섭외하는 책임을 담당하는 별도의 "공연 기획자"가 필요, 공연기획자를 만들고 책임을 확실히 분리해야 한다.

 

애플리케이션도 구현체들은 본인의 역할만 수행하고, 인터페이스에 어떤 구현체가 할당될지는 AppConfig가 해줘야 함. 

*AppConfig - 애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스.

-기존에는 구현체 내에서 직접 설정해 줬지만 이젠 AppConfig에서 memeberService생성. 생성자를 통해서 구현체 설정.

생성자로 인해 memberRepository에는 구현체가 할당됨. MemberServiceImpl 코드에는 구체적인 구현체에 대한 코드 X. 구현체에 대한 것은 전혀 모르고 있고, 오직 인터페이스에 대한 것만.  추상화에만 의존 <=DIP를 지키는 것. "생성자 주입"

-AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.

-AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다. 

 

설계 변경으로 MemberServiceImpl 은 MemoryMemberRepository 를 의존X.  MemberRepository 인터페이스만 의존. MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다. MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부( AppConfig )에서 결정된다.

MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다

 

멤버서비스 인터페이스의 구현체인  멤버서비스Impl이 멤버리포지토리 인터페이스 의존.

AppConfig가 Impl 구현체 생성. 

-객체의 생성과 연결은 AppConfig가 담당.

-DIP 잘지킴(멤버서비스 impl은 멤버리포지토리인 추상에만 의존하면 되고 구현체 몰라도 됨.

-객체 생성 역할과 실행하는 역할이 명확하게 분리, 관심사의 분리

 

AppConfig가 memoryMemberRepository객체를 생성하고, memoryMemberRepository의 참조값인 x001을 memberServiceImpl 생성시 생성자로주입함. 클라이언트인 memberServiceImpl입장에서는 의존관계를 외부에서 주입해주는 것과 같다고 하여 AI(Dependency Injection)이라고 함. 우리 말로 의존관계 주입/의존성 주입.

 

-설계 변경으로 OrderServiceImpl 은 FixDiscountPolicy 를 의존하지 않는다! 단지 DiscountPolicy 인터페이스만 의존한다. -OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다. OrderServiceImpl 의 생성자를 통해서 어떤 구현 객체을 주입할지는 오직 외부( AppConfig )에서 결정한다.

-OrderServiceImpl 은 이제부터 실행에만 집중하면 된다. OrderServiceImpl 에는 MemoryMemberRepository , FixDiscountPolicy 객체의 의존관계가 주입된다.

-MemberApp

MemberService memberService = appConfig.memberService();

기존에는 멤버서비스를 메인 메소드에서 생성, 이제는 appConfig에서 구현체 알아서 결정하고, 요청하면 appConfig가 인터페이스 보내줌. 여기 멤버서비스에는 memberServiceImpl이 들어옴. +OrderApp도 이와 같이 수정필요. 

 

*정리

AppConfig를 통해서 관심사를 확실하게 분리. AppConfig는 구체 클래스를 선택.

애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임.

이제 각 구현체들은 담당 기능을 실행하는 책임만 지면 된다. OrderServiceImpl 은 기능을 실행하는 책임만 지면 된다.

 

=>DIP와 단일 책임 원칙을 지킨 것.

 

--------------------------------------------------------

>AppConfig 리팩터링

그러나 appConfig에서 코드들을 보면 중복이 있고, 역할에 따른 구현이 잘 안보임. 역할이 드러나게 하는게 매우 중요. 

public MemberService memberService() {
 	return new MemberServiceImpl(memberRepository());
 }
 
 public OrderService orderService() {
 	return new OrderServiceImpl(  memberRepository(),  discountPolicy());
 }
 
 public MemberRepository memberRepository() {
 	return new MemoryMemberRepository();
 }
 public DiscountPolicy discountPolicy() {
     return new FixDiscountPolicy();
 }

리팩터링 후 코드를 보면 각 인터페이스가 드러나 있어 메소드명을 보는 순간 역할이 드러남. 어떤 구현체를 쓰고, 구현체 변경 시 어디 코드만 수정해야 할지(+전체 구성) 한눈에 알 수 있음. +)new로 생성하는 중복이 제거됨.

 

------------------------------------------------------------------------------------------------------------------------------

>새로운 구조에 할인 정책 적용.

정액 할인 정책 -> 정률 할인 정책으로 변경. FixDiscountPolicy -> RateDiscountPolicy //기존에는 변경하려고 할때 클라이언트 코드가 영향을 받았었다.

AppConfig가 있기에 얘만 변경하면 된다. AppConfig의 등장으로 애플리케이션이 사용 영역/구성 영역(객체 생성)으로 분리됨. 할인 정책 변경 시 구성영역의 코드만 변경하면 됨.

구성 역할을 하는 appConfig만 변경하고 사용 영역의 어떠한 코드도 변경이 필요 X.

구성 영역은 변경 O. 구현체에 대한 책임을 구성영역이 가짐.

=>영향 범위가 매우 작아짐 / OCP(확장에는 열려있고 변경에는 닫힘), DIP(추상에 의존, 구현체는 모름) 모두 잘 지킴.