일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 자유 프로젝트
- 231103
- BOJ
- MAX
- 백준
- 3주차 회고
- 테이크스트라
- JazzMeet
- 실패했지만성공했다
- 22년도
- 채팅목록조회
- 코드스쿼드max
- 재즈밋
- rotuter
- Map.of()
- NamedParameterJdbcTemplate
- 누구나 자료구조와 알고리즘
- Paths.get()
- 2023
- new File().toPath()
- Til
- Spring
- MapSqlParameterSource
- Python
- requested
- 코드스쿼드
- 회고
- 오류
- 파이썬
- baeldung
- Today
- Total
어제보다 한걸음 더
스프링 핵심 원리 - 기본편 (1) : 객체 지향에 대한 이해 본문
김영한님 강의 스프링 핵심 원리 - 기본편 을 보고 정리한 내용입니다.
제가 잘못 정리한 내용이 있을 수 있으니 발견 시 알려주시면 감사하겠습니다🙇🏻♀️
좋은 객체 지향 설계의 5가지 원칙 (SOLID)
1. SRP: 단일 책임 원칙 (Single Responsibility Principle)
= 한 클래스는 하나의 책임(= 변경의 이유)만 가져야 한다.
- 변경이 있을 때, 파급효과가 적으면 단일 책임 원칙을 잘 따른 것이다.
2. 🌟 OCP: 개방-폐쇄 원칙 (Open/Close Principle)
= 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
- 확장을 하기 위해서는 다형성을 이용한다.
- 하지만 변경에는 닫혀있을 수 있나 다형성을 이용 하더라도 클라이언트 쪽에서 코드를 변경해야하지 않나?
- 이 문제를 해결하기 위해서는: 객체를 생성하고, 연관 관계를 맺어주는 별도의 조립, 설정자가 필요하다. → spring 컨테이너의 역할. DI와 IoC 컨테이너가 필요하다.
✅ 그러면 어떻게 해야되지?
3. LSP 리스코프 치환 원칙 (Liskov Substitution Principle)
= 컴파일 단계의 오류를 내지 않는 것을 이야기 하는 것이 아니라, 인터페이스에 정해진 규약을 지켜야 한다.
- 기능의 보장.
- ex) 자동차의 엑셀 기능은 무조건 앞으로 가게 만들어야한다. 뒤로 가게 만들면 안된다.
4. ISP 인터페이스 분리 원칙 (Interface Segregation Principle)
= 하는 일에 맞게, 기능을 적당한 크기로 쪼개는 것이 중요하다.
- 기능(인터페이스)을 분리 해 놓으면 코드를 수정 할 때, 지정된 기능만 바꿀 수 있다. 다른 기능에 영향을 주지 않게 된다.
- 또한 인터페이스의 대체 가능성이 높아진다.
5. 🌟 DIP 의존관계 역전 원칙 (Dependency Inversion Principle)
= 추상화(인터페이스, 역할)에 의존해야지 구현(각 인스턴스)에 의존하면 안된다.
- 구현체에 의존하게 되면 변경이 아주 어려워진다.
- ex) 뮤지컬 배우들의 조합이 변경되면 공연을 못하게 되는 경우.
- 정보를 많이 알 수록 의존성이 높아진다.
ex) A = new B(); 하게 되면, A와 B 모두 알게 되므로 DIP 위반이라고 할 수 있다.
✅ 그러면 어떻게 해야되지?
정리
- 객체 지향의 핵심은 다형성이다.
- 하지만 다형성 만으로는
- 쉽게 부품을 갈아 끼우듯이 개발할 수 없다. (OCP 위반)
- 구현 객체를 변경 할 때 클라이언트 코드도 함께 변경될 수 밖에 없다. (DIP 위반)
✅ 그러면 어떻게 해야되지? → 무언가가 더 필요하다.
객체 지향 설계와 스프링
스프링은 다형성, OCP, DIP 가능하게 지원해주는 기술이다.
- 다형성: 쉽게 부품을 교체하듯이 개발
- OCP: 클라이언트 코드의 변경 없이 기능 확장
- DIP: DI 컨테이너 제공
- → DI (Dependency Injection): 의존관계, 의존성 주입
스프링이 없던 시절에 OCP, DIP 지키면서 코드를 짜려고 했더니 배보다 배꼽이 더 커지는 상황이 발생했다.
정리
- 모든 설계에 역할과 기능을 분리하자.(현실) 추상화 비용(코드를 더 들어가 봐야 함)이 발생하기 때문에 실질적으로 실무에서 사용하기는 어렵다.
- → 기능을 확장할 가능성이 없다면 구체 클래스를 직접 사용하고 추후에 리팩터링 해서 인터페이스를 도입하는 것도 방법이다.
- (이상) 모든 설계에 인터페이스를 부여하자.
- 스프링 컨테이너는 다형성, OCP, DIP를 가능하게 해준다.
도메인 설계
- https://start.spring.io/ 에서 spring 프로젝트 생성.
- 생성된 프로젝트로 IDE에 접속 후 gradle 설정
- 도메인 설계
- 도메인 협력 관계 : 기획자들도 볼 수 있는 설계
- 클래스 다이어그램: 클래스들의 협력 관계들을 볼 수 있는 설계 (정적인 의존관계)
- 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다.
- 애플리케이션을 실행하지 않아도 분석할 수 있다.
- 객체 다이어그램: 실제 사용되는 구현체들의 관계를 볼 수 있는 설계 (동적인 의존관계)
- 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계
도메인 개발
- 예제이기 때문에 예외처리 제외
- MemoryMemberRepository 클래스의 private static Map<Long, Member> store = new HashMap<>(); 부분은 동시성 이슈 때문에 원래는 concurrent HashMap을 써야하지만 너무 설명이 길어지므로 그냥 이렇게 사용.
- MemberServiceImpl 클래스명의 Impl 부분은, 인터페이스의 구현체가 하나 일 경우 관례상 클래스 명 뒤에 Impl 을 많이 붙인다.
테스트
JUnit 사용해서 테스트 코드 작성.
- test 폴더 안에 테스트 할 파일 만들기
- @Test 해서 JUnit import 하기
- 테스트 할 메서드 안에 given, when, then 세가지의 주석을 달아놓기
- (given이 주어졌을 때, when 이렇게 하면, then 이렇게 된다)
- 테스트 코드 작성을 main()에서 직접 작성하는 것 보다 JUnit을 사용하는 것이 더 좋은 이유
- main(): 출력되는 테스트가 동일한지 아닌지 눈으로 직접 확인을 해야한다.
- JUnit: 여러가지 테스트를 실행했을 때, 어디에서 오류가 나는지 빨리 캐치가 가능하다.
현재까지 작성한 코드 설계의 문제점
OCP, DIP 가 지켜지지 않는다.
// 예시
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
}
OCP: 다른 클래스(기능)를 선택하려고 하면 클라이언트 클래스에서 코드를 직접 변경해야 함.
ex) MemberRepository에서 다른 기능으로 바꾸려고 하면 MemberServiceImpl(클라이언트) 클래스의 코드를 직접 변경해야 함.
DIP: interface 클래스와 상속받는 클래스 모두를 알고 있음.
ex) MemberServiceImpl(클라이언트) 클래스는 MemberRepository(인터페이스, 추상화)와 MemoryMemberRepository(구현체, 구체화) 모두를 알고 있음.
주문과 할인 도메인 설계, 개발
협력 관계 그대로 유지 가능 (OCP)
- 객체 지향의 사실과 오해(책) 의 메인 주제: 역할들의 협력 관계를 그대로 재사용 할 수 있다.
단위 테스트 만드는건 정말 중요하다
test 패키지에 기본으로 들어가있는 CoreApplicationTests 클래스의 @SpringBootTest는, 스프링 라이브러리 및 JPA 엄청 들어가면 테스트 하는데 시간이 오래걸림.
@Test 로 하면 정말 빠름.
단위 테스트: Spring 및 컨테이너의 도움 없이, 순수하게 자바 코드로 테스트 하는 것을 말함.
성공 테스트도 중요하지만 실패 테스트도 만들어봐야한다.
OCP, DIP 해결 방법
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
⬇
private DiscountPolicy discountPolicy;
}
구체에 의존하지 않고 추상화에만 의존하게 된다. → OCP, DIP 해결!
but, 테스트 코드로 작성했던 것들이 NullPointException가 발생하게 된다. (사실상 DIP만 해결 됨)
🌟 진짜 해결 방법: 누군가가 OrderServiceImpl(클라이언트) 에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해줘야한다.
OrderServiceImpl 클래스는 FixDiscountPolicy 를 직접 선택하므로써 주어진 로직을 수행하는 하나의 책임 외의 클래스 선택의 책임도 가지게 된다. 그러므로 클래스 선택의 책임을 다른 클래스에 분리해야한다.
🌟 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스를 만들어야 함. (= AppConfig 클래스)
🌟AppConfig 클래스
- 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
- 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결) 해준다.
= 객체의 생성과 연결은 AppConfig 클래스가 담당하게 됨.
- DIP 완성: MemberServiceImpl는 MemberRepository인 추상에만 의존하면 된다. 이제 구체 클래스를 몰라도 된다.
- 관심사의 분리 : 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.
@BeforeEach : 각 테스트 코드가 실행되기 전에 무조건 실행되는 것.
테스트 코드(메서드)가 n개 있으면 @BeforeEach 안에 있는 코드는 n번 돈다.
AppConfig 클래스
역할을 드러내게 하는 것이 중요하다.
new 여러번 했던 코드의 중복 제거, 한눈에 역할이 들어오게 리팩터링.
AppConfig의 등장으로 애플리케이션이 크게 1.사용영역(AppConfig외의 전부)과 2.구성영역(configuration)으로 분리되었다.
IoC, DI, 컨테이너
IoC 제어 역전 Inversion of Controll :
- 프레임워크 같은 것이 내 코드를 대신 호출해 주는 것.
- 프로그램의 제어 흐름을(생성)을 클라이언트 클래스에서 관리하는 것이 아닌, 외부에서 관리해 주는 것.
프레임워크 vs 라이브러리
프레임워크: 프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 프레임워크이다. (JUnit)
자신만의 라이프 사이클(실행 순서)가 있어서 알아서 적절한 타이밍에 호출이 된다.
라이브러리: 내가 작성한 코드가 직접 제어의 흐름을 담당한다. (xml, json 바꾸는 라이브러리)
클래스 다이어그램: 정적인 의존관계
객체 다이어그램: 동적인 의존관계
예시의 AppConfig 클래스 처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너, 또는 DI 컨테이너라고 한다.
IoC는 너무 추상적이고, 최근에는 의존관계 주입에 초점을 맞춰서 주로 DI 컨테이너라고 부른다.
- 제어권이 넘어가는 것을 IoC 라고 한다.
조립한다는 의미로 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.
스프링으로 전환하기
@Configuration : 설정, 구성 정보를 저장하는 클래스라는 의미이다.
@Bean: 스프링 컨테이너에 등록이 된다. 각 메서드에 적어준다.
ApplicationContext 클래스: 스프링 컨테이너. 스프링의 모든 것은 이걸로 시작한다.
ApplicationContext applicationContext = new AnnotiationConfigApplicationContext(설정정보를담고있는클래스명.class);
- 클래스의 생성자를 담고있던 변수를 제거하는 대신, 위의 코드를 입력한다.
- 이전에는 주입 될 클래스를 직접 찾아왔다면 이제는 스프링 컨테이너에서 찾아오면 된다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
}
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
⬇
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
applicationContext.getBean("memberService", MemberService.class);
}
스프링 컨테이너
- 사용하는 클래스에 @Configuration을 붙여 스프링 컨테이너로 만든다.
- ApplicationContext 를 호출해서 사용 가능하다.
- @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
스프링 Bean
- 스프링 컨테이너에 등록 된 객체
- @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다.
- (메서드 이름, 클래스 타입); → key(메서드 이름):value(클래스 타입) 형태로 저장된다.
- getBean(메서드 이름, 클래스 타입) 을 이용해서 찾을 수 있다.
✅ 코드가 더 복잡해진 것 같은데 스프링 컨테이너를 왜 쓰는걸까? 장점이 뭘까?
'Spring' 카테고리의 다른 글
new File().toPath()와 Paths.get()의 차이 (0) | 2023.05.22 |
---|---|
Spring Boot - DTO, VO, DAO, Repository, Entity, Domain 란? (2) | 2023.04.24 |
Spring - Mysql 연동 시 cannot resolve class or package 'mysql' 오류 해결 (0) | 2023.04.20 |
SpringBoot로 h2 DB와 연동해서 데이터 테이블 저장하기 (1) | 2023.04.08 |
로컬 호스트(8080)가 이미 사용중일 때 오류 해결 방법 (0) | 2023.04.03 |