-
스프링 핵심 원리 (1) - 예제 순수 자바 to 스프링 컨테이너Web/Spring 2023. 8. 16. 18:49
- 객체 지향 프로그래밍의 원리에 맞게 회원 및 주문 서비스를 만들어보자
- 객체 지향 설계에서 협력 - 역할 - 책임의 관계는 매우 중요하다.
역할: 객체가 수행할 책임들을 의미하고, 다형적인 특징을 보여준다.
책임: 객체가 수행해야할 행위 공용인터페이스
- 객체지향-
다형적 특징은 역할과 구현을 분리함으로써 달성할 수 있다.
캡슐화: - 데이터 숨김, 내부 구현 숨김,메시지를 통한 데이터 접근 -> 객체 자율성 부여
- 오류의 범위 캡슐 내부화 -> 유지보수에 유리
타입: 객체를 나누는 기준 -> 할 수 있는 책임(행위)에 따라 객체 분리(classify)
클래스: 객체를 생성할 수 있도록 언어적 차원에서 지원된 정적 객체 생성 방법
- 객체지향적 설계에서 가장 중요한 것은 협력관계를 통해 적절히 책임을 분할하고, 객체에 할당하는 것
- 어떤 메시지를 주고받을 지 결정 -> 객체의 책임 도출
-> 개념적인 인터페이스 (리턴타입,파라미터,메서드명 등 설정)
-> 인터페이스 구현 (상속,인터페이스 구현)
* 메시지 설정은 클라이언트(컨트롤러,메인메서드 등) 중심으로 생각하자
* 이러한 객체 지향의 특징을 통해 갹체 사용하는 클라이언트 코드 수정 없이 구현을 변경,쉬운 확장을 할 수 있게되며,
유지보수에 이점이 있다.
- 의존 -
의존이란 클래스에 필드, 파라미터 등에서 특정 클래스를 사용하는 것 (Has a)
public class ClassA{ private ClassB clb = new ClassB(); public void met01(ClassC c){ c.met02(); } }
> 객체지향설계의 OCP(개방폐쇄)원칙을 지키기 위해서는 강한의존관계는 지양해야한다.
- 확장,유지보수를 위해
1. 객체 지향적 설계와 다형성의 한계
1. 회원도메인
요구사항: 1. 회원 가입, 조회
2. 회원 등급 구분
3. 회원 데이터 자체 DB는 정해지지 않음(변경가능성 높다 -> 역할과 구현을 분리)
- 회원가입 및 조회 협력
> 클라이언트 : 회원 서비스에 회원가입 혹은 조회 메시지
> 회원 서비스: 회원 가입 및 조회 책임, 회원 저장소에 가입 회원 저장 및 저장된 회원 데이터 조회 요청
> 회원 저장소: DB에 회원 저장 및 조회 책임
- 메시지를 통해 결정된 책임에 따라 공용 인터페이스를 뽑고, 역할과 구현 분리
- 동적 객체의 흐름
2. 회원 도메인 구현
2.1 인터페이스
public interface MemberRepository { void save(Member member); Member findByid(Long memberId); } public interface MemberService { void join(Member member); Member findMember(Long memberId); }
- 저장 및 조회를 위한 인터페이스 뽑자
2.2 구현
public class MemberServcieimpl implements MemberService{ private final MemberRepository memberRe = new MemoryMemberRepository(); //.. }
public class MemoryMemberRepository implements MemberRepository{ private static Map<Long,Member> store=new HashMap<>(); //.. }
- 회원 서비스는 회원가입 및 조회를 위해 DB작업이 필요하므로 MemberRepository와 의존관계가 필요하다.
- MemberRepository의 콘크리트 클래스는 데이터를 저장할 수 있는 DB를 내부적으로 가지고 있다.
public class Member { private Long id; private String name; private Grade grade; //.. }
- 저장될 객체인 멤버는 위와 같은 필드를 가지고 있다.
public static void main(String[] args) { MemberService memberS = new MemberServcieimpl(); Member member1 = new Member(1L, "member1", Grade.VIP); memberS.join(member1); Member member = memberS.findMember(1L); //.. }
- 클라이언트는 위와 같이 멤버를 생성해서 서비스를 통해 회원가입 혹은 조회를 한다.
----문제점---------------------------------------------
* 역할과 구현을 분리 객체지향의 원리를 잘 지켰다.
위 협력관계는 추상화된 역할을 바탕으로 재사용할 수 있다(구현 객체만 갈아끼우면 쉽게 확장할 수 있다)
아직 남은 문제점은
public class MemberServcieimpl implements MemberService{ private final MemberRepository memberRe = new MemoryMemberRepository();
- 위와 같이 멤버서비스 구현체가 Repository 추상객체(인터페이스 타입)와 콘크리트 객체 두 가지 모두에 의존
> DIP원칙 위배, 추후 DB수정시 MemberServiceimpl의 코드도 수정해야함 (OCP위배)
3. 주문 할인 도메인 설계
- 주문 도메인 협력,역할,책임
회원등급에 따른 할인을 적용한 주문 데이터 생성 협력
- 클라이언트: 주문 서비스에게 주문 생성 요청
- 주문 서비스: 요청에 따라 주문 생성 및 결과 반환 역할 -> 회원저장소에 회원 조회 요청
-> 할인 정책에 할인 적용 가능 요청
- 회원저장소: 요청에 따른 회원 조회 책임, 할인 정책: 요청에 따라 회원이 할인 적용 가능한지, 할인 금액 얼마인지 결정 책임
- 역할과 구현 분리
- 세부 구현에 따라 객체 다이어그램에 할인 정책을 정률 할인 정책으로 변경할 수도 있다.
- 잘 설계된 객체지향프로그래밍의 이점으로 주문 서비스 구현체 변경없이 할인을 변경할 수 있다.
(재사용 가능한 협력관계)
4. 주문 도메인 구현
4.1 인터페이스
public interface OrderService { Order createOrder(Long memberId,String itemName,int itemPrice); }
4.2 구현체
public class OrderServiceimpl implements OrderService{ private final MemberRepository memberRepository =new MemoryMemberRepository(); private final DiscountPlicy disp = new FixDiscountPlicy(); //.. }
- 클라이언트 요청에 따라 주문객체를 생성하기 위해서는
-> 멤버 id를 통해 주문한 회원을 조회해야하므로 -> memberRepository와 의존
-> 조회한 멤버가 할인 적용이 가능한 멤버인지 확인하여, 할인 금액을 계산해야함 -> 할인 정책 객체와 의존
public class Order{ private Long memberId; private String itemName; private int itemPrice; private int discountPrice; public Order(Long memberId, String itemName, int itemPrice, int discountPrice) { this.memberId = memberId; this.itemName = itemName; this.itemPrice = itemPrice; this.discountPrice = discountPrice; } public int caculatePrice(){ } //.. }
- order객체는 위와 같은 필드를 가지고 있으며, 생성시 전달받은 할인 금액을 통해 최종 주문 금액 계산할 수 있음
public static void main(String[] args) { MemberService memberService = new MemberServcieimpl(); OrderService orderService=new OrderServiceimpl(); //멤버 생성 - 등록 Long memberId =1L; Member memberA = new Member(memberId, "memberA", Grade.VIP); memberService.join(memberA); //주문 생성 Order itemA = orderService.createOrder(memberId, "itemA", 10000); System.out.println("itemA = " + itemA); System.out.println(itemA.caculatePrice()); }
- 클라이언트는 위와 같이 주문을 생성할 수 있음
* 주문 서비스도 회원 서비스와 동일한 문제점이 있다.
* 객체 지향 설계에 잘 맞게 구현 했지만, DIP와 OCP를 지키기 위해서는 다형성만으로는 한계가 있다.
2. 구성 영역과 사용영역 분리
1. 구성영역을 통한 문제점 해결 (AppConfig)
- DIP의 문제점 (MemberService, OrderService) 추상타입이 아닌 콘크리트 타입에도 의존하는 문제점을 고쳐야한다.
- 또한 현 코드는 기능을 확장하려면, 클라이언트 코드를 수정해야한다.
- 현재 위와 같은 의존관계에서 할인 정책의 클라이언트인 OrderServiceimpl와 콘크리트 의존관계를 깨야한다.
- 이를 위해서는 OrderServiceimpl대신에 구현 객체를 주입해줘야한다.
1.1 **관심사의 분리
- 어플리케이션을 개발하는 것은 협력에 따라 적절하게 역할을 설정하고 책임을 분배하는 것이라 했다.
- 연극과 빗대본다면, 배역을 맡은 배우는 대본에 충실할 뿐 그 외에 책임은 없다.
- 배역에 실제 배우를 결정하거나 나머지 여러 일은 공연 기획자의 일이다.
- 서비스 객체는 로직에 맞는 서비스를 실행할 뿐, 실제 의존객체를 결정할 필요가 없다.
- 애플리케이션에도 연극과 같이 실제 배역을 설정해줄 기획자와 같은 객체가 필요하다.
1.2 Appconfig
- 애플리케이션 전체 동작 방식 구성을 위해 존재
- 구현 객체를 생성하고, 연결하는 책임을 별도로 가지는 설정 클래스이다.
public class Appconfig{ public MemberService memberService(){ return new MemberServiceImpl(new MemoryMemberRepository()); } //.. }
- 위처럼 Appconfig를 통해 MemberService를 생성하게 되면, 구현객체와 구현객체가 내부적으로 사용할 의존관계까지 주입할 수 있다.
public class MemberServiceImpl implements MemberService{ private final MemberRepository memberRepository; public MemberServiceImpl(MemberRepository memberRepository) { this.memberRepository = memberRepository; } //.. }
- 이를 바탕으로 MemberServiceImpl은 MemberRepository 인터페이스에만 의존한다.
- MemberServiceImpl입장에서는 생성자로 주입받기 때문에 실제 어떤 구현 객체가 주입될지는 알 수 없다.
- 이는 AppConfig의 역할이다. 의존관계 고민은 외부에 맞기고 실행에만 집중
- AppConfig를 통해 DIP가 만족되었다.
- 역할과 책임에 따라 클래스가 적절히 분리되었다.
- 위와 같은 새로운 구조에서 할인 정책은 변경하는 것은 매우 쉽다.
- 구성 영역만 수정한다면, 사용 영역의 코드는 전혀 수정할 필요 없이 변경 가능하다.
- 위와 같은 구조는 SRP, DIP, OCP에 잘 맞는다.
2. IoC,DI,컨테이너
제어의 역전 IoC : - 서비스 구현 객체가 필요 객체 생성,연결,실행 -> 프로그램 흐름 제어
- AppConfig와 같은 구성 객체 존재시 사용 영역 객체들은 로직 실행만 담당
- 프로그램 제어 흐름을 AppConfig가 가져감
- 위와 같이 각 서비스 구현 객체가 흐름을 직접 제어하는 것이 아니라, 외부에서 관리하는 것을
제어의 역전이라고 한다.
프레임워크 vs 라이브러리: 제어의 역전 유무 차이로 구분 가능
의존관계 주입 DI : - 런타임에 외부에서 실제 구현 객체 생성하고 클라이언트에 전달해서
서버와 클라이언트 실제 의존관계가 연결 되는 것을 의미한다.
- 의존관계를 주입하면, 클라이언트 코드를 변경하지 않고, 클라이언트가
호출하는 대상의 타입 인스턴스를 변경할 수 있다.
- 정적 클래스 의존관계 변경하지 않고, 동적 객체 인스턴스 의존관계 쉽게 변경 가능하다.
* 정적 클래스 의존관계와, 실행 시점 동적(인스턴스) 의존관계는 분리해서 생각하자.
정적 클래스 의존관계: import 코드만으로 쉽게 판단 가능, 실행 하지 않고 확인 가능 (ex 클래스 다이어그램)
동적 객체 인스턴스 의존관계: 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스 연결된 의존관계
(ex 객체 다이어그램)
2.1 IoC컨테이너, DI 컨테이너
- AppConfig처럼 객체 생성 관리하고, 의존관계 연결해주는 것을 IoC 혹은 DI컨테이너라 한다.
3. 스프링으로 전환
3.1 스프링 컨테이너에 Bean등록하기
- AppConfig에 메서드들을 스프링 컨테이너에 등록할 것이다.
@Configuration public class AppConfig { @Bean public MemberService memberService() { return new MemberServiceImpl(memberRepository()); } //.. }
- AppConfig가 설정을 구성한다는 뜻으로 @Configuration을 붙여준다.
- 각 메서드에 @Bean 어노테이션을 붙여준다 -> 스프링 컨테이너에 빈으로 등록
7.2 스프링 컨테이너 사용
public class MemberApp{ public static void main(String[] args){ ApplicationContext appcontext = new AnnotationConfigApplicationContext(AppConfig.class); MemberService memberService = appcontext.getBean("memberService", MemberService.class); //.. } } public class OrderApp { public static void main(String[] args) { 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이 붙은 메서드 명을 스프링 빈의 이름으로 사용한다.
- 스프링 컨테이너를 통해 필요한 스프링 빈(객체)를 getBean메서드를 통해 찾는다.
- 이제 직접 자바코드로 했던 것을 스프링 컨테이너에 객체를 스프링 빈으로 등록하여 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경 되었다.
-> 다형성만으로는 DIP와 OCP를 만족시킬 수 없다.
-> 따라서 의존관계를 주입 받는 방식으로 이를 해결하고자 하였다. (AppConfig, 구성과 사용 분리)
-> AppConfig와 같은 역할이 발전하여 이를 담당하는 IoC컨테이너-> 스프링 컨테이너가 등장
-> 즉 스프링 컨테이너는 기존 자바 코드로 할 수 없었던, DIP와 OCP를 만족시킴과 동시에 여러 편의 기능을 제공하기 위해 등장 (여기선 DIP랑 OCP 측면에서만 봤음)
참고자료: 스프링 핵심 원리 - 김영한
'Web > Spring' 카테고리의 다른 글
스프링 핵심 원리 (3) - 싱글톤 컨테이너 (0) 2023.08.17 스프링 핵심 원리 (2) - 스프링 컨테이너와 스프링 빈 (0) 2023.08.17 나프 - 게시판 만들기 (3) (0) 2023.08.03 나프 - 게시판 만들기(2) (0) 2023.08.02 나프 - 게시판 만들기(1) (0) 2023.08.02