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

저번에 배웠던 CRUD를 사용해서 간단한 프로그램을 구현해보자.

 

package UserProgram;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

public class EMF {

    private static EntityManagerFactory entityManagerFactory;

    public static void init(){
        entityManagerFactory = Persistence.createEntityManagerFactory("jpabegin");
    }

    public static EntityManager getEntityManager() {
        return entityManagerFactory.createEntityManager();
    }

    public static void close(){
        entityManagerFactory.close();
    }
}

어차피 EntityManagerFactory는 계속 사용을 해야하니, 초기화하고 EntityManagerFactory에서 EntityManager를 가져올 수 있는 class를 작성해준다.

 

당연히 간편하게 사용하기 위해 만드는 것으로 굳이 안 만들어도 되기는 한다.

 

그 다음에 DB에서 사용할 User 객체를 만들어준다.

저번에 사용했던 그대로 사용할 예정이다.

package UserProgram;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import java.time.LocalDateTime;

@Entity
@Table(name = "user")
public class User {

    @Id
    private String email;
    private String name;
    private LocalDateTime create_date;

    protected User(){}

    public User(String email, String name, LocalDateTime create_date) {
        this.email = email;
        this.name = name;
        this.create_date = create_date;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public LocalDateTime getCreate_data() {
        return create_date;
    }

    public void setCreate_data(LocalDateTime create_data) {
        this.create_date = create_data;
    }

    @Override
    public String toString() {
        return "User{" +
                "email='" + email + '\'' +
                ", name='" + name + '\'' +
                ", create_data=" + create_date +
                '}';
    }
}

이제 본격적으로 JPA를 사용할 차례이다.

Service 하는 메서드들을 한 곳에 모아 클래스로 작성했다.

 

package UserProgram;

import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityTransaction;

public class UserService {

    public void CreateUser(User user){
        EntityManager entityManager = EMF.getEntityManager();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        try{
            entityTransaction.begin();
            entityManager.persist(user);
            entityTransaction.commit();
        }catch(Exception exception){
            entityTransaction.rollback();
            throw exception;
        } finally {
            entityManager.close();
        }
    }

    public User ReadUser(String email){
        EntityManager entityManager = EMF.getEntityManager();
        try{
            User user = entityManager.find(User.class, email);
            if(user == null) throw new NullPointerException();
            else return user;
        }finally{
            entityManager.close();
        }
    }

    public void UpdateUser(String email, String name){
        EntityManager entityManager = EMF.getEntityManager();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        try{
            entityTransaction.begin();
            User user = entityManager.find(User.class, email);
            if(user == null) throw new NullPointerException();
            user.setName(name);
            entityTransaction.commit();
        }catch (Exception exception){
            entityTransaction.rollback();
            throw exception;
        }finally {
            entityManager.close();
        }
    }

    public void DeleteUser(String email){
        EntityManager entityManager = EMF.getEntityManager();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        try{
            entityTransaction.begin();
            User user = entityManager.find(User.class, email);
            if (user == null) throw new NullPointerException();
            entityManager.remove(user);
            entityTransaction.commit();
        }catch(Exception exception){
            entityTransaction.rollback();
            throw exception;
        }finally {
            entityManager.close();
        }
    }
}

우선 저번에 배운 CRUD를 사용하였고, 다른 점이 있다면 static으로 작성된 EntityManagerFactory를 사용하였다.

우선 예외는 대충 넘기고 나중에 추가로 작성하도록 하겠다.

package UserProgram;

import java.time.LocalDateTime;
import java.util.Scanner;

public class UserProgram {

    private static UserService userService = new UserService();

    private static String email;
    private static String name;

    public static void main(String[] args) {
        EMF.init();

        Scanner scanner = new Scanner(System.in);

        boolean flag = true;

        while(flag){
            System.out.print("1. Create, 2. Read, 3. Update, 4. Delete, 5. exit : ");
            int answer = scanner.nextInt();
            switch(answer){
                case 1:
                    CreateMethod();
                    break;
                case 2:
                    ReadMethod();
                    break;
                case 3:
                    UpdateMethod();
                    break;
                case 4:
                    DeleteMethod();
                    break;
                case 5:
                    flag = false;
                    break;
            }
        }

        EMF.close();
        scanner.close();
    }

    private static void inputEmail(){
        Scanner scanner = new Scanner(System.in);
        System.out.print("input email:");
        email = scanner.nextLine();
    }

    private static void inputName(){
        Scanner scanner = new Scanner(System.in);
        System.out.print("input name:");
        name = scanner.nextLine();
    }

    private static void CreateMethod(){
        inputEmail();
        inputName();
        User user = new User(email, name, LocalDateTime.now());
        try{
            userService.CreateUser(user);
        }catch(Exception exception){
            System.out.println("createError");
        }
    }

    private static void ReadMethod(){
        inputEmail();
        try{
            User user = userService.ReadUser(email);
            System.out.println(user);
        }catch(Exception exception){
            System.out.println("readError");
        }
    }

    private static void UpdateMethod(){
        inputEmail();
        inputName();
        try{
            userService.UpdateUser(email, name);
            System.out.println(userService.ReadUser(email));
        }catch(Exception exception){
            System.out.println("updateError");
        }
    }

    private static void DeleteMethod(){
        inputEmail();
        try{
            userService.DeleteUser(email);
        }catch(Exception exception){
            System.out.println("deleteError");
        }
    }
}

이렇게 프로그램을 작성하였다.

그러면 이렇게 프로그램이 잘 작동한다.

 

값을 추가하고 삭제하는 메서드들도 잘 작동이 된다.

'백엔드 > JPA' 카테고리의 다른 글

JPA 6장 (@Embeddable)  (0) 2023.03.17
JPA 5장 (Entity 식별자 생성 방식)  (0) 2023.03.16
JPA 4장 (Entity에 대하여)  (0) 2023.03.16
JPA 2장 (영속 컨텍스트)  (0) 2023.03.15
JPA 1장 (우선 시작해보기)  (0) 2023.03.15
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
728x90

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

 

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

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

www.inflearn.com

 

지금까지는 스프링 빈을 등록할 때 자바의 @Bean을 사용하였다.

등록할 스프링 빈의 수가 많아 질수록 등록하는 데에 걸리는 시간일 길어진다.

그렇기 때문에 스프링은 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.

의존관계도 자동으로 주입해주는 @Autowired도 제공을 해준다.

 

기존에 만들었던 AppConfig.java에 추가로 AutoAppConfig.java를 만들자.

package hello.core;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
    
}

기존 AppConfig는

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public CustomerService customerService(){
        System.out.println("call AppConfig.customerService");
        return new CustomerServiceImpl(customerRepository());
    }


    @Bean
    public OrderService orderService(){
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(customerRepository(), discountPolicy());
    }

    @Bean
    public CustomerRepository customerRepository(){
        System.out.println("call AppConfig.customerRepository");
        return new MemoryCustomerRepository();
    }

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

안에 @Bean을 나열한 것에 비해 코드가 상당히 짧을 것을 볼 수 있다.

 

이제 @ComponentScan에서 자동으로 읽을 대상이 될 수 있도록 @Component annotation을 붙여주자.

MemoryCustomerRepository

package hello.core.customer;

import org.springframework.stereotype.Component;

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

@Component
public class MemoryCustomerRepository implements CustomerRepository{

    private static Map<Long, Customer> store = new HashMap<>();

    @Override
    public void save(Customer customer) {
        store.put(customer.getId(), customer);
    }

    @Override
    public Customer findById(Long id) {
        return store.get(id);
    }
}

 

RateDiscountPolicy

package hello.core.Discount;

import hello.core.customer.Customer;
import hello.core.customer.Grade;
import org.springframework.stereotype.Component;

@Component
public class RateDiscountPolicy implements DiscountPolicy{

    private int VIPDiscountPercent = 5; // 할인되는 비율
    private int VVIPDiscountPercent = 10; //할인되는 비율

    @Override
    public int discount(Customer customer, int price) {
        if(customer.getGrade() == Grade.VIP){
            return price * VIPDiscountPercent / 100;
        }
        else if (customer.getGrade() == Grade.VVIP){
            return price * VVIPDiscountPercent / 100;
        }
        else{
            return 0;
        }
    }
}

 

CustomerServiceImpl

package hello.core.customer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CustomerServiceImpl implements CustomerService{

    private final CustomerRepository customerRepository;

    @Autowired
    public CustomerServiceImpl(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    @Override
    public void join(Customer customer) {
        customerRepository.save(customer);
    }

    @Override
    public Customer findCustomer(Long id) {
        return customerRepository.findById(id);
    }

    public CustomerRepository customerRepository(){
        return customerRepository;
    }
}

 

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 = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy;

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

    @Override
    public Order createOrder(Long id, String itemName, int itemPrice) {
        Customer customer = customerRepository.findById(id);
        int discountPrice = discountPolicy.discount(customer, itemPrice);

        return new Order(id, itemName, itemPrice, discountPrice);
    }

    public CustomerRepository customerRepository(){
        return customerRepository;
    }
}

생성자에 @Autowired를 사용하면 의존관계를 주입받을 수 있다.

 

이렇게 고치고 AutoAppConfigTest.java를 만들어보자.

package hello.core.scan;

import hello.core.AutoAppConfig;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AutoAppConfigTest {

    @Test
    void basicScan(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        CustomerService customerService = applicationContext.getBean(CustomerServiceImpl.class);
        Assertions.assertInstanceOf(CustomerService.class, customerService);
    }
}

테스트를 해보면 AppConfig와 동일하게 작동하는 것을 볼 수 있다.

 

ComponentScan과 Autowired의 기본 전략

@ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.

그 때 빈 이름은 맨 앞글자만 소문자로 바꾼 후 사용한다.

바꿀 수는 있지만 보통은 그냥 사용한다.

 

@Autowired를 사용하면 의존관계를 자동으로 주입해주는데, 기본적으로 타입이 같은 빈을 찾아서 주입하게 된다.

 

탐색 위치와 기본 스캔 대상

모든 코드들을 찾으면서 @Component를 찾다보면 시간이 오래 걸릴 것이다.

그래서 탐색 범위를 지정해 줄 수 있다.

 

basePackages: 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 해당 패키지의 하위 패키지까지 모두 탐색한다.

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

 

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

 

요즘은 이 설정 정보를 프로젝트 최상단에 올린 후 스캔 하는 것이 권장된다고 한다.

스프링의 시작 정보인 @SpringBootApplication 안에도 @ComponentScan이 들어있어 프로젝트 시작 루트 위치에 두는 것이 관례이다.

 

컴포넌트 스캔의 대상에는

@Component 뿐만 아니라 다음과 내용도 추가로 대상에 포함된다.

  • @Controller: 스프링 MVC 컨트롤러에서 사용, 스프링 MVC 컨트롤러로 인식.
  • @Service: 스프링 비즈니스 로직에서 사용, 특별한 처리는 하지 않는다.
  • @Repository: 스프링 데이터 접근 계층에서 사용, 스프링 데이터 접근 계층으로 인식.
  • @Configuration: 스프링 설정 정보에서 사용, 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.

해당 annotation들을 보면 @Component를 포함하고 있다.

 

필터

필터를 사용해서 스캔 대상을 추가로 지정하거나 스캔 대상에서 재외 할 수 있다.

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

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

 

예제 Component들을 만들고 실습해보록 하자.

우선 annotation들을 만들고

package hello.core.scan.filter;

import java.lang.annotation.*;

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

}
package hello.core.scan.filter;

import java.lang.annotation.*;

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

우리가 스캔할 class들에 달아준다.

package hello.core.scan.filter;

@TmpIncludeComponent
public class MyBeanA {

}
package hello.core.scan.filter;

@TmpExcludeComponent
public class MyBeanB {

}

이 class들을 스캔해보자

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;

public class ComponentFilterAppConfigTest {

    @Configuration
    @ComponentScan(
            includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = TmpIncludeComponent.class),
            excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = TmpExcludeComponent.class)
    )
    static class ComponentFilterAppConfig{

    }

    @Test
    void filterScan() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);

        MyBeanA myBeanA = applicationContext.getBean("myBeanA", MyBeanA.class);
        Assertions.assertNotNull(myBeanA);

        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean("myBeanB", MyBeanB.class));
    }
}

이렇게 테스트를 해보면 includeFilter에 들어간 Component들만 스캔이 되고 exclydeFilter에 들어간 Component는 스캔이 되지 않는 것을 볼 수 있다.

 

중복 등록과 충돌

스프링에서 이름이 같은 경우에 충돌하는 경우가 생긴다.

 

1. 자동 빈 등록 VS 자동 빈 등록

이 경우에는

이렇게 ConflictionBeanDefinitionException이 발생하게 된다.

그럴 때에는 이름을 바꿔주도록 하자.

 

2. 수동 빈 등록 VS 자동 빈 등록

이 경우에는 수동 빈이 자동 빈을 오버라이딩 해버린다.

 

그리고 그런 경우에는 이런 로그가 남는다.

하지만 이런 경우에 의도하지 않는 버그가 만들어 질 수도 있으니, 피하도록 하고 최근에는 스프링 부트에서 오류가 발생하도록 해주고 있다.

최대한 명확하게 사용하도록 하지.

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

스프링 11일차  (0) 2023.02.12
스프링 10일차  (0) 2023.02.11
스프링 8일차  (0) 2023.02.07
스프링 7일차  (0) 2023.02.07
스프링 6일차  (0) 2023.02.06

+ Recent posts