판매 할 상품을 등록, 수정하여 목록을 볼 수 있는 상품 관리 서비스를 만들어보자 

 

 

 

도메인 모델

  • 상품 ID
  • 상품명
  • 가격
  • 수량

 

4가지 기능

  • 상품 목록
  • 상품 등록
  • 상품 수정
  • 상품 상세

 

 

 

상품 도메인 개발

 

Item - 상품 객체

@Getter @Setter
public class item {

    private Long id;
    private String itemName;
    private Integer price; //Integer은 null일 가능성이 있기때문
    private int quantity;

    //기본 생성자
    public item() {
    }

    public item(String itemName, Integer price, int quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

 

* @Data 를 쓰게 되면 getter,setter,required,Tostring...다 만들어줘서 위험하니까 필요한 getter,setter 만 쓴다 

 

 

 

 

 

ItemRepository - Item 저장소

@Repository //@Component 가 포함돼있어 컴포넌트 대상이 된다
public class ItemRepository {

    //Map<Long, Item> 상품의 id를 키로 사용하여 상품을 빠르게 검색하고 접근하기 위해
    private static final Map<Long, Item> store = new HashMap<>(); //static
    private static long sequence = 0L; //static

    //저장
    public Item save(Item item) {
        item.setId(++sequence);
        store.put(item.getId(),item);

        return item;
    }

    //조회
    public Item findById(Long id){
        return store.get(id);
    }

    //전체 조회
    public List<Item> findAll(){
        return new ArrayList<>(store.values());
    }

    //업데이트
    public void update(Long itemId, Item updateParam){
        Item findItem = findById(itemId);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }

    //테스트용 해시맵 데이터 날리기
    public void clearStore(){
        store.clear();
    }
}

 

 

 

테스트용 클래스 만들기

ItemRepositoryTest

 

ItemRepository 클래스에 클릭하고 ctrl + shift + T 하면 테스트용 자동완성이 된다 (JUnit5 라이브러리로 하기!) 

생성된것을 확인

 

 

 

먼저 저장 테스트

class ItemRepositoryTest {

    ItemRepository itemRepository = new ItemRepository();

    //테스트 끝날때마다 실행해준다 (깔끔하게 지워주기)
    @AfterEach
    void afterEach() {
        itemRepository.clearStore();
    }

    @Test
    void save(){
        //given
        Item item = new Item("itemA",10000,10);
        //when
        Item savedItem = itemRepository.save(item);
        //then
        Item findItem = itemRepository.findById(item.getId());
        assertThat(findItem).isEqualTo(savedItem); //조회한값=저장된값 테스트
    }
}

 

 


 

import org.assertj.core.api.Assertions;
Assertions.assertThat(findItem).isEqualTo(savedItem);

 

Assertions 에 커서를 두고 alt + enter 해서 Add on-demand~~ 클릭하면 앞으로 Assertions 생략해도 된다. 

 

 

 

 

두번째 전체 조회 테스트

@Test
void findAll(){
    //given
    Item item1 = new Item("item1",10000,10);
    Item item2 = new Item("item2",20000,20);

    itemRepository.save(item1);
    itemRepository.save(item2);
    
    //when
    List<Item> result = itemRepository.findAll();
    
    //then
    assertThat(result.size()).isEqualTo(2); //2개가 맞는지
    assertThat(result).contains(item1,item2); //item1과 item2 가 있는지

}

 

 

 

테스트 동작해보기 

색체크가 나오면 잘 동작한다는것!

 

 

 

 

세번째 업데이트 테스트 해보고 정상동작하는지 실행해보기

@Test
void updateItem(){
    //given
    Item item = new Item("item1",10000,10);

    Item savedItem = itemRepository.save(item);
    Long itemid = savedItem.getId();

    //when
    Item updateParam = new Item("item2", 20000, 20);
    itemRepository.update(itemid,updateParam);

    //then
    Item findItem = itemRepository.findById(itemid);
    assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
    assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
    assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
}

 

 

 

반응형
LIST

 

프로젝트 생성 및 설정

 

 

스프링 부트 스타터 사이트에서 프로젝트 생성

Spring Initializr

 

 

 

 

프로젝트 선택

  • Project: Gradle Project
  • Language: Java
  • Spring Boot: 3.2.3  (뒤에 아무것도 안 붙인걸로 선택한다.) 

Project Metadata

  • Group: hello
  • Artifact: item-service
  • Name: item-service
  • Package name: hello.itemservice
  • Packaging: Jar (주의!) J
  • ava: 17

Dependencies: Spring Web, Lombok, Thymeleaf (JSP를 안 쓰고 타임리프를 사용)

 

 

 

인텔리제이에서 프로젝트 오픈할때는 build.gradle로 오픈한다 

 

 

 

 

롬북 설정은 Settings > Annotation Processors > Enable annotation processiong 체크하기!

 

 

 

Settings > Gradle > IntelliJ 로 변경하기 

gradle로 하게되면 gradle을 통해 실행이 돼서 속도가 느려진다. 

 

 

 

동작확인

 

기본 메인 클래스 실행

8080확인하기

 

 

 

http://localhost:8080/ 접속했을때 Whitelabel Error Page 동작하면 정상이다. 

 

 

 

 

 

웰컴 페이지 추가

 

정적 리소스 /resources/static/index.html

 

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<ul>
    <li>상품 관리
        <ul>
            <li><a href="/basic/items">상품 관리 - 기본</a></li>
        </ul>
    </li>
</ul>
</body>
</html>

 

 

 

서버 재실행하여 웰컴 페이지가 잘 나오는것을 확인할 수 있다.

 

 

 

 

 

반응형
LIST

 

서블릿 과정 대략적인 과정 설명
package hello.servlet.basic;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    }
}

 

HTTP 요청,응답 메세지가 어떤 과정으로 이루어지는지

전 글에서 정리했지만 서블릿과정에 대해 대략적으로 다시 설명을 먼저 해보자면 

 

1. 내가 HTTP 요청을 WAS(Web Application Server)에게 보낸다.

2. WAS는 요청과 응답에 대한 HttpServletRequest와 HttpServletResponse 객체를 생성한다

3. 요청된 URL( http://localhost:8080/hello )에 매핑된 서블릿을 찾기 위해 서블릿 컨테이너에서 찾는다.

4. 해당 서블릿이 발견되면, 해당 서블릿의 service() 메서드가 호출한다.

5. HelloServlet의 service() 메서드가 실행한다.

 

service() 메서드는 HTTP 요청을 처리하는 핵심 메서드이다. 여기에서 요청을 처리하고 응답을 생성하는 코드를 작성해보자

 

 

 

1.  HTTP 요청

 

서블릿 클래스

String username = request.getParameter("username");
package hello.servlet.basic;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = request.getParameter("username");
        System.out.println("username = " + username);
    }
}

** 참고로 @WebServlet 서블릿 어노테이션에서 name, urlPatterns 은 중복이 있으면 안된다

 

request.getParameter() 를 통해 username을 조회할 수 있다

실행 후 URL  http://localhost:8080/hello?username=choi 검색해보기 

 

 

 

쿼리 파라미터 란?

 

http://localhost:8080/hello?username=choi

 

이 부분을 쿼리 파라미터 라고 하는데 서블릿은 쿼리 파라미터를 쉽게 조회할 수 있다.

 

 

 

콘솔 실행결과 System.out.println("username = " + username); 

 

 

 

 

2. HTTP 응답 

응답 http 는 HttpServletResponse 객체에 넣어야한다

package hello.servlet.basic;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      
        String username = request.getParameter("username");
        System.out.println("username = " + username);

        //setContentType,setCharacterEncoding 는 헤더 정보에 들어간다
        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");
        //write()는 http메세지 바디에 데이터가 들어간다
        response.getWriter().write("hello " + username);
    }
}
response.setContentType("text/plain"); 
response.setCharacterEncoding("utf-8");

response.getWriter().write("hello " + username);

 

 

http://localhost:8080/hello?username=choi 쳐보면 내가 보낸 응답 데이터가 보인다

 

 

개발자 모드(F12) 로 보면 ContentType 정보가 나와있음

 

 

* Content-Length 는웹 애플리케이션 서버가 자동으로 생성해준다.

 

 

 


 

welcome 페이지 만들기

  • index.html
  • basic.html

사용자가 애플리케이션에 처음 접속했을 때 보여지는 페이지

예를들어 사용자가 애플리케이션의 URL을 입력했을 때 특정한 페이지로 검색하지 않는 이상 기본적으로 보여지는 페이지이다.

 

 

> main/webapp/index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<ul>
    <li><a href="basic.html">서블릿 basic</a></li>
</ul>
</body>
</html

 

http://localhost:8080/index.html 검색했을때 나오는 index.html 페이지 화면

 

 

> main/webapp/basic.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<ul>
    <li>hello 서블릿
        <ul>
            <li><a href="/hello?username=servlet">hello 서블릿 호출</a></li>
        </ul>
    </li>
    <li>HttpServletRequest
        <ul>
            <li><a href="/request-header">기본 사용법, Header 조회</a></li>
            <li>HTTP 요청 메시지 바디 조회
                <ul>
                    <li><a href="/request-param?username=hello&age=20">GET - 쿼리
                        파라미터</a></li>
                    <li><a href="/basic/hello-form.html">POST - HTML Form</a></
                    li>
                    <li>HTTP API - MessageBody -> Postman 테스트</li>
                </ul>
            </li>
        </ul>
    </li>
    <li>HttpServletResponse
        <ul>
            <li><a href="/response-header">기본 사용법, Header 조회</a></li>
            <li>HTTP 응답 메시지 바디 조회
                <ul>
                    <li><a href="/response-html">HTML 응답</a></li>
                    <li><a href="/response-json">HTTP API JSON 응답</a></li>
                </ul>
            </li>
        </ul>
    </li>
</ul>
</body>
</html>

 

 

 

http://localhost:8080/basic.html 검색했을때 나오는 basic.html 페이지 화면

반응형
LIST

 

HTTP 

HTTP 메시지를 통해 모든것을 전송한다

  • 이미지, 음성, 영상, 파일
  • HTML, TEXT
  • JSON, XML
  • 서버간 데이터 주고받을때도 HTTP 사용

 

 

웹 서버 Web Server

HTTP 기반으로 동작

정적 파일 (정적 HTML, CSS, JS, 이미지, 영상..) 및 클라이언트 요청처리에 주로 중점을 둔다

예로는 Apache 아파치 같은 웹서버가 해당

 

정적 파일이란? 클라이언트 요청 그대로 서버에게 전달되는 것

 

 

WAS (웹 어플리케이션 서버)

HTTP 기반으로 동작

클라이언트 요청에 동적으로 콘텐츠를 생성하여 DB와 상호작용한다

웹 서버 기능 + 동적 웹 어플리케이션 실행

프로그램 코드를 실행하여 애플리케이션 로직 수행

(동적 HTML, 서블릿, JSP, 스프링 MVC)

 

예로는 톰캣이 있다. 

 

 

 

웹 서버와 WAS 차이점은?
  • 웹서버는 정적 리소스 , WAS 는 애플리케이션 로직과 정적 리소스도 모두 제공가능
  • 웹서버는 정적 컨텐츠를 주로 중점을 두고 WAS는 동적 컨텐츠를 생성하고 DB와 상호작용을 통해 웹 애플리케이션을 실행하는데 사용
  • 클라이언트 요청이 들어오면 웹 서버가 정적 콘텐츠를 담당하고 동적 콘텐츠가 필요하면 WAS 가 처리한다고 보면된다. 그 결과를 클라이언틑에게 전달

 

 

 

 

왜 정적, 동적 처리를 웹서버와 WAS에 나누느냐?

만약 WAS가 정적, 동적 요청을 모두 맡아버리면 서버 과부화가 될 수 있고

WAS에서 장애가 났을 때 오류화면이 노출 불가능하다  

 

 

이렇게 WEB, WAS의 역할이 나눠진다면?

정적 리소스는 웹서버가 처리, 동적인 처리는 WAS에 요청을 하여 처리를 했을때

WAS,DB 에서 장애가 일어났을때 WEB 서버가 오류 화면을 보여줄 수 있다. 

(WAS 서버가 잘 죽기 때문에 역할을 나누는게 중요!)

 

 

 


 

이제 서블릿에 대해 알아보자 

 

WAS 가 서블릿을 지원한다.

WAS는 서블릿 실행환경인 서블릿 컨테이너(아파치, 톰캣)를 제공하고 서블릿이 웹 애플리케이션 로직을 처리하도록 도와준다. 

 

HTTP 요청 메시지를 내가 직접 파싱하고 처리하는건 힘드니까 서블릿을 사용하여 대신 처리할 수 있도록 도와준다. 

클라이언트에게 받은 HTTP 요청을 서블릿에게 전달하고, 서블릿은 요청을 처리하고 응답도 생성해준다 
개발자는 요청과 응답에 대한 처리 로직을 서블릿 클래스에서 작성하여 요청 정보를 편리하게 사용할 수 있다.

 

 

 

서블릿 특징

  • 웹 브라우저가 생성한 요청 HTTP 메시지를 필요한 정보를 추출하고 처리

 

 

  • 서버에서 HTTP 응답 메시지 생성하여 클라이언트에게 반환

 

 

1. 패키지 생성하기

hello > servlet > basic 패키지 순으로 생성하고

 

2. 자바 클래스 생성

basic 패키지에 HelloServlet 자바 클래스를 생성

 

 

 

HelloServlet 클래스

service 오버라이드

ctrl + O 하면 service 메서드(자물쇠 모양) 선택

: 서블릿이 호출되면 이 service  메서드가 호출된다.

 

 

 

서블릿 클래스

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
       
    }
}

 

 

  • urlPatterns 은 url에 /hello 이 있으면 해당 서블릿 코드가 실행된다.  
  • HttpServletRequest 객체에는 HTTP 요청에 관한 정보가 담겨져 있다
    • request 객체를 통해 파라미터 값을 찾을 수 있다.
    • 예를들어 getParameter("username") 을 통해 username 파라미터 값 추출
  • HttpServletResponse 객체에는 HTTP 응답에 관한 정보가 담겨져 있다
  • 서블릿은 HttpServletRequest , HttpServletResponse 객체를 통해 클라이언트의 HTTP 요청 정보,응답 정보를 추출하여 처리한다. 

 

 

전체 서블릿 과정

 

 

  1. 나의 서버인 웹 브라우저가 http://localhost:8080/hello url을 통해 WAS에게 HTTP 요청을 보낸다
  2. WAS 서버가 request, response 객체를 생성
  3. 서블릿 컨테이너는 URL에(urlPatterns = "/hello") 매핑된 서블릿 클래스 'HelloServlet' 를 찾아서 확인
    1. @WebServlet(name = "helloServlet", urlPatterns = "/hello")
  4. 서블릿 컨테이너는 helloServlet 에 객체 생성
  5. 해당 서블릿의 service() 메서드를 호출하여 WAS 서버가 만든 request, response 객체를 전달하여 HTTP 요청과 응답을 처리
  6. 끝나고 서블릿 리턴하여 response 객체정보로 HTTP 응답 메세지를 생성
  7. 웹 브라우저에 응답 메세지 전달

 

* 여기서 응답 생성과 응답 메세지 생성은 조금 다른 개념

'응답 생성'은 서블릿 컨테이너에서 리턴하기 전에 설정, 서블릿 리턴 후에 '응답 메세지'를 생성

 

 

 

 

 

테스트해보기

System.out.println("HelloServlet.service"); 

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("HelloServlet.service");
    }
}

 

 

 

run 실행해보니 이런 에러가 났다 

 

 

 

방법1. Gradle 옵션으로 변경

setting > Gradle >  IntelliJ IDEA 이 아닌 Gradle로 옵션을 변경한다

 

인텔리제이 무료버전은 Gradle로 설정해야 한다고

Jar 파일의 경우는 문제가 없는데, War의 경우 톰캣이 정상 시작되지 않는 문제가 발생한다.

하지만 Gradle 로 할 경우 속도저하가 일어나기 때문에 조금 별로다 

 

 

 

 

방법2. build.gradle 에서 다음 코드를 주석처리하기

 

IntelliJ IDEA 옵션으로 그대로 하되

providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'  제거 후 reflesh 

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	//providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

 

run 실행하면 정상작동! 로그에 8080 확인하고 url 검색해보기 

 

 

 

url 에 http://localhost:8080/hello 검색하면 빈 화면이 나온다 그럼 정상!

 

 

그리고 콘솔창을 보면 System.out.println("HelloServlet.service");  이 호출된 것을 볼 수 있다.

 

 

 

 

이번에는 request, response 객체 추출해보기

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("HelloServlet.service");
        System.out.println("request = " + request);
        System.out.println("response = " + response);
    }
}

 

 

 

 

반응형
LIST

 

 

프로젝트 생성

 

스프링 부트 3.x 버전으로 프로젝트 생성하기 

  • Packaging: War 로 하면 JSP 를 실행할 수 있으므로 꼭 필요
  • 3.x 이상은 자바 17버전 이상을 사용해야한다.

 

 

 

자바 17버전으로 쓰기 때문에 꼭 환경변수 확인필수!

- 나는 이전에 자바 11버전을 썼기때문에 JAVA_HOME에서 17로 수정했다 

 

 

 

 

※ 번외

이클립스에서 프로젝트 가져오기 

(이클립스로는 오류가 나서 해결이 안되어 그냥 인텔리제이로 변경했다..)

 

 

 

인텔리제이는 프로젝트 가져올때 open > 저장한 프로젝트의 build.gradle을 선택하면 프로젝트를 가져올 수 있다.

 

 

인텔리제이 설정
File > Setting > Gradle 

스프링 부트 3.2 부터 IntelliJ IDEA 가 아닌 Gradle 옵션을 선택한 상태

 

  • 차이점
    • IntelliJ IDEA 으로 실행 : tomcat 의존성 유지
    • Gradle : tomcat 의존성 제거

 

 

 

 

 

servletApplication.main()메소드 무한로딩

 

인텔리에서에서 프로젝트 실행했더니 왜 계속 무한실행이 되는걸까?

 

로딩 부분은 프로젝트 로딩 시점에 의존성을 불러올 때만 발생하는 것이고 실행에는 전혀 문제가 없다고 한다!

gradle에서 IntelliJ IDEA로 변경하면 무한로딩이 안된다고는 하지만 IntelliJ 무료 버전은 gradle 옵션으로 설정한다!

  • 스프링부트 3.x  이상부터  gradle 적용
  • 스프링부트 3.x 이전이면 IntelliJ IDEA

 

어쨋든 정상 실행!

tomcat-started-on-port-8080 확인하고 

 

 

url에 http://localhost:8080 접속해서 whitelabel error page 가 뜨면 성공이다

(서버가 정상적으로 실행이 됐고 들어갈 페이지가 없다는 뜻)

 

 

 

 

 

 


 

 

일단 인텔리제이 다시 설정 

인텔리제이 설정
File > Setting > Gradle 

Gradle가 아닌 IntelliJ IDEA 옵션을 선택한다.

Gradle 을 통해 실행하면 좀 느려서..

 

 

 

 

 

롬북 라이브러리 설정하기 

 

 

setting > Pulgin > Marketplace 에서 롬북 설치 후 재시작 하기

 

 

 

 

setting > Annotation Processors > Enable annotation processing 어노테이션 활성화 체크하기! 

그리고 재시작! 이러면 롬북을 사용할 수 있다.

 

 

 

 

Postman 설치

 

https://www.postman.com/downloads

* 회원가입 후 설치할 수 있음

Postman 은 api 테스트할 때 편리하다 

반응형
LIST

 

 

싱글톤 컨테이너

 

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결해주며 객체 인스턴스를 싱글톤으로 관리한다. 

 

public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    private SingletonService(){
    }

    public static SingletonService getInstance(){
        return instance;
    }
}

 

또한 위 코드처럼 지저분한 싱글톤 패턴코드를 쓰지않고도 스프링 컨테이너는 객체를 하나만 생성해서 싱글톤으로 관리한다.

 

 

 

 

[스프링 컨테이너를 이용해 싱글톤 패턴 테스트]

@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    
    //1. 조회: 호출할 때 마다 같은 객체를 반환
    MemberService memberService1 = ac.getBean("memberService", 
    MemberService.class);
    
    //2. 조회: 호출할 때 마다 같은 객체를 반환
    MemberService memberService2 = ac.getBean("memberService", 
    MemberService.class);
    
    //참조값이 같은 것을 확인
    System.out.println("memberService1 = " + memberService1);
    System.out.println("memberService2 = " + memberService2);
    
    //memberService1 == memberService2
    assertThat(memberService1).isSameAs(memberService2);
}

 

 

 

[결과] 

객체 같음 

 

 

싱글톤 컨테이너를 적용하면 

고객의 요청이 올때마다 객체를 생성하는것이 아닌 이미 만들어진 객체를 공유해서 재사용할 수 있다. 

스프링 컨테이너를 사용하면 싱글톤으로 동작한다고 이해하면 된다. 

 

하지만 싱글톤 방식만 지원하는것은 아니고 요청할때마다 새로운 객체를 생성해서 반환하기도 한다 

 

 

 

 

 

싱글톤 방식의 주의점

 

 

 

하나의 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다

특정 클라이언트가 값을 변경할 수 있으면 안된다

무상태로 설계해야한다 

 

만약 상태를 유지하는 경우 값을 변경하는 경우

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;
    }
}

 

StatefulService 클래스의 price 변수가 아래 코드에는 공유되는 필드가 되는데

클라이언트가 값을 변경시켜서 문제가 된다. 

 

 

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();
        }
    }
}

 

 

statefulService1.order("userA", 10000); 다음에는 statefulService2.order("userB", 20000); 가 있으면 

statefulService2.order("userB", 20000); 가 저장이 되기 때문에 price값은 20000원으로 출력이 된다. 

 

이렇게 공유필드는 조심해야해서 스프링 빈은 항상 무상태로 설계해야한다. 

 

 

 

 

 

@Configuration

 

싱글톤을 위해 존재하는 @Configuration 

@Bean 만 사용하면 스프링 빈으로 등록은 되지만 싱글톤 보장은 하지않기 때문에 

 

스프링 설정정보에는 무조건 @Configuration을 써야 싱글톤을 보장해준다. 

@Configuration
public class AppConfig {

	@Bean
	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}
	
    .....
 }

 

 

 

반응형
LIST

+ Recent posts