HTML이란?

  HTML(Hyper Text Markup Language)은 페이지에 제목, 문단, 표, 이미지, 동영상 등을 정의하고 그 구조와 의미를

  부여하는 정적언어로 웹의 구조를 담당한다.

 

HTML문법

  HTML의 문법의 기본형태로는 태그를 사용한다.

  <tag></tag>형태로 열리고 닫히는 구조를 갖고 있으며 한쌍으로 태그의 범위를 만들어 준다.

 

  태그의 기능을 확장하기위해 '속성'을 사용할 수 있다.

  <img />는 이미지를 삽입하기 위해 사용하는 태그지만 태그만 사용하게 되면 어떤 이미지를 삽입하려는지 알 수 없다.

  그래서 src(source)라는 속성을 사용해서 삽입할 이미지의 경로를 지정하고 alt(alternative)라는 속성으로 이미지를

  출력하지 못하는 경우에는 이미지대신 보여질 텍스트를 지정해준다.

 

  태그에는 부모와 자식 요소가 존재한다.

<parent>
  <child> </child>
</parent>

  이러한 형태처럼 child태그가 parent태그의 컨텐츠로 사용이 된다면 child태그는 parent태그의 자식요소가 된다.

 

<section>
  <h1></h1>
  <ul>
    <li></li>
    <li></li>
  </ul>
</section>

  HTML태그들을 보면 이러한 형태들을 많이 볼 수 있는데 h1태그와 ul태그는 section태그의 자식요소가 되는것이고

  li태그는 ul태그의 자식요소가 된다. 이처럼 부모와 자식 요소는 상대적인 개념이다.

  이러한 부모와 자식요소로 작성하는것은 선택자를 통해 CSS와 JS로 HTML을 다룰 때 중요하게 사용된다.

 

  태그에는 '빈태그'라고 해서 닫히는 태그가 없는 태그들이 있는데 다음과 같은 형태를 갖는다.

<!-- /가 없는 빈태그 -->

<tag>

<!-- /가 있는 빈태그 -->

</tag>

  이렇게 두가지 형태의 빈 태그가 있고 둘다 사용할 수 있지만 XHTML버전이나 린트(Lint)환경 혹은 프레임워크

  셋팅에 따라 /를 사용하는것이 필수가 될 수 있다.

 

HTML문서의 범위

  웹 개발을 하며 볼 수 있는 index.html 같은 HTML파일을 HTML문서라고 표현할 수 있다.

 

<!DOCTYPE>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="aurthot" content="홍길동">
  <meta name="description" content="샘플 사이트">
  <title>내 사이트</title>
  <link rel="stylesheet" href="./css/main.css">
  <script src="./js/main.js"></script>
</head>
<body>
  <section>
    <h1></h1>
    <div>
      <ul>
        <li></li>
        <li></li>
      </ul>
    </div>
  </section>
</body>
</html>

  <html>태그는 HTML문서의 전체 범위를 지정한다. 웹 브라우저가 해석해야 할 HTML문서가 어디에서 시작하며,

  어디에서 끝나는지 알려주는 역할을 한다.

  <head>태그는 웹 브라우저가 해석해야 할 HTML문서의 정보 범위를 지정한다.

  정보에는 웹 페이지의 제목, 웹 페이지의 문자 인코딩 방식, 연결할 외부 파일의 위치, 구조화 하기 위한 기본셋팅 값

  등을 말한다.

  다르게는 화면을 구성하기 위한 기본 설정이라고 표현할 수 있다.

  <body>태그는 웹 브라우저가 해석해야 할 HTML문서의 구조 범위를 지정한다.

  구조는 사용자가 화면을 통해서 볼 수 있는 내용(컨텐츠)의 형태나 레이아웃등을 의미하며 로고, 헤더, 푸터, 

  네비게이션, 메뉴, 버튼, 입력창, 팝업, 광고 등 보이는 모든 것들이 구조에 해당한다.

  구조는 body 범위 안에서만 생성한다.

  DOCTYPE(DTD, DocumentType Definition)은 마크업 언어에서 문서 형식을 정의한다.

  이는 웹 브라우저에 우리가 제공할 HTML문서를 어떤 HTML버전의 해석 방식으로 구조화하면 되는지를 알려준다.

  HTML은 크게 1, 2, 3, 4, X-, 5 버전이 있고 현재의 표준 모드는 HTML5다.

<!-- HTML5 -->
<!DOCTYPE html>

<!-- XHTML 1.0 Transitional-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml/DTD/xhtml-transitanl.dtd">

 

HTML 문서의 정보

  <head>태그 안에서 사용하는 태그들은 HTML 문서의 정보를 갖고 있다.

  title태그

    <title>은 HTML문서의 제목을 정의한다. 웹 브라우저의 각 사이트 탭에서 이름으로 표시된다.

<head>
  <title>글쓰기</title>
</head>

    우리가 보통 웹페이지에 들어가면 볼 수 있는 탭에 이렇게 표시된다.

  

  meta태그

    <mata>는 HTML문서(웹페이지)에 관한 정보(표시방식, 제작자(소유자), 내용, 키워드 등)를 검색엔진이나

    브라우저에 제공한다. meta태그는 빈태그이다.

    아래코드로 좀 더 편하게 이해할 수 있다.

<head>
  <meta charset="UTF-8">
  <meta name="author" content="홍길동">
  <meta name="description" content="샘플 사이트">
</head>


<문서의 정보범위>
  <정보 문자 인코딩방식="UTF-8">
  <정보 정보종류="사이트제작자" 정보값="홍길동">
  <정보 정보종류="사이트설명" 정보값="샘플 사이트">
</문서의 정보범위>

 

  meta태그에서 사용할 수 있는 속성은 다음과 같다.

속성

의미

charset

문자 인코딩 방식

UTF-8, EUC-KR

name

검색엔진 등에 제공하기 위한 정보의 종류(메타데이터)

author, description, keywords, viewport

content

name이나 http-equiv 속성의 값을 제공

 

   

  link태그

    link태그는 외부 문서를 연결할 때 사용한다.

    특히 HTML 외붕에서 작성된 CSS파일을 불러와 연결할 때 사용하며 link태그도 빈태그이다.

<head>
  <link rel="stylesheet" href="./css/main.css">
  <link rel="icon" href="./favicon.png">
</head>


<문서의 정보범위>
  <외부문서연결 관계="css" 문서경로="./css/main.css">
  <외부문서연결 관계="사이트대표아이콘" 문서경로="./favicon.png">
</문서의 정보범위>

속성

의미

rel

(필수) 현재 문서와 외부 문서와의 관계를 지정

stylesheet, icon

href

외부 문서의 위치를 지정

경로

 

  style태그

    CSS를 외부문서에 작성하여 연결하는 방식이 아닌 HTML내부에 작성할 때 사용한다.

<style>
  img{
    width: 100px;
    height: 200px;
  }
  
  p{
    font-size: 20px;
    font-weight: bold;
  }
</style>



<스타일정의>
  <!-- CSS 코드 -->
</스타일정의>

 

  script태그

    HTML문서에서 CSS는 link로 불러오거나 style태그 안에 작성할 수 있지만 JS는 script태그로 가져오거나

    작성할 수 있다

<!-- 불러오기 -->
<script src="./js/main.js"></script>

<!-- 작성하기 -->
<script>
  function windowOnClickHandler(event) {
    console.log(event);
  }
  
  windos.addEventListner('click', windowOnClickHandler);
</script>



<!--불러오기-->
<자바스크립트 문서경로="./js/main.js"></자바스크립트>

<!-- 작성하기-->
<자바스크립트>
  <!-- JS 코드 -->
</자바스크립트>

 

HTML 문서의 구조

  DIV태그

    <div></div>의 div는 division의 약자로 분할을 뜻하고 문서의 부분이나 섹션을 정의한다.

    명확한 의미를 갖지 않기 때문에 정말 많은 경우 단순히 특정 범위를 묶는(wrap) 용도로 사용한다.

    보통 이렇게 묶인 부분들에 CSS나 JS를 적용하게 된다.

<body>
  <div>
    <p></p>
  </div>
  <div>
    <div>
      <h1></h1>
      <p></p>
    </div>
  </div>
</body>



<body>
  <묶음1>
    <p></p>
  </묶음1>
  <묶음2>
    <묶음2-1>
      <h1></h1>
      <p></p>
    </묶음2-1>
  </묶음2>
</body>

    이런 형태로 볼 수 있다. div태그에는 클래스로 각 div를 구분하도록 할수 있다.

<body>
  <div class="content">
    <p></p>
  </div>
</body>

    이런 형태로 작성하며 class를 이용해 CSS에서 구분할 수 있다.

 

  IMG태그

    <img>는 HTML에 이미지를 삽입할 때 사용한다.

   

<body>
  <img src="./image.png" alt="사진">
</body>



<body>
  <이미지 경로="./image.png" 대체텍스트="사진">
</body>

속성

의미

src

(필수) 이미지의 URL

URL

alt

(필수) 이미지의 대체 텍스트(alternative)를 지정

 

 

 

 

레퍼런스

패스트캠퍼스 올인원패키지 - HTML

'HTML&CSS' 카테고리의 다른 글

CSS선택자(가상클래스 선택자)  (0) 2021.04.10
CSS 선택자(Selector)  (0) 2021.04.08
CSS란?  (0) 2021.04.07

IOC(Inversion Of Control, 제어의 역전)이란?

  객체의 생성, 생명주기의 관리까지 모든 객체에 대한 제어권이 바뀌었다는 것을 의미한다.

 

  컴포넌트 의존관계 설정(component dependency resolution), 설정(configuration) 및 생명주기(LifeCycle)를

  해결하기 위한 디자인 패턴(Design Pattern)이다.

 

  스프링을 쓰기 전에는 개발자가 프로그램의 흐름을 제어하는 주체였으나 스프링에서는 프로그램의 흐름을

  프레임워크가 주도하게 된다.

  객체의 생성부터 생명주기관리를 컨테어나가 도맡아서 하게 된 것이다.

  즉, 제어권이 컨테이너로 넘어가게 되고, 이것을 제어권의 흐름이 바뀌었다고 하여 IOC(Inversion Of Controll,

  제어의 역전)이라고 하게 된다.

  제어권이 컨테이너로 넘어옴으로써 DI(의존성주입), AOP(관점 지향 프로그래밍)등이 가능하게 된다.

  반대로 말하면 스프링에게 애플리케이션 흐름을 제어하는 권한(IOC)이 없다면 Autowired를 통한

  의존성 주입을 할 수 없을 것이다.

 

 

레퍼런스

jjunii486.tistory.com/84

mo-world.tistory.com/entry/IOC%EC%99%80-DI-%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC-%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B0%9C%EB%85%90-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-%EC%89%BD%EA%B2%8C-%EC%84%A4%EB%AA%85

'Spring' 카테고리의 다른 글

JPA 기초  (0) 2022.01.26
JPA란?  (0) 2022.01.13
AOP(Aspect Oriented Programming)  (0) 2021.02.20
의존성 주입(Dependency Injection, DI)  (0) 2021.02.18
Spring MVC (Front Controller Pattern)  (0) 2021.02.17

AOP(Aspect Oriented Programming)이란?

  AOP는 흔히 '관점 지향 프로그래밍'이라는 용어로 번역된다.

  AOP가 추구하는 것은 '관심사의 분리(separate concerns)'다.

  개발자가 염두에 두어야 하는 일들은 별도의 '관심사'로 분리하고, 핵심 비즈니스 로직만을 작성할 것을 권장한다.

  관심사라는 것을 예를 들면 나눗셈을 구현한다고 했을 때 핵심로직은 두개의 숫자를 나누는 것이지만,

  주변 로직은 0을 나누는 것이 아닌지 등을 체크하는 것이다. 관심사는 이런 중요한 로직은 아니지만 사전 조건이나

  사후 조건등이라고 간주할 수 있다.

 

  AOP는 과거에 개발자가 작성했던 관심사 + 비즈니스 로직을 분리해서 별도의 코드로 작성하도록 하고,

  컴파일 혹은 실행시점에 이를 결합하는 방식으로 접근한다.

  실제 실행은 결합된 상태의 코드가 실행되기 때문에 개발자들은 핵심 비즈니스 로직에만 근거해서 코드를 작성하고,

  나머지는 어떤 관심사들과 결합할 것인지를 설정하는 것 만으로 모든 개발을 마칠 수 있게 된다.

  예를들어 AOP를 이용하면 작성된 모든 메서드의 실행 시간이 얼마인지를 기록하는 기능을 기존 코드의 수정 없이도

  작성할 수 있고, 잘못된 파라미터가 들어와서 예외가 발생하는 상황을 기존 코드의 수정 없이도 제어할 수 있다.

  스프링이 AOP를 지원한다는 것이 스프링의 가장 중요한 특징 중에 하나로 말하게 된 이유 역시 별도의 복잡한

  설정이나 제약없이 스프링 내에서 간편하게 AOP의 기능들을 구현할 수 있기 때문이다.

 

AOP의 용어들

  AOP는 기존 코드를 수정하지 않고, 원하는 기능들과 결합할 수 있는 패러다임이다.

  AOP를 구현하기 위해서는 다음과 같은 핵심적인 그림들을 이해할 필요가 있다.

  개발자의 입장에서 AOP를 적용한다는 것은 기존의 코드를 수정하지 않고도 원하는 관심사(Cross-Concern)들을

  엮을 수 있다는 것이다.

  위의 그림에서 Target에 해당하는 것이 바로 개발자가 작성한 핵심 비즈니스 로직을 갖는 객체다.

 

  Target은 순수한 비즈니스 로직을 의미하고, 어떠한 관심사들과도 관계를 맺지 않는다.

  순수한 코어(core)라고 볼 수 있다. Target을 전체적으로 감싸고 있는 존재를 Proxy라고 한다.

  Proxy는 내부적으로 Target을 호출하지만, 중간에 필요한 관심사들을 거쳐서 Target을 호출하도록 자동 혹은

  수동으로 작성된다. Proxy의 존재는 직접 코드를 통해서 구현하는 경우도 있지만 대부분의 경우 스프링 AOP 기능을

  이용해서 자동으로 생성되는(Auto-Proxy)방식을 이용한다. JoinPoint는 Target객체가 가진 메서드다.

  외부에서의 호출은 Proxy객체를 통해서 Target 객체의 JoinPoint를 호출하는 방식이라고 이해할 수 있다.

 

  Joinpoint는 Target이 가진 여러 메서드라고 보면 된다. 엄밀하게 스프링 AOP에서는 메서드만이 JoinPoint가 된다.

  Target에는 여러 메서드가 존재하기 때문에 어떤 메서드에 관심사를 결합할 것인지를 결정해야 하는데

  이 결정을 'Pointcut'이라고 한다.

 

  Pointcut은 관심사와 비즈니스 로직이 결합되는 지점을 결정하는 것이다. 앞의 Proxy는 이 결합이 완성된 상태이므로

  메서드를 호출하게 되면 자동으로 관심사가 결합된 상태로 동작하게 된다.

 

  Advice는 실제 걱정거리를 분리해놓은 코드를 의미한다. Advice는 그 동작 위치에 따라 다음과 같이 구분된다.

구분

설명

Before Advice

TargetJoinPoint를 호출하기 전에 실행되는 코드다.

코드의 실행 자체에는 관여할 수 없다.

After Returning Advice

모든 실행이 정상적으로 이루어진 후에 동작하는 코드다.

After Throwing Advice

예외가 발생한 뒤에 동작하는 코드다.

After Advice

정상적으로 실행되거나 예외가 발생했을 때 구분없이 실행되는 코드다.

Around Advice

메서드의 실행 자체를 제어할 수 있는 가장 강력한 코드다.

직접 대상 메서드를 호출하고 결과나 예외를 처리할 수 있다.

  Advice는 별도의 인터페이스로 구현되고 이를 클래스로 구현하는 방식으로 제작했으나 Spring 3버전 이후에는

  어노테이션만으로도 모든 설정이 가능하다.

  Target에 어떤 Advice를 적용할 것인지는 XML을 이용한 설정을 이용할 수 있고, 어노테이션을 이용하는 방식을

  이용할 수 있다.

 

  Pointcut은 Advice를 어떤 JoinPoint에 결합할 것인지를 결정하는 설정이다. AOP에서 Target은 결과적으로

  Pointcut에 의해서 자신에게는 없는 기능들을 갖게 된다.

  다양한 형태로 선언해서 사용할 수 있는데 주로 사용되는 설정은 다음과 같다.

구분

설명

execution(@execution)

메서드를 기준으로 Pointcut을 설정한다.

within(@within)

특정한 타입(클래스)을 기준으로 Pointcut을 설정한다.

this

주어진 인터페이슬르 구현한 객체를 대상으로 Pointcut을 설정한다.

args(@args)

특정한 파라미터를 갖는 대상들만을 Pointcut으로 설정한다.

@annotation

특정한 어노테이션이 적용된 대상들만을 Pointcut으로 설정한다.

 

예제코드

  AOP를 위해 의존성을 추가한다.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

 

  간단한 인터페이스를 구현하는 클래스를 빈으로 정의한다.

public interface EventService {
  
  public void created();
  
  public void operation();
  
  public void deleted();
}
import org.springframework.stereotype.Component;

@Component
public class SimpleServiceEvent implements EventService {
  
  @Override
  public void created() {
    long begin = System.currentTimeMillis();
    
    try {
      Thread.sleep(1000);
    }catch(InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("created");
    System.out.println(System.currentTimeMillis() - begin);
  }
  
  @Override
  public void operation() {
    long begin = System.currentTimeMillis();
    
    try {
      Thread.sleep(2000);
    }catch(InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("operation");
    System.out.println(System.currentTimeMillis() - begin);
  }
  
  @Override
  public void deleted() {
    System.out.println("deleted");
  }
}

  created()와 operation() 안에는 수행시간을 측정하는 코드를 담고 있다.

  Runner클래스를 만들어 이 클래스를 활용하도록 한다.

@Component
public class AppRunner implements ApplicationRunner {
  
  @Autowired
  EventService eventService;
  
  @Override
  public void run(ApplicationArguments args) throws Exception {
    eventService.created();
    eventService.operation();
    eventService.deleted();
  }
}

  결과값은

  created

  1013

  operation

  2007

  deleted

  이렇게 출력된다. 물론 값은 계속 차이가 있을 수 있다.

 

  execution expression

  이제 이것을 AOP를 이용하여 개선한다.

  위 코드에서는

long begin = System.currentTimeMillis();
System.out.println(System.currentTimeMillis() - begin);

 

  이 코드들이 반복해 여러곳에서 사용되고 있다.

  이 코드는 Aspect로 묶어서 관리되는 것이 편한것이다.

 

  Aspect클래스는 다음과 같이 정의한다.

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.stereotype.Component;


@Component
@Aspect
public class PerfAspect {
  
  @Around("execution(* com.example..*.EventService.*(..))")
  public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
    long begin = System.currentTimeMillis();
    Object retVal = pjp.proceed();
    System.out.println(System.currentTimeMillis() - begin);
    return retVal;
  }
}

  @Aspect 로 Aspect클래스임을 정의하고 Advice를 정의한다.

  Pointcut 시점은 @Around 형태로 정의한다.

  execution의 경우는 com.example밑에 있는 모든 클래스 중 EventService 안에 들어 있는 모든 메서드에 이 행위를

  적용하라는 것이다.

  이부분이 execution expression이라고 한다. Pointcut을 설정하는 부분이다.

 

  AOP를 적용했으니 적용될 클래스에서 Aspect로 대체된 부분은 삭제해준다.

import org.springframework.stereotype.Component;

@Component
public class SimpleServiceEvent implements EventService {
  
  @Override
  public void created() {
  
    try {
      Thread.sleep(1000);
    }catch(InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("created");
  }
  
  @Override
  public void operation() {
    
    try {
      Thread.sleep(2000);
    }catch(InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("operation");
  }
  
  @Override
  public void deleted() {
    System.out.println("deleted");
  }
}

  다시 실행하면 결과값은

  created

  1006

  operation

  2013

  deleted

  0

  이렇게 출력된다.

 

  여기서의 문제는 created()와 operation()에만 Aspect가 적용되었어야 했는데 deleted()에도 적용이 되었다.

  이를 어노테이션 기반 Advice 정의로 해결한다.

  

  일단 다음과 같은 어노테이션을 만들어야 한다.

import java.lang.annotation.*;

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PerfLogging {
}

  그리고 Aspect 클래스를 다음과 같이 수정한다.

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.stereotype.Component;


@Component
@Aspect
public class PerfAspect {
  
  @Around("@annotation(PerfLogging)")
  public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
    long begin = System.currentTimeMillis();
    Object retVal = pjp.proceed();
    System.out.println(System.currentTimeMillis() - begin);
    return retVal;
  }
}

  execution expression을 어노테이션으로 대체한 것이다.

  이제 적용될 클래스의 메서드에 @PerfLogging을 붙여야 한다.

import org.springframework.stereotype.Component;

@Component
public class SimpleServiceEvent implements EventService {
  
  @PerfLogging
  @Override
  public void created() {
  
    try {
      Thread.sleep(1000);
    }catch(InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("created");
  }
  
  @PerfLogging
  @Override
  public void operation() {
    
    try {
      Thread.sleep(2000);
    }catch(InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("operation");
  }
  
  @Override
  public void deleted() {
    System.out.println("deleted");
  }
}

  결과값은

  created

  1011

  operation

  2001

  deleted

  이렇게 deleted에 한번 더 출력되지 않고 정상적으로 출력된다.

 

 

레퍼런스

● 코드로 배우는 스프링 웹 프로젝트

● dailyheumsi.tistory.com/202

'Spring' 카테고리의 다른 글

JPA란?  (0) 2022.01.13
IOC(Inversion Of Control)  (0) 2021.02.21
의존성 주입(Dependency Injection, DI)  (0) 2021.02.18
Spring MVC (Front Controller Pattern)  (0) 2021.02.17
Spring MVC  (0) 2021.02.16

의존성주입(Dependency Injection, DI)란?

  객체를 직접 생성하는 것이 아닌 외부에서 객체를 주입시켜 사용하는 방식이며

  IoC(Inversion Of Controll)라고 하는 소프트웨어 디자인 패턴 중 하나다.

  이대의 IOC는 인스턴스를제어하는 주도권이 역전된다는 의미로 사용되는데, 컴포넌트를 구성하는 인스턴스의 생성과

  의존 관계의 연결 처리를 해당 소스코드가 아닌 DI Container에서 대신 해주기 때문에 제어가 역전되었다고 본다.

  Spring의 공식문서에서는 Spring Framework를 DI Container가 아니라 IOC Container 라고 기재하고 있다.

  개발을 하다보면 코드에 의존성이 생기기 마련이다.

class Programmer {
  private Coffee coffee;
  
  public Programmer() {
    this.coffee = coffee();
  }

  public startProgramming() {
    this.coffee.drink();
    ....
  }
}

  이와 같이 Programmer 클래스에서 startProgramming 함수가 호출되기 위해서는 Coffee클래스를 필요로 한다.

  이것을 programmer 클래스는 Coffee 클래스의 의존성을 갖는다 라고 한다.

  이와 같이 코드를 설계하였을 때는 코드의 재활용성이 떨어지고, Coffee클래스가 수정되었을 때 Programmer 클래스도

  함께 수정해줘야 하는 문제가 발생한다.

  즉, 결합도(coupling)가 높아지게 된다.

 

  DI로 프로그램을 설계했을때 다음과 같은 이점을 얻을 수 있다.

    ● Unit Test가 용이해진다.

    ● 코드의 재활용성을 높여준다.

    ● 객체간의 의존성(종속성)을 줄이거나 없앨 수 있다.

    ● 객체간의 결합도를 낮추면서 유연한 코드를 작성할 수 있다.

 

  Spring 외에도 DI컨테이너를 제공하는 프레임워크는 많이 있다. 잘 알려진 것은 다음과 같고

  기본적인 기능은 대체로 비슷하다.

     CDI(Contexts & Dependency Injection)

     Google Guide

     Dagger

 

  만약 DI를 사용하지 않고 Coffee클래스를 상속받는 Americano  클래스를 사용한다고 하면 직접 수정해줘야 하는

  문제가 발생한다.

class Coffee {
  ...
}

class Americano extends Coffee {
  ...
}

class Programmer {
  private Coffee coffee;
  
  public Programmer() {
    this.coffee = new Americano();
  }
}

  이렇게 커피는 아메리카노가 되도록 모든 클래스를 수정해줘야 한다.

  만약 이렇게 하나의 클래스가 아니라 100개 혹은 그 이상이라면? 다 찾기도 힘들뿐더러 놓치는 부분도 생기게 된다.

  이렇게 되면 너무 비효율적이다.

 

class Programmer {
  private Coffee coffee;
  
  public Programmer(Coffee coffee) {
    this.coffee = coffee;
  }
  
  public startProgramming() {
    this.coffee.drink();
  }
}

  그래서 의존성 주입으로 위와같이 작성할 수 있다.

  이렇게 필요한(의존하는) 클래스를 직접 생성하는 것이 아닌 주입해줌으로써 객체간의 결합도를 줄이고 좀 더 

  유연한 코드를 작성할 수 있게 된다.

  어떤 클래스가 필요로하는 컴포넌트를 외부에서 생성한 후 내부에서 사용가능하게 만들어 주는 과정을

  의존성 주입(DI)한다 또는 인젝션(Injection)한다고 말한다.

  그리고 이러한 의존성 주입을 자동으로 처리하는 기반을 DI Container라고 한다.

 

 

레퍼런스

velog.io/@wlsdud2194/what-is-di

dayzen1258.tistory.com/entry/%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85%EC%9D%B4%EB%9E%80-DI

스프링 철저 입문

'Spring' 카테고리의 다른 글

IOC(Inversion Of Control)  (0) 2021.02.21
AOP(Aspect Oriented Programming)  (0) 2021.02.20
Spring MVC (Front Controller Pattern)  (0) 2021.02.17
Spring MVC  (0) 2021.02.16
Spring과 SpringBoot 의 차이  (0) 2021.02.15

Front Controller Pattern architecture

  Spring MVC는 '프론트 컨트롤러 패턴(Front Controller)'이라고 하는 아키텍처를 채택하고 있다.

  프론트 컨트롤러 패턴은 클라이언트 요청을 프론트 컨트롤러라는 컴포넌트가 받아 요청 내용에 따라 수행하는

  핸들러(Handler)를 선택하는 아키텍처다.

 

  프론트 컨트롤러 패턴은 공통적인 처리를 프론트 컨트롤러에 통합할 수 있어서 핸들러에서 처리하는 내용을

  줄일 수 있다.

  Spring MVC 아키텍처에서는 다음과 같은 기능들을 프론트 컨트롤러가 대행해주고 있다.

    ● 클라이언트의 요청 접수

    ● 요청 데이터를 자바 객체로 변환

    ● 입력값 검사 실행(Bean Validation)

    ● 핸들러 호출

    ● 뷰 선택

    ● 클라이언트에 요청 결과 응답

    ● 예외처리

 

  Spring MVC의 프론트 컨트롤러는 org.springfarmework.web.servlet.DispatcherServlet 클래스(서블릿)로

  구현되어 있으며, 다음과 같은 흐름으로 처리를 수행한다.

    1. DispatcherServlet 클래스는 클라이언트의 요청을 받는다.

    2. DIspatcherServlet 클래스는 HandlerMapping 인터페이스의 getHandler 메서드를 호출해서 요청 처리를 하는 

       Handler 객체(컨트롤러)를 가져온다.

    3. DispatcherServlet 클래스는 HandlerAdapter 인터페이스의 handle 메서드를 호출해서 Handler 객체의 메서드

       호출을 의뢰한다.

    4. HandlerAdapter 인터페이스 구현 클래스는 Handler 객체에 구현된 메서드를 호출해서 요청 처리를 수행한다.

    5. DispatcherServlet 클래스는 ViewResolver 인터페이스의 resolveViewName 메서드를 호출해서 Handler 객체에서

       반환된 뷰 이름에 대응하는 View 인터페이스 객체를 가져온다.

    6. DispatcherServlet 클래스는 View 인터페이스의 render메서드를 호출해서 응답 데이터에 대한 렌더링을 요청한다.

       View 인터페이스의 구현 클래스에는 JSP와 같은 템플릿 엔진을 사용해 렌더링할 데이터를 생성한다.

    7. DispatcherServlet 클래스는 클라이언트에 응답을 반환한다.

 

  위의 흐름도를 보면 프론트 컨트롤러의 처리 내용 대부분이 인터페이스를 통해 실행되고 있음을 알 수 있다.

  이는 Spring MVC가 가진 특징 중 하나로 인터페이스를 통해 프레이무어크의 기능을 확장할 수 있다는 것을

  알 수 있다.

  이 외에도 프레임워크가 동작하는데 사용되는 다양한 인터페이스가 준비되어 있다.

 

프론트 컨트롤러를 구성하는 컴포넌트

  DispatcherServlet

    프론트 컨트롤러와 연동되는 진입점 역할을 하며 기본적인 처리 흐름을 제어하는 사령탑 역할을 한다.

    다음 표에서 설명하는 인터페이스와도 연동되어 프레임워크의 전체 기능을 완성한다.

인터페이스명

역할

HandlerExceptinoResolver

예외처치를 하기 위한 인터페이스다.

Spring MVC가 제공하는 기본 구현 클래스가 적용되어 있다.

LocaleResolver,

LocaleContextResolver

클라이언트의 로컬정보를 확인하기 위한 인터페이스다.

Spring MVC가 제공하는 기본 구현 클래스가 적용되어 있다.

ThemeResolver

클라이언트의 테마(UI)를 결정하기 위한 인터페이스다.

Spring MVC가 제공하는 기본 구현 클래스가 적용되어 있다.

FlashMapManager

FlashMap 이라는 객체를 관리하기 위한 인터페이스다.

FlashMapPRG(Post Redirect Get) 패턴의 RedirectGet사이에서 모델을 공유하기 위한 Map객체다.

Spring MVC에서 제공하는 기본 구현 클래스가 적용되어 있다.

RequestToViewNameTraslator

핸들러가 뷰 이름과 뷰를 반환하지 않은 경우에 적용되는 뷰 이름을 해결하기 위한 인터페이스다.

Spring MVC에서 제공하는 기본 구현 클래스가 적용되어 있다.

HandlerInterceptor

핸들러 실행 전후에 하는 공통 처리를 구현하기 위한 인터페이스다. 이 인터페이스는 애플리케이션 개발자가 구현하고 Spring MVC에 등록해서 사용할 수 있다.

MultipartResolver

멀티파트 요청을 처리하기 위한 인터페이스다.

Spring MVC에서 몇가지 구현클래스가 제공되고 있지만 기본적으로 적용되지 않는다.

 

  Handler

    Spring MVC에서 하는 일은 프론트 컨트롤러가 받은 요청에 따라 필요한 처리를 수행하는 것이다.

    프레임워크 관점에서는 핸들러라고 부르지만 개발자가 작성하는 클래스 관점에서는 컨트롤러라고 부른다.

    Spring MVC에서는 다음 두가지 방법으로 컨트롤러를 구현할 수 있다.

      ● @Controller 어노테이션(org.springframework.stereotype.Controller)을 클래스에 지정하고 요청 처리를 수행하는

          메서드에 @RequestMapping을 지정한 클래스를 작성한다.

@Controller
public class WelcomeController {
  
  @RequestMapping("/")
  public String home(Model model) {
    model.addAttribute("now", new Date());
    
    return "home";
  }
}

 

      ● org.springframework.web.servlet.mvc.Controller 인터페이스의 구현 클래스를 작성하고 요청을 처리할

          메서드(handleRequest)를 구현한다.

          Spring Framework 3.0.0 이전부터 지원되는 전통적인 구현방법으로 예전부터 Spring을 사용해온 개발자에게는

          친숙한 방법이지만 신규 애플리케이션을 구축하는 경우에는 이 방법으로 컨트롤러를 구현하는것을 권장하지

          않는다고 한다.

@Component("/")
public class WelcomeHomeController extends AbstractController {
  
  @Override
  protected ModelAndView handleRequestInternal(
    HttpServletRequest request, HttpServletResponse response) throws Exception {
      
      ModelAndView mav = new ModelAndView("home");
      mav.addObject("now", new Date());
      
      return mav;
  }
}

 

  HandlerMapping

    HandlerMapping 인터페이스는 요청에 대응할 핸들러를 선택하는 역할을 수행한다.

    Spring MVC는 비슷한 기능을 하는 다양한 종류의 구현 클래스를 제공하는데 그 중에서도 현대적인 개발 방법으로

    사용되는 구현 클래스는 RequestMappingHandlerMapping이다.

    RequestMappingHandlerMapping클래스는 @RequestMapping에 정의된 설정 정보를 바탕으로 실행할 핸들러를

    선택한다. 예를 들어 다음과 같은 컨트롤러 클래스에서는 hello 메서드와 goodbye메서드가 핸들러로 인식된다.

@Controller
public class GreetingController {
  
  @RequestMapping("/hello")
  public String hello() {
    return "hello";
  }
  
  @RequestMapping("/gooodbye")
  public String goodbye() {
    return "goodbye";
  }
}

    핸들러로 인식된 메서드(hello 와 goodbye메서드)는 다음과 같이 @Requestmapping에 지정된 요청 매핑 정보와

    매핑된다.

    이렇게 /hello라는 요청이 들어오면 요청 매핑정보에서 /hello를 찾아 해당 핸들러를 선택하게 된다.

    실제로 클라이언트에서 요청이 올 경우 RequestMappingHandlerMapping 클래스는 요청내용(요청 경로나

    HTTP 메서드 등)과 요청 매핑 정보를 매칭해서 실행할 핸들러를 선택한다.

 

  HandlerAdaptor

    핸들러 메서드를 호출하는 역할을 한다. Spring MVC는 비슷한 기능을 하는 다양한 종류의 구현 클래스를 제공하지만

    ReqeustMappingHandlerMapping 클래스에 의해 선택된 핸들러 메서드를 호출할 때는

    ReqeustMappingHandlerAdaptor 클래스를 사용한다.

 

    RequestMappingHanlderAdaptor 클래스에는 핸들러 메서드에 매개변수를 전달하고 메서드의 처리 결과를

    반환값으로 되돌려 보내는 것과 같은 Spring MVC에서 상당히 중요한 기능을 수행한다.

    핸들러 메서드에 매개변수를 전달할 때는 요청받은 데이터를 자바 객체로 변환하고, 입력값이 올바른지

    검사(Bean Validation)하는 것까지 한번에 이뤄진다.

 

    인수나 반환값에 지정할 수 있는 타입으로는 다양한 타입이 기본적으로 지원되지만 상황에따라 기본적으로 지원되지

    않는 타입을 지원해야 할 수도 있다.

    이러한 상황에 대응하기 위해 Spring MVC는 핸들러 메서드 시그니처를 유연하게 정의할 수 있도록 다음과 같은

    인터페이스를 제공한다.

인터페이스명

역할

HandlerMethodArgumentResolver

핸들러 메서드 매개변수에 전달하는 값을 다르기 위한 인터페이스

HandlerMethodReturnValueHandler

핸들러 메서드에서 반환된 값을 처리하기 위한 인터페이스

 

  ViewResolver

    핸들러에서 반환한 뷰 이름을 보고 이후에 사용할 View 인터페이스의 구현 클래스를 선택하는 역할을 한다.

    Spring MVC는 viewResolver의 다양한 구현 클래스를 제공하는데 주요 구현 클래스는 아래와 같다.

클래스명

설명

InternalResourceViewResolver

뷰가 JSP일 때 사용하며, 가장 기본적인 ViewResolver.

BeanNameViewResolver

DI 컨테이너에 등록된 빈의 형태로 뷰 객체를 가져올 때 사용한다.

 

  View

    클라이언트에 반환하는 응답데이터를 생성하는 역할을 한다. Spring MVC는 다양한 구현 클래스를 제공하는데

    주요 클래스는 아래와 같다.

클래스명

설명

InternalResourceView

템플릿 엔진을로 JSP를 이용할 때 사용하는 클래스

JstlView

템플릿 엔진으로 JSP + JSTL을 이용할 때 사용하는 클래스

 

 

 

레퍼런스

스프링 철저 입문

'Spring' 카테고리의 다른 글

AOP(Aspect Oriented Programming)  (0) 2021.02.20
의존성 주입(Dependency Injection, DI)  (0) 2021.02.18
Spring MVC  (0) 2021.02.16
Spring과 SpringBoot 의 차이  (0) 2021.02.15
mybatis null값처리.  (0) 2020.10.29

Spring MVC

  Spring MVC는 Web Application 개발을 위한 Framework로 architecture에는 MVC 패턴을 채택하고 있다.

  이와 비슷한 역할을 하는 프레임워크로는 스트럿츠(Struts)와 JSF(Java Server Faces)가 유명한데, 이러한 MVC 패턴을

  적용한 프레임워크들은 크게 액션기반 프레임워크와 컴포넌트 프레임워크로 나눌 수 있다.

 

  액션기반 프레임워크는 요청에 따라 실행할 처리행위(action)를 결정하고, 처리결과로 HTML과 같은 응답을 반환하는

  프레임워크다. Spring MVC나 스트럿츠가 액션기반의 프레임워크에 해당하며, 구조가 단순해서 이해하기가 쉽고

  확장성이 높다는 특징이 있다.

 

  반면 컴포넌트 기반 프레임워크는 요청과 응답을 추상화(은폐)하고, 화면을 구성하는 컴포넌트를 기반으로

  web Application을 개발하는 프레임워크다. JSF는 컴포넌트 기반의 프레임워크이며, 버튼이나 입력 필드 등의 화면

  부품을 공통화 할 수 있어 재사용하기 쉽다는 특징이 있다.

 

  Spring MVC는 3.0에서 크게 개선되었다. POJO(Plain Old Java Object)형태로 구현하는 방식이나

  Annotation 기반 설정, Servlet API의 추상화, Spring DI Container와의 연계, 풍부한 확장 기능을 지원하고, 

  각종 서드파티 라이브러리(3rd-party Library, Third party Library)와의 연계와 같은 다양한 특징과 함께

  엔터프라이즈 시스템 개발에 필요한 기능을 갖추고 있다.

 

  지원하는 서드파티 라이브러리로는 JSON을 다루기 위한 Jackson, 템플릿 엔진인 Apache Tiles와 FreeMarker,

  RSS나 Feed를 처리하기 위한 Rome, 보고서를 출력하기 위한 JasperReports, bean 검증을 위한

  Hibernate vaildator, 날짜나 시간 정보처리를 위한 Joda-Time등이 있다.

  그밖에도 템플릿엔진 역할을 하는 Thymeleaf같이 서드파티 라이브러리 자체가 Spring을 지원하는

  형태도 있다.

 

  MVC패턴을 적용한 Web Application은 Model, View, Controller와 같은 세가지 역할의 컴포넌트로 구성되어

  클라이언트의 요청을 처리한다.

  각 컴포넌트에 의한 일반적인 처리흐름은 다음과 같다.

  MVC 패턴에서 각 컴포넌트의 역할

컴포넌트명

설명

모델(Model)

Application 상태(데이터)나 비즈니스 로직을 제공하는 컴포넌트

데이터 디자인을 담당한다. ) 상품목록, 주문내역 등

(View)

모델이 보유한 Application 상태(데이터)를 참조하고 클라이언트에 반환할 응답 데이터를 생성하는 컴포넌트

실제로 렌더링되어 보이는 페이지를 담당한다. ) jsp, Html 파일들이 여기에 해당

컨트롤러(Controller)

요청을 받아 모델과 뷰의 호출을 제어하는 컴포넌트로 컨트롤러라는 이름처럼 요청과 응답의 처리 흐름을 제어한다.

사용자의 요청을 받고 응답해주는 로직을 담당한다. ) GET등의 uri매핑이 여기에 해당

 

  Spring MVC는 MVC 패턴을 채택한 프레임워크라고 했지만 정확히 말하면 프론트 컨트롤러(Front Controller)를 채택한

  것이다. 프론트 컨트롤러 패턴은 MVC 패턴이 가진 약점을 개선한 아키텍처 패턴으로서 많은 MVC 프레임워크에서

  사용된다. 프론트 컨트롤러 패턴은 다음 포스팅에 작성한다.

 

 

WebApplication개발의 특징

  Spring MVC는 Web Application 을 매우 편리하게 개발할 수 있는 프레임워크로 다음과 같은 특징이 있다.

  

  ● POJO(Plain Old Java Object) 구현

      컨트롤러나 모델등의 클래스는 POJO형태로 구현된다. 특정 프레임워크에 종속적인 형태로 구현할 필요가 없기

      때문에 단위 테스트를 하는 것이 상대적으로 수월해진다.

 

  ● Annotation을 이용한 정의 정보 설정

      요청 매핑과 같은 각종 정의 정보를 설정파일이 아닌 Annotation방식으로 설정할 수 있다.

      비즈니스 로직과 그 로직을 수행하기 위한 각종 정의 정보를 자바파일 안에서 함께 기술할 수 있기 때문에

      효율적으로 Web Application을 개발할 수 있다.

 

  ● 유연한 메서드 시그니처 정의

      컨트롤러 클래스의 메서드 매개변수에는 처리에 필요한것만 골라서 정의할 수 있다. 인수에 지정할 수 있는

      타입도 다양한 타입이 지원되며, 프레임워크가 인수에 전달하는 값을 자동으로 담아주거나 변환하기 때문에

      사양변경이나 리펙토링에 강한 아키텍처를 갖고 있다. 반환값도 다양한 타입을 지원한다.

 

  ● Servlet API 추상화

      Spring MVC는 Servlet API(HttpServletRequest, HttpServletResponse, HttpSession 등의 API)를 추상화 하는

      기능을 제공한다. Servlet API를 추상화한 구조를 이용하면 컨트롤러 클래스 구현에서 Servlet API를 직접

      사용하는 코드가 제거되기 때문에 컨트롤러 클래스의 테스트가 Servlet API를 사용할때보다

      상대적으로 쉬워진다.

 

  ● View 구현 기술의 추상화

      컨트롤러는 뷰 이름(뷰의 논리적인 이름)을 반환하고 Spring MVC는 뷰 이름에 해당하는 화면이 표시되게

      한다. 이처럼 컨트롤러는 뷰 이름만 알면 되기 때문에 그 뷰가 어떤 구현기술(JSP, Thymeleaf, Servlet API,

      FreeMarker 등)로 만들어졌는지 구체적인 내용을 몰라도 된다.

  

  ● Spring DI Container와의 연계

      Spring MVC는 Spring의 DI Container상에서 동작하는 프레임워크다.

      스프링의 DI Container가 제공하는 DI(Dependency Injection)나 AOP(Aspect Oriented Programming) 같은

      구조를 그대로 활용할 수 있어 Web Application을 효과적으로 개발할 수 있다.

 

  Spring MVC는 Servlet API를 추상화하는 메커니즘을 제공하는 반면에 Servlet API를 직접 사용할 수도 있다.

  쿠키(Cookie)에 데이터를 쓰는것과 같은 일부 작업은 Servlet API를 직접 사용하지 않으면 구현할 수 없는

  경우도 있다.

 

 

Spring MVC의 특징을 잘 살려서 만든 Controller 클래스의 예

 

@Controller
public class WelcomeController {
  
  @Autowired
  MyService myService;
  
  @RequestMapping("/")
  public String home(Model model) {
    Date now = myService.getCurrentDate();
    model.addAttribute("now", now);
    
    return "home";
  }
}

 

MVC Framework로서의 특징

  Spring MVC는 높은 확장성과 Enterprise Application이 필요로 하는 기능을 갖춘 MVC 프레임워크로서

  다음과 같은 특징이 있다.

 

  ● 풍부한 확장 포인트 제공

      Spring MVC에서는 컨트롤러나 뷰와 같이 각 역할별로 필요한 인터페이스를 제공한다. 그래서 기본 동작을

      확장하고 싶다면 인터페이스를 자신만의 방법으로 구현하면 된다. 이러한 인터펭이스는 커스터마이징을 유연하고

      쉽게 만들어주는 확장점이 된다.

 

  ● Enterprise Application에 필요한 기능 제공

      Spring MVC는 단순히 MVC 패턴의 프레임워크 구현만 제공하는 것이 아니다.

      메시지 관리, 세션 관리, 국제화, 파일 업로드와 같은 엔터프라이즈 애플리케이션 용 웹 애플리케이션을 개발할 때

      필요한 다양한 기능들도 함께 제공한다.

 

  ● 3rd-party Library와의 연계 지원

      Spring MVC는 서드파티 라이브러리를 이용할 때 필요한 각종 어댑터를 제공하며 다음과 같은 라이브러리를

      Spring MVC와 연계해서 사용할 수 있다.

       ○ Jackson(JSON / XML 처리)

       ○ Google Gson(JSON 처리)

       ○ Google Protocol Buffers(Protocol Buffers로 불리는 직렬화 형식 처리)

       ○ Apache Tiles(레이아웃 엔진)

       ○ FreeMarker(템플릿 엔진)

       ○ Rome(RSS/Feed 처리)

       ○ JasperReports(보고서 출력)

       ○ ApachePOI(엑셀처리)

       ○ Hibernate Validator(bean 유효성 검증)

       ○ Joda-Time(날짜 / 시간 처리)

      

      또한 서드파티 라이브러리 자체가 Spring MVC와의 연계를 지원하는 형태도 있다.

       ○ Thymeleaf(템플릿 엔진)

       ○ HDIV(보안 강화)

 

 

전반적인 모듈 구성

 

  ● Config

      각종 설정 클래스 파일을 담고 있다.

      클래스 앞에 @Configuration이 붙는다.

      WebMVC 설정 관련 Config를 제외하고 나머지는 모듈화 한 뒤 Application에서 모두 import한다.

 

  ● Controller

      각종 컨트롤러 클래스 파일들을 담고 있다.

      클래스 앞에 @Controller가 붙는다.

      각 컨트롤러 코드는 URI 매핑을 담당한다.

      Service 인스턴스를 가져와 로직을 실행하고 View단에 나가기 전후 작업을 담당한다.

 

  ● DAO(Data Access Object)

      DB에 대해 접근할 때 사용하는 클래스 파일들을 담고 있다.

      클래스앞에 @Repository가 붙는다.

      실제 DAO클래스와 사용할 SQL만을 담고 있는 클래스가 따로 모듈화해서 사용된다.

      DAO가 아닌 Mapper로 사용하는 경우도 있다.

      Mapper의 경우는 DAO와 차이가 있다.

 

  ● DTO(Data Transfer Object)

      데이터를 모델링한 클래스 파일들을 담는다.

      필드(프로퍼티)와 Getter, Setter를 갖는다.

      VO(Value Object)로 사용하는 경우도 많지만 둘은 차이가 있다.

 

  ● Service

      서비스 로직을 담는 클래스 파일들을 담고 있다.

      클래스앞에 @Service가 붙는다.

      Interface로 핵심 로직을 먼저 정의한 후에 클래스로 구현한다.

      필요한 경우네는 DAO를 직접 사용하는 클래스다.

 

 

 

레퍼런스

● 스프링 철저 입문

 dailyheumsi.tistory.com/159

 

 m.blog.naver.com/PostView.nhn?blogId=scw0531&logNo=220984890046&proxyReferer=https:%2F%2Fwww.google.com%2F

Spring이란?

  Spring Framework는 Java 생태계에서 가장 대중적인 응용프로그램 개발 프레임워크다.

  프레임워크란 라이브러리를 포함하는 개념이고 개발자가 만든 코드를 사용한다.

  코드를 어떻게 실행하는지는 코드를 사용하는 프레임워크에 달려있다.

  라이브러리는 간단하게 특정 기능을 하는 코드뭉치다. 개발자는 자기 코드에 라이브러리를 포함시키고

  원하는 기능을 사용해서 개발을 할 수 있다. 원하는 기능을 하는 함수를 콜해서 사용하는 것으로

  라이브러리를 활용한다.

 

  의존성주입(Dependency Injection, DI)과 제어의 역전(Inversion Of Control, IOC)은 스프링에서

  가장 중요한 특징 중 하나다.

  이들로 인해 좀 더 결합도를 낮추는 방식으로 Application을 개발할 수 있다.

  이러한 개발방식으로 개발한 응용프로그램은 단위테스트가 용이하기 때문에 보다 높은 퀄리티의

  프로그램을 개발 할 수 있다.

  스프링 프레임워크에는 많은 기능들이 있는데 그 기능들은 약 스무개의 모듈로 나누어져 있으며

  흔히 발생하는 문제들을 해결하고 있다.

  대중적인 모듈로는 Spring JDBC, Spring MVC, Spring AOP, Spring ORM, Spring JMS, Spring Test,

  Spring Expression Language(SpEL)등이 있다.

 

  관점지향 프로그래밍(Aspect Oriented Programming, AOP)은 Spring Framework에서 아주 강력한

  기능이다.

  객체지향 프로그래밍(Object Oriented Programming, OOP)에서 중요 키포인트는 Class다.

  반면 AOP에서는 관점(Aspect)다.

  예를들어, 기존 프로젝트에 security나 logging등을 추가하고 싶을 때 기존 비즈니스 로직에는 손을 대지 않고

  AOP를 활용하여 추가할 수 있다.

  Spring은 Spring만의 ORM을 갖고 있지 않다. 그러나 Hibernate, Apache Ibatis 등과 같은 ORM과의 매우

  우수한 통합환경을 제공한다.

 

  정리하자면 Spring Framework는 Web Application을 개발하는데 결합도를 낮추는 방향의 개발 방법을 제공한다고

  말할 수 있다.

  Web Application 개발은 Spring의 이러한 컨셉(Dispatcher Servlet, Model And View, View Resolver) 덕분에

  쉬운 개발을 할 수 있게 되었다.

 

 

SpringBoot란?

  Spring Framework는 기능이 많은 만큼 환경설정이 복잡한 편이다. 이에 어려움을 느끼는 사용자들을 위해

  나온 것이 바로 SpringBoot다.

  Spring Framework를 사용하는 프로젝트를 아주 간편하게 셋업할 수 있는 Spring Farmework의 서브 프로젝트로

  Spring을 사용하기 위한 설정의 많은 부분을 자동화하여 사용자가 정말 편하게 Spring을 활용할 수 있도록 돕는다.

  SpringBoot Starter Dependency만 추가해주면 바로 API를 정의하고 내장된 Tomcat이나 Jetty로 

  Web Application Server를 실행할 수 있다.

  실행환경이나 의존성 관리 등의 인프라 관련 등은 신경 쓸 필요없이 바로 코딩을 시작하면 된다.

  프로젝트 생성시에 Spring에서의 복잡한 설정이 아닌 통합된 설정 파일인 application.yml로 간단하게 사용할 수 있다.

 

  SpringBoot의 starter란?

    특정 목적을 달성하기 위한 의존성 그룹이라고 볼 수 있다. starter는 마치 npm처럼 간단하게 dependency를

    제공해주는데 만약 JPA가 필요하다면 pom.xml(메이븐)이나 build.gradle(그래들)에

    'spring-boot-starter-data-jpa' 만 추가해주면 SpringBoot가 그에 필요한 라이브러리들을 알아서 받아온다.

    starter의 명명규칙은 'spring-boot-starter-*' 다.

    JPA 예시에서처럼 * 부분에 원하는 starter명을 명시하면 된다.

    명명규칙을 알면 손쉽게 원하는 라이브러리를 import할 수 있다.

 

 

Spring과 SpringBoot의 차이점은?

  SpringBoot에는 Embeded Tomcat이 내장되어 있기 때문에 Tomcat을 따로 설치하거나 매번 버전관리를 해줘야하는

  수고를 덜어준다.

  SpringBoot에서는 starter를 통한 Dependency가 자동화 되어있기 때문에 Spring에서 각각의 dependency가 호환되는

  버전을 맞춰주고 하나의 버전을 변경하면 나머지도 다 확인해야하는 번거로움이 사라졌다.

  그리고 XML설정을 하지 않아도 되며 jar file을 이용해 자바 옵션만으로도 쉽게 배포가 가능하다.

 

  Web 기반인 Application은 Tomcat이든 WAS든 Web Container가 설치되어 있어야 한다.

  하지만 비교적 규모가 작은 형태의 Application을 실행시키기 위해 그보다 큰 WAS를 따로 설치하기에는 그다지

  효율적이지 않다.

  이런 경우에는 Embeded Container에서 자신의 Application을 실행하도록 SpringBoot를 쓰는게 적당하다.

  

  하지만 비교적 규모가 큰 웹사이트의 경우 이런 구조로 만드는것 보다는 Spring MVC형태로 만들어

  WAS에 배포하는 슷타일이 낫다.

  규모가 크다보니 Embeded Container에서 Application을 실행시키기엔 불안정하기도 하고 WAS에서 관리되는

  데이터 소스나 메시지 서비스를 이용할 수 있기 때문이다.

 

  즉, 어느정도의 서비스를 하는지에 따라 Spring과 SpringBoot에 대한 선택이 달라지는것 같다고 볼 수 있다.

 

  Spring과 SpringBoot는 해결하고자 하는 문제가 다르다.

  Spring은 의존성주입을 통해 객체간의 결합도를 낮추어 코드 재사용성을 향상시키고 단위테스트를 용이하게 할 수

  있도록 해준다.

  Duplicatation / Plumbing Code(반복되는 코드들)를 제거함으로써 개발자가 비즈니스 로직에만 집중할 수 있도록 하며

  다른 프레임워크와의 통합(Intergration with Other Frameworks)으로 Mockito와 같은 다른 프레임워크와 통합하여

  개발자가 비즈니스 로직 외에 신경써야 할 부분을 덜어준다.

  이는 생산성 향상에도 큰 도움을 준다.

 

  SpringBoot는 최소한의 번거로움으로 Spring 기반의 Production급 응용프로그램 및 서비스를 쉽게 만들 수 있도록

  하는 것을 목표로 한다.

  따라서 SpringBoot는 다음과 같은 기능을 지원한다.

    Auto Configuration - 자동설정

    Easy dependency Management - 쉬운 의존성관리

    Embeded Servlet Container Support - 내장 서블릿 컨테이너(내장서버)

    여기서 Auto Configuration은 Sprint 기능을 위한 자동설정, starter 의존성을 통해 간단히 설정하는 것이다.

 

 

  Spring과 SpringBoot는 전혀 다른 새로운 기술이 아니다. SpringBoot는 Spring Framework라는 큰 틀에 속하는

  도구라고 볼 수 있다.

  

 

 

레퍼런스

ssoco.tistory.com/66

annajinee.tistory.com/20

ooeunz.tistory.com/56

monkey3199.github.io/develop/spring/2019/04/14/Spring-And-SpringBoot.html

sas-study.tistory.com/274

 

'Spring' 카테고리의 다른 글

Spring MVC (Front Controller Pattern)  (0) 2021.02.17
Spring MVC  (0) 2021.02.16
mybatis null값처리.  (0) 2020.10.29
getRealPath("") 경로 wtpwebapps에서 변경하기  (0) 2020.10.29
The prefix "mvc" for element "mvc:interceptors" is not bound.  (0) 2020.10.21

멀티쓰레드 프로그래밍(Multi Thread Programming)이란?

  동시에 여러개의 Thread가 수행되는 프로그래밍이다.

  Thread는 각각의 작업공간(context)를 갖는다.

  공유자원이 있는 경우에는 race condition이 발생하게 된다.

  Critical Section에 대한 동기화(Synchronization)의 구현이 필요하다.

 

임계영역(Critical Section)이란?

  두개 이상의 Thread가 동시에 접근하게 되는 리소스다.

  Critical section에 동시에 Thread가 접근하게 되면 실행결과를 보장할 수 없게 된다.

  그래서 Thread간의 순서를 맞추는 동기화(Synchronization)이 필요하다.

 

동기화(Synchronization)이란?

  임계영역(Critical Section)에 여러 Thread가 접근하는 경우 한 Thread가 수행하는 동안 공유자원을 lock하려

  다른 Thread의 접근을 막는다.

  그렇기때문에 동기화를 잘못 구현하게 되면 교착상태(Deadlock)에 빠질 수 있다.

 

 

교착상태(Deadlock)이란?

  t1과 t2라는 Thread가 존재한다고 한다고 가정한다.

  t1은 t2가 끝날때까지 대기하도록 하는 코드가 중간에 있고 t2도 t1이 끝날때까지 대기하도록 하는 코드가 있다고 하면

  이 두 Thread는 처리하지 못하고 계속해서 대기하게 된다.

  t1은 t2가 끝나지 않았으니 대기하게 되고 t2역시 t1이 끝나지 않았으니 기다리게 되며 교착상태에 빠지게 된다.

  이렇게 서로 무한정으로 대기하고 있는 상태를 교착상태(Deadlock)이라고 한다.

  이것을 방지하기 위해 synchronized 메서드에서는 다른 synchronized 메서드를 부르지 않는것이 좋다.

 

예제코드

 

class Bank {
  private int money = 10000;
  
  public void saveMoney(int save) {
    int m = this.getMoney();
    
    try {
      Thread.sleep(3000);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
    
    setMoney(m + save);
  }
  
  public void minusMoney(int minus) {
    int m = this.getMoney();
    
    try {
      Thread.sleep(200);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
    
    setMoney(m - minus);
  }
  
  public int getMoney() {
    return money;
  }
  
  public void setMoney(int money) {
    this.money = money;
  }
}


class Park extends Thread {
  public void run() {
    System.out.println("start save");
    SyncTest.myBank.saveMoney(3000);
    System.out.println("save Money " + SyneTest.myBank.getMoney());
  }
}


class Parkwife extends Thread {
  public void run() {
    System.out.println("start minus");
    SyncTest.myBank.minusMoney(1000);
    System.out.println("minus money " + SyneTest.myBank.getMoney());
  }
}


public class SyncTest {
  
  public static Bank myBank = new Bank();
  
  public static void main(String[] args) throws InterruptedException {
    Park p = new Park();
    p.start();
    
    Thread.sleep(200);
    
    Parkwife pw = new Parkwife();
    pw.start();
  }
}

  Thread에서 가장 많이 보이는 예제인 금액 인출 예제다.

  통장에는 현재 10,000원이 있고 Park은 3,000원을 입금하고 Parkwife는 1,000원을 출금하려 한다.

  그럼 예상되는 결과값은 Park이 먼저 실행되니까 3,000원을 입금해서 save money 13,000이 출력된 후

  Parkwife가 1,000원을 출금해서 minus money 12,000이 출력될 것이라고 생각할 수 있다.

  하지만 결과값은

  start save

  start minus

  minus money 9000

  save money 13000

  이렇게 출력된다.

  출력 순서로만 본다면 save가 시작 된 후에 save에 대한 결과가 나오기 전에 minus가 수행되었다.

  그리고 minus가 먼저 수행되고 save가 수행되었는데 그래도 결과값은 10,000원에 3,000원을 더한 13,000원이

  출력된다.

  

  코드를 보면 save는 sleep으로 3초를 대기하도록 되어있고 minus는 0.2초를 대기하도록 되어있다.

  그럼 save는 일단 수행하자마자 getMoney()로 10,000원을 가져온다. 그리고 3초간의 대기상태에 빠지게 된다.

  그 사이 minus가 수행되며 똑같이 getMoney()로 10,000원을 가져온다. 그리고 minus는 0.2초간의 대기상태에

  빠지게 되고 대기시간이 짧다보니 save의 대기시간이 채 끝나기도 전에 1,000원을 빼주며 처리를 마무리하고

  wife의 run메서드의 마지막 출력문을 출력하게 되어 minus money 9000을 출력하게 된다.

  그 후에 대기하고 있던 save는 3초의 대기시간이 지난 후에 처리하게 되는데 이때 처리하는 m의 값은

  wife가 인출하고 난 뒤 잔액인  9,000원이 아닌 10,000의 값을 그대로 갖고 있게 되고 그 값에서 3,000을 save하게

  된다. 그래서 save money 13000 을 출력하게 되는것이다.

 

  현실에서의 상황으로 생각해보자면 Park이 은행에 가서 은행 직원이랑 얘기하느라 시간을 좀 더 쓰게 되었고

  Parkwife는 바로 인출해갔다면 인출후의 잔액이 바로 동기화가 되어 Park이 입금하는 금액과 더해져야 한다.

  즉, 이 예제에서는 동기화처리가 전혀 되지 않은 상태이기 때문에 두 Thread가 수행되는 동안 각자의 기준에서만

  결과값을 출력했다는 것이다.

  이렇게 되면 원하는 결과값을 출력할 수 없고 이게 처리 중간과정이었다면 뒤의 후폭풍이 심할 수 있기 때문에

  동기화가 꼭 필요하고 중요한 부분이다.

 

 

  위 예제는 아래와 같이 두가지 방식으로 수정할 수 있다.

// synchronized 메서드 방식

class Bank {
  private int money = 10000;
  
  public synchronized void saveMoney(int save) {
    int m = this.getMoney();
    
    try {
      Thread.sleep(3000);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
    
    setMoney(m + save);
  }
  
  public synchronized void minusMoney(int minus) {
    int m = this.getMoney();
    
    try {
      Thread.sleep(200);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
    
    setMoney(m - minus);
  }
  
  public int getMoney() {
    return money;
  }
  
  public void setMoney(int money) {
    this.money = money;
  }
}


class Park extends Thread {
  public void run() {
    System.out.println("start save");
    SyncTest.myBank.saveMoney(3000);
    System.out.println("save money " + SyncTest.myBank.getMoney());
  }
}


class Parkwife extends Thread {
  public void run() {
    System.out.println("start minus");
    SyncTest.myBank.minusMoney(1000);
    System.out.println("minus money " + SyncTest.myBank.getMoney());
  }
}


public class SyncTest {
  
  public static Bank myBank = new Bank();
  
  public static void main(String[] args) {
    Park p = new Park();
    p.start();
    
    Thread.sleep(200);
    
    Parkwife pw = new Parkwife();
    pw.start();
  }
}

  이렇게 saveMoney와 minusMoney에 synchronized를 붙여준다.

  그럼 이 두 메서드가 속해있는 Bank에 lock이 걸리게 된다.

  하나를 수행하게 되면 다른 Thread의 접근을 막아주는 것이다.

  Park이 수행중이라면 Parkwife는 접근할 수 없게 되는 것이다.

 

  그럼 결과값으로

  start save
  start minus
  save money 13000
  minus money 12000

  이렇게 출력된다.

  처음과 같이 save가 수행되고 처리되기 전에 minus가 실행되었지만 synchronized에 의해 접근할 수 없어서

  save가 끝날때까지 대기하게 되고 save money 13000을 출력해서 처리가 마무리 된 다음에야 수행되어서

  minus money 12000을 출력하는 것이다.

 

//synchronized 수행문(block) 방식

class Bank {
  
  private int money = 10000;
  
  public void saveMoney(int save) {
    
    synchronized(this) {
      int m = this.getMoney();
      
      try {
        Thread.sleep(3000);
      }catch (InterruptedException e) {
        e.printStackTrace();
      }
      
      setMoney(m + save);
    }
  }
  
  public void minusMoney(int minus) {
    
    synchronized(this) {
      int m = this.getMoney();
      
      try {
        Thread.sleep(200);
      }catch (InterruptedException e) {
        e.printStackTrace();
      }
      
      setMoney(m - minus);
    }
  }
  
  public int getMoney() {
    return money;
  }
  
  public void setMoney(int money) {
    this.money = money;
  }
}


class Park extends Thread {
  public void run() {
    System.out.println("start save");
    SyncTest.myBank.saveMoney(3000);
    System.out.println("save money " + SyncTest.myBank.getMoney());
  }
}


class Parkwife extends Thread {
  public void run() {
    System.out.println("start minus");
    SyncTest.myBank.minusMoney(1000);
    System.out.println("minus money " + SyncTest.myBank.getMoney());
  }
}


public class SyncTest {
  
  public static Bank myBank = new Bank();
  
  public static void main(String[] args) throws InterruptedException {
    Park p = new Park();
    p.start();
    
    Thread.sleep(200);
    
    Parkwife pw = new Parkwife();
    pw.start();
  }
}

  이렇게 block방식을 사용할 수 있다.

  block은 메서드 구현부에 작성해 주면 된다.

  이렇게 둘다 block으로 구현해도 되고 saveMoey는 block방식 minusMoney는 메서드 방식으로 구현해도 된다.

  하지만 saveMoney에는 구현했지만 minusMoney에서는 구현하지 않았다면 제대로 동기화되지 않는다.

  block 부분에서 this에 lock을 걸고자 하는 객체를 써주면 되는데 여기서는 Bank에 lock을 걸어야 하므로

  this로 쓴것이다.

public void saveMoney(int save) {
  synchronized(this) {
    int m = this.getMoney();
    
    try {
      Thread.sleep(3000);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
    
    setMoney(m + save);
  }
}

public synchronized void minusMoney(int minus) {
  
  int m = this.getMoney();
  
  try {
    Thread.sleep(200);
  }catch (InterruptedException e) {
    e.printStackTrace();
  }
  
  setMoney(m - minus);
}

  이렇게 작성할 수 있다.

 

  그리고 run메서드에서 lock을 걸수도 있는데

class Park extends Thread {
  public synchronized void run() {
  }
}

  이렇게 run에다가 걸면 리소스에 lock을 걸겠다는 것이기 때문에 소용이 없다.

  만약 run메서드에서 lock을 걸고 싶다면

class Park extends Thread {
  public void run() {
    synchronized(SyncTest.myBank) {
      //구현부
    }
  }
}

  이런 형태로 구현하면 된다.

  이렇게 run메서드에 lock을 걸었을때의 차이점으로는 지금까지의 예제 출력문을 보면 start save   start minus

  이렇게 시작했었다. 즉, save가 시작되고 minus도 실행되었지만 대기하게 되었다는 건데 run메서드에 lock을 걸게되면

  save가 끝나고 난 뒤에 minus가 수행되게 된다.

  그래서 start save    save money 13000   start minus    minus money 12000   이렇게 출력된다.

  run에 lock이 걸려있으니 start minus 조차 출력하지 못하고 대기하게 되기 때문이다.

//run에서 lock

class Bank {
  
  private int money = 10000;
  
  public void saveMoney(int save) {
    
      int m = this.getMoney();
      
      try {
        Thread.sleep(3000);
      }catch (InterruptedException e) {
        e.printStackTrace();
      }
      
      setMoney(m + save);
  }
  
  public void minusMoney(int minus) {
    
      int m = this.getMoney();
      
      try {
        Thread.sleep(200);
      }catch (InterruptedException e) {
        e.printStackTrace();
      }
      
      setMoney(m - minus);
  }
  
  public int getMoney() {
    return money;
  }
  
  public void setMoney(int money) {
    this.money = money;
  }
}


class Park extends Thread {
  public void run() {
    synchronized(SyncTest.myBank) {
      System.out.println("start save");
      SyncTest.myBank.saveMoney(3000);
      System.out.println("save money " + SyncTest.myBank.getMoney());
    }
  }
}


class Parkwife extends Thread {
  public void run() {
    synchronized(SyncTest.myBank) {
      System.out.println("start minus");
      SyncTest.myBank.minusMoney(1000);
      System.out.println("minus money " + SyncTest.myBank.getMoney());
    }
  }
}


public class SyncTest {
  
  public static Bank myBank = new Bank();
  
  public static void main(String[] args) throws InterruptedException {
    Park p = new Park();
    p.start();
    
    Thread.sleep(200);
    
    Parkwife pw = new Parkwife();
    pw.start();
  }
}

 

 

wait()

  리소스가 더이상 유효하지 않은 경우 리소스가 사용가능할 때까지 Thread를 non-runnable상태로 전환한다.

  wait 상태가 된 Thread는 notify가 호출될때까지 기다린다.

 

notify() / notifyAll()

  notify()는 wait상태인 Thread중 한 Thread를 runnable한 상태로 깨운다.

  notifyAll()은 wait상태인 모든 Thread가 runnable한 상태가 되도록한다.

  notify()보다는 notifyAll()을 사용하기를 권장한다고 한다.

  특정 Thread가 통지를 받도록 제어하는 것은 어려우므로 모두 깨운 후 스케쥴러에 CPU를 점유하도록 하는 것이

  좀 더 공평하기 때문이다.

 

예제코드

 

import java.util.ArrayList;

class Library {
  
  public ArrayList<String> books = new ArrayList<String>();
  
  public Library() {
    books.add("Java 1");
    books.add("Java 2");
    books.add("Java 3");
    books.add("Java 4");
    books.add("Java 5");
    books.add("Java 6");
  }
  
  public synchronized String rentBook() {
    Thread t = Thread.currentThread();
    
    String title = books.remove(0);
    System.out.println(t.getName() + " : " + title + " rent");
    return title;
  }
  
  public synchronized void returnBook(String title) {
    Thread t = Thread.currentThread();
    
    books.add(title);
    System.out.println(t.getName() + " : " + title + " return");
  }
}


class Student extends Thread {
  
  public void run() {
    
    try {
      String title = LibraryMain.library.rentBook();
      sleep(5000);
      LibraryMain.library.returnBook(title);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}


public class LibraryMain {

  public static Library library = new Library();
  
  public static void main(String[] args) {
    Student std1 = new Student();
    Student std2 = new Student();
    Student std3 = new Student();
    
    std1.start();
    std2.start();
    std3.start();
  }
}

  결과값은

  Thread-0 : Java 1 rent

  Thread-2 : Java 2 rent

  Thread-1 : Java 3 rent

  Thread-0 : Java 1 return

  Thread-1 : Java 3 return

  Thread-2 : Java 2 return

  이렇게 출력된다.

  순서는 실행할때마다 조금씩 달라지기도 한다.

  이 예제코드에서 shared resource가 되는것은 library다.

  동시에 빌릴일은 별로 없겠지만 synchronized는 rent와 return에 걸어주는것이 좋다.

 

  그럼 이제 여기서 학생이 6명이고 책이 3권밖에 없다고 하면 이 코드를 그대로 사용했을 때

  IndexOutOfBoundException 이 발생한다.

  학생 6명이 책을 빌려달라고 했으나 앞에 있는 학생 3명이 책을 다 빌려갔고 남은 3명은 빌릴 책이 존재하지 않기

  때문에 이런 오류가 발생한다.

  이런 경우에는 리소스가 가능하지 않으면 빌리지 못하도록 하면 되는데 이럴 때 wait()과 notify()를 사용한다.

 

/*
  책은 3권이지만 학생이 6명이라면
  notify()를 사용한 예제
*/

import java.util.ArrayList;

class Library {
  
  public ArrayList<String> books = new ArrayList<String>();
  
  public Library() {
    books.add("Java 1");
    books.add("Java 2");
    books.add("Java 3");
  }
  
  public synchronized String rentBook() throws InterruptedException {
    
    Thread t = Thread.currentThread();
    
    if(books.size() == 0) {
      System.out.println(t.getName() + " waiting start");
      wait();
      System.out.println(t.getName() + " waiting end");
    }
    
    String title = books.remove(0);
    System.out.println(t.getName() + " : " + title + " rent");
    return title;
  }
  
  public synchronized void returnBook(String title) {
    
    Thread t = Thread.currentThread();
    
    books.add(title);
    notify();
    
    System.out.println(t.getName() + " : " + title + " return");
  }
}


class Student extends Thread {
  
  public void run() {
    
    try {
      String title = LibraryMain.library.rentBook();
      
      if(title == null) return;
      
      sleep(5000);
      LibraryMain.library.returnBook(title);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}


public class LibraryMain {
  
  public static Library library = new Library();
  
  public static void main(String[] args) {
    Student std1 = new Student();
    Student std2 = new Student();
    Student std3 = new Student();
    Student std4 = new Student();
    Student std5 = new Student();
    Student std6 = new Student();
    
    std1.start();
    std2.start();
    std3.start();
    std4.start();
    std5.start();
    std6.start();
  }
}

  결과값은 

  Thread-0 : Java 1 rent

  Thread-4 : Java 2 rent

  Thread-5 : Java 3 rent

  Thread-3 waiting start

  Thread-2 waiting start

  Thread-1 waiting start

  Thread-4 : Java 2 return

  Thread-3 waiting end

  Thread-3 : Java 2 rent

  Thread-5 : Java 3 return

  Thread-0 : Java 1 return

  Thread-1 waiting end

  Thread-1 : Java 3 rent

  Thread-2 waiting end

  Thread-2 : Java 1 rent

  Thread-2 : Java 1 return

  Thread-1 : Java 3 return

  Thread-3 : Java 2 return

  이렇게 출력된다.

  이것도 물론 순서는 차이가 있을 수 있다.

 

  만약 책이 없을 때 그냥 아예 빌리지 못하게 할거였으면 rent와 run에서 books.size가 0일때 null을 리턴하도록

  했으면 뒤에 3명의 학생은 아예 처리가 안되도록 할 수도 있다.

  하지만 기다렸다가 빌리겠다고 한다면 대기하도록 해야 하기 때문에 이렇게 처리한다.

  rent에서 books.size가 0이면 책의 재고가 없는 것이므로 wait()으로 대기하도록 한다.

  그리고 다른 Thread가 반납하게 되면 notify()로 대기상태인 Thread를 깨워줌으로써 책을 빌릴 수 있도록

  해준다.

 

 

  notifyAll은 다음과 같이 쓴다.

/*
  책은 3권이지만 학생은 6명이다.
  notifyAll()사용
*/

import java.util.ArrayList;

class Library {
  
  public ArrayList<String> books = new ArrayList<String>();
  
  public Library() {
    books.add("Java 1");
    books.add("Java 2");
    books.add("Java 3");
  }
  
  public synchronized String rentBook() throws InterruptedException {
    Thread t = Thread.currentThread();
    
    while(books.size() == 0) {
      System.out.println(t.getName() + " waiting start");
      wait();
      System.out.println(t.getName() + " waiting end");
    }
    
    String title = books.remove(0);
    System.out.println(t.getName() + " : " + title + " rent");
    return title;
  }
  
  public synchronized String returnBook(String title) {
    Thread t = Thread.currentThread();
    
    books.add(title);
    notifyAll();
    System.out.println(t.getName() + " : " + title + " return");
  }
}


class Student extends Thread {
  
  public void run() {
    
    try {
      String title = LibraryMain.library.rentBook();
      
      if(title == null) return;
      
      sleep(5000);
      LibraryMain.library.returnBook(title);
    }catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}


public class LibraryMain {
  
  public static Library library = new Library();
  
  public static void main(String[] args) {
    Student std1 = new Student();
    Student std2 = new Student();
    Student std3 = new Student();
    Student std4 = new Student();
    Student std5 = new Student();
    Student std6 = new Student();
    
    std1.start();
    std2.start();
    std3.start();
    std4.start();
    std5.start();
    std6.start();
  }
}

  결과값은

  Thread-0 : Java 1 rent
  Thread-5 : Java 2 rent
  Thread-3 : Java 3 rent
  Thread-4 waiting start
  Thread-2 waiting start
  Thread-1 waiting start
  Thread-5 : Java 2 return
  Thread-0 : Java 1 return
  Thread-1 waiting end
  Thread-1 : Java 2 rent
  Thread-2 waiting end
  Thread-2 : Java 1 rent
  Thread-4 waiting end
  Thread-4 waiting start
  Thread-3 : Java 3 return
  Thread-4 waiting end
  Thread-4 : Java 3 rent
  Thread-1 : Java 2 return
  Thread-2 : Java 1 return
  Thread-4 : Java 3 return

  이렇게 출력된다.

  역시 순서는 바뀔 수 있다.

 

  notify()를 사용한 경우는 하나씩 깨어나기 때문에 잘 되면 다행이지만 공정하지 못할 수 있다.

  notifyAll()을 사용하면 대기하고 있던 모든 Thread가 깨어난다.

  그럼 반환된 책은 하나인데 모든 스레드가 깨어나면 처음과 같이 IndexOutOfBoundException이 발생할 수 있다.

  그래서 rentBook에서 wait을 걸어주는 부분을 if문이 아닌 while문으로 작성해준다.

  그래서 깨어난 Thread가 못빌리는 상태라면 다시 wait으로 들어갈 수 있도록 해줘야 한다.

  

  결과값을 보면 Thread4, 2, 1이 대기상태로 들어간다.

  그 후에 Thread5와 0이 책을 반납했다. 이때 반납처리가 되면서 대기중이던 4, 2, 1은 모두 깨어난 상태가 되고

  그 중에서 Thread1과 2는 wait이 풀리자마자 도서를 빌렸다.

  하지만 제일 마지막에 깨어난 Thread4는 깨어나서 waiting end를 찍고 책을 빌리려 했지만 books.size가 0이 

  되었으므로 while문을 벗어나지 못하고 다시 wait에 들어가게 된다.

 

  

 

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 자바 객체지향프로그래밍

'JAVA' 카테고리의 다른 글

Servlet&JSP에서 파일 업로드 처리  (0) 2023.10.20
쓰레드(Thread)  (0) 2021.02.13
입출력스트림(IOStream)  (0) 2021.02.12
스트림(Stream)  (0) 2021.02.11
람다식(Lambda)  (0) 2021.02.10

+ Recent posts