728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -

www.inflearn.com

 

저번에 만들었던 서블릿들을 이제 스프링 부트에 맞게 바꾸어 보도록 하자.

스프링은 기존에 작성했던 서블릿과는 다르게 @annotation을 많이 활용한다.

 

우선 저번에 만들었던 Form을 이렇게 수정했다.

package hello.servlet.web.springmvc.version1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class SpringStudentFormControllerV1 {

    @RequestMapping("/springmvc/v1/students/new-form")
    public ModelAndView process(){
        return new ModelAndView("new-form");
    }
}

@Controller를 달아서 스프링에서 컨트롤러로 인식하도록 한다, 그리고 메서드에 @RequestMapping으로 URL을 mapping 한다.

return은 저번에 작성했던 것과 비슷하게 ModelAndView 클래스에 생성자로 뷰 path를 넘겨주면 된다.

 

Save 부분도

package hello.servlet.web.springmvc.version1;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class SpringStudentSaveControllerV1 {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @RequestMapping("/springmvc/v1/students/save")
    public ModelAndView process(HttpServletRequest request, HttpServletResponse response){
        String studentName = request.getParameter("studentName");
        int year = Integer.parseInt(request.getParameter("year"));
        
        Student student = new Student(studentName, year);
        studentRepository.save(student);
        
        ModelAndView modelAndView = new ModelAndView("save-result");
        modelAndView.addObject("student", student);
        return modelAndView;
    }
}

이렇게 Mapping을 해주고 ModelAndView를 사용해서 Path 입력해주고 객체를 넣어주면 된다.

 

List도 같은 방식으로 작성한다.

package hello.servlet.web.springmvc.version1;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Controller
public class SpringStudentListControllerV1 {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @RequestMapping("/springmvc/v1/students")
    public ModelAndView process(){
        List<Student> students = studentRepository.findAll();

        ModelAndView modelAndView = new ModelAndView("students");
        modelAndView.addObject("students", students);
        return modelAndView;
    }
}

 

이제 여기서 리펙토링을 해보도록 하자.

어차피 mapping은 메서드에 하나씩 들어가게 된다.

그러면 한 컨트롤러에 작성할 수도 있지 않을까?

 

물론 된다.

한 컨트롤러에 작성하고 다른 주소들을 Mapping 해주면 된다.

package hello.servlet.web.springmvc.version2;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Controller
@RequestMapping("/springmvc/v2/students")
public class SpringStudentControllerV2 {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @RequestMapping("/new-form")
    public ModelAndView newForm(){
        return new ModelAndView("new-form");
    }

    @RequestMapping("/save")
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response){
        String studentName = request.getParameter("studentName");
        int year = Integer.parseInt(request.getParameter("year"));
        
        Student student = new Student(studentName, year);
        studentRepository.save(student);
        
        ModelAndView modelAndView = new ModelAndView("save-result");
        modelAndView.addObject("student", student);
        return modelAndView;
    }
    
    @RequestMapping
    public ModelAndView students(){
        List<Student> students = studentRepository.findAll();
        
        ModelAndView modelAndView = new ModelAndView("students");
        modelAndView.addObject("students", students);
        return modelAndView;
    }
}

이렇게 메서드들의 이름만 분리해서 작성할 수 있다.

 

이제 마지막으로 @RequestParam까지 이용해보자.

이걸 이용하면 request에서 getParam을 쓸 필요가 없어진다.

바로 파라미터에서 가져올 수 있게 된다.

package hello.servlet.web.springmvc.version3;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@Controller
@RequestMapping("/springmvc/v3/students")
public class SpringStudentControllerV3 {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @GetMapping("/new-form")
    public String newForm(){
        return "new-form";
    }

    @PostMapping("/save")
    public String save(
            @RequestParam("studentName") String studentName,
            @RequestParam("year") int year,
            Model model){
        Student student = new Student(studentName, year);
        studentRepository.save(student);

        model.addAttribute("student", student);
        return "save-result";
    }

    @GetMapping
    public String students(Model model){
        List<Student> students = studentRepository.findAll();
        model.addAttribute("students", students);
        return "students";
    }
}

이렇게 ModelAndView를 리턴하는게 아니라 String을 리턴하면 자동으로 해당 Path를 찾아가게 된다.

Mapping에 Get, Post를 설정할 수 있으며 Model에 값을 추가하여 ModelAndView처럼 동작하게 할 수 있다.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 16일차  (0) 2023.03.29
스프링 15일차  (0) 2023.03.28
스프링 14일차  (0) 2023.03.26
스프링 13일차  (0) 2023.03.25
스프링 12일차  (0) 2023.02.15
728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -

www.inflearn.com

 

저번에 만들었던 서블릿에 컨트롤러를 추가해보자.

 

현재 만들어둔 컨트롤러는 공통된 부분이 많고 하는 일도 비슷하다.

그럴 때는 FrontController를 생성해준다.

이 FrontController가 요청을 받고 그 요청에 맞는 컨트롤러를 호출해주는 역할을 한다.

 

우선 프론트 컨트롤러 자체를 도입해보자.

우선 다형성을 이용해 컨트롤러 인터페이스를 만들어보자.

package hello.servlet.web.frontcontroller.version1;

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

import java.io.IOException;

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

이렇게 만들고 각 컨트롤러들이 이 인터페이스를 구현하게 해주면 요청에 따른 컨트롤러는 모두 준비가 된다.

 

이제 FrontController를 만들어준다.

package hello.servlet.web.frontcontroller.version1;

import hello.servlet.web.frontcontroller.version1.controller.StudentFormControllerV1;
import hello.servlet.web.frontcontroller.version1.controller.StudentListControllerV1;
import hello.servlet.web.frontcontroller.version1.controller.StudentSaveControllerV1;
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;
import java.util.HashMap;
import java.util.Map;

@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/students/new-form", new StudentFormControllerV1());
        controllerMap.put("/front-controller/v1/students/save", new StudentSaveControllerV1());
        controllerMap.put("/front-controller/v1/students", new StudentListControllerV1());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ControllerV1 controller = controllerMap.get(req.getRequestURI());
        if(controller == null){
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        
        controller.process(req, resp);
    }
}

이렇게 Map에 컨트롤러들을 담아준 후 URI를 확인하여 거기에 맞는 컨트롤러로 보내준다.

 

이제 계속 리팩토링을 진행해보자.

모든 컨트롤러에서 중복되는 부분이 있다.

이 부분을 모아주어야 깔끔하고, 후에 수정하기도 쉽다.

이런 상태에서 중간에 MyView를 추가하여 dispatcher.forward()를 맡길 예정이다.

package hello.servlet.web.frontcontroller;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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 requestDispatcher = request.getRequestDispatcher(viewPath);
        requestDispatcher.forward(request, response);
    }
}

이렇게 생성자로 viewPath를 입력받아서 render를 이용하여 jsp로 넘겨준다.

 

나머지 코드들은 저 부분을 빼서 다시 작성한다고 생각하면 된다.

package hello.servlet.web.frontcontroller.version2;

import hello.servlet.web.frontcontroller.MyView;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public interface ControllerV2 {

    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
package hello.servlet.web.frontcontroller.version2.controller;

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.version2.ControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class StudentFormControllerV2 implements ControllerV2 {

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        return new MyView("/WEB-INF/views/new-form.jsp");
    }
}
package hello.servlet.web.frontcontroller.version2.controller;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.version2.ControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class StudentSaveControllerV2 implements ControllerV2 {

    private StudentRepository studentRepository = StudentRepository.getInstance();

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

        Student student = new Student(studentName, year);
        studentRepository.save(student);

        request.setAttribute("student", student);

        return new MyView("/WEB-INF/views/save-result.jsp");
    }
}
package hello.servlet.web.frontcontroller.version2.controller;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.version2.ControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

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

public class StudentListControllerV2 implements ControllerV2 {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Student> students = studentRepository.findAll();
        request.setAttribute("students", students);

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

그러고 FrontController는 이렇게 작성을 해준다.

package hello.servlet.web.frontcontroller.version2;

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.version2.controller.StudentFormControllerV2;
import hello.servlet.web.frontcontroller.version2.controller.StudentListControllerV2;
import hello.servlet.web.frontcontroller.version2.controller.StudentSaveControllerV2;
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;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {

    private Map<String, ControllerV2> controllerMap = new HashMap<>();

    public FrontControllerServletV2(){
        controllerMap.put("/front-controller/v1/students/new-form", new StudentFormControllerV2());
        controllerMap.put("/front-controller/v1/students/save", new StudentSaveControllerV2());
        controllerMap.put("/front-controller/v1/students", new StudentListControllerV2());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ControllerV2 controller = controllerMap.get(req.getRequestURI());
        if(controller == null){
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        MyView view = controller.process(req, resp);
        view.render(req, resp);
    }
}

request와 response에 데이터를 받고 view로 경로를 받은 후에 실행해주는 것이다.

 

다음 리펙토링이다.

컨트롤러에서는 request와 response가 꼭 필요하지 않다.

어차피 데이터를 넘길거면 담아서 넘기는 게 아니라 그냥 넘기면 되기 때문이다.

그렇기 때문에 컨트롤러에서 서블릿 기술을 제외해보자.

 

스프링처럼 Model을 만들어 데이터를 넘기도록 한다.

이렇게 Controller에서 Model에 데이터만 담아서 가져오는 것이다.

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, Map<String, Object> model) {
        this.viewName = viewName;
    }
}

이렇게 Map으로 key, value로 데이터들을 저장하도록 만든다.

그러고 경로를 저장하기 위해서 viewName을 사용한다.

 

이제 ModelView를 리턴하도록 코드들을 바꾸어보자.

package hello.servlet.web.frontcontroller.version3;

import hello.servlet.web.frontcontroller.ModelView;

import java.util.Map;

public class StudentFormControllerV3 implements ControllerV3{

    @Override
    public ModelView process(Map<String, String> paramMap) {
        return new ModelView("new-form");
    }
}

이렇게 Model에 jsp의 경로와 데이터들을 넣어준다.

package hello.servlet.web.frontcontroller.version3;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import hello.servlet.web.frontcontroller.ModelView;

import java.util.Map;

public class StudentSaveControllerV3 implements ControllerV3{
    
    private StudentRepository studentRepository = StudentRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {
        String studentName = paramMap.get("studentName");
        int year = Integer.parseInt(paramMap.get("year"));
        
        Student student = new Student(studentName, year);
        studentRepository.save(student);
        
        ModelView modelView = new ModelView("save-result");
        modelView.getModel().put("student", student);
        return modelView;
    }
}
package hello.servlet.web.frontcontroller.version3;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import hello.servlet.web.frontcontroller.ModelView;

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

public class StudentListControllerV3 implements ControllerV3{

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {
        List<Student> students = studentRepository.findAll();

        ModelView modelView = new ModelView("students");
        modelView.getModel().put("students", students);
        
        return modelView;
    }
}

 

이제 이렇게 작동할 FrontController를 작성한다.

package hello.servlet.web.frontcontroller.version2;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.version2.controller.StudentFormControllerV2;
import hello.servlet.web.frontcontroller.version2.controller.StudentListControllerV2;
import hello.servlet.web.frontcontroller.version2.controller.StudentSaveControllerV2;
import hello.servlet.web.frontcontroller.version3.ControllerV3;
import hello.servlet.web.frontcontroller.version3.StudentFormControllerV3;
import hello.servlet.web.frontcontroller.version3.StudentListControllerV3;
import hello.servlet.web.frontcontroller.version3.StudentSaveControllerV3;
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;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {

    private Map<String, ControllerV3> controllerMap = new HashMap<>();

    public FrontControllerServletV3(){
        controllerMap.put("/front-controller/v3/students/new-form", new StudentFormControllerV3());
        controllerMap.put("/front-controller/v3/students/save", new StudentSaveControllerV3());
        controllerMap.put("/front-controller/v3/students", new StudentListControllerV3());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ControllerV3 controller = controllerMap.get(req.getRequestURI());
        if(controller == null){
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        Map<String, String> paramMap = createParamMap(req);
        ModelView modelView = controller.process(paramMap);

        String viewName = modelView.getViewName();
        MyView view = viewer(viewName);
        view.render(modelView.getModel(), req, resp);
    }

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

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

        return paramMap;
    }
}

여기에 MyView에 파라미터가 3개 들어가는 생성자가 호출 되었으니, 그에 맞게 생성자를 추가해준다.

 

 

'백엔드 > 스프링' 카테고리의 다른 글

스프링 17일차  (0) 2023.03.31
스프링 15일차  (0) 2023.03.28
스프링 14일차  (0) 2023.03.26
스프링 13일차  (0) 2023.03.25
스프링 12일차  (0) 2023.02.15
728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -

www.inflearn.com

 

간단한 학생 관리 웹을 만들어보자.

학생의 정보에는 학번, 이름, 입학년도가 있다.

 

우선 학생의 클래스를 작성해보자.

전에도 사용했던 Lombok을 사용한다.

package hello.servlet.domain.student;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class Student {
    private Long studentId;
    private String name;
    private String year;
}

학생들의 정보를 저장할 저장소이다.

 

package hello.servlet.domain.student;

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

public class StudentRepository {
    private static Map<Long, Student> store = new HashMap<>();
    private static long STUDENT_ID_SEQUENCE = 0L;

    private static final StudentRepository instance = new StudentRepository();

    public static StudentRepository getInstance(){
        return instance;
    }

    private StudentRepository() {}

    public Student save(Student student){
        student.setStudentId(++STUDENT_ID_SEQUENCE);
        store.put(student.getStudentId(), student);
        return student;
    }

    public Student findById(Long id){
        return store.get(id);
    }

    public List<Student> findAll(){
        return new ArrayList<>(store.values());
    }
    
    public void clearStore(){
        store.clear();
    }
}

저장소는 싱글톤으로 생성했고, 저장과 검색 메서드들을 추가했다.

 

command + shift + T로 테스트 코드를 작성해보자.

package hello.servlet.domain.student;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class StudentRepositoryTest {

    StudentRepository studentRepository = StudentRepository.getInstance();

    @AfterEach
    void afterEach(){
        studentRepository.clearStore();
    }

    @Test
    void save(){
        //given
        Student student = new Student("hyunttai", 2022);

        //when
        Student savedStudent = studentRepository.save(student);

        //then
        Student findStudent = studentRepository.findById(savedStudent.getStudentId());
        Assertions.assertEquals(savedStudent, findStudent);
    }

    @Test
    void findAll(){
        //given
        Student student1 = new Student("hyeontae", 2022);
        Student student2 = new Student("hyeonttai", 2022);

        //when
        studentRepository.save(student1);
        studentRepository.save(student2);
        List<Student> result = studentRepository.findAll();

        //then
        Assertions.assertEquals(result.size(), 2);
        org.assertj.core.api.Assertions.assertThat(result).contains(student1, student2);
    }
}

@AfterEach로 각 테스트마다 저장소를 초기화 해주었다.

문제없이 작성했다면 테스트를 통과 했을 것이다.

 

이제 servlet으로 HTML form을 응답하도록 코드를 작성해보자.

package hello.servlet.web.servlet;

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;
import java.io.PrintWriter;

@WebServlet(name = "studentFormServlet", urlPatterns = "/servlet/students/new-form")
public class StudentFormServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        resp.setCharacterEncoding("utf-8");

        PrintWriter writer = resp.getWriter();
        writer.write("<!DOCTYPE html>\n" +
                "<html>\n" +
                "<head>\n" +
                "    <meta charset=\"UTF-8\">\n" +
                "    <title>Title</title>\n" +
                "</head>\n" +
                "<body>\n" +
                "<form action=\"/servlet/students/save\" method=\"post\">\n" +
                "    studentName: <input type=\"text\" name=\"studentName\" />\n" +
                "           year:      <input type=\"text\" name=\"year\" />\n" +
                " <button type=\"submit\">전송</button>\n" + "</form>\n" +
                "</body>\n" +
                "</html>\n");
    }
}

response 객체에 ContentType을 html로해서 html로 응답한 모습이다.

 

action에 save를 달아주었기 때문에 이 요청을 받을 save도 작성을 해야한다.

package hello.servlet.web.servlet;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
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;
import java.io.PrintWriter;

@WebServlet(name = "studentSaveServlet", urlPatterns = "/servlet/students/save")
public class StudentSaveServlet extends HttpServlet {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String studentName = req.getParameter("studentName");
        int year = Integer.parseInt(req.getParameter("year"));

        Student student = new Student(studentName, year);
        studentRepository.save(student);

        PrintWriter writer = resp.getWriter();

        writer.write("<html>\n" +
                "<head>\n" +
                " <meta charset=\"UTF-8\">\n" + "</head>\n" +
                "<body>\n" +
                "<ul>\n" +
                "    <li>studentId="+student.getStudentId()+"</li>\n" +
                "    <li>name="+student.getStudentName()+"</li>\n" +
                " <li>year="+student.getYear()+"</li>\n" + "</ul>\n" +
                "<a href=\"/index.html\">메인</a>\n" + "</body>\n" +
                "</html>");
    }
}

그러고 http://localhost:8080/servlet/students/new-form 해당 페이지에 접근하면 잘 작동하는 것을 볼 수 있다.

 

이번엔 하나씩 조회하는 것이 아니라 모든 학생들을 조회하는 기능을 만들어보자.

package hello.servlet.web.servlet;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
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;
import java.io.PrintWriter;
import java.util.List;

@WebServlet(name = "studentListServlet", urlPatterns = "/servlet/students")
public class StudentListServlet extends HttpServlet {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        resp.setCharacterEncoding("utf-8");

        List<Student> students = studentRepository.findAll();

        PrintWriter writer = resp.getWriter();

        writer.write("<html>");
        writer.write("<head>");
        writer.write("    <meta charset=\"UTF-8\">");
        writer.write("    <title>Title</title>");
        writer.write("</head>");
        writer.write("<body>");
        writer.write("<a href=\"/index.html\">메인</a>");
        writer.write("<table>");
        writer.write("    <thead>");
        writer.write("    <th>studentId</th>");
        writer.write("    <th>studentName</th>");
        writer.write("    <th>year</th>");
        writer.write("    </thead>");
        writer.write("    <tbody>");
        for (Student student : students) {
            writer.write("    <tr>");
            writer.write("        <td>" + student.getStudentId() + "</td>");
            writer.write("        <td>" + student.getStudentName() + "</td>");
            writer.write("        <td>" + student.getYear() + "</td>");
            writer.write("    </tr>");
        }
        writer.write("    </tbody>");
        writer.write("</table>");
        writer.write("</body>");
        writer.write("</html>");
    }
}

이렇게 for-each문을 이용해서 반복되는 html들을 찍어주었다.

이렇게 잘 출력되는 것을 볼 수 있다.

하지만 이렇게 자바코드 안에 HTML을 넣어서 작업하는 방법은 너무 힘들다.

 

그렇기에 HTML에 자바코드를 작성하는 JSP로 페이지를 만들어보자.

신입생을 등록하는 jsp는 html과 크게 다르지 않다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>신입생 등록</title>
</head>
<body>
<form action = "/jsp/students/save.jsp" method="post">
    studentName: <input type = "text" name="studentName">
    year: <input type="text" name="age">
    <button type="submit">전송</button>
</form>

</body>
</html>

 

여기서 응답을 하는 save.jsp는

<%@ page import="hello.servlet.domain.student.StudentRepository" %>
<%@ page import="hello.servlet.domain.student.Student" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
  StudentRepository studentRepository = StudentRepository.getInstance();

  String studentName = request.getParameter("studentName");
  int year = Integer.parseInt(request.getParameter("year"));

  Student student = new Student(studentName, year);
  studentRepository.save(student);

%>
<html>
<head>
    <title>신입생 등록 완료</title>
</head>
<body>
<ul>
  <li>studentId=<%=student.getStudentId()%></li>
  <li>studentName=<%=student.getStudentName()%></li>
  <li>year=<%=student.getYear()%></li>
</ul>

<a href="/index.html">메인</a>

</body>
</html>

이렇게 자바코드와 html이 합쳐져 있다.

윗 부분에 자바 코드들을 작성하고 html의 body에서 데이터들을 가져오는 것이다.

이렇게 보면 기존과 크게 차이가 없어 보이지만, html에 자바코드를 사용해서 반복문으로 값을 출력하면 훨씬 작업하기 편해진다.

 

학생들의 목록을 출력해주는 JSP이다.

<%@ page import="hello.servlet.domain.student.StudentRepository" %>
<%@ page import="hello.servlet.domain.student.Student" %>
<%@ page import="java.util.List" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    StudentRepository studentRepository = StudentRepository.getInstance();
    List<Student> students = studentRepository.findAll();
%>
<html>
<head>
    <title>학생 조회</title>
    <meta charset="UTF-8">
</head>
<body>
<a href="/index.html">메인</a>
<table>
    <thead>
    <th>studentId</th>
    <th>studentName</th>
    <th>year</th>
    </thead>
    <tbody>
    <%
        for(Student student: students){
            out.write("<tr>\n");
            out.write("<td>" + student.getStudentId() + "</td>\n");
            out.write("<td>" + student.getStudentName() + "</td>\n");
            out.write("<td>" + student.getYear() + "</td>\n");
            out.write("</tr>\n");
        }
    %>
    </tbody>
</table>

</body>
</html>

이렇게 html 안에서 자바코드를 사용하여 html 코드들을 반복해서 출력하는 것으로 코드가 훨씬 보기 좋고 편해졌다.

하지만 현재의 JSP는 너무 많은 역할을 담당하고 있다.

그렇기 때문에 Model, View, Controll의 MVC 패턴에 맞추어 작성하도록 해야한다.

Model은 컨트롤러에서 뷰로 넘기는 데이터로 뷰가 출력하는 데에 필요한 데이터들을 가지고 있다.

View는 Model에 있는 데이터를 사용해서 화면을 보여주는 역할을 한다.

Controller는 HTTP 요청을 받아 파라미터를 보고, 로직을 실행한다. 그러고 Model에 데이터를 담아 View에 넘긴다.

 

여기서 Controller에 로직들을 둘 수 있지만, 역할을 나누기위해 서비스를 만들어 그 서비스를 호출하여 실행하는 역할을 한다.

요청부터 응답까지의 과정을 살펴보면

이렇게 MVC를 이용하여 요청에 응답을 한다.

그러면 여기서 로직을 수행하는 부분은 서블릿으로, 뷰를 보여주는 부분은 JSP로 작성을 한다.

 

package hello.servlet.web.servletmvc;

import jakarta.servlet.RequestDispatcher;
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 = "mvcStudentFormServlet", urlPatterns = "servlet-mvc/students/new-form")
public class MvcStudentFormServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String viewPath="/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
        dispatcher.forward(req, resp);
    }
}

 

이렇게 작성을 하면 urlPattern으로 mapping 한 주소에 접속할 때 dispatcher.forward를 통해 viewPath에 있는 JSP를 열어주게 된다.

여기서 /WEB-INF를 만들어야 하는데, 이 경로 안에 있는 JSP는 외부에서 호출할 수 없고 서버 내부에서만 호출이 가능하다.

 

이제 새로 열릴 new-form.jsp를 만들어준다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>신입생 등록</title>
    <meta charset="UTF-8">
</head>
<body>
<form action="save" method="post">
    studentName: <input type="text" name="studentName"/>
    year: <input type="text" name="year"/>
    <button type="submit">전송</button>
</form>
</body>
</html>

action을 save로 잡아주었으니 이제 save를 작성한다.

전송을 하면 아래의 서블릿에서 로직이 실행이 된다.

 

request를 보고 파라미터들을 이용하여 값을 가져온 후 다시 request.setAttribute()를 이용하여 데이터를 넣어준 후 jsp로 넘겨준다.

package hello.servlet.web.servletmvc;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import jakarta.servlet.RequestDispatcher;
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 = "mvcStudentSaveServlet", urlPatterns = "/servlet-mvc/students/save")
public class MvcStudentSaveServlet extends HttpServlet {
    private StudentRepository studentRepository = StudentRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String studentName = req.getParameter("studentName");
        int year = Integer.parseInt(req.getParameter("year"));
        
        Student student = new Student(studentName, year);
        studentRepository.save(student);
        
        req.setAttribute("student", student);
        
        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
        dispatcher.forward(req, resp);
    }
}

 

이제 결과를 출력할 jsp를 작성해준다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>신입생 등록 완료</title>
    <meta charset="UTF-8">
</head>
<body>

<ul>
  <li>studentId=${student.studentId}</li>
  <li>studentName=${student.studentName}</li>
  <li>year=${student.year}</li>
</ul>

<a href="/index.html">메인</a>

</body>
</html>

${}를 이용하면 attribute에 있는 데이터를 조회할 수 있다.

 

 

'백엔드 > 스프링' 카테고리의 다른 글

스프링 17일차  (0) 2023.03.31
스프링 16일차  (0) 2023.03.29
스프링 14일차  (0) 2023.03.26
스프링 13일차  (0) 2023.03.25
스프링 12일차  (0) 2023.02.15
728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -

www.inflearn.com

 

스프링 부트를 설정해서 다운로드하여 오고, 추가 설정들을 해준다.

저번에 워낙 많이 해서 그냥 넘어가도록 하겠다.

 

이번에는 서블릿을 사용하기 때문에 스프링 메인에 @ServletComponentScan annotation을 달아준다.

그러고 같은 패키지 내에 이런 자바 코드를 작성해 보자.

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;
import java.io.PrintWriter;

@WebServlet(name = "hiServlet", urlPatterns = "/hi")
public class HiServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        resp.setContentType("text/plain");
        resp.setCharacterEncoding("utf-8");


        PrintWriter writer = resp.getWriter();
        writer.println("HiServlet");
        writer.println("request = " + req);
        writer.println("response = " + resp);

        writer.println("name = " + req.getParameter("name"));

    }
}

실행을 하고 url에 접속한 결과이다.

 

@WebServlet annotation은 서블릿 어노테이션으로 name에 들어가는 값은 서블릿의 이름이고, urlPatterns에 들어가는 값은 mapping 되는 url이다.

저번에 서블릿 공부 할 때 공부했을 것이다.

그리고 서블릿을 사용하기 위해서는 HttpServlet을 extends 받아야 한다.

 

메서드 내의 코드를 잠깐 설명해 보자면, 응답의 ContentType을 text/plain으로 설정하고 CharacterEncoding을 utf-8로 설정을 한 후 response에 getWriter로 request와 response 객체를 출력해 보는 것이다.

 

  • HTTPServletRequest 객체

객체의 이름을 보면 알겠지만, HTTP의 Request를 Servlet이 다루기 편하게 만들어주는 객체이다.

이곳을 보면 HTTP 요청 메시지에 대한 정보를 얻어 올 수 있다.

당연히 직접 HTTP 요청 메시지를 사용할 수 있겠지만, 난이도가 매우 어렵다.

 

HTTPServletRequest 객체를 이용해서 안의 내용들을 출력해 보자.

우선 시작 줄부터이다.

주석으로 설명을 달지 않아도 시작줄의 어떤 내용을 출력하려 하는 것인지 알 수 있을 것이다.

package hello.servlet.basic.request;

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;
import java.io.PrintWriter;

@WebServlet(name = "requestHeaderServlet", urlPatterns = "/requestHeader")
public class RequestHeader extends HttpServlet {

    private void printStart(HttpServletResponse httpServletResponse, String string) throws IOException {
        httpServletResponse.getWriter().printf("--- %s-Line start ---\n", string);
    }

    private void printEnd(HttpServletResponse httpServletResponse, String string) throws IOException {
        httpServletResponse.getWriter().printf("--- %s-Line end ---\n", string);
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        StartLine(req, resp);
    }

    private void StartLine(HttpServletRequest request,HttpServletResponse response)throws IOException{

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

        PrintWriter writer = response.getWriter();

        printStart(response, "Request");

        writer.println("Method = " + request.getMethod());
        writer.println("Protocol = " + request.getProtocol());
        writer.println("Scheme = " + request.getScheme());
        writer.println("RequestURL = " + request.getRequestURL());
        writer.println("RequestURI = " + request.getRequestURI());
        writer.println("QueryString = " + request.getQueryString());
        writer.println("isSecure = " + request.isSecure());
        printEnd(response, "Request");

        writer.println();
    }
}

실행을 한 후에 접속해 보면

이렇게 요청 메서드에 관한 정보들이 나오게 된다.

 

이번엔 헤더이다.

package hello.servlet.basic.request;

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;
import java.io.PrintWriter;

@WebServlet(name = "requestHeaderServlet", urlPatterns = "/requestHeader")
public class RequestHeader extends HttpServlet {

    private void printStart(HttpServletResponse httpServletResponse, String string) throws IOException {
        httpServletResponse.getWriter().printf("--- %s-Line start ---\n", string);
    }

    private void printEnd(HttpServletResponse httpServletResponse, String string) throws IOException {
        httpServletResponse.getWriter().printf("--- %s-Line end ---\n", string);
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        StartLine(req, resp);
        Headers(req, resp);
    }

    private void Headers(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");

        PrintWriter writer = response.getWriter();

        printStart(response, "Headers");

        request.getHeaderNames().asIterator()
                .forEachRemaining(headerName -> writer.println(headerName + " = " + request.getHeader(headerName)));

        printEnd(response, "Headers");
        writer.println();
    }
}

이번엔 Iterator로 빠르게 출력했다.

이렇게 이어서 잘 출력이 되는 것을 볼 수 있다.

나머지 부분들도 request를 보면 찾을 수 있지만, 이 정도만 하고 넘어가 보겠다.

 

  • HTTP request - GET

데이터를 서버로 전송해 보자.

서버로 데이터를 전송할 때는 Postman을 사용하면 된다.

package hello.servlet.basic.request;

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;
import java.io.PrintWriter;

@WebServlet(name = "requestParam", urlPatterns = "/requestParam")
public class RequestParam extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        System.out.println("[파리미터 조회] - start");
        req.getParameterNames().asIterator()
                .forEachRemaining(paramName -> System.out.println(paramName + " = " + req.getParameter(paramName)));

        System.out.println("[파리미터 조회] - end");
        System.out.println();

        System.out.println("[개별 파라미터 조회]");
        System.out.println("name : " + req.getParameter("name"));
        System.out.println("age: " + req.getParameter("age"));

        System.out.println();
    }
}

이렇게 Iterator를 이용하여 전체를 조회할 수 있지만, 보통 getParameter()를 이용해서 개별 조회를 한다.

Postman으로 데이터를 전송하면

이렇게 출력되게 된다.

 

  • HTTP request - POST

이번엔 POST 방식이다.

저번 GET 방식은 URL로 데이터를 보낼 수도 있긴 했지만, 이번 POST 방식은 Postman을 이용해야 할 것이다.

근데 코드는 수정할 필요가 없다.

POST 방식도 GET과 쿼리 파라미터 형식이 같기 때문에 조회 메서드는 동일하게 사용할 수 있다.

이렇게 동일한 결과가 나오는 것을 볼 수 있다.

 

  • HTTP request - 텍스트
package hello.servlet.basic.request;

import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.util.StreamUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

@WebServlet(name = "requestBodyString", urlPatterns = "/requestBodyString")
public class RequestBodyString extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ServletInputStream inputStream = req.getInputStream();

        String message = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        System.out.println("message = " + message);

    }
}

InputStream은 바이트 코드를 반환하기 때문에 UTF_8로 지정을 해주어 바꿔줘야 한다.

 

Postman으로 POST에 raw로 그냥 메시지를 보내면

이렇게 잘 출력이 되는 것을 볼 수 있다.

 

  • HTTP request - JSON

요즘 많이 사용하는 JSON 형식이다.

우선 Postman에서 JSON으로 데이터를 보내는 방법부터 알아보겠다.

Body에서 raw, JSON을 선택하고 형식에 맞게 작성을 하면 된다.

 

그러고 JSON 형식으로 데이터를 가져올 수 있게 그에 맞는 객체를 생성한다.

package hello.servlet.basic;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class JsonData {
    private String name;
    private int age;
}

깔끔하게 Lombok을 사용한다.

 

package hello.servlet.basic.request;

import com.fasterxml.jackson.databind.ObjectMapper;
import hello.servlet.basic.JsonData;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.util.StreamUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

@WebServlet(name = "requestBodyJson", urlPatterns = "/requestBodyJson")
public class RequestBodyJson extends HttpServlet {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ServletInputStream inputStream = req.getInputStream();
        String message = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        
        JsonData jsonData = objectMapper.readValue(message, JsonData.class);

        System.out.println(jsonData);
    }
}

ObjectMapper를 이용해서 가져온 데이터를 해당 객체로 변환해 준다.

이렇게 잘 출력되는 것을 볼 수 있다.

 

  • HTTP response

HttpServletResponse를 사용하여 응답 메시지를 작성할 수 있다.

package hello.servlet.basic.response;

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

import java.io.IOException;

@WebServlet(name = "responseHeader", urlPatterns = "/response-header")
public class ResponseHeaderServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //상태 라인
        resp.setStatus(HttpServletResponse.SC_OK);
        
        //response header
        resp.setContentType("text/plain");
        resp.setCharacterEncoding("utf-8");
        resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        resp.setHeader("Pragma", "no-cache");
        resp.setHeader("my-header", "hi");
        
    }
    
    private void redirect(HttpServletResponse response) throws IOException{
        response.sendRedirect("/basic/hi-form.html");
    }
    
    private void cookie(HttpServletResponse response){
        Cookie cookie = new Cookie("myCookie", "good!");
        cookie.setMaxAge(60);
        response.addCookie(cookie);
    }
}

이렇게 response 객체를 이용해서 편하게 응답 메시지를 만들 수 있다.

 

이번에는 중요한 응답 메시지로 JSON을 작성하는 방법이다.

여기서도 ObjectMapper를 사용한다.

package hello.servlet.basic.response;

import com.fasterxml.jackson.databind.ObjectMapper;
import hello.servlet.basic.JsonData;
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 = "responseJson", urlPatterns = "/responseJson")
public class ResponseJson extends HttpServlet {
    
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json");
        resp.setCharacterEncoding("utf-8");
        
        JsonData jsonData = new JsonData();
        jsonData.setName("hi");
        jsonData.setAge(20);
        
        String result = objectMapper.writeValueAsString(jsonData);
        
        resp.getWriter().write(result);
    }
}

이렇게 객체를 만들고 ObjectMapper를 이용하여 문자열로 바꿔준 후 write로 작성을 해주면 된다.

그러면 이렇게 잘 전달이 되는 것을 볼 수 있다.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 16일차  (0) 2023.03.29
스프링 15일차  (0) 2023.03.28
스프링 13일차  (0) 2023.03.25
스프링 12일차  (0) 2023.02.15
스프링 11일차  (0) 2023.02.12
728x90

인프런 김영한님의 강의를 참고했습니다.

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -

www.inflearn.com

HTTP와 JPA 공부를 어느정도 끝내고 다시 스프링으로 돌아왔다.

 

HTTP의 기본도 공부를 했으니

오늘은 스프링이 어떻게 돌아가는지 알아보기 위해 WAS와 그 기원인 tomcat, Servlet & JSP등을 알아보고 온다.

 

  • Web Server, WAS

Web Server

HTTP 기반으로 동작을 하며, 정적 리소스를 제공한다.

정적 리소스라하면 단순한 파일들인 HTML, CSS, JPG 등을 말한다.

 

WAS(Web Application Server)

이 친구도 마찬가지로 HTTP 기반으로 동작한다.

정적 리소스도 제공은 가능하기 때문에 Web Server의 기능을 수행할 수도 있기는 하다.

그러고도 프로그램 코드를 실행 가능하기 때문에 애플리케이션 로직을 수행할 수 있다.

예를 들면 저번에 공부했던 tomcat이 있다.

 

그렇기 때문에 웹 시스템을 WAS와 DB만으로 구성이 가능하다.

WAS는 웹 서버의 기능을 수행할 수 있기 때문이다.

 

하지만 이렇게 만들면 WAS가 너무 많은 역할을 담당한다.

WAS는 복잡한 로직들을 수행하기 때문에 장애가 많이 일어나고, 이 때문에 정적 리소스조차 수행 불가능 할 수 있다.

그렇기 때문에 정적 리소스는 웹 서버가 처리하고 로직은 WAS가 수행하도록 섞어서 사용하게 된다.

 

Web Server는 WAS에 비해 장애가 발생하지 않기 때문에 WAS에 장애가 생기면 Web Server를 통해 오류 화면을 제공할 수도 있다.

 

  • Servlet

서블릿은 저번에 공부 했던 내용이다.

서블릿을 사용하면 HTTP 송수신 과정에서 애플리케이션 로직들만 신경 쓸 수 있게 해준다.

HTTP를 요청하면 WAS는 Request, Response 객체를 만들어서 서블릿 객체를 호출한다.

그러면 그 Request, Response 객체를 받아서 Request를 보고 HTTP 요청 정보를 확인하고, Response에 응답 정보를 입력한다.

 

위 그림처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 한다.

저 컨테이너는 서블릿 객체들을 생성하고 객체들의 생명주기를 관리한다.

객체들은 Spring과 같이 계속 생성하는 비효율적이기 때문에 싱글톤으로 관리한다.

이 싱글톤 객체들을 어떻게 사용하는 것이 좋으며, 주의해야 하는 부분은 무엇인지 알고 있을 것이다.

 

  • 멀티 쓰레드

만약 서버가 단일 쓰레드라면?

먼저 들어온 요청이 모두 끝난 후에 다음 요청이 쓰레드를 사용하여 응답을 받을 수 있을 것이다.

이렇게되면 다음에 들어온 요청은 대기 시간이 굉장히 길어질 것이다.

여기서 먼저 들어온 요청에 장애가 생겨서 쓰레드를 반환하지 않는다면 다음 요청은 평생 응답을 받을 수 없을 것이다.

 

그렇다고 해서 요청마다 쓰레드를 생성한다면 쓰레드를 생성할 때마다 굉장히 오랜 시간이 걸리며, 고객의 요청이 너무 많이 오면 서버의 CPU와 메모리가 다운될 수 있다.

 

그렇기 때문에 미리 쓰레드들을 만들어놓고 도서관에서 책 빌리는 것 처럼 하나씩 쓰레드를 빌리고 반납하는 형식으로 사용한다.

만약 쓰레드 풀에 남은 쓰레드가 없다면 쓰레드를 대기하게 된다.

이렇게하면 쓰레드가 미리 생성되어 있기 때문에 생성하는 시간을 줄일 수 있다.

 

  • HTML, HTTP API, 렌더링

위에서 말했듯이 고정된 HTML 파일들은 웹 브라우저를 사용하여 제공하면 된다.

사용자에 따라 달라지는 동적 HTML 페이지는 WAS를 사용하여 제공한다.

 

만약 HTML이 아닌 데이터만을 전달하는 것이 목적이라면 굳이 HTML에 담아서 전달할 필요가 없다.

WAS에서 주로 JSON 형식으로 주고 받는다.

 

그러면 여기서 렌더링 방식에 따라 서버사이드 렌더링, 클라이언트 사이드 렌더링으로 나뉜다.

SSR(서버 사이드 렌더링) CSR(클라이언트 사이드 렌더링)
HTML 최종 결과를 서버에서 만들어서 클라이언트에 전달 HTML 결과를 자바스크립트를 사용해 동적으로 생성해서 적용
보통 정적인 화면에 사용한다. 보통 동적인 화면에 사용
JSP, Thymeleaf React, Vue.js

SSR은 서버에서 다 받아오기 때문에 바로 이해할 수 있을 것 같고

CSR은 이런 과정을 거쳐 렌더링이 된다.

 

'백엔드 > 스프링' 카테고리의 다른 글

스프링 15일차  (0) 2023.03.28
스프링 14일차  (0) 2023.03.26
스프링 12일차  (0) 2023.02.15
스프링 11일차  (0) 2023.02.12
스프링 10일차  (0) 2023.02.11
728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

빈 스코프에 대해서 배워보자.

 

11일차에 우리는 스프링 빈이 컨테이너가 시작될 때 생성이되어 컨테이너가 종료될 때까지 유지된다고 배웠다.

싱글톤 스코프로 생성이 되기 때문인데, 스코프는 말 그대로 빈이 존재할 수 있는 범위이다.

 

스프링은 다음과 같은 스코프를 지원한다.

  • 싱글톤

기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.

  • 프로토타입

스프링 컨테이너는 프로토타입 빈의 생성과 의존관계까지만 관여하고 더는 관리하지 않는 짧은 범위의 스코프이다.

  • 웹 관련 스코프

웹 요청에 관한 스코프들로 request, session, application이 있다.

 

 

빈 스코프는

@Scope("prototype")로 설정할 수 있다.

 

프로토타입 스코프

기존의 싱글톤을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환했다.

하지만 프로토타입 스코프를 조회하면 항상 새로운 인스턴스를 생성해서 반환해준다.

 

 

프로토타입 스코프의 빈을 스프링 컨테이너에 요청하면

스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.

그 빈을 스프링 컨테이너는 반환한다.

 

스프링은 딱 이 과정에만 관여하고, 그 후에는 관리를 하지 않는다.

따라서 @PreDestroy와 같은 종료 메서드도 호출되지 않는다.

 

늘 하던대로 싱글톤 스코프 빈을 먼저 테스트 해보자

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class SingletonTest {
    
    @Scope("singleton")
    static class SingletonBean{
        @PostConstruct
        public void init(){
            System.out.println("SingletonBean.init");
        }
        
        @PreDestroy
        public void destroy(){
            System.out.println("SingletonBean.destroy");
        }
    }
    
    @Test
    public void singletonBeanFind(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(SingletonBean.class);
        
        SingletonBean singletonBean1 = applicationContext.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = applicationContext.getBean(SingletonBean.class);

        System.out.println("singletonBean1 = " + singletonBean1);
        System.out.println("singletonBean2 = " + singletonBean2);

        Assertions.assertSame(singletonBean1, singletonBean2);
        
        applicationContext.close();
    }
}

이렇게 같은 인스턴스가 출력이 되는 것을 볼 수 있다.

 

이번에는 싱글톤이 아닌 프로토타입을 테스트해보자.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class PrototypeTest {

    @Scope("prototype")
    static class PrototypeBean{
        @PostConstruct
        public void init(){
            System.out.println("PrototypeBean.init");
        }

        @PreDestroy
        public void destroy(){
            System.out.println("PrototypeBean.destroy");
        }
    }

    @Test
    public void prototypeBeanFind(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean prototypeBean1 = applicationContext.getBean(PrototypeBean.class);

        System.out.println("find prototypeBean2");
        PrototypeBean prototypeBean2 = applicationContext.getBean(PrototypeBean.class);

        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);

        Assertions.assertNotSame(prototypeBean1, prototypeBean2);
        applicationContext.close();
    }
}

이렇게 프로토타입을 테스트해보면

다른 인스턴스가 생성이 되는 것을 확인할 수 있다.

그리고 초기화도 2번이 실행된 것을 확인할 수 있다.

또한 프로토타입은 생성, 의존관계주입, 초기화까지만 관여하기에 @PreDestroy의 메서드가 출력되지 않는 것을 볼 수 있다.

 

프로토타입과 싱글톤을 함께 사용시의 문제점

프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 반환한다.

하지만 싱글톤 빈에 들어간 프로토타입 빈은 어떻게 될까?

 

싱글톤 빈이 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈일 것이다.

그렇기 때문에 사용할 때마다 생성되는 것이 아니며, 안에 프로토타입 빈이 있더라도 프로토타입 빈으로의 작동을 못하게 될 것이다.

이렇게 사용할 수도 있지만, 아마 이것은 프로토타입을 사용하는 취지에 어긋날 것이다.

그렇기에 항상 새로운 프로토타입 빈을 생성하는 방법에 대해 알아보자.

 

Provider로 프로토타입 스코프를 사용

프로토타입 빈을 요청받는 방법은 사용할 때마다 스프링 컨테이너에 새로 요청하는 것이다.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.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 org.springframework.context.annotation.Scope;

public class PrototypeProviderTest {

    @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");
        }
    }

    static class ClientBean{
        @Autowired
        private ApplicationContext applicationContext;

        public int logic(){
            PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

    @Test
    void providerTest(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = applicationContext.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertEquals(1, count1);

        ClientBean clientBean2 = applicationContext.getBean(ClientBean.class);
        int count2 = clientBean1.logic();
        Assertions.assertEquals(1, count2);
    }
}

이렇게 의존관계를 찾는 방법을 Dependency Lookup이라고 하는데, 스프링 컨테이너 종속적인 코드가 되고 좋은 코드가 아니다.

 

따라서 다른 방법들을 사용한다.

 

ObjectFactory, ObjectProvider

Dependency Lookup 서비스를 제공하는 것이 바로 ObjectProvider이고, 이걸 상속받아 기능을 추가한 것이 ObjectProvider이다.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.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.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class PrototypeProviderTest {

    @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");
        }
    }

    static class ClientBean{
        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;
        
        public int logic(){
            PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject();
            prototypeBean.addCount();
            return prototypeBean.getCount();
        }
    }

    @Test
    void providerTest(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = applicationContext.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertEquals(1, count1);

        ClientBean clientBean2 = applicationContext.getBean(ClientBean.class);
        int count2 = clientBean1.logic();
        Assertions.assertEquals(1, count2);
    }
}

ClientBean 클래스의 내용을 이렇게 변경하면 된다.

이렇게 다른 인스턴스를 가지는 것을 볼 수 있다.

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

 

여기에 추가로 자바 표준을 사용하는 방법이 있다.

 

JSP-330 Provider

스프링 부트 3.0.1을 사용하는 방법이다.

우선 gradle에

implementation 'jakarta.inject:jakarta.inject-api:2.0.1'

을 추가해준다.

 

그리고 Provider에 맞게 ClientBean의 코드를 수정해준다.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Provider;
import org.junit.jupiter.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 {

    @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");
        }
    }

    static class ClientBean{
        
        @Autowired
        private Provider<PrototypeBean> provider;
        
        public int logic(){
            PrototypeBean prototypeBean = provider.get();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

    @Test
    void prototypeFind(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = applicationContext.getBean(PrototypeBean.class);
        prototypeBean1.addCount();
        Assertions.assertEquals(1, prototypeBean1.getCount());

        PrototypeBean prototypeBean2 = applicationContext.getBean(PrototypeBean.class);
        prototypeBean2.addCount();
        Assertions.assertEquals(1, prototypeBean2.getCount());
    }

    @Test
    void singletonClientUsePrototype(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = applicationContext.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertEquals(1, count1);

        ClientBean clientBean2 = applicationContext.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        Assertions.assertEquals(2, count2);
    }
}

둘 중에 상황에 맞는 DL을 사용하면 된다.

 

웹 스코프

지금까지는 싱글톤과 프로토타입 스코프를 학습했다.

 

이번에는 웹 스코프에 대해서도 알아보자.

웹 스코프는 웹 환경에서만 동작한다.

그리고 프로토타입과는 다르게 해당 스코프의 종료시점까지 관리해준다.

 

종류로는

request: HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프, 요청마다 별도의 인스턴스가 생성된다.

session: HTTP Session과 동일한 생명주기를 가지는 스코프

application: ServletContext와 동일한 생명주기를 가지는 스코프

 

바로 request 스코프 예제를 만들어보자.

웹 스코프는 웹 환경에서만 동작하기 때문에 해당 라이브러리를 추가해줘야 한다.

implementation 'org.springframework.boot:spring-boot-starter-web'

해당 라이브러리를 추가하면

http://localhost:8080/ 해당 페이지를 통해 접속할 수 있게 된다.

 

request 요청이 오면 로그를 남기는 예제를 만들어보자.

우선 로그를 출력하기 위한 클래스를 만든다.

package hello.core.web;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
@Scope(value = "request")
public class MyLog {
    private String uuid;
    private String requestURL;
    
    public void setRequestURL(String requestURL){
        this.requestURL = requestURL;
    }
    
    public void log(String message){
        System.out.println("UUID: " + uuid + ", requestURL: " + requestURL + ", message: " + message);
    }
    
    @PostConstruct
    public void init(){
        uuid = UUID.randomUUID().toString();
        System.out.println("UUID: " + uuid + ", request scope bean create: " + this);
    }
    
    @PreDestroy
    public void close(){
        System.out.println("UUID: " + uuid + ", request scope bean close: " + this);
    }
}

Scope를 request로 지정해서, HTTP 요청 당 하나씩 생성이 되고, 요청이 끝나는 시점에 소멸이 된다.

 

이제 Controller를 만든다.

package hello.core.web;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequiredArgsConstructor
public class LogController {

    private final LogService logService;
    private final ObjectProvider<MyLog> myLogObjectProvider;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURL().toString();
        MyLog myLog = myLogObjectProvider.getObject();
        myLog.setRequestURL(requestURL);

        myLog.log("controller");
        logService.logic("testId");

        return "OK";
    }
}

request를 받아야 URL을 알 수 있기에 이 단계에서 URL을 넣어준다.

 

package hello.core.web;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogService {

    private final ObjectProvider<MyLog> myLogObjectProvider;

    public void logic(String id){
        MyLog myLog = myLogObjectProvider.getObject();
        myLog.log("service id = " + id);
    }
}

여기에서 ObjectProvider로 MyLog를 두개 생성하지만, 같은 HTTP 요청이기 때문에 동일한 빈이 들어간다.

ObjectProvider 덕분에 Object.getObject() 호출 전까지, request scope빈의 생성을 지연할 수 있다.

 

하지만 여기에서 더 나아가

 

스코프와 프록시

프록시 방식을 사용한다.

프록시 방식은

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLog{
}

로 proxyMode를 추가하는게 핵심이다.

적용 대상이 class면 TARGET_CLASS, 적용 대상이 인터페이스면 INTERFACES를 선택한다.

이렇게 하면 MyLog의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 주입해 둘 수 있다.

 

 

이렇게 myLog를 출력해보면, 이 객체를 상속받은 다른 객체가 출력되는 것을 볼 수 있다.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 14일차  (0) 2023.03.26
스프링 13일차  (0) 2023.03.25
스프링 11일차  (0) 2023.02.12
스프링 10일차  (0) 2023.02.11
스프링 9일차  (0) 2023.02.10
728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

빈의 생명주기

객체의 초기화 작업과 종료 작업이 어떻게 진행되는지 알아둘 필요가 있다.

예제 코드로

package hello.core.lifeCycle;

public class LifeCycleClient {

    private String print;

    public LifeCycleClient(){
        System.out.println("생성자 호출, print = " + print);
        beanStart();
        call("생성자!");
    }

    public void setPrint(String print){
        this.print = print;
    }

    //빈 시작시 호출
    public void beanStart(){
        System.out.println("beanStart: " + print);
    }

    public void call(String message){
        System.out.println("call: " + print + " message = " + message);
    }

    //빈 종료시 호출
    public void beanClose(){
        System.out.println("close: " + print);
    }
}

이런 class를 만들어보자.

우리는 이 class로 빈의 생명주기에 대해 알아볼 것이다.

package hello.core.lifecycle;

import hello.core.lifeCycle.LifeCycleClient;
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 applicationContext = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        LifeCycleClient client = applicationContext.getBean(LifeCycleClient.class);
        applicationContext.close();
    }

    @Configuration
    static class LifeCycleConfig{

        @Bean//(initMethod = "init", destroyMethod = "close")
        public LifeCycleClient lifeCycleClient(){
            LifeCycleClient networkClient = new LifeCycleClient();
            networkClient.setPrint("hello!");
            return networkClient;
        }
    }
}

그리고 테스트코드로 해당 빈을 열고 닫아보자.

실행해 보면

이렇게 print에 값이 들어가 있지 않은 것을 볼 수 있다.

생성자를 통해 생성하는 단계에는 print에 대한 값이 들어가있지 않고, 모두 생성이 된 후에 setter를 통해 값이 들어가기 때문이다.

스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 준비가 된다.

그렇기에 스프링은 의존관계 주입이 완료되면 스프링 빈에게 초기화 시점을 알려주는 기능을 제공한다. 당연히 스프링 컨테이너가 종료되기 직전에 소멸을 알려주는 기능도 제공한다.

 

스프링 빈의 LifeCycle

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

의 주기를 가진다.

 

물론 생성자에서 로직들을 추가하여 초기화를 할 수도 있지만, 유지보수를 위해 객체를 생성하는 부분과 초기화하는 부분을 분리는 것이 좋다.

 

생명주기 콜백

스프링은 3가지 방법으로 빈 생명주기 콜백을 지원한다.

InitializingBean, DisposableBean 인터페이스

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

@PostConstruct, @PreDestroy annotation

 

  • InitializingBean, DisposableBean 인터페이스

InitializingBean, DisposableBean 인터페이스를 implements 하고 해당 인터페이스들에 있는 메서드들을 afterPropertiesSet(), destroy() 메서드들을 구현하는것이다.

package hello.core.lifeCycle;

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

public class LifeCycleClient implements InitializingBean, DisposableBean {
    private String print;

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

    public void setPrint(String print) {
        this.print = print;
    }

    //서비스 시작시 호출
    public void beanStart() {
        System.out.println("LifeCycleClient.beanStart, print = " + print);
    }

    public void call(String message) {
        System.out.println("call: " + print + " message = " + message);
    }

    //서비스 종료시 호출
    public void beanClose() {
        System.out.println("LifeCycleClient.disConnect, print = " + print);
    }


    public void afterPropertiesSet() throws Exception{
        System.out.println("NetworkClient.init");
        beanStart();
        call("초기화 연결 메시지");
    }

    public void destroy() throws Exception{
        System.out.println("NetworkClient.close");
        beanClose();
    }
}

이렇게 해당 메서드들을 구현하고 테스트를 진행해보자.

이렇게 생성자가 먼저 호출이 되고, 그 후에 초기화 콜백 메서드가 실행이 된다.

빈의 로직들이 실행이 되고, 소멸 전 콜백 실행 해 소멸이 되는 것을 볼 수 있다.

이 방법은 메서드의 이름을 변경할 수 없고 내가 코드를 고칠 수 없는 외부 라이브러리에는 적용이 불가능하여 스프링 초기에 사용하던 방법이고 최근에는 사용하지 않는다고 한다.

 

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

설정 정보에 @Bean(initMethod = "init", destroyMethod = "close")를 입력해 메서드들을 지정할 수 있다.

바로 해보도록 하자, 우선 LifeCycleClient를 좀 수정하고

package hello.core.lifeCycle;

public class LifeCycleClient{
    private String print;

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

    public void setPrint(String print) {
        this.print = print;
    }

    //서비스 시작시 호출
    public void beanStart() {
        System.out.println("LifeCycleClient.beanStart, print = " + print);
    }

    public void call(String message) {
        System.out.println("call: " + print + " message = " + message);
    }

    //서비스 종료시 호출
    public void beanClose() {
        System.out.println("LifeCycleClient.disConnect, print = " + print);
    }

    public void init(){
        System.out.println("LifeCycleClient.init");
        beanStart();
        call("init!");
    }
    
    public void close(){
        System.out.println("LifeCycleClient.close");
        beanClose();
    }
}

@Bean에 initMethod와 DestroyMethod를 추가한다.

package hello.core.lifeCycle;

import hello.core.lifeCycle.LifeCycleClient;
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 applicationContext = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        LifeCycleClient client = applicationContext.getBean(LifeCycleClient.class);
        applicationContext.close();
    }

    @Configuration
    static class LifeCycleConfig{

        @Bean(initMethod = "init", destroyMethod = "close")
        public LifeCycleClient lifeCycleClient(){
            LifeCycleClient networkClient = new LifeCycleClient();
            networkClient.setPrint("hello!");
            return networkClient;
        }
    }
}

그러면

이렇게 생명주기에 맞게 실행이 되는 것을 볼 수 있다.

@DestroyMethod에 추록기능이 있어 close, shutdown을 찾아가지만 그래도 지정해주도록 하자.

 

  • @PostConstruct, @PreDestroy annotation

가장 많이 사용하는 방법이라고 한다, 하지만 외부 라이브러리에는 적용하지 못하기 때문에 외부 라이브러리에 사용하려면 위의 방법을 사용해야 한다고 한다.

예상했던대로

package hello.core.lifeCycle;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

public class LifeCycleClient{
    private String print;

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

    public void setPrint(String print) {
        this.print = print;
    }

    //서비스 시작시 호출
    public void beanStart() {
        System.out.println("LifeCycleClient.beanStart, print = " + print);
    }

    public void call(String message) {
        System.out.println("call: " + print + " message = " + message);
    }

    //서비스 종료시 호출
    public void beanClose() {
        System.out.println("LifeCycleClient.disConnect, print = " + print);
    }

    @PostConstruct
    public void init(){
        System.out.println("LifeCycleClient.init");
        beanStart();
        call("init!");
    }

    @PreDestroy
    public void close(){
        System.out.println("LifeCycleClient.close");
        beanClose();
    }
}

그냥 이렇게 @PostConstruct, @PreDestroy annotation을 달아주는 방법이다.

 

'백엔드 > 스프링' 카테고리의 다른 글

스프링 13일차  (0) 2023.03.25
스프링 12일차  (0) 2023.02.15
스프링 10일차  (0) 2023.02.11
스프링 9일차  (0) 2023.02.10
스프링 8일차  (0) 2023.02.07
728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

다양한 의존관계 주입 방법

의존관계 주입은 4가지 방법이 있다.

  • 생성자 주입
  • 수정자 주입
  • 필드 주입
  • 일반 메서드 주입

 

  • 생성자 주입

말 그대로 생성자를 통해서 의존 관계를 주입받는다.

이제까지 우리가 사용했던 방법이며, 생성자가 호출되는 시점에서 딱 1번만 호출된다.

불변, 필수 의존관계에 사용이 된다.

 

생성자에 @Autowired를 달아서 사용하며, 생성자가 하나만 있으면 생략이 가능하다.

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(CustomerRepository customerRepository, DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }
}

 

  • 수정자 주입(setter 주입)

setter를 만들고 그 메서드를 통해서 의존관계를 주입하는 방법이다.

선택, 변경 가능성이 있는 의존관계에 사용한다.

@Component
public class OrderServiceImpl implements OrderService{
	private CustomerRepository customerRepository;
    private DiscountPolicy discountPolicy;
    
    @Autowired
    public void setCustomerRepository(CustomerRepository customerRepository){
    	this.customerRepository = customerRepository;
    }
    
    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy){
    	this.discountPolicy = discountPolicy;
	}
}

@Autowired는 주입할 대상이 없으면 오류가 발생한다.

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

 

  • 필드주입

이름 그대로 필드에 주입하는 방법이다.

코드는 간단하지만, 사용하지 말자.

외부에서 변경이 불가능해 테스트가 힘들고, DI 프레임워크가 없으면 아무것도 할 수 없다.

@Component
public class OrderServiceImpl implements OrderService{
	@Autowired
    private CustomerRepository customerRepository;
    
    @Autowired
    private DiscountPolicy discountPolicy;
}

 

  • 일반 메서드 주입

당연히 일반 메서드를 통해서도 주입 받을 수 있다.

하지만 일반적으로 잘 사용하지는 않는다.

@Component
public calss OrderServiceImpl implements OrderService{

    private CustomerRepository customerRepository;
    private DiscountPolicy discountPolicy;
    
    @Autowired
    public void init(CustomerRepository customerRepository, DiscountPolicy discountPolicy){
    	this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }
}

 

가끔은 주입할 스프링 빈이 없어도 동작을 해야한다.

그럴 때는 위에서 말한 방법처럼 @Autowired(required = false)를 사용할 수 있다.

이외에도 방법들이 더 있는데

package hello.core.autowired;

import hello.core.customer.Customer;
import jakarta.annotation.Nullable;
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 java.util.Optional;

public class AutowiredTest {

    @Test
    void AutowiredOption(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(TestBean.class);
    }


    static class TestBean{
        @Autowired(required = false)
        public void setNoBean1(Customer noBean1){
            System.out.println("noBean1 = " + noBean1);
        }

        @Autowired
        public void setNoBean2(@Nullable Customer noBean2){
            System.out.println("noBean2 = " + noBean2);
        }

        @Autowired
        public void setNoBean3(Optional<Customer> noBean3){
            System.out.println("noBean3 = " + noBean3);
        }
    }
}

@Autowired(required = false): 자동 주입할 대상이 없으면 메서드가 호출이 되지 않는다.

@Nullable: 자동 주입할 대상이 없으면 null이 입력된다.

Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력된다.

 

이렇게 주입에는 많은 방법들이 있지만, 생성자 주입이 가장 좋다고 한다.

대부분의 의존관계들은 변경할 필요가 없으며, final로 사용할 수 있고, setter로 의도하지 않은 수정이 일어나지 않기 때문에 가장 좋다.

 

Lombok

우리는 의존관계를 생성자를 통해 주입하기로 하였다.

그러면 이 방법을 더 간단하게 할 수 없을까?

그 방법이 Lombok이다.

기존에 사용하던 OrderServiceImpl이다.

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(CustomerRepository customerRepository, DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }
}

생성자가 하나이기 때문에 @Autowired를 생략 할 수 있다.

 

그리고 Lombok을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    private final DiscountPolicy discountPolicy;
}

이렇게만 해주어도 자동으로 생성자를 만들고 의존관계를 주입해준다.

Lombok 적용하는 방법은 소개하지 않도록 하겠다.

 

조회 빈이 2개 이상

@Autowired는 타입에 맞는 의존관계를 넣어준다.

만약 같은 타입이 2가지 이상이라면 어떻게 될까?

지금 DiscountPolicy에서 FixDiscountPolicy, RateDiscountPolicy가 있는 것 처럼 말이다.

 

일단 둘 다 @Component annotation을 달아주자.

그러고 테스트에 작성한 basicScan을 실행해보면

이런 오류가 발생하게 된다.

 

해결방법으로는

@Autowired 필드 명 매칭

@Qualifier

@Primary

가 있다.

 

  • Autowired 필드 명 매칭
package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(CustomerRepository customerRepository, DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }
}

기존에 이렇게 작성된 코드에서 

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    private final DiscountPolicy rateDiscountPolicy;

    public OrderServiceImpl(CustomerRepository customerRepository, DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.rateDiscountPolicy = discountPolicy;
    }
}

로 필드 명을 rateDiscountPolicy(빈 이름)으로 변경한 것이다.

이러면 우리가 원하는 rateDiscountPolicy가 주입이 된다.

 

  • Qualifier

@Qualifier이라는 추가 구분자를 붙여주는 방법이다.

빈 이름을 변경하는 것은 아니고, 단순히 비교에만 사용한다.

 

이렇게 빈 등록할 때 @Qualifier를 붙여준다.

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

 

 

이렇게 붙여주고 생성자에서

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(CustomerRepository customerRepository, @Qualifier("rateDiscountPolicy") DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }
}

@Qualifier를 추가해준다.

 

  • @Primary

@Primary로 우선 순위를 정하는 방법이다.

@Autowired시에 여러개의 빈이 충돌하면 @Primary가 우선권을 가진다.

 

주입할 빈에 가서

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy{}

@Primary를 추가해준다.

 

우선순위로는 @Qualifier가 @Primary보다 더 높다.

 

Annotation 직접 만들기

가끔씩 annotation을 직접 만들어서 사용하기도 한다고 한다.

package hello.core.annotation;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Indexed;

import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("myDiscountPolicy")
public @interface MyDiscountPolicy {
}

이렇게 @Component들의 요소를 끌어오고 우리가 사용할 @Qualifier도 달아준다.

@Component
@rateDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy{}

그러고 이렇게 달아주면 우리가 원하는 annotation을 달아줄 수 있다.

 

생성자에도 이렇게 annotation을 추가해주면 된다.

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.annotation.MyDiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(CustomerRepository customerRepository, @MyDiscountPolicy DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }
}

annotation에는 상속이라는 개념이 없지만, 여러 annotation을 모아서 사용하는 기능을 spring에서 제공해준다.

하지만 spring에서 제공하는 기능이라고 해도 너무 목적없이 막 사용하지는 말자.

 

다양한 빈들이 필요할 때 List, Map

다양한 빈들 중 필요한 것을 찾아서 사용할 때가 있다.

package hello.core.autowired;

import hello.core.AutoAppConfig;
import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.Grade;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

public class AllBeanTest {

    @Test
    void findAllBean(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
        DiscountService discountService = applicationContext.getBean(DiscountService.class);
        Customer customer = new Customer(1L, "A", Grade.VIP);

        int discountPrice = discountService.discount(customer, 10000, "rateDiscountPolicy");

        Assertions.assertEquals(500, discountPrice);
    }

    static class DiscountService{
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policyList;

        public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policyList) {
            this.policyMap = policyMap;
            this.policyList = policyList;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policies = "+ policyList);
        }

        public int discount(Customer customer, int price, String discountCode){
            DiscountPolicy discountPolicy = policyMap.get(discountCode);

            System.out.println("discountCode = " + discountCode);
            System.out.println("discountPolicy = " + discountPolicy);

            return discountPolicy.discount(customer, price);
        }
    }
}

이 코드를 보면 discount에 관련된 빈들을 Map에 저장을 해서 검색을 하며 사용하고 있다.

 

'백엔드 > 스프링' 카테고리의 다른 글

스프링 12일차  (0) 2023.02.15
스프링 11일차  (0) 2023.02.12
스프링 9일차  (0) 2023.02.10
스프링 8일차  (0) 2023.02.07
스프링 7일차  (0) 2023.02.07

+ Recent posts