반응형

프론트 컨트롤러 패턴 소개

1. FrontController 패턴 특징

 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음

 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출

공통 처리 가능

 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨

 

※ 스프링 웹 MVC와 프론트 컨트롤러

스프링 웹 MVC의 핵심: FrontController패턴, DispatcherServlet이 FrontController 패턴으로 구현되어 있음

 

 

프론트 컨트롤러 도입 - version 1

1. version 1의 구조

 

 

2. 코드

2.1 ControllerV1 인터페이스

package hello.servlet.web.frontcontroller.v1;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public interface ControllerV1 {
	void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

 서블릿과 유사한 ControllerV1 인터페이스를 컨트롤러가 각각 구현함.

  프론트 컨트롤러에서는 해당 인터페이스를 호출해서 다형성으로 각각의 구현 컨트롤러와의 의존관계를 끊을 수 있음.

 

2.2 ControllerV1 인터페이스를 구현한 컨트롤러(회원 등록, 저장, 목록)

MemberFormControllerV1 - 회원 등록 컨트롤러

package hello.servlet.web.frontcontroller.v1.controller;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.web.frontcontroller.v1.ControllerV1;

public class MemberFormControllerV1 implements ControllerV1{
	@Override
	public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String viewPath = "/WEB-INF/views/new-form.jsp";
		RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
		dispatcher.forward(request, response);
	}
}

MemberSaveControllerV1 - 회원 저장 컨트롤러

package hello.servlet.web.frontcontroller.v1.controller;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v1.ControllerV1;


public class MemberSaveControllerV1 implements ControllerV1{
	private MemberRepository memberRepository = MemberRepository.getInstance();

	@Override
	public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String username = request.getParameter("username");
		int age = Integer.parseInt(request.getParameter("age"));
		
		Member member = new Member(username, age);
		memberRepository.save(member);
		
		//Model에 데이터를 보관한다.
		request.setAttribute("member", member);
		
		String viewPath = "/WEB-INF/views/save-result.jsp";
		RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
		dispatcher.forward(request, response);
	}
}

MemberListControllerV1 - 회원 목록 컨트롤러

package hello.servlet.web.frontcontroller.v1.controller;

import java.io.IOException;
import java.util.List;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v1.ControllerV1;

public class MemberListControllerV1 implements ControllerV1 {
	private MemberRepository memberRepository = MemberRepository.getInstance();
	
	@Override
	public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		List<Member> members = memberRepository.findAll();
		
		request.setAttribute("members", members);
		
		String viewPath = "/WEB-INF/views/members.jsp";
		RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
		dispatcher.forward(request, response);		
	}
}

 

2.3 FrontControllerServletV1 - 프론트 컨트롤러

package hello.servlet.web.frontcontroller.v1;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

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

import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;

// urlPatterns 끝의 *은 상위 경로(/front-controller/v1/ )를 포함한 하위 경로 모두 이 서블릿에서 받아들인다는 의미
// /front-controller/v1, /front-controller/v1/depth1, /front-controller/v1/depth1/depth2
// 모두 해당 서블릿에서 받아들인다.
@WebServlet(name="frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet{
	private Map<String, ControllerV1> controllerMap = new HashMap<>();
	
	public FrontControllerServletV1() {
		controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
		controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
		controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
	}

	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		System.out.println("FrontControllerServletV1.service");
		
		// getRequestURI() 메서드로 요청 URI를 얻음
		String requestURI= request.getRequestURI();
		
		// controllerMap에서 적절한 컨트롤러를 찾아서 인터페이스형으로 담음
		ControllerV1 controller = controllerMap.get(requestURI);
		
		// 만약 찾는 컨트롤러가 없으면
		if(controller == null) {
			// 404(SC_NOT_FOUND) 상태 코드를 반환후 리턴
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}
		
		// 컨트롤러가 있으면 ControllerV1 인터페이스를 구현한 컨트롤러에 해당하는
		// process(reqeust, response)메서드를 호출해서 해당 컨트롤러 로직을 실행.
		controller.process(request, response);
		
	}
}

 

 

view 분리 - version 2

1. version2의 구조

 기존에 컨트롤러에서 바로 JSPforward해주는 과정이 사라지고 컨트롤러는 MyView 라는 뷰 인터페이스를 반환하면

컨트롤러에서는 MyView 인터페이스의 render() 메서드를 호출함으로써 해당 인터페이스에서 JSPforward하도록 구조 변경

 

 

2. 코드

2.1 MyView 객체

package hello.servlet.web.frontcontroller;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyView {
    private String viewPath;

    public MyView(String viewPath) {
        this.viewPath = viewPath;
    }

    public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

 컨트롤러에서 JSP forward하는 코드를 모듈화 하기 위해 만든 객체로 클래스 생성시 전달한 jsp 경로를 viewPath로 받아 생성되며 render() 메서드 호출시 인자로 받은 request, response를 인자로 jsp forward를 한다.

 

2.2 ControllerV2

package hello.servlet.web.frontcontroller.v2;

import hello.servlet.web.frontcontroller.MyView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public interface ControllerV2 {
    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

 V1과 유사하지만 반환 타입이 MyView.

 각 컨트롤러는 dispatcher.forward()를 직접 생성해서 호출하지 않아도 된다. 단순히 MyView 객체를 생성하고 거기에 뷰 이름만 넣고 반환하면 됨.

 

2.3 ControllerV2 인터페이스를 구현한 컨트롤러(회원 등록, 저장, 목록)

MemberFormControllerV2 - 회원 등록 컨트롤러

package hello.servlet.web.frontcontroller.v2.controller;

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MemberFormControllerV2 implements ControllerV2 {
    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        return new MyView("/WEB-INF/views/new-form.jsp");
    }
}

MemberSaveControllerV2 - 회원 저장 컨트롤러

package hello.servlet.web.frontcontroller.v2.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MemberSaveControllerV2 implements ControllerV2 {
    MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        Member savedMember = memberRepository.save(member);

        //Model에 데이터 보관
        request.setAttribute("member", member);

        return new MyView("/WEB-INF/views/save-result.jsp");
    }
}

MemberListControllerV2 - 회원 목록 컨트롤러

package hello.servlet.web.frontcontroller.v2.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

public class MemberListControllerV2 implements ControllerV2 {
    MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();

        request.setAttribute("members", members);

        return new MyView("/WEB-INF/views/members.jsp");
    }
}

 모든 컨트롤러에서 공통적으로 구현되있던 dispatcher.forward()가 사라지고 반환타입인 MyView 객체를 생성하여 반환함.

 

2.4 FrontControllerServletV2 - 프론트 컨트롤러

package hello.servlet.web.frontcontroller.v2;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

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

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;


// 1. /front-controller/v2/members/new-form가 urlPattern에 의해서 호출하게됨
@WebServlet(name="frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet{
	private Map<String, ControllerV2> controllerMap = new HashMap<>();
	
	public FrontControllerServletV2() {
		// 3. /front-controller/v2/members/new-form을 controllerMap에서 찾음
		controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
		
		controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
		controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
	}

	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String requestURI= request.getRequestURI();
		
		// 2. /front-controller/v2/members/new-form을 controllerMap에서 찾음
		ControllerV2 controller = controllerMap.get(requestURI);
		if(controller == null) {
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}
		
		// 4. new MemberFormControllerV2()의 process 호출해서 new MyView("/WEB-INF/views/new-form.jsp")를 리턴받음
		// V1과는 다르게 MyView객체를 반환타입으로 반환받는다. 
		MyView view = controller.process(request, response);
		
		// 5. 생성된 myView 인스턴스의 render 실행
		// 반환받은 MyView 객체의 render(request, response) 메서드를 호출하면 forward로직이 수행되어 JSP가 실행된다
		view.render(request, response);
	}
}

 

model 분리 - version 3

1. version3의 구조

 Version2에서 프론트 컨트롤러에서 바로 MyView객체의 render()메서드를 호출해 JSP로 forward 해줬다면

Version3에선 그전에 viewResolver를 호출해 MyView를 반환하도록 함.

 컨트롤러는 MyView가 아니라 ModelView 객체를 반환해주도록 바뀜

※ ModelView

 - 지금까진 컨트롤러에서 모델로 HttpServletRequest의 setAttribute를 사용했는데,

서블릿의 종속성을 제거하기위해 별도의 Model을 만들어 View 이름까지 전달하는 ModelView 객체를 만든다.

 

 

2. 코드

2.1 ModelView 객체

package hello.servlet.web.frontcontroller;

import lombok.Getter;
import lombok.Setter;

import java.util.HashMap;
import java.util.Map;

@Getter @Setter
public class ModelView {
    private String viewName;
    private Map<String, Object> model = new HashMap<>();

    public ModelView(String viewName) {
        this.viewName = viewName;
    }
}

 뷰의 이름과 뷰를 렌더링할 때 필요한 model 객체를 가지고 있는 ModelView 객체.

view의 논리 이름을 저장하는 viewName과 뷰에 필요한 데이터를 담는 Model을 Map으로 구현.

 

2.2 ControllerV3

package hello.servlet.web.frontcontroller.v3;

import hello.servlet.web.frontcontroller.ModelView;
import java.util.Map;

public interface ControllerV3 {
    ModelView process(Map<String, String> paramMap);
}

Request, Response 객체를 인자값으로 받지 않기에 컨트롤러는 서블릿 기술로부터 종속성이 사라짐. 테스트 코드를 구현하기 쉬워졌다는 의미도 됨.

 HttpServletRequest에서 필요한 정보는 프론트 컨트롤러에서 paramMap으로 담아 호출하면 됨.

 ControllerV3에서는 반환타입이 ModelView로 forward될 view의 논리명과 데이터가 담긴 Model이 반환.

 

2.3 ControllerV3 인터페이스를 구현한 컨트롤러(회원 등록, 저장, 목록)

MemberFormControllerV3 - 회원 등록 컨트롤러

package hello.servlet.web.frontcontroller.v3.controller;

import java.util.Map;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

public class MemberFormControllerV3  implements ControllerV3{
	@Override
	public ModelView process(Map<String, String> paramMap) {
		// 파라미터가 없기때문에 paramMap에서 꺼낼게 없음
		return new ModelView("new-form");
	}
}

MemberSaveControllerV3 - 회원 저장 컨트롤러

package hello.servlet.web.frontcontroller.v3.controller;

import java.util.Map;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

public class MemberSaveControllerV3 implements ControllerV3 {
	MemberRepository memberRepository = MemberRepository.getInstance();
	
	@Override
	public ModelView process(Map<String, String> paramMap) {
		// 파라미터를 원래 request.getParameter함수로 꺼내줬던 것을 paramMap에서 꺼냄
		String username = paramMap.get("username");
		int age = Integer.parseInt(paramMap.get("age"));
		
		Member member = new Member(username,age);
		memberRepository.save(member);
		
		ModelView mv = new ModelView("save-result");
		mv.getModel().put("member", member);
		return mv;
	}
}

MemberListControllerV3 - 회원 목록 컨트롤러

package hello.servlet.web.frontcontroller.v3.controller;

import java.util.List;
import java.util.Map;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

public class MemberListControllerV3 implements ControllerV3{
	private MemberRepository memberRepository = MemberRepository.getInstance();
	
	@Override
	public ModelView process(Map<String, String> paramMap) {
		// 파라미터가 없기때문에 paramMap에서 꺼낼게 없음
		List<Member> members = memberRepository.findAll();
		ModelView mv = new ModelView("members");
		mv.getModel().put("members", members);
		return mv;
	}
}

 

2.4 FrontControllerServletV3 - 프론트 컨트롤러

package hello.servlet.web.frontcontroller.v3;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

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

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;


// 1. /front-controller/v3/members/new-form가 urlPattern에 의해서 호출하게됨 
@WebServlet(name="frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet{
	private Map<String, ControllerV3> controllerMap = new HashMap<>();
	
	public FrontControllerServletV3() {
		// 3. /front-controller/v3/members/new-form을 controllerMap에서 찾음
		controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
		
		controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
		controllerMap.put("/front-controller/v3/members", new MemberListControllerV3());
	}

	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String requestURI= request.getRequestURI();
		
		// 2. /front-controller/v3/members/new-form을 controllerMap에서 찾음
		ControllerV3 controller = controllerMap.get(requestURI);
		if(controller == null) {
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}
		
		// 4. new MemberFormControllerV3() 컨트롤러에 넘겨줄 파라미터를 생성
		// paramMap을 만들어줌(컨트롤러에서 쓰기위해)
		Map<String, String> paramMap = createParamMap(request);
		
		// 5. new MemberFormControllerV3() 컨트롤러에 파라미터와 함께 process호출
		// paramMap으로 보낸 파라미터로 처리된 결과 인 model과 view로 보낼 String을 받아옴.
		ModelView mv = controller.process(paramMap);
		
		
		// 6. view 페이지로 넘길 수 있도록 논리 이름인 new-form를 가져옴
		// 논리 이름만 가져옴 new-form
		// view resolver가 다 합쳐줘야함(실제 물리 뷰 경로로 변경)
		String viewName= mv.getViewName();
		
		// 7. viewResolver로 전체 URI를 가져와서 MyView를 인스턴스화
		MyView view = viewResolver(viewName);
		
		// 8. model과 request로 setAttribute를 만들고 view 페이지로 포워딩함
		// 이전에 사용하던 render() 메서드에서 model 파라미터를 추가하는 메서드를 만들어줘야함(오버로딩)
		view.render(mv.getModel(), request, response);
	}
	
	private Map<String, String> createParamMap(HttpServletRequest request){
		Map<String, String> paramMap = new HashMap<>();
		request.getParameterNames().asIterator()
				.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
		return paramMap;
	}

	private MyView viewResolver(String viewName) {
		return new MyView("/WEB-INF/views/"+viewName+".jsp");
	}
}

 

2.5 MyView 객체 수정(render 메서드 오버로딩)

package hello.servlet.web.frontcontroller;

import java.io.IOException;
import java.util.Map;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MyView {
	private String viewPath;

	public MyView(String viewPath) {
		this.viewPath = viewPath;
	}
	
	public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
		RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
		dispatcher.forward(request, response);
	}

	// FrontControllerv3에서 model 객체를 처리하기 위한 메서드 오버로딩
	public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		modelToRequestAttribute(model, request);
		RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
		dispatcher.forward(request, response);
	}
	
	// model 객체를 request의 Attribute에 담기위한 메서드
	private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
		model.forEach((key, value) -> request.setAttribute(key, value));
	}
}

 

 

 

단순하고 실용적인 컨트롤러 - version 4

1. version4의 구조

리팩토링의 방향은 개발자 편의에 맞춰져있기에 큰 변화가 있지는 않고 컨트롤러에서 ModelView를 반환하는 대신 ViewName만 반환함.

 

2. 코드

2.1 ControllerV4

package hello.servlet.web.frontcontroller.v4;

import java.util.Map;

public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);
}

 반환 타입을 String으로 viewName만 그대로 반환하면 되도록 변경.

 ControllerV4 구현 컨트롤러는 따로 ModelView객체를 생성할 필요 없이 viewName만 반환해주면 됨.

 

2.2 ControllerV4 인터페이스를 구현한 컨트롤러(회원 등록, 저장, 목록)

MemberFormControllerV4 - 회원 등록 컨트롤러

package hello.servlet.web.frontcontroller.v4.controller;

import java.util.Map;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v4.ControllerV4;

public class MemberFormControllerV4  implements ControllerV4{
	@Override
	public String process(Map<String, String> paramMap, Map<String, Object> model) {
		return ("new-form");
	}
}

MemberSaveControllerV4 - 회원 저장 컨트롤러

package hello.servlet.web.frontcontroller.v4.controller;

import java.util.Map;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v4.ControllerV4;

public class MemberSaveControllerV4 implements ControllerV4 {
	MemberRepository memberRepository = MemberRepository.getInstance();
	
	@Override
	public String process(Map<String, String> paramMap, Map<String, Object> model) {
		String username = paramMap.get("username");
		int age = Integer.parseInt(paramMap.get("age"));
		
		Member member = new Member(username,age);
		memberRepository.save(member);
		
		model.put("member", member);
		return "save-result";
	}
}

MemberListControllerV4 - 회원 목록 컨트롤러

package hello.servlet.web.frontcontroller.v4.controller;

import java.util.List;
import java.util.Map;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v4.ControllerV4;

public class MemberListControllerV4 implements ControllerV4{
	private MemberRepository memberRepository = MemberRepository.getInstance();
	
	@Override
	public String process(Map<String, String> paramMap, Map<String, Object> model) {
		List<Member> members = memberRepository.findAll();
		
		model.put("members", members);
		return "members";
	}
}

 

2.3 FrontControllerServletV4 - 프론트 컨트롤러

package hello.servlet.web.frontcontroller.v4;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

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

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;


// 1. /front-controller/v4/members/new-form가 urlPattern에 의해서 호출하게됨 
@WebServlet(name="frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet{
	private Map<String, ControllerV4> controllerMap = new HashMap<>();
	
	public FrontControllerServletV4() {
		// 3. /front-controller/v4/members/new-form을 controllerMap에서 찾음
		controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
		
		controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
		controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
	}

	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String requestURI= request.getRequestURI();
		
		// 2. /front-controller/v4/members/new-form을 controllerMap에서 찾음
		ControllerV4 controller = controllerMap.get(requestURI);
		if(controller == null) {
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}
		
		// 4. new MemberFormControllerV4() 컨트롤러에 넘겨줄 파라미터를 생성
		// paramMap을 만들어줌(컨트롤러에서 쓰기위해)
		Map<String, String> paramMap = createParamMap(request);
		// 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 됨.
		Map<String, Object> model = new HashMap<>();
		
		// 5. new MemberFormControllerV4() 컨트롤러에 파라미터와 전달받을 model 참조변수 주소 값과 함께 process호출
		// paramMap으로 보내 처리된 결과인 view로 보낼 String을 받아옴.
		String viewName=controller.process(paramMap ,model);
		
		// 6. viewResolver로 전체 URI를 가져와서 MyView를 인스턴스화
		MyView view = viewResolver(viewName);
		
		// 7. model과 request로 setAttribute를 만들고 view 페이지로 포워딩함
		view.render(model, request, response);
	}
	
	private Map<String, String> createParamMap(HttpServletRequest request){
		Map<String, String> paramMap = new HashMap<>();
		request.getParameterNames().asIterator()
				.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
		return paramMap;
	}

	private MyView viewResolver(String viewName) {
		return new MyView("/WEB-INF/views/"+viewName+".jsp");
	}
}

 

유연한 컨트롤러 - version 5

1. 유연한 컨트롤러의 개요

ControllerV3 방식과 ControllerV4 방식을 모두 사용할 수 있는 컨트롤러를 만들려고 함.

ControllerV3 

public interface ControllerV3 {
    ModelView process(Map<String, String> paramMap);
}

ControllerV4 

public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);
}

두 컨트롤러의 process메서드의 반환타입과 인자 값의 갯수와 타입이 다름.

 다른 타입의 컨트롤러를 혼용해서 쓸 수 있도록 해주는 어댑터 패턴을 활용.

 

2. version5의 구조

 이전과 비교해 달라진 것은 핸들러 어댑터와 핸들러.

핸들러 어댑터

 - 프론트 컨트롤러와 핸들러(컨트롤러) 사이에 핸들러 어댑터가 추가

 - 이 핸들러(컨트롤러)의 어댑터 역할을 하기 때문에 핸들러 어댑터.

 - 여기서 어댑터 역할을 해주는 덕분에 다양한 종류의 컨트롤러(ControllerV3, ContollerV4)를 호출할 수 있음.

※ 핸들러

 - 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경. (컨트롤러를 더 포괄적으로 부르는 명칭)

 - 어댑터를 이용해서 컨트롤러의 개념 뿐 아니라 해당하는 종류의 어댑터만 있으면 처리가 가능

 

3. 코드

3.1 MyHanderAdapter 인터페이스

package hello.servlet.web.frontcontroller.v5;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.web.frontcontroller.ModelView;

public interface MyHandlerAdapter {
	// 이 어댑터가 해당 컨트롤러를 처리할 수 있는지 확인 후 반환하는 메서드
	// 파라미터의 handler는 컨트롤러를 의미
	boolean supports(Object handler);
	
	// 어댑터는 실제 컨트롤러를 호출한 뒤 ModelView를 반환해야 함.
	// 실제 컨트롤러에서 ModelView를 반환하지 않는다면 어댑터에서 생성해서라도 반환해야 함.
	// 컨트롤러가 호출되는 위치가 프론트컨트롤러에서 어댑터로 변경되었다. 
	ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException; 
}

 

3.2 ControllerV3HandlerAdapter

 - MyHanderAdapter 인터페이스를 구현하는 구현체 1

 - ControllerV3를 지원하는 어댑터를 구현

package hello.servlet.web.frontcontroller.v5.adapter;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;

public class ControllerV3HandlerAdapter implements MyHandlerAdapter{
	@Override
	public boolean supports(Object handler) {
		// 핸들러(컨트롤러)가 ControllerV3의 자식이거나 구현체인지 판단해 지원여부를 반환
		// 반환 값이 true일경우 이 어댑터는 해당 핸들러를 지원 지원할 수 있다는 의미
		return (handler instanceof ControllerV3);
	}

	@Override
	public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
		// 인자값으로 받은 핸들러는 Object타입이기에 ControllerV3 타입으로 형변환을 해줘야 함
		// (위의 support 메서드를 통해 ControllerV3의 구현체인것을 확인했으니 형변환시 문제없이 동작)
		ControllerV3 controller =(ControllerV3) handler;
		
		Map<String, String> paramMap = createParamMap(request);
		
		// V3버전은 반환타입이 ModelView이기에 바로 로직 수행 후 반환하면 됨.
		ModelView mv = controller.process(paramMap);
		return mv;
	}
	
	// 파라미터를 받아오는 
	private Map<String, String> createParamMap(HttpServletRequest request){
		Map<String, String> paramMap = new HashMap<>();
		request.getParameterNames().asIterator()
				.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
		return paramMap;
	}
}

 

3.3 FrontControllerServletV5 - 프론트 컨트롤러 (ControllerV3HandlerAdapter와 V3Handler들(컨트롤러들)먼저 적용)

package hello.servlet.web.frontcontroller.v5;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;

@WebServlet(name="frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
	// 기존 frontControllerServlet
	//private Map<String, ControllerV3> controllerMap = new HashMap<>();
	
	// 핸들러를 담는 맵의 value type이 Object인 이유는 담길 핸들러가 V3, V4중 어느 핸들러 인터페이스일지 모르기 때문
	private final Map<String, Object> handlerMappingMap = new HashMap<>();
	
	// 핸들러 어댑터(ControllerV3HandlerAdapter, ControllerV4HandlerAdapter)들을 담아둘 리스트 컬랙션 객체
	private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
	
	public FrontControllerServletV5() {
		initHandlerMappingMap();
		initHandlerAdapters();
	}

	// urlPattern에 매핑되는 컨트롤러를 초기화해주는 메서드
	private void initHandlerMappingMap() {
		handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
		handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
		handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
	}

	// 사용할 어댑터를 리스트 콜렉션에 추가하는 작업을 하는 메서드
	private void initHandlerAdapters() {
		handlerAdapters.add(new ControllerV3HandlerAdapter());
	}
	
	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// 초기화한 핸들러 매핑 정보에서 URL을 key로 매핑된 핸들러(컨트롤러)를 찾아 반환
		Object handler = getHandler(request);
		
		if(handler == null) {
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}
		
		// 핸들러를 처리 할 수 있는 어댑터 조회
		MyHandlerAdapter adapter = getHandlerAdapter(handler);		
		
		// 어댑터의 handle 메서드를 통해 실제 어댑터가 구현한 로직이 수행
		// 어댑터는 handler(컨트롤러)를 호출하고 handler는 로직에서 자신이 지원하는 핸들러(ControllerV3, ControllerV4)로 형변환해서 로직 수행 후 ModelView로 리턴
		ModelView mv = adapter.handle(request, response, handler);
		
		String viewName= mv.getViewName();
		MyView view = viewResolver(viewName);
		
		view.render(mv.getModel(), request, response);
	}

	private Object getHandler(HttpServletRequest request) {
		String requestURI= request.getRequestURI();
		return handlerMappingMap.get(requestURI);
	}

	private MyHandlerAdapter getHandlerAdapter(Object handler) {
		// handlerAdapters 리스트 콜렉션에 등록된 어댑터를 순회하며 해당 핸들러가 지원되는지 확인하기위한 루프
		for(MyHandlerAdapter adapter : handlerAdapters) {
			// 지원하는 어댑터가 있다면 그 어댑터 반환
			if(adapter.supports(handler)) {
				return adapter;
			}
		}
		// 지원하는 어댑터가 없으면 Exception
		throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler="+ handler);
	}

	private MyView viewResolver(String viewName) {
		return new MyView("/WEB-INF/views/"+viewName+".jsp");
	}
}

3.4 ControllerV4HandlerAdapter

 - MyHanderAdapter 인터페이스를 구현하는 구현체 2

 - ControllerV4를 지원하는 어댑터를 구현

package hello.servlet.web.frontcontroller.v5.adapter;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;

public class ControllerV4HandlerAdapter implements MyHandlerAdapter{

	@Override
	public boolean supports(Object handler) {
		// handler가 ControllerV4인지 확인
		return (handler instanceof ControllerV4);
	}

	@Override
	public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
		ControllerV4 controller = (ControllerV4) handler;
		
		Map<String, String> paramMap = createParamMap(request);
		HashMap<String, Object> model = new HashMap<>();
		
		// ControllerV4는 반환타입이 String으로 ModelView가 아니라 viewName만 반환
		// 그래서 이 어댑터에서 이를 ModelView로 만들어줘서 어댑터 반환타입을 일치시켜줘야함
		String viewName = controller.process(paramMap,model);
		
		ModelView mv = new ModelView(viewName);
		mv.setModel(model);
		
		return mv;
	}

	private Map<String, String> createParamMap(HttpServletRequest request){
		Map<String, String> paramMap = new HashMap<>();
		request.getParameterNames().asIterator()
				.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
		return paramMap;
	}
}

 

3.5 FrontControllerServletV5 - 프론트 컨트롤러 (ControllerV4HandlerAdapter와 V4Handler들(컨트롤러들)추가 적용)

	// urlPattern에 매핑되는 컨트롤러를 초기화해주는 메서드
	private void initHandlerMappingMap() {
		handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
		handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
		handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
		
		// V4Handler들(컨트롤러들)추가
		// ControllerV4용 경로(key)와 컨트롤러를 handlerMappingMap에 추가
		handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
		handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
		handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
	}

	// 사용할 어댑터를 리스트 콜렉션에 추가하는 작업을 하는 메서드
	private void initHandlerAdapters() {
		handlerAdapters.add(new ControllerV3HandlerAdapter());
		// ControllerV4도 호환이 되도록 해주는 어댑터를 handlerAdapter 콜렉션에 추가
		handlerAdapters.add(new ControllerV4HandlerAdapter()); // ControllerV4HandlerAdapter 추가
	}

 

컨트롤러 version 정리

 version1: 프론트 컨트롤러를 도입

 version2: View 분리, 단순 반복 되는 뷰 로직 분리

 version3: Model 추가, 컨트롤러의 서블릿 종속성 제거, 뷰 이름 중복 제거

 version4: 단순하고 실용적인 컨트롤러 v3와 거의 비슷, 구현 입장에서 ModelView를 직접 생성해서 반환하지 않도록 View 논리 이름을 String으로 반환

 version5: 유연한 컨트롤러 어댑터 도입, 어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계(controllerV3, controllerV4 어댑터 구현)

반응형
반응형

서블릿으로 웹 애플리케이션 만들기

예시 코드)

@WebServlet(name="memberListServlet",urlPatterns = "/servlet/members")
public class MemberListServlet extends HttpServlet {
	private MemberRepository memberRepository = MemberRepository.getInstance(); 
	
	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		List<Member> members= memberRepository.findAll();
		response.setContentType("text/html");
		response.setCharacterEncoding("utf-8");
		
		PrintWriter w = response.getWriter();
		w.write("<html>");
		w.write("<head>");
		w.write("	<meta charset=\"UTF-8\">");
		w.write("	<title>Title</title>");
		w.write("</head>");
		w.write("<body>");
		w.write("	<a href=\"/index.html\">메인</a>");
		w.write("	<table>");
		w.write("			<thead>");
		w.write("			<th>id</th>");
		w.write("			<th>username</th>");
		w.write("			<th>age</th>");
		w.write("		</thead>");
		w.write("		<tbody>");
				
		for (Member member : members) {
		w.write("			<tr>");
		w.write("				<td>" + member.getId() + "</td>");
		w.write("				<td>" + member.getUsername() + "</td>");
		w.write("				<td>" + member.getAge() + "</td>");
		w.write("			</tr>");
		}
		w.write("		</tbody>");
		w.write("	</table>");
		w.write("</body>");
		w.write("</html>");
	}
}

 정적인 HTML 문서라면 화면이 계속 달라지는 동적인 HTML을 만드는 일은 불가능.

 서블릿에 HTML 소스를 담아 동적으로 원하는 HTML을 만들 수 있음.

 하지만, 서블릿으로만 동적인 HTML을 구현하기엔 복잡하고 비효율적임.

 HTML 문서에 동적으로 변경해야 하는 부분만 자바 코드를 넣어 사용하기 위해 템플릿 엔진이 나옴

 템플릿 엔진을 사용하면 HTML 문서에서 필요한 곳만 코드를 적용해서 동적으로 변경할 수 있음

 템플릿 엔진의 종류 : JSP, Thymeleaf, Freemarker, Velocity 등

 

 

JSP로 웹 애플리케이션 만들기

예시 코드)

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!-- 자바의 import 문-->
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>

<%
	// <% ~~ %> 이 부분에는 자바 코드를 입력할 수 있다.
	// resuest, response 사용 가능
	MemberRepository memberRepository = MemberRepository.getInstance();
	
	String username = request.getParameter("username");
	int age = Integer.parseInt(request.getParameter("age"));
	
	Member member = new Member(username, age);
	memberRepository.save(member);
%>
<html>
<head>
	<title>Title</title>
</head>
<body>
	성공
	<ul>
    	<!-- <%= ~~ %> 이 부분에는 자바 코드를 출력할 수 있다.-->
		<li>id=<%=member.getId()%></li>
		<li>username=<%=member.getUsername()%></li>
		<li>age=<%=member.getAge()%></li>
	</ul>
	<a href="/index.html">메인</a>
</body>
</html>

 JSP는 자바 코드를 그대로 다 사용할 수 있음

 코드의 상위 절반은 비즈니스 로직이고, 나머지 하위 절반만 결과를 HTML로 보여주기 위한 뷰 영역

 JAVA 코드, 데이터를 조회하는 리포지토리 등등 다양한 코드가 모두 JSP에 노출되어 있음

 JSP가 너무 많은 역할을 하게됨

 

 

MVC 패턴으로 웹 애플리케이션 만들기

1. MVC패턴의 개요

1.1 MVC 패턴이 나오게된 배경

 너무 많은 역할

- 하나의 서블릿이나 JSP만으로 비즈니스 로직과 뷰 렌더링을 모두 처리하게 되면, 너무 많은 역할을 하게되고, 유지보수가 어려워짐.

- 비즈니스 로직을 수정해도 해당 코드를 손대야 하고, UI를 수정해도 비즈니스 로직이 함께 있는 해당 파일을 수정해야 함.

 

 변경의 라이프 사이클

- UI 를 일부 수정하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고 대부분 서로에게 영향을 주지 않음.

- 변경의 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수하기 좋지 않음.

 

 기능 특화

JSP 같은 뷰 템플릿은 화면을 렌더링 하는데 최적화 되어 있기 때문에 이 부분의 업무만 담당하는 것이 가장 효과적

 

1.2 MVC 패턴

 Model View Controller : MVC 패턴은 하나의 서블릿이나, JSP로 처리하던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 역할을 나눈 것

 - 컨트롤러: HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행.  뷰에 전달할 결과 데이터를 조회해서 모델에 담음.

 - 모델: 뷰에 출력할 데이터를 담아서 뷰에 전달함. 따라서 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있음

 - 뷰: 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중. (HTML을 생성하는 부분)

 

※ 컨트롤러에 비즈니스 로직을 두면 컨트롤러가 너무 많은 역할을 담당함.

그래서 일반적으로 비즈니스 로직은 서비스(Service)라는 계층을 별도로 만들어서 처리.

그리고 컨트롤러는 비즈니스 로직이 있는 서비스를 호출하는 역할을 담당.

 

 

2. MVC패턴 예시코드

2.1 컨트롤러

@WebServlet(name="mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
	private MemberRepository memberRepository = MemberRepository.getInstance();

	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String username = request.getParameter("username");
		int age = Integer.parseInt(request.getParameter("age"));
		
		Member member = new Member(username, age);
		memberRepository.save(member);
		
		//Model에 데이터를 보관한다.
		request.setAttribute("member", member);
		
		// 외부에서 직접 호출되지 않도록 WEB-INF에 jsp를 넣음
		// 항성 controller에서 호출되도록 함.
		String viewPath = "/WEB-INF/views/save-result.jsp";
		// 컨트롤러에서 view로 이동할때 사용
		RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
		// 다른 서블릿이나 JSP로 이동할 수 있는 기능 서버 내부에서 다시 호출이 발생
		dispatcher.forward(request, response);
		
	}
}

 

 

2.2 뷰 (JSP)

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!--<%@ page import="hello.servlet.domain.member.Member" %>-->
<html>
<head>
	<title>Title</title>
</head>
<body>
성공
<ul>
<!--
	<li>id=<%=((Member)request.getAttribute("member")).getId()%></li>
	<li>username=<%=((Member)request.getAttribute("member")).getUsername()%></li>
	<li>age=<%=((Member)request.getAttribute("member")).getAge()%></li>
-->
	<!-- 위의 코드와 같음 -->
	<li>id=${member.id}</li>
	<li>username=${member.username}</li>
	<li>age=${member.age}</li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>

 

3. MVC 패턴의 한계

3.1 MVC 컨트롤러의 단점

 포워드 중복

View로 이동하는 코드가 항상 중복 호출됨

이 부분을 메서드로 공통화해도 되지만, 해당 메서드도 항상 직접 호출해야 함.

RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

 

 ViewPath의 중복

만약 jsp가 아닌 thymeleaf 같은 다른 뷰로 변경한다면 전체 코드를 다 변경해야 함

String viewPath = "/WEB-INF/views/new-form.jsp";

prefix: /WEB-INF/views/

suffix:.jsp

 

 사용하지 않는 코드

HttpServletRequest, HttpServletResponse 코드를 사용할 때도 있고, 사용하지 않을 때도 있음

리고 위 코드는 테스트 케이스를 작성하기 어려움

 

 공통 처리의 어려움

기능이 복잡해질수록 컨트롤러에서 공통으로 처리해야 하는 부분이  증가

공통 기능을 메서드로 뽑아도, 결과적으로 해당 메서드를 항상 호출해야 하고, 실수로 호출하지 않으면 문제가됨.

그리고 호출하는 것 자체도 중복

 

※ 해결방법 

- 컨트롤러 호출 전에 먼저 공통 기능을 처리해야 함.

- 수문장 역할을 하는 기능이 필요 (입구를 하나로)

- 프론트 컨트롤러(Front Controller) 패턴을 도입.

반응형
반응형

서블릿에서 지원하는 기능

1. 스프링부트 서블릿 환경 구성

 스프링 부트의 main 메서드에 @ServletComponentScan 어노테이션 입력

※ @ServletComponentScan : 스프링 부트가 서블릿을 직접 등록해서 사용할 수 있도록 해주는 어노테이션

 클래스에 @WebServlet(name = "helloServlet", urlPatterns = "/hello") 애노테이션 입력

※ name: 서블릿 이름 urlPatterns: URL 매핑

Servlet클래스 생성시 HttpServlet을 상속 받아 오버라이드한 후 구현

※ HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 다음 메서드를 실행한다.

protected void service(HttpServletRequest request, HttpServletResponse response)

• 동작 방식

- 스프링 부트가 내장 톰켓 서버를 띄워줌

- 톰켓 서버는 내부에 서플릿 컨테이너기능을 가지고 있어서 서블릿 컨테이너를 통해 서블릿을 생성함

- 웹 브라우저가 요청을 보내면 request, respone 객체를 만들어서 싱글톤으로 떠있는 서블릿을 호출함

- 서블릿이 로직 수행을 완료하면 톰켓 서버가 respone에 담긴 정보를 가지고 response 메세지를 만들어서 응답

 

 

2. HttpServletRequest

2.1 서블릿과 HttpServletRequest 객체의 관계

 서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 파싱해줌.

 파싱 결과를 HttpServletRequest 객체에 담아서 제공

 HttpServletRequest를 사용하면 HTTP 요청 메시지를 편리하게 조회할 수 있음

※ HTTP 요청 메시지 예시

POST /save HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded

username=kim&age=20

 

2.2 HTTP 요청 메세지 구성요소와 HttpServletRequest객체에서 조회

START LINE HTTP메서드 request.getMethod()
URL request.getRequestURL()
URI request.getRequestURI()
쿼리스트링 request.getQueryString()
스키마 request.getScheme()
프로토콜 request.getProtocol()
https 여부 request.isSecure()
HEADER 다양한 헤더값 존재 request.getHeaderNames().asIterator()
.forEachRemaining(headerName ->
System.out.println(headerName + ": " + request.getHeader(headerName)));
BODY form 파라미터
형식 조회

※ 쿼리 파라미터
형식 조회와 같음
//단일 파라미터 조회
String username = request.getParameter("username"); 

//파라미터 이름들 모두 조회
Enumeration<String> parameterNames = request.getParameterNames();
 
//파라미터 이름들 모두 조회
Map<String, String[]> parameterMap = request.getParameterMap();

//복수 파라미터 조회
String[] usernames = request.getParameterValues("username"); 
message body
데이터 직접 조회
// 단순한 텍스트 메세지로 서버에 요청 했을 때 데이터 조회
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
// Json 형식 메세지로 서버에 요청 했을 때 데이터 조회
private ObjectMapper objectMapper = new ObjectMapper();

ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);

 

2.3 HttpServletRequest 객체의 기능

HTTP 요청 메시지를 편리하게 조회

• 임시 저장소 기능

 - 저장: request.setAttribute(name, value)

 - 조회: request.getAttribute(name)

※ 해당 HTTP 요청이 시작부터 끝날 때 까지 유지되는 임시 저장소 기능

• 세션 관리 기능

 - request.getSession(create: true)

 

2.4 HTTP 요청(request) 데이터

※ HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법 (3가지)

• GET - 쿼리 파라미터로 데이터 전달

ex) /url?username=hello&age=20

메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달

검색, 필터, 페이징등에서 많이 사용하는 방식

 

• POST - 메시지 바디에 쿼리 파리미터 형식으로 데이터 전달

ex) username=hello&age=20

컨텐트 타입이 content-type: application/x-www-form-urlencoded 으로 메시지 바디에 데이터를 포함해서 전달

회원 가입, 상품 주문 등 HTML Form에서 사용

 

• HTTP 메시지 바디에 데이터를 SON, XML, TEXT형식으로 직접 담아서 전달

ex) {"username":"kim","age":20}

POST, PUT, PATCH 메서드로 전달하고 주로 JSON을 사용

HTTP API에서 주로 사용

 

 

3. HttpServletResponse

3.1 HttpServletResponse의 역할

• HTTP 응답코드 지정

 - response.setStatus(HttpServletResponse.SC_OK);

• 헤더 생성

 - response.setHeader("Content-Type", "text/plain;charset=utf-8");

 - response.setHeader("Location", "/basic/hello-form.html");

※ 헤더의 content 관련 편의 메서드

 - response.setContentType("text/plain");

 - response.setCharacterEncoding("utf-8");

 바디 생성

 쿠키 생성 편의 메서드

Cookie cookie = new Cookie("myCookie", "good");
cookie.setMaxAge(600); //600초
response.addCookie(cookie);

※ Redirect 편의 메서드

- response.sendRedirect("/basic/hello-form.html");

 

3.2 HTTP 응답(response) 메시지

 단순 텍스트 응답

- writer.println("ok");

 HTML 응답 HTTP

response.setContentType("text/html");
response.setCharacterEncoding("utf-8");

PrintWriter writer = response.getWriter();
writer.println("<html>");
writer.println("<body>");
writer.println(" <div>안녕?</div>");
writer.println("</body>");
writer.println("</html>");

• API - MessageBody JSON 응답

private ObjectMapper objectMapper = new ObjectMapper();

// HTTP 응답으로 JSON을 반환할 때는 content-type을 application/json 로 지정해야 함
response.setHeader("content-type", "application/json");
response.setCharacterEncoding("utf-8");

HelloData data = new HelloData();
data.setUsername("kim");
data.setAge(20);

//{"username":"kim","age":20}
// Jackson 라이브러리가 제공하는 objectMapper.writeValueAsString()를 사용하면 객체를 JSON 문자로 변경할 수 있음
String result = objectMapper.writeValueAsString(data);

response.getWriter().write(result);
반응형
반응형

1. HTTP 기반 통신의 이해

1.1 통신 순서

①웹 브라우저(클라이언트)에서 url입력(서버에 데이터 요청)

②인터넷을 통해 서버에 접근

③인터넷을 통해 서버가 클라이언트에게 응답

 

※ 클라이언트에서 서버로 데이터를 전송할때, 서버에서 클라이언트로 데이터를 응답할때

모두 http 프로토콜 기반으로 데이터를 주고 받음

- HTML, TEXT

- IMAGE ,음성, 영상, 파일

- JSON, XML (API) 등

 

 

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

2.1 역할

• 웹 서버

- 정적 리소스를 제공 (html,css,js,이미지,영상)

 WAS

- 웹서버 기능 + 프로그램 코드를 실행가능(애플리케이션 로직 수행)

 - 서블릿, JSP, 스프링 MVC가 웹어플리케이션 서버에서 동작하게 됨

 

2.2 웹시스템 구성 (WEB, WAS, DB) 설명

 웹서버가 앞에서 정적리소스를 응답해줌

 애플리케이션 로직이 있어서 웹서버가 처리를 못하면 WAS에게 넘김

 정적리소스가 많이 필요하면 웹서버를 증설

 애플리케이션 리소스가 많이 사용되면 WAS 증설

 WAS,DB 장애시 웹서버가 오류 화면 제공이 가능함

※ API서버만 제공하면 WEB서버는 필요없음.

 

서블릿

1. 클라이언트 요청 / 서버 응답

1.1 클라이언트에서 요청 

• HTML의 form 데이터를 전송할때 웹브라우저가 HTTP 메세지를 만들어서 서버에 보냄

 

1.2 서버에서 처리 및 응답

• 서버 TCP/IP연결 대기, 소켓연결

• HTTP 요청메세지를 파싱해서 읽기

• POST 방식, /save URL 인지

• content-type 확인

• HTTP메세지 바디 내용 파싱(데이터를 사용할수 있게 파싱)

• 저장 프로세스 실행

• 비지니스 로직 실행(DB 저장 요청)

• HTTP 응답 메세지 생성 

• TPC/IP에 응답 전달, 소켓 종료

※  WAS를 직접 구현할 경우 위의 로직을 구현해야함

 

1.3 역할

• 서블릿을 지원 하는 WAS 사용하면 비지니스 로직 실행 이외의 로직을 대신 수행 해줌

• urlPatterns의 URL이 호출 되면 서블릿 코드가 실행

• http 요청 정보를 편리하게 쓸수있는 HttpServletRequest

• http 응답 정보를 편리하게 제공 할수 있는 HttpServletResponse

• 개발자가 HTTP 스펙을 편리하게사용 가능

 

2. 서블릿 컨테이너

2.1 서블릿 컨테이너란 ?

• 서블릿을 지원하는 WAS를 서블릿 컨테이너라고함

• WAS안에 서블릿 컨테이너가 있는데 서블릿을 자동으로 생성, 호출, 생명주기를 관리

• 서블릿 객체는 싱글톤으로 관리

        ◦ 최초 로딩 시점에 서블릿 객체를 미리 만들어두고 재활용

        ◦ request, response 객체는 요청이 올때마다 새로 생성되지만

        ◦ request와 response의 요청을 실행하는 서블릿 객체는 재사용을함

        ◦ 서블릿 컨테이너 종료 시 함께 종료

• JSP도 서블릿으로 변환되어 사용

• 동시 요청을 위한 멀티쓰레드 처리 지원

 

3. 멀티 쓰레드

3.1 쓰레드의 용도

• 웹브라라우저에서 요청을 할때 WAS(서블릿)와 연결되어 쓰레드가 처리함

• 애플리케이션 코드를 하나하나 순차적으로 실행하는 것은 쓰레드

• 자바 메인 메서드를 처음 실행하면 main이라는 이름의 쓰레드가 실행

• 쓰레드가 없다면 자바 애플리케이션 실행이 불가능함

• 쓰레드는 한번에 하나의 코드 라인만 수행

• 동시 처리가 필요하면 쓰레드를 추가로 생성

 

3.2 HTTP요청과 쓰레드의 역할

• 클라이언트에서 요청이 오면 쓰레드를 할당

 쓰레드를 가지고 서블릿을 실행해서 서블릿이 응답

 

3.3 요청마다 쓰레드를 생성 시 장단점

장점

• 동시 요청을 처리 가능

• 리소스(CPU,메모리)가 허용될때까지 처리가능

• 하나의 쓰레드가 지연 되어도, 나머지 쓰레든느 정상 동작한다.

 

단점

• 쓰레드는 생성비용이 비싸서 많은 쓰레드가 생성되면 응답소도가 느려짐.

• 쓰레드가 많아지면 쓰레드 컨텍스 스위칭 비용이 발생.

• 쓰레드가 많아지면 CPU, 메모리 임계점을 넘어서 서버가 죽을 수 있음.

 

3.4 쓰레드풀

• 요청마다 쓰레드를 생성하는것이 아닌 쓰레드 풀에 쓰레드의 최대 개수를 설정하여 쓰레드를 관리

※ WAS의 주요 튜닝 포인트는 쓰레드 개수이고 쓰레드 개수의 적정선은 애플리케이션 마다 모두 다름

• WAS가 멀티쓰레드에 대한 부분을 처리하고 개발자가 멀티쓰레드 관련 코드를 신경쓰지 않아도 됨

 

프론트 엔드 기술

1. HTTP를 통해서 데이터 주고 받는 방식

1.1 정적인 리소스

- 이미 생성된 리소스 파일(고정된 HTML, css, js, 이미지, 영상 등)을 제공

 

1.2 HTML 페이지

- 동적인 페이지는 클라이언트 요청 시 WAS에서 처리한 로직을 동적으로 HTML을 생성해줌(ex JSP, Thyemleaf)

 

1.3 HTTP API

- HTML이 아니라 데이터를 전달(주로 JSON 형식)

- WAS에서 처리한 로직을 JSON형식의 데이터로 응답해줌

※ JSON 형식 예시 → {"상품명": "스프링","수량" : 100, "금액" : 4000}

- UI화면이 필요하면 클라이언트가 별도로 처리함

- 다양한 시스템에서 호출 : 웹 클라이언트(React, Vue.js, Ajax) to 서버 / 앱 클라이언트 to 서버 / 서버 to 서버

 

2. SSR

2.1 SSR 이란

• 서버사이드 렌더링

• 서버에서 생성된 동적인 HTML을 클라이언트에 최종적으로 전달

• 정적인 화면에 사용

• 관련 기술 : JSP, Thyemleaf (백엔드 개발자)

 

3. CSR

3.1 CSR이란

• 클라이언트 사이드 렌더링

• 처리 방식

        ◦ <클라이언트> HTML에서 url 요청 

        ◦ <서버> 내용이 없고 자바스크립트 링크를 담은 HTML을 전달

        ◦ <클라이언트> 서버에서 받은 자바스크립트를 요청

        ◦ <서버> 자바스크립트안에 클라이언트 로직, HTML 렌더링 로직을 전달

        ◦ <클라이언트> HTTP API 데이터 요청

        ◦ <서버> JSON 데이터 전달

※ 웹브라우저가 Javacript의 클라이언트로직과 HTML 렌더링 로직으로 JSON 데이터를 실어서 클라이언트 사이드에서 HTML 생성 

 

• HTML 결과를 자바스크립트를 사용해서 웹브라우저에서 동적으로 생성해서 적용

• 동적인 화면에 사용, 웹환경을 앱처럼 필요한 부분부분 변경 가능

• 관련기술 : React, Vue.js (웹 프론트엔드 개발자)

 

 

자바 백엔드 웹 기술 역사

 서블릿 1997 : HTML 생성이 어려움 (java 소스안에서 html을 생성해야함)

 JSP 1999 : HTML 생성은 편리하지만, 비지니스 로직까지 담당

 서블릿 JSP 조합 MVC 패턴 : 모델, 뷰, 컨트롤러로 역할을 나눠 개발

 MVC 프레임워크 춘추 전국 시대 2000초~2010초 

 애노테이션 기반의 스프링 MVC : MVC 프레임워크의 통일

 스프링부트 : 빌드결과 jar파일에 WAS서버를 포함

반응형
반응형

1. 빈 스코프

: 빈이 존재할 수 있는 범위

 

-  스코프의 종류

싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
프로토타입: 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프

웹 관련 스코프
    request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
    session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
    application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

 

- 프로토타입 스코프의 특징

스프링 컨테이너에 요청할 때 마다 새로 생성됨
스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여한다.
종료 메서드가 호출되지 않는다.
종료 메서드에 대한 호출을 클라이언트가 직접 해야한다.

2. 싱글톤 빈 - 프로토타입 빈과 함께 사용시 문제점

스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다. 그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제이다. 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할 때 마다 새로 생성해서 사용하는 것이 목적이다.

 

해결방법1 : ObjectFactory, ObjectProvider

ObjectProvider 의 getObject() 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)

과거에 ObjectFactory가 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider 가 만들어짐.

예제)

package hello.core.scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class SingletonWithPrototypeTest1 {
	@Test
	void singletonClientUsePrototype() {
		AnnotationConfigApplicationContext ac = 
				new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
		
		ClientBean clientBean1 = ac.getBean(ClientBean.class);
		int count1 = clientBean1.logic();
		Assertions.assertThat(count1).isEqualTo(1);
		
		ClientBean clientBean2 = ac.getBean(ClientBean.class);
		int count2 = clientBean2.logic();
		Assertions.assertThat(count2).isEqualTo(1);
	}
	
	@Scope("singleton")
	static class ClientBean{
		// DL ObjectProvider 적용
		@Autowired
		private ObjectProvider<PrototypeBean> prototypeBeanProvider;
		
		public int logic() {
			PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
			prototypeBean.addCount();
			int count = prototypeBean.getCount();
			return count;
		}		
	}
	
	@Scope("prototype")
	static class PrototypeBean{
		private int count = 0;
		
		public void addCount() {
			count++;
		}
		
		public int getCount() {
			return count;
		}
		
		@PostConstruct
		public void init() {
			System.out.println("PrototypeBean.init " + this);
		}
		
		@PreDestroy
		public void destroy() {
			System.out.println("PrototypeBean.destroy");
		}		
	}
}

※ 특징 

ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존

 

해결방법 2 : JSR-330 Provider

javax.inject:javax.inject:1 라이브러리를 gradle에 추가.

예제)

package hello.core.scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Provider;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class SingletonWithPrototypeTest1 {	
	@Test
	void singletonClientUsePrototype() {
		AnnotationConfigApplicationContext ac = 
				new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
		
		ClientBean clientBean1 = ac.getBean(ClientBean.class);
		int count1 = clientBean1.logic();
		Assertions.assertThat(count1).isEqualTo(1);
		
		ClientBean clientBean2 = ac.getBean(ClientBean.class);
		int count2 = clientBean2.logic();
		Assertions.assertThat(count2).isEqualTo(1);
	}
	
	@Scope("singleton")
	static class ClientBean{
		// DL Provider적용
		@Autowired
		private Provider<PrototypeBean> prototypeBeanProvider;
		
		public int logic() {
			PrototypeBean prototypeBean = prototypeBeanProvider.get();
			prototypeBean.addCount();
			int count = prototypeBean.getCount();
			return count;
		}
	}
	
	@Scope("prototype")
	static class PrototypeBean{
		private int count = 0;
		
		public void addCount() {
			count++;
		}
		
		public int getCount() {
			return count;
		}
		
		@PostConstruct
		public void init() {
			System.out.println("PrototypeBean.init " + this);
		}
		
		@PreDestroy
		public void destroy() {
			System.out.println("PrototypeBean.destroy");
		}		
	}
}

※ 특징

get() 메서드 하나로 기능이 매우 단순하다.
별도의 라이브러리가 필요하다.
자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

반응형
반응형

1. 빈 생명 주기

- 스프링 빈의 이벤트 라이프사이클
스프링 컨테이너 생성 → 스프링 빈 생성  의존관계 주입  초기화 콜백  사용  소멸전 콜백  스프링 종료

 

※ 초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
※ 소멸전 콜백: 빈이 소멸되기 직전에 호출

 

- 스프링 빈은 간단하게 다음과 같은 라이프사이클을 가짐.
객체 생성  의존관계 주입

※ 스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료된다. 따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다.

※ 생성자 주입은 객체 생성과 동시에 의존 관계가 주입됨

 

왜 콜백함수가 필요할까?

데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.

 

생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다.

반면에 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는등 무거운 동작을 수행한다.
따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다. 

 

이때, 콜백 함수를 통해 초기화 됐을때의 실행 될 로직과 소멸전 실행될 로직을 구현 할 수 있다.

 

 

2. 빈 생명주기 콜백

- 인터페이스(InitializingBean, DisposableBean) 구현

예제)

package hello.core.lifecycle;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class NetworkClient implements InitializingBean, DisposableBean {
	private String url;

	public NetworkClient() {
		System.out.println("생성자 호출, url = " + url);
	}

	public void setUrl(String url) {
		this.url = url;
	}

	//서비스 시작시 호출 할 로직
	public void connect() {
		System.out.println("connect: " + url);
	}
    
	public void call(String message) {
		System.out.println("call: " + url + " message = " + message);
	}

	//서비스 종료시 호출 할 로직
	public void disConnect() {
		System.out.println("close + " + url);
	}
    
	// InitializingBean 인터페이스 구현
	@Override
	public void afterPropertiesSet() throws Exception {
		connect();
		call("초기화 연결 메시지");
	}
    
	// DisposableBean 인터페이스 구현
	@Override
	public void destroy() throws Exception {
		disConnect();
	}
}

※ 초기화, 소멸 인터페이스 단점
이 인터페이스는 스프링 전용 인터페이스다. 해당 코드가 스프링 전용 인터페이스에 의존한다.
초기화, 소멸 메서드의 이름을 변경할 수 없다.
내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.

거의 사용하지 않음

 


- 설정 정보에 초기화 메서드, 종료 메서드 지정

설정 정보에 @Bean(initMethod = "init", destroyMethod = "close") 처럼 초기화, 소멸 메서드를 지정할 수 있다.

예제)

초기화 메소드, 소멸 메소드 작성 (init 메소드, close 메소드)

package hello.core.lifecycle;

public class NetworkClient {
	private String url;

	public NetworkClient() {
		System.out.println("생성자 호출, url = " + url);
	}

	public void setUrl(String url) {
		this.url = url;
	}

	//서비스 시작시 호출
	public void connect() {
		System.out.println("connect: " + url);
	}
    
	public void call(String message) {
		System.out.println("call: " + url + " message = " + message);
	}

	//서비스 종료시 호출
	public void disConnect() {
		System.out.println("close + " + url);
	}

	public void init() {
		System.out.println("NetworkClient.init");
		connect();
		call("초기화 연결 메시지");
	}
    
	public void close() {
		System.out.println("NetworkClient.close");
		disConnect();
	}
}

설정 정보에 초기화 소멸 메서드 지정

package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class BeanLifecycleTest {

	@Test
	public void lifeCycleTest() {
		ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
		NetworkClient client = ac.getBean(NetworkClient.class);
		ac.close();
	}
	
	@Configuration
	static class LifeCycleConfig{
		@Bean(initMethod = "init", destroyMethod = "close")
		public NetworkClient networkClient() {
			NetworkClient networkClient = new NetworkClient();
			networkClient.setUrl("http://hello-spring.dev");
			return networkClient;
		}
		
	}
}

※ 설정 정보 사용 특징
메서드 이름을 자유롭게 줄 수 있다.
스프링 빈이 스프링 코드에 의존하지 않는다.
코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.

 

※ 종료 메소드의 추론

라이브러리는 대부분 close , shutdown 이라는 이름의 종료 메서드를 사용하는게 일반적이라, close , shutdown 라는 이름의 메서드를 자동으로 호출한다. 따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작함.

 

 

- @PostConstruct, @PreDestroy 애노테이션 지원

예제)

package hello.core.lifecycle;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class NetworkClient{

	private String url;

	public NetworkClient() {
		System.out.println("생성자 호출, url: "+url);
	}
	
	public void setUrl(String url) {
		this.url = url;
	}
	
	// 서비스 시작 시 호출
	public void connect() {
		System.err.println("connect: " + url);
	}

	public void call(String message) {
		System.out.println("call: " + url + ", message: " + message);
	}
	
	// 서비스 종료 시 호출
	public void disconnect() {
		System.out.println("close: "+url );
	}

	@PostConstruct
	public void init() throws Exception {
		System.out.println("NetworkClient.init");
		connect();
		call("초기화 연결 메세지");
	}

	@PreDestroy
	public void close() throws Exception {
		System.out.println("NetworkClient.close");
		disconnect();
	}
	
}

※ @PostConstruct, @PreDestroy 애노테이션 특징
최신 스프링에서 가장 권장하는 방법으로 애노테이션 하나만 붙이면 되므로 매우 편리하다.
스프링에 종속적인 기술이 아니라 JSR-250 라는 자바 표준, 스프링이 아닌 다른 컨테이너에서도 동작.
외부 라이브러리에는 적용하지 못함.

외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용.

반응형
반응형

 

1. 의존관계 주입 방법 4가지
생성자 주입

- 생성자를 통해서 의존 관계를 주입 받음.

- 생성자 호출시점에 딱 1번만 호출되는 것이 보장.
- 불변, 필수 의존관계에 사용

- 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입

 

수정자 주입(setter 주입)

- setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입 받음.

- 선택, 변경 가능성이 있는 의존관계에 사용
- 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법

※ @Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생.

주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false) 로 지정하면 된다.

 

필드 주입

- 코드가 간결 하지만 외부에서 변경이 불가능해서 테스트 하기 힘듦
- DI 프레임워크가 없으면 아무것도 할 수 없음.

- 사용 안하는것을 추천


일반 메서드 주입

- 일반 메서드를 통해서 주입 받음.
- 한번에 여러 필드를 주입 받을 수 있음, 일반적으로 잘 사용하지 않는다.

 

※ 주입할 스프링 빈이 없어도 동작해야 할 때 옵션 사용법

package hello.core.autowired;

import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.lang.Nullable;

import hello.core.member.Member;

public class AuotowiredTest {
	
	@Test
	void AutowiredOption() {
		ApplicationContext ac =  new AnnotationConfigApplicationContext(TestBean.class);
		
	}
	
	static class TestBean{		
/*		// 오류남
		@Autowired
		public void setNoBean0(Member noBean0) {
			System.out.println("noBean0 = "+ noBean0);
		}
 */
		
		// 호출 자체가 안됨
		@Autowired(required = false)
		public void setNoBean1(Member noBean1) {
			System.out.println("noBean1 = "+ noBean1);
		}
		
		// 호출은 되지만 null
		@Autowired
		public void setNoBean2(@Nullable Member noBean2) {
			System.out.println("noBean2 = "+ noBean2);			
		}
		
		// 호출 되고 Optional.empty로 들어옴
		@Autowired
		public void setNoBean3(Optional<Member> noBean3) {
			System.out.println("noBean3 = "+ noBean3);			
		}
	}

}

 

생성자 주입을 사용해야 함

- 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안됨.(불변)

- 수정자 주입을 하게 되면 누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것 자체가 안좋은 설계.

- 생성자 주입을 사용하면 필드에 final 키워드를 사용가능, 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아줌.

- 생성자 주입이 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법

 

※ Lombok

@RequiredArgsConstructor어노테이션을 붙이면 final이 붙은 필드에 자동으로 생성자 주입을 시켜줌.

예시)

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;
}

 

 

2. 의존관계 주입 시 조회한 빈이 2개 이상일 경우

@Autowired 는 타입(Type)으로 조회 하는데 조회된 타입이 2개일 경우 NoUniqueBeanDefinitionException 오류가 발생.

 

해결 방법 

1) @Autowired 필드 명 매칭 

- @Autowired는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가
매칭.

예시)

private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
	
	
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
	this.memberRepository = memberRepository;
	this.discountPolicy = rateDiscountPolicy;
}

 


2) @Qualifier끼리 매칭 빈 이름 매칭

- @Qualifier 는 추가 구분자를 붙여주는 방법. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것이 아님.

예시)

조회된 첫번째 빈에 @Qualifier 적용

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

 

조회된 두번째 빈에 @Qualifier 적용

@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}

 

@Qualifier 사용

private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
	this.memberRepository = memberRepository;
	this.discountPolicy = discountPolicy;
}

 

 

3) @Primary 사용

- @Primary 는 우선순위를 정하는 방법. @Autowired 시에 여러 빈이 매칭되면 @Primary 가 우선권을 가짐.

예시)

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy { ... }



@Component
public class FixDiscountPolicy implements DiscountPolicy { ... }

 

※ 우선순위
@Primary 는 기본값 처럼 동작하는 것이고, @Qualifier 는 매우 상세하게 동작한다. 

스프링은 자동보다는 수동이, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선 순위가 높다. 여기서도 @Qualifier 가 우선권이 높다.

 

 

3. 조회한 빈이 모두 필요할때 (List, Map)

예제)

package hello.core.autowired;

import java.util.List;
import java.util.Map;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;

public class AllBeanTest {

	@Test
	void findAllBean() {
		ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
		
		DiscountService discountService = ac.getBean(DiscountService.class);
	 
		Member member = new Member(1L, "userA", Grade.VIP);
		int discountPrice =  discountService.discount(member, 10000, "fixDiscountPolicy");
	 
		Assertions.assertThat(discountService).isInstanceOf(DiscountService.class);
		Assertions.assertThat(discountPrice).isEqualTo(1000);
		
		int rateDiscountPrice =  discountService.discount(member, 20000, "rateDiscountPolicy");
		Assertions.assertThat(rateDiscountPrice).isEqualTo(2000);
	}
	
	
	static class DiscountService{
		// DiscountPolicy의 2개의 구현체 자동 주입
		private final Map<String, DiscountPolicy> policyMap;
		private final List<DiscountPolicy> policies;
		
		@Autowired
		public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
			this.policyMap = policyMap;
			this.policies = policies;
			System.out.println("policyMap = "+ policyMap);
			System.out.println("policies = "+ policies);
		}

		public int discount(Member member, int price, String discountCode) {
			// discountCode로 주입된 구현체 선택
			DiscountPolicy discountPolicy = policyMap.get(discountCode);
			return discountPolicy.discount(member, price);
		}
	}
}

 

 

4. 수동빈과 자동빈의 선택

- 편리한 자동 기능을 기본으로 사용.
- 직접 등록하는 기술 지원 객체는 수동 등록
- 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민

반응형
반응형

1. 의존 관계 자동 주입

- 자동 빈 등록

이전 예제에서 스프링 빈을 등록할 때 자바 코드의 @Bean이나 XML의 등을 통해서 설정 정보에 직접 등록할 스프링 빈을 수동으로 등록했지만, 스프링에선 자동으로 스프링 빈을 등록하는 @ComponentScan이라는 기능을 제공한다.

 

자동 구성을 해주는 새로운 기획자

package hello.core;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import static org.springframework.context.annotation.ComponentScan.*;

@Configuration
@ComponentScan
public class AutoAppConfig {
 
}

@ComponentScan 어노테이션을 붙여주고, @Bean 어노테이션과 내용은 하나도 없다.

Bean으로 등록하고 싶은 클래스에 직접 @Component어노테이션을 붙여주면 자동으로 Bean에 등록된다.

MemberRepository에 의존관계를 주입하고자 하는 MemoryMemberRepository만 Bean에 등록하기 위해 @Component어노테이션을 붙인다.

DiscountPolicy도 마찬가지로 RateDiscountPolicy에만 @Component어노테이션을 붙인다.

 

예시)

@Component
public class MemoryMemberRepository implements MemberRepository {
	...
}
@Component
public class RateDiscountPolicy implements DiscountPolicy {
	...
}

 

컴포넌트 스캔 기본 대상

- @Component : 컴포넌트 스캔에서 사용

- @Controlller : 스프링 MVC 컨트롤러에서 사용

- @Service : 스프링 비즈니스 로직에서 사용

- @Repository : 스프링 데이터 접근 계층에서 사용

- @Configuration : 스프링 설정 정보에서 사용

 

 

스프링 컨테이너를 인터스턴스화 할때 해당 config파일을 인자로 넘겨주면 된다.

 

- 자동 의존 관계 주입 등록

@Autowire어노테이션을 통해 자동 의존 관계를 주입.

 

예시)

@Component
public class MemberServiceImpl implements MemberService {
	private final MemberRepository memberRepository;
	
    @Autowired
	public MemberServiceImpl(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}
}
@Component
public class OrderServiceImpl implements OrderService {
	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;
	
    // 여러 의존관계 한번에 주입 가능
    @Autowired
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
	}
}

@Autowire의 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다. getBean(MemberRepository.class), getBean(DiscountPolicy.class)와 동일.

 

2. Bean 탐색 위치와 기본 스캔 대상

모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.

@ComponentScan( basePackages = "hello.core" )

 

basePackages : 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다. basePackages = {"hello.core", "hello.service"} 이렇게 여러 시작 위치를 지정할 수도 있다.

basePackageClasses : 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.

 

만약 지정하지 않으면 @ComponentScan 이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.

 

※ 권장하는 방법

패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것이다. 최근 스프링 부트도 이 방법을 기본으로 제공한다.

 

3. 컴포넌트 스캔 대상 필터

includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.

excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.

 

- 어노테이션 생성

컴포넌트 스캔 대상에 추가할 애노테이션

package hello.core.scan.filter;
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent { }

컴포넌트 스캔 대상에서 제외할 애노테이션

package hello.core.scan.filter;
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent { }

 

- 어노테이션 적용

컴포넌트 스캔 대상에 추가할 클래스

package hello.core.scan.filter;

@MyIncludeComponent
public class BeanA { }

 

컴포넌트 스캔 대상에 제외할 클래스

package hello.core.scan.filter;
@MyExcludeComponent
public class BeanB { }

 

- 테스트

package hello.core.scan.filter;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.context.annotation.ComponentScan.Filter;

public class ComponentFilterAppConfigTest {
	@Test
	void filterScan() {
		ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
		BeanA beanA = ac.getBean("beanA", BeanA.class);
		assertThat(beanA).isNotNull();
		Assertions.assertThrows(
			NoSuchBeanDefinitionException.class,
			() -> ac.getBean("beanB", BeanB.class));
	}
    
	@Configuration
	@ComponentScan(
		includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
		excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
	)
	static class ComponentFilterAppConfig { }
}

 

FilterType의 5가지 옵션

ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.

ASSIGNABLE_TYPE: 지정한 클래스의 타입과 자식 클래스 타입을 인식해서 동작한다. 

ASPECTJ: AspectJ 패턴 사용

REGEX: 정규 표현식

CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리

 

 

4. 중복 등록과 충돌

- 자동 Bean 등록 vs 자동 Bean  등록

컴포넌트 스캔에 의해 자동으로 스프링 Bean이 등록될때 이름이 같은 경우 스프링은 ConflictingBeanDefinitionException 예외를 발생시킨다. 

 

- 수동 Bean  등록 vs 자동 Bean  등록

수동 Bean 등록이 우선권을 가지며, 오버라이드 됐다는 로그를 출력 

Overriding bean definition for bean 'XXXX' with a different definition: replacing

 

반응형
반응형

1. 순수 DI 컨테이너

맨 처음 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다.
고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸되어 메모리 낭비가 심하다.
해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다. 싱글톤 패턴 적용

package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {
	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}
	
	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(),discountPolicy());
	}
    
	public MemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}

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

 

2. 싱글톤 패턴

클래스의 인스턴스가 딱 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("싱글톤 객체 로직 호출");
	}
}

- 싱글톤 패턴의 문제점

싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
의존관계상 클라이언트가 구체 클래스에 의존한다. (DIP 위반)
클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
테스트하기 어렵다.
내부 속성을 변경하거나 초기화 하기 어렵다.
private 생성자로 자식 클래스를 만들기 어렵다.
결론적으로 유연성이 떨어진다.
안티패턴으로 불리기도 한다.

 

 

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

 

싱글톤 패턴 적용 시 주의점

여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안되고 무상태(stateless)로 설계해야 한다.

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

※ 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다

 

 

4. @Configuration어노테이션

스프링은 싱글톤을 보장하기 위해 클래스의 바이트코드를 조작하는 라이브러리를 사용한다. 

클래스 명에 xxxCGLIB가 붙으면서 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용하여 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록함.

@Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.

 

 

 

 

 

반응형
반응형

1. 스프링으로 전환하기

- AppConfig 파일 

package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 어노테이션 기반의 스프링컨테이너
// AppConfig에 설정을 구성한다는 뜻의 @Configuration 을 붙여준다.
@Configuration
public class AppConfig {
	// 각 메서드에 @Bean 을 붙여준다. 이렇게 하면 스프링 컨테이너에 스프링 빈으로 등록한다.
	@Bean
	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}

	@Bean
	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(),discountPolicy());
	}
    
	@Bean
	public MemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}
    
	@Bean
	public DiscountPolicy discountPolicy() {
		return new RateDiscountPolicy();
	}
}

 

- 회원 서비스 클라이언트 수정

package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MemberApp {
	public static void main(String[] args) {
		// AppConfig appConfig = new AppConfig();
		// MemberService memberService = appConfig.memberService();
		ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
		MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
		Member member = new Member(1L, "memberA", Grade.VIP);
		memberService.join(member);
		Member findMember = memberService.findMember(1L);
		System.out.println("new member = " + member.getName());
		System.out.println("find Member = " + findMember.getName());
	}
}

 

- 주문 서비스 클라이언트 수정

package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class OrderApp {
	public static void main(String[] args) {
		// AppConfig appConfig = new AppConfig();
		// MemberService memberService = appConfig.memberService();
		// OrderService orderService = appConfig.orderService();
		ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
		MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
		OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
		long memberId = 1L;
		Member member = new Member(memberId, "memberA", Grade.VIP);
		memberService.join(member);
		Order order = orderService.createOrder(memberId, "itemA", 10000);
		System.out.println("order = " + order);
	}
}

ApplicationContext 를 스프링 컨테이너라 한다.
- 기존에는 개발자가 AppConfig 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.
- 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성) 정보로 사용한다. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
- 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. ( memberService , orderService )
- 이전에는 개발자가 필요한 객체를 AppConfig 를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다. 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.
- 기존에는 개발자가 직접 자바코드로 모든 것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다.

 

 

2. 스프링 컨테이너

ApplicationContext = 인터페이스

 

스프링 컨테이너는 XML 기반으로 만들 수도 있고 어노테이션 기반으로 만들수도 있음

위의 예제는 어노테이션 기반으로 만듦

// AnnotationConfigApplicationContext는 ApplicationContext의 구현체 (어노테이션 기반)

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class) 

 

// GenericXmlApplicationContext ApplicationContext의 구현체 (xml기반)

ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");

※ BeanFactory
스프링 컨테이너의 최상위 인터페이스, 스프링 빈을 관리하고 조회하는 역할을 담당, getBean() 제공

※ ApplicationContext

BeanFactory 기능을 모두 상속받아서 제공, 애플리케이션을 개발할 때는 빈을 관리하고 조회하는 기능은 물론이고, 수 많은 부가기능이 필요하다.(메시지 소스를 활용한 국제화 기능, 환경변수, 애플리케이션 이벤트, 편리한 리소스 조회)

 

 

3. 스프링 Bean 설정 메타 정보 (BeanDefinition)

- 다양한 설정 형식을 지원 가능하게 해주는 BeanDefinition 추상화

역할과 구현을 개념적으로 나누어 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 BeanDefinition만 알면 된다.
@Bean , <bean> 당 각각 하나씩 메타 정보가 생성되고, 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성.

 

반응형

+ Recent posts