스프링부트 나가기 전 복습

유요한·2022년 12월 28일
1

Spring Boot

목록 보기
4/25
post-thumbnail

부트로 취업을 위해 평가가 좋은 김영한 강사님의 수업을 듣고자 합니다. 스프링도 부트 기반으로 스프링으로 진행하다보니 부트공부와 스프링 공부가 동시에 할 수 있을거라고 생각합니다. 물론 인프런 강의 뿐만 아니라 기타적으로 검색한 것 정리나 책에서 배운 것들도 함께 포함시킬 예정입니다.


자바 웹 기술 역사

애노테이션 기반의 스프링 MVC 등장

  • @Controller
  • MVC 프레임워크의 춘추 전국 시대 마무리

스프링 부트의 등장

  • 스프링 부트는 서버를 내장
  • 과거에는 서버에 WAS를 직접 설치하고, 소스는 War 파일을 만들어서 설치한 WAS에 배포
  • 스프링 부트는 빌드 결과(Jar)에 WAS 서버를 포함 → 빌드 배포 단순화

스프링 프레임워크

스프링 부트를 나가기 전에 스프링에 대해서 복습겸 한 번 정리하고 넘어가려고 합니다. 스프링 부트는 스프링과 별도의 프레임워크가 아닌 스프링의 연장선이기 때문입니다.

프레임워크란?

사전적의미를 보자면 뼈대 혹은구조다. 이러한 의미를 소프트웨어 관점에서 해석해서 보자면, 프레임워크는 애플리케이션의 아키텍처에 해당하는 골격 코드라고 할 수 있다.

애플리케이션을 개발할 때 가장 중요한 것은 애플리케이션 전체 구조를 결정하는 아키텍처다. 그러한 아키텍처를 직접 개발하는 것이 아니라 프레임워크로 빌려쓰면 비즈니스 로직을 개발하는데만 집중할 수 있다.

스프링 프레임워크

POJO를 기반으로 하는 경량의 환경을 제공한다.

스프링 단어

스프링이라는 단어는 문맥에 따라 다르게 사용된다.

  • 스프링 DI 컨테이너 기술
  • 스프링 프레임워크
  • 스프링 부트, 스프링 프레임워크 등을 모두 포함한 스프링 생태계

스프링 프레임워크의 장점

  • 복잡함에 반기를 들어서 만들어진 프레임워크

  • 프로젝트 전체 구조를 설계할 때 유용한 프레임워크

  • 다른 프레임워크들의 포용(여러 프레임워크를 혼용해서 사용가능)

  • 개발 생산성과 개발도구의 지원

    스프링 MVC 기본 구조

스프링 프레임워크는 하나의 기능을 위해서만 만들어진 프레임 워크가 아닌 코어라고 할 수 있는 여러 서브 프로젝트들을 결합해서 다양한 상황에 대처할 수 있도록 개발되었다. 그 중 하나가 스프링 MVC 구조이다.

스프링 빈과 의존관계

  • 컴포넌트 스캔과 자동 의존관계 설정
  • 자바 코드로 직접 스프링 빈 등록하기

자바 코드로 직접 스프링 빈 등록하기

SpringConfig를 생성해줍니다.

package com.example.hellospring.service;

import com.example.hellospring.repository.MemberRepositrory;
import com.example.hellospring.repository.MemoryMemberRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepositrory());
    }

    @Bean
    public MemberRepositrory memberRepositrory() {
        return new MemoryMemberRepository();
    }
}

실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다. 그리고 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.

@Autowired 를 통한 DI는 helloController , memberService 등과 같이 스프링이 관리하는 객체에서만 동작한다. 스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.

컴포넌트 스캔과 자동 의존관계 설정

회원 컨트롤러가 회원서비스와 회원 레포지토리를 사용할 수 있게 의존관계 준비

여기서 보면 @Controller로 MemberController을 빈으로 등록을 해주고 생성자에 @Autowired를 해줬는데 에러가 발생하는 것을 볼 수 있습니다.

  • 생성자에 @Autowired가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어준다. 이렇게 객체의 의존관계를 외부에서 넣어주는 것이 DI 즉, 의존성 주입이라고 한다.

  • 이전 테스트에서는 개발자가 직접 주입했고, 여기서는 @Autowired에 의해 스프링이 주입해준다.

그러면 여기서 왜 에러가 발생했을까?

이렇게 생각할 수 있다. @Autowired라는 어노테이션을 적었으니 연관된 객체를 스프링이 찾아서 넣어주면 에러가 안생겨야는 것이 아닌가...

맞는 소리기는 하다. 다만 스프링이 컨테이너에서 찾으려고 하면 일단 컨테이너에 빈을 넣어야는데 여기서는 아직 넣지 않았기 때문에 못찾아서 에러가 발생한 것이다.

MemberService를 찾아와야는데 아직 빈등록을 안한 상태이다.

service관련된 클래스이니 @Service어노테이션을 달아줘서 빈등록을 해준다. 그러면 이제 에러가 사라진 것을 볼 수 있다.

그러면이제 전부 어노테이션 처리를 해주자!

이제 그러면 스프링은 MemberService가 만들어질 때 빈으로 등록하고 생성자를 호출할 때 @Autowired 어노테이션을 만나서 필요한 것을 넣어줄 것이다. 그렇기 때문에 MemoryMemberRepository가 구현체라서 MemoryMemberRepository에 @Repository 어노테이션을 해준다.

  • memberService와 memberRepository가 스프링 컨테이너에 스프링 빈으로 등록되었다.

스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다(유일하게 하나만 등록해서 공유한다) 따라서 같은 스프링 빈이면 모두 같은 인스턴스다. 설정으로 싱글톤이 아니게 설정할 수 있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다

지금 같은 경우는 생성자 주입의 방법이다.

@Autowired 어노테이션을 이용한 의존성 주입은 3가지 방법이 있습니다.

  1. 필드 주입
  2. 수정자 주입
  3. 생성자 주입

이중 생성자 주입이 제일 권고되는 사항이다.

필드 주입

필드 주입방식은 Class에 속한 Field위에 @Autowired 어노테이션을 붙여주면 된다.

수정자 주입(Setter Injection)

이게 스프링할 때 가장 많이 사용했던 방법이였다.

위의 WebProject Class에 아래와 같이 Setter Mehod를 만들어 준 후 @Autowired 어노테이션을 붙여줍니다.

이걸 룸북을 사용한 설정자 주입으로 바꾸면 다음과 같이 된다.

위에 꺼는 setter을 만들어준 다음 @Autowired만 붙여줘서 했다면 아래는 setter도 어노테이션을 사용해서 간단하게 만들 수 있다. 의미는 같지만 코드를 줄일 수 있는 장점이 있다.

생성자 주입(Constructor Injection)

생성자는 빈 생성자가 아닌 클래스의 필드를 파라미터로 사용하는 생성자여야 합니다.

이것을 룸북을 사용하면 줄일 수 있다.

Lombok의 @AllArgsConstructor 어노테이션은 모든 필드를 파라미터로 받는 생성자를 만들어주는 역할을 한다. @Setter 어노테이션과 달리 @Autowired를 붙일 수 있는 속성은 없는데 Developer인스턴스는 정상적으로 생성자의 argument로 주입이 됩니다. Spring 4.3 버전 이후부터 단일 생성자의 경우 묵시적 자동 주입이 가능합니다. 즉, 단일 생성자일 경우 @Autowired 어노테이션을 붙이지 않아도 자동으로 생성자 주입을 해주는 것입니다.

추가적으로 모든 필드를 파라미터로 받는 생성자가 아닌 특정 필드만 파라미터로 받는 생성자를 생성하고 싶을 때는 @RequiredArgsConstructor Class에 붙여주고, 파라미터로 받고 싶은 필드에는 @NonNull어노테이션 혹은 final을 붙여주시면 됩니다.

생성자 주입을 권고하는 이유

1. SRP(단일 책임의 원칙)를 위반할 확률이 줄어든다.
우리가 작성한 비즈니스 로직을 담당하는 클래스는 하나의 책임에 집중되어 있어야 합니다. 이는 객체지향 프로그래밍의 원칙 중 하나입니다. 생성자 주입을 사용하지 않고 필드 주입을 사용하게 될 경우 클래스 내부에 선언된 필드에 그저 @Autowired를 붙이는 것만으로 쉽게 의존성을 사용할 수 있습니다. 쉽게 의존성을 주입할 수 있다는 것은 하나의 클래스가 여러 가지 기능을 담당하게 만들기도 쉽다는 이야기랑 같습니다. 그러나 생성자 주입을 사용하게 되면 생성자 파라미터에 사용하고자 하는 필드를 모두 넣어주어야 하기 때문에 코드가 길어지고 그로 인해 경각심을 가질 수 있습니다.

2. 필드에 final을 선언할 수 있다.
생성자 주입을 제외한 필드, 수정자 주입은 final을 선언할 수 없습니다. 필드에 final을 붙이기 위해서는 클래스의 인스턴스가 생성될 때 final이 붙은 필드를 반드시 초기화해야 합니다. Field /Setter Injection은 우선 인스턴스가 생성된 후에 해당 필드에 의존성 주입이 진행되므로 final을 붙일 수 없습니다. 그러나 생성자 주입은 필드를 파라미터로 받는 생성자를 통해 클래스의 인스턴스가 생성될 때 의존성 주입이 일어나고, 이 때 final 붙은 필드가 초기화됩니다. 우리가 웹 개발을 할 때 Bean객체의 필드 값이 바뀌는 일은 거의 없을 겁니다. 그러므로 필드에 final을 붙여 불변성을 가지도록 하는 것이 좋습니다.

3. DI 컨테이너에 독립적인 테스트 코드를 작성할 수 있다.
개발을 할 때 테스트 코드를 작성하는 것은 매우 중요합니다. 필드 주입을 사용하게 되면 테스트 코드에서 어떻게 내부 필드에 인스턴스를 넣어 줄 수 있을까요??? 아래와 같이 일반적인 JUnit을 사용하는 테스트 코드에서는 불가능합니다. 왜냐하면 일반적으로 필드의 접근 제한자를 public으로 하게 되면 외부에서 필드의 값을 변경할 수 있으므로 대부분은 이를 방지하기 위해 private로 선언합니다. Field Injection일 때 테스트 코드를 작성하기 위해서는 DI컨테이너를 사용하는 테스트 코드를 작성해야 합니다. 그러나 Constructor / Setter Injection을 사용하게 되면 DI컨테이너에 독립적으로 테스트 코드를 작성할 수 있습니다.

4. 순환 참조를 발견할 수 있다.

스프링 구동 순서, 과정

프로젝트 구동은 web.xml에서 시작한다. web.xml 상단에는 가장 먼저 구동되는 Context Listener가 등록되어 있다.

1) ContextLoaderListenr는 해당 웹 어플리케이션을 구동하게 되면 같이 작동이 시작되므로 해당 프로젝트를 실행하면 가장 먼저 로그를 출력하면서 실행된다.

2) root-context.xml이 처리되면 파일에 있는(설정해 놓은) Bean들이 작동한다.

3) root-context.xml이 처리된 후에는 DispatcherServlet이라는 서블릿과 관련된 설정이 작동한다. MVC구조에서 가장 핵심적인 역할을 하는 클래스이며 내부적으로 앱 관련 처리의 준비 작업을 진행한다. 내부적으로 웹 관련 처리의 준비 작업을 진행하기 위해 사용하는 파일이 있고 servler-context.xml이다.

4) DispatcherServlet에서 XmlWebApplicationContext를 이용해서 servlet-context.xml을 로딩하고 해석한다. 이 과정에서 등록된 객체(Bean)들은 기존에 만들어진 객체(Bean)들과 같이 연동하게 된다.

Front-controller 패턴

스프링 MVC 프로젝트를 구성해서 사용한다는 의미는 내부적으로는 root-context로 사용하는 일반 Java영역(흔히 POJO)과 servlet-context로 설정하는 Web관련 영역을 같이 연동해서 구동하게 됩니다.

1 :
사용자의 Reaquest는 Front-Controller인 DispatcherServlet을 통해서 처리합니다. 생성된 프로젝트의 web.xml을 보면 모든 Request를 DispatcherServlet이 받도록 처리하고 있습니다.

2, 3 :
HandlerMapping은 Request의 처리를 담당하는 컨트롤러를 찾기 위해서 존재합니다. HandlerMapping 인터페이스를 구현한 여러 객체들 중 RequestMappingHandlerMaping같은 경우는 개발자가 @RequestMapping어노테이션이 적용된 것을 기준으로 판단하게 됩니다. 적절한 컨트롤러를 찾았다면 HandlerAdapter를 이용해서 해당 컨트롤러를 동작시킵니다.

4 :
Controller는 개발자가 작성하는 클래스로 실제 Request를 처리하는 로직을 작성하게 됩니다. 이 때 View에 전달해야 하는 데이터를 Model이라는 객체에 담아서 전달합니다. Controller는 다양한 타입의 결과를 반환하는데 이에 대한 처리를 ViewResolver를 이용하게 됩니다.

5 :
ViewResolver는 Controller가 반환한 결과를 어떤 View를 통해서 처리하는 것이 좋을지 해석하는 역할입니다. 가장 흔하게 사용하는 설정은 servlet-context.xml에 정의된 InteralResourceViewResovler입니다.

6, 7 :
View는 실제로 응답 보내야 하는 데이터를 JSP등을 이용해서 생성하는 역할을 하게 됩니다. 만들어진 응답은 DispatcherServlet을 통해서 전송됩니다.

여기서 작성해야 하는 것은 Controller, Service, DAO, DTO만 만들어주면 된다.

스프링 MVC의 Controller

스프링 MVC를 이용하는 경우 작성되는 Controller는 다음과 같은 특징이 있습니다.

  • HttpServletRequest, HttpServletResponse를 거의 사용할 필요 없이 필요한 기능 구현

  • 다양한 타입의 파리미터 처리, 다양한 타입의 리턴 타입 사용 가능

  • GET방식, POST방식 등 전송 방식에 대한 처리를 어노테이션으로 처리 가능

  • 상속/인터페이스 방식 대신에 어노테이션만으로도 필요한 설정 가능

스프링 프레임워크의 특징

스프링부트도 스프링 프레임워크의 확장이기 때문에 이 특성을 가지고 간다.

  • POJO 기반의 구성
  • 의존성 주입(DI)을 통한 객체 간의 관계 구성
  • AOP(Aspect-Oriented-Programming)지원
  • 편리한 MVC 구조
  • WAS의 종속적이지 않은 개발 환경

POJO(Plain Old Java Object) 기반의 구성

  • 오래된 방식의 간단한 자바 객체
  • Java 코드에서 일반적으로 객체를 구성하는 방식을 스프링에서도 그대로 사용할 수 있다는 말이다.

장점

  • 객체지향적인 설계를 자유롭게 적용할 수 있다. 그렇기 때문에 언제든지 재활용할 수 있다.

  • POJO로 개발된 코드는 자동화된 테스트에 매우 유리하다.

  • 특정 기술과 환경에 종속되지 않은 오브젝트는 재사용이 가능하며 유연하게 확장할 수 있는 코드를 작성할 수 있다.

  • 특정 종속 코드를 작성하지 않기 때문에 간결한 코드를 작성할 수 있고 테스트를 단순하게 진행할 수 있다.

    "진정한 POJO란 객체지향적인 원리에 충실하면서, 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트이다"

의존성 주입(DI)을 통한 객체 간의 관계 구성

DI : DI란 말그대로 의존성 주입이다.

의존성(Dependaency)이란 하나의 객체가 다른 객체 없이 제대로 된 역할을 할 수 없다는 것을 의미한다. 예를 들어 A객체가 B 객체 없이 동작이 불가능한 상황을 A가 B에 의존적이다 라고 표현한다.

주입(Injection)은 말 그대로 외부에서 밀어 넣는 것을 의미한다. 예를 들어 어떤 객체가 필요로 하는 객체를 외부에서 밀어 넣는 것을 의미한다. 주입을 받는 입장에서는 어떤 객체인지 신경쓸 필요가 없고 어떤 객체에 의존하든 자신이 하던 역할은 변하지 않게 된다. 필요할 때만 주입을 통해서 넣는 것이다.

*의존
ⓐ → ⓑ
a 객체에서 b 객체를 직접 생성

*의존성 주입
ⓐ → ???? ← ⓑ
a가 b를 필요로 한다는 신호를 보내고, b 객체를 주입하는 것은 
외부에서이루어짐

의존성 주입방식을 사용하기 위해서는 ???라는 존재가 필요하게 된다. 스프링 프레임워크에서는 ApplicationContext가 ???라는 존재이며, 필요한 객체를 생성하고 주입까지 해주는 역할을 한다. 따라서 개발자들은 기존의 프로그래밍과 달리 객체와 객체를 분리해서 생성하고, 이러한 객체들을 엮는(Wiring) 작업의 형태로 개발하게 된다.

ApplicationContext가 관리하는 객체들을 빈(Bean)이라 부르고, 빈과 빈 사이의 의존 관계를 처리하는 방식으로 XML, Java 코드, 어노테이션 방식을 이용할 수 있다.

DI 사용 장점

  • 객체 간 결합도를 낮춘다.
  • 유연한 코드 작성 가능
  • 가독성 증가
  • 코드 중복 방지
  • 유지보수가 용이
  • 기존에는 개발자가 직접 객체의 생성과 소멸을 제어했는데 DI로 인해 객체의 생성과 소멸 등 클래스간 의존관계를 스프링 컨테이너가 제어
  • DI는 객체의 생성, 소멸, 의존 관계를 개발자가 직접 설정하는 것이 아니라 XML이나 어노테이션을 통해 스프링 프레임워크가 제어
  • 기존에는 개발자가 직접 객체를 생성해줬던 반면에 스프링 프레임워크에서는 객체의 제어를 스프링이 직접 담당해주는 IoC 특징을 가진다

스프링 컨테이너

  • ApplicationContext를 스프링 컨테이너라고 한다.

  • 기존에는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.

  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성) 정보로 사용한다. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.

  • 스프링 빈은 @Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. (memberService, orderService)

  • 이전에는 개발자가 필요한 객체를 AppConfig를 직접 사용하여 직접 조회했지만 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다. 스프링 빈은 applicationContext.getBean()메서드를 사용해서 찾을 수 있다.

  • 기존에는 개발자가 직접 자바코드로 모든 것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다.

스프링 컨테이너 생성

  • ApplicationContext를 스프링 컨테이너라 한다.

  • ApplicationContext 는 인터페이스이다.

  • 스프링 컨테이너는 XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수 있다.

  • 직전에 AppConfig 를 사용했던 방식이 애노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든것이다.

  • 자바 설정 클래스를 기반으로 스프링 컨테이너( ApplicationContext )를 만들어보자.

    • new AnnotationConfigApplicationContext(AppConfig.class)
    • 이 클래스는 ApplicationContext 인터페이스의 구현체이다.

스프링 컨테이너 생성 과정

  1. 스프링 컨테이너 생성

  • new AnntationconfigApplicationContext(AppConfig.class)
  • 스프링 컨테이너를 생성할 때는 구성 정보를 지정해줘야 한다.
  • 여기서는 AppConfig.class를 구성 정보로 지정했다.
  1. 스프링 빈 등록

스프링 컨테이너ㅏ는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다.

빈이름

  • 빈 이름은 메서드 이름을 사용한다.
  • 빈 이름을 직접 부여할 수 있다.
@Bean(name="memberService2")

빈 이름은 항상 다른 이름을 부여해야 한다. 같은 이름을 부여하면 다른 빈이 무시되거나 기존 빈을 덮어버리거나 설정에 따라 오류가 발생한다.

  1. 스프링 빈과 의존관계 설정

  1. 스프링 빈 의존관계 설정-완료
  • 스프링 컨테이너는 설정 정보를 참고해서 의존 관계를 주입(DI)한다.
  • 단순히 자바 코드를 호출하는 것 같지만 차이가 있다.

BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스다.
  • 스프링 빈을 관리하고 조회하는 역하을 담당한다.
  • getBean()을 제공한다.

ApplicationContext

  • BeanFactory 기능을 모두 상속받아서 제공한다.
  • 빈을 관리하고 검색하는 기능을 BeanFactory가 제공해주는데 그러면 둘의 차이는 뭘까?
  • 애플리케이션을 개발할 때는 빈을 관리하고 조회하는 기능은 물론이고 수 많은 부가기능이 필요하다.

  • 메시지소스를 활용한 국제화 기능
    예를들어서, 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력

  • 환경 변수
    로컬, 개발, 운영 등을 구분해서 처리

  • 애플리케이션 이벤트
    이벤트를 발생하고 구독하는 모델을 편리하게 지원

  • 편리한 리소스 조회
    파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

정리

  • ApplicationContext는 BeanFactory를 상속받는다.
  • ApplicationContext는 빈 관리기능 + 편리한 부가 기능 제공
  • BeanFactory를 직접 사용할 일은 거의 없다. 부가기능이 포함된 ApplicationContext를 사용한다.
  • BeanFactory나 ApplicationContext를 스프링 컨테이너라고 한다.

스프링 빈 설정 메타 정보 - BeanDefinition

  • 스프링은 어떻게 이런 다양한 설정 형식을 지원하는 것일까?
    그 중심에는 BeanDefinition이라는 추상화가 있습니다.

  • 쉽게 이야기 하자면 역할과 구현을 개념적으로 나눈 것이다.

    • XML을 읽어서 BeanDefinition을 만들면 된다.
    • 자바 코드를 읽어서 BeanDefinition을 만들면 된다.
    • 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 된다. 오직 BeanDefinition만 알면 된다.
  • BeanDefinition을 빈 설정 메타정보라 한다.

    @Bean, <bean> 당 각각 하나씩 메타 정보가 생성된다.

  • 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.


웹애플리케이션과 싱글톤

  • 스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.
  • 대부분의 스프링 애플리케이션은 웹 애플리케이션이다. 물론 웹이 아닌 애플리케이션 개발도 얼마든지 개발할 수 있다.
  • 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다

싱글톤

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.

  • 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야한다.

    private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.

  • 스프링을 적용하지 않은 자바로만 된 DI컨테이너인 AppConfig는 요청을 할 때마다 객체를 새로 생성한다.
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다.

    메모리 낭비가 심하다.

  • 해결방안은 해당 객체가 딱 1개만 생성되고 공유하도록 생성하면 된다.

    싱글톤 패턴

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
  • 그래서 객체가 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.

    private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.

package hello.core.singleton;
public class SingletonService {
 //1. static 영역에 객체를 딱 1개만 생성해둔다.
 private static final SingletonService instance = new SingletonService();
 //2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록
허용한다.
 public static SingletonService getInstance() {
 return instance;
 }
 //3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
 private SingletonService() {
 }
 public void logic() {
 System.out.println("싱글톤 객체 로직 호출");
 }
}
  1. static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.

  2. 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.

  3. 딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 막아서 혹시 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.

싱글톤 패턴을 사용하는 테스트 코드를 보자.

@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
public void singletonServiceTest() {
 //private으로 생성자를 막아두었다. 컴파일 오류가 발생한다.
 //new SingletonService();
 //1. 조회: 호출할 때 마다 같은 객체를 반환
 SingletonService singletonService1 = SingletonService.getInstance();
 //2. 조회: 호출할 때 마다 같은 객체를 반환
 SingletonService singletonService2 = SingletonService.getInstance();
 //참조값이 같은 것을 확인
 System.out.println("singletonService1 = " + singletonService1);
 System.out.println("singletonService2 = " + singletonService2);
 // singletonService1 == singletonService2
 assertThat(singletonService1).isSameAs(singletonService2);
 singletonService1.logic();
}
  • private로 new 키워드를 막았다.
  • 호출할 때 마다 같은 객체 인스턴스를 반환하는 것을 확인할 수 있다.

싱글톤 패턴을 구현하는 방법은 여러가지가 있다. 여기서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택한다.

싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다. 하지만 싱글톤 패턴은 다음과 같은 수 많은 문제점들을 가지고 있다.

싱글톤의 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다.

    DIP를 위반

  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다.

싱글톤 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제를 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.

  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.

    이전에 설명한 컨테이너 생성 과정을 자세히 보자. 컨테이너는 객체를 하나만 생성해서 관리한다.

  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
  • 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
    • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
    • DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다.

적용후

싱글톤 방식의 주의점

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태유지(stateful)하게 설계하면 안된다.

  • 무상태(stateless)로 설계해야 한다.

    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    • 가급적 읽기만 가능해야 한다.
    • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
  • 스프링 빈의 필드에 공유 값을 설정하면 큰 장애가 발생할 수 있다.

싱글톤 방식의 주의점

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.

  • 무상태(stateless)로 설계해야 한다!

    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!
    • 가급적 읽기만 가능해야 한다.
    • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다
  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!!

상태를 유지할 경우 발생하는 문제점 예시

package hello.core.singleton;
public class StatefulService {
 private int price; //상태를 유지하는 필드
 public void order(String name, int price) {
 System.out.println("name = " + name + " price = " + price);
 this.price = price; //여기가 문제!
 }
 public int getPrice() {
 return price;
 }
}
package hello.core.singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import
org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
public class StatefulServiceTest {
 @Test
 void statefulServiceSingleton() {
 ApplicationContext ac = new
AnnotationConfigApplicationContext(TestConfig.class);

 StatefulService statefulService1 = ac.getBean("statefulService",
StatefulService.class);

 StatefulService statefulService2 = ac.getBean("statefulService",
StatefulService.class);

 //ThreadA: A사용자 10000원 주문
 statefulService1.order("userA", 10000);
 //ThreadB: B사용자 20000원 주문
 statefulService2.order("userB", 20000);
 //ThreadA: 사용자A 주문 금액 조회
 int price = statefulService1.getPrice();
 //ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
 System.out.println("price = " + price);
 Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
 }
 static class TestConfig {
 @Bean
 public StatefulService statefulService() {
 return new StatefulService();
 }
 }
}
  • 최대한 단순히 설명하기 위해, 실제 쓰레드는 사용하지 않았다.
  • ThreadA가 사용자A 코드를 호출하고 ThreadB가 사용자B 코드를 호출한다 가정하자.
  • StatefulService 의 price 필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다.
  • 사용자A의 주문금액은 10000원이 되어야 하는데, 20000원이라는 결과가 나왔다.
  • 실무에서 이런 경우를 종종 보는데, 이로인해 정말 해결하기 어려운 큰 문제들이 터진다.(몇년에 한번씩 꼭 만난다.)
  • 진짜 공유필드는 조심해야 한다! 스프링 빈은 항상 무상태(stateless)로 설계하자

객체 다이어그램

  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라고 한다.

  • 객체 인스턴스를 생성하고 그 참조값을 전달해서 연결한다.

  • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.

  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.


IoC 컨테이너, DI 컨테이너

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 한다.

  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라고 한다.

  • 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.


웹 서버, 웹 애플리케이션 서버(WAS) 차이

  • 웹 서버는 정적 리소스(파일), WAS는 애플리케이션 로직
  • 사실은 둘의 용어도 경계도 모호함
    • 웹 서버도 프로그램을 실행하는 기능을 포함하기도 함
    • 웹 애플리케이션 서버도 웹 서버의 기능을 제공함
  • 자바는 서블릿 컨테이너 기능을 제공하면 WAS
    • 서블릿 없이 자바 코드를 실행하는 서버 프레임워크도 있음
  • WAS는 애플리케이션 코드를 실행하는데 더 특화

웹 애플리케이션 서버(WAS -Web Application Server)

  • HTTP 기반으로 동작
  • 웹 서버 기능 포함 + (정적 리소스 제공 가능)
  • 프로그램 코드를 실행해서 애플리케이션 로직 수행
    • 동적 HTML, HTTP API(JSON)
    • 서블릿, JSP, 스프링 MVC

웹 시스템 구성 - WEB, WAS, DB

  • 정적 리소스는 웹 서버가 처리
  • 웹 서버는 애플리케이션 로직 같은 동적인 처리가 필요하면 WAS에 요청을 위임
  • WAS는 중요한 애플리케이션 로직 처리 전담

  • 효율적인 리소스 관리
    • 정적 리소스가 많이 사용되면 Web 서버 증설
    • 애플리케이션 리소스가 많이 사용되면 WAS 증설
  • 정적 리소스만 제공하는 웹 서버는 잘 죽지 않음
  • 애플리케이션 로직이 동작하는 WAS 서버는 잘 죽음
  • WAS, DB 장애시 WEB 서버가 오류 화면 제공 가능

서블릿 컨테이너

  • 톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 한다.
  • 서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기 관리
  • 서블릿 객체는 싱글톤으로 관리
    • 고객의 요청이 올 때 마다 계속 객체를 생성하는 것은 비효율
    • 최초 로딩 시점에 서블릿 객체를 미리 만들어두고 재활용
    • 모든 고객 요청은 동일한 서블릿 객체 인스턴스에 접근
    • 공유 변수 사용 주의
    • 서블릿 컨테이너 종료시 함께 종료
  • JSP도 서블릿으로 변환 되어서 사용
  • 동시 요청을 위한 멀티 쓰레드 처리 지원

동시 요청 - 멀티스레드

서블릿 객체를 호출하는 것은 쓰레드입니다.

쓰레드
• 애플리케이션 코드를 하나하나 순차적으로 실행하는 것은 쓰레드
• 자바 메인 메서드를 처음 실행하면 main이라는 이름의 쓰레드가 실행
• 쓰레드가 없다면 자바 애플리케이션 실행이 불가능
• 쓰레드는 한번에 하나의 코드 라인만 수행
• 동시 처리가 필요하면 쓰레드를 추가로 생성

요청 마다 쓰레드 생성

장점

  • 동시 요청을 처리할 수 있다.
  • 리소스(CPU, 메모리)가 허용할 때 까지 처리가능
  • 하나의 쓰레드가 지연 되어도, 나머지 쓰레드는 정상 동작한다.

단점

  • 쓰레드는 생성 비용은 매우 비싸다.

    고객의 요청이 올 때 마다 쓰레드를 생성하면, 응답 속도가 늦어진다.

  • 쓰레드는 컨텍스트 스위칭 비용이 발생한다.

  • 쓰레드 생성에 제한이 없다.

    고객 요청이 너무 많이 오면, CPU, 메모리 임계점을 넘어서 서버가 죽을 수 있다.

    그렇기 때문에 이런식으로 한다.

    쓰레드를 즉각 만들고 다 사용하면 죽이는 것이 아니라 미리 만들어 넣고 사용하고 다시 쓰레드 풀에 넣어준다. 이렇게 하면 장점은 200개를 만들어 놨는데 200개가 넘는 요청이 들어오면 200개는 실행하고 나머지는 쓰레드 대기나 거절을 한다.

쓰레드 풀

요청마다 쓰레드 생성의 단점 보안

특징

  • 필요한 쓰레드를 쓰레드 풀에 보관하고 관리한다.
  • 쓰레드 풀에 생성 가능한 쓰레드의 최대치를 관리한다. 톰캣은 최대 200개 기본 설정(변경 가능)

사용

  • 쓰레드가 필요하면 이미 생성되어 있는 쓰레드를 쓰레드 풀에서 꺼내서 사용한다.
  • 사용을 종료하면 쓰레드 풀에 해당 쓰레드를 반납
  • 최대 쓰레드가 모두 사용중이여서 쓰레드 풀에 쓰레드가 없으면

    기다리는 요청은 거절하거나 특정 숫자만큼만 대기하도록 설정할 수 있다.

장점

  • 쓰레드가 미리 생성되어 있으므로 쓰레드를 생성하고 종료하는 비용(CPU)이 절약되고 응답 시간이 빠르다.
  • 생성 가능한 쓰레드의 최대치가 있으므로 너무 많은 요청이 들어와도 기존 요청은 안전하게 처리할 수 있다.

실무팁

  • WAS의 주요 튜닝 포인트는 최대 쓰레드(max thread) 수이다.
  • 이 값을 너무 낮게 설정하고 동시 요청이 많으면 서버 리소스는 여유롭지만 클라이언트는 응답 지연
  • 이 값을 너무 높게 설정하고 동시 요청이 많으면 CPU, 메모리 리소스 임계점 초과로 서버 다운
  • 장애가 발생하면 클라우드면 일반 서버부터 늘리고 이후에 튜닝한다. 클라우드가 아니면 열심히 튜닝

그러면 적정 숫자는 어떻게 찾을까?
애플리케이션 로직의 복잡도, CPU, 메모리, IO 리소스 상황에 따라 모두 다르고 최대한 실제 서비스와 유사하게 성능 테스트 시도

WAS의 멀티 쓰레드 지원

  • 멀티 쓰레드에 대한 부분은 WAS가 처리
  • 개발자가 멀티 쓰레드 관련 코드를 신경쓰지 않아도 됨
  • 개발자는 마치 싱글 쓰레드 프로그래밍을 하듯이 편리하게 소스 코드를 개발
  • 멀티 쓰레드 환경이므로 싱글톤 객체(서블릿, 스프링 빈)는 주의해서 사용
profile
발전하기 위한 공부

0개의 댓글