|  소프트웨어 공학의 필요성

- 소프트웨어 공학이란? 시스템 초기부터 오픈 후 유지/보수까지의 소프트웨어 개발과 관련된 모든 측면을 의미

- 왜 필요한가?

  (1) 비용의 문제 (개발 60% + 테스트 40%) : 계획 없는 개발은 추가 비용을 발생시킨다.

  (2) 시스템 복잡도 향상 : 점차 복잡해지는 시스템 속에서 방향성을 잡는 것이 필요해졌다.

- 소프트웨어의 종류

Generic products 어떤 기능을 수행할지 개발자의 결정에 의해 만들어진 소프트웨어
Customized products 특정 고객의 요구사항에 맞추어 개발되는 소프트웨어

- 좋은 소프트웨어란? 고객에게 필요한 기능과 성능을 제공하는 소프트웨어

  > "높은 다양성과, 믿을 수 있는 소프트웨어"

  > 좋은 소프트웨어가 꼭 가져야하는 특성 : 유지가능성, 신뢰성과 보안, 효율성, 접근가능성(쉽고 단순해야)

 

|  소프트웨어 프로세스 정형화

1. 전통적인 방식 

- 고객/사용자(Client)/플젝 관리자(PM)/개발자

- SI의 방식과 비슷하다.

- 요구사항 > 요구명세 & 요구분석 > 설계 > 구현 > 테스트 > 납품/유지보수

단계 내용
요구사항 고객의 요구 사항 (Pain point + scope + benefit)
요구명세 & 분석 실제 구현을 위한 상세한 기능 분석 

- 요구사항에 대한 구체화를 위한 유도, 분석, 기록
- 기능 레벨, 필요 시스템, 테스트 요구사항,
  외부 시스템과의 연결 요구사항, 제약 사항
설계(Design) 세부적인 구현 레벨의 설계 (절차지향 / 객체지향)

- 객체지향 : Use Case Diagram, Class Diagram, Sequence Diagram

* Use Case는 사용자가 여러명이 있을 때 유용
* Class Diagram은 Class간 관계를 구체화할 때 유용
* Sequence Diagram은 각 Class간 호출 관계 간단히 표현
구현(Implemenation) Project Management로 각 개발 단계 세부화(WBS) 

* 과거에는 MS Project 툴을 사용했다고 한다.
테스트 QA라고도 하며 발생할 수 있는 버그 사항들에 대해 테스트
오픈(Release) 여러 버전의 사전 오픈에서 실제 오픈에 이르기까지

- pre-alpha, alpha, beta, RC(release candidate), official release
유지보수 오픈 후 운영 시 개선이 필요한 사항에 대해 하나씩 개선

1-1. 고전적인 개발 방법론

  폭포수 모델 프로토타입 모델 나선형 모델
내용 순차적인 단계 간단한 프로토타입을 만든 후 수정 작은 단위의 task로 점진적 개발
장점 사전 설계가 세부적일 때 유용 폭포수 보다 요구사항 도출 용이 프로토타입 보다 요구사항 파악 쉬움
위험을 최소화할 수 있음
대규모 시스템에 유리
단점 변화에 유동적이지 않음
일정 산출의 어려움
프로토타입을 완제품으로 오해
프로토타입 제작의 비효율성
개발 시간이 오래 소요
시스템이 복잡해 관리가 어려움
img

* 출처 : 나무위키

* 이미지 출처 : https://m.blog.naver.com/PostView.nhn?isHttpsRedirect=true&blogId=seilius&logNo=130185596031 

 

 

[ 완벽한 기획이 실제로 가능할까 (더보기) ]

- 고객의 요구사항이 코딩 테스트 문제처럼 세부적일 거란 기대는 하지 않는 게 좋다.

처음부터 개발자를 만족시킬 수 있는 기획이 있을까? 그럼 처음부터 고객을 만족시킬 수 있는 개발물은?

더보기

[ 완벽한 기획이 실제로 가능할까 ]

- 처음부터 개발자를 만족시킬 수 있는 기획이 있을까? 그럼 처음부터 고객을 만족시킬 수 있는 개발물은?

  - 사용자의 요구사항 + 고객의 요구사항 + 디자이너의 요구사항 + 개발자의 요구사항?

- 완벽한 고객의 요구사항이란 존재할 수가 없다.

- 그러니 완벽한 분석 또한 어려운 게 사실이다.

  - 고객의 계속해서 바뀌는 요구사항. 

  - 갑작스런 내외부 변동 사항 (ex. 내부적인 오류, 라이브러리 보안 이슈 등)

- 기획을 무시하기 보다, 

 

2. 오늘날의 개발 방식

- 최신 서비스 특성 : Mobile First, 웹 + 모바일의 서비스 개발, 클라우드 컴퓨팅 기반, 개인정보 보호 강화

- 최신 서비스 개발 Trend :  빠르게 micro 단위로 순차적인 개발을 한다.

   > 새로운 기술, Micro Features

   > 고객 경험을 모니터링 - micro단위로 AB테스트, 고객 로그를 활용

 

2-1. 최신 소프트웨어 업계 개발방식

  방식 내용
제공 환경 다양 반응형 웹, 하이브리드 앱 등
개발 방법론


Agile 고객 중심, 변화에 유연한, 협력적인 개발 문화

- Agile 툴 : Jira
CI 지속적 통합, Continuous Integration.
지속적으로 품질 관리를 적용하는 프로세스를 실행하는 것
 
- CI 툴 : Jenkins
TDD 테스트 주도 개발, Test Driven Development
테스트를 먼저 만들고 테스트를 통과하기 위한 것을 개발한다.
버전 관리 Git, Github  
유지 보수 / 운영 자동화 DevOps 개발팀 + 운영팀이 합쳐진 형태의 새로운 조직

 

Agile이란?

개발과 함께 즉시 피드백을 받아서 유동적으로 개발하는 방법

* 출처 : 나무위키

(1) Agile 선언문

애자일 소프트웨어 개발 선언
우리는 소프트웨어를 개발하고, 또 다른 사람의 개발을 도와주면서 소프트웨어 개발의 더 나은 방법들을 찾아가고 있다.
이 작업을 통해 우리는 다음을 가치 있게 여기게 되었다:

공정과 도구보다 개인과 상호작용
포괄적인 문서보다 작동하는 소프트웨어
계약 협상보다 고객과의 협력
계획을 따르기보다 변화에 대응하기

가치 있게 여긴다.
이 말은, 왼쪽에 있는 것들도 가치가 있지만, 우리는 오른쪽에 있는 것들에 더 높은 가치를 둔다는 것이다.

(2) Scrum 프로세스

* Scrum이란? 순차적이고 점진적인 에자일 방식의 개발 과정

1️⃣ Scrum 팀 구성 및 미팅
2️⃣ Sprint 계획 & 진행
3️⃣ Daily Scrum으로 진척사항/변동사항을 확인 후 공유
4️⃣ Sprint 끝나면 진행한 것 리뷰/반성

출처:https://velog.io/@katanazero86/%EC%95%A0%EC%9E%90%EC%9D%BCagile%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80

 

1️⃣ Scrum 팀 구성 : 일반적으로 10명 안밖으로 구성된다.

👥Product Owner(Manager) + Scrum Master + Senior Development Manager + Developer + .. (10명 가량)

출처:https://www.visual-paradigm.com/scrum/what-is-scrum-team/

2️⃣ Sprint 계획(1달, 2주) & 진행 

- Sprint : Epic(Story) - Tickets(Backlog) - Priorization - Dev Estimation - Test - Release

- Product Backlog 예시 이미지

SprintBacklog예시_출처:https://itwiki.kr/w/%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8_%EB%B0%B1%EB%A1%9C%EA%B7%B8


3️⃣ Daily Scrum으로 진척사항/변동사항을 확인 후 공유

4️⃣ Sprint 끝나면 진행한 것 리뷰/반성

 

DevOps란?

- 소프트웨어의 개발(Development)과 운영(Operations)의 합성어로 최근에 새롭게 나타난 조직명이다.

- 에자일 방식의 경우 작은 단위로 서비스를 오픈한다.(Micro Feature Release)
- 새로운 기능이 릴리즈될 때 개발팀에서 운영팀에 어떻게 운영할지를 알려주어야 한다.
- 그러나 고전적인 기업 조직 구조에서와 같이 개발팀과 운영팀이 별도로 나뉘어져 있다면, 

- 부서가 분리되어 있으니 개발팀에서 운영팀에 최근 오픈된 서비스 사항을 제대로 공유하지 않게 되는데
- 그러다 특정 서비스를 오픈하다 이슈가 발생해 다운 현상이 일어나면 개발을 한 개발자가 blame을 받게 된다.
- 그러니 운영팀은 기존 서비스를 유지하고 신규 오픈은 막으려 하고, 개발팀도 되도록 신규 서비스 개발은 지양하게 된다.

- 이런 현상이 반복되면 결국 고객의 니즈에 맞춰가지 못하는 서비스가 되어 도태되는 현상 발생

- 역할 : Release System의 자동화, 코드 리뷰 및 테스트 자동화, 서비스 모니터링, 이슈 발생 시 커뮤니케이션 시스템

 

스타트업과 린 스타트업

- [린 스타트업], Eric Ries 

" 승리할 수 있는 유일한 방법은 다른 누구보다 빨리 배우는 것이다 "

- 스타트업이란? 극심한 불확실성 속에서 신규 제품이나 서비스를 만들고자 하는 조직

 

|  정리

✅ 소프트웨어 공학은 소프트웨어 개발 과정 전반에 대한 이론을 말한다. 
✅ 전통적인 소프트웨어 개발 방법론으로는 폭포수, 프로토타입, 나선형이 있다.
✅ 오늘날의 소프트웨어는 (스타트업 기준)애자일 방식을 사용하며, 유지/보수에 관하여는 DevOps조직이 새롭게 생성되었다.
✅ 애자일은 개발 즉시 피드백을 받아 빠르고 유동적으로 개발하는 방법을 말한다.

 

 

[ 참조 및 출처 ]

부트캠프 강의를 들은 후 기타 자료와 함께 정리한 내용입니다.

[Software Engineering] 소프트웨어 공학이란?, 소프트웨어 공학의 필요성, 좋은 소프트웨어의 특성

https://velog.io/@katanazero86/%EC%95%A0%EC%9E%90%EC%9D%BCagile%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80

TDD란? https://gmlwjd9405.github.io/2018/06/03/agile-tdd.html

|  개요

- 스프링은 SOLID 원칙을 담은 아키텍쳐(설계도)에 따라 만든 프레임워크이다.

- 웹 개발 기술의 발전 : HTML -> CGI -> Sevlet/JSP(EJB) -> Spring MVC -> node, ktor 등 경량 웹 프레임워크

- 스프링에 대한 주요 기술은 아래와 같다.

코어 DI, IoC, 컨테이너
Resource, AOP, Validation, SpEL
MVC Web MVC, HTTP 요청/응답처리, 필터와 인터셉터, 예외처리
더보기

[ 라이브러리와 프레임워크의 차이점 ]

- 프레임워크가 개발을 하기 위한 전체적인 뼈대/틀이라면,

- 라이브러리는 특수한 기능에 대한 도구, 기능의 집합을 말한다.

- 프레임워크와 라이브러리의 가장 큰 차이는, 흐름을 제어하는 것이 누구이냐인데,

  프레임워크에서는 IoC(Inversion of Control) 곧, 제어의 역전이라 하여 프레임워크 자체가 흐름을 통제한다.

  반면, 라이브러리의 경우, 흐름을 작성하는 사용자(개발자)에 따라 그 방향성이 달라질 수 있다.

 

[ SOLID 원칙 ]

- SRP  : 단일 책임 원칙

- OCP : 개방 폐쇄 원칙 (인터페이스를 통한 사용 기능의 교체 가능성)

- LSP  : 리스코프 치환 원칙 (상위 -> 하위 상속된 메소드는 동일 기능이여야한다.)

- ISP   : 인터페이스 분리 원칙 (인터페이스도 단일 책임을 져야한다)

- DIP  : 의존성 역전 원칙 (하위의 변경이 상위의 변경까지 요구하지 않도록 인터페이스를 통해 의존성을 끊어버린다)

 

|  스프링의 코어(핵심)

1. DI, IoC, 컨테이너

- 스프링에서는 컨테이너라는 공간 안에 클래스들을 규격화된 Bean형태로 저장한다.

* 수업에서는 컨테이너를 레고판으로, Bean을 레고에 비유했다.

- 이렇게 규격화된 방식을 사용하게 되면 프로젝트의 개발자가 바뀌어도 작업이 원활하게 이루어질 수 있다.

(1) DI(Dependency Injection) - 의존성 주입

✅ DI 곧, 의존성 주입은 A와 B의 의존 관계를 외부에서 주입해주는 걸 말한다.

(2) IoC(Inversion Of Control) - 제어의 역전

✅ IoC 곧, 제어의 역전이란, 사용자가 클래스를 생성(제어)하지 않고 스프링 프레임워크가 제어하게 하는 것이다.

(3) DI 설정 방법의 역사

✅ DI 설정 방법의 역사
1️⃣ XML을 통한 Bean 등록
2️⃣ XML ComponentScan을 통한 Bean 등록
3️⃣ JavaConfig Class를 통한 Bean 등록
4️⃣ JavaConfig Class의 ComponentScan를 통한 Bean 등록

 

- DI 설정방법은 XML을 통할 수도 있고, JavaConfig 클래스를 통할 수도 있는데 후자를 많이 사용한다.

- JavaConfig를 사용하는 방법만 보면, 아래와 같이 @Configuration을 통해 이 클래스가 Di config임을 알려주고,

  내부에 사용하려는 각 클래스의 구현체를 @Bean을 통해 전달하는 코드를 담는다.

@Configuration
public class ApplicationConfig{
    @Bean
    public MyService myService(){
    	return new MyService();
    }
}

- 클래스를 Bean으로 등록할 때에는 일일히 @Bean을 치지 않고 각 클래스에 @component를 하면 Bean으로 등록된다.

- 거기에 플러스로 config 파일에 @ComponentScan(//생략)을 추가하면 스프링이 알아서 의존 관계를 찾아준다.

@Configuration
@ComponentScan(basePackages = "com.sample.myApplication")  // 패키지 기준으로 탐색
public class ApplicationConfig{
    
}

// 또는

@Configuration
@ComponentScan(basePackageClasses = MyApplication.class)  // 메인 클래스 기준으로 탐색
public class ApplicationConfig{
    
}

 

- main함수에서 설정을 호출할때

// DI config 호출
// 1. xml을 사용할 경우
ApplicationContext applicationContext =
        new ClassPathXmlApplicationContext("spring-config.xml");
// 2. config클래스를 사용할 경우
ApplicationContext applicationContext =
        new AnnotationConfigApplicationContext(ApplicationConfig.class);

 

[ Bean 설정 방법 - 더보기 ]

구분 설정 방법 설명
구현체 지정 @Primary 구현체에 직접 지정
@Qualifier("beanName") 사용자인 클래스의 생성자 매개변수에 구현체 지정
Set이나 List로 모두 받기 사용자인 클래스의 생성자 매개변수에 Set, List로 지정
프로퍼티 이름을 Bean과 동일하게 ex. PayByCard --> payByCard
Scope 싱글톤 default, 한 번 만들어 계속 사용
Prototype Request, Session, WebSocket
Profile @Profile("!production") 여러 환경(test, stage, open)에서만 동작하는 Bean을 만들 때
클래스 단위나 메소드 단위에 사용 가능
- Dspring.profiles.active = sandbox, beta, production
대체로 !, &, | 와 함께 사용
더보기

[ Bean 설정 방법 ]

[1] Bean의 구현체가 여러 개인 경우

(1) @Primary : 구현체에 직접 Primary를 단다.

(2) @Qualifer : 서비스 생성자의 매개변수에 자격 요건을 단다. 

@Component
public class ConveniencePayService { // 편결이

    private final Map<PayMethodType, PaymentInterface> paymentInterfaceMap
            = new HashMap<>();
    private final DiscountInterface discountInterface;

    // 생성자를 통해 결제수단의 종류를 Map으로 저장
    public ConveniencePayService(Set<PaymentInterface> paymentInterfaceSet,
    							 @Qualifier("paymentByMethod")
                                 DiscountInterface discountInterface) {
        paymentInterfaceSet.forEach(
                paymentInterface -> paymentInterfaceMap.put(
                        paymentInterface.getPayMethodType(),
                        paymentInterface
                )
        );
        this.discountInterface = discountInterface;
    }
}

(3) Set 또는 List로 모두 받는다.

(4) 프로퍼티 이름을 Bean과 동일하게 한다.

@Component
public class ConveniencePayService { // 편결이

    private final Map<PayMethodType, PaymentInterface> paymentInterfaceMap
            = new HashMap<>();
    private final DiscountInterface discountInterface;

    // 생성자를 통해 결제수단의 종류를 Map으로 저장
    public ConveniencePayService(Set<PaymentInterface> paymentInterfaceSet,
    							 DiscountInterface discountByPayMethod) {
        paymentInterfaceSet.forEach(
                paymentInterface -> paymentInterfaceMap.put(
                        paymentInterface.getPayMethodType(),
                        paymentInterface
                )
        );
        this.discountInterface = discountByPayMethod;
    }
}

 

[2] Scope설정하기

- 싱글톤 : default설정(아무것도 설정X시)

- Prototype : 데이터를 클렌징할 때 사용하며, 매번 새로 Bean을 생성한다.

ㄴ Request/Session/Web Socket

 

[3] Profile

- 인텔리j의 경우, 실행버튼 좌측에 셀렉박스 선택 - [Edit Configuration...] 에서 개발 환경변수를 입력할 수 있고

- 쉘에 직접 칠 경우, - Dspring.profiles.active = sandbox, beta, production 로 개발 환경변수를 입력할 수 있다.

- 특정 환경에서만 사용할 클래스에 @Profile("test")와 같이 작성하면 된다.

@Component
@Profile("test")
public class DiscountByConvenience implements DiscountInterface{
}

 

2.  스프링의 부가기능 - Resource, AOP, Validation&Data Binding, SpEL

(1) Resource(외부 자원)

✅ 외부자원은 외부 sftp, http, 파일 등에서 얻는다. (pf. sftp는 ssh의 ftp 버전)
✅ Resource는 인터페이스며 다양한 구현체를 갖고 있다.
✅ ApplicationContext는 ResourceLoader를 상속하며 프로그램이 시작할 때 Resource는 자동 로딩된다.
✅ 단, 추가적으로 파일을 로딩해야하는 경우 @Service와 @Autowired를 통해 직접 로딩할 수 있다.
✅ ApplicationContext는 ResourcePatternResolver도 상속하는데, 위치 지정자 패턴(classPath, file, http)에 따라 Resource 구현체가 자동 선택된다.
인터페이스 구현체 메소드
Resource(외부자원 가져오기) UrlResource
ClassPathResource
FileSystemResource
ServletContextResource
InputStreamResource
ByteArrayResource
exists()
isReadable()
isOpen()
getFile().....

- 추가 파일 로딩

@Service
public calss ResourceService{
    @AutoWired
    ApplicationContext ctx;
    
    public void setResource(){
    	Resource myTemplate = ctx.getResource("classPath:some/resource/path/myTemplate.txt");
    }
}

- ApplicationContext(스프링의 핵심설정)을 이루는 설정값을 가져오기

// 스프링의 핵심설정을 이루는 설정값을 가져오기
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");
ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml");
ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");

Bear bear = (Bear) ctx.getBean("bear");

 

(2) AOP(Aspect Orient Programing, 관점지향 프로그래밍)

✅ 단순 자바 OOP로는 반복적인 메소드를 관리하기 어려워, AOP를 사용하게 되었다.
✅ AOP로 공통 관심사(로깅, 트랜잭션, 인증)를 여러 메소드 Before/After/Around에 원할 때마다 쉽게 추가할 수 있다.
✅ AOP의 기본 개념
- [ Aspect(관심사) : Pointcut(포인트 선택 방법 - 조건식) + Advice(조언) ]
- Join point(연결 포인트) : 프로세스 내의 삽입 지점
- AOP proxy : target object에 advice를 덧붙이기 위해 하는 작업
- Weaving : advice를 비즈니스 로직 코드에 삽입하는 것
✅ AOP는 AspectJ 라이브러리를 사용해야 제대로 쓸 수 있다.

* 로깅/트랜잭션이란?

- 간단히 말해 로깅은 로그(log) 곧, 기록을 남기는 것을 말하고, 

- 트랜잭션이란 DB관련 작업 단위를 말하는 것으로 DB와의 작업을 한다는 의미이다.

  OOP AOP
포커스 객체 간의 상호작용 관점의 핵심과 부가지점
사용 방식 객체를 독립적으로 사용하거나 부품처럼 결합 관점에 따라 공통 로직의 객체들을 하나로 모듈화

 

- AOP 예시 코드

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect    // Aspect 생성
@Component // Aspect Class를 스프링 Bean으로 등록
public class CertainAspect{
    @PointCut("execution(public * *(..))") // public 대상 PointCut선언
    private void certainAdviceA(){
    	// 조건식 생성
    }
    
    @PointCut("within(com.xyz.myapp.trading..*)") // 특정 패키지 대상 PointCut선언
    private void certainAdviceB(){
    	// 조건식 생성
    }
    
    @PointCut("certainAdviceA() && certainAdviceB()") // PointCut결합
    private void mergeAdvice(){
    	// 조건식 생성
    }
}

- 미리 정의된 포인트컷의 before/after/around에 advice실행

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect    // Aspect 생성
public class CertainAspect{
    @Before("com.xyz.myapp.certainPointCutMethod()")
    private void certainAdvice(){
    	
    }
    
    @AfterReturning("com.xyz.myapp.certainPointCutMethod()")
    private void certainAdvice(){
    	
    }
    
    @Around("com.xyz.myapp.certainPointCutMethod()")
    private void certainAdvice(){
    	
    }
}

 

(3) Validation & Data Binding

- Validation

✅ Validation은 유효성 검증을 말하며, 주로 HTTP Request에서 잘못된 내용을 검증할 때 사용한다.
- 데이터 검증     : 필수 데이터 / 문자열 길이 및 숫자형 데이터 범위 / 이메일 및 신용카드 번호 등 형식 확인
- 비즈니스 검증 : 서비스 정책에 따라 데이터 검증
✅ Validation 방식
1) Java Bean Validation : dto클래스 맴버에 Annotaion(ex. @NotBlank, @Size, @Email...)을 붙이는 방식
2) Spring validator 인터페이스 구현을 통한 validation 
✅ Validation 주의사항 : Validation을 멀리 두면 테스트 및 유지보수성이 떨어지므로 가능한 Annotaion방식을 쓰는 게 좋다.
* 수업에서의 제안 : 1차로 dto에 Annotation방식 사용, 2차로 비즈니스 검증 후 실패 시 Custom Exception throw

- Data Binding

✅ Data Binding이란 요청 데이터를 특정 도메인 객체에 저장해 프로그램 Request에 담아주는 걸 말한다.
>> ex. 어떤 데이터를 특정 dto객체로 변환하고 싶을 때
✅ Data Binding 방식
1) Converter<S, T> interface를 구현한 클래스를 bean으로 등록해 사용
2) Formatter<T> interface를 구현한 클래스를 통해 특정 객체를 String, String를 특정 객체로 변환

 

(4) 스프링 표현 언어 (SpEL)

✅ 표현언어(EL)는 간단한 문법으로 필요한 데이터 및 설정값을 얻을 수 있게 하는 언어를 말한다.

* 이 부분은 차후에 더 정리할 예정.

 

|  MVC

- MVC란? Model + View + Controller 의 약자

  Model View Controller
내용 로직 내 이동 중인 데이터
(pf. 객체)
보여지는 화면 비즈니스 로직에 따라
처리하는 처리자
명칭 VO(value object),
DTO(data transfer object)
- -

- 최근에는 일반적으로 Request와 Response 모두 JSON 타입을 사용한다.

- Spring MVC의 흐름

출처:https://codingnotes.tistory.com/28

[1] Dispatcher Servlet이 요청 URL을 받아 HandlerMapping에 전달  * Dispatch : 파견 보내다. 
rf. 도서관의 사서(dispatcher)는 도서를 요청(request)받았고, 도서 코드 목록(HalderMapping)서 도서를 어떻게 찾을지 본다.

[2] HandlerMapping는 요청 URL에 맞는 Controller와 Method 정보를 반환
rf. 도서의 코드 목록에 따르면 해당 도서는 '역사' 카테고리 구역에 있단다.

[3] Dispatcher Servlet이 Handler Adapter에게 요청 처리를 위임  
rf. 사서는 인턴에게 '역사' 카테고리 책장으로 이동해서, 가나다순으로 진열된 책들을 보면 된다고 알려준다.

[4] HandlerAdapter가 Controller와 Method를 실행
rf. 인턴이 찾으려는 도서의 코드와, 사서의 안내를 가지고 '역사' 구역에 가서 도서를 찾는다.

[5] Cotroller는 비즈니스 로직을 처리하고, 그 결과를 바탕으로 뷰(ex.JSP)에 전달할 객체를 Model 객체에 저장
rf. 인턴은 찾은 도서를 가져와 책수레에 담는다.

[6] Dispatcher Servlet은 view name을 View Resolver에 전달하여 View 객체를 얻음
rf. 사서는 인턴으로부터 "00책 가져왔어?"라고 물어보고, 인턴은 "00책 여기있어요."하고 책수레에 있던 도서를 전달한다.

[7] Dispatcher Servlet은 View 객체에 화면 표시를 의뢰
rf. 사서는 책을 요청한 사람에게 책을 전달하기 위해 "00책 요청하신분~?"하고 물어본다. 

[8] View 객체는 해당하는 뷰(ex.JSP.Thymeleaf)를 호출하며,
    뷰는 Model 객체에서 화면 표시에 필요한 객체를 가져와 화면 표시를 처리
rf. 사서는 책을 요청한 사람에게 책을 대여해주고, 반납일에 대해 안내한다.

 

 

|  스프링 프로젝트 생성하기

1. 프로젝트 생성하기

[1] 인텔리J를 통해 바로 Gradle프로젝트 형성 가능
[2] spring initializer를 통한 프로젝트 생성

[1] https://start.spring.io/ 에서 프로젝트를 하나 만들어 생성 

 

- 압축파일 해제한 뒤 IDE에 가져와 Build를 보면 아래와 같은데, 알아서 기본 세팅을 해준 걸 볼 수 있다.

/* 플러그인의 의존성(library) 관리 */
plugins {
	id 'org.springframework.boot' version '2.7.3'
	id 'io.spring.dependency-management' version '1.0.13.RELEASE'
	id 'java'
}

group = 'com.zerobase'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

/* 각종 의존성들(libraries)을 어떤 원격 저장소에서 받을 것인지 지정 */
repositories {
	mavenCentral() // jcenter로 업로드 설정을 간소화할 수 있다.
}

/* 프로젝트 개발에 필요한 의존성들을 선언하는 곳 */
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}
더보기

[ Maven Project와 Gradle Project ]

* Gradle이 Maven에 비해 최대 100배가 빠르다. (Maven vs Gradle 바로가기)

- Maven Project : xml 기반

- Gradle Project : 그루비 기반

 

[2] 인텔리j를 통해 바로 프로젝트 생성

 

[ 정리 ]


✅ 스프링 프로젝트를 생성할 때에는 spring.io 홈페이지나 IDE를 통해 생성할 수 있다.

Bean 등록 : 각 클래스에  @Component를 쓰면 별도 등록이 필요 없다.

✅ DI Container에 Config 설정하기 : 
     - config클래스에서 @confuration을 통해 각 클래스간 의존 관계를 DI(의존성 주입)하고,
     - @componentScan을 통해 IoC(제어의 역전) - 자동으로 의존 관계 설정 - 을 한다.

✅ Resource는 ApplicationContext 시작 시 ResourceLoader에 의해 자동 업로드되며, 별도 업로드 또한 가능하다.

✅ AOP는 관점 지향 프로그래밍으로 로깅, 트랜잭션, 인증 등의 반복적인 처리가 필요할 때 AspectJ lib를 통해 구현한다.

✅ Validation은 HTTP request에 대한 데이터 유효성 검증 또는 비즈니스 로직 검증을 하는 것으로,
      1차적으로는 dto bean에 Annotation방식으로 데이터 유효성 검증 후,
      2차적으로는 validator를 구현한 bean을 통해 비즈니스 로직을 검증하는 방식을 사용할 수 있다.

✅ Data-Binding이란 HTTP request로 받은 데이터를 특정 데이터 폼으로 변경하는 것을 말하는 것으로,
      Converter나 Formatter 인터페이스를 구현한 Bean을 가져와 사용하는 것으로 구현할 수 있다.

✅ SpEL이란, 스프링 표현언어를 말하는 것으로 간단한 문법을 통해 필요 데이터 및 설정값을 가져올 수 있는 언어를 말한다.

 

 

 

[ 참고 및 출처 ]

부트캠프 수업 내용 정리

mvc란? https://hanamon.kr/mvc%EB%9E%80-mvc-design-pattern/

트랜잭션이란 https://mommoo.tistory.com/62

spring MVC https://codingnotes.tistory.com/28

프레임워크와 라이브러리, OOP와 AOP https://geminihoroscope.tistory.com/104

|  최소신장트리

- Mininum Spanning Tree

- 그래프의 모든 노드를 연결할 때 가중치 합이 최소가 나오는 트리

- 특징 : 

(1) 사이클 제외 -- 가중치의 합은 사이클이 포함될 때 최소가 될 수 없다. 

(2) N개의 노드에 대한 최소신장트리 간선 수는 늘 N - 1개이다.

 

|  종류

  크루스칼(Kruskal) 프림(Prim)
핵심 - 최소값의 간선 우선 연결
- 사이클 발생 시 다른 간선 선택
- 임의의 노드에서 시작
- 연결된 간선 중 낮은 가중치 선택
언제? - 간선 수가 비교적 적을 때 - 간선 수가 많을 때 유리
메모리 에지 리스트(or 배열)
유니온 파인드 배열
인접 리스트(or 배열)
방문 배열
속도 O(ElogE) O(ElogV)  - 우선순위 큐 사용 시

*O(v2) - 정렬되지 않은 배열을 사용할 때

 

|  크루스칼 구현

- 구현 순서

1 에지 리스트(그래프) & 유니온 파인드 배열 초기화
2 에지 리스트를 가중치 기준으로 정렬
3 가중치가 낮은 에지부터 연결하기 반복

 

[1] 에지 리스트(그래프) 정렬 & 유니온 파인드 배열 초기화

- 수업 시간의 경우, 아예 외부에서 입력값으로 미정렬된 간선 배열을 받는 것을 가정으로 했다.

- 이 경우에는 위의 [1][2]를 묶어 간선 배열을 정렬하면 된다.

// 전역변수로 유니온-파인드 배열 지정
static int[] parents;

// data : 간선배열, v : 노드 수, e : 간선 수
public static int kruskal(int[][] data, int v, int e) {
    int weightSum = 0; // 전체 가중치
    
    // 1. 그래프 및 유니온 파인드 배열 초기화
    // 1-1. 그래프 정렬
    Arrays.sort(data);
    
    // 1-2. 유니온 파인트 배열 초기화
    parents = new int[v + 1];
    
    for (int i = 1; i < v + 1; i++){
    	parents[i] = i; // 처음에는 자기 자신
    }
    
    return weightSum;
}

 

[2] 가중치가 낮은 에지부터 연결하기 x 반복

- 가중치가 낮은 에지부터 연결을 하려면 먼저 아래와 같은 메소드가 필요하다.

- 참고로 수업시간에 받은 간선 배열은 무방향 간선들로, 모두 (작은 숫자 노드 -> 큰 숫자 노드)로 연결되어 있었다.

 

2-1. find(int a) :: int 

public static int find(int a){
	// 자기 자신과 연결된 경우
	if (parents[a] == a){
    	return a;
    }
    // 자기 자신과 연결되지 않은 경우
    return parents[a] = find(parents[a]);
}

 

2-2. union(int a, int b) :: void

public static void union(int a, int b){
    int aP = parents[a];
    int bP = parents[b];
    
    if (aP != bP){ // 여기서는 크기비교x (필요할 경우 추가)
    	parents[aP] = bP;
    }
}

 

2-3. 최소값 우선으로 간선 연결

public static int kruskal(int[][] data, int v, int e){
    // 생략
    
    // 2. 최소값 우선 간선 연결
    for (int i = 0; i < e; i++){
    	// 서로 연결된 적이 없었던 경우
        if (find(data[i][0]) != find(data[i][1]){
        	// 연결
            union(data[i][0], data[i][1]);
            // 전체 가중치 값 갱신
            weightSum += data[i][2];
        }
    }
}

 

- 전체 코드

 

static int[] parents;
public static int kruskal(int[][] data, int v, int e) {
    int weightSum = 0;

    // 1. 그래프 및 유니온 파인드 배열 초기화
    // edge graph
    Arrays.sort(data, (x, y) -> x[2] - y[2]);

    // union-find
    parents = new int[v + 1];
    for (int i = 1; i < v + 1; i++) {
        parents[i] = i; // 최초에는 자기자신
    }

    // 2. 최소값 우선으로 간선 연결
    for (int i = 0; i < e; i++) {
        if (find(data[i][0]) != find(data[i][1])) {
            union(data[i][0], data[i][1]);
            weightSum += data[i][2];
        }
    }

    return weightSum;
}

public static void union(int a, int b) {
    int aP = find(a);
    int bP = find(b);

    if (aP != bP) {
        parents[aP] = bP;
    }
}

public static int find(int a) {
    if (a == parents[a]) {
        return a;
    }
    return parents[a] = find(parents[a]);
}

 

|  프림 구현

- 구현 순서

1 인접 리스트 및 방문 배열 초기화
2 우선 순위 큐를 통해 자동 정렬
3 임의의 노드(ex. 1)를 선택한 후 낮은 가중치의 간선 연결 x 노드 수만큼 반복

 

- 원리

입력받은 간선 데이터
int[][] graph = 
{{1, 3, 1}, {1, 2, 9}, {1, 6, 8}, 
{2, 4, 13}, {2, 5, 2}, {2, 6, 7}, 
{3, 4, 12}, 
{4, 7, 17}, {5, 6, 5}, {5, 7, 20}};

 

(1) 방문 배열 : 총 노드의 수(v)만큼 방문배열이 입력된다.

  1 2 3 4 5 6 7
1 t            
2 t   t        
3 t   t     t  
4 t   t   t t  
5 t t t   t t  
6 t t t t t t  
7 t t t t t t t

 

(2) 우선순위 큐

- 가중치의 크기를 기준으로 오름차순 자동 정렬

- 가장 작은 가중치의 노드를 꺼내, 방문 배열을 체크하고, 전체 가중치(weightSum)를 +하는 형태

  * 방문했던 노드는 다시 방문하지 않는다면, 결과적으로는 1-3-6-5-2-4-7 과 같이 노드가 연결될 것

- 타입 : Node (도착노드, 가중치)

흐름 우선순위 큐
초기 N(1, 0)        
1 N(3, 1) N(6, 8) N(2, 9)    
2 N(6, 8) N(2, 9) N(4, 12)    
3 N(5, 5) N(2, 7) N(2, 9) N(4, 12)  
4 N(2, 5) N(6, 5) N(2, 7) N(2, 9) N(4, 12)
5 N(6, 5) N(2, 7) N(2, 9) N(4, 12) N(4, 13)
6 N(4, 13) N(7, 17)      

 

- 구현

 

[1] 인접 리스트 및 방문 배열 초기화

static class Node{
    int to;
    int weight;
    
    Node(int to, int weight;){
    	this.to = to;
        this.weight = weight;
    }
}

public static int prim(int[][] data, int v, int e){
    // 인접리스트
    ArrayList<ArrayList<Node>> adjList = new ArrayList();
    for (int i = 0; i < v + 1; i++){
    	adjList.add(new ArrayList());
    }
    
    for (int i = 0; i < e; i++){
    	// 양방향으로 저장
    	adjList.get(data[i][0]).add(new Node(data[i][1], data[i][2]));
        adjList.get(data[i][1]).add(new Node(data[i][0], data[i][2]));
    }
    
    // 방문 배열
    boolean[] visited = new boolean[v + 1];
}

 

[2] 우선 순위 큐를 통해 자동 정렬

public static int prim(int[][] data, int v, int e){
    // 생략
    
    // 우선순위 큐
    PriorityQueue<Node> pq = new PriorityQueue((x, y) -> x.weight - y.weight);
}

 

[3] 임의의 노드(ex. 1)를 선택한 후 낮은 가중치의 간선 연결 x 노드 수만큼 반복

public static int prim(int[][] data, int v, int e){
    // 생략
    
    // 우선순위 큐
    PriorityQueue<Node> pq = new PriorityQueue((x, y) -> x.weight - y.weight);
    pq.add(new Node(1, 0)); // 임의의 노드를 시작점으로 지정
    
    int cnt = 0; // 노드만큼만 반복
    while (!pq.isEmpty()){
    	Node cur = pq.poll();
        cnt++;
        
        // 방문 여부확인
        if (visited[cur.to]){
        	continue;
        }
        visited[cur.to] = true; // 방문 체크
        weightSum += cur.weight; // 전체 가중치 갱신
        
        // 반복 수 확인(마지막 뽑는 노드)
        if (cnt == v) {
        	break;
        }
        
        // 인접한 정점을 작은 가중치 순으로 offer
        for (int i = 0; adjList.get(cur.to).size(); i++){
        	Node adjNode = adjList.get(cur.to).get(i);
            
            if (visited[adjNode.to]) {
            	continue;
            }
            
            pq.offer(adjNode);
        }
        
    }
    
    return weightSum;
}

 

- 전체 코드

static class Node {
    int to;
    int weight;

    public Node(int to, int weight) {
        this.to = to;
        this.weight = weight;
    }
}

public static int prim(int[][] data, int v, int e) {
    int weightSum = 0;

    ArrayList<ArrayList<Node>> adjList = new ArrayList();
    for (int i = 0; i < v + 1; i++) {
        adjList.add(new ArrayList<>());
    }

    for (int i = 0; i < e; i++) {
        // 양방향으로 연결
        adjList.get(data[i][0]).add(new Node(data[i][1], data[i][2]));
        adjList.get(data[i][1]).add(new Node(data[i][0], data[i][2]));
    }

    boolean[] visited = new boolean[v + 1];

    PriorityQueue<Node> pq = new PriorityQueue<>((x, y) -> x.weight - y.weight);
    pq.add(new Node(1, 0));

    int cnt = 0;
    while (!pq.isEmpty()) {
        Node cur = pq.poll();
        cnt++;

        if (visited[cur.to]) {
            continue;
        }
        visited[cur.to] = true;
        weightSum += cur.weight;

        if (cnt == v) {
            return weightSum;
        }

        // 인접한 정점을 작은 가중치 순으로 offer
        for (int i = 0; i < adjList.get(cur.to).size(); i++) {
            Node adjNode = adjList.get(cur.to).get(i);

            if (visited[adjNode.to]) {
                continue;
            }

            pq.offer(adjNode);
        }
    }

    return weightSum;
}

 

 

 

[ 참고 및 출처 ]

- 부트캠트 수업 내용 정리

- [Do it! 알고리즘 코딩 테스트 자바 편]

|  플로이드-워셜 알고리즘

- 모든 노드의 최단 경로를 구하는 알고리즘으로 전체 간선을 인접 행렬로 표현하고 dp를 통해 최단 경로를 구하는 방식

핵심 : 전체 경로의 최단 경로 = 부분 경로의 최단 경로의 조합

- 기능 : 모든 노드 간에 최단 경로 탐색

- 특징 : 음수 가중치 Ok, 동적 계획법의 원리를 사용

- 시간복잡도 : O(V3)

 

|  플로이드-워셜 구현하기

1 배열을 선언하고 초기화
2 노드의 각 인접 노드 경로 입력
3 점화식으로 배열 업데이트
4 음수사이클 확인

 

[1] 배열을 선언하고 초기화

- 각각의 노드를 Start지점으로 두고 모든 Start지점에 대한 최단 거리를 구할 예정

  1 2 3 4 5
1 0
2 0
3 0
4 0
5 0
static int[][] distDP; // 행 : 출발점, 열 : 도착점
static final int INF = 100_000_000; // overflow방지 큰 값 지정

public static void floydWarshall(int V, int E, int[][] data, int start) {

    // 1. 배열을 선언하고 초기화
    distDP = new int[V + 1][V + 1];
    for (int i = 1; i < V + 1; i++) {
        for (int j = 1; j < V + 1; j++) {
            if (i != j) {
                distDP[i][j] = INF;
            }
        }
    }
        
}

 

[2] 노드의 각 인접 노드 경로 입력

- 먼저 dp에 인접 행렬를 입력하는 것

- 인접 행렬을 기준으로 각각의 경유지에 대한 최단 거리를 업데이트할 예정

// 2. 노드의 각 인접 노드 경로 입력
for (int i = 0; i < E; i++) {
    distDP[data[i][0]][data[i][1]] = data[i][2];
}

 

[3] 점화식으로 배열 업데이트

- 각각의 경유지에 관해서, 어떤 출발지~경유지 + 경유지~어떤 도착지가 최소일 경우를 구한다.

for 경유지 K에 관해
   for 출발지 S에 관해
    	for 도착지 E에 관해
          D[S][E] = Math.min(D[S][E], D[S][K] + D[K][E]);
// 3. 점화식으로 배열 업데이트
// :  어떤 경유지를 거쳐 가는 것이 가장 빠른 가를 파악
for (int k = 1; k < V + 1; k++) { // 경유지
     for (int s = 1; s < V + 1; s++) { // 출발지
        for (int e = 1; e < V + 1; e++) { // 도착지
            // 경유지를 출발지 또는 도착지로 두는 거리가 입력된 바 없으면 무시
            if (distDP[s][k] != INF && distDP[k][e] != INF) {
                distDP[s][e] = Math.min(distDP[s][e], distDP[s][k] + distDP[k][e]);
            }
        }
    }
}

 

[4] 음수 사이클의 경우

- 음수사이클의 경우, 위 알고리즘에서 자기자신의 dp지점이 음수로 나타나게 된다.

 

** 최종 코드

static int[][] distDP; // 행 : 출발점, 열 : 도착점
static final int INF = 100_000_000; // overflow방지 큰 값 지정

public static void floydWarshall(int V, int E, int[][] data, int start) {

    // 1. 배열을 선언하고 초기화
    distDP = new int[V + 1][V + 1];
    for (int i = 1; i < V + 1; i++) {
        for (int j = 1; j < V + 1; j++) {
            if (i != j) {
                distDP[i][j] = INF;
            }
        }
    }

    // 2. 노드의 각 인접 노드 경로 입력
    for (int i = 0; i < E; i++) {
        distDP[data[i][0]][data[i][1]] = data[i][2];
    }

    // 3. 점화식으로 배열 업데이트
    // :  어떤 경유지를 거쳐 가는 것이 가장 빠른 가를 파악
    for (int k = 1; k < V + 1; k++) { // 경유지
         for (int s = 1; s < V + 1; s++) { // 출발지
            for (int e = 1; e < V + 1; e++) { // 도착지
                // 경유지를 출발지 또는 도착지로 두는 거리가 입력된 바 없으면 무시
                if (distDP[s][k] != INF && distDP[k][e] != INF) {
                    distDP[s][e] = Math.min(distDP[s][e], distDP[s][k] + distDP[k][e]);
                }
            }
        }
    }

    // 출력
    for (int i = 1; i < V + 1; i++) {
        for (int j = 1; j < V + 1; j++) {
            if (distDP[i][j] >= INF) {
                System.out.printf("%5s ", "INF");
            } else {
                System.out.printf("%5d ", distDP[i][j]);
            }
        }
        System.out.println();
    }

}

 

 

[ 참고 및 출처 ]

부트캠프

Do it 알고리즘 코딩 테스트

|  벨만-포드

- 가중치가 양수만 있을 때에 다익스트라를 사용했다면,

- 가중치에 음수가 있을 때엔 벨만-포드를 사용한다.

- 기능 : 특정 출발 노드에서 도착 노드까지의 최단 경로 탐색

- 특징 : 가중치 음수 OK,  음수 사이클 여부 판단

- 시간 복잡도 : O(VE)

 

|  벨만-포드 구현하기

- 지난번에 구현했던 다익스트라의 우선순위 큐 버전으로 쓰면 음수도 커버가 가능한데

- 벨만-포드를 사용하게 되면 음수 사이클 여부까지 커버할 수 있다.

- 구현의 순서

1 에지 배열 & 최단 경로 dp 배열 초기화
2 모든 에지를 하나씩 확인하여 최단 경로 업데이트
3 음수 사이클 여부 확인

 

[1] 에지 배열(또는 리스트) & 최단 경로 dp 배열 초기화

- 이 부분은 다익스트라의 노드 클래스가 간선 클래스로 바뀐 것 밖에 없다.

- Edge라는 클래스안에 출발지점, 도착지점, 가중치를 넣어 배열에 저장한다.(또는 리스트에 저장)

edge 1 2 3 4 5
출발          
종료          
가중          

- 최단 경로 dp 배열

dp 1 2 3 4 5
최단          
class Edge{
    int from;
    int to;
    int weight;
    
    public Edge(int from, int to, int weight) {
        this.from = from;
        this.to = to;
        this.weight = weight;
    }
}

public void Bellmanford(int v, int e, int[][] data, int start){
    // 1. 간선 정보 및 최단 경로 배열 초기화
    // 1-1. 간선 정보를 배열로 저장
    Edge[] edge = new Edge[e];
    for (int i = 0; i < edge.length; i++) {
        edge[i] = new Edge(data[i][0], data[i][1], data[i][2]);
    }

    // 1-2. 최단 경로 배열
    int[] distDP = new int[v + 1];
    for (int i = 1; i < v + 1; i++) {
        distDP[i] = Integer.MAX_VALUE;
    }
    distDP[start] = 0;
}

 

[2] 모든 에지를 하나씩 확인하여 최단 경로 업데이트

* D는 dp, S는 출발노드, e는 도착노드
D[s] != ∞ 이며, D[e] > D[s] + w 일 때, D[e] = D[s] + w

- 음수 사이클이 없을 때 최대 에지 사용 횟수는 V - 1이다.

* 만약 노드의 개수가 5개이고, 음수사이클이 없다면 아래와 같이 4번 반복하여 최단거리를 확인한다.

dp 1 2 3 4 5
초기 0
1          
2          
3          
4          
// 2. 모든 간선을 확인해 정답 배열 업데이트 하기
for (int i = 1; i < v + 1; i++) { // 간선 횟수 + 1만큼 (음수사이클 확인)
    for (int j = 0; j < e; j++) { // 각 간선에 대하여
        Edge cur = edge[j]; 

        if (distDP[cur.from] == Integer.MAX_VALUE) { // 초기화되지x간선 무시(간선X)
            continue;
        }

        // 기존 거리 > 새로운 거리 : 현재 출발지 + 가중치  
        if (distDP[cur.to] > distDP[cur.from] + cur.weight) {
            distDP[cur.to] = distDP[cur.from] + cur.weight;
        }
    }
}

 

[3] 음수 사이클 여부 확인

- 만약에 음수 사이클이 없다면, 최대 에지 사용 횟수는 V 이상이다.

- 바깥 if문 안으로 들어오는 경우가 v번째이상인 경우는 음수사이클이 발생하므로 아래와 같이 작성해주면 완료된다.

if (distDP[cur.to] > distDP[cur.from] + cur.weight) {
    distDP[cur.to] = distDP[cur.from] + cur.weight;

    // 3. 음수 사이클 유무 확인하기
    if (i == v) {
        isMinusCycle = true;
    }
}

* 최종 코드 

public void bellmanFord(int v, int e, int[][] data, int start) {
    // 1. 간선 정보 및 최단 경로 배열 초기화
    // 1-1. 간선 정보를 배열로 저장
    Edge[] edge = new Edge[e];
    for (int i = 0; i < edge.length; i++) {
        edge[i] = new Edge(data[i][0], data[i][1], data[i][2]);
    }

    // 1-2. 최단 경로 배열
    int[] distDP = new int[v + 1];
    for (int i = 1; i < v + 1; i++) {
        distDP[i] = Integer.MAX_VALUE;
    }
    distDP[start] = 0;


    boolean isMinusCycle = false;
    // 2. 모든 간선을 확인해 정답 배열 업데이트 하기
    for (int i = 0; i < v + 1; i++) {
        for (int j = 0; j < e; j++) {
            Edge cur = edge[j];

            if (distDP[cur.from] == Integer.MAX_VALUE) {
                continue;
            }

            if (distDP[cur.to] > distDP[cur.from] + cur.weight) {
                distDP[cur.to] = distDP[cur.from] + cur.weight;

                // 3. 음수 사이클 유무 확인하기
                if (i == v) {
                    isMinusCycle = true;
                }
            }

        }
    }

    System.out.println("음수 사이클 발생 : " + isMinusCycle);
    for (int i = 1; i < v + 1; i++) {
        if (distDP[i] == Integer.MAX_VALUE) {
            System.out.print("INF" + " ");
        } else {
            System.out.print(distDP[i] + " ");
        }
    }
    System.out.println();

}

 

|  백준문제풀기

11657 타임머신  
1219 세일즈맨  

 

 

 

[ 출처 및 참조 ]

부트캠프 수업 내용 정리

 doit 알고리즘 코딩 테스트 책

|  다익스트라

- 가중치가 있는 그래프에서, 최단 경로를 구하는 알고리즘

- 핵심 : 그래프의 인접리스트와 DP를 이용

- 기능 : 출발 노드와 모든 노드 간의 최단 거리 탐색

- 특징 : 간선은 모두 양수

- 시간 복잡도 : O(ElogV) * E : 간선수, V : 정점수

 

|  다익스트라 구현

- 배열을 사용하는 것도 좋지만 배열 크기가 너무 클 경우를 대비해 인접 리스트를 사용

- (1),(2) 모두 기본적으로 동일하게 아래를 세팅

(1) 간선 정보 리스트 : 인접 리스트에 Node(연결된 노드, 가중치) 타입으로 저장

(2) 임시 거리 배열 : 출발지부터 노드 X까지의 현재 최소 거리를 업데이트할 배열

ㄴ start 위치는 자기 자신이므로 0으로 세팅한다.

class Node{
    int to;
    int weight;
    
    Node (int to, int weight){
    	this.to = to;
        this.weight = weight;
    }
}
// v : 간선수, data : 각 노드의 간선과 가중치 정보, start : 시작점
public void dijkstra(int v, int[][] data, int start){
    // (1) 간선 정보 업데이트
    ArrayList<ArrayList<Node>> adjList = new ArrayList();
    for (int i = 0; i < v + 1; i++){
    	adjList.add(new ArrayList());
    }
    
    for (int i = 0; i < data.length; i++){
    	adjList.get(data[i][0]).add(new Node(data[i][1], data[i][2]));
    }  
}
// v : 간선수, data : 각 노드의 간선과 가중치 정보, start : 시작점
public void dijkstra(int v, int[][] data, int start){
	// (1) 간선 정보 업데이트
    ArrayList<ArrayList<Node>> adjList = new ArrayList();
    for (int i = 0; i < v + 1; i++){
    	adjList.add(new ArrayList());
    }
    
    for (int i = 0; i < data.length; i++){
    	adjList.get(data[i][0]).add(new Node(data[i][1], data[i][2]));
    }  
    
    // (2) 현재까지 start부터 각 노드들까지 최소거리 dp 배열
    // : 초기값을 무한대(Integer.MAX_VALUE)로 설정한 후, 최소값을 갱신
    int distDP = new int[v + 1];
    for (int i = 1; i < v + 1; i++){
    	distDP = Integer.MAX_VALUE:
    }
    distDP[start] = 0; // start를 0으로 세팅
}

 

1. 방문 배열과 반복문 사용하기

(1) 먼저 방문 배열을 만들고, 첫번째 노드부터 끝 노드까지 현재 지점을 짚는다

// v : 간선수, data : 각 노드의 간선과 가중치 정보, start : 시작점
public void dijkstra(int v, int[][] data, int start){
    // (3) 방문 배열 + 반복문으로 다익스트라 구현
    
    // 방문배열
    boolean[] visited = new boolean[v + 1];
    
    // 반복문
    // start부터 각 노드들까지
    for (int i = 1; i < v + 1; i++){
        int minDistance = Integer.MAX_VALUE; // 최초 거리 : 무한대
        int curIdx = 0; // 최단 거리 노드
        
        // 1부터 V 중에 현재 노드
        for (int j = 1; j < v + 1; j++){
            if (!visited[j] && distDP[j] < minDistance){
            	minDistance = distDP[j];
                curIdx = j;
            }
        }
        
        visited[curIdx] = true;
        
        // 현재 노드에서 
        for (int j = 0; j < adjList.get(curIdx).size(); j++){
            // 연결된 노드들에 대하여
            Node adjNode = adjList.get(curIdx).get(j);
            
            if (distDP[adjNode.to] > distDP[curIdx] + adjNode.weight){
            	distDP[adjNode.to] = distDP[curIdx] + adjNode.weight;
            }
        }
    }
}

- 처음 Start부터 각 노드까지의 최선거리 DP는 모두 무한대(Integer.MAX_VALUE)로 2^32인 2147483647인 상태였다.

1 2 3 4 5
무한대 무한대 무한대 무한대 무한대

- 여기에 앞에서 기본 세팅해줄 때에 Start지점을 0으로 세팅했으므로 현재 상태는 아래와 같다.

1 2 3 4 5
0 무한대 무한대 무한대 무한대

- 위의 for문에서 아래 내용의 distDP[j] > minDistance는 따라서 가장 처음엔 Start값인 1이다.

// 1부터 V의 노드들 중에
for (int j = 1; j < v + 1; j++){
    // 현재 노드 짚기
    if (!visited[j] && distDP[j] < minDistance){
    	minDistance = distDP[j]; // minDistance = distDP[1];
        curIdx = j; // curIdx = 1;
    }
}

- 1 다음부터는 무한대 값인 녀석들을 처음에 curIdx로 짚어주고, 

(2) 현재노드부터 연결된 노드까지의 거리 중 최단 거리를 찾는다.

  (1) 현재까지  X노드까지 거리 : distDP[adjNode.to]

  (2) 새롭게 찾은 X노드까지 거리 : distDP[curIdx] + adjNode.weight 

  이 둘 중에 새롭게 찾은 (2) 가 더 작은 경우 갱신하라는 의미이다.

// 현재 노드에서 
for (int j = 0; j < adjList.get(curIdx).size(); j++){
    // 다시 파생되는 노드들에 대하여
    Node adjNode = adjList.get(curIdx).get(j);

    if (distDP[adjNode.to] > distDP[curIdx] + adjNode.weight){
        distDP[adjNode.to] = distDP[curIdx] + adjNode.weight;
    }
}
  curId 1 2 3 4 5
초기 - 0
1 1 0 2 3
2 2 0 2 3 7
3 3 0 2 3 7
4 4 0 2 3 7
5 5 0 2 3 7

 

2. 우선순위 큐 사용하기

- 우선순위 큐는 원리는 비슷한데, 

- 공간이나 시간 복잡도 면에서 조금 더 개선된 코드이다.

- 하나씩 해보는 것이 필요한 코드..

boolean[] visited;

public void dijkstra(int v, int[][] data, int start){
    // (3) 방문 배열 초기화
    visited = new boolean[v + 1];

    // (4) 우선순위 큐를 통한 다익스트라 구현
    // -- 가중치 기준 오름차순으로 자동 정렬
    PriorityQueue<Node> pq = new PriorityQueue((x, y) -> x.weight - y.weight);
    pq.offer(new Node(start, 0)); // start지점을 0으로 갱신
    
    while (!pq.isEmpty()){
    	Node curNode = pq.poll(); // 현재 지점
        
        if (visited[curNode.to]) { // 방문 했던 곳은 X
            continue;
        }
        visited[curNode.to] = true; 
        
        for (int i = 0; i < adjList.get(curNode.to).size(); i++){ // 최단거리 갱신
            Node adjNode = adjList.get(curNode.to).get(i);
            
            if (!visited[curNode.to] && distDP[adjNode.to] > curNode.weight + adjNode.weight){
            	distDP[adjNode.to] = curNode.weight + adjNode.weight;
                pq.offer(new Node(adjNode.to, distDP[adjNode.to]);
            }
        } 
    }
}
  curNode pq 1 2 3 4 5
초기 - (1, 0) 0
1 (1, 0) (2, 2) (3, 3) 0 2 3
2 (2, 2) (3, 3) (4, 7) 0 2 3 7
3 (3, 3) (4, 7)  0 2 3 7
4 (4, 7)    0 2 3 7

 

|  백준 문제 풀이

최단 경로 구하기 https://www.acmicpc.net/problem/1753 😦
최소 비용 구하기 https://www.acmicpc.net/problem/1916 🙃
K번째 최단 경로 찾기    

 

 

[ 출처 및 참조 ]

부트캠프 수업 후 정리

Do it! 알고리즘 코딩 테스트

|  백트래킹

- 모든 가능한 경우의 수 중에서 유망하지 않은 쪽은 제외하고 최적해를 구하는 방법

- 주로 재귀를 사용해서 구현한다.

유망(Promising) 가능성 있다.
가지치기(Pruning) 가능성 없는 것 제외하기
백트래킹(Backtracking) 유망하지 않은 쪽으로는 가지 않고 Back한다.

 

|  N-Queen 을 통한 백트래킹 이해

https://www.acmicpc.net/problem/9663

 

9663번: N-Queen

N-Queen 문제는 크기가 N × N인 체스판 위에 퀸 N개를 서로 공격할 수 없게 놓는 문제이다. N이 주어졌을 때, 퀸을 놓는 방법의 수를 구하는 프로그램을 작성하시오.

www.acmicpc.net

- N x N의 체스판에 N개의 퀸을 서로 공격할 수 없도록 배치한다고 할 때

- 메모리 공간 : 

(1) 각 행의 열을 짚어줄 배열

(2) 카운팅 변수

- Flow

(1) i번째 행의 각 열에 퀸을 배치한다.

(2) 유망여부 : 이전 행의 퀸 열의 상하좌우, 대각선에 해당되는가? 

- 2-1 ) YES : 다음 행으로 이동 

- 2-2 ) NO : 가지치기

(2) 모든 행을 다 돌은 경우, 방법의 수 카운팅

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;

public class Main {

    static int[] board; // 각 행의 퀸 열 위치
    static int cnt = 0; // 전체 카운트 수
    static int nQueen(int n, int r){
        // 탈출문
        if (r == n) {
            cnt++;
            //System.out.println(Arrays.toString(board));
            return cnt;
        }

        // 구현문
        // r번째 행에 대하여
        for (int i = 0; i < n; i++) {
            // 열 위치값 지정
            board[r] = i;

            // promising
            if (isPromising(r, i)){
                nQueen(n, r + 1);
            }
        }

        return cnt;
    }

    static boolean isPromising(int r, int c) {
        // 현재 행 전까지 겹치는 부분 확인
        for (int i = 0; i < r; i++) {
            int pre_r = i;
            int pre_c = board[i];

            if (pre_c == c || r - pre_r == Math.abs(c - pre_c)){
                return false;
            }
        }

        return true;
    }

    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        board = new int[N];

        System.out.println(nQueen(N, 0));
    }
}

 

|  그 외에 백트래킹을 사용하는 예시

(1) Permutation(순열)을 구하는 알고리즘 

import java.util.Arrays;

public class Practice {

    // 순열
    static int cnt = 0;
    static boolean[] visited;
    static int[] out;
    static int solution(int[] arr, int n, int r) {
        visited = new boolean[n];
        out = new int[r];

        return permutation(arr, n, r, 0);
    }

    static int permutation(int[] arr, int n, int r, int depth){
        if (depth == r){
            cnt++;
            System.out.println(Arrays.toString(out));
            return cnt;
        }

        for (int i = 0; i < n; i++) {
            if (visited[i] == false) {
                visited[i] = true;
                out[depth] = arr[i];
                permutation(arr, n, r, depth + 1);
                out[depth] = 0;
                visited[i] = false;
            }
        }

        return cnt;
    }

    public static void main(String[] args) {
        int[] arr = { 1, 2, 3 };
        int n = 3; // n개의 수 중에
        int r = 2; // r개의 수를 고르는 방법
        System.out.println(solution(arr, n, r));
    }
}

(2) N자리수에서 앞자리부터 일의 자리수, 십의 자리수... N의 자리수가 모두 소수인 수를 찾는 알고리즘

- 첫번째 자릿수는 무조건 2,3,5,7중 하나가 들어가야한다.

- 두번재 자릿수부터 0 ~ 9 중에 유망한 수를 구한다.

  > 유망하다 : 2나 5로 나누어떨어지지 않는 수

     > 백트래킹 : 현재까지 자릿수의 숫자(ex.233)가 소수인지 확인

  > 유망하지X : 가지치기 

public class Practice2 {

    static void solution(int n) {
        // 첫번째 자릿수는 무조건 2, 3, 5, 7 중 하나
        int[] primes = {2, 3, 5, 7};

        for (int i = 0; i < primes.length; i++) {
            getPrimes(primes[i], n,1);
        }
    }

    static void getPrimes(int prime, int n, int digitLen){
        
        if (digitLen >= n){
            System.out.print(prime + " ");
            return;
        }

        // 두번째 자릿수부터는 0 - 9중에
        for (int i = 0; i < 10; i++) {
            // 가지치기 : 2 또는 5로 나누어떨어지지 않는 수
            if (i % 2 != 0 && i % 5 != 0) {
                int primeCandidate = 10 * prime + i;

                // backtracking : 소수이면
                if (isPrime(primeCandidate)){
                    getPrimes(primeCandidate, n,digitLen + 1);
                }
            }
        }
    }
    
    static boolean isPrime(int n) {

        if (n < 2) {
            return false;
        }

        for (int i = 2; i <= Math.sqrt(n); i++) {
            if (n % i == 0) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        int n = 3;
        solution(n);
    }
}

 

|  백준 문제집

https://www.acmicpc.net/step/34

 

백트래킹 단계

삼성 SW 역량 테스트 기출 문제 1

www.acmicpc.net

 

[ 출처 및 참조 ]

부트캠프 수업 후 정리한 내용입니다.

 

|  자바 웹 프로그래밍

- 동적인 웹사이트를 만들기 위해서 과거부터 지금까지 아래와 같은 역사를 거쳐왔다.

CGI
(Common Gateway Integerface)
Servlet EJB
(Enterprise Java Beans)
스프링
프로세스 단위 쓰레드 단위 비즈니스 로직을 포함 자바 기반 프레임워크

 

- CGI는 프로세스단위로 동적 페이지를 생성하기 때문에 자원의 효율성이 떨어졌기에 

- Servlet을 통해 쓰레드단위로 동적 페이지를 생성하여 극복하고자 하였다.

- EJB는 여기에 데이터의 저장 및 수정 시의 안정성을 높이고 트랜잭션 처리 기능을 추가 한 것으로

             안정적인 코딩을 가능해졌지만, 코드가 너무 복잡해서 테스트 코드를 생성하는 것도 어려웠다고 한다.

- 이때, Rod Johnson이라는 사람이 자반만의 쉬운 자바 웹 프로그래밍 방법을 담은 도서를 지필하면서,

  개발자 사이에 해당 코딩이 붐을 이루었는데, 이 코드 방식을 아예 프레임워크로 만든 것이 오늘날의 스프링이라고 한다.

 

|  스프링과 SOLID 원칙

- 객체지향에 대해서 다루면서 SOLID에 대해서 배웠었다. 바로가기

- 스프링은 자체적으로 이 SOLID 원칙을 지키는 프로그래밍을 지원한다.

 

1.  스프링의 웹 MVC 3-tier 아키텍처 : SRP 지원(분류)

- SRP는 "하나의 클래스는 단일 책임을 지어야한다"는 원칙이다.(Single Responsibility Principle)

  * SRP는 클래스의 응집도와 관련이 있으며,

    전체 기능들이 확장되고 세분화될 수록 클래스 분류가 애매모호해지는 이슈가 생기는 걸 방지한다.

- 스프링은 아래와 같이 3개의 단계를 가진 아키텍처에 의해 만들어진 프레임워크이다.

- OSI 7-layer와도 유사한 형태로,

  단계별로 역할을 나누어주어 클래스 역할이 더 명확해지고 코드파악이 더 쉬워지게 한다.

Presentation
Layer
  Business Logic
Layer
  Data Access
Layer
Controller Classes Service Classes
   

 

2. 스프링의 DI 컨테이너와 DI config - OCP, LSP, DIP 지원(교체)

- SOLID 중 DIP는 의존성 역전 원칙으로써, 하위 모듈간의 의존관계를 중간 인터페이스를 통해 해소할 것을 제안한다.

- 스프링에는 DI 컨테이너(의존성 주입 컨테이너, Dependency Injection Cotainer)가 있다.

- 더불어 DI 컨테이너 내에는 DI config가 존재하는데, DI config는 의존 클래스를 변경하는 설정(config)을 한다.

- 따라서 스프링을 사용하면 보다 쉽게 DIP를 지킨 코딩이 가능해진다.

 

 

|  스프링에 관한 참조 사이트

Google, Stackoverflow
Baeldung https://www.baeldung.com/
Medium https://medium.com/

 

- 수업 시간에 평소 알고 있었던 구글 검색이나, 스택오버플로우 페이지 외에도 두 가지의 참조 사이트를 소개받았다.

- 처음 접하게 된 사이트들이어서 신기한 마음에 몇 가지 페이지들을 공유해보고 싶다.

- Baeldung의 Start Here페이지이다. 튜토리얼을 제공하고 있는데 일반 코스에서 시큐어 코딩, REST API까지 다룬다.

  돈 주고 배워야 하는 내용들이 다 들어가 있다. 아름다운 사이트..

- Medium이라는 사이트는 일반 블로그 사이트같다. IT뿐 아니라 여러 분야를 다 섭렵하고 있는데,

  [technoligies]인가 태그를 선택해 들어가보니, 아래처럼 IT 기술 관련된 포스팅이 많이 있었다.

  신기한 건 TTS까지 제공해주고 있다는 거다. 진정한 의미의 웹 접근성 달성을 이룬 사이트다.

 

[ 참고 및 출처 ]

부트캠프 강의를 들은 후 배운 내용을 복습한 내용입니다.

|  OOP의 핵심

[ 지난 번 OOP 관련 포스팅 내용 요약 ]
- OOP는 객체 간 상호 관계에 포커스를 맞춘 방법론으로, 추상화, 상속, 다형성, 캡슐화 4가지의 주요 특징이 있다.
- 추상화는 모델링이고, 상속은 확장과 재사용성이며, 다형성은 사용 편의, 캡슐화는 정보 은닉에 해당된다.
- 추상화 단계에서는 클래스 내 응집도를 높이는 게 필요하고, 상속 시에는 클래스 간 결합도에 주의해야 한다.

- OOP는 풀면 참 복잡했는데, 오늘 배운 수업에서는 분류교체라는 단어로 그 핵심을 단순화해 설명했다.

* 현업에 오래 종사했던 강사분은 스파게티 소스에 대해 언급하며,

  소프트웨어를 유연하게 만들려면 객체지향 방식이 유용하다 얘기주셨다.

분류한다 코드를 적절히 잘 분류한다. -> 클래스
교체한다 필요에 따라서 특정 모듈을 통째로 교체하기도 한다. -> DBMS 업체나 라이브러리를 통째로 교체하기도 한다.

※ 라이브러리와 프레임워크와 아키텍처 간의 상관관계 바로가기

 

|  SOLID 원칙

- 로버트 C. 마틴에 의해 만들어진 원칙

SRP 단일 책임 원칙(분류)    Simple Responsibility principle
OCP 개방 폐쇄 원칙(교체) Open/Closed principle
LSP 리스코프 치환 법칙(교체) Liskov subsituation principle
ISP 인터페이스 분리 원칙(분류) Interface segregation principle
DIP 의존성 역전 원칙(교체) Dependency inversion principle

 

※ 참조 : 실제 현장에서 사용하는 코드는 아래와 달라 보입니다. 원리를 이해하기 위한 코드로 봐주시면 감사하겠습니다.

 

1. SRP : 한 클래스는 단일의 책임만 가져야 한다.

 

SRP가 지켜지지 않은 코드 

class PaymentService{
    public void pay(Customer customer, Product product) {

        if (!isValidatedPrice(customer, product)) {
            throw new IllegalArgumentException("charge your money");
        }

        customer.setMoney(customer.getMoney() - product.getPrice());
    }

    public boolean isValidatedPrice(Customer customer, Product product) {
        if (customer.getMoney() < product.getPrice()) {
            return false;
        }

        return true;
    }

    public void sendSMS(Customer customer, Product product) {
        System.out.println("phone number : " + customer.getPhone());
        System.out.println("상품 " + product.getName() + "(" 
                          + product.getProduct_no() + ")이 결제되었습니다.");
    }
}

- 고객이 상품을 주문할 때, PaymentService라는 클래스에서 아래와 같이 세가지의 기능을 제공한다고 할 때에

  (1) 결제하기 - pay()

  (2) 결제가능 여부 확인하기 - isValidatedPrice()

  (3) 결제완료 SMS(메세지) 전송하기 - sendTMS()

- 이 세가지 기능은 엄밀히 말하면 서로 다른 기능이라 볼 수 있다. 

- 전체 scope이 작은 경우에는 세가지 기능이 묶일 수도 있겠지만, 만약 기능이 더 복잡해지고 확장된다면 세가지를 분리할 수 있다.

 

클래스를 분리

class PaymentService{
    public void pay(Customer customer, Product product) {

        if (!PaymentValidation.isValidatedPrice(customer, product)) {
            throw new IllegalArgumentException("charge your money");
        }

        customer.setMoney(customer.getMoney() - product.getPrice());
    }
}
class PaymentValidation{
    public static boolean isValidatedPrice(Customer customer, Product product) {
        if (customer.getMoney() < product.getPrice()) {
            return false;
        }

        return true;
    }
}
class SMSService{
    public void sendSMS(Customer customer, Product product) {
        System.out.println("phone number : " + customer.getPhone());
        System.out.println("상품 " + product.getName() + "(" 
                           + product.getProduct_no() + ")이 결제되었습니다.");
    }
}

 

2. OCP : 확장에는 열려있고, 변경에는 닫혀있다.

* if-else의 반복적인 케이스가 보일 때에는 클래스를 분리하는 것이 효과적일 수 있다.

 

OCP가 지켜지지 않은 코드

class PointService {
    public double getPointRate(String category) {
        // 구매 카테고리별로 포인트 비율을 다르게 지급
        if (category.equals("옷")){
            return 0.05;
        } else if (category.equals("잡화")) {
            return 0.02;
        } else if (category.equals("기타")) {
            return 0.01;
        }

        return 0;
    }

    public double getVIPRate(String category){
        // 구매 카테고리 별로 VIP 여부에 따라서 비율을 다르게 지급
        if (category.equals("잡화")){
            return 0.12;
        } else if (category.equals("도서")) {
            return 0.08;
        } else if (category.equals("기타")) {
            return 0.01;
        }

        return 0;
    }
}

- 만약에 포인트 제도가 추가되어, 구매 카테고리 별로 서로 다르게 포인트를 지정한다고 하자.

- 이럴때 카테고리가 위와 같이 한정적이라면 상관이 없겠지만 카테고리가 계속 늘어난다면 문제가 발생할 수 있다.

- 더불어 getPoint()와 getVIPRate()의 카테고리명과 순서가 다른 걸 볼 수 있다.

- 이런 경우 로직이 통일적이지 않아지기 때문에 결과적으로 문제가 발생할 수 있다.

 

인터페이스를 통한 OCP 확보

interface PointServiceIF{
    public abstract double getPointRate(String category);
    public abstract double getVIPRate(String category);
}

class Clothe implements PointServiceIF{

    @Override
    public double getPointRate(String category) {
        return 0.05;
    }

    @Override
    public double getVIPRate(String category) {
        return 0.00;
    }
}

class Book implements PointServiceIF{

    @Override
    public double getPointRate(String category) {
        return 0.12;
    }

    @Override
    public double getVIPRate(String category) {
        return 0.25;
    }
}

- 인터페이스를 통해서 메소드를 프로토타입으로 만들어 준 후에, 각각의 카테고리를 클래스로 만들어 메소드를 구현하면 OCP를 확보할 수 있다.

 

3. LSP : 서브타입은 언제나 기반타입으로 교체할 수 있어야 한다.

- 이 부분은 상위 클래스(또는 인터페이스)의 기능과 동일한 기능을 하위 클래스가 상속(또는 구현)하는 가를 말한다.

상속 is kind of  하위 클래스는 상위 클래스의 한 분류이다.
구현 is able to 하위 클래스는 상위 인터페이스를 구현할 수 있다.

- 여기서는 예제를 만들지 않고 대신 예제 링크만 가져왔다.

https://blog.itcode.dev/posts/2021/08/15/liskov-subsitution-principle

- 위 예제에서는, 직사각형 > 정사각형으로 LSP를 설명하는데, 정사각형은 직사각형의 메소드를 모두 동일하게 가져가지 않기 때문에, 사각형 클래스를 만들고 직사각형과 정사각형이 그것을 상속받게 했다.

더보기

- 실무에서는 상속을 많이 사용하지 않는다고 한다.

왜냐하면, 오버라이딩을 했는지 안 했는지 매번 확인하기가 어렵고,

오버라이딩 잘못하면 로직 충돌 (Fragile base class문제)이 일어나기도 하며, 

기능을 너무 확장하거나 변경하면 재활용성이 낮아지기 때문이라고 한다.

 

- 그래서 상속의 대안 및 상속을 잘하기 위해서는

(1) 상위 클래스를 한 클래스만 상속하거나

(2) 클래스 상속이 아닌, 인터페이스로 대체하거나

(3) 상위 클래스와 상호 치환(=동일 기능을 갖도록)이 가능하도록 상속을 해야한다. 

 

4. ISP : 인터페이스도 단일 책임을 갖도록 분리해야 한다.

* 인터페이스를 너무 크게 만들면, 사용하지 않는 메소드들도 구현하도록 해야 한다.

* 실무에서 보면 단일 메소드를 가진 인터페이스들이 있다. > 세분화된 인터페이스로 쪼개서 단일 책임을 갖게 한 것

 

- 이 부분은 별도로 내용을 추가하지 않고, 바로 앞의 예시로 이야기하겠다.

interface PointServiceIF{
    public abstract double getPointRate(String category);
    public abstract double getVIPRate(String category);
}

class Clothe implements PointServiceIF{

    @Override
    public double getPointRate(String category) {
        return 0.05;
    }

    @Override
    public double getVIPRate(String category) {
        return 0.00;
    }
}

class Book implements PointServiceIF{

    @Override
    public double getPointRate(String category) {
        return 0.12;
    }

    @Override
    public double getVIPRate(String category) {
        return 0.25;
    }
}

- 바로 위의 코드를 보면 "옷"의 경우 안 쓰는 메소드인 getVIPRate()를 구태어 어거지로 구현한 걸 볼 수 있는데,

  이유는 인터페이스 안에 메소드를 두 개를 넣어 두어서 두 가지를 모두 구현해야 하기 때문이다.

- 이렇듯 상속은 주의해서 사용하는 게 좋다. 필요하지 않은 메소드까지 구현해야할 수 있고,

  너무 많이 확장되고 재사용되다보면 최초에 목적하고 있던 기능에서 많이 동떨어진 메소드가 만들어질 수 있기 때문이다.

- 이러한 문제를 해결하기 위해서는 단일 인터페이스, 곧 단일 메소드를 가진 인터페이스를 만드는 것이 좋다.

interface GeneralPointServiceIF{
	public abstract double getPointRate(String category);
}

interface VIPPointServiceIF{
	public abstract double getVIPRate(String category);
}

class Clothe implements GeneralPointServiceIF{
    @Override
    public double getPointRate(String category) {
        return 0.05;
    }
}

class Book implements GeneralPointServiceIF, VIPPointServiceIF{
    @Override
    public double getPointRate(String category) {
        return 0.12;
    }

    @Override
    public double getVIPRate(String category) {
        return 0.25;
    }
}

 

5. DIP : 하위 모듈의 변경이 상위 모듈의 변경을 요구하는 의존성을 끊어내야 한다.

- 개발을 하다보면 보안상의 이슈가 발생한 라이브러리를 다른 라이브러리로 교체해야할 때가 있다고 한다.

- 또는 외주업체에 맡겨 서비스를 추가하는 경우도 있는데, 업체를 변경할 때에도 기존 서비스를 변경해야할 때가 있다고 한다.

- 만약 호텔에 Deposit 금액으로 20만원을 넣어두고, 호텔의 서비스를 사용할 때마다 deposit 금액을 차감시킨다고 하자.

[1] 호텔 비용 정산 클래스 - HotelPaymentService

[2] 고객 deposit에 연결하는 클래스 - HotelDepositAdapter

class HotelPaymentService{
    String pay(Customer customer, ServiceRequest serviceRequest){
    	HotelDepositAdapter hotelDepositAdapter = new HotelDepositAdapter();
        int payResult = hotelDepositAdapter.useService(customer, serviceRequest);
    	
        if (payResult == -1){
        	return "Fail";
        }
        
        return "Success : left deposit - " + payResult + "(원)"; 
    }
}

class HotelDepositAdapter{  
    int useService(Customer customer, ServiceRequest serviceRequest){
    	int possibleDeposit = customer.getDeposit() - serviceRequest.getServicePrice();
        
        if (possibleDeposit < 0) {
        	return -1;
        }
        
        return possibleDeposit;
    }
}

- 이 상태에서, 만약 호텔측에서 Deposit 금액 외에 카드를 맡기면 추가적으로 결제가 가능하게 해달라는 요청을 한다면

[1] 호텔 비용 정산 클래스 - HotelPaymentService

[2] 고객 deposit에 연결하는 클래스 - HotelDepositAdapter

[3] 고객 card에 연결하는 클래스 - HotelCardAdapter

- 위와 같이 card를 연결하는 어뎁터 클래스를 하나 더 추가하게 될 수 있다.

- 이런 경우 HotelPaymentService이 이미 HotelDepositAdapter에 의존적이라 코드를 전반적으로 수정해주어야 한다.

- 앞으로 비용 정산 방식이 추가적으로 바뀌게 되면 이 방식은 비효율적이다

class HotelPaymentService{
    String pay(Customer customer, ServiceRequest serviceRequest, PaymentType type){
    	
        if (type.getType() < 0 || type.getType() >= 2) {
        	return "PaymentRequest Error : write correct PaymentType";
        }
        
        int payResult = -1;
        
        if (type.getType() == 0){
        	HotelDepositAdapter hotelDepositAdapter = new HotelDepositAdapter();
        	payResult = hotelDepositAdapter.useService(customer, serviceRequest);
        } else if (type.getType() == 1){
        	HotelCardAdapter hotelCardAdapter = new HotelCardAdapter();
        	payResult = hotelCardAdapter.useService(customer, serviceRequest);
        } 
    	
        if (payResult == -1){
        	return "Fail";
        }
        
        return "Success : left deposit - " + payResult + "(원)"; 
    }
}

- 이런 경우 아래와 같이 어뎁터 자체가 인터페이스를 구현하도록 하고,

  HotelPaymentService가 인터페이스를 바라보도록 하면 해결된다.

enum HotelPaymentType{
    DEPOSIT, CARD
}

class HotelPaymentService{

    private final HotelDepositAdapter hotelDepositAdapter = new HotelDepositAdapter();
    private final HotelCardAdapter hotelCardAdapter = new HotelCardAdapter();

    String pay(Customer customer, ServiceRequest serviceRequest){
    
    	HotelPaymentIF hotelPaymentIF;
        
        if (serviceRequest.getPaymentType() == HotelPaymentType.DEPOSIT){
        	hotelPaymentIF = hotelDepositAdapter;
        } else {
        	hotelPaymentIF = hotelCardAdapter;
        }
    
    	int payResult = hotelPaymentIF.useService(customer, serviceRequest);
    	
        if (payResult == -1){
        	return "Fail";
        }
        
        return "Success : left deposit - " + payResult + "(원)"; 
    }
}

interface HotelPaymentIF{
	int useService(Customer customer, ServiceRequest serviceRequest);
}

class HotelDepositAdapter implements HotelPaymentIF{  
    int useService(Customer customer, ServiceRequest serviceRequest){
    	int possibleDeposit = customer.getDeposit() - serviceRequest.getServicePrice();
        
        if (possibleDeposit < 0) {
        	return -1;
        }
        
        return possibleDeposit;
    }
}

class HotelCardAdapter implements HotelPaymentIF{  
    int useService(Customer customer, ServiceRequest serviceRequest){
    	if (CardService.useCard(customer, serviceRequest) < 0) {
        	return -1;
        }
        
        return serviceRequest.getServicePrice();
    }
}

 

 

[ 출처 및 참조 ]

부트캠프 강의를 들은 후 정리한 내용입니다.

[스프링 입문을 위한 자바 객체지향의 원리와 이해]

SOLID https://bottom-to-top.tistory.com/27

위키백과 https://ko.wikipedia.org/wiki/SOLID_(%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5_%EC%84%A4%EA%B3%84) 

 

+ Recent posts