서블릿 과정 대략적인 과정 설명
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

 

 

싱글톤 패턴

 

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

 

그렇다면 객체 인스턴스를 2개 이상 생성하지 못하도록 하는 방법은?

private 생성자를 이용해 외부에서 new 키워드를 사용못하게 막기!

 

 

 

싱글톤 패턴이 생겨야 하는 이유

먼저 웹 애플리케이션을 살펴보자면 

보통 웹 애플리케이션은 여러 클라이언트의 요청이 동시에 들어오기 때문에 객체는 무한생성이 된다. 

아래 그림보면 고객이 3번 요청을 하면 3번 객체가 생성이 됨

 

 

위의 그림 코드

@Configuration
public class AppConfig {

	@Bean
	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}
	
	@Bean
	public MemoryMemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}
	
	@Bean
	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(), discountPolicy()); //discountPolicy()로 대체해주기
	}
	
	@Bean
	public DiscountPolicy discountPolicy() {
//		return new FixDiscountPolicy();  // 1만원이든 2만원이든 천원 할인적용
		return new RateDiscountPolicy(); // 1만원 천원할인, 2만원이면 2천원할인
	}
}

 

 

그렇다면 요청할때마다 객체가 계속 생성되는데 모두 다른지 확인해보기

 

[테스트]

public class SingletonTest {
 	@Test
 	@DisplayName("스프링 없는 순수한 DI 컨테이너")
 	void pureContainer() {
    
            AppConfig appConfig = new AppConfig();

            //1. 조회: 호출할 때 마다 객체를 생성
            MemberService memberService1 = appConfig.memberService();

            //2. 조회: 호출할 때 마다 객체를 생성
            MemberService memberService2 = appConfig.memberService();

            //참조값이 다른 것을 확인
            System.out.println("memberService1 = " + memberService1);
            System.out.println("memberService2 = " + memberService2);

            //memberService1 != memberService2
            assertThat(memberService1).isNotSameAs(memberService2);
     }
}

 

 

[결과] 

▶memberService1, memberService2 는 각각 새로운 MemberService 객체를 생성해낸다 

콘솔에 보이듯 요청할때마다 객체가 모두 새로 생성되어 다른 인스턴스인걸 확인할 수 있고

jvm 메모리에 계속 객체가 생성되고 소멸되어 메모리 낭비가 심해진다.

 

 

[해결방안]

그렇기 때문에 객체가 딱 하나만 생성을 해서 공유하도록 설계하는것이 '싱글톤 패턴'이라고 한다. 

 

 

 

 

 

싱글톤 패턴 적용하기

클래스의 인스턴스 1개만 생성하기

private 생성자를 사용하여 외부에 객체가 생성하지 못하도록 한다. 

 

 

싱글톤 패턴 적용한 테스트 코드를 main말고 test 폴더에 생성해보기 

[비 싱글톤]

public class NotSingletonService {

    //생성자
    private NotSingletonService(){
    }

    public NotSingletonService getInstance(){
        return new NotSingletonService();;
    }
}

getInstance() 를 호출할때마다 NotSingletonService 객체가 계속 생성되기 때문에 싱글톤 패턴이 아니다.  

 

 

 

[싱글톤]

public class SingletonService {

    //static 영역에 객체가 딱 1개만 생성되도록
    private static final SingletonService instance = new SingletonService();

    //생성자 private 을 통해 외부로부터 막음
    private SingletonService(){
    }

    //static 메서드를 통해 객체 인스턴스를 조회하도록 
    // instance 를 반환하여 동일한 객체가 반환됨.
    public static SingletonService getInstance(){
        return instance;
    }
}

 

1. private static final SingletonService instance = new SingletonService();

: static 영역의 instance 변수를 통해 SingletonService 객체를 생성한다.

 

2. private SingletonService() {} 

: 생성자 private을 통해 외부에서 객체를 생성하지 못하도록 막아준다.

 

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

:  getInstance() 메서드를 호출할때 이미 1번에서 객체가 생성된 상태여서 이미 생성된 동일한 객체가 반환이 된다. 

 

 

※ 여기서 SingletonService 클래스를 SingletonService  타입인 getInstance () 를 정의하는데
클래스 내에서 클래스를 다시 재정의 한다?


public static SingletonService getInstance(){
        return instance;
}

>> 자바는 클래스 내 클래스를 정의가능하며 이것을 중첩 클래스 또는 내부 클래스라고 한다.
그렇다면 내부 클래스를 정의하는 이유는? 
>> 외부적으로 사용될 일이 없고 내부적으로 별도의 클래스를 정의가 필요할때 사용한다. 
여기서는 SingletonService 타입을 반환하는 이유는 싱글톤 패턴에서 유일한 인스턴스( instance )를 가져오기 위한 설계 구조상 필요한 부분이기 때문에

 

 

 

 

instance 변수와 getInstance() 메서드에 static을 쓰는 이유??

  • 1. 클래스 레벨에서 접근 가능 / 간편 접근성
    • static으로 선언된 정적 변수인 instance는 클래스 로딩시점에 메모리에 올라가서 객체가 생성된다.
    • 그러므로 같은 클래스 내에 static으로 선언한 메서드에서 객체 생성 없이 바로 접근가능하다.
      • ▶ SingletonService.getInstance()
    • 2. 싱글톤 유지
      • 단 하나만의 객체를 생성하고 재사용하는데에 적합하다. 
    • static을 쓰면 클래스 로딩 시점에 객체를 생성하고 동시에 메모리에 할당하기 때문에 시점이 안 맞는 오류를 방지하기도 한다.
  • 만약 getInstance() 메서드를 static으로 선언하지 않으면?
    • 싱글톤으로 구현은 가능
    • getInstance() 메서드를 호출했을때 이미 객체가 생성된 이후여서 메서드를 호출할 수는 있다. 
    • 하지만 static을 선언하면 클래스 내의 명확한 접근성을 보장하기 때문에 쓰는게 좋다. 

 

 

 

[테스트]

public class SingletonTest {
    @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)
}

 

[결과]

객체 같은걸 볼 수 있다. 

 

 

SingletonService는 private를 사용했기때문에 클래스 외부에서 객체 생성을 막아놨다. 

그래서 SingletonTest에서 new SingletonService()를 사용하지 않고, getInstance() 메서드를 통해 이미 만들어진 객체를 가져와서 인스턴스를 반환했다 

 

여기서 중요한 점은 getInstance() 메서드를 두번 호출했지만 반환되는 객체는 동일한 것!

이것이 싱글톤 패턴이다. 

 

 

 

 

싱글톤의 문제점

 

public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    private SingletonService(){
    }

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

 

  • 싱글톤 패턴을 구현하는 위의 코드를 모두 넣어야하는 문제점이 있다.  
  • private 생성자로서 자식클래스를 만들기 어렵다. 
  • 유연성이 떨어진다. (? 정확한 이해는 못하겠지만 DIP 위반이 되기 때문)

 

 

 

 

하지만 스프링 컨테이너는 싱글톤 패턴의 문제점을 해견해준다. 

 

반응형
LIST

관심사의 분리

인터페이스를 역이라 생각하고 

로미오의 역할(인터페이스)을 하는 디카프리오가 여주인공 역할인 줄리엣 역할(인터페이스)을 직접 캐스팅해야한다면 

디카프리오는 공연도 하고 동시에 여주인공도 캐스팅하는 '다양한 책임'을 가지고 있다 

 

 

 

[예시]

private DiscountPolicy discountPolicy = new FixDiscountPolicy(); 

 

OrderServiceIml 클래스에서 직접 객체(DiscountPolicy)를 생성하고 구체적으로 FixDiscountPolicy 를 선택하여 discountPolicy 에 할당한다. 

마치 디카프리오가 여주역할인 줄리엣 역할을 캐스팅하는 것 처럼

 

 

여기서 

관심사를 분리해보자.

 

배우는 본인의 역할만 수행하도록 집중해야한다  

공연을  구성하고 담당배우 캐스팅하는 것은 각 책임을 담당하는 공연 기획자를 만들어 확실히 분리를 하는 것!

인터페이스에 어떤 구현체가 할당될지는 공연 기획자가 해야한다 AppConfig가 공연기획자라고 생각하자

 

 

 

 

 

AppConfig 

 

AppConfig 등장

 

애플리케이션 전체 동작을 설정하고 구성(config)하기 위해 구현객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스이다. (배우들을 담당 역할을 실행하는 책임만 지면된다.)

public class AppConfig {
	public MemberService memberService() {
		return new MemberServiceImpl(new MemoryMemberRepository());
	}
}​

 

 

- 실제 동작에 필요한 구현객체를 생성한다 (memoryMemberRepository 객체 생성)

- 생성한 객체 인스턴스 참조를 생성자를 통해 주입해준다 (= Dependency injection 의존성 주입) 


MemoryMemberRepository 객체를 생성한 후
public class AppConfig {
	public MemberService memberService() {
		return new MemberServiceImpl(new MemoryMemberRepository());
	}
}​


MemberServiceImpl 클래스의 생성자를 생성 한 뒤 
MemoryMemberRepository 객체의 참조값을 전달한다.

public class MemberServiceImpl implements MemberService{
    public MemberServiceImpl(MemberRepository memberRepository) {
         this.memberRepository = memberRepository;
    }
}​

 

 

의존성 주입

  • MemberServiceImpl 은 MemberRepository 인터페이스에만 의존하고
  • MemberRepository 인터페이스를 구현한 MemoryMemberRepository 구현객체에는 의존하지 않음
  • 예를들어 MemberRepository 인터페이스를 구현만 한다면 MemoryMemberRepository 에서DatabaseMemberRepository 로 바꾸어도  MemberServiceImpl 은 변경 없이 DatabaseMemberRepository 주입받아 사용가능하다. 
  • MemberServiceImpl 입장에는 의존관계를 외부(AppConfig)에서 주입해주는 것 같다해서 DI '의존성 주입'이라고 한다.
  • MemberServiceImpl 은 실행에만 집중하면 된다.

 

 

 


 

 

과정 정리 

 

 

1. 실행할때 Appconfig 를 통해 memberService 를 달라고 하면 

public class MemberApp {

	public static void main(String[] args) {
		//관심사 분리 DI
		AppConfig appConfig = new AppConfig();
		MemberService memberService = appConfig.memberService(); 
        
        ...
      }
}

 

 

2. MemberServiceImpl 객체를 반환하면서 생성자로 MemoryMemberRepository 를 참조값으로 주입하여 사용한다. 

public class AppConfig {
	public MemberService memberService() {
		return new MemberServiceImpl(new MemoryMemberRepository());
	}
}​

 

 

 


 

여기까지 AppConfig 에서는 역할에 어떤 구현을 하는지 한눈에 보여야하는데

MemoryMemberRepository() 역할이 안보여서 리팩토링을 해보자

 

AppConfig  리팩터링 

 

 

 

리팩터링이란?

 

코드내부의 기능은 바꾸지 않고 코드 중복 제거하여 가독성을 높이고 개선하는 방식으로 재조정하는 것

 

 

 

 

어떻게? 

new MemoryMemberRepository() 스크랩하고 alt + shift + m 단축키 누르고 Extract Method 창을 연다 

 

 

* Ctrl + Alt + M(Extract Method) 단축키가 보편적인데 안되길래

검색하면 바로 나오는 해결방법이 Geforce Experience 앱에 단축키를 먹어서라고.. 근데 삭제를 해도 안됨

알고보니 단축키가 다른거였다

 

 

 

memberRepository 라고 변경하면 MemoryMemberRepository 에서 memberRepository 로 모두 바뀐다

 

 

[리팩터링 전]

public class AppConfig {
	public MemberService memberService() {
		return new MemberServiceImpl(new MemoryMemberRepository());
	}

	public OrderService orderService() {
		return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
	}
}

 

 

[리팩터링 후]

public class AppConfig {

	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}
	
    //리팩터링
	public MemoryMemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}
	
	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(),new FixDiscountPolicy()); 
	}
}

 

MemoryMemberRepository 를 반환해주는 역할을 만들어서

memberService, orderService 의 참조값에 반복되는 new MemoryMemberRepository() 객체를 대신해 사용한다.

 

예를들어 DatabaseMemberRepository 로 변경하면 일일이 바꿔줄 필요 없이 해당 코드만 바꿔주면 다 변경가능하니까 

코드가 쉬워진다.

public MemoryMemberRepository memberRepository() {
    return new DatabaseMemberRepository();
}

 

 

 

orderService() 부분도 다시 리팩터링 해보자면 

discountPolicy()을 만들어줌으로써 리턴값만 바꿔주면

FixDiscountPolicy() , RateDiscountPolicy() 을 사용하는 리턴값들을 일일이 바꿀 필요가 없다.

public OrderService orderService() {
    return new OrderServiceImpl(memberRepository(), discountPolicy()); //discountPolicy()로 대체해주기
}

public DiscountPolicy discountPolicy() {
	return new FixDiscountPolicy(); 
	//return new RateDiscountPolicy(); 
}

 

 

 

리팩터링 장점

  • 역할을 구현 명확히 분리
  • 특히 역할이 잘 드러남
  • 중복 제거

 

반응형
LIST

+ Recent posts